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#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add tool

Usage#

usage.tsxTSX
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.

Input
{  "command": "pnpm test auth",  "cwd": "apps/web"}
Output
✓ session.test.ts (6)
✓ reset.test.ts (4)

Test Files  2 passed (2)
     Tests  10 passed (10)

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.

Input
{  "table": "orders",  "range": "30d"}
Error

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.

Input
{  "paths": [    "drafts/q3-pricing-v1.md",    "drafts/q3-pricing-v2.md",    "drafts/q3-pricing-old.md"  ]}

Allow Koala to delete 3 files?

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.

Input
{  "to": "dana@northwind.co",  "subject": "Your Q3 renewal",  "body": "Hi Dana, your Team plan renews on 1 October at $79 per seat."}

Send this email?

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.

Which of my drafts are out of date?

All three drafts in drafts/ are superseded by pricing/q3-final.md. Want me to remove them?

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.

Input
{  "path": "pricing/q3-final.md"}
Input
{  "path": "pricing/q3-final.md"}

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.

PropTypeDefaultDescription
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).
openboolean-Controlled open state of the arguments and result.
defaultOpenboolean-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 attributeDescription
data-statusThe call's status.
data-stateopen 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.

PropTypeDefaultDescription
variantBadgeProps["variant"]-Overrides the tone the status picks.
childrenReact.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.

PropTypeDefaultDescription
valueunknown-An object is pretty-printed and highlighted as JSON; a string is shown as plain text.
labelReact.ReactNode"Input"The sheet's label.
editablebooleanfalseTurns the listing into an inline editor in the same mono type.
onValueChange(value: string) => void-Called with the raw text on every edit.
invalidbooleanfalseRings the sheet in the destructive tone and sets aria-invalid.
childrenReact.ReactNode-Your own content instead of the listing; the sheet and label stay.
Data attributeDescription
data-invalidPresent when invalid.

ToolOutput

What came back. Renders nothing while the call is pending, approval or running. Forwards native <div> props.

PropTypeDefaultDescription
valueunknown-The result: JSON for an object, plain text for a string, the message for an error.
labelReact.ReactNode"Output"The sheet's label; Error when the call failed.
errorboolean-Show value as an error. Defaults to status === "error".
collapsiblebooleantrueClamp a long JSON result behind CodeSnippet's Show more.
collapsedHeightnumber200The clamp, in px.
childrenReact.ReactNode-Your own rendering of the result; the sheet and label stay.
Data attributeDescription
data-errorPresent 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.

FAQ#

One to one: input-streaming is pending, approval-requested is approval, input-available is running, output-available is done, output-error is error and output-denied is rejected. Map the part's state once and pass it as status; ToolOutput and ToolApproval take care of when they appear.