Components
ToolCallTimeline
Every tool call with its state, a measured duration and a waterfall; errors inline.
Example
ENOENT: no such file or directory, open 'middleware.ts'
Interactive, and the same on the components page. Try the keyboard below on it.
Installation
npm i signoff-ui aiimport { ToolCallTimeline } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/tool-call-timeline.jsonimport { ToolCallTimeline } from '@/components/signoff-ui/tool-call-timeline';The styles are once per app; Getting started has the Tailwind v4 and plain CSS options.
Usage
<ToolCallTimeline
parts={message.parts}
tools={{
read_file: { label: 'Read file', summary: (input) => (input as { path?: string }).path },
run_command: { label: 'Run command', risk: 'high' },
}}
// Once the run has ended, calls that never settled read "Stopped".
active={status === 'submitted' || status === 'streaming'}
/>How it works
Every AI SDK tool state (input-streaming, input-available, approval-requested, approval-responded, output-available including preliminary, output-error, output-denied) with live durations, a waterfall that makes parallel calls visible, and expandable input and output. Failures show their error inline. Calls are disclosure buttons: ↑, ↓, Home and End move between them, and settled calls are announced.
Pass active={false} once the run has ended (stopped, failed, or restored from history), so calls that never settled read "Stopped" instead of counting up forever.
Durations are measured, not invented: message parts carry no timestamps, so the timeline records state changes on the client. Execution time starts when a call is approved, not when it was proposed, so waiting on a person never reads as a slow tool, and calls already settled when first seen get no duration rather than a fake zero. Pass server-measured timings if you have them. renderOutput replaces a call's JSON output with your own content, and renderExtra adds content under a call.
API reference
Generated from the types the package ships, so it matches the version you install.
ToolCallTimeline
A vertical timeline of tool calls with live states, durations, a waterfall, and expandable input/output. Follows the WAI-ARIA disclosure pattern; Arrow Up/Down, Home and End move between calls.
Props
partsrequiredreadonly AnyUIPart[]Tool parts, or every part of a message: non-tool parts are ignored.
toolsRecord<string, ToolMeta>timingsToolTimingsExternally measured timings by
toolCallId. Measured client-side when omitted.expandedreadonly string[]Controlled set of expanded
toolCallIds.defaultExpandedreadonly string[]onExpandedChange(expanded: string[]) => voidexpandErrorsbooleanDefaultfalseExpand calls automatically when they fail. Default
false: the error message is always shown inline under a failed call, with details on demand.waterfallbooleanDefaulttrueShow a per-call waterfall bar relative to the whole timeline.
announcebooleanDefaulttrueAnnounce completions and failures to screen readers. Calls that settle within moments of each other, such as parallel calls, are announced together.
renderExtra(part: ToolPart) => ReactNodeExtra content under a call, e.g. an approval card. Your content, like
renderOutput's.labelstringAccessible name for the list. Default "Tool calls" (
labels.toolCallTimeline.list).activebooleanDefaulttrueWhether the run can still make progress. Pass
falseonce it has ended (stopped, failed, or restored from history): calls still streaming their input or running then read "Stopped" and their clocks stop, instead of counting up forever.labelsSignoffLabelsInputWords to use instead of the English defaults: see
SignoffLabelsProvider.
Other props go to the root <div>: className (merged with tailwind-merge), id, aria-*, data-* and event handlers.
ToolMeta
Per-tool presentation. Every field is optional.
labelstringDisplay label. Defaults to the humanized tool name.
iconReactNodesummary(input: unknown, part: ToolPart) => ReactNodeOne-line summary shown next to the label. Defaults to the most descriptive input field. Not called before any input has arrived; while the input streams it can be partial.
renderOutput(output: unknown, part: ToolPart) => ReactNodeReplace the default JSON output view. Your content, like
AgentMessage'srenderTool.riskRiskLevel | ((input: unknown) => RiskLevel)Risk shown on approval requests for this tool.
ToolTiming
startedAtnumberFirst time the call was observed (input started streaming).
runningAtnumberWhen execution began: input complete, or approval granted.
endedAtnumberWhen the call settled (output, error or denial).
RiskLevel
type RiskLevel = 'low' | 'medium' | 'high' | 'critical';Accessibility
| Keys | Action |
|---|---|
| ↑↓ | Previous or next call |
| HomeEnd | First or last call |
| EnterSpace | Show or hide the call’s input and output |
- An ordered list named by
label("Tool calls"). Each call is a disclosure button witharia-expanded, as in the WAI-ARIA accordion pattern; the arrow keys move between them. - A call’s name, summary, state and duration are in its button’s accessible name. In narrow containers the summary is hidden visually but still read.
- Completions and failures are announced through a polite live region, such as "Read file finished in 1.2 seconds" or "Read file failed: ENOENT…". Calls that settle together, such as parallel calls, are announced together. Turn it off with
announce={false}when something else announces them. - The state node beside each call is hidden from screen readers: the call’s button already says "Needs approval", "Failed" or "Stopped".
- States are never color alone: each has an icon and a word ("Failed", "Needs approval", "Denied").
- The expand and collapse animation runs only without
prefers-reduced-motion.
Theming
Every color, radius and font is a CSS variable, so the overrides in Theming apply, on :root or scoped to any element. These hooks are read from the component's markup.
- Slots (data-slot)
- signoff-tool-callsignoff-tool-call-triggersignoff-tool-call-timelinesignoff-tool-call-chunksignoff-tool-call-details
- State attributes
- data-phaseon
signoff-tool-call'denied' | 'awaiting-approval' | 'error' | 'streaming' | 'running' | 'success'data-stateonsignoff-tool-call'input-streaming' | 'input-available' | 'approval-requested' | 'approval-responded' | 'output-available' | 'output-error' | 'output-denied'data-interruptedonsignoff-tool-callset once the run ended before the call settled - Tokens it uses
- --signoff-accent--signoff-accent-fg--signoff-bg--signoff-border--signoff-border-strong--signoff-fg--signoff-fg-muted--signoff-fg-subtle--signoff-font-mono--signoff-font-sans--signoff-hot--signoff-hot-fg--signoff-ring--signoff-surface-2
/* Only this component, and only inside .settings-panel */
.settings-panel [data-slot='signoff-tool-call'] {
--signoff-radius: 4px;
}
[data-slot='signoff-tool-call'][data-phase='denied'] {
outline: 1px solid var(--signoff-accent);
}