Tool
One call an agent makes to a tool: what it called, with which arguments, what came back, and the approval step when a person has to say yes. Collapsed, a call is one quiet line; open, the arguments and the result sit on sheets set into the frame.
Installation#
Usage#
import {
Tool,
ToolHeader,
ToolIcon,
ToolName,
ToolSummary,
ToolMeta,
ToolStatus,
ToolContent,
ToolInput,
ToolOutput,
ToolApproval,
ToolApprovalMessage,
ToolApprovalActions,
} from "@/components/ui/tool"
export function Example({ call }) {
return (
<Tool status={call.status}>
<ToolHeader>
<ToolIcon><MagnifyingGlass weight="bold" /></ToolIcon>
<ToolName>Searched the web</ToolName>
<ToolSummary>{call.input.query}</ToolSummary>
<ToolMeta>{call.duration}</ToolMeta>
</ToolHeader>
<ToolContent>
<ToolInput value={call.input} />
<ToolOutput value={call.output ?? call.error} />
</ToolContent>
</Tool>
)
}Anatomy#
Three blocks. A header line that is also the disclosure: the status column, the name, a summary of the arguments and a meta read-out. The content it opens: an input sheet and an output sheet. And an approval row that exists only while the call is waiting for a person.
import {
Tool,
ToolHeader,
ToolIcon,
ToolName,
ToolSummary,
ToolStatus,
ToolMeta,
ToolContent,
ToolInput,
ToolOutput,
ToolApproval,
ToolApprovalMessage,
ToolApprovalActions,
} from "@/components/ui/tool"
<Tool>
<ToolHeader>
<ToolIcon />
<ToolName />
<ToolSummary />
<ToolStatus />
<ToolMeta />
</ToolHeader>
<ToolContent>
<ToolInput />
<ToolOutput />
</ToolContent>
<ToolApproval>
<ToolApprovalMessage />
<ToolApprovalActions />
</ToolApproval>
</Tool>States#
Six states, one per stage an agent SDK reports. They live in the 16px column at the head of the line: the tool's own glyph at rest (at half strength while the model is still writing the arguments), a spinner while it runs, a warning when it threw, a prohibit sign when someone turned it down. While pending or running, the name carries the same ink sweep as Chat's “Thinking…”. A finished call gets no green tick: a column of checks says nothing a finished line doesn't.
Status chip#
ToolStatus is an optional Badge worded and toned from the root's status, so a label and a state can never drift apart. Leave it out in a chat, where the status column already says it; reach for it in an audit log or an activity feed, where each call has to announce itself.
Input and output#
Pass the arguments and the result as they come from your SDK. An object is pretty-printed and highlighted as JSON, with copy on hover; a string is shown as plain text, so a shell's stdout keeps its own layout. A long JSON result clamps at 200px behind a Show more, so a 400-line response never unrolls at whoever opens the call.
Errors#
Mark the call error and the output sheet reads its value as the message: labelled “Error”, in the destructive ink, wrapped rather than scrolled sideways, and announced as an alert. Nothing else on the card turns red; the warning in the status column is enough to find it in a list.
Approval#
A call that needs a person mounts open, so you never have to expand a card to learn what you are being asked to allow, and ToolApproval sits outside the collapsible content, so the decision can't be folded away. It renders nothing in any other status: leave it in the markup and let the status decide. The pair is the house rule, ghost on the left and primary on the right.
Editing the arguments#
ToolInput editable turns the same sheet into an editor: no new field, no frame, the same mono type, so nothing moves until you type. Pass the raw text back as value, validate it yourself and set invalid while it does not parse; this demo holds Send back until the JSON is valid again.
In a chat#
Inside a Chat turn or an AI Panel, use variant="plain". It drops the frame, so the calls read as a few quiet lines of the answer rather than as cards inside a card, and the sheets indent to the name's axis when opened.
Density#
Like Card and Plan, Tool reads the density context (or a density prop). It tunes the header band (44px or 40px) and the sheets' padding; the labels move with the code so “Input” always starts on the listing's left edge. The 8px gutter between frame and sheet is fixed in both, because it is what keeps the sheet's corner concentric with the frame's.
API reference#
Tool
The call, and the context every part reads. A Radix Collapsible.Root, so it forwards its props. Every part accepts className, merged last, and every part but the header is droppable.
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "pending" | "approval" | "running" | "done" | "error" | "rejected" | "done" | Where the call is. Drives the status column, the name's sweep, ToolOutput, ToolApproval, ToolStatus and aria-busy (pending or running). |
| variant | "card" | "plain" | "card" | card is the tinted frame with the sheets set into it; plain drops the frame for a Chat turn or an AI Panel. |
| density | "comfortable" | "compact" | - | Header band height and sheet padding. Inherits the nearest DensityProvider (compact with none). |
| open | boolean | - | Controlled open state of the arguments and result. |
| defaultOpen | boolean | - | Initial open state. Collapsed by default, except a call that mounts in approval. |
| onOpenChange | (open: boolean) => void | - | Called when the call opens or closes. |
| Data attribute | Description |
|---|---|
| data-status | The call's status. |
| data-state | open or closed. |
ToolHeader
The line you see collapsed, and the disclosure trigger. Adds the caret and an sr-only status word. Forwards Radix Collapsible.Trigger props.
ToolIcon
The 16px status column. Pass the tool's glyph; it cross-fades to a spinner while running, a warning on error, a prohibit sign on rejected. aria-hidden. Renders a <span>.
ToolName
What was called. The line's one piece of ink; sweeps while pending or running. Renders a <span>.
ToolSummary
The arguments in a few words. Muted, and the first thing to truncate. Renders a <span>.
ToolMeta
Trailing read-out (duration, a count) in tabular-nums. Renders a <span>.
ToolStatus
An optional chip worded and toned from the root's status. Forwards Badge props.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | BadgeProps["variant"] | - | Overrides the tone the status picks. |
| children | React.ReactNode | - | Overrides the wording (Running, Needs approval, and so on). |
ToolContent
What the header hides: the input and output sheets. Tweens its height. Forwards Radix Collapsible.Content props.
ToolInput
The arguments, on their own sheet. Forwards native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | unknown | - | An object is pretty-printed and highlighted as JSON; a string is shown as plain text. |
| label | React.ReactNode | "Input" | The sheet's label. |
| editable | boolean | false | Turns the listing into an inline editor in the same mono type. |
| onValueChange | (value: string) => void | - | Called with the raw text on every edit. |
| invalid | boolean | false | Rings the sheet in the destructive tone and sets aria-invalid. |
| children | React.ReactNode | - | Your own content instead of the listing; the sheet and label stay. |
| Data attribute | Description |
|---|---|
| data-invalid | Present when invalid. |
ToolOutput
What came back. Renders nothing while the call is pending, approval or running. Forwards native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | unknown | - | The result: JSON for an object, plain text for a string, the message for an error. |
| label | React.ReactNode | "Output" | The sheet's label; Error when the call failed. |
| error | boolean | - | Show value as an error. Defaults to status === "error". |
| collapsible | boolean | true | Clamp a long JSON result behind CodeSnippet's Show more. |
| collapsedHeight | number | 200 | The clamp, in px. |
| children | React.ReactNode | - | Your own rendering of the result; the sheet and label stay. |
| Data attribute | Description |
|---|---|
| data-error | Present when the output is an error. |
ToolApproval
The human-in-the-loop row. Renders nothing unless status is approval, and sits outside ToolContent so it can never be folded away. A role="group" <div>.
ToolApprovalMessage
What the person is allowing. <strong> steps a name up to full ink. Forwards native <p> props.
ToolApprovalActions
The decision: a ghost Reject, then the primary Approve. Forwards native <div> props.