Skip to content
Star on GitHub
Agent statusRun queue
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

Run queue

Grouped tasks with live progress, retries and a compact view. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add run-queue
0 of 3 done
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add run-queue

React

$npx designme add run-queue --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.

Run queue lists the tasks an agent has lined up, each with its own steps, a status and a thin progress bar that fills as steps finish. A count at the top says how many tasks are done, a switch folds the steps away for a compact view, and a failed step can offer a Retry button.

  • When your agent works through several tasks in a row and people want to see what is done, running and still waiting.
  • When a step can fail and people should be able to retry just that step.
  • When the queue can get long and people want a compact view with only the task rows.
03

Usage

Import it and drive it from your own state.

Import

import { RunQueue } from '@/components/designme/run-queue/RunQueue';

Example

The demo above as a React component, 42 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 { RunQueue, type RunGroup } from './RunQueue';
import { useScript, useTakeover } from '../lib/useScript';

/* ---------- Run queue: three tasks run in turn; one step fails and waits for Retry, and retries by itself after a moment ---------- */
const TASKS: { title: string; steps: [string, string, boolean?][] }[] = [
  { title: 'Research three competitors', steps: [['Read their pricing pages', '3/3'], ['Compared plans and limits', '5 gaps']] },
  { title: 'Draft the launch email', steps: [['Pulled the new features', 'Done'], ['Wrote two subject lines', 'Done'], ['Sent a test to the team', 'Done', true]] },
  { title: 'Update the help centre', steps: [['Billing questions', '4 articles'], ['Inviting your team', '2 articles']] }
];

export default function RunQueueDemo() {
  const [groups, setGroups] = useState<RunGroup[]>(() => TASKS.map(t => ({ title: t.title, state: 'queued', steps: t.steps.map(([label, meta]) => ({ label, meta, state: 'queued' })) })));
  const retry = useRef<() => void>(undefined);
  const ref = useRef<HTMLDivElement>(null), mine = useTakeover(ref);
  const edit = (gi: number, g: Partial<RunGroup>, si?: number, s?: Partial<RunGroup['steps'][number]>) =>
    setGroups(all => all.map((x, k) => k !== gi ? x : { ...x, ...g, steps: x.steps.map((y, j) => (j === si ? { ...y, ...s } : y)) }));
  useScript(async ({ wait }) => {
    await wait(600);
    for (let gi = 0; gi < TASKS.length; gi++) {
      edit(gi, { state: 'running' });
      for (let si = 0, retried = false; si < TASKS[gi].steps.length; si++) {
        edit(gi, {}, si, { state: 'running' });
        await wait(900);
        if (TASKS[gi].steps[si][2] && !retried) {
          edit(gi, { state: 'failed' }, si, { state: 'failed' });
          // waits for Retry, and carries on by itself after a moment so the run never stalls, until you take over
          await new Promise<void>(res => { retry.current = res; wait(2800).then(() => { if (!mine()) res(); }); });
          retried = true; si--;
          edit(gi, { state: 'running', meta: 'Retrying' });
          continue;
        }
        edit(gi, {}, si, { state: 'done' });
      }
      edit(gi, { state: 'done', meta: undefined });
    }
  });
  return <div ref={ref} style={{ width: '100%' }}><RunQueue groups={groups} onRetry={() => retry.current?.()} /></div>;
}
04

Props

Everything RunQueue takes.

PropTypeWhat it does
groupsrequiredRunGroup[]
densityRunQueueDensitydetailed lists every step under its task, compact folds them away. Leave it out to let the switch change it
defaultDensityRunQueueDensity
onDensityChange(density: RunQueueDensity) => void
onRetry(group: number, step: number) => voidCalled by the Retry button on a failed step. Without it, failed steps show no button
barebooleanSits flat in a chat thread (the default, as on designme.dev). Set false to put the list on a card
classNamestring
05

Code

The full source, as the CLI installs it.

RunQueue.tsxReact · 101 lines
'use client';
import { cx } from '../lib/cx';
import { useControllable } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { Dot, Fold, Seg, Swap } from '../lib/motion';
import '../designme.css';
import './run-queue.css';

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

export interface RunStep {
  label: string;
  state: RunState;
  /** Quiet detail at the end once the step is done, like "3/3" or "4 articles". Defaults to "Done" */
  meta?: string;
}

export interface RunGroup {
  /** One task in the queue, like "Draft the launch email" */
  title: string;
  state: RunState;
  steps: RunStep[];
  /** Overrides the words at the end of the row, like "Retrying". Defaults to Queued, Running, "1 step failed" or Done */
  meta?: string;
}

export type RunQueueDensity = 'detailed' | 'compact';

export interface RunQueueProps {
  groups: RunGroup[];
  /** detailed lists every step under its task, compact folds them away. Leave it out to let the switch change it */
  density?: RunQueueDensity;
  defaultDensity?: RunQueueDensity;
  onDensityChange?: (density: RunQueueDensity) => void;
  /** Called by the Retry button on a failed step. Without it, failed steps show no button */
  onRetry?: (group: number, step: number) => void;
  /** Sits flat in a chat thread (the default, as on designme.dev). Set false to put the list on a card */
  bare?: boolean;
  className?: string;
}

const groupMeta = (g: RunGroup) => {
  if (g.meta) return g.meta;
  if (g.state === 'failed') { const n = g.steps.filter(s => s.state === 'failed').length || 1; return `${n} step${n > 1 ? 's' : ''} failed`; }
  return g.state === 'running' ? 'Running' : g.state === 'done' ? 'Done' : 'Queued';
};

function GroupGlyph({ state }: { state: RunState }) {
  if (state === 'running') return <span className="dm-spin" style={{ color: 'var(--dm-fg)' }} />;
  if (state === 'failed') return <span style={{ color: 'var(--dm-bad)' }}><Icon name="alert" size="xs" /></span>;
  if (state === 'done') return <span style={{ color: 'var(--dm-ok)' }}><Icon name="check" size="xs" /></span>;
  return <Dot color="var(--dm-faint)" />;
}

function StepGlyph({ state }: { state: RunState }) {
  if (state === 'running') return <span className="dm-spin" style={{ width: 10, height: 10 }} />;
  if (state === 'failed') return <span style={{ color: 'var(--dm-bad)' }}><Icon name="x" size="xs" /></span>;
  if (state === 'done') return <span style={{ color: 'var(--dm-ok)' }}><Icon name="check" size="xs" /></span>;
  return <span className="dm-dot" style={{ color: 'var(--dm-faint)', width: 5, height: 5 }} />;
}

/** A queue of agent tasks, each with its own steps, a progress line and a retry for the step that failed. */
export function RunQueue({ groups, density, defaultDensity = 'detailed', onDensityChange, onRetry, bare = true, className }: RunQueueProps) {
  const [dens, setDens] = useControllable(density, defaultDensity, onDensityChange);
  const done = groups.filter(g => g.state === 'done').length;
  return (
    <div data-dm="" className={cx('dm-rq', className)}>
      <div className="dm-rq-top">
        <Seg label="Density" value={dens} onChange={setDens} options={[{ value: 'detailed', label: 'Detailed' }, { value: 'compact', label: 'Compact' }]} />
        <Swap className="dm-rq-all dm-muted dm-num">{`${done} of ${groups.length} done`}</Swap>
      </div>
      <div className={cx('dm-rq-list', !bare && 'dm-panel')} style={bare ? { paddingInline: 0 } : undefined}>
        {groups.map((g, gi) => {
          const finished = g.steps.filter(s => s.state === 'done').length;
          return (
            <div key={gi} className="dm-rq-g">
              <div className="dm-rq-gh">
                <span className="dm-g"><GroupGlyph state={g.state} /></span>
                <span>{g.title}</span>
                <Swap className="dm-m">{groupMeta(g)}</Swap>
              </div>
              <div className="dm-rq-bar"><i style={{ width: (g.steps.length ? finished / g.steps.length * 100 : 0) + '%' }} /></div>
              <Fold open={dens === 'detailed'}>
                {g.steps.map((s, si) => (
                  <div key={si} className={cx('dm-rq-sub', s.state === 'queued' && 'dm-wait')}>
                    <span className="dm-g"><StepGlyph state={s.state} /></span>
                    <span>{s.label}</span>
                    {s.state === 'failed' && onRetry
                      ? <span className="dm-m"><button type="button" className="dm-rq-retry" onClick={() => onRetry(gi, si)}>Retry</button></span>
                      : <Swap className="dm-m">{s.state === 'done' ? s.meta ?? 'Done' : ''}</Swap>}
                  </div>
                ))}
              </Fold>
            </div>
          );
        })}
      </div>
    </div>
  );
}
run-queue.cssReact · 20 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Run queue ---------- */
.dm-rq{display:flex;flex-direction:column;gap:var(--dm-s-3)}
.dm-rq-top{display:flex;align-items:center;justify-content:space-between;font-size:12.5px}
.dm-rq-list{padding:6px}
.dm-rq-g{padding:var(--dm-s-1) 0;border-radius:var(--dm-r-in)}
.dm-rq-g+.dm-rq-g{border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent);border-radius:0}
.dm-rq-gh{display:flex;align-items:center;gap:10px;padding:var(--dm-s-2) 10px;font-weight:500}
.dm-rq-gh .dm-g{width:18px;height:18px;display:grid;place-items:center;flex:none}
.dm-rq-gh .dm-m{margin-left:auto;font-weight:400;font-size:12.5px;color:var(--dm-muted)}
.dm-rq-bar{height:2px;margin:-2px 10px var(--dm-s-1) 38px;border-radius:2px;background:var(--dm-line);overflow:hidden}
.dm-rq-bar i{display:block;height:100%;width:0;background:var(--dm-accent-mark,var(--dm-accent));transition:width .8s var(--dm-ease)}
.dm-rq-sub{display:flex;align-items:center;gap:10px;padding:5px 10px 5px 38px;font-size:13px;color:var(--dm-fg2)}
.dm-rq-sub .dm-g{width:14px;display:grid;place-items:center;flex:none}
.dm-rq-sub .dm-m{margin-left:auto;font-size:12px;color:var(--dm-muted)}
.dm-rq-sub.dm-wait{color:var(--dm-muted)}
.dm-rq-retry{height:22px;padding:0 9px;border-radius:var(--dm-r-pill);background:var(--dm-chip);font-size:11.5px;color:var(--dm-fg);transition:background .2s}
.dm-rq-retry.dm-on{background:var(--dm-hover)}
@media (hover:hover) and (pointer:fine){.dm-rq-retry:hover{background:var(--dm-hover)}}
run-queue.htmlHTML · 9 lines
<div class="card bare" data-c="run-queue">
  <div class="stage-in">
    <div class="rq">
      <div class="rq-top"><div class="seg2" role="group" aria-label="Density"><button aria-pressed="true" data-v="full">Detailed</button><button aria-pressed="false" data-v="compact">Compact</button></div><span class="rq-all muted num">0 of 3 done</span></div>
      <div class="rq-list panel"></div>
    </div>
  </div>
</div>
run-queue.cssHTML · 19 lines
/* ---------- Run queue ---------- */
.rq{display:flex;flex-direction:column;gap:var(--s-3)}
.rq-top{display:flex;align-items:center;justify-content:space-between;font-size:12.5px}
.rq-list{padding:6px}
.rq-g{padding:var(--s-1) 0;border-radius:var(--r-in)}
.rq-g+.rq-g{border-top:1px solid color-mix(in srgb,var(--line) 50%,transparent);border-radius:0}
.rq-gh{display:flex;align-items:center;gap:10px;padding:var(--s-2) 10px;font-weight:500}
.rq-gh .g{width:18px;height:18px;display:grid;place-items:center;flex:none}
.rq-gh .m{margin-left:auto;font-weight:400;font-size:12.5px;color:var(--muted)}
.rq-bar{height:2px;margin:-2px 10px var(--s-1) 38px;border-radius:2px;background:var(--line);overflow:hidden}
.rq-bar i{display:block;height:100%;width:0;background:var(--accent-mark,var(--accent));transition:width .8s var(--ease)}
.rq-sub{display:flex;align-items:center;gap:10px;padding:5px 10px 5px 38px;font-size:13px;color:var(--fg2)}
.rq-sub .g{width:14px;display:grid;place-items:center;flex:none}
.rq-sub .m{margin-left:auto;font-size:12px;color:var(--muted)}
.rq-sub.wait{color:var(--muted)}
.rq-retry{height:22px;padding:0 9px;border-radius:var(--r-pill);background:var(--chip);font-size:11.5px;color:var(--fg);transition:background .2s}
.rq-retry.on{background:var(--hover)}
@media (hover:hover) and (pointer:fine){.rq-retry:hover{background:var(--hover)}}
run-queue.jsHTML · 37 lines
DM.def('run-queue', (r, c) => {
  const G = [
    { t:'Research three competitors', subs:[['Read their pricing pages','3/3'],['Compared plans and limits','5 gaps']] },
    { t:'Draft the launch email', subs:[['Pulled the new features','Done'],['Wrote two subject lines','Done'],['Sent a test to the team','', 'fail']] },
    { t:'Update the help centre', subs:[['Billing questions','4 articles'],['Inviting your team','2 articles']] }
  ];
  const list = $('.rq-list', r), all = $('.rq-all', r);
  let compact = false, doneCount = 0;
  G.forEach((g, gi) => {
    const el = html(`<div class="rq-g"><div class="rq-gh"><span class="g"><span class="dot" style="color:var(--faint)"></span></span><span>${g.t}</span><span class="m">Queued</span></div><div class="rq-bar"><i></i></div><div class="fold"><div>${g.subs.map(s => `<div class="rq-sub wait"><span class="g"><span class="dot" style="color:var(--faint);width:5px;height:5px"></span></span><span>${s[0]}</span><span class="m"></span></div>`).join('')}</div></div></div>`);
    g.el = el; list.appendChild(el);
  });
  seg($('.seg2', r), v => { compact = v === 'compact'; G.forEach(g => $('.fold', g.el).classList.toggle('shut', compact)); });
  const setG = (g, glyph, m) => { $('.rq-gh .g', g.el).innerHTML = glyph; swap($('.rq-gh .m', g.el), m); };
  const ok = `<span style="color:var(--ok)">${I('check','xs')}</span>`;
  async function runGroup(g){
    setG(g, '<span class="spin" style="color:var(--fg)"></span>', 'Running');
    const subs = $$('.rq-sub', g.el), bar = $('.rq-bar i', g.el);
    for (let i = 0; i < subs.length; i++){
      const s = subs[i]; s.classList.remove('wait'); $('.g', s).innerHTML = '<span class="spin" style="width:10px;height:10px"></span>';
      await c.wait(900);
      if (g.subs[i][2] === 'fail' && !g.retried){
        $('.g', s).innerHTML = `<span style="color:var(--bad)">${I('x','xs')}</span>`;
        $('.m', s).innerHTML = '<button class="rq-retry">Retry</button>';
        setG(g, `<span style="color:var(--bad)">${I('alert','xs')}</span>`, '1 step failed');
        // waits for Retry, and presses it by itself after a moment so the run never stalls, until you take over
        await new Promise(res => { const b = $('.rq-retry', s); b.onclick = res; c.auto(2600, () => { if (b.isConnected){ b.classList.add('on'); c.auto(220, res); } }); });
        g.retried = true; i--; $('.m', s).textContent = ''; setG(g, '<span class="spin" style="color:var(--fg)"></span>', 'Retrying'); continue;
      }
      bar.style.width = ((i + 1) / subs.length * 100) + '%';
      $('.g', s).innerHTML = ok; swap($('.m', s), g.subs[i][1] || 'Done');
    }
    setG(g, ok, 'Done'); doneCount++; swap(all, doneCount + ' of 3 done');
  }
  c.run(async () => { await c.wait(600); for (const g of G) await runGroup(g); });
});
06

Questions

What people ask about it.

How do I let people retry a failed step?
Pass onRetry. It is called with the task index and the step index, and you rerun the step and set its state back to 'running'. Without onRetry, failed steps show no button.
Does the task's state follow its steps automatically?
No. You set each task's state and each step's state yourself. The component only works out the progress bar from the done steps and the "N of M done" count from the done tasks.
Can I start in the compact view?
Yes. Set defaultDensity="compact", or control density from your own state.
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.
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.
All 18 in Agent status →