# ToolCallTimeline

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

Source: https://signoff-ui.vercel.app/docs/components/tool-call-timeline

## Installation

From npm:

```bash
npm i signoff-ui ai
```

```tsx
import { ToolCallTimeline } from 'signoff-ui';
import 'signoff-ui/styles.css'; // once per app, or @import 'signoff-ui/tailwind.css' with Tailwind v4
```

Or as source, with the shadcn CLI:

```bash
npx shadcn@latest add https://signoff-ui.vercel.app/r/tool-call-timeline.json
```

```tsx
import { ToolCallTimeline } from '@/components/signoff-ui/tool-call-timeline';
```

## Usage

```tsx
<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

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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `parts` (required) | `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 `toolCallId`s. |
| `defaultExpanded` | `readonly string[]` |  |  |
| `onExpandedChange` | `(expanded: string[]) => void` |  |  |
| `expandErrors` | `boolean` | `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` | `boolean` | `true` | Show a per-call waterfall bar relative to the whole timeline. |
| `announce` | `boolean` | `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` | `boolean` | `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`, `id`, `aria-*`, `data-*` and event handlers.

### ToolMeta

Per-tool presentation. Every field is optional.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `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

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

## Accessibility

| Keys | Action |
| --- | --- |
| ↑ ↓ | Previous or next call |
| Home End | First or last call |
| Enter Space | Show 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

Slots (`data-slot`): `signoff-tool-call`, `signoff-tool-call-trigger`, `signoff-tool-call-timeline`, `signoff-tool-call-chunk`, `signoff-tool-call-details`.

- `data-phase` on `signoff-tool-call`: 'denied' | 'streaming' | 'running' | 'awaiting-approval' | 'success' | 'error'
- `data-state` on `signoff-tool-call`: 'input-streaming' | 'input-available' | 'approval-requested' | 'approval-responded' | 'output-available' | 'output-error' | 'output-denied'
- `data-interrupted` on `signoff-tool-call`: set 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`. See https://signoff-ui.vercel.app/docs/getting-started#theming.
