DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ChatBubbleStreamingResponseCitationsToolCallPlanListParameterDiffModelSelectorElicitationArtifactPanelPromptInputKeyboardToolbar
AI agentsContributing
  1. Components
  2. ToolCall

ToolCall

What an agent shows when it calls a tool — the tool's name, its arguments as they stream in, a live status, the result or the error, and Approve / Deny when a person has to say yes first. A card on its own, or quiet rows under one header in a transcript.

$ pnpm add @tessinaui/ui

When to use

  • An agent calls a tool and the reader should see what it asked for and what came back.
  • A call needs a person's approval first: sending, deleting, publishing, spending. Approve and Deny sit outside the disclosure, so a pending decision can never be hidden.
  • Arguments that stream in token by token. The card stays valid under every prefix of them, and a row that has arrived never moves.
  • Approvals that scale with what a call can do. Set risk and a high-risk call states its effects, says it is irreversible and waits for an acknowledgement before Approve works.
  • A status the reader can trust. Six statuses, each with its own word and glyph, and the word is part of the card's accessible name.

When not to use

  • A run of steps the model already took, inside a chat bubble. Use ChatBubbleSteps: a collapsible trace, not a card per call, with no approval.
  • A plan of steps. Use PlanList. A plan is an ordered list of operations; a tool call is one operation with inputs and an output.
  • A read-only call that is safe to run. Ask for approval where a call writes, deletes, spends or sends; a read usually needs none.
  • An action you can simply undo. A Toast with an Undo action is often better than an approval, and approving every call teaches people to approve without reading.
  • Code or a long output on its own. Use CodeBlock; a JSON result already renders in one.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { ToolCall } from "@tessinaui/ui/tool-call";

<ToolCall
  name="send_email"
  args={{ to: "finance@acme.com", subject: "Q3 revenue report" }}
  status={status}
  result="Sent."
  onApprove={() => setStatus("running")}
  onDeny={() => setStatus("denied")}
/>

The card is a snapshot: pass the call as it stands on every render — the arguments parsed so far, the status, the result once there is one. It owns no timer and no state machine. Approve and Deny call your handlers, and your handlers move status on.

Examples

Default

In a transcript

A turn's calls read as quiet rows under one header. ToolCallGroup counts them while the agent works ("Using 3 tools"), says when one waits for you, and sums the turn once every call has finished: "Used 3 tools · 1 failed · 3.4s", with each tool's mark stacked once. It folds itself when the turn is done, so the answer reads right under it — unless you have opened or closed it, which wins from then on. Run it again to watch the rows arrive.

Rows

variant="row" is the transcript look: the tool's mark, the call's phrase, its status as a glyph and a word, and a short summary with how long it took. A row has no frame until it needs you. Calls inside a group are rows by default.

Write the phrase the way the call reads at each moment: "Searching issues" while it runs, "Searched issues" once it is done.

Chips

variant="chip" is a call inside an answer, drawn as a pill. While the call runs, fails or stops it is a static pill that reads as its status. Once it is done the pill opens to what the call did: a search lists its queries (pass args as an array of strings) and its sources, with the first two marks stacked on the pill; any other call shows its input as text, so pass args as a string, and its result. A chip never announces anything itself, because the answer around it already speaks. Use marksClassName to hide the stacked marks in a narrow column.

Retry

Pass onRetry and a failed call offers Retry in place, named for the call. Move status on in the handler; focus goes back to the call's header. A row shows the reason under its header, so neither hides when the row is closed.

Copy

copyable adds Copy input and Copy output beside the open call's sections. Text copies as it is, anything else as indented JSON. The button turns to a check and the result is read out once.

Approval in a transcript

A row that needs your decision takes a frame, the group header says it is waiting, and the group opens if it was folded. Risk tiers work the same as on the card.

In a narrow column

In a narrow column the meta wraps under the name and Retry drops under its reason. Nothing scrolls sideways.

States

Six statuses, and none of them is carried by colour alone: each has its own word and its own glyph, and the word is part of the card's accessible name.

Approval

Risk tiers

risk scales the approval with what the call can do. Each tier is a word and a glyph with its own shape — a shield, a triangle, a diamond — so the tiers stay apart in greyscale and in forced-colors mode; the colour, taken from the house intents, is the third signal and never the only one. Nothing is focused for you on any tier.

Choosing a tier

TierUse it forWhat the card does
nonethe plain cardDeny · Approve, as without risk
lowreversible, contained writes: a draft, a label, a scratch fileone click; "Low risk" with a shield
mediumwrites other people see, or that take effort to undo: an edited record, a restarted serviceone click; "Medium risk" with a warning triangle
highdeleting data, spending money, sending on someone's behalf"High risk" with a diamond, a destructive Approve, and an acknowledgement that must be ticked first

A read-only call usually needs no approval at all, and a reversible action you can simply undo is often better as a toast with an Undo action than as an approval.

High risk

A high-risk call says plainly what will happen (effects) and that it cannot be undone (irreversible), and Approve waits for the acknowledgement. Name Approve with a verb through strings.approve ("Delete 3 files"); the tier word never goes into a button's name, because the approval group already carries it.

With a parameter diff

When a call edits something, show the edit. A ParameterDiff in effects lists each value before and after, sits beside Deny and Approve, and describes Approve, so the change is what a reader approves.

Streaming arguments

Results

On color

The tiers on a coloured plate: the badge, the acknowledgement and the destructive Approve take the plate's recipes. On the error, info and success plates every intent ink is the plate's own ink, so the word and the glyph carry the tier there.

Right to left

dir="rtl" mirrors the card and swaps in the Arabic strings: the status words, the section labels, the buttons, the risk tier, the acknowledgement and every announcement. dir does not set a language: pass lang="ar" with it, or a screen reader reads the Arabic in the page's voice. An inherited direction mirrors the layout only; pass strings for another language.

Argument values, a text result and an error each read in their own direction, so a path, a URL or an address stays left to right inside an Arabic card.

Loading

Streaming

The card follows the streaming-stable contract every AI-kit component shares (CONVENTIONS §7c):

  • Any prefix renders. No props at all, an object cut mid-key, a raw JSON string cut mid-value, a status cut mid-word ("runn" reads as pending) — each renders, none throws.
  • Arrived rows never move. A plain-object args renders as one row per key, keyed by the key, so a row that has arrived keeps its DOM node while the next one streams in below it.
  • Streaming is never announced. While the call is pending the arguments group is aria-busy with aria-live="off", so an assistive technology that honours an ancestor role="log" still reads no argument token.

Pass the arguments parsed — a partial object, the shape the AI SDKs hand over — and each value renders on its own row. A string renders as text, and any other structure as JSON.

API Reference

ToolCall

PropTypeDefaultDescription
nameReactNode"Tool call"The tool's name
toolNamestring—The tool's own name, read before the call's phrase in a row's accessible name
nameTextstringname when it is a stringPlain text for announcements and the action buttons' names; required when name is a node
argsunknown—The arguments so far. A plain object renders as rows; a string as text; any other structure as JSON
statusToolCallStatus"pending"pending · awaiting-approval · running · done · failed · denied. An unknown value reads as pending
resultunknown—Shown once done: a string as text, a React node as itself, anything else as JSON
errorReactNode—Shown once failed. A string is also read out
onApprove() => void—Renders Approve while awaiting-approval
onDeny() => void—Renders Deny while awaiting-approval. Escape inside the approval controls calls it too
risk"low" | "medium" | "high"—The approval's risk tier. Unset keeps the plain card; an unrecognised value reads as "high"
effectsReactNode—What approving will do, shown beside the buttons while the call waits. An array renders as a list
irreversiblebooleanfalseAdds "This can't be undone." and names it in the acknowledgement
iconReactNodewrenchReplaces the glyph before the name. In a group it also joins the header's stack of marks
variant"card" | "row" | "chip""card", "row" in a grouprow is the quiet transcript look; it takes a frame only while the call waits for approval. chip is a pill inside an answer that opens to what the call did
sourcesToolCallSource[]—A search's sources (id, title, url, domain, icon): listed in the chip's panel, the first two marks stacked on the pill
marksClassNamestring—Classes for the chip's stacked marks, say to hide them in a narrow column
durationMsnumber—How long the call took. Shown after the name ("1.2s", <1s under a second) and added to the group's total
summaryReactNode—A short line on what came back ("12 issues"), shown after the name
onRetry() => void—Renders Retry on a failed call. Move status on in the handler
copyablebooleanfalseAdds Copy input and Copy output to the open call
copyResultTextstring—What Copy output writes when result is a node (a list, a table)
openboolean—Controlled disclosure
defaultOpenbooleanopen when it mounts waitingUncontrolled starting state
onOpenChange(open: boolean) => void—Called when the disclosure opens or closes
tone"default" | "on-color"inheritedInherited from a wrapping <Surface>, else "default"
dir"ltr" | "rtl"—Text direction; "rtl" also picks the Arabic strings
stringsPartial<ToolCallStrings>—Per-entry string overrides

Everything else lands on the root.

ToolCallGroup

PropTypeDefaultDescription
labelReactNodederivedReplaces "Using 3 tools" / "Used 3 tools". The meta still follows it
autoFoldbooleantrueFold once every call has settled, unless the reader has opened or closed the group
openboolean—Controlled disclosure
defaultOpenbooleantrueUncontrolled starting state
onOpenChange(open: boolean) => void—Called when the group opens or closes
tone"default" | "on-color"inheritedInherited from a wrapping <Surface>, else "default"
dir"ltr" | "rtl"—Text direction; "rtl" also picks the Arabic strings
stringsPartial<ToolCallGroupStrings>—Per-entry string overrides, with plural variants (used_one, …)

Calls inside report their status, duration and mark to the group, so the header never needs its own copy of their state.

ToolCallSkeleton

PropTypeDefaultDescription
variant"card" | "row""card"The look it stands in for
charsnumber24Length of the name the placeholder stands in for
labelstring—What AT announces while the placeholder is up. Silent without it
dir"ltr" | "rtl"—Text direction

AgentStatusGlyph

The one glyph set every agent list draws state with: ToolCall rows, PlanList rows and the agent templates. It is exported from @tessinaui/ui/tool-call for a list of your own, so your states read the same way.

PropTypeDefaultDescription
status"pending" | "running" | "done" | "failed" | "waiting" | "denied" | "skipped" | "paused" | "stopped"—The state. Each is a different shape, so none rests on colour
shape"ring" | "plain""ring"ring is the outlined set a plan's steps use. plain is the bare marks a transcript row uses, such as a check and an alert

The glyph sits in a 16px box and is decorative (aria-hidden): say the state's word yourself. Size the box with className. agentStatusGlyph(status, shape) returns the bare icon node.

DOM contract

AttributeWhereValues
data-slotevery parttool-call, tool-call-trigger, tool-call-name, tool-call-status, tool-call-panel, tool-call-args, tool-call-arg, tool-call-arg-value, tool-call-arg-reveal, tool-call-text, tool-call-json, tool-call-result, tool-call-error, tool-call-actions, tool-call-deny, tool-call-approve, tool-call-announcer, tool-call-skeleton, tool-call-approval, tool-call-risk, tool-call-effects, tool-call-irreversible, tool-call-acknowledge, tool-call-meta, tool-call-failure, tool-call-retry, tool-call-copy, tool-call-arrival, tool-call-group, tool-call-group-trigger, tool-call-group-label, tool-call-group-meta, tool-call-group-marks, tool-call-group-panel, tool-call-group-announcer, tool-call-marks
data-variantthe rootcard · row · chip
data-settled / data-waitingthe group rootpresent once every call has settled / while a call waits
data-statusthe rootpending · awaiting-approval · running · done · failed · denied
data-riskthe root, the approval blocklow · medium · high; absent without a tier
data-tonethe rootdefault · on-color

Motion

Every movement explains a change, and nothing moves when a call first appears.

  • A call that arrives in a group that has already painted grows into its space on the panel curve.
  • A changed phrase or status fades in place.
  • The group folds on the panel curve once the turn is done, a moment after the last call settles.

With reduced motion every state stays: calls appear at full height, labels swap at once, and the group still folds.

Accessibility

There is no APG pattern for a tool call. The card is a disclosure (the house Collapsible) with real buttons for the decision, and the contract is:

  • The status is in the name. The header is a <button aria-expanded> whose accessible name is the tool and its status together — "delete_file, Awaiting approval" — so the status is heard with the name, not after a pause and never only as a colour or an icon. The badge's glyph is aria-hidden.
  • The decision is two real buttons, in one order. Deny first, Approve last — the order approval dialogs in the field use — and neither is focused for the reader, so the destructive choice is never the one an extra keypress lands on. Each button's name carries the tool ("Approve delete_file"), so a page with several waiting calls stays unambiguous.
  • The decision is reachable first. The buttons sit outside the disclosure and before the result in the DOM, so Tab goes from the header straight to Deny, then Approve, and collapsing the arguments can never hide a pending decision.
  • You see what you approve. A call that starts waiting opens its arguments; the card never closes on its own.
  • Focus is never lost. After a decision the buttons go, and focus moves to the header, whose name now carries the new status.
  • Escape cancels. Escape pressed on the acknowledgement, Deny or Approve denies the call, like Deny, and stops there, so a Drawer or Modal around the card stays open. Escape on the header does nothing, and nothing ever decides on a timer.
  • A tier is never only a colour. With risk, the approval block is a group named for the tier and the tool — "High risk: delete_file" — so entering the buttons announces it once. Approve is described by the effects and the irreversibility note, so the consequence is heard on the control that causes it. In forced-colors mode the word and the glyph's shape carry the tier.
  • High risk cannot be approved by reflex. Approve stays focusable but reports aria-disabled until the acknowledgement is ticked; pressing it earlier moves focus to the checkbox, which says what is being acknowledged. A double click or a double Enter does nothing.
  • Announcements are transitions, once, politely. A call that starts waiting is announced — "delete_file needs your approval.", with the tier when there is one ("… High risk.") — and so is a failure, with its reason. A call that mounts already waiting is announced too, so an approval that arrives in one piece is not lost. Nothing else speaks: not the arguments, not running, not done. Standalone, the card mounts one empty polite region; inside a ChatBubbleList it reports to the list's region instead of adding a second.
  • Long values stay reachable. A value past 160 characters cuts with a "Show more" toggle (a real button with aria-expanded); a text result scrolls in a named, focusable region; a JSON result is a CodeBlock, which is copyable and keyboard-scrollable.
  • Reduced motion: the only animation is the running glyph's pulse, gated with motion-safe:.

AT matrix: not yet screen-reader tested, hence beta.

Rows and groups

  • A row's name carries its whole line: the phrase, the status, the summary and the duration ("Searched issues, Done, 12 issues, 1.2s"). The mark and the separator dots are hidden from assistive tech.
  • The group header is a disclosure named by its summary ("Used 3 tools, 1 failed, 3.4s"). The stacked marks are decoration. Closed rows are out of the tab order with the panel.
  • One region speaks for the turn. Calls inside a group report approvals and failures to the group's polite region, or to the conversation's when the group sits in one. A copy result is read once.
  • Retry is a real button named for the call ("Retry Query deploys"). Once the status moves on, focus goes to the call's header, never to the page.

Notes

  • denied is a status, not a failure. It is the reader's own answer, so it keeps full ink and its own glyph. A call that failed after it was approved is failed.
  • No lock after a decision. The card does not disable its buttons once one is pressed — a card waiting on a status you never send would stay locked. Move status in the handler.
  • Buttons need a handler. awaiting-approval with no onApprove and no onDeny shows the status and no buttons — for a call someone else is approving.
CitationsPlanList

On this page

When to useWhen not to useInstallationUsageExamplesDefaultIn a transcriptRowsChipsRetryCopyApproval in a transcriptIn a narrow columnStatesApprovalRisk tiersChoosing a tierHigh riskWith a parameter diffStreaming argumentsResultsOn colorRight to leftLoadingStreamingAPI ReferenceToolCallToolCallGroupToolCallSkeletonAgentStatusGlyphDOM contractMotionAccessibilityRows and groupsNotes