# Tool Call Timeline

> A React tool call timeline for AI agents: each step with its status, time, input and output, plus approval and retry when a call needs them.

- Type: React component (AI Kit)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add tool-call-timeline`
- Docs: https://wingo-ui.com/components/tool-call-timeline

## Interaction

Each call is a row: a round marker says its status (a dashed ring while queued, a spinner while it runs, a check that pops once when it finishes, a cross when it fails, a hand that pings while it waits for a person), then a human title, the tool's id in a small mono chip and how long it took. A running call's title shimmers and its time ticks in tenths of a second. Click or tap a row, or press Enter or Space on it, to open its input and output as highlighted JSON (text results wrap), its error and its logs; long blocks scroll inside themselves and every block has a copy button. A call that waits for approval shows Reject and Approve; a failed one opens by itself and shows Retry; a button whose handler returns a promise spins until it settles, and focus moves back to the row when the buttons go away. Sub-steps sit indented under their step. The card variant adds a header with the run's state (a spinner, or Needs approval, Failed, Done in words; a hand or an alert in its icon tile when a person is needed or a call failed, which alone carries it in a card narrower than 448px), its progress while it runs (2 of 5 done) and total time, and a button that opens or closes every row at once; the inline variant is a one-line summary (Used 4 tools, 2.8 s) that opens by itself while a call runs or waits for approval and closes again 1.2 s after everything finished, unless you opened it yourself or focus is inside it (you just approved a call), so focus never drops to the page. Status changes are announced once each to screen readers. Rows lay out by their own width, so a timeline in a chat column or a side panel fits: when a row is narrow the tool id moves under the title and Approve and Reject share the width; on a phone they are 44px tall, rows have 44px targets and code blocks scroll sideways inside themselves. Reduced motion swaps the springs for quick fades and stops the shimmer and the ping.

## Installation

```bash
npx wingo-ui@latest add tool-call-timeline
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/tool-call-timeline` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/tool-call-timeline.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Code Block](https://wingo-ui.com/components/code-block) ([Markdown](https://wingo-ui.com/components/code-block.md))
- [Copy Button](https://wingo-ui.com/components/copy-button) ([Markdown](https://wingo-ui.com/components/copy-button.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useStickToBottom](https://wingo-ui.com/components/use-stick-to-bottom) ([Markdown](https://wingo-ui.com/components/use-stick-to-bottom.md))
- [AI Chat](https://wingo-ui.com/components/ai-chat) ([Markdown](https://wingo-ui.com/components/ai-chat.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { ToolCallTimeline, type ToolCall } from "@/components/ui/tool-call-timeline"

export function AgentRun({
  calls,
  onApprove,
  onReject,
}: {
  calls: ToolCall[]
  onApprove: (call: ToolCall) => Promise<void>
  onReject: (call: ToolCall) => void
}) {
  return (
    <ToolCallTimeline
      variant="card"
      title="Collections agent"
      calls={calls}
      onApprove={onApprove}
      onReject={onReject}
      locale="en-US"
    />
  )
}

// a call waiting for a person, as the model sent it
export const sendReminder: ToolCall = {
  id: "call-send",
  name: "send_email",
  title: "Sending the email",
  status: "awaiting-approval",
  input: { to: "adam.foster@techsolutions.com", subject: "Reminder for invoice #2026-0405" },
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `calls` | `ToolCall[]` |  | The calls in order, in the ToolCall shape from lib/ai-chat: id, name, title, status, input, output, error, startedAt, endedAt, durationMs, logs and steps (sub-steps). Keep them immutable so only the call that changed re-renders. |
| `variant?` | `"timeline" \| "card" \| "inline"` | `"timeline"` | timeline: markers on a rail, for an agent page. card: the timeline in a panel with a header (title, progress, total time and the run's state). inline: a one-line summary that opens into the timeline, for a chat message. |
| `title?` | `ReactNode` | `""` | The card's heading or the inline summary's text. Empty falls back to labels.summary (Used 4 tools). The card's second line shows the progress while the run goes on (2 of 5 done), then the summary, with the total time. |
| `size?` | `"sm" \| "md"` | `"md"` | Titles 13 / 14px, rows 32 / 40px, markers 20 / 24px. Sub-steps always use sm. On touch screens md rows grow to 44px and sm rows keep their look with a 44px hit area. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Paints the Approve button, the card header's icon, the inline summary's spinner and the focus tint. Markers always use the status colors (running is info, done success, failed danger, waiting warning). |
| `color?` | `string` | `""` | Any CSS color; replaces the tone. Text on the Approve button turns black or white by lightness. |
| `codeAccent?` | `string` | `""` | A hex color the input and output blocks build their syntax colors from (the code block's accent). Empty keeps the code block's own. |
| `showToolName?` | `boolean` | `true` | The tool's id (search_invoices) in a mono chip next to the title; in a row narrower than 512px it sits under the title (sm rows hide it below 448px). |
| `showDurations?` | `boolean` | `true` | How long each call took (live while it runs, from startedAt) and the run's total in the card and inline summaries. |
| `showInput?` | `boolean` | `true` | The arguments the model sent, in an opened row. |
| `showOutput?` | `boolean` | `true` | What the tool returned, in an opened row. |
| `codeMaxHeight?` | `number` | `240` | Height in px an input or output block may reach before it scrolls inside itself. Copy always takes the whole value. |
| `defaultOpen?` | `boolean` | `false` | inline: start with the timeline open under the summary. |
| `open?` | `boolean` |  | inline: controlled open state. autoOpen asks for changes through onOpenChange. |
| `onOpenChange?` | `(open: boolean) => void` |  | inline: called when the summary is pressed and when autoOpen opens or closes it. |
| `autoOpen?` | `boolean` | `true` | inline: opens while a call runs or waits for approval and closes 1.2 s after everything finished, unless the person opened it themselves or focus is inside it. |
| `expandErrors?` | `boolean` | `true` | A failed call starts with its details open, and opens by itself when a running call fails. |
| `expandedKeys?` | `string[]` |  | Controlled: the ids of the calls (and sub-steps) whose details are open. |
| `defaultExpandedKeys?` | `string[]` |  | The calls whose details start open (failed calls are added when expandErrors is on). |
| `onExpandedKeysChange?` | `(keys: string[]) => void` |  | Called with the next ids when a row is opened or closed, and when a call fails with expandErrors on. |
| `onApprove?` | `(call: ToolCall) => (void \| Promise<unknown>)` |  | Shows Approve on calls awaiting approval. Return a promise to keep the button spinning (and Reject disabled) until it settles; then set the call's status. |
| `onReject?` | `(call: ToolCall) => (void \| Promise<unknown>)` |  | Shows Reject next to Approve. Set the call to canceled, or back to the model, in the handler. |
| `onRetry?` | `(call: ToolCall) => (void \| Promise<unknown>)` |  | Shows Retry on failed calls, spinning while a returned promise is pending. |
| `renderInput?` | `(call: ToolCall) => ReactNode` |  | Replaces the JSON view of a call's input (a map, a diff). Return undefined to keep the JSON, null to hide it. |
| `renderOutput?` | `(call: ToolCall) => ReactNode` |  | Replaces the JSON view of a call's output, e.g. invoices as a small table. Return undefined to keep the JSON, null to hide it. |
| `locale?` | `string` | `""` | Locale for durations (0.8 s in en-US, 0,8 s in de-DE) and counts. Empty uses UIProvider's, then the page's. |
| `now?` | `Date \| number` |  | A fixed clock for running durations, for screenshots and tests. Without it running calls tick on the shared ticker (lib/ai-chat). |
| `emptyState?` | `ReactNode` |  | Replaces the No steps yet row shown while calls is empty. |
| `labels?` | `Partial<ToolCallTimelineLabels>` |  | Every visible and spoken string: summary(count, formatted), progress(done, total, formattedDone, formattedTotal), running(title), input, output, error, logs, approve, reject, retry, showDetails, hideDetails, empty, status (one word per status, partial), announce(title, status), copy, copied. English defaults in DEFAULT_TOOL_CALL_TIMELINE_LABELS. |
| `classNames?` | `Partial<Record<"summary" \| "summaryIcon" \| "summaryTitle" \| "summaryMeta" \| "list" \| "item" \| "rail" \| "marker" \| "header" \| "title" \| "name" \| "duration" \| "status" \| "chevron" \| "details" \| "description" \| "input" \| "output" \| "error" \| "logs" \| "approval" \| "retry" \| "steps" \| "empty", string>>` |  | Extra classes for the inner parts; summary is the card header or the inline summary button. |
| `className?` | `string` |  | Extra classes for the root, merged last. |

## Parts

### ToolCallItem (component)

One call as an <li> with its details, approval and sub-steps, for lists of your own: render it inside an <ol> and set last on the final one to end the rail. Props: { call: ToolCall; last?: boolean; expanded?: boolean; defaultExpanded?: boolean; onExpandedChange?; expandErrors?; showInput?; showOutput?; showToolName?; showDurations?; renderInput?; renderOutput?; codeMaxHeight?; codeAccent?; onApprove?; onReject?; onRetry?; size?; tone?; color?; now?; locale?; labels?; classNames? }.

### ToolCallStatusMarker (component)

The 20 / 24px round status marker on its own (agent steps, workflow node runs); decorative, so pair it with the status in words. Props: { status: ToolCallStatus; size?: "sm" | "md" }.

### DEFAULT_TOOL_CALL_TIMELINE_LABELS (constant)

The English strings, to spread and override. Type: ToolCallTimelineLabels.

## Examples

- **Inline in a message**: A one-line summary above the answer that opens while tools run and folds away 1.2 s after.
- **Failure and retry**: The tax portal answers 503: the call opens with its error and logs, and Retry succeeds on the second try.
- **Approval in brand**: A deal the agent wants to create waits for a person; Approve spins until its promise settles.
- **Nested steps**: A report step that ran three sub-steps, replayed live.
- **Custom output**: renderOutput turns the invoices into rows with dollar amounts.
- **Narrow side panel**: size sm in a 320px card, with a pinned clock for a stable running time.
- **Queued plan in English**: Default labels, color="#7C3AED" and pending steps that run one by one.

## See also

- [Timeline](https://wingo-ui.com/components/timeline) ([Markdown](https://wingo-ui.com/components/timeline.md))
- [Code Block](https://wingo-ui.com/components/code-block) ([Markdown](https://wingo-ui.com/components/code-block.md))
- [Steps](https://wingo-ui.com/components/steps) ([Markdown](https://wingo-ui.com/components/steps.md))
- [AI Chat](https://wingo-ui.com/components/ai-chat) ([Markdown](https://wingo-ui.com/components/ai-chat.md))
- [Reasoning Indicator](https://wingo-ui.com/components/reasoning-indicator) ([Markdown](https://wingo-ui.com/components/reasoning-indicator.md))
- [Agent Status Card](https://wingo-ui.com/components/agent-status-card) ([Markdown](https://wingo-ui.com/components/agent-status-card.md))

---

Canonical: https://wingo-ui.com/components/tool-call-timeline
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
