fix(chat): show every item when expanding a tool group

Merged
TE
7d ago
+738-201

Summary

The BLOCK at 94ea775c (one streamed chunk mapped all 15 displayed entries and reran the unchanged thought row) is fixed at 67fb58a4673: ToolGroupRow keeps each item's row element in a WeakMap keyed by the immutable item snapshot, so a chunk builds one element and React skips every unchanged sibling, and ThoughtRow is memoized. The per-chunk pass left is one identity lookup per item, the walk React already makes over the children; it creates, renders and parses nothing for an unchanged item, and it catches a delayed update to any earlier call, not only the tail.

Lawrence asked “why +7 more” after opening “Ran 12 commands.” Opened tool groups showed only their last five calls and required another click to reveal the rest. Opening a group now reveals every command, read, or search. Each full tool body keeps its own toggle, and group open/closed defaults stay as they are.

The change removes the secondary disclosure and its visibility assumption in the live-line model. Command and tool rows memoize the immutable item snapshot. Nested child views fold newly appended records once, preserving those snapshots too, so a streamed call skips unchanged siblings and their output processing. Fresh call starts after an assistant segment get a separate row even when a child reuses the call ID; earlier delayed updates keep the earlier row. Public-safe gallery fixtures include 13 commands and a nested group of 14.

Performance

Harness: a jsdom bench that renders ToolGroupRow (commands) with N completed calls plus one thought, opens every row (on base, by clicking "+K more"), then streams 20 chunks into the last call with mergeToolItem. Row renders are counted through toolFailure.rowFailed (once per CommandRow render), thought renders through thoughtLayout.enabled (once per ThoughtRow render). The same file ran on base 0ce3c31ee53 and head 67fb58a4673 on Linux fast checks; base by default shows only the last 5 calls, so it renders at most 5 rows per chunk until "+K more" is clicked.

metricbeforeafterdeltaharness or run link
call rows rendered per chunk, N=13141-13base / head
call rows rendered per chunk, N=2002011-200base / head
unchanged thought rows rendered per chunk10-1base / head
median ms per chunk (jsdom), N=131.020.44-57%base / head
median ms per chunk (jsdom), N=2007.280.43-94%base / head

Stop condition and cleanup: no timer, observer or animation loop is added. The row-element cache is a WeakMap keyed by item snapshots, so an entry goes away with its item.

Testing

Observed

  • Head 67fb58a4673 on Linux fast checks (run): bun run check passes; ToolGroupExpanded.test.tsx, childRows.test.ts, transcript-density.test.tsx and transcript-group-thought-state.test.tsx all pass, including the strict work-count regression that failed at 94ea775c (at most one entry mapped, zero thought renders, only the streamed row rendered per chunk).
  • Failing regression committed first at e11880ba11f81b1e6627b70b561a568703e52696: bun test src/agent-session/acpmux/conversation/ToolGroupExpanded.test.tsx fails all three command/read/search cases (13 expected, 5 shown).
  • Streaming regression committed first at f71a103c1841d072dd2c549264539323574d5377: one streamed call redraws all 13 completed siblings in command/read/search groups. At the repaired head, five output chunks render only their changed item; a thought-only update redraws no tool rows, and the open output still updates.
  • Nested regression committed first at 9e6f22e2cb9bff984fc0006de0afa299fe52f02e: real SubagentFold.reduce → views → childRows → ToolRows updates reconstructed all 13 completed siblings. The repair preserves their identities through five streamed chunks while the changed row’s open output updates. Batched views match cold replay; delayed results, hidden tools, parent group metadata, and old snapshot immutability are covered.
  • Open-thought regression committed first at f5051ac0063e22592bde18e607613b5112003545: an unchanged open thought was reparsed on each sibling tool chunk. Memoizing its pure text renderer skips that parsing while preserving the row’s settings and disclosure behavior.
  • Reused-call regression committed first at 7aed36863f51fdf8bdced9c26b6dccf48bac6dab: a new tool_call after an assistant segment overwrote the earlier row and inherited its late output. At 94ea775c9dfe9900e47c601a01345ed1eefb1a86, the two rows retain their separate commands and output; incremental views still match cold replay and preserve old snapshots. The related subagent, hidden-tool, and thought suites pass 83 tests across 10 files.
  • Previous head 6f44d164fb6115b7a880455e91b819394c1d459b passes 217 behavior/render tests, but those tests did not bound parent list mapping and thought-row hook work.
  • The new strict work-count regression intentionally fails three nested stream cases: one changed call maps all 15 displayed entries and reruns the unchanged thought row. Six all-items/direct cases still pass. This was the blocking result at 94ea775c; it passes at 67fb58a4673. bun run check, scripts/web-lint.sh, and python3 scripts/cmux-next/prepush-gate.py pass.
  • Chromium gallery verifies exact ordered IDs and positive, unclipped visible DOM rows: all 13 main commands and all 14 nested commands, including hover. Opening one body leaves the other 26 closed; closing it preserves all item rows.
  • Linux headless gallery play at 6f44d164fb6115b7a880455e91b819394c1d459b records a 77.8 ms frame during the hover fixture’s initial expansion; DOM/count/visibility checks pass. This is not fleet performance evidence. The later call-generation repair is covered by the production-path regression; the gallery has not been recaptured at the latest head.
  • Localization audit found zero new or changed message keys. verify-local.py and localize-changes encounter existing catalog debt; the untouched before checkout reports the same 45,327 parity errors.

Not run

Native app build, native tests, fleet capture, and latest-head browser recapture. The change is in the existing web transcript renderer; browser evidence uses the production React components. If a child reuses a call ID and then sends an untagged late update for its prior generation, the protocol carries no generation provenance to distinguish that update; this scoped repair does not invent one.

Changelog

Fixed: Expanding a tool group reveals every item with one click.

Proof

Reference matched: Claude transcript-tool-rows-and-task-list and Codex transcript-expanded-command-activity in cmux-design-references. The private report capture was not published.

Gallery: agent-pane.tool-group-expanded, variants expanded, nested, and hover. The same public-safe fixture before and after. Matched before/after captures use installed Chrome 153.0.8010.36. Fresh captures at 6f44d164fb6115b7a880455e91b819394c1d459b are byte-identical to the published after media:

BeforeAfter
Before: second disclosure hides earlier commandsAfter: all 13 and 14 command rows show

One command body opens independently:

One command body open, every item row remains visible

Agent session: agent session