diff --git a/apps/web/src/components/ChatView.browser.test.tsx b/apps/web/src/components/ChatView.browser.test.tsx index 0ee0caa61d480..5535e76953a28 100644 --- a/apps/web/src/components/ChatView.browser.test.tsx +++ b/apps/web/src/components/ChatView.browser.test.tsx @@ -229,3 +229,32 @@ it("a rejected send leaves the timeline when a new draft was typed meanwhile", a await expect.element(page.getByText("Failed prompt")).not.toBeInTheDocument(); expect(editor.element().textContent).toContain("Next draft"); }); + +it("Enter on an empty prompt resumes an interrupted thread", async () => { + const history = threadWithHistory("interrupted", 1); + const interruptedRun = { ...history.runs[0]!, status: "interrupted" as const }; + const thread = { ...history, runs: [interruptedRun] }; + using _environment = installFakeEnvironment([thread], { acceptCommands: true }); + __setClientSettingsForTests({ + ...DEFAULT_CLIENT_SETTINGS, + onboardingCompletedAt: "2026-01-01T00:00:00.000Z", + }); + const router = getRouter( + createMemoryHistory({ initialEntries: [`/${ENVIRONMENT_ID}/${thread.id}`] }), + ); + await router.load(); + await using _app = await render(, { + // AppRoot brings its own router. + router: false, + }); + await expect.element(page.getByRole("button", { name: "Resume thread" })).toBeVisible(); + + await userEvent.click(page.getByTestId("composer-editor")); + await userEvent.keyboard("{Enter}"); + + await vi.waitFor(() => + expect(dispatchedCommands()).toContainEqual( + expect.objectContaining({ manualContinuationOfRunId: interruptedRun.id }), + ), + ); +}); diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 4150db637d45e..c9a7ec62066c8 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -2143,6 +2143,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }); return false; } + // An empty draft has nothing to send; Enter does what the Resume thread button does. + if (showResumeAction && !activePendingProgress) { + event?.preventDefault(); + if (isSendBusy || environmentUnavailable !== null) return false; + onResume(); + return true; + } const submission = submitComposerDraft({ prompt: promptRef.current, submissionTarget,