Tool call
Shows an agent tool call through its whole life: streaming input, approval, running, output, failure, denial and cancellation.
On this page
A coding agent reads a file, asks to edit it, then runs the tests.
Installation
Usage
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.
{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
| State | Shows | Opens by itself |
|---|---|---|
input-streaming | Spinner, partial input with the streaming caret | No |
input-available | Spinner, elapsed time after one second | No |
approval-requested | Hand, the reason and the approval actions | Yes |
approval-responded | Spinner while the approved tool runs, or Denied | No |
output-available | The tool icon and its duration | No |
output-error | Red cross, Failed, the error and Retry | Yes |
output-denied | Ban, Denied and the reason given | No |
cancelled | Stop, Cancelled | No |
Props
| Prop | Type | Notes |
|---|---|---|
name | string | Tool name. Picks the icon and the default title |
state | ToolDisplayState | One of the eight states above |
title | ReactNode | Defaults to a readable name |
detail | ReactNode | Mono context. Defaults to path, query, command or url from the input |
icon | ReactNode | Override the tool icon |
input | unknown | Parsed input |
inputText | string | Raw input while streaming |
output | unknown | Strings render as text, objects as JSON, elements as is |
errorText | ReactNode | Shown on output-error |
approval | { id, reason? } | Shown on approval-requested |
decision | { approved, scope?, reason? } | The user's answer, shown afterwards |
onApprove | (id, scope) => void | scope is once or always |
onDeny | (id, reason?) => void | reason is the note the user typed |
onRetry | () => void | Shows Retry on failures |
durationMs, startedAt | number | Optional. Measured automatically when omitted |
variant | "card" | "inline" | Inline is for long traces inside a message |
open, defaultOpen, onOpenChange | Controlled or uncontrolled |
Keyboard
| Key | Action |
|---|---|
Enter, Space | Expand or collapse the details |
Tab | Move between the header, copy buttons and approval actions |
Esc | Leave the deny note without denying |
Enter in the deny note | Deny 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
switchwitharia-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.