diff --git a/.codex/config.toml b/.codex/config.toml index 3c13613998..6d34a198c0 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -1,3 +1,9 @@ +sandbox_mode = "workspace-write" +approval_policy = "never" + +[sandbox_workspace_write] +network_access = true + [agents] max_threads = 6 max_depth = 1 diff --git a/apps/web/components/task/chat/clarification-custom-input.test.tsx b/apps/web/components/task/chat/clarification-custom-input.test.tsx index 70de0a5af6..f4ae780e31 100644 --- a/apps/web/components/task/chat/clarification-custom-input.test.tsx +++ b/apps/web/components/task/chat/clarification-custom-input.test.tsx @@ -1,6 +1,8 @@ +import { createRef } from "react"; import { afterEach, describe, it, expect, vi } from "vitest"; import { cleanup, render, fireEvent } from "@testing-library/react"; import { ClarificationCustomInput } from "./clarification-overlay-parts"; +import { routePanelMouseDown } from "./route-panel-mouse-down"; // Mutable pointer state so individual tests can flip to a touch device without // touching matchMedia internals. @@ -36,6 +38,51 @@ function pressEnter(el: HTMLElement, init: Partial = {}): boo return fireEvent.keyDown(el, { key: "Enter", ...init }); } +function renderInPanel(overrides: Partial[0]> = {}) { + const panelRef = createRef(); + return render( +
routePanelMouseDown(event, panelRef)} + > + +
, + ); +} + +describe("ClarificationCustomInput row focus", () => { + it("focuses the textarea when the non-interactive row surface is pressed", () => { + const { getByTestId } = renderInPanel(); + + const notDefaulted = fireEvent.mouseDown(getByTestId("clarification-custom-input")); + + expect(notDefaulted).toBe(false); + expect(document.activeElement).toBe(getByTestId(INPUT_TESTID)); + }); + + it("does not focus the disabled textarea from the row surface", () => { + const { getByTestId } = renderInPanel({ isSubmitting: true }); + + fireEvent.mouseDown(getByTestId("clarification-custom-input")); + + expect(document.activeElement).not.toBe(getByTestId(INPUT_TESTID)); + }); + + it("does not route a touch Send press through the textarea", () => { + pointer.isFinePointer = false; + const onSubmit = vi.fn(); + const { getByTestId } = renderInPanel({ draft: "answer", onSubmit }); + + fireEvent.mouseDown(getByTestId(TOUCH_SUBMIT_TESTID)); + fireEvent.click(getByTestId(TOUCH_SUBMIT_TESTID)); + + expect(document.activeElement).not.toBe(getByTestId(INPUT_TESTID)); + expect(onSubmit).toHaveBeenCalledOnce(); + }); +}); + describe("ClarificationCustomInput multiline", () => { it("renders a textarea so answers can span multiple lines", () => { const { getByTestId } = render(); diff --git a/apps/web/components/task/chat/clarification-overlay-parts.tsx b/apps/web/components/task/chat/clarification-overlay-parts.tsx index 2e1ec3877e..8bea1820c7 100644 --- a/apps/web/components/task/chat/clarification-overlay-parts.tsx +++ b/apps/web/components/task/chat/clarification-overlay-parts.tsx @@ -245,11 +245,20 @@ export function ClarificationCustomInput({
{ + if (isSubmitting) return; + const target = event.target as HTMLElement; + if (target.closest("textarea, button")) return; + event.preventDefault(); + event.stopPropagation(); + textareaRef.current?.focus({ preventScroll: true }); + }} className={cn( - "mt-2.5 flex items-start gap-2 px-3 py-2 rounded-lg border transition-colors", + "mt-2.5 flex min-h-11 items-start gap-2 px-3 py-2 rounded-lg border transition-colors", active ? "bg-blue-500/15 border-blue-500/50 text-foreground" : "border-dashed border-border/70 bg-muted/30", + isSubmitting ? "cursor-not-allowed" : "cursor-text", )} > { await expect(session.chat).not.toContainText("linesecond line"); }); + test("custom answer row ignores a hidden stale session chat", async ({ + testPage, + apiClient, + seedData, + }) => { + const session = await seedClarificationTask( + testPage, + apiClient, + seedData, + "Clarification Active Chat Locator", + "clarification", + ); + + await expect(session.clarificationOverlay()).toBeVisible({ timeout: 30_000 }); + + // Dockview can retain a prior session's chat in the DOM after its tab is + // hidden. The custom-answer row must come from the visible chat only. + await testPage.locator("body").evaluate((body) => { + body.insertAdjacentHTML( + "beforeend", + '
stale custom answer
', + ); + }); + + await expect(session.clarificationCustomInput()).toHaveCount(1); + }); + test("skip clarification", async ({ testPage, apiClient, seedData }) => { const session = await seedClarificationTask( testPage, @@ -100,7 +127,7 @@ test.describe("Clarification flow", () => { await expect(session.idleInput()).toBeVisible({ timeout: 30_000 }); }); - test("timeout detaches clarification but keeps overlay for deferred answer", async ({ + test("timeout detaches clarification and accepts a custom deferred answer", async ({ testPage, apiClient, seedData, @@ -154,9 +181,17 @@ test.describe("Clarification flow", () => { ); expect(primarySession?.state).toBe("WAITING_FOR_INPUT"); - // Agent moved on; a late answer goes through the event fallback as a new prompt. - await session.clarificationOption("PostgreSQL").click(); + // Agent moved on; a late custom answer remains editable and goes through + // the event fallback as a new prompt. + const input = session.clarificationInput(); + await expect(input).toBeEnabled(); + await session.clarificationCustomInput().click({ position: { x: 4, y: 4 } }); + await expect(input).toBeFocused(); + await input.pressSequentially("Use the embedded database for this task"); + await expect(input).toHaveValue("Use the embedded database for this task"); + await input.press("Enter"); await expect(session.idleInput()).toBeVisible({ timeout: 30_000 }); + await expect(session.chat).toContainText("Use the embedded database for this task"); }); test("options render label and description on separate rows", async ({ diff --git a/apps/web/e2e/tests/chat/mobile-clarification.spec.ts b/apps/web/e2e/tests/chat/mobile-clarification.spec.ts index 53aef5e3a3..d57c6862de 100644 --- a/apps/web/e2e/tests/chat/mobile-clarification.spec.ts +++ b/apps/web/e2e/tests/chat/mobile-clarification.spec.ts @@ -28,7 +28,10 @@ test.describe("Mobile clarification multiline answer", () => { await expect(session.clarificationOverlay()).toBeVisible({ timeout: 30_000 }); const input = session.clarificationInput(); - await input.click(); + // Tap the apparent row surface, outside the textarea itself. The whole row + // is the mobile touch target and should transfer focus into the textarea. + await session.clarificationCustomInput().tap({ position: { x: 4, y: 4 } }); + await expect(input).toBeFocused(); await input.pressSequentially("first line"); // On touch, Enter inserts a newline rather than submitting. await input.press("Enter");