Agent chat: a steer is one continuous turn; only Stop or Esc reads as stopped
Problem
When Leo sends a message while a turn is running, and the agent can't take it mid-turn, acpmux ends the running turn and runs the message next. It ends the turn with a cancel, or, after acpmux #22808, Claude ends it with end_turn. The transcript drew this as a Stop: "You stopped after 55s" over the work, "Sent now · stopped the turn" under his message, and a fresh "Working for 18s". He never pressed Stop.
The turn really does end and restart under the hood. This PR keeps that data model and only merges how it is drawn. The "Worked for" and "You stopped after" lines below are the turn's fold label.
Change
direct.ts: telling a Send now from a real Stop.- acpmux sends a
queue_orderwithnext: truewhile a turn is open and the prompt is still queued. That armsinterrupting. - The next
turn_resultthat is notfailed(cancelled orend_turn) marks its summarycontinued. A failure still shows. - The prompt's
user_messagerow is markedcontinues. That means accepted and drawn inline, with no mark of its own: nothing reads "Steered". - A real Stop or Esc sends no
queue_order next, so it is unchanged. foldOldersaves, resets and restoresinterrupting, and patches a held sent-now row after an older page brings its cancel.- A Send now that never runs leaves the turn ended, but only on a record that says so:
queue_removed(removed or withdrawn),prompt_refused(a local acpmux streams it now that the pane asks for it by name), another prompt'suser_message, or the refusal answering this pane's own send. A transport error on that send (a closed socket fails it withnative.timed_out) is not a refusal: acpmux still runs the prompt, so the turn stays continued. The summary then dropscontinuedand shows its footer, "You stopped after" and its checkpoint. Betweendequeuedand the prompt's record, the chat reads idle while the prompt is about to run; the turn stays continued through that gap. - Stop clears a pending Send now, and a Send now inside Stop's grace period does not arm one.
- acpmux sends a
- New
turnStart.ts. It holdscontinuesTurn(a steer, or a prompt thatcontinues) andopensTurn. Everything that finds a turn by its prompt now walks past such a prompt:turns.tsturns, with one Working and Worked clock from the first prompt. It also draws no "You stopped after" for acontinuedsummary, including the moment before the prompt's record arrives.diff.tsturnRows, and App's last edit turn.- The checkpoint pair, which spans from the first part's start to the summary that ends the turn (
endsTurn). rowMarks.ts, which marks only turn-ending summaries.- Find and shell reveal (
turnFoldFor), and the hover footer (turnFooterFrom). runningGroupsand the minimap's turns.
- Removed: the
stoppedsteer state and itssteer.stoppedstring. Native steers keep their existing marks; #22773 removes those.
Screenshots
These show the first version, where the sent-now message read "Steered". It now carries no mark.
| Before | After (first version) |
|---|---|
![]() | ![]() |
Verification
Each fix lands as a tests-only commit followed by the fix commit. The tests are in webviews/test/transcript-steer-continuous.test.tsx. Each test commit fails on the fleet and its fix passes, running scripts/ci/run-webviews-tests.sh with cmux-ci run. They cover:
- the inline steer and the clock;
- a real Stop;
- the checkpoint span;
- Find and footer;
- the live flash;
- history paging;
- the edit-then-Send-now checkpoint request;
- both
foldOlderleaks; end_turn;- a failed turn;
- a Send now removed before or after the turn's end, or refused with its record, each live and replayed;
- a snapshot drawn between
dequeuedand the Send now's record (still continued, no checkpoint request); - a Send now whose request a dropped socket fails (
native.timed_out): the turn stays continued; - Stop followed by Send now.
The suite's other failures are the same on each test commit and its fix: agent-pane-column-edges, apps-flow, composer-html-paste-edit, hover-row-sweep, stats-page, thread-minimap-feel, tooltip-surface, whats-new-text-selection.
Observed / Not run
-
The red-to-green proof is
transcript-steer-continuous.test.tsx(bun + JSDOM, runs on the fleet).agent-pane-queue-steer.test.tsis a browser-lane file:requireBrowserLaneskips it unlessCI=trueorCMUX_BROWSER_TESTS=1, so it never ran on the fleet and its update is checked only by GitHub CI. -
The later commits were made in a sparse
/tmpclone, because the shared repo's git was frozen on cosmic-gray.scripts/web-lint.shthere reported no errors in changed files, also after the base merge. Its other errors were in untouched pages (missing generatedstrings.jsonand out-of-clone fixtures), plus checks that needschemas/,Packages/or the acpmux spec, which the clone lacks. CI's web lint lane is the check. -
No macOS build or dogfood.
-
Known gaps:
- A refused Send now stays drawn as continued until the next prompt when the pane never sees its
prompt_refused: a remote acpmux, a pane reloaded after the refusal, or a pane other than the sender's.prompt_refusedis a log record, not a transcript record, so only the sending pane's own refusal answer or a local live stream ends the turn. - A refusal can sit on an older-page boundary: when the attached page starts between the cancel and the
prompt_refused, the refusal is read before the turn it ends, so loading the older page draws that turn as continued. - Stop is live-only.
cancel()clears the pending Send now, but acpmux records no Stop, so replaying Stop followed by Send now draws a continued turn where the live pane drew it stopped. The durable fix is for acpmux to record acancel_requestedevent; that is not built here.
- A refused Send now stays drawn as continued until the next prompt when the pane never sees its
-
Localization: one key removed, no new strings.
Agent session: cosmic-gray
🤖 Generated with Claude Code

