Skip to content

ui-react: built-in tool renderers bypass ToolCallView with a cast and a name switch #1373

Open

Description

@schlessera

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 ToolCall in renderer packs.
  • tool-views.tsx contains no switch (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-react

Contract 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

  1. added
    type: refactorStructure changes; behaviour does not. No changeset-visible effect.
    area: ui-sdkWire protocol, runtime schemas, the server and client SDK surfaces.
    area: ui-reactReact chat/files/voice components, stores, WebSocket transport.
    priority: p2Wanted, not scheduled. Pulled in when a milestone has room.
    needs: decisionBlocked on a maintainer or designer ruling, not on code.
    contractTouches the integration contract: CONTRACT: commit, contract doc updated in the same commit.
    on Oct 9, 2026
  2. added
    blockedWaiting on another issue or an upstream dependency. Say which in the body.
    agent-readyContext and acceptance criteria are complete enough for an agent to take this unattended.
    and removed
    needs: decisionBlocked on a maintainer or designer ruling, not on code.
    on Oct 9, 2026
  3. schlessera commented on Oct 9, 2026

    @schlessera
    OwnerAuthor

    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, add agent-ready, and retain the #1380 blocker. Contract additions are conditional on genuinely missing public fields.

  4. schlessera commented on Oct 9, 2026

    @schlessera
    OwnerAuthor

    Unblocked: #1380 is closed, so scripts/sync-project.ts is removing the blocked label.

  5. removed
    blockedWaiting on another issue or an upstream dependency. Say which in the body.
    on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-readyContext and acceptance criteria are complete enough for an agent to take this unattended.area: ui-reactReact chat/files/voice components, stores, WebSocket transport.area: ui-sdkWire protocol, runtime schemas, the server and client SDK surfaces.contractTouches the integration contract: CONTRACT: commit, contract doc updated in the same commit.priority: p2Wanted, not scheduled. Pulled in when a milestone has room.type: refactorStructure changes; behaviour does not. No changeset-visible effect.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions