Skip to content
Join UI
FeedbacknewUpdated

Tool Trace

A run log for agent tool calls: the step in flight opens itself, writes its output a line at a time, then folds away and goes still.

View sourceon GitHub (opens in a new tab)Open full preview

ToolTrace is what one run looks like from the inside — every tool the agent reached for, in order, and what came back. Pass the steps with a state on each and it draws a rail through them: dashed markers for what has not happened yet, tinted ones for what has, a hairline filling behind each step as it resolves. The motion is spent on the step in flight and nowhere else. It opens as it starts, its console fills a line at a time, and the moment it lands the panel folds away and hands the rail to the next tool — so an unattended trace always shows the work that is actually happening without the reader touching anything. Every other step is a still drawing. Open one yourself and follow yields for good, because a reader who went looking should not have the page close under them. States carry a glyph and a spoken label as well as a hue, transitions are announced through a single polite live region rather than by streaming the console at a screen reader, and the whole thing resolves instantly under prefers-reduced-motion.

Preview

Tool Trace
A run, as it happens

Press play. Each tool opens itself as it starts, writes its output a line at a time, then folds away and hands the rail to the next one — until one of them fails and the rest are skipped.

Agent runQueued
  1. open_pull_requestfeat/tool-trace → main, Queued
0 / 5
Agent run, Queued
icon

At rest the trace is a still drawing. A glyph per tool replaces the state marker while the ring, the rail and the hidden label keep carrying the outcome — and the step that failed is the one left open.

  1. read_filelib/registry/components.ts, Done0.3s
  2. grep_searchpattern: "defineComponent\(", Done0.4s
  3. edit_fileregistry/components/tool-trace.tsx, Done1.1s
  4. ✗ lint tool-trace.tsx:284 react-hooks/exhaustive-deps→ 1 error, exit 1
  5. open_pull_requestfeat/tool-trace → main, Skipped
Run 4812, Failed
multiple={false}

Opening a step folds the last one shut, which keeps a long retrieval chain to one screen. detail takes markup, so a step with nothing to print still has somewhere to explain itself.

RetrievalDone
  1. 0.91 docs/registry-setup.mdx0.88 docs/installation.mdx0.74 docs/theming.mdx0.61 docs/ai.mdx
Retrieval, Done

Installation

Install Tool Trace with the shadcn CLI. The registry item resolves its own dependencies and writes the file to components/joinui/tool-trace.tsx.

pnpm dlx shadcn@latest add @joinui/tool-trace

Usage

example.tsx
import { ToolTrace } from "@/components/joinui/tool-trace"

export function Example() {
  return (
    <ToolTrace
      label="Agent run"
      steps={[
        {
          id: "read",
          name: "read_file",
          summary: "lib/registry/components.ts",
          state: "done",
          meta: "0.3s",
          output: ["→ 1096 lines, 38.2 kB"],
        },
        {
          id: "test",
          name: "run_tests",
          summary: "pnpm check",
          state: "running",
          output: ["✓ typecheck", "✓ lint"],
        },
        { id: "pr", name: "open_pull_request" },
      ]}
    />
  )
}

Props

ToolTrace

Props for ToolTrace
PropTypeDefaultDescription
steps (required)ToolTraceStep[]—The tool calls to render, in order.
labelstring"Run"Header eyebrow, and the accessible name of the list when the header is hidden.
statusstring—Overrides the header chip copy, which is otherwise rolled up from the steps.
variant"card" | "plain""card"Plain drops the surrounding rule, background and padding so the trace can sit inside your own container.
showHeaderbooleantrueRenders the eyebrow and status chip above the steps.
size"sm" | "md""md"Marker, type and spacing scale.
expandedstring[]—Controlled disclosure — the ids of the open steps. Passing it hands the whole disclosure over to you, and turns follow off.
defaultExpandedstring[]—Where an uncontrolled trace starts. Falls back to the steps that set defaultOpen.
onExpandedChange(expanded: string[]) => void—Fires with the new set when the reader opens or closes a step. Follow does not fire it — what it opens is derived from the steps you already own.
multiplebooleantrueAllows more than one step to be open at a time. Off, the trace behaves like an accordion.
followbooleantrueOpens the step that starts running and closes it again when it resolves. Yields permanently on any step the reader opens or closes themselves, and is ignored while expanded is controlled.
streambooleantrueReveals a running step's output a line at a time. Only a step that starts running after mount streams; one that was already running when the page loaded renders whole.
lineSpeednumber90Milliseconds per revealed line.
maxConsoleHeightstring"11rem"Height the console scrolls within. Any CSS length.
announcebooleantrueAnnounces each step's state change through a polite live region. Turn it off when several traces share a page.
footerReact.ReactNode—Content placed below a rule — an action, a note, a summary row.
classNamestring—Merged onto the root element through `cn`.

ToolTraceStep

Shape of a single entry in the `steps` array.

Props for ToolTraceStep
PropTypeDefaultDescription
id (required)string—Stable identity for the rendered list item, and the value reported by onExpandedChange.
name (required)string—The tool that ran. Set in mono, because it is an identifier.
summarystring—Its arguments in one line, set beside the name in the interface face.
state"pending" | "running" | "done" | "failed" | "skipped""pending"Picks the hue and the glyph: blue while running, green when it lands, red when it does not, grey for a step that is queued or was skipped.
metastring—Trailing meta, set in mono with tabular figures — a duration, a token count.
detailReact.ReactNode—Markup revealed under the row, above the console. A step with a detail or an output is the only kind that can be opened.
outputstring[]—The console body, one entry per line. Revealed a line at a time while the step is running.
iconReact.ReactNode—Replaces the state glyph inside the marker. The ring, the rail and the hidden label keep carrying the state. Sized by the component, so pass a bare icon element.
defaultOpenboolean—Opens the step on first render.

Dependencies

npm packages

  • motion
  • lucide-react

Registry items

  • utils

Installed automatically by the CLI when they are missing.

Accessibility

  • Steps render as an ordered list, so assistive technology reports both position and total.
  • The step in flight carries `aria-current="step"`.
  • Only a step with a `detail` or an `output` becomes a button; one with nothing to show stays a plain row, which keeps the tab order down to the steps worth opening.
  • Each toggle carries `aria-expanded` and `aria-controls`, and the panel it opens is a `region` named by it — the WAI accordion pattern, including the optional arrow-key navigation between headers.
  • Colour is never the only signal, which is what satisfies WCAG 1.4.1: every state also has its own glyph — a spinner, a tick, a cross, a dash, a pip — and appends a visually hidden label reading “Running”, “Done”, “Failed”, “Skipped” or “Queued”.
  • A failed step additionally shifts its name to the critical hue and tints its console, so the failure is findable without opening every panel.
  • State changes are announced once each through a single polite live region (`announce`), and the console itself is not live — a log writing itself line by line is unreadable through a screen reader, but “run_tests, Failed” is exactly what someone following the run needs.
  • The console scrolls to its newest line only while the reader is already at the bottom, so reading back through output is never yanked forward.
  • Nothing animates on mount: a trace rendered with a step already running shows that step open and its output whole, rather than replaying a run that happened before the page loaded.
  • The one loop in the component — the marker's pulse and the caret — runs only while a tool is genuinely in flight, and stops when it resolves.
  • Under `prefers-reduced-motion` every transition resolves at zero duration: panels open instantly, output arrives whole, and neither the pulse nor the caret runs.
  • Both themes are covered by the tokens rather than by `dark:` variants, so the component keeps its contrast inside a forced-theme subtree.

Keyboard interactions

Keyboard interactions for Tool Trace
KeyBehaviour
EnterSpaceOpens or closes the focused step, and pins it against `follow`.
↓Moves the focus to the next step that can be opened.
↑Moves the focus to the previous step that can be opened.
HomeEndJumps to the first or last step that can be opened.
TabLeaves the trace. Steps with nothing to show are skipped entirely, and an open panel adds nothing of its own unless you put a control in `detail`.

Customization

Drive it from a real run

The trace is stateless — it draws whatever the steps say — so streaming one is a matter of mapping your events onto states. Everything past a failure reads better as skipped than as queued, because those tools are never going to run.

tsx
const steps = plan.map((tool, index) => ({
  id: tool.id,
  name: tool.name,
  summary: tool.args,
  meta: tool.duration,
  output: tool.stdout,
  state:
    index < cursor
      ? tool.error
        ? "failed"
        : "done"
      : failed
        ? "skipped"
        : index === cursor
          ? "running"
          : "pending",
}))

<ToolTrace label="Agent run" steps={steps} />

One panel at a time

Off, multiple turns the trace into an accordion: opening a step folds the last one shut, which keeps a long chain of retrieval calls to a single screen.

tsx
<ToolTrace
  label="Retrieval"
  size="sm"
  multiple={false}
  defaultExpanded={["search"]}
  steps={steps}
/>

Hold the panels open yourself

Pass expanded and the disclosure is entirely yours — follow steps aside, and nothing opens or closes unless you say so. Useful when the open step has to survive a re-mount, or when it belongs in the URL.

tsx
const [open, setOpen] = React.useState<string[]>(["test"])

<ToolTrace
  label="Agent run"
  steps={steps}
  expanded={open}
  onExpandedChange={setOpen}
/>

Let it sit still

A finished run is a document, not a performance. Turn follow and stream off and every step renders exactly as its state says, which is what you want for a trace loaded from history.

tsx
<ToolTrace
  label="Run 4812"
  steps={steps}
  follow={false}
  stream={false}
  defaultExpanded={steps.filter((step) => step.state === "failed").map((s) => s.id)}
/>

Give each tool its own glyph

An icon replaces the state glyph while the ring, the rail and the hidden state label keep carrying the meaning. The component sizes it, so pass the element bare.

tsx
import { FileText, PenLine, Search, TestTube } from "lucide-react"

<ToolTrace
  label="Run 4812"
  steps={[
    { id: "read", name: "read_file", state: "done", icon: <FileText /> },
    { id: "grep", name: "grep_search", state: "done", icon: <Search /> },
    { id: "edit", name: "edit_file", state: "done", icon: <PenLine /> },
    { id: "test", name: "run_tests", state: "failed", icon: <TestTube /> },
  ]}
/>

Retint the states, and the console

The states read the component palette rather than literal colours, so a brand hue is a token override — no props to thread and no variants to add. The console surface is a wash of the theme's own ink over the card, which is what lets one declaration cover both themes; redeclare it only if you want a console that is a surface in its own right.

css
/* app/globals.css */
:root,
.light {
  /* A violet running state instead of the default blue. */
  --info: oklch(0.5 0.19 292);
  --info-soft: oklch(0.965 0.025 292);
  --info-foreground: oklch(0.99 0.005 292);

  /* Any CSS colour. The default is 4% of the foreground over whatever is behind it. */
  --tool-trace-console: oklch(0.21 0.011 48 / 0.04);
}

.dark {
  --info: oklch(0.76 0.15 292);
  --info-soft: oklch(0.255 0.06 292);
  --info-foreground: oklch(0.16 0.04 292);
  --tool-trace-console: oklch(0 0 0 / 0.25);
}
  • A honeycomb of frosted glass with one tinted tile that travels to whatever you pick, an action, and the queue of runs it dispatches.

  • An inline request from an agent to run a command: the tool, the command, a clock that holds while you read, and two keys to answer with.

  • A colour-coded step tracker for orders, deployments and onboarding, still at rest and animated only when a step advances.