MynduiMyndui
0

Anatomy of the Agent Timeline

How a live agent run is a spring-filled connector, three cross-faded ring states, and a collapsible body that morphs open on its own clock.

01/04The three pieces
Ring
border-color + fill keyed by status
Connector
scaleY 0 → 0.5 → 1, spring 320/32/0.9
Body
height + opacity, collapsible per step
tsx
<li data-status={status} className="relative flex gap-3 pb-2"><div className="relative flex flex-col items-center">  <span className={STATUS_RING[status]}>{/* icon */}</span>  {!last ? (    <span className="relative my-1 w-px flex-1 overflow-hidden rounded bg-border">      <motion.span className={`absolute inset-0 origin-top ${CONNECTOR_FILL[status]}`} />    </span>  ) : null}</div><div className="min-w-0 flex-1 pb-2">{/* title, meta, chevron, body */}</div></li>
01

The three pieces

An agent run isn't a progress bar — it's a rail of independent decisions, each one reporting its own status as it lands. AgentTimeline renders that rail as an <ol> of AgentSteps, and every piece of motion in it is keyed off one prop: status.

Each AgentStep is a ring, a connector, and a body. The ring shows the step's current status. The connector below it fills as that status resolves, drawing the eye down to whatever's next. The body is optional — only steps with children get a chevron and something to expand.

last is the only structural prop — it just hides the connector on the final step, so the rail doesn't dangle a line into nothing.

02/04The connector: one number, three destinations
Pending
hollow ring, dot
Running
spinner + ping, half fill
Success
solid ring, check, full fill
tsx
animate={{scaleY:  status === "success" || status === "error"    ? 1    : status === "running"      ? 0.5      : 0,}}transition={{ type: "spring", stiffness: 320, damping: 32, mass: 0.9 }}
02

The connector: one number, three destinations

The connector is a motion.span inside an overflow-hidden track, scaled from an origin-top. scaleY maps directly off status: 0 while pending, 0.5 while running — a half-fill that reads as "in flight" — and 1 once the step lands on success or error. Every transition rides the same spring: stiffness: 320, damping: 32, mass: 0.9.

A connector that grows by animating height has to re-run layout on every tick — the browser recalculates where every element below it sits, every frame. scaleY on a fixed-height track is a compositor transform: the browser scales an already-painted layer with no layout or paint work at all. The track's own height never changes; only the fill inside it stretches from a pinned top edge.

tsx
 
<span className="relative my-1 w-px flex-1 overflow-hidden rounded bg-border">
  <motion.span className="absolute inset-0 origin-top ..." /> {/* scaleY, not height */}
</span>

The ring's icon swap — dot → spinner → check — is a plain conditional render, not a cross-fade transition; only the connector and the collapsible body get the spring treatment. The running ring also grows a motion-reduce-gated animate-ping halo, layered absolutely over the same box so it never disturbs the ring's own layout.

03/04The collapsible body
Body
scaleY + opacity, spring 320/32/0.9
Chevron
rotate 0 → 90deg, independent curve
tsx
<AnimatePresence initial={false}>{hasBody && open ? (  <motion.div    initial={{ height: 0, opacity: 0 }}    animate={{ height: "auto", opacity: 1 }}    exit={{ height: 0, opacity: 0 }}    transition={{ type: "spring", stiffness: 320, damping: 32, mass: 0.9 }}    className="overflow-hidden"  >    {children}  </motion.div>) : null}</AnimatePresence>
03

The collapsible body

Steps with children get a chevron and an AnimatePresence-wrapped panel. Opening one is the same spring as the connector — height: "auto" and opacity animating together, so the panel never overshoots into visible overflow the way a plain duration tween would.

height: "auto" is the one place this component reaches for a layout animation instead of a transform — Motion measures the panel's natural height and interpolates a real pixel value under the hood, which is why the wrapper needs overflow-hidden to hide the in-between states. It's the right trade-off here: the panel's content is arbitrary (reasoning text, tool output), so there's no fixed size to fake with a transform, and it only happens on a deliberate click, not every frame of a running step.

The chevron is unrelated motion living on the same button — a plain rotate-90 toggle driven by open, with its own transition-transform, so it never has to wait on the spring to know which way it's pointing.

tsx
 
<ChevronIcon
  className={`transition-transform ${open ? "rotate-90" : ""}`}
/>

The title pulses only while status === "running"motion-safe:animate-pulse scoped to that one class, so a step that's already resolved sits perfectly still. Nothing here polls or ticks on an interval; every step is a pure function of the status its caller passes in, so the whole timeline is exactly as busy as the run actually is.

tsx
 
className={`... ${status === "running" ? "text-muted-foreground motion-safe:animate-pulse" : ""}`}
04/04Result
  1. Indexed 128 files across 6 packages.
04

The result

A full run, advancing on its own — watch the connectors spring-fill as each step lands and the running step's body morph open.

One prop drives all of it: status in, a filled connector and a settled ring out. That's the whole timeline — a rail that never has to be told how to move, only what happened.

Accessibility

The step header is a real <button>, disabled when there's no body to open — so keyboard users can Tab to it and toggle with Enter/Space for free, and screen readers get aria-expanded reflecting the live state. Every animate-spin, animate-ping, and motion-safe:animate-pulse in the rail carries a motion-reduce counterpart, and the connector/body springs both collapse to { duration: 0 } under useReducedMotion() — a run still reports its status instantly, just without the flourish.

tsx
 
const reduce = useReducedMotion();
transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 320, damping: 32, mass: 0.9 }}
tsx
 
<button
  type="button"
  disabled={!hasBody}
  aria-expanded={hasBody ? open : undefined}
  onClick={() => hasBody && setOpen((o) => !o)}
>

Motion Score

Agent TimelineDDLayout-triggering
Dheighttimeline row collapse to height:auto
SscaleScale spring / press
SrotateRotation
SopacityFade / cross-fade
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →