chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
const keybinds: Record<string, string> = {
|
||||
"file.attach": "mod+u",
|
||||
"prompt.mode.shell": "mod+shift+x",
|
||||
"prompt.mode.normal": "mod+shift+e",
|
||||
"permissions.autoaccept": "mod+shift+a",
|
||||
"agent.cycle": "mod+.",
|
||||
"model.choose": "mod+m",
|
||||
"model.variant.cycle": "mod+shift+m",
|
||||
}
|
||||
|
||||
export function useCommand() {
|
||||
return {
|
||||
options: [],
|
||||
register() {
|
||||
return () => undefined
|
||||
},
|
||||
trigger() {},
|
||||
keybind(id: string) {
|
||||
return keybinds[id]
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
type Comment = {
|
||||
id: string
|
||||
file: string
|
||||
selection: { start: number; end: number }
|
||||
comment: string
|
||||
time: number
|
||||
}
|
||||
|
||||
const [list, setList] = createSignal<Comment[]>([])
|
||||
const [focus, setFocus] = createSignal<{ file: string; id: string } | null>(null)
|
||||
const [active, setActive] = createSignal<{ file: string; id: string } | null>(null)
|
||||
|
||||
export function useComments() {
|
||||
return {
|
||||
all: list,
|
||||
replace(next: Comment[]) {
|
||||
setList(next)
|
||||
},
|
||||
remove(file: string, id: string) {
|
||||
setList((current) => current.filter((item) => !(item.file === file && item.id === id)))
|
||||
},
|
||||
clear() {
|
||||
setList([])
|
||||
setFocus(null)
|
||||
setActive(null)
|
||||
},
|
||||
focus,
|
||||
setFocus,
|
||||
active,
|
||||
setActive,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export type FileSelection = {
|
||||
startLine: number
|
||||
startChar: number
|
||||
endLine: number
|
||||
endChar: number
|
||||
}
|
||||
|
||||
export type SelectedLineRange = {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export function selectionFromLines(selection?: SelectedLineRange): FileSelection | undefined {
|
||||
if (!selection) return undefined
|
||||
return {
|
||||
startLine: selection.start,
|
||||
startChar: 0,
|
||||
endLine: selection.end,
|
||||
endChar: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const pool = [
|
||||
"src/session/timeline.tsx",
|
||||
"src/session/composer.tsx",
|
||||
"src/components/prompt-input.tsx",
|
||||
"src/components/session-todo-dock.tsx",
|
||||
"README.md",
|
||||
]
|
||||
|
||||
export function useFile() {
|
||||
return {
|
||||
tab(path: string) {
|
||||
return `file:${path}`
|
||||
},
|
||||
pathFromTab(tab: string) {
|
||||
if (!tab.startsWith("file:")) return ""
|
||||
return tab.slice(5)
|
||||
},
|
||||
load: async () => undefined,
|
||||
async searchFilesAndDirectories(query: string) {
|
||||
const text = query.trim().toLowerCase()
|
||||
if (!text) return pool
|
||||
return pool.filter((path) => path.toLowerCase().includes(text))
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
|
||||
const provider = {
|
||||
all: [
|
||||
{
|
||||
id: "anthropic",
|
||||
models: {
|
||||
"claude-3-7-sonnet": {
|
||||
id: "claude-3-7-sonnet",
|
||||
name: "Claude 3.7 Sonnet",
|
||||
cost: { input: 1, output: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connected: ["anthropic"],
|
||||
default: { anthropic: "claude-3-7-sonnet" },
|
||||
}
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
todo: {} as Record<string, any[]>,
|
||||
provider,
|
||||
session: [] as any[],
|
||||
config: { permission: {} },
|
||||
})
|
||||
|
||||
export function useGlobalSync() {
|
||||
return {
|
||||
data: {
|
||||
provider,
|
||||
session_todo: store.todo,
|
||||
},
|
||||
child() {
|
||||
return [store, setStore] as const
|
||||
},
|
||||
todo: {
|
||||
set(sessionID: string, todos: any[]) {
|
||||
setStore("todo", sessionID, todos)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
const dict: Record<string, string> = {
|
||||
"session.todo.title": "Todos",
|
||||
"session.todo.collapse": "Collapse todos",
|
||||
"session.todo.expand": "Expand todos",
|
||||
"prompt.loading": "Loading prompt...",
|
||||
"prompt.placeholder.normal": "Ask anything...",
|
||||
"prompt.placeholder.simple": "Ask anything...",
|
||||
"prompt.placeholder.shell": "Run a shell command... {{example}}",
|
||||
"prompt.placeholder.summarizeComment": "Summarize this comment",
|
||||
"prompt.placeholder.summarizeComments": "Summarize these comments",
|
||||
"prompt.action.attachFile": "Attach files",
|
||||
"prompt.action.send": "Send",
|
||||
"prompt.action.stop": "Stop",
|
||||
"prompt.attachment.remove": "Remove attachment",
|
||||
"prompt.dropzone.label": "Drop image to attach",
|
||||
"prompt.dropzone.file.label": "Drop file to attach",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"dialog.model.select.title": "Select model",
|
||||
"common.default": "Default",
|
||||
"common.key.esc": "Esc",
|
||||
"command.category.file": "File",
|
||||
"command.category.session": "Session",
|
||||
"command.agent.cycle": "Cycle agent",
|
||||
"command.model.choose": "Choose model",
|
||||
"command.model.variant.cycle": "Cycle model variant",
|
||||
"command.prompt.mode.shell": "Switch to shell mode",
|
||||
"command.prompt.mode.normal": "Switch to prompt mode",
|
||||
"command.permissions.autoaccept.enable": "Enable auto-accept",
|
||||
"command.permissions.autoaccept.disable": "Disable auto-accept",
|
||||
"prompt.example.1": "Refactor this function and keep behavior the same",
|
||||
"prompt.example.2": "Find the root cause of this error",
|
||||
"prompt.example.3": "Write tests for this module",
|
||||
"prompt.example.4": "Explain this diff",
|
||||
"prompt.example.5": "Optimize this query",
|
||||
"prompt.example.6": "Clean up this component",
|
||||
"prompt.example.7": "Summarize the recent changes",
|
||||
"prompt.example.8": "Add accessibility checks",
|
||||
"prompt.example.9": "Review this API design",
|
||||
"prompt.example.10": "Generate migration notes",
|
||||
"prompt.example.11": "Patch this bug",
|
||||
"prompt.example.12": "Make this animation smoother",
|
||||
"prompt.example.13": "Improve error handling",
|
||||
"prompt.example.14": "Document this feature",
|
||||
"prompt.example.15": "Refine these styles",
|
||||
"prompt.example.16": "Check edge cases",
|
||||
"prompt.example.17": "Help me write a commit message",
|
||||
"prompt.example.18": "Reduce re-renders in this component",
|
||||
"prompt.example.19": "Verify keyboard navigation",
|
||||
"prompt.example.20": "Make this copy clearer",
|
||||
"prompt.example.21": "Add telemetry for this flow",
|
||||
"prompt.example.22": "Compare these two implementations",
|
||||
"prompt.example.23": "Create a minimal reproduction",
|
||||
"prompt.example.24": "Suggest naming improvements",
|
||||
"prompt.example.25": "What should we test next?",
|
||||
}
|
||||
|
||||
function render(template: string, params?: Record<string, unknown>) {
|
||||
if (!params) return template
|
||||
return template.replace(/\{\{([^}]+)\}\}/g, (_, key: string) => {
|
||||
const value = params[key.trim()]
|
||||
if (value === undefined || value === null) return ""
|
||||
// oxlint-disable-next-line no-base-to-string -- value is Record<string, unknown>, always coerced intentionally
|
||||
return String(value)
|
||||
})
|
||||
}
|
||||
|
||||
export function useLanguage() {
|
||||
return {
|
||||
locale: () => "en" as const,
|
||||
t(key: string, params?: Record<string, unknown>) {
|
||||
return render(dict[key] ?? key, params)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
const [all, setAll] = createSignal<string[]>([])
|
||||
const [active, setActive] = createSignal<string | undefined>(undefined)
|
||||
const [reviewOpen, setReviewOpen] = createSignal(false)
|
||||
|
||||
const tabs = {
|
||||
all,
|
||||
active,
|
||||
open(tab: string) {
|
||||
setAll((current) => (current.includes(tab) ? current : [...current, tab]))
|
||||
},
|
||||
setActive(tab: string) {
|
||||
if (!all().includes(tab)) {
|
||||
tabs.open(tab)
|
||||
}
|
||||
setActive(tab)
|
||||
},
|
||||
}
|
||||
|
||||
const view = {
|
||||
reviewPanel: {
|
||||
opened: reviewOpen,
|
||||
open() {
|
||||
setReviewOpen(true)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function useLayout() {
|
||||
return {
|
||||
tabs: () => tabs,
|
||||
view: () => view,
|
||||
fileTree: {
|
||||
setTab() {},
|
||||
},
|
||||
handoff: {
|
||||
setTabs() {},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
const model = {
|
||||
id: "claude-3-7-sonnet",
|
||||
name: "Claude 3.7 Sonnet",
|
||||
provider: { id: "anthropic" },
|
||||
variants: { fast: {}, thinking: {} },
|
||||
}
|
||||
|
||||
const agents = [{ name: "build" }, { name: "review" }, { name: "plan" }]
|
||||
|
||||
const [agent, setAgent] = createSignal(agents[0].name)
|
||||
const [variant, setVariant] = createSignal<string | undefined>(undefined)
|
||||
|
||||
export function useLocal() {
|
||||
return {
|
||||
slug: () => "c3Rvcnk=",
|
||||
agent: {
|
||||
list: () => agents,
|
||||
current: () => agents.find((item) => item.name === agent()) ?? agents[0],
|
||||
set(value?: string) {
|
||||
if (!value) {
|
||||
setAgent(agents[0].name)
|
||||
return
|
||||
}
|
||||
const hit = agents.find((item) => item.name === value)
|
||||
setAgent(hit?.name ?? agents[0].name)
|
||||
},
|
||||
},
|
||||
model: {
|
||||
current: () => model,
|
||||
variant: {
|
||||
list: () => Object.keys(model.variants),
|
||||
current: () => variant(),
|
||||
set(next?: string) {
|
||||
setVariant(next)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
const accepted = new Set<string>()
|
||||
|
||||
function key(sessionID: string, directory?: string) {
|
||||
return `${directory ?? ""}:${sessionID}`
|
||||
}
|
||||
|
||||
export function usePermission() {
|
||||
return {
|
||||
autoResponds() {
|
||||
return false
|
||||
},
|
||||
isAutoAccepting(sessionID: string, directory?: string) {
|
||||
return accepted.has(key(sessionID, directory))
|
||||
},
|
||||
toggleAutoAccept(sessionID: string, directory?: string) {
|
||||
const next = key(sessionID, directory)
|
||||
if (accepted.has(next)) {
|
||||
accepted.delete(next)
|
||||
return
|
||||
}
|
||||
accepted.add(next)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Platform } from "../../../../../app/src/context/platform"
|
||||
|
||||
const value: Platform = {
|
||||
platform: "web",
|
||||
openLink() {},
|
||||
restart: async () => {},
|
||||
back() {},
|
||||
forward() {},
|
||||
notify: async () => {},
|
||||
fetch: globalThis.fetch.bind(globalThis),
|
||||
parseMarkdown: async (markdown: string) => markdown,
|
||||
}
|
||||
|
||||
export function usePlatform() {
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
interface PartBase {
|
||||
content: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface TextPart extends PartBase {
|
||||
type: "text"
|
||||
}
|
||||
|
||||
export interface FileAttachmentPart extends PartBase {
|
||||
type: "file"
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface AgentPart extends PartBase {
|
||||
type: "agent"
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ImageAttachmentPart {
|
||||
type: "image"
|
||||
id: string
|
||||
filename: string
|
||||
mime: string
|
||||
dataUrl: string
|
||||
}
|
||||
|
||||
export type ContentPart = TextPart | FileAttachmentPart | AgentPart | ImageAttachmentPart
|
||||
export type Prompt = ContentPart[]
|
||||
|
||||
type ContextItem = {
|
||||
key: string
|
||||
type: "file"
|
||||
path: string
|
||||
selection?: { startLine: number; startChar: number; endLine: number; endChar: number }
|
||||
comment?: string
|
||||
commentID?: string
|
||||
commentOrigin?: "review" | "file"
|
||||
preview?: string
|
||||
}
|
||||
|
||||
export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||
|
||||
function clonePart(part: ContentPart): ContentPart {
|
||||
if (part.type === "image") return { ...part }
|
||||
if (part.type === "agent") return { ...part }
|
||||
if (part.type === "file") return { ...part }
|
||||
return { ...part }
|
||||
}
|
||||
|
||||
function clonePrompt(prompt: Prompt) {
|
||||
return prompt.map(clonePart)
|
||||
}
|
||||
|
||||
export function isPromptEqual(a: Prompt, b: Prompt) {
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((part, i) => JSON.stringify(part) === JSON.stringify(b[i]))
|
||||
}
|
||||
|
||||
let index = 0
|
||||
const [prompt, setPrompt] = createSignal<Prompt>(clonePrompt(DEFAULT_PROMPT))
|
||||
const [cursor, setCursor] = createSignal<number>(0)
|
||||
const [items, setItems] = createSignal<ContextItem[]>([])
|
||||
|
||||
const withKey = (item: Omit<ContextItem, "key"> & { key?: string }): ContextItem => ({
|
||||
...item,
|
||||
key: item.key ?? `ctx:${++index}`,
|
||||
})
|
||||
|
||||
export function usePrompt() {
|
||||
return {
|
||||
ready: () => true,
|
||||
current: prompt,
|
||||
cursor,
|
||||
dirty: () => !isPromptEqual(prompt(), DEFAULT_PROMPT),
|
||||
set(next: Prompt, cursorPosition?: number) {
|
||||
setPrompt(clonePrompt(next))
|
||||
if (cursorPosition !== undefined) setCursor(cursorPosition)
|
||||
},
|
||||
reset() {
|
||||
setPrompt(clonePrompt(DEFAULT_PROMPT))
|
||||
setCursor(0)
|
||||
setItems((current) => current.filter((item) => !!item.comment?.trim()))
|
||||
},
|
||||
context: {
|
||||
items,
|
||||
add(item: Omit<ContextItem, "key"> & { key?: string }) {
|
||||
const next = withKey(item)
|
||||
if (items().some((current) => current.key === next.key)) return
|
||||
setItems((current) => [...current, next])
|
||||
},
|
||||
remove(key: string) {
|
||||
setItems((current) => current.filter((item) => item.key !== key))
|
||||
},
|
||||
removeComment(path: string, commentID: string) {
|
||||
setItems((current) =>
|
||||
current.filter((item) => !(item.type === "file" && item.path === path && item.commentID === commentID)),
|
||||
)
|
||||
},
|
||||
updateComment(path: string, commentID: string, next: Partial<ContextItem>) {
|
||||
setItems((current) =>
|
||||
current.map((item) => {
|
||||
if (item.type !== "file" || item.path !== path || item.commentID !== commentID) return item
|
||||
return withKey({ ...item, ...next })
|
||||
}),
|
||||
)
|
||||
},
|
||||
replaceComments(next: Array<Omit<ContextItem, "key"> & { key?: string }>) {
|
||||
const nonComment = items().filter((item) => !item.comment?.trim())
|
||||
setItems([...nonComment, ...next.map(withKey)])
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
const make = (directory: string) => ({
|
||||
session: {
|
||||
create: async () => ({ data: { id: "story-session" } }),
|
||||
prompt: async () => ({ data: undefined }),
|
||||
shell: async () => ({ data: undefined }),
|
||||
command: async () => ({ data: undefined }),
|
||||
abort: async () => ({ data: undefined }),
|
||||
},
|
||||
worktree: {
|
||||
create: async () => ({ data: { directory: `${directory}/worktree-1` } }),
|
||||
},
|
||||
})
|
||||
|
||||
const root = "/tmp/story"
|
||||
|
||||
export function useSDK() {
|
||||
return {
|
||||
directory: root,
|
||||
url: "http://localhost:4096",
|
||||
client: make(root),
|
||||
createClient(input: { directory: string }) {
|
||||
return make(input.directory)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
|
||||
const [data, setData] = createStore({
|
||||
session: [] as Array<{ id: string; parentID?: string }>,
|
||||
permission: {} as Record<string, Array<{ id: string; sessionID: string; permission: string; patterns: string[] }>>,
|
||||
question: {} as Record<string, Array<{ id: string; questions: unknown[] }>>,
|
||||
session_diff: {} as Record<string, Array<{ file: string }>>,
|
||||
message: {
|
||||
"story-session": [] as Array<{ id: string; role: string }>,
|
||||
} as Record<string, Array<{ id: string; role: string }>>,
|
||||
session_status: {} as Record<string, { type: "idle" | "busy" }>,
|
||||
agent: [{ name: "build", mode: "task", hidden: false }],
|
||||
command: [{ name: "fix", description: "Run fix command", source: "project" }],
|
||||
})
|
||||
|
||||
export function useSync() {
|
||||
return {
|
||||
data,
|
||||
set(...input: unknown[]) {
|
||||
;(setData as (...args: unknown[]) => void)(...input)
|
||||
},
|
||||
session: {
|
||||
get(id: string) {
|
||||
return { id }
|
||||
},
|
||||
optimistic: {
|
||||
add() {},
|
||||
remove() {},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user