Activity Trail

The searches, reads and tool calls behind an answer, so the user can see how it was made.

Pattern

≈ 1,189 tokens per use (tool 636 · guidance 266 · typical payload 287)How token costs are estimated

Example: Activity Trail

Why are users dropping off at the consent screen?

  1. Search: Searched 'consent screen errors'12 results, 1.2 seconds
  2. Read: Read the consent screen help articlehelp.acme.example/consent-screen(opens in a new tab), 0.8 seconds
  3. Search: Searched logs for 'consent_timeout'1,912 events, 3.4 seconds
  4. Read: Read release notes for 4.12Consent moved before paymentrelease-notes-4.12.md, 0.9 seconds
  5. Thinking: Compared drop-off before and after 4.12, 5.8 seconds

Finished. 5 steps, 12 seconds.

When to use

  • Your answer depends on searches, files or tool calls the user may want to check.
  • The work took several steps or noticeable time, and showing it builds trust in the result.

When not to use

  • You answered from general knowledge without using tools. There's nothing to show.
  • There was a single trivial lookup. Mention it in a sentence.
  • You want the user to approve an action before it happens. Use a Change Preview.

How the agent should use it

  • Only list steps you actually performed, in the order you performed them. The steps are data-bound: they come from your own tool calls, never from a plausible-sounding story.
  • Write labels in the past tense and name the thing, e.g. "Searched 'consent screen errors'" or "Read release-notes-4.12.md".
  • Keep detail to a few words: a count, a status code, a short finding.
  • Add input and output to the steps worth checking (a query, a tool call) and leave them off the rest. Keep each to a short excerpt of the real call.
  • Report failures. A failed step stays in the trail as error, with the reason in detail, even when a retry succeeded.

How it works

  • The header sums up the work: step count, total time, and how many steps failed. While a step is running it reads Working…, and the running step's label shimmers.
  • The list starts open. The header toggles it, and the list stays in the DOM while collapsed.
  • Each step is one line: the label, then its outcome or source in quiet text ("12 results", or the page it read as a link). The kind and each step's duration are read to screen readers but not shown; the header carries the total time. Failed steps are shown in the destructive color and announced as failed.
  • Steps can open. A step with an input (what it was given) or an output (what it returned) gets a small chevron. Opening it shows both as short plain-text blocks (inputs are capped at 400 characters and outputs at 600, so they never need to scroll). With two or more such steps the header offers Expand all and Collapse all. They're short excerpts from the real call, never the raw payload, and never secrets or personal data.
  • A live clock. While the trail is working the header counts up each second; once it finishes it shows the steps' own durations, summed. A trail restored from history never ticks.
  • Targets can link out. Only http: and https: URLs become links, and they open in a new tab with rel="noopener noreferrer nofollow".
  • Streaming. Steps appear as they arrive and flip from running to done or error. When the work finishes, a polite live region announces the summary once.

Installation

npx shadcn@latest add https://your-site.com/r/activity-trail.json

Usage

The Activity Trail is display-only, so the tool can return immediately:

app/api/chat/route.ts
import { tool } from "ai"
import { activityTrailTool } from "@/components/genui/activity-trail.schema"

const tools = {
  [activityTrailTool.name]: tool({
    description: activityTrailTool.description,
    inputSchema: activityTrailTool.inputSchema,
    execute: async () => ({ shown: true }),
  }),
}

Render it through the Renderer under show_activity, or directly:

<ActivityTrail {...part.input} status={part.state === "input-streaming" ? "streaming" : "ready"} />

A host can also build the trail from the run's own tool calls instead of asking the agent to describe them. Map each tool part to a step:

<ActivityTrail
  steps={toolParts.map((p) => ({
    id: p.toolCallId,
    kind: "tool",
    label: describeCall(p),
    status: p.state === "output-available" ? "done" : p.state === "output-error" ? "error" : "running",
  }))}
/>

With LangGraph

Push it from a graph node with ui.push({ name: "show_activity", props }, { message }), and update it with the same id as steps finish. Render the thread's UI messages with the Renderer.

Schema

PropTypeNotes
titlestring?e.g. "Researched consent screen errors". The header always shows the step count
stepsStep[]1–40 steps. Data-bound

Step

FieldTypeNotes
idstringKeep it stable while the step updates
kindsearch | read | tool | think | writeOptional. Read to screen readers ("Search: …")
labelstringWhat was done, past tense
detailstring?Short outcome: "12 results", "Rate limited (429)"
statusrunning | done | errorDefault done
durationMsnumber?Omit while running. Summed in the header; per step it's spoken only
target{ label, url? }?The file, page or record. Only http(s) URLs are linked
inputstring?What the step was given, up to 400 characters. Shown when the step is opened
outputstring?What the step returned, up to 600 characters. Shown when the step is opened

Host props: status (streaming or ready) and className.

Source

On this page