refactor(ui): render a tool call and its result as one collapsible card - #32
Merged
Merged
Conversation
The template drew a call and its result as two full-width cards, because the wire delivers them as two messages — the call on an AI message, the result as its own `tool` message. One operation now renders as one card. `buildThreadItems()` pairs them by `tool_call_id` and groups consecutive calls. Pairing is never by adjacency: results arrive when they resolve, and a call paused at the approval gate never gets one at all. Collapsed by default. The header names a single call with its own gist (`run_sql · 37 rows`) and falls back to `N tools called` for a run of them, since no summary spans differently-shaped payloads. Two things are never collapsed, because hiding them would hide the product: a gated call's ARGUMENTS — you cannot approve what you cannot read — and a CHART, which is the answer rather than a detail of one, and which no longer carries a JSON receipt underneath it. `summarizeResult` now takes the tool name: `ok: true` read as "written" for every tool, so read-only `load_skill` claimed a write it never made. Drops the global hide-tools toggle — its state, its context entry and its button — now that each card collapses on its own. Two fixes found while building it. Dedup MERGES repeated messages rather than discarding them: the pending-approval marker arrives as a chunk bearing the calling message's id, so dropping it threw the gate away. And the cards are direct children of the list now, where nothing constrained their width — Radix renders its scroll viewport as `display: table`, which sizes to its widest content, so a wide payload stretched the whole thread column instead of scrolling in its own box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The tool call / result rendering was inherited from the template and had three problems: a call and its result were drawn as two separate full-width cards, most tools showed arguments and payloads nobody needs to read, and
render_chartdrew its chart with a JSON receipt stuck underneath it.One card per operation
The wire delivers a call and its result as two messages — the call on an AI message, the result as its own
toolmessage — which is why the UI drew two.buildThreadItems()(src/services/toolActivity.ts) pairs them bytool_call_idand groups consecutive calls into one card.Pairing is by id, never by adjacency. Results arrive when they resolve, and a call paused at the approval gate never gets a result at all — adjacency would look right in the common case and break exactly where it matters.
Collapsed by default, one toggle to open.
The header label
load_skill · ok,run_sql · 37 rowsN tools calledCounting is the only honest claim across a run: the payloads have different shapes and no summary spans them.
While wiring this up I found
summarizeResultmapped anyok: trueto "written", so read-onlyload_skillclaimed a side effect it never had. It now takes the tool name and says "written" only for a mutating tool. In a finance agent that word is load-bearing.The two exceptions
render_chart · …above its own chart.hide toolsremovedThe button, the
hideToolMessagesstate, andtoggleToolMessages— not just the UI. Per-card collapsing replaced it.Two fixes found while building this
Dedup merges rather than discards. The pending-approval marker (
pendingToolCallIds, from #31) arrives as a chunk bearing the calling message's id. The first version of the dedup dropped it, silently throwing the approval gate away. Repeats are now folded into the message already kept. Caught by a test, not by hand.Thread column width. Tool cards used to sit inside
AIMessage'smin-w-0column; as direct children of the list nothing constrained them. Root cause was Radix rendering its scroll viewport asdisplay: table, which sizes to its widest content — so a wide JSON payload stretched the whole column instead of scrolling in its own box. Fixed scoped toThread.tsx; the sharedui/scroll-area.tsxprimitive is untouched. Measured: expanded card right edge 1040 → 884, matching collapsed exactly.Verification
toolActivity.test.ts),tsc --noEmitclean, production build succeeds, prettier clean.tool_callblocks, so an array-length check counted a calls-only message as text and split the group. Both shapes are now pinned by tests.set_config+log_expense) paused together → both gated with arguments shown → approved → both ran, zero stale pending chunks, row written.mydb_dev:5544, port 3101). Production untouched; no rows deleted.Net: -421 lines, +782 (the additions are mostly the new tests and the extracted pairing module;
ToolCallDisplay.tsxandToolMessage.tsxare deleted).🤖 Generated with Claude Code