Agent Console — replaying what an AI agent actually did
Self-initiated concept. Not a client project, and no client data appears in it. Design and build: Canon Chong · ~2 days · Next.js 16, React 19, Tailwind v4, custom design system
The problem
When an AI agent fails, the logs tell you that it failed. They rarely tell you why.
A production agent run is a tree, not a line: the model plans, calls a tool, reads the result, delegates to a sub-agent, retries something that timed out. Flatten that into a log stream and you lose the two things you actually need — what nested inside what, and how long each piece really took. The team ends up scrolling a wall of JSON trying to reconstruct a sequence that the system already knew and threw away.
So the question this concept asks is narrow: what would it take to watch a run happen, instead of reading about it afterwards?
Constraints I set
- No backend. A portfolio piece nobody can click is worth nothing, so everything runs
from a pre-recorded event stream. That is a constraint, not a shortcut — see below.
- One surface deep, not five shallow. The nav shows Runs, Agents, Evals, Tools, Costs.
Only Runs is built. The other four are explicit shells that show their real column sets and say plainly that they are out of scope.
- Everything on the design system. No hard-coded colours, no arbitrary values. If a value
can't be expressed with a semantic token, the token layer is wrong and gets fixed.
- Motion has to degrade.
prefers-reduced-motionis a requirement, not a nice-to-have.
Decisions
The event stream is the single source of truth
Every span carries an absolute offset from run start and its own end time. Not a nesting level, not a width percentage — real timestamps.
That one choice is what makes the rest possible. The flame graph derives bar position and width from it. The replay derives the playhead from it. The list page derives step count, token totals, duration, and cost from it. There is no second copy of any number, so the list and the detail page cannot disagree — a class of bug that would otherwise show up the first time someone edited one and forgot the other.
Cost is computed from token counts and per-model pricing rather than stored, for the same reason.
Replay is the differentiator, so it got the most care
A scrubber, play/pause, and 1×/2×/4× speed. Playing walks the trace on its real timeline: spans light up as the cursor reaches them, bars fill proportionally as they run, and the inspector follows the playhead so the right panel always describes the moment you're looking at.
The state model is deliberately one variable — a cursor in milliseconds, initialised to the run's total duration. "Everything lit" is just the cursor sitting at the end, so the resting view and the replay view are the same code path rather than two modes that drift apart.
Two interaction calls worth naming:
- Clicking a span pauses playback. Without it, the next animation frame overwrites your
selection and the click looks broken.
- Failed spans get a red tick on the scrubber. You can drag straight to the part that went
wrong without reading the tree first.
Reduced motion steps instead of sweeping
The design system already kills CSS transitions under prefers-reduced-motion. But the replay is a requestAnimationFrame loop — JS-driven motion that CSS cannot reach. Honouring the preference in CSS alone would have looked correct and done nothing.
So the fallback lives in the playback engine: instead of a continuous sweep, the cursor steps from one span boundary to the next. The feature survives; the continuous movement doesn't. Verified by measurement — 3.6 seconds of playback produces 6 discrete cursor values under reduced motion versus roughly 24 in continuous mode.
Restraint over spectacle
Flame graphs invite rainbow palettes. This one uses three hues with meaning: amber for model calls, azure for external tools, neutral for orchestration. Red is reserved for failure so it never competes with decoration. Two border weights, applied by rule — structural separators between regions and rows, subtle ones for dividers inside a row and control outlines.
The part that took the longest, and why it matters
Four bugs in the design system layer. All four compiled cleanly. None produced a warning. All four failed silently in the browser:
1. Cascade layer order was never pinned. The design system declared @layer utilities before Tailwind declared its layer order, and CSS orders layers by first appearance — so utilities landed before base, and Tailwind's preflight reset overrode every utility in the app. Page headings rendered at body size; every border computed to 0px.
2. The design system's base reset was unlayered. Unlayered rules beat every cascade layer, so * { margin: 0; padding: 0 } silently defeated every spacing utility in the codebase.
3. Border tokens had no Tailwind mapping, so call sites had reached for Tailwind v3's border-[--token] syntax. In v4 that emits border-color: --token — an invalid value the browser discards. Every divider in the app was invisible.
4. Motion tokens used the wrong namespace. Tailwind v4 resolves duration-* from --transition-duration-*, not --duration-*, so none of the timing tokens generated a class.
I found them by reading the compiled CSS and the computed styles in the browser, not by looking at the source — the source looked fine, which is exactly the problem.
The fix in each case was to correct the token layer rather than work around it at the call site. A designer who can only write border-[#26201C] when the token doesn't resolve will keep the system consistent for about a week. Being able to tell "the token is missing" from "the token is broken" is most of what a design system is worth.
Result
A run detail view where the trace, the inspector, and the replay all read from one event stream, built on a token layer that now behaves the way it claims to.
Three recorded runs: a clean success, a timeout where one span eats 98% of the wall clock, and — the one worth watching — a tool call that fails on gateway backpressure, gets health-checked, retried, and succeeds, with the run still landing green.
What it demonstrates: that I understand the shape of agent products well enough to know which single view carries the weight, and that I'll go and fix the cascade layer instead of papering over it.
Concept work, built to explore the problem. No affiliation with any AI infrastructure vendor.