Skip to content
Star on GitHub
Agent statusAgent tree
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 tree

An orchestrator and its sub-agents, with cost rolled up per branch. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add agent-tree
Redesign the pricing page3 agents working
$0.00
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add agent-tree

React

$npx designme add agent-tree --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 tree shows an orchestrator and the sub-agents it hands work to, as a tree a person can fold. Every row has its task, a status and what it has spent, and each branch adds up its own cost and everything under it, so you can see where the money went. When a step fails, its row turns red and offers the fix in place.

  • When one agent splits a job across sub-agents and people need to see who is doing what, and what is still queued.
  • When a run spends money as it goes and people should see the cost per agent and per step, not just the total.
  • When a step can fail and the run should wait for a person to take the fix it offers.
  • When the tree gets long and finished branches should fold away.
03

Usage

Import it and drive it from your own state.

Import

import { AgentTree } from '@/components/designme/agent-tree/AgentTree';

Example

The demo above as a React component, 64 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 { AgentTree, type AgentNode, type AgentNodeStatus } from './AgentTree';
import { useScript, useStory } from '../lib/useScript';

/* ---------- Agent tree: the design agent works through its steps, one fails on a missing token, and the fix it offers waits for you;
   the finished research starts folded ---------- */
type Row = { name: string; task: string; status: AgentNodeStatus; cost: number; kids?: string[] };
// the run: an orchestrator, its three agents and their steps. cost is what each spends on its own, in dollars
const START: Record<string, Row> = {
  root: { name: 'Orchestrator', task: 'Plans the work and hands it out', status: 'running', cost: .04, kids: ['res', 'des', 'cop'] },
  res: { name: 'Research agent', task: 'Compare 3 competitor pricing pages', status: 'done', cost: .02, kids: ['res1', 'res2'] },
  res1: { name: 'Read the 3 pricing pages', task: '3 pages, 9.4k words', status: 'done', cost: .06 },
  res2: { name: 'Summarise their plans', task: 'Notes added to Pricing v4', status: 'done', cost: .04 },
  des: { name: 'Design agent', task: 'Rework the plan cards in Figma', status: 'running', cost: .02, kids: ['des1', 'des2', 'des3'] },
  des1: { name: 'Update the Pro card', task: 'Pricing v4, frame 2', status: 'running', cost: .03 },
  des2: { name: 'Update the Team card', task: 'Pricing v4, frame 3', status: 'queued', cost: 0 },
  des3: { name: 'Apply library tokens', task: 'Colour and spacing', status: 'queued', cost: 0 },
  cop: { name: 'Copy agent', task: 'Rewrite the plan descriptions', status: 'queued', cost: 0 }
};

export default function AgentTreeDemo() {
  const ref = useRef<HTMLDivElement>(null);
  const [rows, setRows] = useState(START);
  const [fixing, setFixing] = useState(false);
  const fixed = useRef<() => void>(undefined);
  const set = (id: string, patch: Partial<Row>) => setRows(r => ({ ...r, [id]: { ...r[id], ...patch } }));

  // whatever is running spends as it goes, so its cost and every total above it count up
  useEffect(() => {
    const id = setInterval(() => setRows(r => Object.fromEntries(Object.entries(r).map(([k, x]) => [k, x.status === 'running' ? { ...x, cost: x.cost + (k === 'root' ? .0006 : .0016) } : x]))), 100);
    return () => clearInterval(id);
  }, []);

  useScript(async ({ wait }) => {
    await wait(2000); set('des1', { status: 'done' }); set('des2', { status: 'running' });
    await wait(2500); set('des2', { status: 'done' }); set('des3', { status: 'running' });
    await wait(2000); set('des3', { status: 'failed', task: "color.accent.600 isn't in the library" }); setFixing(true);
    await new Promise<void>(res => { fixed.current = res; });
    setFixing(false); set('des3', { status: 'running', task: 'Using color.accent.500' });
    await wait(2000); set('des3', { status: 'done', task: 'Colour and spacing, from the library' }); set('des', { status: 'done' }); set('cop', { status: 'running' });
    await wait(3000); set('cop', { status: 'done' }); set('root', { status: 'done' });
  });

  // the preview's story: take the fix when a step fails, then fold the design branch once it's done
  useStory(ref, async ({ wait, press }) => {
    await wait(8500); press('.dm-atr-fix');
    await wait(6000); press('[data-id="des"]');
  });

  const node = (id: string): AgentNode => {
    const x = rows[id];
    return { id, name: x.name, task: x.task, status: x.status, cost: x.cost, children: x.kids?.map(node),
      ...(id === 'des3' && fixing ? { fix: { label: 'Use color.accent.500', onClick: () => fixed.current?.() } } : {}) };
  };

  return (
    <div ref={ref} style={{ width: '100%' }}>
      <AgentTree root={node('root')} title="Redesign the pricing page" defaultCollapsed={['res']} />
    </div>
  );
}
04

Props

Everything AgentTree takes.

PropTypeWhat it does
rootrequiredAgentNodeThe orchestrator, with its agents and their steps as children
titlerequiredstringThe run's goal, like "Redesign the pricing page"
statusstringThe line under the title. Defaults to "Running", "1 step failed" or "Done", then the clock
secondsnumberSeconds to show as m:ss. Leave it out to let the clock count until the root is done
summarystring | falseShown under the tree once the root is done. Defaults to "Done in 0:14 · 3 agents · $0.66"; false hides it
collapsedstring[]Ids of the folded branches. Leave it out to let each branch fold itself
defaultCollapsedstring[]
onCollapsedChange(ids: string[]) => void
formatCost(dollars: number) => stringWrites a cost. Defaults to dollars and cents, like "$0.12"
classNamestring
05

Code

The full source, as the CLI installs it.

AgentTree.tsxReact · 144 lines
'use client';
import { useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode } from 'react';
import { cx } from '../lib/cx';
import { useControllable, useSeconds } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { enter, Fold, Spin, Swap, Tip } from '../lib/motion';
import '../designme.css';
import './agent-tree.css';

export type AgentNodeStatus = 'queued' | 'running' | 'done' | 'failed';

export interface AgentNode {
  id: string;
  /** Like "Design agent", or a step's name */
  name: string;
  /** Its task, or what it is doing now. It crossfades when it changes */
  task: string;
  status: AgentNodeStatus;
  /** What this node spent on its own, in dollars. A branch shows this plus everything under it */
  cost?: number;
  /** A way out of a failed step, shown as a button under its task */
  fix?: { label: string; onClick: () => void };
  children?: AgentNode[];
}

export interface AgentTreeProps {
  /** The orchestrator, with its agents and their steps as children */
  root: AgentNode;
  /** The run's goal, like "Redesign the pricing page" */
  title: string;
  /** The line under the title. Defaults to "Running", "1 step failed" or "Done", then the clock */
  status?: string;
  /** Seconds to show as m:ss. Leave it out to let the clock count until the root is done */
  seconds?: number;
  /** Shown under the tree once the root is done. Defaults to "Done in 0:14 · 3 agents · $0.66"; false hides it */
  summary?: string | false;
  /** Ids of the folded branches. Leave it out to let each branch fold itself */
  collapsed?: string[];
  defaultCollapsed?: string[];
  onCollapsedChange?: (ids: string[]) => void;
  /** Writes a cost. Defaults to dollars and cents, like "$0.12" */
  formatCost?: (dollars: number) => string;
  className?: string;
}

const CLS: Record<AgentNodeStatus, string> = { queued: 'dm-queued', running: 'dm-run', done: 'dm-done', failed: 'dm-fail' };
const clock = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
const total = (n: AgentNode): number => (n.cost ?? 0) + (n.children ?? []).reduce((a, k) => a + total(k), 0);
const branch = (n: AgentNode) => !!n.children?.length;

/** How many times `value` has changed since the first render, so a glyph 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;
}

function Glyph({ status }: { status: AgentNodeStatus }) {
  const moved = useChanges(status) > 0;
  const e = moved ? enter() : undefined;
  return (
    <span key={status} className={cx('dm-atr-g', e?.className)} style={e?.style} aria-hidden="true">
      {status === 'running' ? <Spin /> : status === 'done' ? <Icon name="ok" size="xs" /> : status === 'failed' ? <Icon name="alert" size="xs" /> : <span className="dm-dot" />}
    </span>
  );
}

/** An orchestrator and its sub-agents as a tree. Each row shows its task, status and cost; a branch's cost rolls up
    everything under it, and branches fold. The arrow keys move through it as in any tree view. */
export function AgentTree({ root, title, status, seconds, summary, collapsed, defaultCollapsed = [], onCollapsedChange, formatCost, className }: AgentTreeProps) {
  const [shut, setShut] = useControllable(collapsed, defaultCollapsed, onCollapsedChange);
  const [focus, setFocus] = useState(root.id);
  const treeRef = useRef<HTMLDivElement>(null);
  const finished = root.status === 'done';
  const counted = useSeconds(seconds === undefined && !finished);
  const secs = seconds ?? counted;
  const money = formatCost ?? ((v: number) => '$' + v.toFixed(2));

  const all: { n: AgentNode; lv: number; parent?: AgentNode }[] = [];
  (function walk(n: AgentNode, lv: number, parent?: AgentNode) { all.push({ n, lv, parent }); n.children?.forEach(k => walk(k, lv + 1, n)); })(root, 0);
  const failed = all.filter(x => x.n.status === 'failed').length;
  const line = status ?? (finished ? 'Done' : failed ? `${failed} step${failed > 1 ? 's' : ''} failed` : 'Running');
  const agents = root.children?.length ?? 0;
  const done = summary === undefined ? `Done in ${clock(secs)} · ${agents} agent${agents === 1 ? '' : 's'} · ${money(total(root))}` : summary;

  const isOpen = (n: AgentNode) => !shut.includes(n.id);
  const toggle = (n: AgentNode, open = !isOpen(n)) => { if (branch(n)) setShut(open ? shut.filter(id => id !== n.id) : [...shut, n.id]); };
  // the rows a person can reach: everything not inside a folded branch
  const visible = all.filter(x => { for (let p = x.parent; p; p = all.find(y => y.n === p)?.parent) if (!isOpen(p)) return false; return true; });
  const go = (n?: AgentNode) => { if (!n) return; setFocus(n.id); treeRef.current?.querySelector<HTMLElement>(`[data-id="${CSS.escape(n.id)}"]`)?.focus({ preventScroll: true }); };
  const onKey = (e: KeyboardEvent<HTMLDivElement>) => {
    const i = visible.findIndex(x => x.n.id === focus); if (i < 0) return;
    const { n, parent } = visible[i], open = branch(n) && isOpen(n);
    if (e.key === 'ArrowDown') go(visible[i + 1]?.n);
    else if (e.key === 'ArrowUp') go(visible[i - 1]?.n);
    else if (e.key === 'ArrowRight') { if (branch(n) && !open) toggle(n, true); else if (open) go(n.children![0]); }
    else if (e.key === 'ArrowLeft') { if (open) toggle(n, false); else go(parent); }
    else if (e.key === 'Enter' || e.key === ' ') toggle(n);
    else if (e.key === 'Home') go(visible[0].n);
    else if (e.key === 'End') go(visible[visible.length - 1].n);
    else return;
    e.preventDefault();
  };

  const node = (n: AgentNode, lv: number): ReactNode => {
    const sum = total(n), own = n.cost ?? 0, k = n.children?.length ?? 0;
    const cost = <span className="dm-atr-c dm-num">{n.status === 'queued' && !sum ? '–' : money(branch(n) ? sum : own)}</span>;
    const fx = n.status === 'failed' && n.fix ? enter() : undefined;
    return (
      <div key={n.id} role="none" style={{ '--dm-lv': lv } as CSSProperties}>
        <div data-id={n.id} className={cx('dm-atr-n', CLS[n.status])} role="treeitem" aria-level={lv + 1} aria-expanded={branch(n) ? isOpen(n) : undefined}
          tabIndex={focus === n.id ? 0 : -1}
          onClick={e => { if ((e.target as Element).closest('.dm-atr-fix')) return; setFocus(n.id); toggle(n); }}>
          {branch(n) ? <span className="dm-atr-tg" aria-hidden="true"><Icon name="chev-d" size="xs" /></span> : <span className="dm-atr-sp" />}
          <Glyph status={n.status} />
          <span className="dm-atr-t">
            <b>{n.name}</b><Swap>{n.task}</Swap>
            {fx && <button type="button" className={cx('dm-pill dm-sm dm-atr-fix', fx.className)} style={fx.style} onClick={n.fix!.onClick}>{n.fix!.label}</button>}
          </span>
          {branch(n) ? <Tip label={`Its own ${money(own)}, ${k} ${lv ? (k > 1 ? 'steps' : 'step') : (k > 1 ? 'agents' : 'agent')} ${money(sum - own)}`}>{cost}</Tip> : cost}
        </div>
        {branch(n) && (
          <Fold open={isOpen(n)} className="dm-atr-kids">
            <div role="group">{n.children!.map(c => node(c, lv + 1))}</div>
          </Fold>
        )}
      </div>
    );
  };

  const sum = finished && done !== false ? enter() : undefined;
  return (
    <div data-dm="" className={cx('dm-atr dm-panel', className)}>
      <div className="dm-atr-h">
        <div className="dm-atr-ht"><b>{title}</b><span className="dm-muted dm-atr-st"><span aria-live="polite"><Swap>{line}</Swap></span> · <span className="dm-num">{clock(secs)}</span></span></div>
        <span className="dm-atr-tot dm-num">{money(total(root))}</span>
      </div>
      <div ref={treeRef} className="dm-atr-tree" role="tree" aria-label="Agents" onKeyDown={onKey}>{node(root, 0)}</div>
      {sum && <div className={cx('dm-atr-sum', sum.className)} style={sum.style}><Icon name="ok" size="xs" /><span>{done}</span></div>}
    </div>
  );
}
agent-tree.cssReact · 40 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Agent tree ---------- */
/* an orchestrator and its sub-agents as a tree: a chevron folds a branch, each row shows its task, status and cost, and a
   branch's cost is its own plus everything under it */
.dm-atr{width:100%;max-width:440px;margin:0 auto;padding:6px}
.dm-atr-h{display:flex;align-items:center;justify-content:space-between;gap:var(--dm-s-3);padding:var(--dm-s-2) 10px 10px}
.dm-atr-ht{display:flex;flex-direction:column;min-width:0}
.dm-atr-ht b{font-weight:500;font-size:14px}
.dm-atr-st{font-size:12.5px}
.dm-atr-tot{font-size:14px;font-weight:500}
.dm-atr-tree{display:flex;flex-direction:column;padding-top:var(--dm-s-1);border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-atr-n{display:flex;align-items:center;gap:var(--dm-s-2);min-height:44px;padding:5px 10px 5px calc(4px + var(--dm-lv,0) * 20px);border-radius:10px;font-size:13px;outline:0;transition:background .16s}
@media (hover:hover) and (pointer:fine){.dm-atr-n:hover{background:var(--dm-row)}}
.dm-atr-n:focus-visible{box-shadow:inset 0 0 0 2px var(--dm-focus)}
.dm-atr-tg{width:20px;height:20px;display:grid;place-items:center;flex:none;border-radius:6px;color:var(--dm-muted);transition:background .16s,color .16s}
@media (hover:hover) and (pointer:fine){.dm-atr-tg:hover{background:var(--dm-row);color:var(--dm-fg)}}
.dm-atr-tg svg{transition:transform .32s var(--dm-ease)}
.dm-atr-n[aria-expanded="false"] .dm-atr-tg svg{transform:rotate(-90deg)}
.dm-atr-sp{width:20px;flex:none}
.dm-atr-g{width:16px;height:16px;display:grid;place-items:center;flex:none;color:var(--dm-faint)}
.dm-atr-g .dm-spin{width:11px;height:11px;color:var(--dm-accent-fg,var(--dm-accent))}
.dm-atr-n.dm-done .dm-atr-g{color:var(--dm-ok)}
.dm-atr-n.dm-fail .dm-atr-g{color:var(--dm-bad)}
.dm-atr-t{flex:1;min-width:0;display:flex;flex-direction:column}
.dm-atr-t b{font-weight:500}
.dm-atr-t span{font-size:12px;color:var(--dm-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-atr-n.dm-fail .dm-atr-t span{color:var(--dm-bad)}
.dm-atr-n.dm-queued .dm-atr-t b{color:var(--dm-fg2)}
.dm-atr-c{font-size:12.5px;color:var(--dm-fg2);min-width:44px;text-align:right}
.dm-atr-n.dm-queued .dm-atr-c{color:var(--dm-muted)}
/* the way out of a failed step: a white button on its red-tinted row */
.dm-atr-n.dm-fail{background:var(--dm-bad-bg)}
.dm-atr-fix{flex:none;align-self:flex-start;margin-top:var(--dm-s-1);background:var(--dm-surface);color:var(--dm-fg);box-shadow:var(--dm-shadow)}
@media (hover:hover) and (pointer:fine){.dm-atr-fix:hover{background:var(--dm-surface);box-shadow:var(--dm-shadow-pop)}}
/* a folded branch keeps its children's space shut; the guide line marks the level */
.dm-atr-kids{position:relative}
.dm-atr-kids::before{content:"";position:absolute;top:2px;bottom:6px;left:calc(14px + var(--dm-lv,0) * 20px);width:1px;background:var(--dm-line)}
.dm-atr-sum{display:flex;align-items:center;gap:var(--dm-s-2);margin:var(--dm-s-2) var(--dm-s-1) 2px;padding:10px var(--dm-s-3);border-radius:var(--dm-r-in);background:var(--dm-ok-bg);font-size:13px}
.dm-atr-sum svg{color:var(--dm-ok);flex:none}
agent-tree.htmlHTML · 9 lines
<div class="card" data-c="agent-tree">
  <div class="stage-in">
    <div class="atr panel">
      <div class="atr-h"><div class="atr-ht"><b>Redesign the pricing page</b><span class="muted atr-st">3 agents working</span></div><span class="atr-tot num">$0.00</span></div>
      <div class="atr-tree" role="tree" aria-label="Agents"></div>
    </div>
  </div>
</div>
agent-tree.cssHTML · 39 lines
/* ---------- Agent tree ---------- */
/* an orchestrator and its sub-agents as a tree: a chevron folds a branch, each row shows its task, status and cost, and a
   branch's cost is its own plus everything under it */
.atr{width:100%;max-width:440px;margin:0 auto;padding:6px}
.atr-h{display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);padding:var(--s-2) 10px 10px}
.atr-ht{display:flex;flex-direction:column;min-width:0}
.atr-ht b{font-weight:500;font-size:14px}
.atr-st{font-size:12.5px}
.atr-tot{font-size:14px;font-weight:500}
.atr-tree{display:flex;flex-direction:column;padding-top:var(--s-1);border-top:1px solid color-mix(in srgb,var(--line) 50%,transparent)}
.atr-n{display:flex;align-items:center;gap:var(--s-2);min-height:44px;padding:5px 10px 5px calc(4px + var(--lv,0) * 20px);border-radius:10px;font-size:13px;outline:0;transition:background .16s}
@media (hover:hover) and (pointer:fine){.atr-n:hover{background:var(--row)}}
.atr-n:focus-visible{box-shadow:inset 0 0 0 2px var(--focus)}
.atr-tg{width:20px;height:20px;display:grid;place-items:center;flex:none;border-radius:6px;color:var(--muted);transition:background .16s,color .16s}
@media (hover:hover) and (pointer:fine){.atr-tg:hover{background:var(--row);color:var(--fg)}}
.atr-tg svg{transition:transform .32s var(--ease)}
.atr-n[aria-expanded="false"] .atr-tg svg{transform:rotate(-90deg)}
.atr-sp{width:20px;flex:none}
.atr-g{width:16px;height:16px;display:grid;place-items:center;flex:none;color:var(--faint)}
.atr-g .spin{width:11px;height:11px;color:var(--accent-fg,var(--accent))}
.atr-n.done .atr-g{color:var(--ok)}
.atr-n.fail .atr-g{color:var(--bad)}
.atr-t{flex:1;min-width:0;display:flex;flex-direction:column}
.atr-t b{font-weight:500}
.atr-t span{font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.atr-n.fail .atr-t span{color:var(--bad)}
.atr-n.queued .atr-t b{color:var(--fg2)}
.atr-c{font-size:12.5px;color:var(--fg2);min-width:44px;text-align:right}
.atr-n.queued .atr-c{color:var(--muted)}
/* the way out of a failed step: a white button on its red-tinted row */
.atr-n.fail{background:var(--bad-bg)}
.atr-fix{flex:none;align-self:flex-start;margin-top:var(--s-1);background:var(--surface);color:var(--fg);box-shadow:var(--shadow)}
@media (hover:hover) and (pointer:fine){.atr-fix:hover{background:var(--surface);box-shadow:var(--shadow-pop)}}
/* a folded branch keeps its children's space shut; the guide line marks the level */
.atr-kids{position:relative}
.atr-kids::before{content:"";position:absolute;top:2px;bottom:6px;left:calc(14px + var(--lv,0) * 20px);width:1px;background:var(--line)}
.atr-sum{display:flex;align-items:center;gap:var(--s-2);margin:var(--s-2) var(--s-1) 2px;padding:10px var(--s-3);border-radius:var(--r-in);background:var(--ok-bg);font-size:13px}
.atr-sum svg{color:var(--ok);flex:none}
agent-tree.jsHTML · 86 lines
DM.def('agent-tree', (r, c) => {
  // the run: an orchestrator, its three agents and their steps. cost is what each spends on its own, in dollars; a branch
  // shows its own cost plus everything under it
  const T = { id:'root', n:'Orchestrator', t:'Plans the work and hands it out', cost:.04, s:'run', kids:[
    { id:'res', n:'Research agent', t:'Compare 3 competitor pricing pages', cost:.02, s:'done', kids:[
      { id:'res1', n:'Read the 3 pricing pages', t:'3 pages, 9.4k words', cost:.06, s:'done' },
      { id:'res2', n:'Summarise their plans', t:'Notes added to Pricing v4', cost:.04, s:'done' } ] },
    { id:'des', n:'Design agent', t:'Rework the plan cards in Figma', cost:.02, s:'run', kids:[
      { id:'des1', n:'Update the Pro card', t:'Pricing v4, frame 2', cost:.03, s:'run' },
      { id:'des2', n:'Update the Team card', t:'Pricing v4, frame 3', cost:0, s:'queued' },
      { id:'des3', n:'Apply library tokens', t:'Colour and spacing', cost:0, s:'queued' } ] },
    { id:'cop', n:'Copy agent', t:'Rewrite the plan descriptions', cost:0, s:'queued' } ] };
  const tree = $('.atr-tree', r), stEl = $('.atr-st', r), tot = $('.atr-tot', r), root = $('.atr', r);
  const all = []; (function walk(n, lv, p){ n.lv = lv; n.parent = p; all.push(n); (n.kids || []).forEach(k => walk(k, lv + 1, n)); })(T, 0, null);
  const by = id => all.find(n => n.id === id), kids = n => !!(n.kids && n.kids.length);
  const sum = n => n.cost + (n.kids || []).reduce((a, k) => a + sum(k), 0), money = v => '$' + v.toFixed(2);
  const G = { run:'<span class="spin"></span>', done:I('ok','xs'), fail:I('alert','xs'), queued:'<span class="dot"></span>' };
  function build(n){
    const row = html(`<div class="atr-n ${n.s}" role="treeitem" aria-level="${n.lv + 1}"${kids(n) ? ' aria-expanded="true"' : ''} tabindex="-1" style="--lv:${n.lv}">${kids(n) ? `<span class="atr-tg" aria-hidden="true">${I('chev-d','xs')}</span>` : '<span class="atr-sp"></span>'}<span class="atr-g">${G[n.s]}</span><span class="atr-t"><b>${n.n}</b><span>${n.t}</span></span><span class="atr-c num"></span></div>`);
    n.el = row; const out = [row];
    if (kids(n)){ const f = html(`<div class="fold atr-kids" role="group" style="--lv:${n.lv}"><div></div></div>`); n.kids.forEach(k => build(k).forEach(e => f.firstElementChild.appendChild(e))); n.fold = f; out.push(f); }
    return out;
  }
  build(T).forEach(e => tree.appendChild(e)); T.el.tabIndex = 0;
  // each branch folds by its row (or its chevron); folded, it keeps its total
  function toggle(n, open){
    if (!kids(n)) return; const o = open ?? n.el.getAttribute('aria-expanded') !== 'true';
    n.el.setAttribute('aria-expanded', o); n.fold.classList.toggle('shut', !o); n.fold.inert = !o;
  }
  function paint(){
    all.forEach(n => { const ce = $('.atr-c', n.el), v = kids(n) ? sum(n) : n.cost;
      ce.textContent = n.s === 'queued' && !v ? '–' : money(v);
      if (kids(n)){ const k = n.kids.length, what = n.lv ? (k > 1 ? ' steps ' : ' step ') : (k > 1 ? ' agents ' : ' agent ');
        ce.dataset.tip = `Its own ${money(n.cost)}, ${k}${what}${money(sum(n) - n.cost)}`; } });
    tot.textContent = money(sum(T));
  }
  // the line under the title: what the run is doing, and a clock
  const t0 = performance.now(); let ended = 0;
  const clock = () => { const s = Math.floor(((ended || performance.now()) - t0) / 1000); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
  stEl.innerHTML = '<span class="m" aria-live="polite">Running</span> · <span class="num ck">0:00</span>';
  const setMode = m => swap($('.m', stEl), m);
  // whatever is running spends as it goes, so its cost and every total above it count up
  c.every(100, () => { all.forEach(n => { if (n.s === 'run') n.cost += n.lv ? .0016 : .0006; }); paint(); $('.ck', stEl).textContent = clock(); });
  paint();
  function step(n, s, t){ n.s = s; n.el.className = 'atr-n ' + s; const g = $('.atr-g', n.el); g.innerHTML = G[s]; enter(g); if (t !== undefined) swap($('.atr-t span', n.el), t); paint(); }
  // clicking a branch folds it; the arrow keys move through the tree as in any tree view
  c.on(tree, 'click', e => { const row = e.target.closest('.atr-n'); if (!row || e.target.closest('.atr-fix')) return; const n = all.find(x => x.el === row); all.forEach(x => x.el.tabIndex = x === n ? 0 : -1); toggle(n); });
  const visible = () => all.filter(n => { for (let p = n.parent; p; p = p.parent) if (p.el.getAttribute('aria-expanded') === 'false') return false; return true; });
  c.on(tree, 'keydown', e => {
    const v = visible(), i = v.findIndex(n => n.el === document.activeElement); if (i < 0) return;
    const n = v[i], open = n.el.getAttribute('aria-expanded') === 'true'; let go = null;
    if (e.key === 'ArrowDown') go = v[i + 1]; else if (e.key === 'ArrowUp') go = v[i - 1];
    else if (e.key === 'ArrowRight'){ if (kids(n) && !open) toggle(n, true); else if (kids(n)) go = n.kids[0]; }
    else if (e.key === 'ArrowLeft'){ if (kids(n) && open) toggle(n, false); else go = n.parent; }
    else if (e.key === 'Enter' || e.key === ' ') toggle(n);
    else if (e.key === 'Home') go = v[0]; else if (e.key === 'End') go = v[v.length - 1];
    else return;
    e.preventDefault(); if (go){ all.forEach(x => x.el.tabIndex = -1); go.el.tabIndex = 0; go.el.focus({ preventScroll:true }); }
  });
  // the run: the design agent works through its steps, one fails on a token the library doesn't have, and the fix it offers
  // waits for you
  c.run(async () => {
    await c.wait(2000); step(by('des1'), 'done'); step(by('des2'), 'run');
    await c.wait(2500); step(by('des2'), 'done'); step(by('des3'), 'run');
    await c.wait(2000);
    const tk = by('des3'); step(tk, 'fail', "color.accent.600 isn't in the library"); setMode('1 step failed');
    const fix = html(`<button class="pill sm atr-fix">Use color.accent.500</button>`); $('.atr-t', tk.el).appendChild(fix); enter(fix);
    await new Promise(res => { fix.onclick = res; });
    fix.remove(); step(tk, 'run', 'Using color.accent.500'); setMode('Running');
    await c.wait(2000); step(tk, 'done', 'Colour and spacing, from the library'); step(by('des'), 'done'); step(by('cop'), 'run');
    await c.wait(3000); step(by('cop'), 'done'); step(T, 'done'); ended = performance.now(); setMode('Done');
    const sm = html(`<div class="atr-sum">${I('ok','xs')}<span>Done in ${clock()} · 3 agents · ${money(sum(T))}</span></div>`); root.appendChild(sm); enter(sm);
  });
  // the finished research starts folded, as a done branch would
  toggle(by('res'), false);
  // the preview's story: read a branch's cost, take the fix when a step fails, and fold the design branch once it's done
  c.story(async s => {
    await s.beat(3);
    await s.point($('.atr-c', by('des').el)); await s.beat(4); s.away();
    await s.until(() => $('.atr-fix', r)); await s.beat(4);
    await s.tap($('.atr-fix', r)); s.away();
    await s.until(() => T.s === 'done'); await s.beat(2);
    await s.tap(by('des').el); s.away(); await s.beat(1);
  });
});
06

Questions

What people ask about it.

Does it keep track of costs?
No. Give each node the cost it spent on its own and the tree adds up the branches. Update the costs as your run reports them and the totals follow.
Can it show more than three levels?
Yes. Nodes can nest as deep as your run does; each level indents a step further, with a guide line down the side.
What does the fix button do?
Whatever you pass as its onClick. In the demo it lets the failed step run again with the token the library has.
Can costs be in another currency?
Yes. Pass formatCost, which gets each amount and returns the text to show.
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 lanesSeveral agents working in parallel, each with its own lane and hand-offs.
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 →