Agent statusAgent inbox
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callAgent 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.
Install
Add it to your project with the CLI.
HTML, CSS and JavaScript
npx designme add agent-inboxReact
npx designme add agent-inbox --reactThe 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.
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.
runsis the list. Each run has atitle, theagentrunning it, agroupofneeds,runningordone, and adetailline, like "Waiting for your approval" or "Editing frame 3 of 6".- Each group shows its count, and a group with nothing in it is hidden. The line beside the title reads "2 need you", then "2 running" once nothing needs you, and "All clear · 4 done today" when every run is done.
onApproveadds a filled Approve button to the runs that need you. A run withfailedturns red and gets a white Retry button instead, throughonRetry;whyadds a tooltip on its detail line.- A running run spins and its clock counts on from its
seconds. A finished one shows a green check, when itended, and its agent. - Each row starts with the agent's first letter on its
color. - When a run moves to another group, every row and group title glides to its new place and the moving row rides above the rest.
- Each group is a labelled
section, and its runs form a list. - Approve and Retry are buttons named for their run, like "Approve Publish the pricing page".
- The line beside the title is announced politely when it changes.
- With reduced motion the rows move without gliding.
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>
);
}
Props
Everything AgentInbox takes.
| Prop | Type | What it does |
|---|---|---|
runsrequired | InboxRun[] | |
onApprove | (id: string) => void | Adds Approve to the runs that need you and haven't failed |
onRetry | (id: string) => void | Adds Retry to the runs that failed |
title | string | Defaults to "Inbox" |
status | string | The line beside the title. Defaults to "2 need you", then "2 running" once nothing needs you, then "All clear · 4 done today" |
className | string |
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);
});
});
Questions
What people ask about it.
- Does Approve start the run?
- No. It calls
onApprovewith the run's id. Start the run in your own code and move it torunning, then todonewhen 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
secondsa 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;
titleandstatuschange the header.
More in Agent status
Show what the agent is doing, how far along it is, and what it is working with.