Agent chat start: no "Starting… Cancel" row, no layout shift, Stop takes the held prompt back
Leo, 2026-11-01:
i hate the "Starting Claude Code... cancel" thing that it starts with
then the stupid ass layout shift
surely we can have a better load/faster load/intro than that
What changes
A new chat whose agent is still starting (the New Tab page's prompt seeded onto the harness switch, harnessSwitch.ts) now reads like a turn that is already running:
- No "Starting Claude Code… Cancel" row. The line under the prompt is the turn's own Thinking line from the send (
conversation/turns.ts,Thinking.tsx). The held prompt carries no start status (applySwitch). - No layout shift when the agent starts. A held prompt already draws its footer (time, Copy) and the row estimate counts it (
UserBubble.tsx,model.ts userMetaHeight). Before, the 4+24 px footer appeared once the prompt went out and pushed the line below it down. The line keeps one id and one version, so it doesn't redraw at that point either. - Cancel moves to the composer's Stop. While prompts are held, the composer shows Stop (and the double Esc works) right from the send. It hands every held prompt back to the composer (
HarnessSwitch.cancelQueuedAll). Once the turn runs, Stop stops the turn as before. Thechat.harness.cancelPromptaction stays for the host. - New Tab's start note no longer pushes the card. When a chat on another machine or in no project keeps the page up until it starts, Send spins in its own box (
data-starting). The "Starting Claude Code…" words are kept only as a hidden live region for screen readers. - Typing in a new chat already prewarms its agent locally (
newChatPrewarm.ts, unchanged), so the harness starts before Enter. A remote connection can't prewarm, so this PR makes the wait read as the turn itself instead.
Gallery
agent-pane.agent-start runs the real pane against the in-page mock daemon. The daemon's session/new now answers late or refuses through a new mockStart knob (mock.ts, gallery/frame/agentPane.ts, gallery/format.ts):
starting: the agent is still starting. The prompt reads as sent, Thinking runs under it, and the composer offers Stop.slow-start: play. The agent takes 2.5 s to start, then answers. The composer is anchored, and the play fails if the prompt moves.failed-start: the existing failed card with Retry, and the prompt back in the composer.
Compare arms: the agent-start-look experiment draws the starting line three ways: thinking (ships), silent (the line keeps its room and draws nothing) and spinner (a lone spinner). I picked thinking because it is one look from send to first output, so the start never reads as a separate phase. The other two arms are a deletion away.
Design spec: leo-workspace design/chat-transcript.md (new section "New chat start"), design/composer.md, design/new-tab.md.
Performance
Same harness both sides: the gallery play agent-pane.agent-start / slow-start (the agent takes 2.5 s to start, then answers), run in the gallery health VM. "Before" is base 318f3d1 with this PR's gallery entry and mock hook applied; "after" is this PR. The play runs its own layout-shift observer from the send to the first output, plus the line's top on every frame. The chat header is excluded (its Needs-you badge widens on any count change, whatever the start does: #30663).
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| line under the prompt, travel from send to first output | 28.0 px (the start row turns into Thinking) | 0 px | -28 px | measure run, 3 runs per side |
| layout shifts in the transcript and composer during the start | 1 (CLS 0.0027) | 0 | -1 | same run; after confirmed at the final head by this PR's gallery health (the play throws on any shift) |
| prompt bubble movement | 0 px | 0 px | 0 | same |
| composer usable while the agent starts (type, queue more) | yes | yes | — | starting variant |
| cancel from the composer (Stop / Esc) while the agent starts | no (only the row's Cancel) | yes | added | harnessSwitch.test.ts |
| "Starting Claude Code… Cancel" row drawn | yes | no | removed | starting variant |
The last shift at an intermediate head (the Model chip widening 52.8 px when the session opened) was the gallery mock not listing each harness's start level the way acpmux does; the entry's own fixture now lists it, so the real app was never affected.
Timers: none added. The Thinking line's ticking clock is the existing useTicking, which stops when the line unmounts or the turn's first output replaces it. The mock's mockStart.delayMs is a gallery-only setTimeout that ends when it fires.
Tests
bun test src/agent-session/acpmux/harnessSwitch.test.ts: the new test "a prompt held while the agent starts reads as sent, and Stop hands every held prompt back" fails on base (the held row carries "Starting Claude Code…" andcancelQueuedAlldoesn't exist) and passes here.- Updated to the new behavior:
test/transcript-live-runs.test.tsx,transcript-working-line,transcript-live-line-below,transcript-codex-live-line,new-tab-ask-model(Send spins, and no note is drawn in the card),guest-read-only-transcript,conversation/waitingOn.test.tsx.
Observed / Not run
- Observed: the focused bun tests above pass locally (35 pass), the red/green on the switch test, and
vp fmt --check/vp linton the changed files. - Full webview suite and type check: this PR's
web-validationand galleryhealthchecks at the final head. Not run: a DEV build (checked after landing).
Changelog
- Changed: A new agent chat no longer shows "Starting Claude Code… Cancel" or shifts its layout when the agent becomes ready. The prompt shows as sent with Thinking under it, and Stop in the composer takes it back.
Prior art checked: #29938 and #28596 (live line clock) and #23103 (send gesture through a slow start) touch neighbouring code with different intent; no duplicate.
🤖 Generated with Claude Code
Agent session: agent session