From 8162fc96706e2c04276673304349089fa71e5ae3 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:07:51 +1000 Subject: [PATCH] fix(app): preserve composer caret after requests --- .../regression/session-request-docks.spec.ts | 62 +++++++++++++++++++ packages/app/src/components/prompt-input.tsx | 4 +- packages/app/src/pages/session.tsx | 7 ++- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/app/e2e/regression/session-request-docks.spec.ts b/packages/app/e2e/regression/session-request-docks.spec.ts index 036eaaef4221..714d6ca96f15 100644 --- a/packages/app/e2e/regression/session-request-docks.spec.ts +++ b/packages/app/e2e/regression/session-request-docks.spec.ts @@ -1,6 +1,7 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { expect, test, type Page } from "@playwright/test" import { mockOpenCodeServer } from "../utils/mock-server" +import { installSseTransport } from "../utils/sse-transport" import { expectSessionTitle } from "../utils/waits" const directory = "C:/OpenCode/RequestDocks" @@ -100,6 +101,67 @@ test("shows a pending permission dock", async ({ page }) => { expect(request.postDataJSON()).toEqual({ response: "once" }) }) +test("restores the draft caret before typing after a request dock closes", async ({ page }) => { + const transport = await installSseTransport(page, { + server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`, + retry: 20, + }) + await mockServer(page, { questions: [] }) + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await transport.waitForConnection() + await expectSessionTitle(page, title) + + const editor = page.locator('[data-component="prompt-input"][contenteditable="true"]') + const draft = "keep the caret at the end" + await editor.fill(draft) + await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => resolve()))) + for (let index = 0; index < 4; index++) await page.keyboard.press("ArrowLeft") + const cursor = draft.length - 4 + await expect + .poll(() => + editor.evaluate((element) => { + const selection = window.getSelection() + if (!selection?.rangeCount || !element.contains(selection.anchorNode)) return -1 + const range = selection.getRangeAt(0).cloneRange() + range.selectNodeContents(element) + range.setEnd(selection.anchorNode!, selection.anchorOffset) + return range.toString().length + }), + ) + .toBe(cursor) + await transport.send({ + directory, + payload: { + type: "question.asked", + properties: { + id: "question-caret", + sessionID, + questions: [ + { + header: "Continue", + question: "Continue?", + options: [{ label: "Yes", description: "Continue the session" }], + }, + ], + tool: { messageID: "message-caret", callID: "call-caret" }, + }, + }, + }) + const question = page.locator('[data-component="dock-prompt"][data-kind="question"]') + await expect(question).toBeVisible() + await expect(editor).toHaveCount(0) + + await transport.send({ + directory, + payload: { type: "question.rejected", properties: { sessionID, requestID: "question-caret" } }, + }) + await expect(question).toHaveCount(0) + await expect(editor).toBeVisible() + await page.keyboard.press("x") + + await expect(editor).toHaveText(`${draft.slice(0, cursor)}x${draft.slice(cursor)}`) +}) + async function mockServer( page: Page, requests: { diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index bbb71e87224a..b18b58173778 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -632,7 +632,9 @@ export const PromptInput: Component = (props) => { const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229 const handleBlur = () => { - savedCursor = currentCursor() + const cursor = currentCursor() + savedCursor = cursor + if (cursor !== null && cursor !== prompt.cursor()) prompt.set(prompt.current(), cursor) closePopover() setComposing(false) } diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 66afcfa9bf2f..5b878feab07b 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -57,6 +57,8 @@ import { useSync } from "@/context/sync" import { useTabs } from "@/context/tabs" import { TerminalProvider, useTerminal } from "@/context/terminal" import { PromptInput } from "@/components/prompt-input" +import { setCursorPosition } from "@/components/prompt-input/editor-dom" +import { promptLength } from "@/components/prompt-input/history" import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit" import { createPromptInputController, @@ -1052,7 +1054,10 @@ export default function Page() { if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) { if (composer.blocked() || isChildSession()) return - inputRef?.focus() + const input = inputRef + if (!input) return + input.focus() + setCursorPosition(input, prompt.cursor() ?? promptLength(prompt.current())) } }