perf(agent pane): a streamed chunk re-renders only its row; each frame is parsed once
Summary
HOT PATH AUDIT area F, rows W1 and W2 (AUDIT.md; lead decision 1). Every streamed chunk re-rendered the whole agent pane (AcpmuxPane, with about 20 memos over all rows), the Composer and Session Info. And each frame from acpmux was parsed twice.
- Streaming row as its own store (
streamingRow.ts): when a snapshot from the direct client differs from the one the pane drew only in the streaming reply's text, the pane keeps its snapshot. The row's newer text goes to a one-value store.textOnlyRowis one pointer compare per row.summary,subagentViewsandcatalog, which the client rebuilds every time, compare by value.- The live
VirtualTranscriptreads the store (useSyncExternalStore) and draws that one row with its newer text and version. - Any other change draws through the pane as before and clears the store.
- Safety: the fast path is taken only while the transcript draws the reply's row as the client holds it (
drawsAsIs), so rows thatconversation/turns.tsreshapes take the old path.rowsRefandsnapshotRefstill take every snapshot, so copy, menus and other imperative readers see the newest text. - One parse per frame:
direct.tsparses each frame once and hands the parse to the wire log (wire.tsreceived(text, parsed)). - Test seam:
acpmuxPerf.render(name)counts renders while measuring.loadRunreports them.
To the page owner (Leo): this touches App.tsx (pane listener, VirtualTranscript prop streams), direct.ts (receive) and wire.ts.
Testing
Red tests first (on this cloud VM, Chrome 156; render and parse counts are deterministic, times are directional only):
test/agent-pane-stream-renders.test.ts: the real pane on the in-page mock daemon, 700 seeded turns, one text-only turn at full speed.- Base 7035f8f337f: 111 chunks gave 112 pane renders and 112 composer renders (fails).
- Head: 224 chunks gave 5 and 5; the reply's row rendered 221 times. loadRun React p95 went from 14.5 ms to 9.5 ms, work p95 from 55 ms to 21 ms.
test/agent-pane-session-change-coalesce.test.ts, "a streamed chunk's frame is parsed once": base 100JSON.parsecalls for 50 frames (fails); head 50.
Existing tests with the fix (VM Chrome; WebKit not installed here, so only Chromium ran): agent-pane-streaming-no-flicker, agent-pane-stream-jitter (10), streaming-parked-still (4, 1 skip), transcript-row-renders, agent-pane-session-change-coalesce (8), composer-stream-typing, streaming-markdown-forming (38), chat-find: all pass. bun run check passes.
Observed / Not run
- Observed: the runs above on this Linux VM.
- Not run: WebKit (the engine cmux NEXT draws the pane with); the GitHub Actions browser lane runs it. No fleet timing (browser tests never run on fleet Macs).
Changelog
- Changed: agent chats stay smooth while a long reply streams; typing and scrolling no longer compete with each streamed word.
No capture: no visible change.
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Fixes the agent pane so each streamed chunk re-renders only the streaming reply's row instead of the whole pane (~20 memos, the Composer, and Session Info), and parses each acpmux frame once instead of twice.
- The reply's newer text goes to a one-value store (
streamingRow.ts) read only by the live transcript; any other change draws through the pane and clears the store, and chunks draw through the pane while an artifact or Find is open. - The live line under the reply stays quiet while it streams: its quiet key moves with the reply's newer version.
- The fast path runs only while the transcript draws the reply's row unchanged, snapshots compare by identity without serializing, and
rowsRefandsnapshotRefstill take every snapshot. direct.tsparses each frame once and passes the parse to the wire log.