All work

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

from a pre-recorded event stream. That is a constraint, not a shortcut — see below.

five are built. They are not five independent datasets: everything the traces already know — latency, error profiles, token counts, cost — is computed from the same spans the flame graph draws. Only genuinely non-derivable facts are authored by hand, which is a short list: team ownership, timeout configuration, the budget.

can't be expressed with a semantic token, the token layer is wrong and gets fixed.

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:

selection and the click looks broken.

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

Five bugs in the design system layer. All five compiled cleanly. None produced a warning. All five 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.

5. A global focus rule was overriding component geometry. :focus-visible set border-radius, and like bug 2 it was deliberately unlayered so it could beat Tailwind's preflight. But unlayered beats every layer, so it silently squared off the corners of any focused element regardless of specificity — pill-shaped components snapped from 999px to 4px on focus and sprang back on blur. The declaration also turned out to be unnecessary: outline already follows an element's own border-radius.

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 fifth one surfaced months after the first four, when a floating panel became the first rounded thing in the system to take focus. That is the honest version of the story: a system doesn't get audited into correctness, it gets used into it.

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.

Open Agent ConsoleStart a conversation