PixelDosa UI

Activity Trail

What the agent did, why it was allowed to, and what came of it — readable, and exportable.

What happened

  1. 09:00

    Scheduler · Started the morning invoice check

    Allowed by: Schedule set by Dana

  2. 09:00

    Finance agent · Read the August import

    ledger/august-import.json

    Allowed by: Autonomy: acts on read-only workResult: 412 rows

  3. 09:02

    Finance agent · Flagged 5 invoices as duplicates

    Allowed by: Autonomy: acts on low-risk changesResult: 5 flagged, none deleted

  4. 09:03

    Dana · Approved emailing the summary

    finance@acme.com · 3 recipients

    Allowed by: Approval gate

  5. 09:03

    Finance agent · Emailed the summary to 3 people

    Allowed by: You approved itResult: Sent

Installation

bash
npx shadcn@latest add @pixeldosa/activity-trail

Requires `cn` at `@/lib/utils`. Write `action` as the consequence ("Emailed the summary to 3 people"), never as a function name.

Usage

"use client";

import { ActivityTrail } from "@/components/ui/activity-trail";

<ActivityTrail entries={entries} onUndo={undo} onExport={exportTrail} />

Why it exists

Audit logging is becoming a legal requirement for high-risk AI, and regulated industries now expect months of retention. But those records are built for auditors. The person whose inbox the agent used needs a different thing: one readable line per action, and the answer to "who let it do that?".

Features

  • The authority is a field. "You approved it", "Autonomy: acts on low-risk changes", "Schedule set by Dana" — the line every log leaves out.
  • Actions read as consequences, not function names.
  • Undo where it still applies, driven by your undoable flag, because only you know what's still reversible.
  • One array, two audiences. onExport hands the same records to your compliance process, so the readable view and the audit record can't drift apart.
  • Oldest first, the way a story is read, so causation runs forwards.

Props

PropTypeNotes
entries{ id, time, actor, actorName?, action, target?, authority?, result?, undoable? }[]actor is agent, person or system. time is formatted by you.
onUndo(id: string) => voidShown on entries you mark undoable.
onExport / exportLabel() => void / string
emptyLabelstring
titlestringDefaults to "What happened".
headingLevel2 | 3 | 4 | 5 | 6Defaults to 3.

Accessibility

  • An ordered list inside a labelled <section>.
  • Every field is text: actor, authority and result are read in order.
  • Nothing animates, so the record never looks like it's still being written.

When not to use

  • A developer event log. Raw tool calls belong in Tool Call Card.
  • A chat transcript. This is what was done, not what was said.
  • As your only compliance store. Export to a system built for retention.

why it's built this way

Engineering Notes

Entries are newest last, the order a story is read in, rather than the newest-first order of a log — this surface is meant to be reconstructed rather than monitored, and reversing it makes causation read backwards. `authority` is a required-shaped optional: nothing enforces it, but its absence in a row is visible, which pushes teams to record what permitted an action at the moment they render it rather than reconstructing it later from three systems. Undo is per entry and driven by the caller's `undoable` flag, because only the caller knows whether a specific action is still reversible; the component never infers reversibility from the action's words. Export exists so the human-readable trail and the compliance record come from one array instead of two implementations that disagree.

how it moves, and when it doesn't

Motion Notes

Nothing animates. Every row describes something that already happened, and this system reserves motion for a machine that is still busy; an entry that slid or faded in would imply the record was still being written. Only the standard instant colour change on Undo, dropped under `prefers-reduced-motion`.