Chat streaming and motion

Branch chat-shimmer-motion · 9 commits on e5ecb289fe · not pushed · 2026-10-05

The ACP chat now streams replies smoothly and follows the reference transcript's layout: one live clock line under the prompt, one-line reasoning and tool rows, and runs of steps on a rail. It started as the shimmer fix (PR 1 of the chat polish series) and grew when the live chat still felt off next to the reference.

17 ms
median time between screen updates while a reply streams
was 51 ms
37
largest jump of text in one update, in characters
was 145
1
live indicator per turn: the clock under the prompt
was 2, one of which flickered

Before and after

A running turn with a reasoning row, two running tool calls and the working line. The sweep is frozen mid-label in every capture. The before shots come from the base commit with a scripted agent; the after shots are real agent turns.

Before: brain-icon Thinking trigger, tool rows with kind icons, separate Working line at the bottom
Before, dark. The band was the page background, so the sweep read as a dark notch. "Working…" sits apart at the bottom.
Before, light theme
Before, light. The notch reads as a washed-out gap.
After: clock line under the prompt, a run of commands on a rail with a dashed ring on the running step
After, dark. The clock sweeps under the prompt; the run's summary sweeps; the running step turns its ring on the rail.
After, light theme
After, light.
Settled turn: Worked for 15s, Ran 3 commands folded, reply below
Settled. The clock stays as "Worked for 15s" and the run folds to its summary.
Reduced motion: same running turn with no sweep and a still ring
Reduced motion. Labels are plain muted text and the ring holds still.
A finished reply after a single ls call shown as one mono line
A finished reply, dark: a single call is one mono line with nothing under it; prose is 14px on a 24px line with 16px between paragraphs.
The same reply in the light theme
The same reply, light.

What changed

The loader

First pass: branch components above, reference recreation below; the branch sweep is faint
First pass, dark: branch components (top) against a recreation of the reference CSS (bottom). Our sweep barely shows.
Final: branch and reference rows render the same
Final, dark: the two render the same at the same point in the sweep. The light theme matches too.

Streaming

One live line per turn

Layout

Rows

No user-facing strings were added. Four strings left with the old rows and are removed from every catalog: "Working…", "Show detail", "Hide detail" and "+N more lines".

Your questions

Why did the sleep tool call have a detail dropdown under it?
Our row printed a preview under the title. For sleep 8 that was the agent's own description of the command, "Pause for eight seconds", not output. It also added "Show detail" whenever the call had content behind it. The reference has neither, so both are gone and the row itself is the disclosure.
When is the rail from the comparison page used?
In the reference, two or more work steps in a row between pieces of prose form a group: a summary line with the steps on a rail. It stays open while live and folds once the agent writes prose. A single call renders as one plain line with no header. Ours follows the same rule, except that our runs fold when the turn ends rather than when prose arrives.
Is the dashed ring real?
The component exists in the reference source, but it never renders: the rows that would show it always opt out of the icon, so a running tool there is static. The page's recreation draws it. We kept ours because the original spec asked for it.
And "Preparing a handoff" with the Braille spinner?
That only appears when handing a conversation to another agent. Nothing in our chat needs it.

Performance finding, not fixed here

Inserting any element in the desktop renderer triggers a style recalculation that can cover the whole document: about 6,500 elements and roughly 40 ms per insertion. Growing an existing text node does not. I traced it with invalidation tracking, reproduced it in-page by appending one span per frame into the transcript, and bisected the stylesheet by deleting rules.

Rules presentInsertions in 3 sStyle workWhole-document recalcs
All rules692,764 ms70
Without the 14 group-has-* variants2862,456 ms0
Without any :has() rule35736 ms0
All rules, text-node growth only361170 ms4

The fix is an app-wide audit of :has() usage, which belongs in its own PR. It would also speed up anything else that adds DOM nodes per frame.

Verification

Not done yet

Commits

CommitWhat
d1f1844b41Text shimmer sweeps a foreground highlight, in CSS
5bef044a1aBusy labels share one tempo and one weight
8cdc3c11ffA running tool call shows a slowly turning dashed ring
37b28aed65Shimmer base at 40% foreground so the sweep shows in dark
ce3fd31d30Running ring uses the CircleDashed glyph, dimmed
e50e6bb157Replies stream a word at a time and fade in by block
5d47e4d31cOne live clock line under the prompt, and the transcript's measure
35df536ad5One-line reasoning and tool rows, runs of steps on a rail
0ce80d2a84A running tool call joins the run it continues