Agent statusRun cost
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callRun cost
Live credit and token spend, with a cap that pauses the run. 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 run-costReact
npx designme add run-cost --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.
Run cost shows how many credits an agent run has used against its budget, as a big number and a bar with a marker where the agent should pause. Under the bar it breaks the spend down, for example into input tokens, output tokens and tool calls, and a switch turns the pause on or off.
- When agent runs cost money or credits and people want to watch the spend while the run is going.
- When a long run should stop at a set share of the budget and wait for someone to approve more.
- When people ask what a run's cost is made of and you want to show the token and tool numbers behind it.
usedandbudgetset the big number and how far the bar is filled. Numbers are rounded and shown with thousands separators.- A marker on the bar sits at
capAtpercent (90 by default), with a tooltip saying the agent pauses there. It fades when the cap switch is off. - The "Pause at N%" switch can be left to the component or controlled with
capOnandonCapChange. statussets the tag and colour: idle, running (blue), stopped, paused (amber, and the bar turns amber too) or spent (red).statusLabelreplaces the tag's words.- The main button's label follows the status: Start run, Stop, Resume, Raise cap or Start over. It calls
onAction, andactionLabelreplaces the label. rowslists the breakdown under the bar, andtitlereplaces the "Credits used" heading.
- The bar is a
role="meter"witharia-valuenowset to the credits used andaria-valuemaxset to the budget, labelled with the title. - The cap control is a
role="switch"witharia-checkedand a label like "Pause at 90 percent".
Usage
Import it and drive it from your own state.
Import
import { RunCost } from '@/components/designme/run-cost/RunCost';Example
The demo above as a React component, 48 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 { RunCost, type RunCostStatus } from './RunCost';
import { useTakeover } from '../lib/useScript';
/* ---------- Run cost: the run starts by itself and burns credits until the cap pauses it; raise the cap to finish ---------- */
const BUDGET = 2000;
export default function RunCostDemo() {
const [status, setStatus] = useState<RunCostStatus>('idle');
const [capOn, setCapOn] = useState(true);
const [n, setN] = useState({ used: 0, input: 0, output: 0, tools: 0 });
// the ticker reads these without restarting
const live = useRef({ n, capOn });
useEffect(() => { live.current = { n, capOn }; });
useEffect(() => {
if (status !== 'running') return;
const id = setInterval(() => {
const { n: p, capOn: cap } = live.current, limit = cap ? BUDGET * 0.9 : BUDGET;
const next = { used: Math.min(BUDGET, p.used + 8 + Math.random() * 18), input: p.input + 120 + Math.random() * 220, output: p.output + 30 + Math.random() * 70, tools: p.tools + (Math.random() < 0.12 ? 1 : 0) };
if (next.used >= limit) { next.used = limit; setStatus(limit < BUDGET ? 'paused' : 'spent'); }
live.current.n = next; setN(next);
}, 90);
return () => clearInterval(id);
}, [status]);
// the run starts by itself once the card has settled, unless you take over first
const ref = useRef<HTMLDivElement>(null), mine = useTakeover(ref);
useEffect(() => { const t = setTimeout(() => { if (!mine()) setStatus(s => (s === 'idle' ? 'running' : s)); }, 1500); return () => clearTimeout(t); }, [mine]);
const act = () => {
if (status === 'running') return setStatus('stopped');
// Raise cap turns the pause off and carries on to the full budget
if (status === 'paused') { setCapOn(false); live.current.capOn = false; }
if (n.used >= BUDGET) setN({ used: 0, input: 0, output: 0, tools: 0 });
setStatus('running');
};
return (
<div ref={ref} style={{ width: '100%' }}>
<RunCost used={n.used} budget={BUDGET} capOn={capOn} onCapChange={setCapOn} status={status} onAction={act}
rows={[{ label: 'Input tokens', value: n.input }, { label: 'Output tokens', value: n.output }, { label: 'Tool calls', value: n.tools }]} />
</div>
);
}
Props
Everything RunCost takes.
| Prop | Type | What it does |
|---|---|---|
usedrequired | number | Credits used so far |
budgetrequired | number | The whole budget, like 2000 |
capAt | number | Where the agent pauses, as a percentage of the budget. Defaults to 90 |
capOn | boolean | Whether the agent pauses at the cap. Leave it out to let the switch change it |
defaultCapOn | boolean | |
onCapChange | (on: boolean) => void | |
status | RunCostStatus | running is blue, paused (at the cap) amber, spent (the whole budget) red |
statusLabel | string | Words in the status tag. Defaults to Idle, Running, Stopped, "Paused at 90%" or "Budget used" |
rows | RunCostRow[] | The breakdown under the bar, like input tokens, output tokens and tool calls |
actionLabel | string | Words on the button. Defaults to Start run, Stop, Resume, Raise cap or Start over, by status |
onAction | () => void | Called by the button. What it does is up to you: start, stop, resume, raise the cap |
title | string | |
bare | boolean | Sits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own |
className | string |
Code
The full source, as the CLI installs it.
RunCost.tsxReact · 79 lines
'use client';
import { cx } from '../lib/cx';
import { useControllable } from '../lib/hooks';
import { Swap, Tip } from '../lib/motion';
import '../designme.css';
import './run-cost.css';
export type RunCostStatus = 'idle' | 'running' | 'stopped' | 'paused' | 'spent';
export interface RunCostRow {
/** Like "Input tokens" */
label: string;
value: number;
}
export interface RunCostProps {
/** Credits used so far */
used: number;
/** The whole budget, like 2000 */
budget: number;
/** Where the agent pauses, as a percentage of the budget. Defaults to 90 */
capAt?: number;
/** Whether the agent pauses at the cap. Leave it out to let the switch change it */
capOn?: boolean;
defaultCapOn?: boolean;
onCapChange?: (on: boolean) => void;
/** running is blue, paused (at the cap) amber, spent (the whole budget) red */
status?: RunCostStatus;
/** Words in the status tag. Defaults to Idle, Running, Stopped, "Paused at 90%" or "Budget used" */
statusLabel?: string;
/** The breakdown under the bar, like input tokens, output tokens and tool calls */
rows?: RunCostRow[];
/** Words on the button. Defaults to Start run, Stop, Resume, Raise cap or Start over, by status */
actionLabel?: string;
/** Called by the button. What it does is up to you: start, stop, resume, raise the cap */
onAction?: () => void;
title?: string;
/** 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;
}
const fmt = (n: number) => Math.round(n).toLocaleString('en-US');
const TONE: Partial<Record<RunCostStatus, string>> = { running: 'dm-b', paused: 'dm-warn', spent: 'dm-bad' };
const ACTION: Record<RunCostStatus, string> = { idle: 'Start run', running: 'Stop', stopped: 'Resume', paused: 'Raise cap', spent: 'Start over' };
/** Credits a run has used against its budget, with a cap the agent pauses at and the tokens behind the number. */
export function RunCost({ used, budget, capAt = 90, capOn, defaultCapOn = true, onCapChange, status = 'idle', statusLabel, rows, actionLabel, onAction, title = 'Credits used', bare = true, className }: RunCostProps) {
const [cap, setCap] = useControllable(capOn, defaultCapOn, onCapChange);
const label = statusLabel ?? { idle: 'Idle', running: 'Running', stopped: 'Stopped', paused: `Paused at ${capAt}%`, spent: 'Budget used' }[status];
return (
<div data-dm="" className={cx('dm-rc', !bare && 'dm-panel', className)} style={bare ? { paddingInline: 0 } : undefined}>
<div className="dm-rc-h">
<span className="dm-muted">{title}</span>
<span className={cx('dm-rc-st dm-tag', TONE[status])}><span className="dm-dot" /><Swap className="dm-rc-stt">{label}</Swap></span>
</div>
<div className="dm-rc-big"><span className="dm-rc-n dm-num">{fmt(used)}</span><span className="dm-muted"> / {fmt(budget)}</span></div>
<div className="dm-rc-track" role="meter" aria-label={title} aria-valuemin={0} aria-valuemax={budget} aria-valuenow={Math.round(used)}>
<div className="dm-rc-fill" style={{ width: Math.max(0, Math.min(100, used / budget * 100)) + '%', background: status === 'paused' ? 'var(--dm-warn)' : 'var(--dm-accent)' }} />
<Tip label="The agent pauses here"><div className="dm-rc-cap" style={{ left: capAt + '%', opacity: cap ? 1 : 0.2 }} /></Tip>
</div>
{rows && (
<div className="dm-rc-rows">
{rows.map(r => <div key={r.label}><span>{r.label}</span><span className="dm-num">{fmt(r.value)}</span></div>)}
</div>
)}
<div className="dm-rc-f">
<label className="dm-rc-cap-t" onClick={e => { e.preventDefault(); setCap(!cap); }}>
<button type="button" className="dm-toggle" role="switch" aria-checked={cap} aria-label={`Pause at ${capAt} percent`} />
<span>Pause at {capAt}%</span>
</label>
<button type="button" className={cx('dm-pill dm-sm dm-rc-go', status === 'running' || status === 'stopped' ? 'dm-soft' : 'dm-primary')} onClick={onAction}>
<Swap>{actionLabel ?? ACTION[status]}</Swap>
</button>
</div>
</div>
);
}
run-cost.cssReact · 17 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Run cost ---------- */
.dm-rc{padding:18px}
.dm-rc-h{display:flex;align-items:center;justify-content:space-between;font-size:12.5px}
.dm-rc-st{transition:background .3s,color .3s}
.dm-rc-big{margin-top:6px;font-size:13px}
.dm-rc-n{font-size:14px;font-weight:500;letter-spacing:-.8px}
.dm-rc-track{position:relative;height:6px;border-radius:var(--dm-r-xs);background:var(--dm-chip);margin:14px 0 var(--dm-s-4)}
.dm-rc-fill{height:100%;width:0;border-radius:var(--dm-r-xs);background:var(--dm-accent-mark,var(--dm-accent));transition:background .4s}
.dm-rc-cap{position:absolute;left:90%;top:-5px;bottom:-5px;width:2px;border-radius:1px;background:var(--dm-fg2);transition:opacity .3s}
.dm-rc-cap::before{content:"";position:absolute;inset:-6px -8px}
.dm-rc-rows{display:flex;flex-direction:column;gap:2px}
.dm-rc-rows div{display:flex;justify-content:space-between;padding:5px 0;font-size:13px;color:var(--dm-fg2)}
.dm-rc-rows .dm-num{color:var(--dm-fg)}
.dm-rc-f{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:var(--dm-s-3);padding-top:var(--dm-s-3);border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-rc-cap-t{display:flex;align-items:center;gap:9px;font-size:13px;color:var(--dm-fg2);cursor:pointer}
run-cost.htmlHTML · 19 lines
<div class="card bare" data-c="run-cost">
<div class="stage-in">
<div class="rc panel">
<div class="rc-h"><span class="muted">Credits used</span><span class="rc-st tag"><span class="dot"></span><span class="rc-stt">Idle</span></span></div>
<div class="rc-big"><span class="rc-n num">0</span><span class="muted"> / 2,000</span></div>
<div class="rc-track"><div class="rc-fill"></div><div class="rc-cap" data-tip="The agent pauses here"></div></div>
<div class="rc-rows">
<div><span>Input tokens</span><span class="num rc-in">0</span></div>
<div><span>Output tokens</span><span class="num rc-out">0</span></div>
<div><span>Tool calls</span><span class="num rc-tools">0</span></div>
</div>
<div class="rc-f">
<label class="rc-cap-t"><button class="toggle" role="switch" aria-checked="true" aria-label="Pause at 90 percent"></button><span>Pause at 90%</span></label>
<button class="pill primary sm rc-go">Start run</button>
</div>
</div>
</div>
</div>
run-cost.cssHTML · 16 lines
/* ---------- Run cost ---------- */
.rc{padding:18px}
.rc-h{display:flex;align-items:center;justify-content:space-between;font-size:12.5px}
.rc-st{transition:background .3s,color .3s}
.rc-big{margin-top:6px;font-size:13px}
.rc-n{font-size:14px;font-weight:500;letter-spacing:-.8px}
.rc-track{position:relative;height:6px;border-radius:var(--r-xs);background:var(--chip);margin:14px 0 var(--s-4)}
.rc-fill{height:100%;width:0;border-radius:var(--r-xs);background:var(--accent-mark,var(--accent));transition:background .4s}
.rc-cap{position:absolute;left:90%;top:-5px;bottom:-5px;width:2px;border-radius:1px;background:var(--fg2);transition:opacity .3s}
.rc-cap::before{content:"";position:absolute;inset:-6px -8px}
.rc-rows{display:flex;flex-direction:column;gap:2px}
.rc-rows div{display:flex;justify-content:space-between;padding:5px 0;font-size:13px;color:var(--fg2)}
.rc-rows .num{color:var(--fg)}
.rc-f{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:var(--s-3);padding-top:var(--s-3);border-top:1px solid color-mix(in srgb,var(--line) 50%,transparent)}
.rc-cap-t{display:flex;align-items:center;gap:9px;font-size:13px;color:var(--fg2);cursor:pointer}
run-cost.jsHTML · 32 lines
DM.def('run-cost', (r, c) => {
// the label crossfades on its own span, so the button itself never blurs
const go = $('.rc-go', r); go.innerHTML = '<span class="rc-gl">Start run</span>'; const gl = () => $('.rc-gl', go);
const tg = $('.toggle', r), cap = $('.rc-cap', r), fill = $('.rc-fill', r), st = $('.rc-st', r), stt = $('.rc-stt', r);
let used = 0, inT = 0, outT = 0, tools = 0, running = false, limit = 1800, loop = 0;
const fmt = n => Math.round(n).toLocaleString('en-US');
const paint = () => { $('.rc-n', r).textContent = fmt(used); fill.style.width = (used / 2000 * 100) + '%'; $('.rc-in', r).textContent = fmt(inT); $('.rc-out', r).textContent = fmt(outT); $('.rc-tools', r).textContent = tools; };
const status = (t, cls) => { st.className = 'rc-st tag ' + (cls || ''); swap(stt, t); };
const capOn = () => tg.getAttribute('aria-checked') === 'true';
$('.rc-cap-t', r).onclick = e => { e.preventDefault(); tg.setAttribute('aria-checked', !capOn()); cap.style.opacity = capOn() ? 1 : .2; limit = capOn() ? 1800 : 2000; if (!running && used < limit && used > 0) go.disabled = false; };
function stop(t, cls){ running = false; clearInterval(loop); status(t, cls); }
go.onclick = () => {
if (running){ stop('Stopped'); swap(gl(), 'Resume'); return; }
if (used >= 2000){ used = inT = outT = tools = 0; paint(); }
running = true; status('Running', 'b'); swap(gl(), 'Stop'); go.className = 'pill soft sm rc-go'; fill.style.background = 'var(--accent-mark,var(--accent))';
loop = c.every(90, () => {
used = Math.min(2000, used + 8 + Math.random() * 18); inT += 120 + Math.random() * 220; outT += 30 + Math.random() * 70; if (Math.random() < .12) tools++;
paint();
if (used >= limit){
used = limit; paint();
if (limit < 2000){ stop('Paused at 90%', 'warn'); fill.style.background = 'var(--warn)'; swap(gl(), 'Raise cap'); go.className = 'pill primary sm rc-go'; go.onclick2 = true; }
else { stop('Budget used', 'bad'); swap(gl(), 'Start over'); go.className = 'pill primary sm rc-go'; }
}
});
};
const orig = go.onclick;
go.addEventListener('click', e => { if (go.onclick2 && !running){ go.onclick2 = false; e.stopImmediatePropagation(); tg.setAttribute('aria-checked','false'); cap.style.opacity = .2; limit = 2000; orig(); } }, true);
paint();
// the run starts by itself once the card has settled, so the spend can be seen climbing to the cap
c.auto(1500, () => { if (!running && !used) go.click(); });
});
Questions
What people ask about it.
- Does the component pause the agent when it reaches the cap?
- No. It shows the cap and whether it is on. Your code compares
usedwith the cap, stops the agent and setsstatusto 'paused'. - What should the button do?
- Whatever fits the status, through
onAction. Its label suggests the usual move, for example Stop while running or Raise cap when paused. The demo turns the cap off and runs on to the full budget when Raise cap is pressed. - Can I show money instead of credits?
- The numbers are shown as whole numbers with thousands separators and no currency sign. You can change
titleto name the unit, for example "Cents used".
More in Agent status
Show what the agent is doing, how far along it is, and what it is working with.