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

What the agent did, what changed and what needs you. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add run-summary
Run complete
Finished in 12m 40s

Pricing cards now use spacing tokens

0 files 0 steps 0 tokens 0 cost
What changed
PricingCard.tsx+24 −18
tokens.css+9 −2
Pricing.test.tsx+31 −0
Needs you
The yearly badge overlaps the price on mobile
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add run-summary

React

$npx designme add run-summary --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 summary is the report an agent leaves when a run finishes: a one-line summary, a few headline numbers, the files it changed with lines added and removed, and anything that still needs the person. Buttons at the bottom open the changes or undo them all.

  • At the end of an agent run, when people need to see at a glance what happened before they review the details.
  • When the run changed files and people should see which ones and by how much.
  • When the agent finished but left questions or checks that need a person.
  • When people should be able to undo a whole run in one click.
03

Usage

Import it and drive it from your own state.

Import

import { RunSummary } from '@/components/designme/run-summary/RunSummary';

Example

The demo above as a React component, 24 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 { useState } from 'react';
import { RunSummary, type RunNeed } from './RunSummary';
import { useScript } from '../lib/useScript';

/* ---------- Run summary: the numbers roll in, Review settles the one thing left, Undo all takes it back ---------- */
export default function RunSummaryDemo() {
  const [need, setNeed] = useState<'open' | 'busy' | 'fixed'>('open');
  // Review spins for a second, then the question is answered
  useScript(async ({ wait }) => { if (need === 'busy') { await wait(1000); setNeed('fixed'); } }, [need]);
  const needs: RunNeed[] = [need === 'fixed'
    ? { key: 'badge', text: 'Badge moved above the price, nothing left to review' }
    : { key: 'badge', text: 'The yearly badge overlaps the price on mobile', action: { label: 'Review', busy: need === 'busy', onClick: () => setNeed('busy') } }];
  return (
    <div style={{ width: '100%' }}>
      <RunSummary subtitle="Finished in 12m 40s" summary="Pricing cards now use spacing tokens"
        stats={[{ label: 'files', value: 6 }, { label: 'steps', value: 14 }, { label: 'tokens', value: 182, suffix: 'k' }, { label: 'cost', value: 0.84, prefix: '$', decimals: 2 }]}
        changes={[{ file: 'PricingCard.tsx', added: 24, removed: 18 }, { file: 'tokens.css', added: 9, removed: 2 }, { file: 'Pricing.test.tsx', added: 31, removed: 0 }]}
        needs={needs} onDetails={() => {}} onOpenChanges={() => {}} />
    </div>
  );
}
04

Props

Everything RunSummary takes.

PropTypeWhat it does
titlestringDefaults to "Run complete"
undoneTitlestringThe title once the changes are undone
subtitlestringThe quiet line under the title, like "Finished in 12m 40s"
summaryReactNodeOne line on what the run did
statsRunStat[]
changesRunChange[]The files the run changed, under "What changed"
needsRunNeed[]What is left for the person, under "Needs you"
onDetails() => voidAdds a button in the corner that opens the run's details
onOpenChanges() => voidThe main button, "Open changes"
undonebooleanWhether the changes are undone. Leave it out to let "Undo all" toggle it
defaultUndoneboolean
onUndoneChange(undone: boolean) => void
barebooleanSits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own
classNamestring
05

Code

The full source, as the CLI installs it.

RunSummary.tsxReact · 138 lines
'use client';
import { useEffect, useState, type ReactNode } from 'react';
import { cx } from '../lib/cx';
import { useControllable, useTween } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { enter, Spin, Swap, Tip } from '../lib/motion';
import '../designme.css';
import './run-summary.css';

export interface RunStat {
  /** Like "files" or "cost" */
  label: string;
  /** Counts up from 0 when the summary appears */
  value: number;
  /** Before the number, like "$" */
  prefix?: string;
  /** After the number, like "k" */
  suffix?: string;
  /** Decimal places, like 2 for a cost */
  decimals?: number;
}

export interface RunChange {
  file: string;
  added: number;
  removed: number;
}

export interface RunNeed {
  key: string;
  /** What needs the person. Crossfades when it changes */
  text: string;
  /** A button at the end, like Review. busy swaps its label for a spinner */
  action?: { label: string; onClick: () => void; busy?: boolean };
}

export interface RunSummaryProps {
  /** Defaults to "Run complete" */
  title?: string;
  /** The title once the changes are undone */
  undoneTitle?: string;
  /** The quiet line under the title, like "Finished in 12m 40s" */
  subtitle?: string;
  /** One line on what the run did */
  summary?: ReactNode;
  stats?: RunStat[];
  /** The files the run changed, under "What changed" */
  changes?: RunChange[];
  /** What is left for the person, under "Needs you" */
  needs?: RunNeed[];
  /** Adds a button in the corner that opens the run's details */
  onDetails?: () => void;
  /** The main button, "Open changes" */
  onOpenChanges?: () => void;
  /** Whether the changes are undone. Leave it out to let "Undo all" toggle it */
  undone?: boolean;
  defaultUndone?: boolean;
  onUndoneChange?: (undone: boolean) => void;
  /** Sits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own */
  bare?: boolean;
  className?: string;
}

function Stat({ stat, delay }: { stat: RunStat; delay: number }) {
  // each number starts rolling a beat after the one before it
  const [go, setGo] = useState(false);
  useEffect(() => { const t = setTimeout(() => setGo(true), delay); return () => clearTimeout(t); }, [delay]);
  const v = useTween(go ? stat.value : 0, 1100, 0);
  return <span><b className="dm-num">{go ? (stat.prefix ?? '') + v.toFixed(stat.decimals ?? 0) + (stat.suffix ?? '') : '0'}</b> {stat.label}</span>;
}

/** What an agent run did, what it cost and what still needs the person, once it finishes. */
export function RunSummary({ title = 'Run complete', undoneTitle = 'Changes undone', subtitle, summary, stats, changes, needs, onDetails, onOpenChanges, undone, defaultUndone = false, onUndoneChange, bare = true, className }: RunSummaryProps) {
  const [isUndone, setUndone] = useControllable(undone, defaultUndone, onUndoneChange);
  // the glyph only plays its entrance once it has changed, not when the summary first appears
  const [seen, setSeen] = useState(isUndone);
  const [moved, setMoved] = useState(false);
  if (seen !== isUndone) { setSeen(isUndone); setMoved(true); }
  // the rows come in one after another, across both sections
  let row = 0;
  const next = () => enter(300 + row++ * 70);

  return (
    <div data-dm="" className={cx('dm-rsu', !bare && 'dm-panel', isUndone && 'dm-undone', className)} style={bare ? { paddingInline: 0 } : undefined}>
      <div className="dm-rsu-h">
        <span className="dm-rsu-g" style={isUndone ? { background: 'var(--dm-chip)', color: 'var(--dm-fg2)' } : undefined}>
          <Icon key={isUndone ? 'undo' : 'check'} name={isUndone ? 'undo' : 'check'} className={moved ? 'dm-enter' : undefined} />
        </span>
        <div>
          <Swap className="dm-rsu-t">{isUndone ? undoneTitle : title}</Swap>
          {subtitle && <div className="dm-rsu-s dm-muted">{subtitle}</div>}
        </div>
        {onDetails && <Tip label="Run details"><button type="button" className="dm-ib dm-rsu-more" aria-label="More" onClick={onDetails}><Icon name="more" /></button></Tip>}
      </div>
      {summary && <p className="dm-rsu-title">{summary}</p>}
      {stats?.length ? <div className="dm-rsu-stats">{stats.map((s, i) => <Stat key={s.label} stat={s} delay={200 + i * 90} />)}</div> : null}
      {changes?.length ? (
        <div className="dm-rsu-sec">
          <div className="dm-rsu-l">What changed</div>
          {changes.map(c => {
            const e = next();
            return (
              <div key={c.file} className={cx('dm-rsu-it', e.className)} style={e.style}>
                <span className="dm-mono">{c.file}</span>
                <span className="dm-num"><i className={c.added ? 'dm-a' : 'dm-z'}>+{c.added}</i> <i className={c.removed ? 'dm-x' : 'dm-z'}>−{c.removed}</i></span>
              </div>
            );
          })}
        </div>
      ) : null}
      {needs?.length ? (
        <div className="dm-rsu-sec">
          <div className="dm-rsu-l">Needs you</div>
          {needs.map(n => {
            const e = next();
            return (
              <div key={n.key} className={cx('dm-rsu-it dm-rsu-need', e.className)} style={e.style}>
                <span className="dm-dot" style={{ color: 'var(--dm-warn)' }} />
                <Swap className="dm-rsu-nt">{n.text}</Swap>
                {n.action && <button type="button" className="dm-pill dm-soft dm-sm dm-rsu-fix" onClick={n.action.busy ? undefined : n.action.onClick} aria-busy={n.action.busy || undefined}>{n.action.busy ? <Spin /> : n.action.label}</button>}
              </div>
            );
          })}
        </div>
      ) : null}
      {(() => {
        const e = next();
        return (
          <div className={cx('dm-rsu-acts', e.className)} style={e.style}>
            <button type="button" className="dm-pill dm-primary dm-sm" onClick={onOpenChanges}>Open changes</button>
            <button type="button" className="dm-pill dm-ghost dm-sm dm-rsu-undo" onClick={() => setUndone(!isUndone)}><Swap>{isUndone ? 'Redo' : 'Undo all'}</Swap></button>
          </div>
        );
      })()}
    </div>
  );
}
run-summary.cssReact · 23 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Run summary ---------- */
.dm-rsu{padding:var(--dm-s-4)}
.dm-rsu-h{display:flex;align-items:center;gap:var(--dm-s-3)}
.dm-rsu-g{width:34px;height:34px;border-radius:50%;background:var(--dm-ok-bg);color:var(--dm-ok);display:grid;place-items:center;transition:background .4s,color .4s}
.dm-rsu-t{font-weight:500}
.dm-rsu-s{font-size:12.5px}
.dm-rsu-more{margin-left:auto}
.dm-rsu-title{margin:14px 0 0;font-size:14px;font-weight:500;line-height:1.45}
.dm-rsu-stats{display:flex;flex-wrap:wrap;gap:var(--dm-s-1) var(--dm-s-4);margin-top:6px;font-size:12.5px;color:var(--dm-muted)}
.dm-rsu-stats b{font-weight:500;color:var(--dm-fg2)}
.dm-rsu-sec{padding-top:var(--dm-s-3);margin-top:14px;border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-rsu-l{font-size:12px;color:var(--dm-muted);margin-bottom:6px}
.dm-rsu-it{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:var(--dm-s-1) 0;font-size:13px;transition:opacity .4s,filter .4s}
.dm-rsu-it .dm-mono{font-size:12.5px}
.dm-rsu-need{justify-content:flex-start}
.dm-rsu-need .dm-rsu-nt{flex:1;min-width:0}
.dm-rsu-it i{font-style:normal}.dm-rsu-it .dm-a{color:var(--dm-add)}.dm-rsu-it .dm-x{color:var(--dm-del)}.dm-rsu-it .dm-z{color:var(--dm-muted)}
.dm-rsu.dm-undone .dm-rsu-it:not(.dm-rsu-need){opacity:.45;filter:grayscale(1)}
.dm-rsu.dm-undone .dm-rsu-it .dm-mono{text-decoration:line-through}
.dm-rsu-acts{display:flex;gap:6px;margin-top:14px}
@media (max-width:420px){.dm-rsu-stats{grid-template-columns:repeat(2,1fr)}}
run-summary.htmlHTML · 24 lines
<div class="card bare" data-c="run-summary">
  <div class="stage-in">
    <div class="rsu panel">
      <div class="rsu-h"><span class="rsu-g"><svg class="i s"><use href="#i-check"/></svg></span><div><div class="rsu-t">Run complete</div><div class="rsu-s muted">Finished in 12m 40s</div></div><button class="ib rsu-more" aria-label="More" data-tip="Run details"><svg class="i s"><use href="#i-more"/></svg></button></div>
      <p class="rsu-title">Pricing cards now use spacing tokens</p>
      <div class="rsu-stats">
        <span><b class="num" data-n="6">0</b> files</span>
        <span><b class="num" data-n="14">0</b> steps</span>
        <span><b class="num" data-n="182" data-u="k">0</b> tokens</span>
        <span><b class="num" data-n="0.84" data-p="$">0</b> cost</span>
      </div>
      <div class="rsu-sec"><div class="rsu-l">What changed</div>
        <div class="rsu-it"><span class="mono">PricingCard.tsx</span><span class="num"><i class="a">+24</i> <i class="x">−18</i></span></div>
        <div class="rsu-it"><span class="mono">tokens.css</span><span class="num"><i class="a">+9</i> <i class="x">−2</i></span></div>
        <div class="rsu-it"><span class="mono">Pricing.test.tsx</span><span class="num"><i class="a">+31</i> <i class="z">−0</i></span></div>
      </div>
      <div class="rsu-sec"><div class="rsu-l">Needs you</div>
        <div class="rsu-it rsu-need"><span class="dot" style="color:var(--warn)"></span><span class="rsu-nt">The yearly badge overlaps the price on mobile</span><button class="pill soft sm rsu-fix">Review</button></div>
      </div>
      <div class="rsu-acts"><button class="pill primary sm">Open changes</button><button class="pill ghost sm rsu-undo">Undo all</button></div>
    </div>
  </div>
</div>
run-summary.cssHTML · 22 lines
/* ---------- Run summary ---------- */
.rsu{padding:var(--s-4)}
.rsu-h{display:flex;align-items:center;gap:var(--s-3)}
.rsu-g{width:34px;height:34px;border-radius:50%;background:var(--ok-bg);color:var(--ok);display:grid;place-items:center;transition:background .4s,color .4s}
.rsu-t{font-weight:500}
.rsu-s{font-size:12.5px}
.rsu-more{margin-left:auto}
.rsu-title{margin:14px 0 0;font-size:14px;font-weight:500;line-height:1.45}
.rsu-stats{display:flex;flex-wrap:wrap;gap:var(--s-1) var(--s-4);margin-top:6px;font-size:12.5px;color:var(--muted)}
.rsu-stats b{font-weight:500;color:var(--fg2)}
.rsu-sec{padding-top:var(--s-3);margin-top:14px;border-top:1px solid color-mix(in srgb,var(--line) 50%,transparent)}
.rsu-l{font-size:12px;color:var(--muted);margin-bottom:6px}
.rsu-it{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:var(--s-1) 0;font-size:13px;transition:opacity .4s,filter .4s}
.rsu-it .mono{font-size:12.5px}
.rsu-need{justify-content:flex-start}
.rsu-need .rsu-nt{flex:1;min-width:0}
.rsu-it i{font-style:normal}.rsu-it .a{color:var(--add)}.rsu-it .x{color:var(--del)}.rsu-it .z{color:var(--muted)}
.rsu.undone .rsu-it:not(.rsu-need){opacity:.45;filter:grayscale(1)}
.rsu.undone .rsu-it .mono{text-decoration:line-through}
.rsu-acts{display:flex;gap:6px;margin-top:14px}
@media (max-width:420px){.rsu-stats{grid-template-columns:repeat(2,1fr)}}
run-summary.jsHTML · 13 lines
DM.def('run-summary', (r, c) => {
  r.innerHTML = r.innerHTML.replace('{ok}', I('check')).replace('{more}', I('more'));
  const root = $('.rsu', r), undo = $('.rsu-undo', r), fix = $('.rsu-fix', r), g = $('.rsu-g', r);
  $$('.rsu-stats b', r).forEach((b, i) => c.later(200 + i * 90, () => tween(0, parseFloat(b.dataset.n), 1100, v => { b.textContent = (b.dataset.p || '') + (b.dataset.n.includes('.') ? v.toFixed(2) : Math.round(v)) + (b.dataset.u || ''); })));
  stagger($$('.rsu-it,.rsu-acts', r), 70, 300);
  undo.onclick = () => {
    const on = root.classList.toggle('undone');
    swap(undo, on ? 'Redo' : 'Undo all'); swap($('.rsu-t', r), on ? 'Changes undone' : 'Run complete');
    g.style.background = on ? 'var(--chip)' : ''; g.style.color = on ? 'var(--fg2)' : ''; g.innerHTML = I(on ? 'undo' : 'check'); enter(g.firstElementChild);
  };
  fix.onclick = () => { fix.innerHTML = '<span class="spin"></span>'; c.later(1000, () => { fix.remove(); swap($('.rsu-nt', r), 'Badge moved above the price, nothing left to review'); }); };
});
06

Questions

What people ask about it.

Does Undo all revert the files?
No. It changes the card's state and calls onUndoneChange(true). Your code reverts the changes, and Redo calls it again with false.
How do I show that a "Needs you" item is being handled?
Set busy: true on its action while you work on it, then replace its text, and drop the action, once it is resolved. The new text crossfades in.
Can I show a cost with two decimals?
Yes. Pass a stat like { label: 'cost', value: 0.84, prefix: '$', decimals: 2 }.
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.
Agent run timelineEvery model and tool step of a run on one time axis.
All 18 in Agent status →