Skip to content
Star on GitHub
Agent statusAgent lanes
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Agent lanes

Several agents working in parallel, each with its own lane and hand-offs. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add agent-lanes
3 agents working0:000 of 9 steps
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add agent-lanes

React

$npx designme add agent-lanes --react

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

02

About

What it is for and what it does.

Agent lanes shows several agents working on one job, each in its own row and its own colour, with a name, what it is doing right now, a step track and a status tag. A relay line joins the agents' badges. When one agent passes work to another, a light runs down that line to the next badge, a line like "Researcher handed the brief to Designer" opens above the lane that takes it, and the stretch of line it crossed stays lit.

  • When your product runs several agents with different roles, like a researcher, a designer and a reviewer, on one task.
  • When some agents wait on others and people need to see who is blocked and why.
  • When people should be able to pause every agent at once and resume them later.
  • When each agent's own log should be one click away without crowding the overview.
03

Usage

Import it and drive it from your own state.

Import

import { AgentLanes } from '@/components/designme/agent-lanes/AgentLanes';

Example

The demo above as a React component, 63 lines you can copy.

'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useEffect, useRef, useState } from 'react';
import { AgentLanes, type AgentHandoff, type AgentLane, type LaneTone } from './AgentLanes';
import type { IconName } from '../lib/icons';
import { useScript } from '../lib/useScript';

/* ---------- Agent lanes: three agents work in turn, each step filling as it goes, and hand the work along; Pause all holds them ---------- */
const PLAN: { key: string; name: string; icon: IconName; tone: LaneTone; wait?: string; acts: string[]; log: string[] }[] = [
  { key: 'researcher', name: 'Researcher', icon: 'search', tone: 'blue', acts: ['Reading competitor sites', 'Comparing 5 pricing pages', 'Writing the brief'], log: ['Read stripe.com/pricing', 'Read linear.app/pricing', 'Noted 3 patterns'] },
  { key: 'designer', name: 'Designer', icon: 'palette', tone: 'purple', wait: 'Waiting for the brief', acts: ['Laying out pricing cards', 'Setting type and spacing', 'Exporting frames'], log: ['Made 3 card variants', 'Applied spacing tokens', 'Exported 4 frames'] },
  { key: 'reviewer', name: 'Reviewer', icon: 'eye', tone: 'green', wait: 'Waiting for the designer', acts: ['Checking contrast', 'Checking copy', 'Signing off'], log: ['Contrast passes AA', 'Fixed 2 lines of copy', 'Approved'] }
];

export default function AgentLanesDemo() {
  const [lanes, setLanes] = useState<AgentLane[]>(() => PLAN.map(p => ({ key: p.key, name: p.name, icon: p.icon, tone: p.tone, status: 'queued', activity: p.wait ?? 'Queued', done: 0, log: [] })));
  const [handoff, setHandoff] = useState<AgentHandoff>();
  const [paused, setPaused] = useState(false);
  const [secs, setSecs] = useState(0);
  const [finished, setFinished] = useState(false);
  // the script reads these while it runs
  const live = useRef({ paused, secs });
  useEffect(() => { live.current = { paused, secs }; });
  // the clock ticks on a steady beat and skips the beats that fall in a pause
  useEffect(() => {
    if (finished) return;
    const id = setInterval(() => { if (!live.current.paused) setSecs(s => s + 1); }, 1000);
    return () => clearInterval(id);
  }, [finished]);

  useScript(async ({ wait }) => {
    const set = (key: string, patch: Partial<AgentLane> | ((l: AgentLane) => Partial<AgentLane>)) =>
      setLanes(ls => ls.map(l => l.key === key ? { ...l, ...(typeof patch === 'function' ? patch(l) : patch) } : l));
    // work only moves on while the agents are not paused
    const hold = async (ms: number, each?: (p: number) => void) => { for (let t = 0; t < ms;) { await wait(100); if (!live.current.paused) { t += 100; each?.(t / ms); } } };
    const work = async (p: (typeof PLAN)[number]) => {
      for (let i = 0; i < 3; i++) {
        set(p.key, { status: 'working', activity: p.acts[i], done: i, progress: 0 });
        await hold(1500, f => set(p.key, { progress: f }));
        const time = live.current.secs + 's';
        set(p.key, l => ({ done: i + 1, log: [...(l.log ?? []), { text: p.log[i], time }] }));
      }
      set(p.key, { status: 'done', activity: p.log[2] });
    };
    // the line opens, the light runs down to the next badge (about 1.5s), then the line closes
    const hand = async (from: string, to: string, what: string) => { setHandoff({ from, to, what }); await wait(2750); setHandoff(undefined); await wait(700); };

    await wait(600);
    const first = work(PLAN[0]); set('designer', { status: 'waiting' });
    await first; await hand('researcher', 'designer', 'the brief');
    set('reviewer', { status: 'waiting' });
    await work(PLAN[1]); await hand('designer', 'reviewer', '4 frames');
    await work(PLAN[2]);
    setFinished(true);
  });

  return (
    <div style={{ width: '100%' }}>
      <AgentLanes lanes={lanes} handoff={handoff} seconds={secs} paused={paused} onPausedChange={setPaused} />
    </div>
  );
}
04

Props

Everything AgentLanes takes.

PropTypeWhat it does
lanesrequiredAgentLane[]
titlestringHeader text. Defaults to "3 agents working", "All agents paused" or "All 3 agents finished"
secondsnumberSeconds to show as m:ss. Leave it out to let the header count while agents work
pausedbooleanWhether every agent is paused. Working lanes then show Paused and the clock stops
defaultPausedboolean
onPausedChange(paused: boolean) => voidAdds the Pause all / Resume button, until every lane is done
handoffAgentHandoffThe handoff to show. Set it back to undefined and its line slides away; the stretch of relay line it crossed stays lit
expandedstring[]Keys of the lanes that are open. Leave it out to let each row toggle itself
defaultExpandedstring[]
onExpandedChange(keys: string[]) => void
barebooleanSits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own
classNamestring
05

Code

The full source, as the CLI installs it.

AgentLanes.tsxReact · 209 lines
'use client';
import { Fragment, useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
import { cx } from '../lib/cx';
import { useControllable, useReducedMotion, useSeconds } from '../lib/hooks';
import { Icon, type IconName } from '../lib/icons';
import { enter, Fold, Swap } from '../lib/motion';
import '../designme.css';
import './agent-lanes.css';

export type LaneStatus = 'queued' | 'waiting' | 'working' | 'paused' | 'done';
export type LaneTone = 'blue' | 'purple' | 'green' | 'orange' | 'red' | 'yellow' | 'pink' | 'teal';

export interface AgentLane {
  key: string;
  /** Like "Researcher" */
  name: string;
  icon: IconName;
  /** The lane's hue: its badge, its track, its Working tag and its stretch of the relay line. Defaults to blue */
  tone?: LaneTone;
  /** working breathes, shimmers and fills the next step, paused holds it, done fills the badge with its hue */
  status: LaneStatus;
  /** What the agent is doing now, or what it is waiting for. It crossfades when it changes, and shimmers while the agent works */
  activity: string;
  /** How many steps the track is cut into. Defaults to 3 */
  steps?: number;
  /** How many steps are finished. The next one fills while the agent works */
  done?: number;
  /** How far the agent is through the step in hand, from 0 to 1. Leave it out and that step fills part way */
  progress?: number;
  /** What the agent did, shown when the lane is opened. New entries slide in */
  log?: { text: string; time?: string }[];
}

/** One agent passing work to another: a light runs down the relay line between their badges, with a line saying what changed hands */
export interface AgentHandoff {
  /** Key of the lane handing over */
  from: string;
  /** Key of the lane taking it */
  to: string;
  /** What changes hands, like "the brief" */
  what: string;
}

export interface AgentLanesProps {
  lanes: AgentLane[];
  /** Header text. Defaults to "3 agents working", "All agents paused" or "All 3 agents finished" */
  title?: string;
  /** Seconds to show as m:ss. Leave it out to let the header count while agents work */
  seconds?: number;
  /** Whether every agent is paused. Working lanes then show Paused and the clock stops */
  paused?: boolean;
  defaultPaused?: boolean;
  /** Adds the Pause all / Resume button, until every lane is done */
  onPausedChange?: (paused: boolean) => void;
  /** The handoff to show. Set it back to undefined and its line slides away; the stretch of relay line it crossed stays lit */
  handoff?: AgentHandoff;
  /** Keys of the lanes that are open. Leave it out to let each row toggle itself */
  expanded?: string[];
  defaultExpanded?: string[];
  onExpandedChange?: (keys: string[]) => void;
  /** Sits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own */
  bare?: boolean;
  className?: string;
}

const TONE: Record<LaneTone, string> = { blue: 'pb', purple: 'pl', green: 'pm', orange: 'pp', red: 'pr', yellow: 'py', pink: 'pk', teal: 'pt' };
const hue = (l?: AgentLane) => TONE[l?.tone ?? 'blue'];

/** How many times `value` has changed since the first render, so a part can play its entrance on a change but not on mount */
function useChanges<T>(value: T) {
  const [seen, setSeen] = useState({ value, n: 0 });
  if (Object.is(seen.value, value)) return seen.n;
  setSeen({ value, n: seen.n + 1 });
  return seen.n + 1;
}

/** Keeps the last value on screen for `ms` after it is cleared, so it can play its exit */
function usePresence<T>(value: T | undefined, ms = 700) {
  const [kept, setKept] = useState(value);
  if (value !== undefined && value !== kept) setKept(value);
  useEffect(() => {
    if (value !== undefined || kept === undefined) return;
    const t = setTimeout(() => setKept(undefined), ms);
    return () => clearTimeout(t);
  }, [value, kept, ms]);
  return { shown: value ?? kept, leaving: value === undefined && kept !== undefined };
}

/** The hand-off line: it mounts shut and opens on the next frame, so the lanes below glide down, then closes before it goes */
function Handoff({ leaving, lit, style, children }: { leaving: boolean; lit: boolean; style: CSSProperties; children: ReactNode }) {
  const [open, setOpen] = useState(false);
  useEffect(() => { let b = 0; const a = requestAnimationFrame(() => { b = requestAnimationFrame(() => setOpen(true)); }); return () => { cancelAnimationFrame(a); cancelAnimationFrame(b); }; }, []);
  const e = enter(80);
  return (
    <div className={cx('dm-fold dm-aln-hf', !(open && !leaving) && 'dm-shut', lit && 'dm-lit')} style={style} inert={!(open && !leaving)} aria-hidden={!(open && !leaving)}>
      <div><div className={cx('dm-aln-hand', e.className)} style={e.style}>{children}</div></div>
    </div>
  );
}

function StatusTag({ status }: { status: LaneStatus }) {
  const moved = useChanges(status) > 0 ? 'dm-enter' : undefined;
  if (status === 'working') return <span key="working" className={cx('dm-tag dm-aln-tg', moved)}><span className="dm-spin" aria-hidden="true" />Working</span>;
  if (status === 'paused') return <span key="paused" className={cx('dm-tag dm-warn', moved)}>Paused</span>;
  if (status === 'done') return <span key="done" className={cx('dm-tag dm-ok', moved)}><Icon name="check" size="xs" />Done</span>;
  if (status === 'waiting') return <span key="waiting" className={cx('dm-tag', moved)}><Icon name="clock" size="xs" />Waiting</span>;
  return <span key="queued" className={cx('dm-tag', moved)}>Queued</span>;
}

function Lane({ lane, paused, open, sent, got, ping, onToggle }: { lane: AgentLane; paused: boolean; open: boolean; sent: boolean; got: boolean; ping: boolean; onToggle: () => void }) {
  const status = paused && lane.status === 'working' ? 'paused' : lane.status;
  const tone = hue(lane), steps = lane.steps ?? 3, done = lane.done ?? 0;
  const live = status === 'working' || status === 'paused';
  return (
    <div className={cx('dm-aln-lane', sent && 'dm-sent', got && 'dm-got')} data-s={status} data-lane={lane.key} style={{ '--dm-c': `var(--dm-${tone})`, '--dm-cb': `var(--dm-${tone}-bg)`, '--dm-cm': `var(--dm-aln-${tone},var(--dm-${tone}))` } as CSSProperties}>
      <button type="button" className="dm-aln-row" aria-expanded={open} onClick={onToggle}>
        <span className={cx('dm-aln-av', ping && 'dm-ping')}>
          <Icon name={lane.icon} />
        </span>
        <span className="dm-aln-who"><span className="dm-aln-n">{lane.name}</span><Swap className={cx('dm-aln-a', live && 'dm-live', status === 'working' && 'dm-shim')}>{lane.activity}</Swap></span>
        <span className="dm-aln-tr" aria-hidden="true">
          {Array.from({ length: steps }, (_, i) => {
            const run = i === done && live;
            return <i key={i} className={cx(i < done && 'dm-on', run && 'dm-run', run && lane.progress === undefined && 'dm-ind')}
              style={run && lane.progress !== undefined ? { '--dm-p': Math.max(0, Math.min(1, lane.progress)) } as CSSProperties : undefined} />;
          })}
        </span>
        <span className="dm-aln-st"><StatusTag status={status} /></span>
      </button>
      <Fold open={open}>
        <div className="dm-aln-det">
          {lane.log?.map((l, i) => <div key={i} className="dm-enter"><span>{l.text}</span>{l.time && <span className="dm-muted">{l.time}</span>}</div>)}
        </div>
      </Fold>
    </div>
  );
}

const clock = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;

/** Several agents working side by side, each in its own lane and hue, handing work to one another down a relay line. */
export function AgentLanes({ lanes, title, seconds, paused, defaultPaused = false, onPausedChange, handoff, expanded, defaultExpanded = [], onExpandedChange, bare = true, className }: AgentLanesProps) {
  const [isPaused, setPaused] = useControllable(paused, defaultPaused, onPausedChange);
  const [open, setOpen] = useControllable(expanded, defaultExpanded, onExpandedChange);
  const finished = lanes.length > 0 && lanes.every(l => l.status === 'done');
  const counted = useSeconds(seconds === undefined && !isPaused && !finished);
  const { shown: hand, leaving } = usePresence(handoff);
  const reduce = useReducedMotion();
  const rows = useRef<HTMLDivElement>(null);
  // the hand-offs whose light has arrived: their stretch of the relay line stays lit
  const [relayed, setRelayed] = useState<string[]>([]);
  const [ping, setPing] = useState<string>();
  const handKey = hand ? `${hand.from}>${hand.to}` : '';
  useEffect(() => {
    if (!handoff) return;
    const { from, to } = handoff, key = `${from}>${to}`, box = rows.current;
    let an: Animation | undefined, pk: HTMLSpanElement | undefined, t2 = 0;
    const arrive = () => { pk?.remove(); setRelayed(r => r.includes(key) ? r : [...r, key]); setPing(to); t2 = window.setTimeout(() => setPing(p => p === to ? undefined : p), 950); };
    // the line opens first, then the light runs from the sender's badge down to the taker's
    const t1 = window.setTimeout(() => {
      const a = box?.querySelector(`[data-lane="${CSS.escape(from)}"] .dm-aln-av`), b = box?.querySelector(`[data-lane="${CSS.escape(to)}"] .dm-aln-av`);
      if (!box || !a || !b || reduce) return arrive();
      const rr = box.getBoundingClientRect(), y0 = a.getBoundingClientRect().bottom - rr.top + 4, y1 = b.getBoundingClientRect().top - rr.top - 4;
      const f = lanes.find(l => l.key === from), g = lanes.find(l => l.key === to);
      pk = document.createElement('span'); pk.className = 'dm-aln-pk'; pk.setAttribute('aria-hidden', 'true');
      pk.style.setProperty('--dm-from', `var(--dm-${hue(f)})`); pk.style.setProperty('--dm-to', `var(--dm-${hue(g)})`);
      box.appendChild(pk);
      an = pk.animate([{ transform: `translateY(${y0}px)`, opacity: 0 }, { opacity: 1, offset: .12 }, { opacity: 1, offset: .88 }, { transform: `translateY(${y1}px)`, opacity: 0 }], { duration: 950, easing: 'cubic-bezier(.65,0,.35,1)' });
      an.finished.then(arrive, () => {});
    }, reduce ? 0 : 500);
    return () => { clearTimeout(t1); clearTimeout(t2); an?.cancel(); pk?.remove(); };
  }, [handoff?.from, handoff?.to]); // eslint-disable-line react-hooks/exhaustive-deps

  const nameOf = (key: string) => lanes.find(l => l.key === key)?.name ?? key;
  const toneOf = (key: string) => hue(lanes.find(l => l.key === key));
  const head = title ?? (finished ? `All ${lanes.length} agents finished` : isPaused ? 'All agents paused' : `${lanes.length} agents working`);
  const total = lanes.reduce((n, l) => n + (l.steps ?? 3), 0), stepsDone = lanes.reduce((n, l) => n + Math.min(l.done ?? 0, l.steps ?? 3), 0);

  return (
    <div data-dm="" className={cx('dm-aln', !bare && 'dm-panel', finished && 'dm-fin', className)} style={bare ? { paddingInline: 0 } : undefined}>
      <div className="dm-aln-h">
        <span className="dm-aln-ic">{finished ? <span key="ok" {...enter()} style={{ display: 'grid' }}><Icon name="ok" /></span> : <Icon name="users" />}</span>
        <Swap className="dm-aln-t">{head}</Swap>
        <span className="dm-aln-m dm-muted dm-num">{clock(seconds ?? counted)}</span>
        <Swap className="dm-aln-ct dm-num">{`${stepsDone} of ${total} steps`}</Swap>
        {onPausedChange && !finished && (
          <button type="button" className="dm-pill dm-soft dm-sm" onClick={() => setPaused(!isPaused)}>
            <Icon name={isPaused ? 'play' : 'pause'} size="xs" />{isPaused ? 'Resume' : 'Pause all'}
          </button>
        )}
      </div>
      <div className="dm-aln-rows" ref={rows}>
        {lanes.map(l => (
          <Fragment key={l.key}>
            {hand?.to === l.key && (
              <Handoff key={handKey} leaving={leaving} lit={relayed.includes(handKey)} style={{ '--dm-from': `var(--dm-${toneOf(hand.from)})`, '--dm-to': `var(--dm-${toneOf(hand.to)})` } as CSSProperties}>
                <b className="dm-f">{nameOf(hand.from)}</b> handed {hand.what} to <b className="dm-t">{nameOf(hand.to)}</b>
              </Handoff>
            )}
            <Lane lane={l} paused={isPaused} open={open.includes(l.key)} ping={ping === l.key}
              sent={relayed.some(k => k.startsWith(l.key + '>'))} got={relayed.some(k => k.endsWith('>' + l.key))}
              onToggle={() => setOpen(open.includes(l.key) ? open.filter(k => k !== l.key) : [...open, l.key])} />
          </Fragment>
        ))}
      </div>
    </div>
  );
}
agent-lanes.cssReact · 95 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Agent lanes ---------- */
/* each agent has its own hue (--c, with its tint --cb): its badge, its track, its tag and its stretch of the relay line that
   joins the badges. A hand-off sends a light down that line from one agent to the next, and the stretch it crossed stays lit */
.dm-aln{padding:var(--dm-s-2)}
/* the lanes' marks (tracks, lines, a finished badge) in livelier versions of the agents' hues, each 3:1 on the surface; their
   text keeps the agent's text colour. Dark mode's agent colours are light already, so they are used as they are */
.dm-aln{--dm-aln-pb:#2a7bff;--dm-aln-pm:#1fa66a;--dm-aln-pl:#7a5af5}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .dm-aln{--dm-aln-pb:var(--dm-pb);--dm-aln-pm:var(--dm-pm);--dm-aln-pl:var(--dm-pl)}}
:root[data-theme="dark"] .dm-aln{--dm-aln-pb:var(--dm-pb);--dm-aln-pm:var(--dm-pm);--dm-aln-pl:var(--dm-pl)}
/* on a card the lanes sit centred in the room kept for their tallest moment, and glide as a hand-off line opens and shuts */
.dm-stage-in:has(>.dm-aln){display:flex;flex-direction:column;justify-content:center}
.dm-aln-h{display:flex;align-items:center;gap:10px;padding:var(--dm-s-2) 10px 10px}
.dm-aln-ic{color:var(--dm-fg2);display:grid;transition:color .3s}
.dm-aln.dm-fin .dm-aln-ic{color:var(--dm-ok)}
.dm-aln-t{font-weight:500}
.dm-aln-m{font-size:12.5px}
.dm-aln-ct{font-size:12.5px;color:var(--dm-muted);margin-right:auto;white-space:nowrap}
.dm-aln-ct::before{content:"·";margin-right:6px}
.dm-aln-rows{position:relative}
.dm-aln-rows>*{position:relative}
.dm-aln-lane{border-radius:14px;transition:background .3s}
@media (hover:hover) and (pointer:fine){.dm-aln-lane:hover{background:var(--dm-row)}}
/* the lane at work carries a wash of its hue from the left */
.dm-aln-lane[data-s="working"]{background:linear-gradient(90deg,color-mix(in srgb,var(--dm-c) 8%,transparent),transparent 72%)}
@media (hover:hover) and (pointer:fine){.dm-aln-lane[data-s="working"]:hover{background:linear-gradient(90deg,color-mix(in srgb,var(--dm-c) 8%,transparent),transparent 72%),var(--dm-row)}}
/* the relay line runs from badge to badge, through the lanes and the hand-offs between them, and stops short of each badge */
.dm-aln-rows>*::before{content:"";position:absolute;left:26px;width:2px;top:0;bottom:0;border-radius:1px;background:var(--dm-line);transition:background .5s var(--dm-ease);pointer-events:none}
.dm-aln-lane::before{top:48px}
.dm-aln-lane:last-child::before{display:none}
.dm-aln-lane::after{content:"";position:absolute;left:26px;width:2px;top:0;height:6px;border-radius:1px;background:var(--dm-line);transition:background .5s var(--dm-ease);pointer-events:none}
.dm-aln-lane:first-child::after{display:none}
.dm-aln-lane.dm-sent::before{background:linear-gradient(var(--dm-cm),color-mix(in srgb,var(--dm-cm) 45%,var(--dm-line)))}
.dm-aln-lane.dm-got::after{background:var(--dm-cm)}
.dm-aln-hf.dm-lit::before{background:linear-gradient(color-mix(in srgb,var(--dm-from) 45%,var(--dm-line)),var(--dm-to))}
.dm-aln-row{display:grid;grid-template-columns:34px minmax(0,1.2fr) minmax(0,1fr) auto;align-items:center;gap:var(--dm-s-3);width:100%;padding:10px;text-align:left}
.dm-aln-av{position:relative;width:34px;height:34px;border-radius:11px;display:grid;place-items:center;color:var(--dm-c);background:var(--dm-cb);transition:color .4s,background .4s}
.dm-aln-av::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none}
.dm-aln-lane[data-s="working"] .dm-aln-av{animation:dm-alnbreathe 2.4s var(--dm-ease-io) infinite}
@keyframes dm-alnbreathe{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--dm-c) 0%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--dm-c) 18%,transparent)}}
/* the badge that takes a hand-off rings once as the light reaches it */
.dm-aln-av.dm-ping::before{animation:dm-alnping .9s var(--dm-ease)}
@keyframes dm-alnping{from{box-shadow:0 0 0 0 color-mix(in srgb,var(--dm-c) 50%,transparent)}to{box-shadow:0 0 0 12px color-mix(in srgb,var(--dm-c) 0%,transparent)}}
/* a finished agent's badge fills with its hue */
.dm-aln-lane[data-s="done"] .dm-aln-av{background:var(--dm-cm);color:var(--dm-surface)}
.dm-aln-who{min-width:0}
.dm-aln-n{display:block;font-weight:500;font-size:13.5px}
.dm-aln-a{display:block;font-size:12.5px;color:var(--dm-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-aln-a.dm-live{color:var(--dm-fg2)}
/* the track: one part a step. Finished parts are solid, the one in hand fills as the agent works, with a light running along it */
.dm-aln-tr{display:flex;gap:3px}
.dm-aln-tr i{flex:1;height:10px;border-radius:5px;background:color-mix(in srgb,var(--dm-cm) 13%,var(--dm-line));position:relative;overflow:hidden}
.dm-aln-tr i::after{content:"";position:absolute;inset:0;border-radius:inherit;background:var(--dm-cm);transform:scaleX(var(--dm-p,0));transform-origin:left;transition:transform .25s linear}
.dm-aln-tr i.dm-on::after{transform:scaleX(1);transition:transform .5s var(--dm-ease)}
.dm-aln-tr i.dm-run::after{background:linear-gradient(90deg,var(--dm-cm) 0 30%,color-mix(in srgb,var(--dm-cm) 30%,var(--dm-surface)) 50%,var(--dm-cm) 70% 100%) 0 0/250% 100%;animation:dm-alnsheen 1.6s linear infinite}
/* given no step progress, the part in hand fills part way and keeps its light */
.dm-aln-tr i.dm-run.dm-ind::after{transform:scaleX(.6);transition:transform 2.4s var(--dm-ease)}
@keyframes dm-alnsheen{from{background-position:100% 0}to{background-position:-150% 0}}
.dm-aln-st{min-width:92px;display:flex;justify-content:flex-end}
.dm-aln-tg{background:var(--dm-cb);color:var(--dm-c)}
.dm-aln-tg .dm-spin{width:9px;height:9px}
.dm-aln-det{padding:0 10px 10px 56px;font-size:12.5px;color:var(--dm-fg2)}
.dm-aln-det div{display:flex;align-items:center;gap:var(--dm-s-2);padding:3px 0}
.dm-aln-det div::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--dm-c);flex:none}
.dm-aln-det .dm-muted{margin-left:auto}
/* a hand-off opens a line between the lanes, glides the lanes below down and closes again */
.dm-aln-hf{transition-duration:var(--dm-t-slow)}
.dm-aln-hand{padding:6px 10px 6px 56px;font-size:12.5px;line-height:18px;color:var(--dm-fg2)}
.dm-aln-hand b{font-weight:500}
.dm-aln-hand .dm-f{color:var(--dm-from)}.dm-aln-hand .dm-t{color:var(--dm-to)}
/* the light that carries the work down the line: a bright head with a tail in the sender's hue */
.dm-aln-pk{position:absolute;left:27px;top:0;width:8px;height:8px;margin:-4px 0 0 -4px;border-radius:50%;background:var(--dm-to);box-shadow:0 0 0 3px color-mix(in srgb,var(--dm-to) 22%,transparent),0 0 12px 2px color-mix(in srgb,var(--dm-to) 45%,transparent);pointer-events:none;z-index:2;opacity:0}
.dm-aln-pk::before{content:"";position:absolute;left:3px;bottom:4px;width:2px;height:30px;border-radius:1px;background:linear-gradient(transparent,var(--dm-from))}
@media (max-width:600px){
  .dm-aln-row{grid-template-columns:34px minmax(0,1fr) auto;row-gap:var(--dm-s-2)}
  .dm-aln-tr{grid-column:2 / -1;grid-row:2}
  .dm-aln-lane::before{top:48px}
  .dm-aln-ct{display:none}
  .dm-aln-m{margin-right:auto}
}
@media (max-width:400px){.dm-aln-ic{display:none}}
:root:not([data-style="soft"]) .dm-aln-rows>*:not(:where([data-style="soft"],[data-style="soft"] *))::before,:root:not([data-style="soft"]) .dm-aln-lane:not(:where([data-style="soft"],[data-style="soft"] *))::after{width:1px;left:26.5px;background:repeating-linear-gradient(var(--dm-rail) 0 3px,transparent 3px 6px)}
:root:not([data-style="soft"]) .dm-aln-lane.dm-sent:not(:where([data-style="soft"],[data-style="soft"] *))::before,:root:not([data-style="soft"]) .dm-aln-lane.dm-got:not(:where([data-style="soft"],[data-style="soft"] *))::after{background:var(--dm-c)}
:root:not([data-style="soft"]) .dm-aln-hf.dm-lit:not(:where([data-style="soft"],[data-style="soft"] *))::before{background:var(--dm-to)}
:root:not([data-style="soft"]) .dm-aln-av:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-c) 35%,transparent)}
:root:not([data-style="soft"]) .dm-aln-lane[data-s="working"] .dm-aln-av:not(:where([data-style="soft"],[data-style="soft"] *)){animation:dm-alnbp 1.1s steps(1,end) infinite}
@keyframes dm-alnbp{0%,55%{box-shadow:inset 0 0 0 1px var(--dm-c)}56%,100%{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-c) 35%,transparent)}}
:root:not([data-style="soft"]) .dm-aln-lane[data-s="working"]:not(:where([data-style="soft"],[data-style="soft"] *)){background:color-mix(in srgb,var(--dm-c) 5%,transparent)}
:root:not([data-style="soft"]) .dm-aln-tr i:not(:where([data-style="soft"],[data-style="soft"] *)){height:8px}
:root:not([data-style="soft"]) .dm-aln-tr i.dm-run:not(:where([data-style="soft"],[data-style="soft"] *))::after{background:repeating-linear-gradient(135deg,var(--dm-cm) 0 4px,color-mix(in srgb,var(--dm-cm) 55%,var(--dm-surface)) 4px 7px);animation:none}
:root:not([data-style="soft"]) .dm-aln-pk:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:none;width:7px;height:7px;margin:-3px 0 0 -3px}
:root:not([data-style="soft"]) .dm-aln-pk:not(:where([data-style="soft"],[data-style="soft"] *))::before{width:1px}
:root:not([data-style="soft"]) .dm-aln-av.dm-ping:not(:where([data-style="soft"],[data-style="soft"] *))::before{animation:dm-alnpingbp .6s steps(3,end)}
@keyframes dm-alnpingbp{from{box-shadow:0 0 0 1px var(--dm-c)}to{box-shadow:0 0 0 7px transparent}}
agent-lanes.htmlHTML · 9 lines
<div class="card wide bare" data-c="agent-lanes">
  <div class="stage-in">
    <div class="aln panel">
      <div class="aln-h"><span class="aln-ic"><svg class="i s"><use href="#i-users"/></svg></span><span class="aln-t">3 agents working</span><span class="aln-m muted num">0:00</span><span class="aln-ct num">0 of 9 steps</span><button class="pill soft sm aln-pause"><svg class="i xs"><use href="#i-pause"/></svg>Pause all</button></div>
      <div class="aln-rows"></div>
    </div>
  </div>
</div>
agent-lanes.cssHTML · 94 lines
/* ---------- Agent lanes ---------- */
/* each agent has its own hue (--c, with its tint --cb): its badge, its track, its tag and its stretch of the relay line that
   joins the badges. A hand-off sends a light down that line from one agent to the next, and the stretch it crossed stays lit */
.aln{padding:var(--s-2)}
/* the lanes' marks (tracks, lines, a finished badge) in livelier versions of the agents' hues, each 3:1 on the surface; their
   text keeps the agent's text colour. Dark mode's agent colours are light already, so they are used as they are */
.aln{--aln-pb:#2a7bff;--aln-pm:#1fa66a;--aln-pl:#7a5af5}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .aln{--aln-pb:var(--pb);--aln-pm:var(--pm);--aln-pl:var(--pl)}}
:root[data-theme="dark"] .aln{--aln-pb:var(--pb);--aln-pm:var(--pm);--aln-pl:var(--pl)}
/* on a card the lanes sit centred in the room kept for their tallest moment, and glide as a hand-off line opens and shuts */
.stage-in:has(>.aln){display:flex;flex-direction:column;justify-content:center}
.aln-h{display:flex;align-items:center;gap:10px;padding:var(--s-2) 10px 10px}
.aln-ic{color:var(--fg2);display:grid;transition:color .3s}
.aln.fin .aln-ic{color:var(--ok)}
.aln-t{font-weight:500}
.aln-m{font-size:12.5px}
.aln-ct{font-size:12.5px;color:var(--muted);margin-right:auto;white-space:nowrap}
.aln-ct::before{content:"·";margin-right:6px}
.aln-rows{position:relative}
.aln-rows>*{position:relative}
.aln-lane{border-radius:14px;transition:background .3s}
@media (hover:hover) and (pointer:fine){.aln-lane:hover{background:var(--row)}}
/* the lane at work carries a wash of its hue from the left */
.aln-lane[data-s="working"]{background:linear-gradient(90deg,color-mix(in srgb,var(--c) 8%,transparent),transparent 72%)}
@media (hover:hover) and (pointer:fine){.aln-lane[data-s="working"]:hover{background:linear-gradient(90deg,color-mix(in srgb,var(--c) 8%,transparent),transparent 72%),var(--row)}}
/* the relay line runs from badge to badge, through the lanes and the hand-offs between them, and stops short of each badge */
.aln-rows>*::before{content:"";position:absolute;left:26px;width:2px;top:0;bottom:0;border-radius:1px;background:var(--line);transition:background .5s var(--ease);pointer-events:none}
.aln-lane::before{top:48px}
.aln-lane:last-child::before{display:none}
.aln-lane::after{content:"";position:absolute;left:26px;width:2px;top:0;height:6px;border-radius:1px;background:var(--line);transition:background .5s var(--ease);pointer-events:none}
.aln-lane:first-child::after{display:none}
.aln-lane.sent::before{background:linear-gradient(var(--cm),color-mix(in srgb,var(--cm) 45%,var(--line)))}
.aln-lane.got::after{background:var(--cm)}
.aln-hf.lit::before{background:linear-gradient(color-mix(in srgb,var(--from) 45%,var(--line)),var(--to))}
.aln-row{display:grid;grid-template-columns:34px minmax(0,1.2fr) minmax(0,1fr) auto;align-items:center;gap:var(--s-3);width:100%;padding:10px;text-align:left}
.aln-av{position:relative;width:34px;height:34px;border-radius:11px;display:grid;place-items:center;color:var(--c);background:var(--cb);transition:color .4s,background .4s}
.aln-av::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none}
.aln-lane[data-s="working"] .aln-av{animation:alnbreathe 2.4s var(--ease-io) infinite}
@keyframes alnbreathe{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--c) 0%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--c) 18%,transparent)}}
/* the badge that takes a hand-off rings once as the light reaches it */
.aln-av.ping::before{animation:alnping .9s var(--ease)}
@keyframes alnping{from{box-shadow:0 0 0 0 color-mix(in srgb,var(--c) 50%,transparent)}to{box-shadow:0 0 0 12px color-mix(in srgb,var(--c) 0%,transparent)}}
/* a finished agent's badge fills with its hue */
.aln-lane[data-s="done"] .aln-av{background:var(--cm);color:var(--surface)}
.aln-who{min-width:0}
.aln-n{display:block;font-weight:500;font-size:13.5px}
.aln-a{display:block;font-size:12.5px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.aln-a.live{color:var(--fg2)}
/* the track: one part a step. Finished parts are solid, the one in hand fills as the agent works, with a light running along it */
.aln-tr{display:flex;gap:3px}
.aln-tr i{flex:1;height:10px;border-radius:5px;background:color-mix(in srgb,var(--cm) 13%,var(--line));position:relative;overflow:hidden}
.aln-tr i::after{content:"";position:absolute;inset:0;border-radius:inherit;background:var(--cm);transform:scaleX(var(--p,0));transform-origin:left;transition:transform .25s linear}
.aln-tr i.on::after{transform:scaleX(1);transition:transform .5s var(--ease)}
.aln-tr i.run::after{background:linear-gradient(90deg,var(--cm) 0 30%,color-mix(in srgb,var(--cm) 30%,var(--surface)) 50%,var(--cm) 70% 100%) 0 0/250% 100%;animation:alnsheen 1.6s linear infinite}
/* given no step progress, the part in hand fills part way and keeps its light */
.aln-tr i.run.ind::after{transform:scaleX(.6);transition:transform 2.4s var(--ease)}
@keyframes alnsheen{from{background-position:100% 0}to{background-position:-150% 0}}
.aln-st{min-width:92px;display:flex;justify-content:flex-end}
.aln-tg{background:var(--cb);color:var(--c)}
.aln-tg .spin{width:9px;height:9px}
.aln-det{padding:0 10px 10px 56px;font-size:12.5px;color:var(--fg2)}
.aln-det div{display:flex;align-items:center;gap:var(--s-2);padding:3px 0}
.aln-det div::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--c);flex:none}
.aln-det .muted{margin-left:auto}
/* a hand-off opens a line between the lanes, glides the lanes below down and closes again */
.aln-hf{transition-duration:var(--t-slow)}
.aln-hand{padding:6px 10px 6px 56px;font-size:12.5px;line-height:18px;color:var(--fg2)}
.aln-hand b{font-weight:500}
.aln-hand .f{color:var(--from)}.aln-hand .t{color:var(--to)}
/* the light that carries the work down the line: a bright head with a tail in the sender's hue */
.aln-pk{position:absolute;left:27px;top:0;width:8px;height:8px;margin:-4px 0 0 -4px;border-radius:50%;background:var(--to);box-shadow:0 0 0 3px color-mix(in srgb,var(--to) 22%,transparent),0 0 12px 2px color-mix(in srgb,var(--to) 45%,transparent);pointer-events:none;z-index:2;opacity:0}
.aln-pk::before{content:"";position:absolute;left:3px;bottom:4px;width:2px;height:30px;border-radius:1px;background:linear-gradient(transparent,var(--from))}
@media (max-width:600px){
  .aln-row{grid-template-columns:34px minmax(0,1fr) auto;row-gap:var(--s-2)}
  .aln-tr{grid-column:2 / -1;grid-row:2}
  .aln-lane::before{top:48px}
  .aln-ct{display:none}
  .aln-m{margin-right:auto}
}
@media (max-width:400px){.aln-ic{display:none}}
:root:not([data-style="soft"]) .aln-rows>*:not(:where([data-style="soft"],[data-style="soft"] *))::before,:root:not([data-style="soft"]) .aln-lane:not(:where([data-style="soft"],[data-style="soft"] *))::after{width:1px;left:26.5px;background:repeating-linear-gradient(var(--rail) 0 3px,transparent 3px 6px)}
:root:not([data-style="soft"]) .aln-lane.sent:not(:where([data-style="soft"],[data-style="soft"] *))::before,:root:not([data-style="soft"]) .aln-lane.got:not(:where([data-style="soft"],[data-style="soft"] *))::after{background:var(--c)}
:root:not([data-style="soft"]) .aln-hf.lit:not(:where([data-style="soft"],[data-style="soft"] *))::before{background:var(--to)}
:root:not([data-style="soft"]) .aln-av:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--c) 35%,transparent)}
:root:not([data-style="soft"]) .aln-lane[data-s="working"] .aln-av:not(:where([data-style="soft"],[data-style="soft"] *)){animation:alnbp 1.1s steps(1,end) infinite}
@keyframes alnbp{0%,55%{box-shadow:inset 0 0 0 1px var(--c)}56%,100%{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--c) 35%,transparent)}}
:root:not([data-style="soft"]) .aln-lane[data-s="working"]:not(:where([data-style="soft"],[data-style="soft"] *)){background:color-mix(in srgb,var(--c) 5%,transparent)}
:root:not([data-style="soft"]) .aln-tr i:not(:where([data-style="soft"],[data-style="soft"] *)){height:8px}
:root:not([data-style="soft"]) .aln-tr i.run:not(:where([data-style="soft"],[data-style="soft"] *))::after{background:repeating-linear-gradient(135deg,var(--cm) 0 4px,color-mix(in srgb,var(--cm) 55%,var(--surface)) 4px 7px);animation:none}
:root:not([data-style="soft"]) .aln-pk:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:none;width:7px;height:7px;margin:-3px 0 0 -3px}
:root:not([data-style="soft"]) .aln-pk:not(:where([data-style="soft"],[data-style="soft"] *))::before{width:1px}
:root:not([data-style="soft"]) .aln-av.ping:not(:where([data-style="soft"],[data-style="soft"] *))::before{animation:alnpingbp .6s steps(3,end)}
@keyframes alnpingbp{from{box-shadow:0 0 0 1px var(--c)}to{box-shadow:0 0 0 7px transparent}}
agent-lanes.jsHTML · 65 lines
DM.def('agent-lanes', (r, c) => {
  r.innerHTML = r.innerHTML.replace('{users}', I('users')).replace('{pause}', I('pause','xs'));
  // each agent has a hue of its own (t names the palette slot): its badge, its track, its tag and its stretch of the relay line
  const L = [
    { n:'Researcher', ic:'search', t:'pb', acts:['Reading competitor sites','Comparing 5 pricing pages','Writing the brief'], log:['Read stripe.com/pricing','Read linear.app/pricing','Noted 3 patterns'] },
    { n:'Designer', ic:'palette', t:'pl', wait:'Waiting for the brief', acts:['Laying out pricing cards','Setting type and spacing','Exporting frames'], log:['Made 3 card variants','Applied spacing tokens','Exported 4 frames'] },
    { n:'Reviewer', ic:'eye', t:'pm', wait:'Waiting for the designer', acts:['Checking contrast','Checking copy','Signing off'], log:['Contrast passes AA','Fixed 2 lines of copy','Approved'] }
  ];
  const root = $('.aln', r), rows = $('.aln-rows', r), ct = $('.aln-ct', r), STEPS = L.length * 3;
  L.forEach(l => {
    l.el = html(`<div class="aln-lane" data-s="queued" style="--c:var(--${l.t});--cb:var(--${l.t}-bg);--cm:var(--aln-${l.t},var(--${l.t}))"><button class="aln-row" aria-expanded="false"><span class="aln-av">${I(l.ic)}</span><span class="aln-who"><span class="aln-n">${l.n}</span><span class="aln-a">${l.wait || 'Queued'}</span></span><span class="aln-tr" aria-hidden="true"><i></i><i></i><i></i></span><span class="aln-st"><span class="tag">Queued</span></span></button><div class="fold shut"><div><div class="aln-det"></div></div></div></div>`);
    rows.appendChild(l.el); const b = $('.aln-row', l.el), f = $('.fold', l.el);
    b.onclick = () => { const s = f.classList.toggle('shut'); b.setAttribute('aria-expanded', !s); };
  });
  const TAG = { queued:'<span class="tag">Queued</span>', waiting:`<span class="tag">${I('clock','xs')}Waiting</span>`, working:'<span class="tag aln-tg"><span class="spin"></span>Working</span>', paused:'<span class="tag warn">Paused</span>', done:`<span class="tag ok">${I('check','xs')}Done</span>` };
  const state = (l, s) => { l.el.dataset.s = s; const t = $('.aln-st', l.el); t.innerHTML = TAG[s]; enter(t.firstElementChild); };
  const act = (l, t, live) => { const a = $('.aln-a', l.el); a.classList.toggle('live', !!live); a.classList.toggle('shim', !!live); swap(a, t); };
  let paused = false, secs = 0, finished = false, steps = 0;
  const count = () => swap(ct, `${steps} of ${STEPS} steps`);
  const pb = $('.aln-pause', r);
  pb.onclick = () => { paused = !paused; pb.innerHTML = I(paused ? 'play' : 'pause','xs') + (paused ? 'Resume' : 'Pause all'); swap($('.aln-t', r), paused ? 'All agents paused' : '3 agents working');
    L.forEach(l => { if (l.running){ state(l, paused ? 'paused' : 'working'); $('.aln-a', l.el).classList.toggle('shim', !paused); } }); };
  c.every(1000, () => { if (!paused && !finished){ secs++; $('.aln-m', r).textContent = Math.floor(secs / 60) + ':' + String(secs % 60).padStart(2, '0'); } });
  // time only passes for the agents while they aren't paused; each() hears how far through the wait they are
  const hold = async (ms, each) => { let t = 0; while (t < ms){ await c.wait(100); if (!paused){ t += 100; each && each(t / ms); } } };
  async function work(l){
    l.running = true; state(l, 'working');
    const bars = $$('.aln-tr i', l.el), det = $('.aln-det', l.el);
    for (let i = 0; i < 3; i++){
      bars[i].classList.add('run'); act(l, l.acts[i], true);
      await hold(1500, p => bars[i].style.setProperty('--p', p.toFixed(3)));
      bars[i].classList.replace('run', 'on'); steps++; count();
      const d = html(`<div><span>${l.log[i]}</span><span class="muted">${secs}s</span></div>`); det.appendChild(d); enter(d);
    }
    l.running = false; act(l, l.log[2]); state(l, 'done');
  }
  // a light runs down the relay line from one badge to the next, the badge it reaches rings, and the stretch stays lit
  function carry(from, to){
    const rr = rows.getBoundingClientRect(), a = $('.aln-av', from.el).getBoundingClientRect(), b = $('.aln-av', to.el).getBoundingClientRect();
    const pk = html(`<span class="aln-pk" aria-hidden="true" style="--from:var(--${from.t});--to:var(--${to.t})"></span>`); rows.appendChild(pk);
    const y0 = a.bottom - rr.top + 4, y1 = b.top - rr.top - 4;
    const an = REDUCE ? null : pk.animate([{ transform:`translateY(${y0}px)`, opacity:0 }, { opacity:1, offset:.12 }, { opacity:1, offset:.88 }, { transform:`translateY(${y1}px)`, opacity:0 }], { duration:950, easing:'cubic-bezier(.65,0,.35,1)' });
    c.offs.push(() => pk.remove());
    return (an ? an.finished.catch(() => {}) : Promise.resolve()).then(() => pk.remove());
  }
  async function handoff(from, to, txt){
    const h = html(`<div class="fold shut aln-hf" style="--from:var(--${from.t});--to:var(--${to.t})"><div><div class="aln-hand"><b class="f">${from.n}</b> handed ${txt} to <b class="t">${to.n}</b></div></div></div>`);
    to.el.before(h); void h.offsetWidth; h.classList.remove('shut'); enter($('.aln-hand', h), 80);
    await c.wait(REDUCE ? 0 : 500); await carry(from, to);
    from.el.classList.add('sent'); h.classList.add('lit'); to.el.classList.add('got');
    const av = $('.aln-av', to.el); av.classList.add('ping'); c.later(950, () => av.classList.remove('ping'));
    await c.wait(1300); h.classList.add('shut'); await c.wait(REDUCE ? 0 : 700); h.remove();
  }
  c.run(async () => {
    await c.wait(600);
    const r1 = work(L[0]); act(L[1], L[1].wait); state(L[1], 'waiting');
    await r1; await handoff(L[0], L[1], 'the brief');
    act(L[2], L[2].wait); state(L[2], 'waiting');
    await work(L[1]); await handoff(L[1], L[2], '4 frames');
    await work(L[2]);
    finished = true; root.classList.add('fin'); $('.aln-ic', r).innerHTML = I('ok'); enter($('.aln-ic', r).firstElementChild);
    swap($('.aln-t', r), 'All 3 agents finished'); pb.remove();
  });
});
06

Questions

What people ask about it.

Does Pause all stop my agents?
No. It changes how the lanes look and calls onPausedChange. Your code pauses the agents, and the lanes' own status values stay as you set them.
How do I show one agent handing work to another?
Set handoff with the from and to lane keys and a short what, like "the brief". The light takes about a second and a half to arrive; clear handoff after that and the line closes before it goes.
Can a lane have more than three steps?
Yes. Set steps on the lane to cut its track into as many parts as you need, move done forward as each one finishes, and set progress to fill the one in hand.
07

More in Agent status

Show what the agent is doing, how far along it is, and what it is working with.

ThinkingThe live trace, written as the agent works, then folded away.
Reasoning trailThe agent's plan, notes, sources and edits, after it thinks.
Agent treeAn orchestrator and its sub-agents, with cost rolled up per branch.
Agent inboxEvery run that needs you, is running or is done, in one list.
Trace viewOne agent run as a waterfall of spans, with latency and tokens.
Browser sessionThe agent browsing for you, step by step, with Pause and Take over.
Run summaryWhat the agent did, what changed and what needs you.
Agent run timelineEvery model and tool step of a run on one time axis.
All 18 in Agent status →