Skip to content

inspector: render tool calls and results with AI Elements - #31

Open
Mark-Life wants to merge 1 commit into
mainfrom
atm/task-019fe01b-cde2-77c7-9b6e-f2e77f1352b4
Open

inspector: render tool calls and results with AI Elements#31
Mark-Life wants to merge 1 commit into
mainfrom
atm/task-019fe01b-cde2-77c7-9b6e-f2e77f1352b4

Conversation

@Mark-Life

Copy link
Copy Markdown
Owner

Problem:

Every transcript event in the session history rendered as the same generic disclosure row, so a tool call, its result, and an attachment were indistinguishable until expanded. A result row was labelled tool-result and nothing more: the transcript records only tool_use_id on the result block, never the tool's name, so e.toolName ?? e.kind had nothing to fall back on. Nothing surfaced whether a call failed, or whether it ever returned at all.

Expanding a call showed one of its arguments — displayBody unwrapped code or command and dropped the rest.

Solution:

Vendor AI Elements' Tool into @workspace/ui/components/ai-elements/, beside the code-block already installed there, and use it for tool-call and tool-result. Upstream is built on the AI SDK's streaming message-parts model, which we have none of, so this copy takes code + language for the shared highlighter instead of an unknown payload, and replaces ToolUIPart["state"] with a ToolState a finished transcript can justify: completed, error, or unanswered for a call whose result the transcript never recorded. ToolHeader gains icon, lead and children slots so the row keeps its turn number, sidechain badge, summary and token estimate.

Calls and results are paired by toolUseId, so a result names the tool it came from and a call shows the outcome its result recorded. Unwrapping a payload arg now puts the remaining arguments in their own pane rather than hiding them, and the collapsed summary is the command itself instead of the JSON envelope.

One row per event either way. Pairing a call with its result into a single card would reorder the transcript and merge two token figures, which a forensics view cannot afford. Search, the type filter, expand-all, the position-keyed collapse state, the dumb-zone divider and the subagent cards are untouched. assistant-thinking stays a plain row — the AI Elements reasoning block pulls in streamdown, mermaid and math to render content our transcripts do not store.


Security Impact:

The tool rows render TimelineEvent.body and preview, which redactSession has already rewritten server-side. No new field is read, so the reveal toggle remains the only path to raw bytes; a test pins that.


Testing:

bun test (full workspace), bun run typecheck, bunx ultracite check, bun run --filter=inspector build:

 218 pass
 0 fail
 629 expect() calls
Ran 218 tests across 31 files.

apps/inspector/test/tool-event.test.ts drives the new shaping through the real parser and the shared Claude fixture: name-borrowing across the pair, all three outcomes, payload unwrapping, and that nothing rendered from a redacted session contains the fixture's sk-ant- key while the raw session still does.

Two caveats on what was not run. bun test needs CLAUDE_CONFIG_DIR unset in this container or two unrelated AgentRegistry tests fail. peektrace#build:binary fails on a missing msgpackr-extract native module — verified identical on a stashed clean tree, so it predates this branch.

The component is not rendered under test: @workspace/ui resolves React 19.2.8 while inspector resolves 19.2.7, so hooks throw outside Vite, which dedupes via @vitejs/plugin-react. The shaping logic lives in lib/tool-event.ts for that reason; typecheck and the Vite build cover the JSX. No screenshot — no browser can launch in this container.

🤖 Generated with Claude Code

The transcript rendered every event as the same generic disclosure row, so a
tool call and its result were indistinguishable at a glance and a result never
said which tool produced it — the transcript only records `tool_use_id` on the
result, never the name.

Vendor AI Elements' `Tool` component into `@workspace/ui`, next to the
`code-block` already installed there, and use it for `tool-call` and
`tool-result`. Upstream is built on the AI SDK's streaming message-parts model;
this copy replaces `ToolUIPart["state"]` with a `ToolState` a finished
transcript can actually justify, and takes `code` + `language` for the shared
highlighter rather than an `unknown` payload.

The pairing, unwrapping and outcome logic lives in `lib/tool-event.ts` so it is
pure and testable — `@workspace/ui` and `inspector` resolve different copies of
React, so the component itself cannot be rendered outside Vite.

One row per event either way: pairing a call with its result into one card
would reorder the transcript and merge two token figures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant