Repository navigation
ui-react: built-in tool renderers bypass ToolCallView with a cast and a name switch #1373
Description
Part of #1345.
Context
The renderer registry is the sanctioned seam (packages/ui-sdk/src/client/renderers.ts:131-148), but the Claude, pi and generic packs cast ToolCallView to an internal ToolCall (packages/ui-react/src/components/chat/renderers/claude-tools.tsx:45, pi-tools.tsx:27, generic.tsx:20) and forward to switch (tool.name) blocks (packages/ui-react/src/components/chat/tool-views.tsx:137, :190, :245, :429). Built-ins bypass the declared seam through casts, and adding a built-in view edits a shared switch. Source inspection confirms ToolCallView already includes status, inputJson, startedAt and endedAt; the cast comment’s claim that these require the richer internal call is stale. Several shared view helpers accept internal ToolCall even though their consumed rendering fields are already public.
Approved renderer contract — 2026-10-09
A — use ToolCallView for built-in and third-party renderers. Retype shared rendering helpers to the public neutral contract and handle its optional fields honestly. Add a public field only if a source-based audit demonstrates a renderer need not already expressible by the existing shape; document its semantics and optional/history behavior. Do not expose a full internal-call accessor, arbitrary store state, approval authority or private call types through this seam.
The existing public status/inputJson/timing fields suffice for the cited cast rationale; widening is not automatically required. Keep approval/controller ownership in the host. This selects the shared public-data approach, not an unrestricted public mirror of the internal ToolCall.
Scope
- Make rendering helpers consume ToolCallView (or an explicitly narrower public subset), remove internal-call casts/imports from renderer packs, and convert each tool-name switch arm into its own registered renderer entry. Compose reusable view components without repeating a central name-dispatch switch. Keep registry precedence, backend scoping, generic shape fallbacks, input aliasing and approval/controller ownership unchanged.
Out of scope
- Visual changes to any tool card.
Acceptance criteria
- No
as unknown as ToolCallin renderer packs. -
tool-views.tsxcontains noswitch (tool.name). - Inventory every field read by built-ins/shared view helpers against ToolCallView. Existing public fields are reused; any genuinely missing public optional field has explicit semantics, fixture coverage, API report and contract documentation. No full internal accessor or store-type escape is introduced.
- A third-party renderer using public entries only compiles and runs through the real registry/timeline on nonempty fictional calls, including streamed/missing inputJson, missing history timing and terminal/error output. Built-ins pass through the same contract without assertions to internal ToolCall.
- Registry precedence/backend scoping and generic fallback remain valid. A removed per-tool registration mutation fails the corresponding real rendering assertion rather than being masked by an equivalent generic fallback.
- Actual browser renderer baselines remain unchanged at narrow and desktop widths, with representative edit/write/shell/search and streaming/error cards. Existing tests and typecheck pass; no visual redesign is authorized.
Verification
bun run test packages/ui-reactContract impact
Conditional: if the existing ToolCallView suffices, this is a behavior-preserving internal renderer refactor with a normal changeset. A demonstrated new optional public field is additive and requires API-report/contract documentation and CONTRACT: classification. No public removal or changed required-field contract is approved.
Blocked by #1380
2026-10-09: sequenced against the other code-quality issues to avoid overlapping edits; see the epic's planned-order comment.
Current decision record — 2026-10-09
Maintainer selected the shared ToolCallView approach A, with minimal source-justified public additions only. The stale cast rationale is corrected above. No policy/design question remains; remove needs: decision and add agent-ready. Keep blocked on #1380 and preserve #1345 membership. Implementation and renderer proof remain outstanding.
Activity
schlessera commentedon Oct 9, 2026
Decision (maintainer, 2026-10-09): renderer contract A
Use ToolCallView for built-in and third-party renderers. Shared helpers should accept that public shape and handle optional fields; only add fields when a source audit proves a real missing rendering need. No full internal-call accessor or exposure of approval/store authority.
Source correction: status, inputJson, startedAt and endedAt are already present in ToolCallView. The cited casts do not demonstrate that those fields require a wider contract. Retype helpers, remove internal casts, and replace name-switch dispatch with registered entries while preserving visuals and registry/fallback behavior.
The body now requires public-only third-party runtime fixtures, field inventory and meaningful registry/browser/mutation proof. No decision remains: remove
needs: decision, addagent-ready, and retain the #1380 blocker. Contract additions are conditional on genuinely missing public fields.schlessera commentedon Oct 9, 2026
Unblocked: #1380 is closed, so
scripts/sync-project.tsis removing theblockedlabel.