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

Every run that needs you, is running or is done, in one list. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add agent-inbox
Inbox
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add agent-inbox

React

$npx designme add agent-inbox --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 inbox puts every agent run in one list, grouped by what it needs from you: Needs you, Running and Done. A run waiting for you has Approve, a failed one has Retry, and the running ones have live clocks. When a run changes group it glides to its new place, so you can follow it.

  • When several agents run at once and people need one place to see which ones are waiting for them.
  • When some runs need a person's approval before they go on, like publishing a page.
  • When a run can fail for a reason that a retry fixes, like a rate limit, and people should retry it from the list.
  • When people want to see what finished recently, and when.
03

Usage

Import it and drive it from your own state.

Import

import { AgentInbox } from '@/components/designme/agent-inbox/AgentInbox';

Example

The demo above as a React component, 47 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 { useRef, useState } from 'react';
import { AgentInbox, type InboxRun } from './AgentInbox';
import { useScript, useStory, useWait } from '../lib/useScript';

/* ---------- Agent inbox: approve the publish, retry the failed export, and watch both land in Done ---------- */
const COLOR: Record<string, string> = { 'Design agent': 'var(--dm-av-l)', 'Build agent': 'var(--dm-av-m)', 'Research agent': 'var(--dm-av-t)', 'Copy agent': 'var(--dm-av-p)', 'QA agent': 'var(--dm-av-b)' };
const START: InboxRun[] = [
  { id: 'pub', title: 'Publish the pricing page', agent: 'Design agent', group: 'needs', detail: 'Waiting for your approval' },
  { id: 'tok', title: 'Export tokens to CSS', agent: 'Build agent', group: 'needs', failed: true, detail: 'Failed: rate limited by Figma', why: 'Figma answered 429 at 10:42. Safe to retry' },
  { id: 'cards', title: 'Rework the plan cards', agent: 'Design agent', group: 'running', detail: 'Editing frame 3 of 6', seconds: 102 },
  { id: 'cmp', title: 'Compare 3 competitor pages', agent: 'Research agent', group: 'running', detail: 'Reading page 3 of 3', seconds: 38 },
  { id: 'copy', title: 'Rewrite plan descriptions', agent: 'Copy agent', group: 'done', ended: '12m ago' },
  { id: 'aud', title: 'Audit pricing contrast', agent: 'QA agent', group: 'done', ended: '25m ago' }
].map(r => ({ ...r, color: COLOR[r.agent] }) as InboxRun);

export default function AgentInboxDemo() {
  const ref = useRef<HTMLDivElement>(null);
  const wait = useWait();
  const [runs, setRuns] = useState(START);
  // a run that moves goes to the top of its new group
  const move = (id: string, patch: Partial<InboxRun>) => setRuns(rs => { const r = { ...rs.find(x => x.id === id)!, ...patch }; return [r, ...rs.filter(x => x.id !== id)]; });

  // Approve starts the run and Retry tries a failed one again; either runs, then lands in Done
  const start = async (id: string, retry: boolean) => {
    move(id, { group: 'running', failed: false, seconds: 0, detail: retry ? 'Retrying the export' : 'Publishing to designme.studio/pricing' });
    await wait(retry ? 5000 : 4000);
    move(id, { group: 'done', ended: 'Just now' });
  };

  // the research run finishes on its own while you look
  useScript(async ({ wait }) => { await wait(6000); move('cmp', { group: 'done', ended: 'Just now' }); });

  // the preview's story: approve the publish, then retry the export
  useStory(ref, async ({ wait, press }) => {
    await wait(3000); press('[aria-label="Approve Publish the pricing page"]');
    await wait(2500); press('[aria-label="Retry Export tokens to CSS"]');
  });

  return (
    <div ref={ref} style={{ width: '100%' }}>
      <AgentInbox runs={runs} onApprove={id => void start(id, false)} onRetry={id => void start(id, true)} />
    </div>
  );
}
04

Props

Everything AgentInbox takes.

PropTypeWhat it does
runsrequiredInboxRun[]
onApprove(id: string) => voidAdds Approve to the runs that need you and haven't failed
onRetry(id: string) => voidAdds Retry to the runs that failed
titlestringDefaults to "Inbox"
statusstringThe line beside the title. Defaults to "2 need you", then "2 running" once nothing needs you, then "All clear · 4 done today"
classNamestring
05

Code

The full source, as the CLI installs it.

AgentInbox.tsxReact · 137 lines
'use client';
import { useEffect, useRef, useState } from 'react';
import { cx } from '../lib/cx';
import { useIsoLayoutEffect, useReducedMotion, useSeconds } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { avatarInk, enter, Spin, Swap, Tip } from '../lib/motion';
import '../designme.css';
import './agent-inbox.css';

export type InboxGroup = 'needs' | 'running' | 'done';

export interface InboxRun {
  id: string;
  /** What the run is for, like "Publish the pricing page" */
  title: string;
  /** Which agent runs it. Its first letter marks the row, and a finished run shows it under the title */
  agent: string;
  /** The mark's fill, like 'var(--dm-av-l)'. An avatar colour brings its own ink, any other colour takes white initials. Defaults to the accent */
  color?: string;
  group: InboxGroup;
  /** The line under the title: why it needs you, or what it is doing now. A finished run shows its agent instead */
  detail?: string;
  /** Marks a run that needs you because it failed: its row turns red and offers Retry instead of Approve */
  failed?: boolean;
  /** More on the failure, in a tooltip on its detail line */
  why?: string;
  /** How long a running run has been going, in seconds. Its clock counts on from here */
  seconds?: number;
  /** When a finished run ended, like "12m ago" */
  ended?: string;
}

export interface AgentInboxProps {
  runs: InboxRun[];
  /** Adds Approve to the runs that need you and haven't failed */
  onApprove?: (id: string) => void;
  /** Adds Retry to the runs that failed */
  onRetry?: (id: string) => void;
  /** Defaults to "Inbox" */
  title?: string;
  /** The line beside the title. Defaults to "2 need you", then "2 running" once nothing needs you, then "All clear · 4 done today" */
  status?: string;
  className?: string;
}

const GROUPS: [InboxGroup, string][] = [['needs', 'Needs you'], ['running', 'Running'], ['done', 'Done']];
const clock = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;

/** Every agent run in one list, grouped as Needs you, Running and Done, with live clocks on the running ones. Approve
    starts a run that waits for you and Retry tries a failed one again; a run that changes group glides to its place. */
export function AgentInbox({ runs, onApprove, onRetry, title = 'Inbox', status, className }: AgentInboxProps) {
  const reduce = useReducedMotion();
  const list = useRef<HTMLDivElement>(null);
  // what changes after the first render plays its entrance; what is there from the start just appears
  const ready = useRef(false);
  useEffect(() => { ready.current = true; }, []);
  // one tick a second drives every running clock: each counts on from the seconds it had when it was last given
  const tick = useSeconds(runs.some(r => r.group === 'running'));
  const base = useRef(new Map<string, { s: number; at: number }>());
  const secs = (r: InboxRun) => {
    const b = base.current.get(r.id), s = r.seconds ?? 0;
    if (!b || b.s !== s) { base.current.set(r.id, { s, at: tick }); return s; }
    return b.s + tick - b.at;
  };
  for (const id of [...base.current.keys()]) if (!runs.some(r => r.id === id && r.group === 'running')) base.current.delete(id);

  const need = runs.filter(r => r.group === 'needs').length, running = runs.filter(r => r.group === 'running').length, done = runs.filter(r => r.group === 'done').length;
  const line = status ?? (need ? `${need} need${need > 1 ? '' : 's'} you` : running ? `${running} running` : `All clear · ${done} done today`);

  // a run that changes group glides there: rows and group titles are measured after each move and eased from where
  // they were, the moving row rides above the rest, and a group that comes back fades in
  const where = useRef(new Map<string, number>());
  const groupOf = useRef(new Map<string, InboxGroup>());
  const sig = runs.map(r => r.id + ':' + r.group).join();
  useIsoLayoutEffect(() => {
    const box = list.current; if (!box) return;
    const top = box.getBoundingClientRect().top, next = new Map<string, number>();
    box.querySelectorAll<HTMLElement>('[data-k]').forEach(el => {
      const k = el.dataset.k!, y = el.getBoundingClientRect().top - top, was = where.current.get(k);
      next.set(k, y);
      if (reduce || !where.current.size) return;
      if (was === undefined) { el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 260 }); return; }
      if (Math.abs(was - y) > 1) el.animate([{ transform: `translateY(${was - y}px)` }, { transform: 'none' }], { duration: 420, easing: getComputedStyle(box).getPropertyValue('--dm-ease').trim() || 'ease' });
      const moved = groupOf.current.get(k), now = runs.find(r => r.id === k)?.group;
      if (moved && now && moved !== now) { el.classList.add('dm-mv'); setTimeout(() => el.classList.remove('dm-mv'), 440); }
    });
    where.current = next;
    groupOf.current = new Map(runs.map(r => [r.id, r.group]));
  }, [sig]); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <div data-dm="" className={cx('dm-aib dm-panel', className)}>
      <div className="dm-aib-h"><b>{title}</b><span aria-live="polite"><Swap className="dm-muted dm-aib-st">{line}</Swap></span></div>
      <div ref={list}>
        {GROUPS.map(([g, name]) => {
          const rows = runs.filter(r => r.group === g);
          // a group with nothing in it goes away
          if (!rows.length) return null;
          return (
            <section key={g} className="dm-aib-g" aria-label={name}>
              <div className="dm-aib-gh" data-k={'h:' + g}><span>{name}</span><span className="dm-n">{rows.length}</span></div>
              <div className="dm-aib-gl" role="list">
                {rows.map(r => {
                  const col = r.color ?? 'var(--dm-accent)', fail = g === 'needs' && !!r.failed;
                  const detail = g === 'done' ? r.agent : r.detail ?? '';
                  return (
                    <div key={r.id} data-k={r.id} className={cx('dm-aib-i', fail && 'dm-fail')} role="listitem">
                      <span className="dm-aib-av" style={{ background: col, color: avatarInk(col) ?? (r.color ? '#fff' : 'var(--dm-on-accent)') }} aria-hidden="true">{r.agent.charAt(0)}</span>
                      <span className="dm-aib-t">
                        <b>{r.title}</b>
                        {fail && r.why ? <Tip label={r.why}><span><Swap>{detail}</Swap></span></Tip> : <Swap>{detail}</Swap>}
                      </span>
                      <Right key={g + (fail ? '!' : '')} run={r} group={g} fail={fail} secs={g === 'running' ? secs(r) : 0} fresh={ready.current} onApprove={onApprove} onRetry={onRetry} />
                    </div>
                  );
                })}
              </div>
            </section>
          );
        })}
      </div>
    </div>
  );
}

function Right({ run, group, fail, secs, fresh, onApprove, onRetry }: { run: InboxRun; group: InboxGroup; fail: boolean; secs: number; fresh: boolean; onApprove?: (id: string) => void; onRetry?: (id: string) => void }) {
  // it plays its entrance when it mounts after the first render, as a run changes group, and not again on each clock tick
  const [e] = useState(() => fresh ? enter() : undefined);
  let body = null;
  if (group === 'needs') {
    if (fail && onRetry) body = <button type="button" className="dm-pill dm-sm dm-aib-go" aria-label={`Retry ${run.title}`} onClick={() => onRetry(run.id)}><Icon name="refresh" size="xs" />Retry</button>;
    else if (!fail && onApprove) body = <button type="button" className="dm-pill dm-primary dm-sm dm-aib-go" aria-label={`Approve ${run.title}`} onClick={() => onApprove(run.id)}>Approve</button>;
  } else if (group === 'running') body = <><Spin /><span className="dm-num">{clock(secs)}</span></>;
  else body = <><span className="dm-ok"><Icon name="ok" size="xs" /></span><span>{run.ended}</span></>;
  return <span className={cx('dm-aib-r', e?.className)} style={e?.style}>{body}</span>;
}
agent-inbox.cssReact · 26 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Agent inbox ---------- */
/* every run in one list, grouped by what it needs: Needs you, Running, Done. A run glides to its new group when it moves */
.dm-aib{width:100%;max-width:440px;margin:0 auto;padding:6px}
.dm-aib-h{display:flex;align-items:baseline;justify-content:space-between;gap:var(--dm-s-3);padding:var(--dm-s-2) 10px 6px}
.dm-aib-h b{font-weight:500;font-size:14px}
.dm-aib-st{font-size:12.5px}
.dm-aib-g{display:flex;flex-direction:column}
.dm-aib-gh{display:flex;align-items:center;gap:var(--dm-s-2);padding:var(--dm-s-3) 10px var(--dm-s-1);font-size:12px;color:var(--dm-muted)}
.dm-aib-gh .dm-n{font-variant-numeric:tabular-nums}
.dm-aib-gl{display:flex;flex-direction:column}
.dm-aib-i{display:flex;align-items:center;gap:10px;padding:var(--dm-s-2) 10px;border-radius:10px;font-size:13px;transition:background .16s}
@media (hover:hover) and (pointer:fine){.dm-aib-i:hover{background:var(--dm-row)}}
.dm-aib-i.dm-mv{position:relative;z-index:1;background:var(--dm-surface);box-shadow:var(--dm-shadow)}
.dm-aib-i.dm-mv.dm-fail{background:var(--dm-bad-bg)}
.dm-aib-av{width:28px;height:28px;border-radius:var(--dm-r-sm);display:grid;place-items:center;flex:none;font-size:11px;font-weight:500}
.dm-aib-t{flex:1;min-width:0;display:flex;flex-direction:column}
.dm-aib-t b{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-aib-t span{font-size:12px;color:var(--dm-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-aib-i.dm-fail .dm-aib-t span{color:var(--dm-bad)}
.dm-aib-r{display:flex;align-items:center;gap:6px;flex:none;font-size:12.5px;color:var(--dm-fg2)}
.dm-aib-r .dm-spin{width:11px;height:11px;color:var(--dm-accent-fg,var(--dm-accent))}
.dm-aib-r .dm-ok{color:var(--dm-ok);display:grid}
.dm-aib-i.dm-fail{background:var(--dm-bad-bg)}
.dm-aib-i.dm-fail .dm-aib-r .dm-pill{background:var(--dm-surface);color:var(--dm-fg);box-shadow:var(--dm-shadow)}
agent-inbox.htmlHTML · 9 lines
<div class="card" data-c="agent-inbox">
  <div class="stage-in">
    <div class="aib panel">
      <div class="aib-h"><b>Inbox</b><span class="muted aib-st" aria-live="polite"></span></div>
      <div class="aib-gs"></div>
    </div>
  </div>
</div>
agent-inbox.cssHTML · 25 lines
/* ---------- Agent inbox ---------- */
/* every run in one list, grouped by what it needs: Needs you, Running, Done. A run glides to its new group when it moves */
.aib{width:100%;max-width:440px;margin:0 auto;padding:6px}
.aib-h{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-3);padding:var(--s-2) 10px 6px}
.aib-h b{font-weight:500;font-size:14px}
.aib-st{font-size:12.5px}
.aib-g{display:flex;flex-direction:column}
.aib-gh{display:flex;align-items:center;gap:var(--s-2);padding:var(--s-3) 10px var(--s-1);font-size:12px;color:var(--muted)}
.aib-gh .n{font-variant-numeric:tabular-nums}
.aib-gl{display:flex;flex-direction:column}
.aib-i{display:flex;align-items:center;gap:10px;padding:var(--s-2) 10px;border-radius:10px;font-size:13px;transition:background .16s}
@media (hover:hover) and (pointer:fine){.aib-i:hover{background:var(--row)}}
.aib-i.mv{position:relative;z-index:1;background:var(--surface);box-shadow:var(--shadow)}
.aib-i.mv.fail{background:var(--bad-bg)}
.aib-av{width:28px;height:28px;border-radius:var(--r-sm);display:grid;place-items:center;flex:none;font-size:11px;font-weight:500}
.aib-t{flex:1;min-width:0;display:flex;flex-direction:column}
.aib-t b{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.aib-t span{font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.aib-i.fail .aib-t span{color:var(--bad)}
.aib-r{display:flex;align-items:center;gap:6px;flex:none;font-size:12.5px;color:var(--fg2)}
.aib-r .spin{width:11px;height:11px;color:var(--accent-fg,var(--accent))}
.aib-r .ok{color:var(--ok);display:grid}
.aib-i.fail{background:var(--bad-bg)}
.aib-i.fail .aib-r .pill{background:var(--surface);color:var(--fg);box-shadow:var(--shadow)}
agent-inbox.jsHTML · 73 lines
DM.def('agent-inbox', (r, c) => {
  // the runs, grouped by what they need: one waits for approval, one failed and can be retried, two are running with their
  // clocks, two are done
  const A = { 'Design agent':['D','var(--av-l)'], 'Build agent':['B','var(--av-m)'], 'Research agent':['R','var(--av-t)'], 'Copy agent':['C','var(--av-p)'], 'QA agent':['Q','var(--av-b)'] };
  const R = [
    { id:'pub', t:'Publish the pricing page', a:'Design agent', g:'needs', why:'Waiting for your approval' },
    { id:'tok', t:'Export tokens to CSS', a:'Build agent', g:'needs', fail:true, why:'Failed: rate limited by Figma', tip:'Figma answered 429 at 10:42. Safe to retry' },
    { id:'cards', t:'Rework the plan cards', a:'Design agent', g:'run', sec:102, doing:'Editing frame 3 of 6' },
    { id:'cmp', t:'Compare 3 competitor pages', a:'Research agent', g:'run', sec:38, doing:'Reading page 3 of 3' },
    { id:'copy', t:'Rewrite plan descriptions', a:'Copy agent', g:'done', ago:'12m ago' },
    { id:'aud', t:'Audit pricing contrast', a:'QA agent', g:'done', ago:'25m ago' }
  ];
  const GR = [['needs', 'Needs you'], ['run', 'Running'], ['done', 'Done']];
  const gs = $('.aib-gs', r), st = $('.aib-st', r);
  gs.innerHTML = GR.map(([k, n]) => `<section class="aib-g" data-g="${k}" aria-label="${n}"><div class="aib-gh"><span>${n}</span><span class="n"></span></div><div class="aib-gl" role="list"></div></section>`).join('');
  const clock = s => Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
  const meta = x => x.g === 'needs' ? x.why : x.g === 'run' ? x.doing : x.a;
  const right = x => x.g === 'needs' ? (x.fail ? `<button class="pill sm aib-go" aria-label="Retry ${x.t}">${I('refresh','xs')}Retry</button>` : `<button class="pill primary sm aib-go" aria-label="Approve ${x.t}">Approve</button>`)
    : x.g === 'run' ? `<span class="spin"></span><span class="num aib-ck">${clock(x.sec)}</span>` : `<span class="ok">${I('ok','xs')}</span><span>${x.ago}</span>`;
  function row(x){
    const [m, col] = A[x.a];
    x.el = html(`<div class="aib-i" role="listitem"><span class="aib-av" style="background:${col};color:${col.replace(')', '-ink)')}" aria-hidden="true">${m}</span><span class="aib-t"><b>${x.t}</b><span></span></span><span class="aib-r"></span></div>`);
    update(x, true); return x.el;
  }
  function update(x, now){
    const el = x.el, sp = $('.aib-t span', el), rr = $('.aib-r', el);
    el.classList.toggle('fail', !!x.fail && x.g === 'needs');
    if (x.tip && x.g === 'needs') sp.dataset.tip = x.tip; else delete sp.dataset.tip;
    if (now){ sp.textContent = meta(x); rr.innerHTML = right(x); } else { swap(sp, meta(x)); rr.innerHTML = right(x); enter(rr); }
    const b = $('.aib-go', el); if (b) b.onclick = () => act(x);
  }
  const list = g => $(`.aib-g[data-g="${g}"] .aib-gl`, gs);
  R.forEach(x => list(x.g).appendChild(row(x)));
  function counts(){
    // a group with nothing in it goes away, and the line at the top says what is left
    GR.forEach(([k]) => { const n = R.filter(x => x.g === k).length, sec = $(`.aib-g[data-g="${k}"]`, gs); $('.n', sec).textContent = n; sec.hidden = !n; });
    const need = R.filter(x => x.g === 'needs').length, run = R.filter(x => x.g === 'run').length, done = R.filter(x => x.g === 'done').length;
    const t = need ? `${need} need${need > 1 ? '' : 's'} you` : run ? `${run} running` : `All clear · ${done} done today`;
    if (!st.textContent) st.textContent = t; else swap(st, t);
  }
  counts();
  // a run that moves glides to its place in the new group: rows and group titles are measured before and after, and eased
  // from where they were; the moving row rides above the rest, and a group that comes back fades in
  const ease = getComputedStyle(r).getPropertyValue('--ease').trim() || 'ease';
  const shown = () => $$('.aib-g:not([hidden]) .aib-gh,.aib-g:not([hidden]) .aib-i', gs);
  function move(x, g){
    const first = new Map(shown().map(e => [e, e.getBoundingClientRect().top]));
    x.g = g; update(x); list(g).prepend(x.el); counts();
    if (REDUCE) return;
    x.el.classList.add('mv'); c.later(440, () => x.el.classList.remove('mv'));
    shown().forEach(e => { if (!first.has(e)) return enter(e); const d = first.get(e) - e.getBoundingClientRect().top; if (Math.abs(d) > 1) e.animate([{ transform:`translateY(${d}px)` }, { transform:'none' }], { duration:420, easing:ease }); });
  }
  // every running run's clock ticks
  c.every(1000, () => R.forEach(x => { if (x.g === 'run'){ x.sec++; const ck = $('.aib-ck', x.el); if (ck) ck.textContent = clock(x.sec); } }));
  // Approve starts the run and Retry tries a failed one again; either runs, then lands in Done
  function act(x){
    const retry = !!x.fail; x.fail = false; x.sec = 0; x.doing = retry ? 'Retrying the export' : 'Publishing to designme.studio/pricing';
    move(x, 'run');
    c.later(retry ? 5000 : 4000, () => { x.ago = 'Just now'; move(x, 'done'); });
  }
  // the research run finishes on its own while you look
  c.later(6000, () => { const x = R.find(y => y.id === 'cmp'); if (x.g === 'run'){ x.ago = 'Just now'; move(x, 'done'); } });
  // the preview's story: read why the export failed, approve the publish, and retry the export
  c.story(async s => {
    const by = id => R.find(x => x.id === id);
    await s.beat(3);
    await s.point($('.aib-t span', by('tok').el)); await s.beat(3);
    await s.tap($('.aib-go', by('pub').el)); s.away(); await s.beat(5);
    await s.tap($('.aib-go', by('tok').el)); s.away();
    await s.until(() => by('tok').g === 'done' && by('pub').g === 'done'); await s.beat(3);
  });
});
06

Questions

What people ask about it.

Does Approve start the run?
No. It calls onApprove with the run's id. Start the run in your own code and move it to running, then to done when it ends.
Where does a moved run go in its group?
Wherever it sits in runs. The demo puts a moved run first, so the latest change is at the top.
Do the clocks need updating every second?
No. Pass the seconds a run has been going and its clock counts on by itself. Pass a new value and it counts on from that.
Can I name the groups differently?
Not yet. The groups are Needs you, Running and Done; title and status change the header.
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 treeAn orchestrator and its sub-agents, with cost rolled up per branch.
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 →