Skip to content
AgentsTool callMenu

Tool call

Shows an agent tool call through its whole life: streaming input, approval, running, output, failure, denial and cancellation.

A coding agent reads a file, asks to edit it, then runs the tests.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/tool-call.json

Usage

tsx
import { ToolCall } from "@/components/ui/chatcn/tool-call"

<ToolCall
  name="edit_file"
  state="approval-requested"
  input={{ path: "src/app/page.tsx" }}
  approval={{ id: "approval-1", reason: "Limit the runs query to 20." }}
  onApprove={(id, scope) => approve(id, scope)}
  onDeny={(id, reason) => deny(id, reason)}
/>

Pass a tool part straight from AI SDK or the chatcn contract with toolCallPropsFromPart. It reads the name, state, partial input, output, error and approval for you.

tsx
{message.parts.filter(isToolPart).map((part) => (
  <ToolCall key={part.toolCallId} {...toolCallPropsFromPart(part)} />
))}

Examples

Every state

All eight display states side by side.

Approval

Allow runs the tool once. The switch grants it for good. Deny opens a field so the user can tell the agent what to do instead, which is usually more useful than a bare no.

Allow once, always allow, or deny with a note for the agent.

Streaming input

While the model writes the arguments, inputText is parsed tolerantly, so half a JSON object still renders. The square caret marks where the model is writing.

Partial JSON renders as it arrives, with the streaming caret.

Inline

The quiet variant for long agent traces inside a message.

Error and retry

A failed command with the error and a retry action.

States

StateShowsOpens by itself
input-streamingSpinner, partial input with the streaming caretNo
input-availableSpinner, elapsed time after one secondNo
approval-requestedHand, the reason and the approval actionsYes
approval-respondedSpinner while the approved tool runs, or DeniedNo
output-availableThe tool icon and its durationNo
output-errorRed cross, Failed, the error and RetryYes
output-deniedBan, Denied and the reason givenNo
cancelledStop, CancelledNo

Props

PropTypeNotes
namestringTool name. Picks the icon and the default title
stateToolDisplayStateOne of the eight states above
titleReactNodeDefaults to a readable name
detailReactNodeMono context. Defaults to path, query, command or url from the input
iconReactNodeOverride the tool icon
inputunknownParsed input
inputTextstringRaw input while streaming
outputunknownStrings render as text, objects as JSON, elements as is
errorTextReactNodeShown on output-error
approval{ id, reason? }Shown on approval-requested
decision{ approved, scope?, reason? }The user's answer, shown afterwards
onApprove(id, scope) => voidscope is once or always
onDeny(id, reason?) => voidreason is the note the user typed
onRetry() => voidShows Retry on failures
durationMs, startedAtnumberOptional. Measured automatically when omitted
variant"card" | "inline"Inline is for long traces inside a message
open, defaultOpen, onOpenChangeControlled or uncontrolled

Keyboard

KeyAction
Enter, SpaceExpand or collapse the details
TabMove between the header, copy buttons and approval actions
EscLeave the deny note without denying
Enter in the deny noteDeny with the note

Accessibility

  • The header is a real button with aria-expanded, from Base UI Collapsible.
  • The status label is a polite live region, so screen readers hear Needs approval, Failed or the duration without hearing every token.
  • Always allow is a switch with aria-checked. Copy buttons announce Copied.
  • Failure is never shown by color alone: the red cross sits next to the word Failed.

Motion

  • The status glyph swaps with a 150 ms blur crossfade, so running to done reads as one object changing.
  • Details open with a 200 ms height and opacity transition. While open, the panel grows with streamed content without animating.
  • The chevron turns over 200 ms. Under reduced motion, everything snaps.