shadcn.io is not affiliated with official shadcn/ui
A tool call shows up as one line: what the agent ran or read, then whether it worked, with the output a click away.
Use it for each call an agent makes, such as running a command or reading a file, in the same column as the reply. The line is a Trace line, plain while the call runs and expandable once its output arrives, and the output sits in a Code Block, so there is no Tool component to install in React. Say the call in the title, "Ran git status --short", not just the tool's name, and don't hide a failed call: it keeps the error status and its output.
Installation
Tool is a pattern. The command adds the parts it's built from, Trace and Code Block, and the code to copy is the demo's.
Anatomy

Trace
└── TraceCollapsible (one per call with output)
├── TraceCollapsibleTrigger
│ └── TraceLine (status from the call)
│ ├── TraceIcon
│ └── TraceTitle
└── TraceCollapsibleContent
└── CodeBlock (size="sm")
└── CodeBlockContent (the output)A call that is still running, or that was denied, is a TraceLine on its own: there is nothing to open yet.
Usage
import { TerminalIcon } from "lucide-react";
import {
CodeBlock, CodeBlockContent,
} from "@/components/ui/code-block";
import {
Trace, TraceCollapsible, TraceCollapsibleContent,
TraceCollapsibleTrigger, TraceIcon, TraceLine, TraceTitle,
} from "@/components/ui/trace";export function ToolCall(props: { output: string }) {
return (
<Trace>
<TraceCollapsible>
<TraceCollapsibleTrigger>
<TraceLine>
<TraceIcon>
<TerminalIcon aria-hidden="true" />
</TraceIcon>
<TraceTitle>Ran git status --short</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<CodeBlock size="sm">
<CodeBlockContent className="max-h-64" wrap>
{props.output}
</CodeBlockContent>
</CodeBlock>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
);
}Demos
States
A call that ran, one that failed, one the reader denied and one still running. Trace doesn't read tool calls: map each call's state to a status and a title, as in the table, with the AI SDK's tool part states.
| Tool part state | status | Title |
|---|---|---|
input-streaming, input-available | "running" while the chat streams | Running … |
input-streaming, input-available after the chat stops | "error" | Failed to run … |
approval-requested | none | Not a line: an Item outside Trace, with Approve and Deny |
approval-responded | "running" if approved, none if not | Running … if approved, Denied … if not |
output-available | none | Ran … |
output-error | "error" | Failed to run … |
output-denied | none | Denied … |
A call is a plain TraceLine while it runs and a TraceCollapsible once its output arrives. A tool that runs on the client stays input-available after the chat stops until you add its output: treat it as waiting, not failed.
Output in a Sheet
A file read whose content opens in a Sheet from the side, with line numbers: press Send message to play it. For long output, put the TraceLine in the sheet's trigger instead of a collapsible, so the transcript keeps its place while the reader looks. The same works with a Dialog, as in Trace.
Approval
A command that waits for the reader before it runs: press Send message, then Approve or Deny. A pending approval is a question for the reader, not a step the agent took, so it sits outside Trace as an Item with the two buttons. Once the reader answers, the call goes back to being a line: Running, then Ran, or Denied. For a question with more to say, use Confirmation.
AI SDK
Render each tool part as a line, from its state, as the States table says.
function ToolLine({ part, streaming }: { part: ToolPart; streaming: boolean }) {
const command = part.input?.command;
if (
part.state === "output-denied" ||
(part.state === "approval-responded" && !part.approval.approved)
) {
return (
<TraceLine>
<TraceIcon>
<TerminalIcon aria-hidden="true" />
</TraceIcon>
<TraceTitle>Denied {command}</TraceTitle>
</TraceLine>
);
}
if (part.state === "output-available" || part.state === "output-error") {
const failed = part.state === "output-error";
return (
<TraceCollapsible>
<TraceCollapsibleTrigger>
<TraceLine status={failed ? "error" : undefined}>
<TraceIcon>
<TerminalIcon aria-hidden="true" />
</TraceIcon>
<TraceTitle>
{failed ? "Failed to run" : "Ran"} {command}
</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<CodeBlock size="sm">
<CodeBlockContent className="max-h-64" wrap>
{failed ? part.errorText : part.output.stdout}
</CodeBlockContent>
</CodeBlock>
</TraceCollapsibleContent>
</TraceCollapsible>
);
}
return (
<TraceLine status={streaming ? "running" : "error"}>
<TraceIcon>
<TerminalIcon aria-hidden="true" />
</TraceIcon>
<TraceTitle>
{streaming ? "Running" : "Failed to run"} {command}
</TraceTitle>
</TraceLine>
);
}
<Trace>
{message.parts
.filter((part) => part.type === "tool-runCommand")
.map((part) => (
<ToolLine key={part.toolCallId} part={part} streaming={streaming} />
))}
</Trace>A call that needs approval is an Item until the reader answers it.
if (part.state === "approval-requested") {
return (
<Item size="sm" variant="outline">
<ItemMedia variant="icon">
<TerminalIcon aria-hidden="true" />
</ItemMedia>
<ItemContent>
<ItemTitle>Run {part.input.command}?</ItemTitle>
</ItemContent>
<ItemActions>
<Button
onClick={() =>
addToolApprovalResponse({ id: part.approval.id, approved: false })
}
size="sm"
variant="outline"
>
Deny
</Button>
<Button
onClick={() =>
addToolApprovalResponse({ id: part.approval.id, approved: true })
}
size="sm"
>
Approve
</Button>
</ItemActions>
</Item>
);
}Pass sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses to useChat, so the chat goes on once every approval has an answer.
Migrating from AI SDK Elements
If you added Tool from AI SDK Elementscomponents/ai-elements/tool.tsx wrapper. Paste this prompt into your coding agent to rebuild it on Trace and Code Block. Every export keeps its name and props, so the code that uses it keeps working.
Migrate components/ai-elements/tool.tsx from AI SDK Elements to the shadcn.io Trace and Code Block components.
Keep every export, with the same name and props, so the code that imports it keeps working. Only change how each part renders.
1. Read https://kit.dev/ui/radix/tool.md and build Tool the same way. If the project hasn't got Trace and Code Block, run the command in its Installation section.
2. Rebuild each part:
- Import CodeBlock and CodeBlockContent from @/components/ui/code-block, not from ./code-block.
- Tool renders a Trace with a TraceCollapsible.
- ToolHeader renders a TraceCollapsibleTrigger with a TraceLine, a TraceIcon and a TraceTitle, instead of a status Badge. Map state to the status and title: "running" and "Running …" while the input streams or is available, no status and "Ran …" for output-available, "error" and "Failed to run …" for output-error, and "Denied …" for output-denied.
- ToolContent renders a TraceCollapsibleContent.
- ToolInput and ToolOutput render a CodeBlock with size="sm" and a CodeBlockContent. ToolOutput shows errorText when the tool failed.
- Keep getStatusBadge and ToolPart exported, even if nothing renders the badge.
3. Remove the imports the file no longer uses.
4. Run the type checker and fix any errors.FAQ
What does installing Tool add in React?
Trace and Code Block, the two parts a tool call is built from, with the Collapsible a line opens on. Tool is a pattern, so no tool file lands in your ui folder: copy the demo's code into your own component and map your tools' calls onto it. The command uses the shadcn CLI.
Why is a running call a plain line and not one that expands?
It has nothing to show yet. The line reads Running with a shimmer until the output arrives, then becomes a TraceCollapsible that opens on the output. A line that opens onto nothing reads as broken.
How should a failed call look?
Keep it in the list with status="error", a title that says so, "Failed to run pnpm dev", and the error as its output. The demo opens the failed call once the reply is done, so the reader sees what went wrong without hunting for it. A call still waiting when the chat stops has failed too, unless it runs on the client and waits for its output.
How do I ask the reader before a tool runs?
Show an Item with the command and two buttons, Deny and Approve, outside the Trace, as the Approval demo does. With the AI SDK, set needsApproval on the tool and answer with addToolApprovalResponse; once the reader answers, the call is a line again. For a question that needs more words, use Confirmation.
What if the output is long?
Cap it: the demos give CodeBlockContent a max-h-64, so it scrolls inside the line. For a whole file, open it in a Sheet from the line instead, as the Output in a Sheet demo does, with line numbers.
Can the reader open a call with the keyboard?
Yes. A call with output is a button: Tab reaches it, and Enter or Space opens or closes it. A running or denied call is a plain line, so Tab skips it.
Is this the same as AI SDK Elements' Tool?
It does the same job without a wrapper: Tool was a Collapsible with a status Badge in its header and its own code block, and here a call is a Trace line whose status and title say the state, over a Code Block. The Migrating from AI SDK Elements section has a prompt that rebuilds the old file on these parts and keeps its exports.
API Reference
Tool has no props of its own. Each call is a Trace line or collapsible, and its output a Code Block with size="sm". The approval is an Item with two Buttons.












