diff --git a/packages/app/e2e/browser/composer-whitespace.spec.ts b/packages/app/e2e/browser/composer-whitespace.spec.ts index ca8704ed25..8e1a527f0f 100644 --- a/packages/app/e2e/browser/composer-whitespace.spec.ts +++ b/packages/app/e2e/browser/composer-whitespace.spec.ts @@ -170,7 +170,8 @@ test("composer growth keeps a bottom-pinned chat at the bottom", async ({ page } async function pausePageClock(page: Page): Promise { const now = Date.now(); await page.clock.install({ time: now }); - await page.clock.pauseAt(now); + // The installed clock advances between protocol calls; pause at a future time. + await page.clock.pauseAt(now + 60_000); } /** The send error shares the footer with Send, so whatever keeps Send visible keeps it visible. */ @@ -206,7 +207,7 @@ test("compact long drafts stop growing before the header", async ({ page }) => { } }); -test("compact fullscreen editing preserves the live draft and selection and stays open after send", async ({ +test("compact fullscreen editing preserves the live draft and selection and returns to chat after send", async ({ page, }) => { const agent = await seedMockAgentWorkspace({ @@ -260,7 +261,7 @@ test("compact fullscreen editing preserves the live draft and selection and stay await sendComposerWithButton(page); await expect(composerLocator(page)).toHaveValue(""); }); - await test.step("sending inside fullscreen clears the editor and keeps fullscreen open", async () => { + await test.step("sending inside fullscreen returns to chat and clears the editor", async () => { await writeCompactDraft(page, "First line\nSecond line\nThird line"); await enterComposerFullscreen(page); await writeCompactDraft(page, "Short draft"); @@ -274,10 +275,12 @@ test("compact fullscreen editing preserves the live draft and selection and stay await resizeComposerViewport(page, "portrait"); await sendComposerWithButton(page); await expect(composerLocator(page)).toHaveValue(""); - await expectComposerFullscreenControls(page); - await expect(page.getByRole("button", { name: "Send message", exact: true })).toBeDisabled(); + await expectWorkspaceHeaderVisible(page); + await expect(page.getByTestId("composer-fullscreen")).toHaveCount(0); }); await test.step("typing and sending keep working after leaving fullscreen again", async () => { + await writeCompactDraft(page, "First line\nSecond line\nThird line"); + await enterComposerFullscreen(page); await writeCompactDraft(page, "Typed inside fullscreen"); await expect(page.getByRole("button", { name: "Send message", exact: true })).toBeEnabled(); await exitComposerFullscreen(page); @@ -339,16 +342,16 @@ test("fullscreen submit rejection retains the editable draft and supports retry" await writeCompactDraft(page, draft); await enterComposerFullscreen(page); await sendComposerWithButton(page); - await expect(page.getByTestId("composer-fullscreen").getByRole("alert")).toHaveText( - "Requested mock prompt rejection", - ); + await expectWorkspaceHeaderVisible(page); + await expect(page.getByRole("alert")).toHaveText("Requested mock prompt rejection"); + await enterComposerFullscreen(page); await expectFullscreenErrorAboveSend(page); await expect(composerLocator(page)).toHaveValue(draft); await expect(composerLocator(page)).toBeEditable(); await expect(page.getByRole("button", { name: "Send message", exact: true })).toBeEnabled(); await sendComposerWithButton(page); await expect(composerLocator(page)).toHaveValue(""); - await expectComposerFullscreenControls(page); + await expectWorkspaceHeaderVisible(page); } finally { await agent.cleanup(); } @@ -440,8 +443,7 @@ test("fullscreen Send keeps the configured queue behavior while an agent runs", await expectComposerFullscreenControls(page); await queueComposerWithButton(page); await expect(composerLocator(page)).toHaveValue(""); - await expectComposerFullscreenControls(page); - await exitComposerFullscreen(page); + await expectWorkspaceHeaderVisible(page); await expectQueuedMessageButton(page); } finally { await agent.cleanup(); diff --git a/packages/app/e2e/browser/new-workspace-composer-draft.spec.ts b/packages/app/e2e/browser/new-workspace-composer-draft.spec.ts index e7dbde3bd9..906efcfb42 100644 --- a/packages/app/e2e/browser/new-workspace-composer-draft.spec.ts +++ b/packages/app/e2e/browser/new-workspace-composer-draft.spec.ts @@ -1,7 +1,14 @@ +import { + composerLocator, + enterComposerFullscreen, + exitComposerFullscreen, + resizeComposerViewport, +} from "../support/helpers/composer"; import { expect, test } from "../support/fixtures"; import { gotoAppShell } from "../support/helpers/app"; import { getE2EDaemonPort } from "../support/helpers/daemon-port"; import { + delayBrowserWorkspaceCreatedResponse, expectNewWorkspaceDraft, expectNewWorkspaceProjectSelected, fillNewWorkspaceDraft, @@ -134,3 +141,45 @@ test.describe("New workspace composer draft", () => { } }); }); + +test("fullscreen creation returns inline and keeps submission locked when reopened", async ({ + page, +}) => { + const project = await seedWorkspace({ repoPrefix: "fullscreen-submit-pending-" }); + const delay = await delayBrowserWorkspaceCreatedResponse(page); + try { + await gotoAppShell(page); + await waitForSidebarHydration(page); + await openNewWorkspaceComposer(page, project); + await resizeComposerViewport(page, "portrait"); + await fillNewWorkspaceDraft(page, DRAFT); + await enterComposerFullscreen(page); + await page.getByRole("button", { name: "Create", exact: true }).click(); + await delay.waitForCreateRequest(); + + await test.step("submission returns inline with the original draft and spinner", async () => { + await expect(page.getByTestId("composer-fullscreen")).toHaveCount(0); + await expect(composerLocator(page)).toHaveValue(DRAFT); + await expect(composerLocator(page)).not.toBeEditable(); + await expect(page.getByRole("button", { name: "Create", exact: true })).toBeDisabled(); + await expect(page.getByRole("progressbar")).toBeVisible(); + }); + await test.step("fullscreen can reopen while pending without unlocking or duplicating submission", async () => { + await enterComposerFullscreen(page); + await expect(page.getByTestId("composer-fullscreen")).toBeVisible(); + await expect(composerLocator(page)).toHaveValue(DRAFT); + await expect(composerLocator(page)).not.toBeEditable(); + await expect(page.getByRole("button", { name: "Create", exact: true })).toBeDisabled(); + await expect(page.getByRole("progressbar")).toBeVisible(); + await exitComposerFullscreen(page); + }); + delay.release(); + await page.waitForURL((url) => url.pathname.includes("/workspace/")); + await expect(composerLocator(page)).toHaveValue(""); + await expect(composerLocator(page)).toBeEditable(); + expect(delay.agentRequests).toHaveLength(1); + } finally { + delay.release(); + await project.cleanup(); + } +}); diff --git a/packages/app/src/composer/input/input.tsx b/packages/app/src/composer/input/input.tsx index 01051bd52c..7346095d5b 100644 --- a/packages/app/src/composer/input/input.tsx +++ b/packages/app/src/composer/input/input.tsx @@ -942,6 +942,7 @@ interface SendMessageContext { onSubmit: (payload: MessagePayload) => void; onMinimizeHeight: () => void; preserveHeightOnSubmit: boolean; + exitFullscreen: () => void; } function sendMessageImpl(ctx: SendMessageContext): void { @@ -954,6 +955,7 @@ function sendMessageImpl(ctx: SendMessageContext): void { ) { return; } + ctx.exitFullscreen(); ctx.onSubmit({ text: trimmed, attachments: ctx.attachments, @@ -974,12 +976,14 @@ interface QueueMessageContext { onQueue: ((payload: MessagePayload) => void) | undefined; replaceText: (text: string) => void; onMinimizeHeight: () => void; + exitFullscreen: () => void; } function queueMessageImpl(ctx: QueueMessageContext): void { if (!ctx.onQueue) return; const trimmed = ctx.value.trim(); if (!trimmed && ctx.attachments.length === 0) return; + ctx.exitFullscreen(); ctx.onQueue({ text: trimmed, attachments: ctx.attachments, cwd: ctx.cwd }); ctx.replaceText(""); ctx.onMinimizeHeight(); @@ -1640,6 +1644,7 @@ export const MessageInput = forwardRef( onSubmit, onMinimizeHeight: minimizeInputHeight, preserveHeightOnSubmit, + exitFullscreen, }); }, [ allowEmptySubmit, @@ -1651,6 +1656,7 @@ export const MessageInput = forwardRef( minimizeInputHeight, preserveHeightOnSubmit, updateLiveTextPresence, + exitFullscreen, ]); const handleQueueMessage = useCallback( @@ -1662,8 +1668,9 @@ export const MessageInput = forwardRef( onQueue, replaceText, onMinimizeHeight: minimizeInputHeight, + exitFullscreen, }), - [attachments, cwd, onQueue, replaceText, minimizeInputHeight], + [attachments, cwd, onQueue, replaceText, minimizeInputHeight, exitFullscreen], ); const handleDefaultSendAction = useCallback(() => {