feat(chat): continue an interrupted turn from the tool call it stopped at - #9189
feat(chat): continue an interrupted turn from the tool call it stopped at#9189tamimbinhakim wants to merge 3 commits into
Conversation
…d at Stopping a turn, or having it fail, leaves the user retyping a message to get the agent moving again, and the agent has no idea which step it was on. None of the providers can resume a half-finished tool call, so Continue is a new turn under the hood, made invisible and precise. Messages gain an optional origin. A continuation turn is a normal thread.turn.start whose message carries origin: continuation; the decider, projection (new nullable column), in-memory projector and client reducer pass it through. Web and mobile render such messages as a small marker instead of a user bubble. The composer shows a Pick up where it stopped notice when the latest turn is interrupted or errored; Continue builds a prompt naming the command (with its directory) or file change that was in flight and sends it with the thread's current model and modes. The prompt builder lives in client-runtime so both surfaces send the same words. Works for every provider since it rides the normal resume path.
There was a problem hiding this comment.
Two consistency gaps in the new Continue flow; both are in apps/web and both stem from the continuation message being a real user message that the timeline deliberately hides.
Posted via Macroscope — UI Consistency
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR introduces a new Continue workflow that starts agent turns from interrupted or failed tool calls and adds cross-cutting UI, orchestration, and persistence behavior. The additive metadata and nullable migration reduce compatibility risk, but the new workflow and potential tool execution are substantial enough to require human review. You can add or adjust custom eligibility rules. Learn more. |
- Continue runs the same optimistic path as a send: sendInFlightRef is held, local dispatch begins, the view follows to the end, and the state is reset if the turn fails to start, so a second send cannot race it. - The cut-off step prefers a call that never finished (stopped, failed or still in progress), then the last tool call of the turn, so the prompt names the right step. - Continuation messages are excluded from the timeline minimap.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e785e1e. Configure here.

Problem
When you stop a turn, or it dies with an error, the only way to get the agent moving again is to type another message. Two things are wrong with that: you have to retype, and the agent has no idea which step it was on when it got cut off, so it tends to re-run everything or wander.
None of the five providers can resume a half-finished tool call, so "Continue" has to be a new turn under the hood. This PR makes that turn invisible in the transcript and precise about where to pick up.
What this does
Messages get an optional
origin.thread.turn.startacceptsmessage.origin: "continuation". The decider stamps it on thethread.message-sentevent, the projection stores it (migration 045 adds a nullableorigincolumn onprojection_thread_messages), and the in-memory projector and client reducer carry it through. Nothing else in the pipeline changes: the provider still receives it as a normal user prompt on the resumed session, so it works for Codex, Claude, Cursor, Grok and OpenCode alike.Continue banner. When the latest turn is
interruptedorerror, nothing is running and no approval is pending, the composer stack shows "Pick up where it stopped" (or "Pick up after the error") with a Continue button. The client builds the prompt from the thread's work log: the last in-flight tool call of that turn, its command with any leadingcdsplit off into "in<dir>", or the tool label for non-command calls. The words come from one builder inclient-runtimeso web and mobile send the same prompt. The turn is sent with the thread's current model, runtime mode and interaction mode.Marker, not bubble. Web renders a continuation message as a thin "Continued from the interrupted step" divider with the timestamp; mobile renders the same divider. No optimistic bubble is pushed.
Reverse state. The banner is dismissable per turn and disappears once a message is sent or a turn is running.
Mobile gets the marker in this PR; the mobile Continue button can follow once the composer banner pattern exists there.
Testing
decider.continue.test.ts: origin stamped on the recorded message, absent otherwise.045_ProjectionThreadMessageOrigin.test.ts: migration adds the nullable column.threadReducer.test.ts: origin kept on the appended message.continuation-prompt.test.ts: prompt wording with command + directory, tool label, and bare fallback;cdsplitting.ProjectionThreadMessages,ProjectionPipeline, decider and timeline suites pass.Related: #9186 (tool rows) gives the interrupted row its visual state; this PR does not depend on it.
Note
Medium Risk
Touches turn-start events, message projection upserts, and a new DB column; incorrect origin handling could mislabel transcripts or drop metadata on streaming completion, but changes are additive and optional for older clients.
Overview
Adds an optional
origin: "continuation"on user messages so Continue after an interrupted or errored turn is stored and projected like a normal turn start, but shown in the transcript as a divider instead of a user bubble (web and mobile). Contracts, decider, in-memory projector, client reducer, andprojection_thread_messages(migration 045, nullableorigin) all carry the field through streaming and upsert paths, preserving origin when later writes omit it.On web, when the latest turn is interrupted or error and nothing else is blocking work, the composer shows a Continue banner. It builds a shared
buildContinuationPrompt(plussplitLeadingCdForPrompt) from the cut-off tool/work-log entry and callsthread.turn.startwith that origin. The timeline minimap skips continuation messages. User docs describe the flow.Medium scope: new user-facing orchestration path and DB column, but backward-compatible optional metadata and targeted tests (decider, migration, reducer, prompt builder).
Reviewed by Cursor Bugbot for commit f6e158d. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add
Continuebanner to resume interrupted chat turns withorigin=continuationOrchestrationMessageOriginto contracts and propagates the optionalorigin=continuationthroughthread.turn.startcommands,thread.message-sentevents, the decider, projector, persistence layer, and the client thread reducer.origincolumn toprojection_thread_messages; subsequent upserts preserve an existing origin when the incoming row omits one.buildContinuationPrompthelper inclient-runtimethat produces a reason-specific prompt using the interrupted tool call, command, or directory context and instructs the agent not to repeat completed work.Continuecomposer banner in ChatView.tsx for interrupted or errored latest server turns; pressingContinuedispatches a new user turn carrying the generated prompt andorigin=continuation.origincolumn is nullable and defaults to absent; readers inProjectionSnapshotQuery.tsandProjectionThreadMessages.tsare updated, but any out-of-tree consumers ofprojection_thread_messagesmust tolerate the new column.Macroscope summarized f6e158d.