/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/app/src/context/prompt-state.ts
266 строк
8 KB
Brendan Allan
feat(session-ui): rewrite v2 prompt input (#37102)
17 июл 2026, 13:02
Не верифицирован
17 июл 2026, 13:02
c0a258b
Код
Авторство
О чём код?
import { checksum } from "@opencode-ai/core/util/encode" import type { FilePartSource } from "@opencode-ai/sdk/v2/client" import { batch, createMemo, type Accessor } from "solid-js" import { createStore, type SetStoreFunction } from "solid-js/store" import type { FileSelection } from "@/context/file" import { Persist, persisted } from "@/utils/persist" import type { ServerScope } from "@/utils/server-scope" interface PartBase { content: string start: number end: number } export interface TextPart extends PartBase { type: "text" } export interface FileAttachmentPart extends PartBase { type: "file" path: string selection?: FileSelection mime?: string filename?: string url?: string source?: FilePartSource } export interface AgentPart extends PartBase { type: "agent" name: string } export interface ImageAttachmentPart { type: "image" id: string filename: string sourcePath?: string mime: string dataUrl: string } export type ContentPart = TextPart | FileAttachmentPart | AgentPart | ImageAttachmentPart export type Prompt = ContentPart[] export type PromptModel = { providerID: string modelID: string variant?: string | null } export type FileContextItem = { type: "file" path: string selection?: FileSelection comment?: string commentID?: string commentOrigin?: "review" | "file" preview?: string } export type ContextItem = FileContextItem export type PromptScope = { draftID: string } | { dir: string; id?: string } export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] export type PromptStore = { prompt: Prompt cursor?: number model?: PromptModel context: { items: (ContextItem & { key: string })[] } } type InitialPrompt = { prompt?: string model?: PromptModel } function isSelectionEqual(a?: FileSelection, b?: FileSelection) { if (!a && !b) return true if (!a || !b) return false return ( a.startLine === b.startLine && a.startChar === b.startChar && a.endLine === b.endLine && a.endChar === b.endChar ) } function isPartEqual(partA: ContentPart, partB: ContentPart) { switch (partA.type) { case "text": return partB.type === "text" && partA.content === partB.content case "file": return ( partB.type === "file" && partA.path === partB.path && partA.mime === partB.mime && partA.filename === partB.filename && isSelectionEqual(partA.selection, partB.selection) ) case "agent": return partB.type === "agent" && partA.name === partB.name case "image": return partB.type === "image" && partA.id === partB.id } } export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean { if (promptA.length !== promptB.length) return false for (let i = 0; i < promptA.length; i++) { if (!isPartEqual(promptA[i], promptB[i])) return false } return true } function cloneSelection(selection?: FileSelection) { if (!selection) return undefined return { ...selection } } function clonePart(part: ContentPart): ContentPart { if (part.type === "text") return { ...part } if (part.type === "image") return { ...part } if (part.type === "agent") return { ...part } return { ...part, selection: cloneSelection(part.selection), } } function clonePrompt(prompt: Prompt): Prompt { return prompt.map(clonePart) } function contextItemKey(item: ContextItem) { if (item.type !== "file") return item.type const start = item.selection?.startLine const end = item.selection?.endLine const key = `${item.type}:${item.path}:${start}:${end}` if (item.commentID) return `${key}:c=${item.commentID}` const comment = item.comment?.trim() if (!comment) return key const digest = checksum(comment) ?? comment return `${key}:c=${digest.slice(0, 8)}` } export function isCommentItem(item: ContextItem | (ContextItem & { key: string })) { return item.type === "file" && !!item.comment?.trim() } function createPromptActions(setStore: SetStoreFunction<PromptStore>) { return { set(prompt: Prompt, cursorPosition?: number) { const next = clonePrompt(prompt) batch(() => { setStore("prompt", next) if (cursorPosition !== undefined) setStore("cursor", cursorPosition) }) }, reset() { batch(() => { setStore("prompt", clonePrompt(DEFAULT_PROMPT)) setStore("cursor", 0) }) }, } } function promptTarget(serverScope: ServerScope, scope: PromptScope) { if ("draftID" in scope) return Persist.draft(scope.draftID, "prompt") const legacy = `${scope.dir}/prompt${scope.id ? "/" + scope.id : ""}.v2` return Persist.serverScoped(serverScope, scope.dir, scope.id, "prompt", [legacy]) } function promptStore(initial?: InitialPrompt): PromptStore { const text = initial?.prompt return { prompt: text === undefined ? clonePrompt(DEFAULT_PROMPT) : [{ type: "text", content: text, start: 0, end: text.length }], cursor: text === undefined ? undefined : text.length, model: initial?.model ? { ...initial.model } : undefined, context: { items: [], }, } } function createPromptStateValue(store: PromptStore, setStore: SetStoreFunction<PromptStore>) { const actions = createPromptActions(setStore) const value = { store: [() => store, setStore] as [Accessor<PromptStore>, SetStoreFunction<PromptStore>], current: () => store.prompt, cursor: createMemo(() => store.cursor), dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT), model: { current: () => store.model, set: (model: PromptModel | undefined) => setStore("model", model), }, context: { items: createMemo(() => store.context.items), add(item: ContextItem) { const key = contextItemKey(item) if (store.context.items.find((x) => x.key === key)) return setStore("context", "items", (items) => [...items, { key, ...item }]) }, remove(key: string) { setStore("context", "items", (items) => items.filter((x) => x.key !== key)) }, removeComment(path: string, commentID: string) { setStore("context", "items", (items) => items.filter((item) => !(item.type === "file" && item.path === path && item.commentID === commentID)), ) }, updateComment(path: string, commentID: string, next: Partial<FileContextItem> & { comment?: string }) { setStore("context", "items", (items) => items.map((item) => { if (item.type !== "file" || item.path !== path || item.commentID !== commentID) return item const value = { ...item, ...next } return { ...value, key: contextItemKey(value) } }), ) }, replaceComments(items: FileContextItem[]) { setStore("context", "items", (current) => [ ...current.filter((item) => !isCommentItem(item)), ...items.map((item) => ({ ...item, key: contextItemKey(item) })), ]) }, }, set: actions.set, reset: actions.reset, capture: () => value, } return value } function createPersistedPrompt(target: ReturnType<typeof promptTarget>, initial?: InitialPrompt) { const [store, setStore, _, ready] = persisted(target, createStore<PromptStore>(promptStore(initial))) return { ready, ...createPromptStateValue(store, setStore) } } export function createPromptSession(serverScope: ServerScope, scope: PromptScope, initial?: InitialPrompt) { return createPersistedPrompt(promptTarget(serverScope, scope), initial) } export function createDraftPromptSession(draftID: string, initial?: InitialPrompt) { return createPersistedPrompt(Persist.draft(draftID, "prompt"), initial) } export type PromptSession = ReturnType<typeof createPromptSession> export function createPromptReady(session: Accessor<PromptSession>) { return Object.defineProperty(() => session().ready(), "promise", { get: () => session().ready.promise, }) as (() => boolean) & { readonly promise: Promise<unknown> | undefined } } export function createPromptState(initial?: InitialPrompt) { const [store, setStore] = createStore<PromptStore>(promptStore(initial)) const ready = Object.assign(() => true, { promise: Promise.resolve(true) }) return { ready, ...createPromptStateValue(store, setStore), } }