Skip to content

Docs

Components

ToolCallTimeline

Every tool call with its state, a measured duration and a waterfall; errors inline.

Example

  1. 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 ai
import { ToolCallTimeline } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';

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

partsrequired
readonly AnyUIPart[]

Tool parts, or every part of a message: non-tool parts are ignored.

tools
Record<string, ToolMeta>
timings
ToolTimings

Externally measured timings by toolCallId. Measured client-side when omitted.

expanded
readonly string[]

Controlled set of expanded toolCallIds.

defaultExpanded
readonly string[]
onExpandedChange
(expanded: string[]) => void
expandErrors
booleanDefault false

Expand calls automatically when they fail. Default false: the error message is always shown inline under a failed call, with details on demand.

waterfall
booleanDefault true

Show a per-call waterfall bar relative to the whole timeline.

announce
booleanDefault true

Announce completions and failures to screen readers. Calls that settle within moments of each other, such as parallel calls, are announced together.

renderExtra
(part: ToolPart) => ReactNode

Extra content under a call, e.g. an approval card. Your content, like renderOutput's.

label
string

Accessible name for the list. Default "Tool calls" (labels.toolCallTimeline.list).

active
booleanDefault true

Whether the run can still make progress. Pass false once 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.

labels
SignoffLabelsInput

Words 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.

label
string

Display label. Defaults to the humanized tool name.

icon
ReactNode
summary
(input: unknown, part: ToolPart) => ReactNode

One-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) => ReactNode

Replace the default JSON output view. Your content, like AgentMessage's renderTool.

risk
RiskLevel | ((input: unknown) => RiskLevel)

Risk shown on approval requests for this tool.

ToolTiming

startedAt
number

First time the call was observed (input started streaming).

runningAt
number

When execution began: input complete, or approval granted.

endedAt
number

When the call settled (output, error or denial).

RiskLevel

type RiskLevel = 'low' | 'medium' | 'high' | 'critical';

Accessibility

Keyboard
KeysAction
↑↓Previous or next call
HomeEndFirst or last call
EnterSpaceShow or hide the call’s input and output
  • An ordered list named by label ("Tool calls"). Each call is a disclosure button with aria-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-stateon signoff-tool-call'input-streaming' | 'input-available' | 'approval-requested' | 'approval-responded' | 'output-available' | 'output-error' | 'output-denied'data-interruptedon signoff-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
app/globals.css
/* 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);
}