Skip to content

Render the current Claude Code tool surface in playback#45

Open
chigichan24 wants to merge 8 commits into
mainfrom
feature/21-toolcallblock-update
Open

Render the current Claude Code tool surface in playback#45
chigichan24 wants to merge 8 commits into
mainfrom
feature/21-toolcallblock-update

Conversation

@chigichan24
Copy link
Copy Markdown
Owner

Summary

  • Add dedicated renderers in ToolCallBlock for the modern Claude Code tool set so playback no longer collapses into raw JSON dumps. Per-tool helpers are extracted into ToolCallRenderers / toolCallHelpers to keep the main component readable.
  • High: TaskCreate, TaskUpdate, AskUserQuestion, Skill, WebFetch, WebSearch, NotebookEdit, plus MultiEdit and Task as aliases of the existing Edit and Agent renderers.
  • Mid: ExitPlanMode (plan body + allowed prompts), EnterPlanMode, EnterWorktree / ExitWorktree, Monitor, ScheduleWakeup, ToolSearch.
  • Low: a single generic mcp__<server>__<method> formatter that shows server, method and 1–2 key args via pickKeyInputArgs.
  • Improve the unknown-tool fallback so it shows 1–2 high-signal scalar fields (url, file_path, command, query, …) instead of dumping JSON.
  • Add scripts/aggregate-tool-names.ts: one-shot histogram of tool_use blocks across ~/.claude/projects/ used to derive the priority list. Running it locally on this corpus reproduces the order the issue asked for (TaskUpdate / ToolSearch / WebFetch / WebSearch / ExitPlanMode / TaskCreate / AskUserQuestion at the top of the new-tool tail).
  • Co-locate badge colors and component primitives (task / question / web / plan / notebook / mcp / kv list) in ToolCallBlock.css.

Notes / assumptions:

  • No React Testing Library is installed in this repo, so tests target the pure helpers (getToolCategory, parseMcpToolName, summarizeValue, pickKeyInputArgs, truncate) rather than rendered output. Visual review of the renderers is intentionally left to manual playback.
  • Monitor and ScheduleWakeup did not appear in the local corpus often enough to firmly pin field shapes, so their renderers fall back to GenericInputRenderer if none of the expected fields (command/spec, at/when/after, …) are present.
  • EnterPlanMode input is usually empty in the corpus, so the renderer prints a short status line in that case.
  • MultiEdit is currently rendered with the Edit layout. Switching to a list-of-edits view can be done in a follow-up once we see real MultiEdit payloads.

Test plan

  • npm run lint (only pre-existing PlaybackSidePanel warnings remain)
  • npm test (18 files, 230 tests, including the new toolCallHelpers suite)
  • npx tsc --noEmit -p tsconfig.app.json
  • npx tsc --noEmit -p tsconfig.node.json
  • npx tsx scripts/aggregate-tool-names.ts --limit 25 reproduces the issue's priority list
  • Manual playback review: confirm TaskCreate/Update, AskUserQuestion, WebFetch/WebSearch, ExitPlanMode, Skill, ToolSearch, and an MCP tool render cleanly in the side drawer

Closes #21

🤖 Generated with Claude Code

chigichan24 and others added 8 commits April 26, 2026 02:05
Adds dedicated renderers in ToolCallBlock for the modern Claude Code tool
set so playback no longer collapses into raw JSON dumps. Per-tool helpers
were extracted into ToolCallRenderers / toolCallHelpers to keep the main
component readable.

New / updated coverage (priority follows the corpus histogram emitted by
scripts/aggregate-tool-names.ts):

- High: TaskCreate, TaskUpdate, AskUserQuestion, Skill, WebFetch,
  WebSearch, NotebookEdit, MultiEdit (alias of Edit), Task (alias of
  Agent)
- Mid: ExitPlanMode (with plan + allowed prompts), EnterPlanMode,
  EnterWorktree / ExitWorktree, Monitor, ScheduleWakeup, ToolSearch
- Low: a single generic mcp__server__method formatter showing server,
  method, and 1–2 key args
- Fallback: unknown tools now show 1–2 high-signal scalar args via
  pickKeyInputArgs instead of dumping the whole input as JSON

Also adds:

- scripts/aggregate-tool-names.ts: one-shot histogram of tool_use blocks
  across ~/.claude/projects/, used to derive the priority list
- Pure helpers (getToolCategory, parseMcpToolName, summarizeValue,
  pickKeyInputArgs, truncate) with unit tests

Closes #21

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous code cast q.options items to {label?: string; description?: string}
without runtime validation. Since the input arrives from JSONL data of unknown
shape, accessing opt.label / opt.description on a non-object item would throw.
Filter the array to objects up front and validate field types per option.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
input.command / input.spec may contain a structured object spec. Calling
asString on it produced the unhelpful "[object Object]" rendering. Use a
scalar-only guard so structured payloads fall through to the generic key/value
renderer instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
q.header and q.question were typed as optional strings but never validated at
runtime. A non-string header (e.g. null cleared via JSON, or an unexpected
object) would either render incorrectly or, in the case of question, throw
inside truncate() which calls String.prototype.slice. Coerce to null when the
field is not a string before rendering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
p.tool and p.prompt were rendered without runtime validation. A non-string
prompt would cause truncate() to call slice() on a non-string and throw, and
a non-string tool would render via React's default coercion. Coerce both to
null when they aren't strings before rendering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
asString previously fell back to String(value) for non-string values, which
produced "[object Object]" for objects and "1,2,3" for arrays whenever the
JSONL payload had an unexpected shape. Restrict it to strings, numbers, and
booleans so structured payloads return null and downstream renderers can opt
into a structured fallback (e.g. GenericInputRenderer) instead. Removes the
local asScalarString helper that was added earlier for the same reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
allowed_domains / blocked_domains were rendered via map(String).join(', '),
which produces "[object Object]" for any non-string entry that slips through
the JSONL payload. Filter to non-empty strings so display stays clean and
sliced counts match what is actually shown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The status string from JSONL was concatenated into a CSS class modifier
without any validation. A status containing whitespace (e.g. "in progress")
would split into two class tokens, dropping the intended modifier and adding
a stray class. Restrict the modifier to safe characters and fall back to
"unknown" otherwise.

Co-Authored-By: Claude Opus 4.7 (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.

Update ToolCallBlock to render the current Claude Code tool surface

1 participant