AI

Run Summary

The receipt for one finished agent run — outcome and reason, a duration/token/cost stat strip, and an expandable per-model table that says so when its rows don't add up to the headline totals.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import {
  ArrowDown,
  ArrowDownToLine,
  ArrowUp,
  ArrowUpFromLine,
  Ban,
  Check,
  ChevronRight,
  ChevronsUpDown,
  CircleCheck,
  Clock,

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/run-summary.json

Prompt

Build a React + TypeScript + Tailwind "RunSummary" component with zod and
lucide-react. It is the receipt for ONE agent run that has already finished.

Contract
- A zod schema (`runSummaryRunSchema` in a sibling contract file) is the single
  source of truth: { id, label?, startedAt: string|number|Date, durationMs,
  tokensIn, tokensOut, costUsd, costEstimated?, toolCalls, filesTouched?,
  outcome, reason?, models: ModelUsage[] } where ModelUsage is { model, calls?,
  tokensIn, tokensOut, cachedTokensIn?, costUsd? }.
- The outcome union is FOUR values, not two: "succeeded" | "partial" | "failed" |
  "cancelled". `partial` exists because most agent runs end between "done" and
  "broken" — 212 of 214 tool calls landed — and collapsing that into a green
  check teaches the reader to distrust the check. `cancelled` exists because a
  run the human stopped is not a failure, and a receipt that calls it one sends
  people hunting for a bug they caused on purpose.
- Every optional field means "not reported", never zero. `filesTouched: 0` is a
  run that wrote nothing; `filesTouched: undefined` is a provider that does not
  track it — they must not render the same. Same for a model row's `calls` and
  `costUsd`.
- A separate envelope status — "loading" | "empty" | "error" | "ready" — is the
  card's own render state, independent of `outcome`. `status="error"` means the
  SUMMARY failed to load (retryable, offers a Retry); `outcome: "failed"` means
  the RUN failed (not retryable from here). The two read differently on screen.
- Props: run: Run | null; status; defaultOpen?; stats? (which tiles, in order);
  maxModelRows? (6); locale? ("en-US"); currency? ("USD"); timeZone? ("UTC");
  copyable? (true); formatDuration?; formatStartedAt?; onRetry?; errorMessage?;
  emptyState?; label? ("Run summary"); className plus the rest of the element
  props, ref forwarded to the <article>.

Behavior
- THE CARD OWNS NO CLOCK. A run summary is a post-run artifact: it renders
  `durationMs` and `startedAt` as data and never starts a timer, an interval or
  an animation frame. Two renders a day apart of the same record produce the same
  pixels, which is what makes it identical on the server, in a replay and in a
  screenshot fixture. If you need a live elapsed time, you are looking at a
  progress panel, not a receipt.
- RECONCILIATION IS A FIRST-CLASS STATE. Sum the model rows and compare them with
  the headline totals. When tokens disagree, say so in the reader's units ("rows
  account for 412,564 of 446,419 tokens — 33,855 aren't attributed to any
  model"); when cost disagrees by more than half a cent, say by how much and in
  which direction. A receipt whose parts do not add up must admit it — silently
  showing both numbers is how people stop trusting the whole panel.
- The per-model table is COLLAPSED by default when the run reconciles and OPENS
  ITSELF when it does not: that is the one case where the breakdown is not
  optional detail but the explanation for a number the reader is about to doubt.
  The disclosure header carries a quiet "doesn't add up" flag so the state is
  visible while closed. `defaultOpen` pins either behaviour.
- The panel is UNMOUNTED when closed, not height-collapsed: a zero-height
  collapse leaves every sort button reachable by Tab — an invisible keyboard
  trap — and `aria-controls` is only set while open so it never points at an id
  that is not in the document.
- Every column sorts (model, calls, in, out, cost, share) through a real button
  inside the <th>, with `aria-sort` on the header. A new column opens in the
  direction that answers the question you just asked: names A→Z, magnitudes
  biggest-first. Ties break on ARRIVAL ORDER in a direction-independent way, so
  toggling ascending/descending twice restores the exact original order. Rows
  whose `calls` or `costUsd` is missing sort below zero instead of pretending to
  be zero.
- Each row's colour is keyed to its position in the incoming array, never to its
  position after sorting, so re-sorting never repaints a row the reader was
  tracking.
- Truncation is announced. Only `maxModelRows` rows are drawn, above an explicit
  "Show all 13 models" toggle and a "Showing 6 of 13" count — while the totals row
  and the copied receipt keep covering ALL rows. A cap you can see is a cap you
  can trust.
- Copy writes a plain-text digest of the WHOLE run — every model row, in arrival
  order, regardless of the current sort or the visible slice — because what you
  paste into a ticket has to be the whole receipt. The clipboard result is
  visible in all three outcomes (idle / copied / failed): a silent no-op on a
  blocked clipboard reads as a broken button. The reset timer lives in a ref and
  is cleared both on unmount and before a new one is scheduled.
- Costs keep four decimals below a cent (a $0.0037 run must not round to
  "$0.00"), two above it, and anything under $0.0001 renders as "less than
  $0.0001". An unknown locale or a non-ISO currency code falls back to the
  default instead of throwing: a receipt must not take the page down over a typo
  in a prop. An unparseable `startedAt` renders as an em dash, never a RangeError.
- Identity changes reset the card (adjust state during render, no effect): a new
  run id restores the default disclosure, the default sort and the row cap, so a
  recycled card can never show the previous run's expanded state.
- Cache share is derived, not stored: sum `cachedTokensIn` across models and show
  it as a share of the run's input ("55% read from cache") — that is the number
  that explains why a 147k-token run cost $2.61.
- An empty `models` array is a real branch: say "per-model usage wasn't reported
  for this run" instead of drawing an empty table. So is a partially costed
  breakdown: if any row is missing `costUsd`, the cost total is withheld rather
  than under-reporting the bill.

Rendering & styling
- Semantic tokens only: bg-card / border / text-muted-foreground for chrome, and
  one tone table mapping outcome → theme variable — succeeded: var(--chart-2),
  partial: var(--chart-4), failed: var(--destructive), cancelled:
  var(--muted-foreground). The banner tints itself with
  color-mix(in oklab, <tone> 7%, transparent) and the icon and outcome word take
  the tone directly. Per-model rows cycle var(--chart-1..5). No hard-coded
  colours anywhere.
- Four outcomes are four SHAPES — check / triangle / octagon / ban — so they
  survive a monochrome theme and a colour-blind reader, and one persistent
  sr-only role="status" line speaks the whole receipt as a sentence rather than
  as eleven separate numbers.
- The stat strip is a <dl> in a `repeat(auto-fit, minmax(min(9rem,100%), 1fr))`
  grid with a 1px gap over a bg-border parent: the gap draws the dividers, so
  hairlines stay correct at every wrap point without a border that dangles at the
  end of a row. Figures are tabular-nums.
- Long model ids and run ids use `wrap-anywhere` plus `min-w-0`, NOT
  `break-words`: only overflow-wrap:anywhere lowers the element's min-content
  width, which is what actually stops a 70-character gateway model id from making
  the card wider than its column. The table itself keeps its own horizontal
  scroll.
- Numbers, currency and the start time are formatted with a PINNED locale and
  time zone by default. `undefined` would mean "whatever the runtime thinks",
  which differs between the server and the browser and shows up as a hydration
  mismatch on a date string.
- Motion is decorative only: a rotating chevron and a share-bar width transition,
  both motion-reduce-guarded; the skeleton's pulse stops too, and every function
  still works with animation off.
- cn() merges the consumer's className into the root, which also spreads the
  remaining element props, forwards its ref and exposes data-outcome for styling.

Customization levers
- Which tiles: `stats` picks the strip's contents AND their order — a sidebar can
  ask for ["duration","cost"] only. An unknown key is ignored; an empty list
  falls back to all six. A requested tile with no datum renders an em dash rather
  than disappearing, so the layout never reflows behind the reader's back.
- Breakdown budget: `maxModelRows` decides how many rows are drawn before the
  reveal control; raise it for a two-model run, lower it for a gateway sweep, but
  never make it silent — the count is part of the reading.
- Disclosure: `defaultOpen` overrides the reconcile-driven default — true for a
  debugging surface, false for a customer-facing receipt.
- Money and time: `currency` + `locale` swap the formatting wholesale;
  `formatDuration` swaps the wording (a compact "4m51s", a bare seconds count);
  `formatStartedAt` takes over the timestamp entirely (a relative "2 hours ago",
  the viewer's own zone once you know it); `timeZone` alone is enough if you just
  want a different pinned zone.
- Chrome: `label` names the region, `copyable` hides the copy affordance,
  `emptyState` replaces the empty body, `errorMessage` + `onRetry` own the
  envelope failure, and data-outcome lets you restyle a whole card per outcome
  without touching the component.
- Tone: remap the four outcome colours in one table — that is the only place
  colour is decided, and it is where a project with a real "warning" token should
  point `partial`.

Concepts

  • A receipt, not a meter — every number is a fact the run already produced, so the component starts no timer and never derives a state from elapsed time. That is what lets the same record render identically on the server, in a replayed stream and in a screenshot; the moment a card needs a live number it has stopped being a summary and become a progress panel.
  • Reconciliation as a visible state — the per-model rows are summed and checked against the headline totals, and a disagreement is named in the reader's own units (how many tokens went unattributed, how much money the rows are short by). The breakdown opens itself only in that case: detail is optional right up until it is the explanation for a number you are about to doubt.
  • Four outcomes, four shapes — partial is not failure and cancelled is not failure. Painting either red trains people to ignore red, and hiding either behind a green check teaches them to distrust the check; each outcome gets its own icon shape so the distinction survives a monochrome theme.
  • Announced truncation with surviving totals — only a few model rows are drawn, but the count withheld is on screen, the totals row still sums every row, and the copied receipt still contains every row in arrival order. A silent cap makes people debug the wrong thing; an announced one is just a smaller table.
  • Sorting that never repaints — each row's colour is keyed to its position in the incoming array rather than its position on screen, and ties break on arrival order independently of direction, so flipping ascending and descending twice returns the table to exactly where it started.
  • Missing is not zero — an absent per-model cost, an absent call count and an absent files-touched figure all render as an em dash and are excluded from totals, because a zero you cannot tell apart from "not reported" is how a receipt starts lying.

On This Page