From 33565fe4e16eba1308b322568fd453538bf67937 Mon Sep 17 00:00:00 2001 From: Ronny Date: Fri, 11 Sep 2026 17:14:34 +0200 Subject: [PATCH 1/3] feat(session-ui): add prompt input v2 dom parser --- packages/session-ui/package.json | 1 + .../v2/components/prompt-input/editor-dom.ts | 69 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 packages/session-ui/src/v2/components/prompt-input/editor-dom.ts diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 7db27630da53..873b7fb0f226 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -20,6 +20,7 @@ "./v2/*.css": "./src/v2/components/*.css", "./v2/*": "./src/v2/components/*.tsx", "./v2/prompt-input": "./src/v2/components/prompt-input/index.tsx", + "./v2/prompt-input/editor-dom": "./src/v2/components/prompt-input/editor-dom.ts", "./v2/prompt-input/interaction": "./src/v2/components/prompt-input/interaction.ts", "./v2/prompt-input/store": "./src/v2/components/prompt-input/store.ts", "./v2/prompt-input/types": "./src/v2/components/prompt-input/types.ts" diff --git a/packages/session-ui/src/v2/components/prompt-input/editor-dom.ts b/packages/session-ui/src/v2/components/prompt-input/editor-dom.ts new file mode 100644 index 000000000000..822302ccb9eb --- /dev/null +++ b/packages/session-ui/src/v2/components/prompt-input/editor-dom.ts @@ -0,0 +1,69 @@ +import type { PromptInputV2Attachment, PromptInputV2Prompt } from "./types" + +export function parsePromptInputV2Editor(editor: HTMLDivElement) { + const parts: Exclude[] = [] + let buffer = "" + let position = 0 + + const flush = () => { + if (!buffer) return + parts.push({ type: "text", content: buffer, start: position, end: position + buffer.length }) + position += buffer.length + buffer = "" + } + const mention = (element: HTMLElement) => { + flush() + const content = element.textContent ?? "" + if (element.dataset.mention === "agent") { + parts.push({ + type: "agent", + name: element.dataset.name ?? content.slice(1), + content, + start: position, + end: position + content.length, + }) + position += content.length + return + } + parts.push({ + type: "file", + path: element.dataset.path ?? content.slice(1), + content, + start: position, + end: position + content.length, + ...(element.dataset.mime ? { mime: element.dataset.mime } : {}), + ...(element.dataset.filename ? { filename: element.dataset.filename } : {}), + }) + position += content.length + } + const visit = (node: Node) => { + if (node.nodeType === Node.TEXT_NODE) { + buffer += node.textContent ?? "" + return + } + if (!(node instanceof HTMLElement)) return + if (node.dataset.mention) { + mention(node) + return + } + if (node.tagName === "BR") { + buffer += "\n" + return + } + Array.from(node.childNodes).forEach(visit) + } + + Array.from(editor.childNodes).forEach((node, index, nodes) => { + visit(node) + if (node instanceof HTMLElement && ["DIV", "P"].includes(node.tagName) && index < nodes.length - 1) buffer += "\n" + }) + flush() + if ( + parts.every((part) => part.type === "text") && + parts.every((part) => part.content.replace(/[\n\u200B]/g, "") === "") + ) { + return [{ type: "text" as const, content: "", start: 0, end: 0 }] + } + if (parts.length > 0) return parts + return [{ type: "text" as const, content: "", start: 0, end: 0 }] +} \ No newline at end of file From f1b6e65dd1f2ddde02c1b15ec6b171d985f6ccb5 Mon Sep 17 00:00:00 2001 From: Ronny Date: Fri, 11 Sep 2026 17:22:38 +0200 Subject: [PATCH 2/3] fix(app): recover prompt text from editor dom on empty store --- .../app/src/components/prompt-input/submit.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index c82c3a439939..9708e6ef4e59 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -15,6 +15,7 @@ import { useSDK, type DirectorySDK } from "@/context/sdk" import { useSync, type DirectorySync } from "@/context/sync" import { Identifier } from "@/utils/id" import { Worktree as WorktreeState } from "@/utils/worktree" +import { parsePromptInputV2Editor } from "@opencode-ai/session-ui/v2/prompt-input/editor-dom" import { buildRequestParts } from "./build-request-parts" import { setCursorPosition } from "./editor-dom" import { formatServerError } from "@/utils/server-errors" @@ -324,12 +325,25 @@ export function createPromptSubmit(input: PromptSubmitInput) { prompt: target.current(), context: target.context.items().slice(), }) - const currentPrompt = submission.prompt + let currentPrompt = submission.prompt const context = submission.context - const text = currentPrompt.map((part) => ("content" in part ? part.content : "")).join("") + let text = currentPrompt.map((part) => ("content" in part ? part.content : "")).join("") const images = input.imageAttachments().slice() const mode = input.mode() + if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) { + const editor = input.editor() + if (editor) { + const domPrompt = parsePromptInputV2Editor(editor as HTMLDivElement) + const domText = domPrompt.map((p) => ("content" in p ? p.content : "")).join("") + if (domText.trim().length > 0) { + currentPrompt = domPrompt as Prompt + text = domText + target.set(domPrompt as Prompt, domText.length) + } + } + } + if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) { if (input.working()) void abort() return From 5db847ac69f0bfc48fdf5d3267cbfb2ed4a7c9dc Mon Sep 17 00:00:00 2001 From: Ronny Date: Fri, 11 Sep 2026 17:24:18 +0200 Subject: [PATCH 3/3] fix(app): send v1 follow-up prompts to their session directory --- .../components/prompt-input/submit.test.ts | 39 +++++++++++++++++++ .../app/src/components/prompt-input/submit.ts | 3 +- packages/app/src/utils/server-compat.ts | 5 ++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index b3201b3ef68a..9f221f8a5b99 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -135,6 +135,13 @@ beforeAll(async () => { mock.module("@opencode-ai/ui/toast", () => ({ Toast: { Region: () => null }, showToast: () => 0, + toaster: { dismiss: () => undefined }, + })) + + mock.module("@opencode-ai/ui/v2/toast-v2", () => ({ + ToastV2: { Region: () => null }, + showToastV2: () => 0, + toasterV2: { dismiss: () => undefined }, })) mock.module("@opencode-ai/core/util/encode", () => ({ @@ -595,4 +602,36 @@ describe("prompt submit worktree selection", () => { expect(storedSessions["/repo/worktree-a"]?.[0]).toMatchObject({ id: "session-1", title: "New session 1" }) expect(optimisticSeeded).toEqual([true]) }) + + test("synchronizes text from DOM editor element if target.current is empty", async () => { + params = { id: "session-1" } + promptValue = [{ type: "text", content: "", start: 0, end: 0 }] + const domEditor = document.createElement("div") + domEditor.textContent = "Hello from DOM editor" + + const submit = createPromptSubmit({ + prompt, + info: () => ({ id: "session-1" }), + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => false, + mode: () => "normal", + working: () => false, + editor: () => domEditor, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + }) + + await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) + + expect(sentPrompts).toContain("/repo/main") + expect(promptInputs[promptInputs.length - 1]).toMatchObject({ + sessionID: "session-1", + text: "Hello from DOM editor", + }) + }) }) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 9708e6ef4e59..4987fbca7b4e 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -168,6 +168,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) { await input.api.prompt({ sessionID: input.draft.sessionID, + sessionDirectory: input.draft.sessionDirectory, id: messageID, agent: input.draft.agent, model: input.draft.model, @@ -630,7 +631,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { return true } - void sendFollowupDraft({ + await sendFollowupDraft({ api: sdk().api.session, sync: sync(), serverSync: serverSync(), diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts index 1df1338b71e5..3fe5dd0bb520 100644 --- a/packages/app/src/utils/server-compat.ts +++ b/packages/app/src/utils/server-compat.ts @@ -44,6 +44,8 @@ type LegacyPrompt = { model?: { providerID: string; modelID: string } variant?: string legacyParts?: (TextPartInput | FilePartInput | AgentPartInput)[] + sessionDirectory?: string + location?: { directory?: string } } type LegacyLocation = { directory?: string } type CompatibleInput = { @@ -198,7 +200,8 @@ function createV1Api(input: CompatibleInput): CompatibleApi { await legacy().session.abort(value) }, async prompt(value: SessionPromptInput & LegacyPrompt) { - await legacy().session.promptAsync({ + const loc = value.location ?? (value.sessionDirectory ? { directory: value.sessionDirectory } : undefined) + await legacy(loc).session.promptAsync({ sessionID: value.sessionID, messageID: value.id ?? undefined, agent: value.agent,