Shows one agent tool call: the tool name, a pending, running, success or error status with icon and text, duration, and collapsible pretty-printed input and result.
{
"query": "install a component into a src/ project",
"limit": 3
}[
{
"title": "Installation",
"url": "/docs/installation"
},
{
"title": "CLI & registry",
"url": "/docs/registry"
}
]import { ToolCallCard } from "@/components/ballmac/tool-call-card"
export default function ToolCallCardDemo() {
return (
<div className="w-full max-w-xl">
<ToolCallCard
name="search_docs"
description="Searched the documentation"
status="success"
duration={842}
defaultOpen
input={{ query: "install a component into a src/ project", limit: 3 }}
result={[
{ title: "Installation", url: "/docs/installation" },
{ title: "CLI & registry", url: "/docs/registry" },
]}
/>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/tool-call-cardInstall the dependencies.
$ pnpm add lucide-react radix-uiAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/tool-call-card.tsx// Ballmac UI: Tool Call Card. https://ui.ballmac.com/components/tool-call-card "use client" import * as React from "react" import { ChevronRight, CircleCheck, CircleDashed, CircleX, LoaderCircle, Wrench } from "lucide-react" import { Collapsible as CollapsiblePrimitive } from "radix-ui" import { cn } from "@/lib/utils" import { defineMessage, useMessages, type Message } from "@/lib/ballmac/i18n" type ToolCallStatus = "pending" | "running" | "success" | "error" const statusConfig: Record<ToolCallStatus, { label: Message; icon: React.ElementType; className: string }> = { pending: { label: defineMessage("tool-call-card.statusConfig.pending", "Pending"), icon: CircleDashed, className: "text-muted-foreground" }, running: { label: defineMessage("tool-call-card.statusConfig.running", "Running"), icon: LoaderCircle, className: "text-foreground" }, success: { label: defineMessage("tool-call-card.statusConfig.success", "Success"), icon: CircleCheck, className: "text-chart-2" }, // icon only; chart-2 is too light for small text error: { label: defineMessage("tool-call-card.statusConfig.error", "Error"), icon: CircleX, className: "text-destructive" }, } /** 340 -> "340ms", 1240 -> "1.2s", 72000 -> "1m 12s". Locale-independent so SSR and the browser agree. */ function formatToolDuration(ms: number) { if (ms < 1000) return `${Math.max(0, Math.round(ms))}ms` if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s` const s = Math.round(ms / 1000) return `${Math.floor(s / 60)}m ${s % 60}s` } function formatPayload(value: unknown) { if (typeof value === "string") return value try { return JSON.stringify(value, null, 2) ?? String(value) } catch { return String(value) } } type ToolCallStatusBadgeProps = React.ComponentProps<"span"> & { /** The tool call's lifecycle state. Rendered as an icon plus text, never color alone. */ status: ToolCallStatus } function ToolCallStatusBadge({ status, className, ...props }: ToolCallStatusBadgeProps) { const msg = useMessages() const { label, icon: Icon, className: tone } = statusConfig[status] return ( <span data-slot="tool-call-status" data-status={status} className={cn("inline-flex shrink-0 items-center gap-1 text-xs font-medium", status === "pending" ? "text-muted-foreground" : "text-foreground", className)} {...props} > <Icon aria-hidden="true" className={cn("size-3.5", tone, status === "running" && "animate-spin motion-reduce:animate-none")} /> {msg.of(label)} </span> ) } function PayloadBlock({ label, value, tone }: { label: string; value: unknown; tone?: "error" }) { return ( <div data-slot="tool-call-section" className="grid gap-1.5"> <div className="font-mono text-[11px] tracking-wide text-muted-foreground uppercase">{label}</div> <pre dir="ltr" tabIndex={0} aria-label={label} className={cn( "max-h-64 overflow-auto rounded-md border bg-muted/50 px-3 py-2.5 font-mono text-xs leading-5 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50", tone === "error" && "border-destructive/30 text-destructive" )} > {formatPayload(value)} </pre> </div> ) } type ToolCallCardProps = Omit<React.ComponentProps<"div">, "dir"> & { /** Tool name, shown in monospace, for example "search_docs". */ name: string /** Lifecycle state of the call. */ status?: ToolCallStatus /** How long the call took, in milliseconds. */ duration?: number /** Arguments the model passed. Objects are pretty-printed as JSON; strings are shown as-is. */ input?: unknown /** What the tool returned, or the error message when status is "error". */ result?: unknown /** Short human description shown under the name. */ description?: React.ReactNode /** Controlled open state of the details panel. */ open?: boolean /** Initial open state when uncontrolled. */ defaultOpen?: boolean /** Called when the details panel opens or closes. */ onOpenChange?: (open: boolean) => void } function ToolCallCard({ name, status = "pending", duration, input, result, description, open, defaultOpen = false, onOpenChange, className, children, ...props }: ToolCallCardProps) { const hasInput = input !== undefined const hasResult = result !== undefined const hasDetails = hasInput || hasResult || children != null return ( <CollapsiblePrimitive.Root data-slot="tool-call-card" data-status={status} open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange} disabled={!hasDetails} className={cn("group/tool w-full overflow-hidden rounded-xl border bg-card text-card-foreground", className)} {...props} > <div className="flex min-w-0 items-center gap-2 px-3 py-2"> <CollapsiblePrimitive.Trigger data-slot="tool-call-trigger" className="-mx-1 flex min-w-0 flex-1 items-center gap-2 rounded-md px-1 py-1 text-start outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-default" > <ChevronRight aria-hidden="true" className={cn( "size-3.5 shrink-0 text-muted-foreground transition-transform duration-150 group-data-[state=open]/tool:rotate-90 rtl:group-data-[state=closed]/tool:rotate-180 motion-reduce:transition-none", !hasDetails && "invisible" )} /> <Wrench aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground" /> <span className="min-w-0"> <span className="block truncate font-mono text-[13px] font-medium">{name}</span> {description ? <span className="block truncate text-xs text-muted-foreground">{description}</span> : null} </span> </CollapsiblePrimitive.Trigger> <span aria-live="polite" className="flex shrink-0 items-center gap-2.5"> <ToolCallStatusBadge status={status} /> {duration !== undefined ? ( <span className="font-mono text-[11px] text-muted-foreground tabular-nums">{formatToolDuration(duration)}</span> ) : null} </span> </div> {hasDetails ? ( <CollapsiblePrimitive.Content data-slot="tool-call-details" className="grid gap-3 border-t px-3 py-3"> {hasInput ? <PayloadBlock label="Input" value={input} /> : null} {hasResult ? ( <PayloadBlock label={status === "error" ? "Error" : "Result"} value={result} tone={status === "error" ? "error" : undefined} /> ) : null} {children} </CollapsiblePrimitive.Content> ) : null} </CollapsiblePrimitive.Root> ) } export { ToolCallCard, ToolCallStatusBadge, formatToolDuration, type ToolCallCardProps, type ToolCallStatusBadgeProps, type ToolCallStatus, }Update the import paths to match your project setup.
Usage
import { ToolCallCard, ToolCallStatusBadge, formatToolDuration } from "@/components/ballmac/tool-call-card"The full example is in the Code tab above.
Examples
All states
import { ToolCallCard } from "@/components/ballmac/tool-call-card"
export default function ToolCallCardStates() {
return (
<div className="flex w-full max-w-xl flex-col gap-3">
<ToolCallCard name="read_file" status="pending" input={{ path: "src/app/page.tsx" }} />
<ToolCallCard name="run_tests" status="running" input={{ pattern: "checkout" }} />
<ToolCallCard name="list_items" status="success" duration={214} input={{ kind: "block" }} result="12 blocks" />
<ToolCallCard name="deploy_preview" status="error" duration={3120} input={{ branch: "preprod" }} result="Build failed: missing environment variable" />
</div>
)
}API reference
<ToolCallStatusBadge>
| Prop | Type | Default |
|---|---|---|
status*The tool call's lifecycle state. Rendered as an icon plus text, never color alone. | ToolCallStatus | — |
<ToolCallCard>
| Prop | Type | Default |
|---|---|---|
name*Tool name, shown in monospace, for example "search_docs". | string | — |
statusLifecycle state of the call. | ToolCallStatus | "pending" |
durationHow long the call took, in milliseconds. | number | — |
inputArguments the model passed. Objects are pretty-printed as JSON; strings are shown as-is. | unknown | — |
resultWhat the tool returned, or the error message when status is "error". | unknown | — |
descriptionShort human description shown under the name. | React.ReactNode | — |
openControlled open state of the details panel. | boolean | — |
defaultOpenInitial open state when uncontrolled. | boolean | false |
onOpenChangeCalled when the details panel opens or closes. | (open: boolean) => void | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Enter / Space | On the tool name, opens or closes the input and result (aria-expanded) |
| Tab | Focuses the scrollable input and result blocks so long JSON can be scrolled with the keyboard |
| — | Status changes are announced politely; status is always icon plus text |
Use with AI
Render each tool invocation of an agent: <ToolCallCard name status duration input result />. Objects are shown as indented JSON, strings as-is; the details panel is a disclosure (collapsed by default). With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Tool Call Card (@ballmac/tool-call-card) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Showing function/tool calls inside an assistant message
- Agent run logs where each step calls a tool and returns data
- Debug views for MCP tool calls
Not for
- Generic key/value data (use a table or description list)
- Long source files (use code-block)
Registry JSON: https://ui.ballmac.com/r/tool-call-card.json
Credits
Free to use in personal and commercial projects.
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
AI Message
A chat message with user, assistant and system roles: right-aligned user bubbles, full-width assistant prose, an avatar slot, hover-revealed actions and a hydration-safe timestamp.
AI Chat
The layout for a chat UI: a message log that sticks to the bottom while replies stream unless the reader scrolls up, a jump-to-latest button, an empty state with suggestions and a footer.
Reasoning Disclosure
A collapsible 'thinking' panel for model reasoning: shows a shimmering 'Thinking…' while streaming, opens automatically, then collapses to 'Thought for 12s' when done.
Agent Plan
A live task plan for agents: a vertical timeline with pending, running, done, failed and skipped steps, nested substeps, expandable output, segmented progress and retry on failure.