diff --git a/packages/app/e2e/regression/session-request-docks.spec.ts b/packages/app/e2e/regression/session-request-docks.spec.ts index 036eaaef42..714d6ca96f 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 7db0583c51..284e04447f 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -633,7 +633,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/home.tsx b/packages/app/src/pages/home.tsx index d697e53ae7..74c9b05d29 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -240,8 +240,8 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) { return { setViewport, setContentRef, setHeaderRef, update, titleOpacity } } -// Middle-click or Cmd+click on macOS (Ctrl+click elsewhere) opens a session -// tab in the background without navigating, matching browser conventions. +// Cmd+click on macOS (Ctrl+click elsewhere) opens a session tab in the +// background without navigating, matching browser conventions. function isBackgroundOpen(event: MouseEvent) { return shouldOpenSessionInBackground({ button: event.button, @@ -466,8 +466,8 @@ export function NewHome() { } function editProject(conn: ServerConnection.Any, project: LocalProject) { - void import("@/components/dialog-edit-project-v2").then((x) => { - void dialog.show(() => ) + void import("@/components/dialog-edit-project").then((x) => { + dialog.show(() => ) }) } @@ -1387,15 +1387,7 @@ function HomeSessionSearchResultRow(props: { group: !!showProjectName(), }} onMouseEnter={() => props.onHighlight()} - onMouseDown={(event) => { - if (event.button === 1) event.preventDefault() - }} onClick={(event) => props.onSelect(props.record.session, { background: isBackgroundOpen(event) })} - onAuxClick={(event) => { - if (!isBackgroundOpen(event)) return - event.preventDefault() - props.onSelect(props.record.session, { background: true }) - }} > { - if (event.button === 1) event.preventDefault() - }} onClick={(event) => props.openSession(props.record.session, { background: isBackgroundOpen(event) })} - onAuxClick={(event) => { - if (!isBackgroundOpen(event)) return - event.preventDefault() - props.openSession(props.record.session, { background: true }) - }} >