Agent statusAgent run timeline
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callAgent run timeline
Every model and tool step of a run on one time axis. 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-run-timelineReact
npx designme add agent-run-timeline --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 run timeline draws one agent run as a Gantt chart: every model, tool and app step is a bar on a shared time axis, in its kind's colour and with its own icon or app mark, so people can see what ran in parallel, what took longest and where a step failed and was retried. While the run is live, a playhead with the time on it sweeps across, the bars grow up to it and the step at work carries a passing light.
- When you need to see why an agent run was slow and which steps overlapped.
- When a step failed and was retried, and people should see where in the run that happened.
- When people care about cost per step as well as time.
- In a run history or debugging view, to replay a finished run or follow one live.
- Each step is a bar placed by its
startanddurationin seconds, with an optionalicon(orbrand, an app's own mark like Figma, GitHub or Slack) andcost. - A step's
kindcolours it:modelin purple,toolin teal andappin orange, in light and dark. Its bar, its icon's tint and its live time take the colour, and a key under the title adds up how long each kind has run.coloroverrides the bar's colour. - Pass
timewhile the run is live. Bars grow up to it, a playhead in the accent carries the time on the axis, the lanes it has passed take a faint wash, and the line under the title names the steps at work, shimmering. The step at work has a passing light along its bar, a bright leading edge and a soft ring that breathes round its icon. Steps that haven't started yet are dimmed. - The column at the right shows how long each step took, counting up in the step's colour while it runs.
- Leave
timeout once the run is over. The tag turns from Running to Done, the playhead stops at the end of the last step and turns green with a check, and the status line reads "Finished in Ns" (with the number of retries when a step failed) unless you setstatus. failedhatches a bar red and puts a red tag after the label, readingtagor "Failed".retryon the next step draws a dashed amber line from the end of the failed step to it once it starts.noteis added to the bar's tooltip, which also lists the step's duration and cost.- The footer counts up total time, cost, steps and retries as the playhead moves, each with an icon. Pass
statsto show your own figures once the run is done. - The axis covers
spanseconds, defaulting to the end of the last step rounded up to a multiple of 4, with labels at quarters unless you passticks. An axis label hides while the playhead's time covers it. - In Blueprint the bars are flat, the running bar is hatched and steps along, its icon's ring blinks in steps, the lanes behind the playhead are hatched and the lines are hairlines, with no glow.
- The run's state is written as text, Running or Done, and the spinner beside it is
aria-hidden. - The key is a list labelled "Time by kind of step", so the colours are never the only way to tell the kinds apart. Icons, app marks, the axis, the playhead and the retry line are
aria-hidden; each step's label and time are text. - A failed step says so in its tag, not only with its hatching and colour.
- With
prefers-reduced-motion: reduce, the passing light, the shimmer and the breathing ring stop, and the status text changes without a crossfade.
Usage
Import it and drive it from your own state.
Import
import { AgentRunTimeline } from '@/components/designme/agent-run-timeline/AgentRunTimeline';Example
The demo above as a React component, 44 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 { AgentRunTimeline, type TimelineStep } from './AgentRunTimeline';
import { useStory } from '../lib/useScript';
/* ---------- Agent run timeline: the playhead sweeps the run at 1.6x, the tests fail and run again, then the totals settle ---------- */
const STEPS: TimelineStep[] = [
{ label: 'Plan the change', icon: 'brain', kind: 'model', start: 0, duration: 1.6, cost: 0.03 },
{ label: 'Read 6 files', icon: 'file-text', kind: 'tool', start: 1.6, duration: 2.2, cost: 0.02 },
{ label: 'Search Figma', brand: 'figma', kind: 'app', start: 1.9, duration: 1.4, cost: 0.01 },
{ label: 'Edit PricingCard', icon: 'edit', kind: 'model', start: 4, duration: 2.4, cost: 0.06 },
{ label: 'Run tests', icon: 'term', kind: 'tool', start: 6.5, duration: 1.5, cost: 0.01, failed: true, tag: '2 failed', note: '2 tests failed' },
{ label: 'Run tests again', icon: 'refresh', kind: 'tool', start: 8.1, duration: 1.9, cost: 0.01, retry: true },
{ label: 'Open a PR on GitHub', brand: 'github', kind: 'app', start: 10.1, duration: 1, cost: 0.02 },
{ label: 'Post to Slack', brand: 'slack', kind: 'app', start: 11.1, duration: 0.6, cost: 0.01 }
];
const SPAN = 12, END = 11.7, PACE = 1.6;
export default function AgentRunTimelineDemo() {
const ref = useRef<HTMLDivElement>(null);
const [time, setTime] = useState<number | undefined>(0);
const [started, setStarted] = useState(false);
// the run plays at 1.6 times real speed; while a mouse rests on it the story holds, and the clock waits with it
useStory(ref, async ({ wait }) => {
await wait(500);
setStarted(true);
let t = 0, last = performance.now();
while (t < END) {
await wait(16);
const now = performance.now();
// a long gap is a hold ending, and counts as one frame
t = Math.min(END, t + ((now - last < 250 ? now - last : 16) / 1000) * PACE); last = now;
setTime(t);
}
setTime(undefined);
});
return (
<div ref={ref} style={{ width: '100%' }}>
<AgentRunTimeline title="Run 4821, update the pricing page" steps={STEPS} span={SPAN} time={time} status={started ? undefined : 'Starting'} />
</div>
);
}
Props
Everything AgentRunTimeline takes.
| Prop | Type | What it does |
|---|---|---|
titlerequired | string | Like "Run 4821, update the pricing page" |
stepsrequired | TimelineStep[] | |
time | number | Seconds into the run. Bars grow up to it and the playhead sits on it. Leave it out once the run has finished |
span | number | Seconds the axis covers. Defaults to the end of the last step, rounded up to a multiple of 4 |
ticks | number[] | Labels on the axis, in seconds. Defaults to quarters of the span |
status | string | The line under the title. Defaults to the steps running now, then "Finished in 11.4s" |
stats | TimelineStat[] | The figures in the footer once the run is done. Defaults to the total time, cost, steps and retries, which count up while it plays |
kindLabels | Partial<Record<StepKind, string>> | The names of the kinds in the key. Defaults to Model, Tools and Apps |
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.
AgentRunTimeline.tsxReact · 203 lines
'use client';
import { useEffect, useRef, useState, type CSSProperties } from 'react';
import { cx } from '../lib/cx';
import { useIsoLayoutEffect, useReducedMotion, useTween } from '../lib/hooks';
import { Icon, type IconName } from '../lib/icons';
import { Brand, type BrandName } from '../lib/brands';
import { enter, Tip } from '../lib/motion';
import '../designme.css';
import './agent-run-timeline.css';
/** What a step is: the model thinking or writing (purple), a tool like a file read or a test run (teal), or an app the agent
works in, like Figma or GitHub (orange) */
export type StepKind = 'model' | 'tool' | 'app';
export interface TimelineStep {
label: string;
icon?: IconName;
/** An app's own mark in place of the icon, like 'figma', 'github' or 'slack' */
brand?: BrandName;
/** Colours the step's bar, icon and live time, and counts it in the key under the title */
kind?: StepKind;
/** Seconds into the run when the step started */
start: number;
/** How long it took, in seconds */
duration: number;
/** Bar colour, any CSS colour. Defaults to the kind's colour, or the chart blue, var(--dm-c1), for a step with no kind */
color?: string;
/** What the step cost, in dollars */
cost?: number;
/** Hatches the bar red and puts a red tag after the label, for a step that failed */
failed?: boolean;
/** The tag on a failed step, like "2 failed". Defaults to "Failed" */
tag?: string;
/** Added to the bar's tooltip, like "2 tests failed" */
note?: string;
/** A retry of the failed step just before it: a dashed line joins the end of that step to this one once it starts */
retry?: boolean;
}
export interface TimelineStat {
/** Like "11.4s" */
value: string;
/** Like "total" */
label: string;
icon?: IconName;
}
export interface AgentRunTimelineProps {
/** Like "Run 4821, update the pricing page" */
title: string;
steps: TimelineStep[];
/** Seconds into the run. Bars grow up to it and the playhead sits on it. Leave it out once the run has finished */
time?: number;
/** Seconds the axis covers. Defaults to the end of the last step, rounded up to a multiple of 4 */
span?: number;
/** Labels on the axis, in seconds. Defaults to quarters of the span */
ticks?: number[];
/** The line under the title. Defaults to the steps running now, then "Finished in 11.4s" */
status?: string;
/** The figures in the footer once the run is done. Defaults to the total time, cost, steps and retries, which count up while it plays */
stats?: TimelineStat[];
/** The names of the kinds in the key. Defaults to Model, Tools and Apps */
kindLabels?: Partial<Record<StepKind, 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;
}
// each kind's bar colour, a chart colour (teal and orange take a little of their text slot, so the bars keep 3:1 on a light
// page), and palette slot (its icon tint and live time)
const KIND: Record<StepKind, [string, string, string]> = {
model: ['Model', 'var(--dm-c2)', 'pl'],
tool: ['Tools', 'color-mix(in srgb,var(--dm-c6) 70%,var(--dm-pt))', 'pt'],
app: ['Apps', 'color-mix(in srgb,var(--dm-c4) 75%,var(--dm-pp))', 'pp']
};
const KINDS = Object.keys(KIND) as StepKind[];
/** How many times `value` has changed since the first render, so a part can play its entrance on a change but not on mount */
function useChanges<T>(value: T) {
const [seen, setSeen] = useState({ value, n: 0 });
if (Object.is(seen.value, value)) return seen.n;
setSeen({ value, n: seen.n + 1 });
return seen.n + 1;
}
/** Like Swap, except that while `instant` is set the text changes straight away: the live status moves too fast to crossfade */
function Status({ text, instant, className }: { text: string; instant: boolean; className?: string }) {
const [shown, setShown] = useState(text);
const [out, setOut] = useState(false);
const reduce = useReducedMotion();
if (instant && shown !== text) setShown(text);
useEffect(() => {
if (instant || text === shown) return;
setOut(true);
const t = setTimeout(() => { setShown(text); setOut(false); }, reduce ? 0 : 170);
return () => clearTimeout(t);
}, [text, shown, instant, reduce]);
return <div className={cx(className, !instant && 'dm-sw', out && 'dm-out')}>{instant ? text : shown}</div>;
}
const pct = (n: number) => n * 100 + '%';
const secs = (n: number) => String(+n.toFixed(1));
type State = 'wait' | 'run' | 'done' | 'fail';
/** A run drawn as a Gantt chart: every step as a bar in its kind's colour on a shared time axis, with a playhead while it runs. */
export function AgentRunTimeline({ title, steps, time, span, ticks, status, stats, kindLabels, bare = true, className }: AgentRunTimelineProps) {
const end = Math.max(0, ...steps.map(s => s.start + s.duration));
const T = span ?? Math.max(4, Math.ceil(end / 4) * 4);
const done = time === undefined || time >= T;
const t = done ? end : Math.max(0, time);
// the bars ease after the playhead in JS: a CSS width transition restarted on every frame never gets to move
const eased = useTween(t, 250);
const changed = useChanges(done) > 0 ? 'dm-enter' : undefined;
const state = (s: TimelineStep): State => t < s.start ? 'wait' : t < s.start + s.duration && !done ? 'run' : s.failed ? 'fail' : 'done';
// the line under the title names the steps at work; between two steps it keeps the last one's names
const now = steps.filter(s => state(s) === 'run').map(s => s.label).join(', ');
const last = useRef('');
if (now) last.current = now;
const retries = steps.filter(s => s.failed && t >= s.start + s.duration).length;
const line = status ?? (done ? `Finished in ${end.toFixed(1)}s${steps.some(s => s.failed) ? `, ${retries} ${retries === 1 ? 'retry' : 'retries'}` : ''}` : now || last.current || 'Starting');
// the footer counts up with the playhead, so it is there from the start and never pops in at the end
const started = steps.filter(s => t >= s.start), hasRetries = steps.some(s => s.failed), n = started.length;
const ran = (s: TimelineStep) => Math.max(0, Math.min(s.duration, t - s.start));
const cost = started.reduce((sum, s) => sum + (s.cost ?? 0) * (ran(s) / (s.duration || 1)), 0);
const figures = (done && stats) || [
{ value: `${Math.min(t, end).toFixed(1)}s`, label: 'total', icon: 'clock' as IconName },
...(steps.some(s => s.cost !== undefined) ? [{ value: `$${cost.toFixed(2)}`, label: 'cost', icon: 'dollar' as IconName }] : []),
{ value: String(n), label: n === 1 ? 'step' : 'steps', icon: 'layers' as IconName },
...(hasRetries ? [{ value: String(retries), label: retries === 1 ? 'retry' : 'retries', icon: 'refresh' as IconName }] : [])
];
const kinds = KINDS.filter(k => steps.some(s => s.kind === k));
const per = (k: StepKind) => steps.filter(s => s.kind === k).reduce((sum, s) => sum + ran(s), 0);
// a step that finishes while you watch gives its icon a small bump
const seenRunning = useRef(new Set<number>());
steps.forEach((s, i) => { if (state(s) === 'run') seenRunning.current.add(i); });
// the time on the playhead covers the axis labels it passes
const grid = useRef<HTMLDivElement>(null);
useIsoLayoutEffect(() => {
const g = grid.current; if (!g) return;
const b = g.querySelector('.dm-art-now')?.getBoundingClientRect(); if (!b) return;
g.querySelectorAll('.dm-art-ticks span').forEach(el => { const x = el.getBoundingClientRect(); el.classList.toggle('dm-hid', x.right > b.left - 4 && x.left < b.right + 4); });
});
return (
<div data-dm="" className={cx('dm-art', !bare && 'dm-panel', done ? 'dm-fin' : 'dm-live', className)} style={bare ? { paddingInline: 0 } : undefined}>
<div className="dm-art-h">
<div className="dm-art-hd">
<div className="dm-art-t">{title}</div>
<Status className={cx('dm-art-s dm-num', done ? 'dm-muted' : 'dm-shim')} text={line} instant={!done} />
</div>
{done
? <span key="done" className={cx('dm-art-tag dm-tag dm-ok', changed)}><Icon name="check" size="xs" />Done</span>
: <span key="run" className={cx('dm-art-tag dm-tag', changed)}><span className="dm-spin" aria-hidden="true" />Running</span>}
</div>
{kinds.length > 0 && (
<div className="dm-art-key dm-num" role="list" aria-label="Time by kind of step">
{kinds.map(k => <span key={k} role="listitem" style={{ '--dm-k': KIND[k][1] } as CSSProperties}><i />{kindLabels?.[k] ?? KIND[k][0]}<b>{per(k).toFixed(1)}s</b></span>)}
</div>
)}
<div className="dm-art-grid" ref={grid} style={{ '--dm-p': (t / T).toFixed(4) } as CSSProperties}>
<div className="dm-art-axis" aria-hidden="true">
<span />
<div className="dm-art-ticks">{(ticks ?? [0, 1, 2, 3, 4].map(k => (T * k) / 4)).map(v => <span key={v} style={{ left: pct(v / T) }}>{secs(v)}s</span>)}</div>
<span />
</div>
<div className="dm-art-past" aria-hidden="true" />
<div className="dm-art-rows">
{steps.map((s, i) => {
const st = state(s), w = Math.max(0, Math.min(s.duration, eased - s.start)), prev = steps[i - 1];
const k = s.kind ? KIND[s.kind] : null;
const tip = `${s.label}: ${s.duration.toFixed(1)}s${s.cost !== undefined ? `, $${s.cost.toFixed(2)}` : ''}${s.note ? `, ${s.note}` : ''}`;
const e = enter(i * 40);
const hue = { '--dm-k': s.color ?? (k ? k[1] : 'var(--dm-c1)'), '--dm-kt': k ? `var(--dm-${k[2]})` : 'var(--dm-fg2)', '--dm-kb': k ? `var(--dm-${k[2]}-bg)` : 'var(--dm-chip)' } as CSSProperties;
return (
<div key={i} className={cx('dm-art-row', e.className, st === 'done' && seenRunning.current.has(i) && 'dm-bump')} style={{ ...e.style, ...hue }} data-k={s.kind} data-s={st}>
<span className="dm-art-lab">
<span className="dm-art-ic" aria-hidden="true">{s.brand ? <Brand name={s.brand} /> : s.icon && <Icon name={s.icon} size="xs" />}</span>
<span className="dm-t">{s.label}</span>
{st === 'fail' && <span className="dm-tag dm-bad dm-enter">{s.tag ?? 'Failed'}</span>}
</span>
<span className="dm-art-lane">
{s.retry && prev?.failed && <span className="dm-art-link" aria-hidden="true" style={{ left: pct((prev.start + prev.duration) / T), width: pct((s.start - prev.start - prev.duration) / T) }} />}
<Tip label={tip}>
<span className="dm-art-bar" style={{ left: pct(s.start / T), width: pct(w / T) }}><i /></span>
</Tip>
</span>
<span className="dm-art-d dm-num">{st === 'wait' ? '' : `${ran(s).toFixed(1)}s`}</span>
</div>
);
})}
</div>
<div className="dm-art-head" aria-hidden="true"><i /><span className="dm-art-now dm-num">{done && <Icon name="check" size="xs" />}{t.toFixed(1)}s</span></div>
</div>
<div className="dm-art-f dm-muted dm-num">
{figures.map((f, i) => <span key={i}>{f.icon && <Icon name={f.icon} size="xs" />}<span><b>{f.value}</b> {f.label}</span></span>)}
</div>
</div>
);
}
agent-run-timeline.cssReact · 93 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Agent run timeline ---------- */
/* each kind of step has a hue: --k paints its bar, --kt its text and icon, --kb the icon's tint. The model is purple, tools
are teal and apps orange, each app with its own mark. The playhead runs in the accent with the time on it, and the lanes it
has passed take a faint wash; once the run is done it stops at the finish and turns green */
.dm-art{padding:var(--dm-s-4);--dm-lab:176px;--dm-dur:44px}
.dm-art-h{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--dm-s-3)}
.dm-art-hd{min-width:0}
.dm-art-t{font-weight:500}
.dm-art-s{font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-art-s.dm-sw{display:block}
.dm-art-tag{flex:none;transition:background .3s,color .3s}
.dm-art-tag .dm-spin{width:9px;height:9px}
.dm-art.dm-live .dm-art-tag{background:var(--dm-accent-bg);color:var(--dm-accent-fg,var(--dm-fg))}
/* the key: how long each kind has run so far */
.dm-art-key{display:flex;flex-wrap:wrap;gap:2px var(--dm-s-4);margin-top:10px;font-size:12px;color:var(--dm-muted)}
.dm-art-key span{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.dm-art-key i{width:8px;height:8px;border-radius:2px;background:var(--dm-k);flex:none}
.dm-art-key b{font-weight:500;color:var(--dm-fg2)}
.dm-art-grid{position:relative;margin-top:var(--dm-s-3)}
.dm-art-axis,.dm-art-row{display:grid;grid-template-columns:var(--dm-lab) minmax(0,1fr) var(--dm-dur);align-items:center}
.dm-art-axis{height:20px;font-size:11px;color:var(--dm-muted)}
.dm-art-ticks{position:relative;height:16px}
.dm-art-ticks span{position:absolute;top:0;transform:translateX(-50%);transition:opacity .2s}
.dm-art-ticks span:first-child{transform:none}
.dm-art-ticks span:last-child{transform:translateX(-100%)}
.dm-art-ticks span.dm-hid{opacity:0}
.dm-art-rows{position:relative}
.dm-art-row{height:32px}
.dm-art-lab{display:flex;align-items:center;gap:var(--dm-s-2);min-width:0;padding-right:var(--dm-s-3);font-size:12.5px;color:var(--dm-fg2);white-space:nowrap;transition:color .3s}
.dm-art-lab .dm-t{min-width:0;overflow:hidden;text-overflow:ellipsis}
.dm-art-lab .dm-tag{flex:none}
.dm-art-ic{--dm-ring:0 0 0 0 transparent;width:22px;height:22px;border-radius:7px;display:grid;place-items:center;flex:none;background:var(--dm-kb);color:var(--dm-kt);box-shadow:var(--dm-ring);transition:opacity .3s}
.dm-art-ic .dm-brand{width:13px;height:13px}
/* an app's own mark sits on a plain tile */
.dm-art-row[data-k="app"] .dm-art-ic{--dm-ring:inset 0 0 0 1px color-mix(in srgb,var(--dm-rail) 50%,transparent);background:var(--dm-surface);color:var(--dm-fg)}
/* a step that hasn't started waits quietly; the one at work breathes a ring of its hue round its icon */
.dm-art-row[data-s="wait"] .dm-art-lab{color:var(--dm-muted)}
.dm-art-row[data-s="wait"] .dm-art-ic{opacity:.45}
.dm-art-row[data-s="run"] .dm-art-lab{color:var(--dm-fg)}
.dm-art-row[data-s="run"] .dm-art-ic{animation:dm-artbreathe 1.8s var(--dm-ease-io) infinite}
@keyframes dm-artbreathe{0%,100%{box-shadow:var(--dm-ring),0 0 0 0 color-mix(in srgb,var(--dm-k) 0%,transparent)}50%{box-shadow:var(--dm-ring),0 0 0 3px color-mix(in srgb,var(--dm-k) 24%,transparent)}}
.dm-art-row.dm-bump .dm-art-ic{animation:dm-artpop .45s var(--dm-ease)}
@keyframes dm-artpop{40%{transform:scale(1.14)}}
/* the lanes: quarter lines at half strength, a bar for each step */
.dm-art-lane{position:relative;height:100%;background:linear-gradient(90deg,color-mix(in srgb,var(--dm-line) 50%,transparent) 1px,transparent 1px) 0 0/25% 100%;box-shadow:inset -1px 0 color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-art-bar{position:absolute;top:9px;height:14px;width:0;border-radius:5px;background:linear-gradient(90deg,var(--dm-k),color-mix(in oklab,var(--dm-k) 82%,#fff));cursor:default;transition:filter .2s}
@media (hover:hover) and (pointer:fine){.dm-art-bar:hover{filter:brightness(1.08) saturate(1.1)}}
/* the bar at work carries a passing light and a bright leading edge */
.dm-art-row[data-s="run"] .dm-art-bar{background:linear-gradient(100deg,transparent 30%,rgba(255,255,255,.42) 46%,transparent 62%) 0 0/250% 100% no-repeat,var(--dm-k);animation:dm-artsheen 1.3s linear infinite}
.dm-art-bar i{display:none;position:absolute;right:-1px;top:-3px;bottom:-3px;width:2px;border-radius:1px;background:var(--dm-k);box-shadow:0 0 8px 1px color-mix(in srgb,var(--dm-k) 60%,transparent)}
.dm-art-row[data-s="run"] .dm-art-bar i{display:block}
@keyframes dm-artsheen{from{background-position:120% 0,0 0}to{background-position:-120% 0,0 0}}
.dm-art-row[data-s="fail"] .dm-art-bar{background:repeating-linear-gradient(135deg,var(--dm-c5) 0 3px,color-mix(in srgb,var(--dm-c5) 45%,var(--dm-surface)) 3px 6px)}
/* a retry hangs off the end of the step that failed */
.dm-art-link{position:absolute;top:-9px;height:25px;min-width:6px;border-left:1px dashed var(--dm-warn);border-bottom:1px dashed var(--dm-warn);pointer-events:none;display:none}
.dm-art-row:not([data-s="wait"]) .dm-art-link{display:block;animation:dm-artdraw .5s var(--dm-ease) backwards}
@keyframes dm-artdraw{from{clip-path:inset(0 100% 100% 0)}to{clip-path:inset(0)}}
/* how long each step took, counting while it runs */
.dm-art-d{font-size:12px;color:var(--dm-muted);text-align:right;white-space:nowrap}
.dm-art-row[data-s="run"] .dm-art-d{color:var(--dm-kt)}
.dm-art-row[data-s="done"] .dm-art-d{color:var(--dm-fg2)}
.dm-art-row[data-s="fail"] .dm-art-d{color:var(--dm-bad)}
/* the playhead, with the time on the axis, and the wash over what it has passed */
.dm-art-past{position:absolute;top:20px;bottom:0;left:var(--dm-lab);width:calc((100% - var(--dm-lab) - var(--dm-dur)) * var(--dm-p,0));background:linear-gradient(90deg,transparent,var(--dm-sel));pointer-events:none;opacity:0;transition:opacity .5s}
.dm-art.dm-live .dm-art-past{opacity:1}
.dm-art-head{position:absolute;top:2px;bottom:0;left:calc(var(--dm-lab) + (100% - var(--dm-lab) - var(--dm-dur)) * var(--dm-p,0));width:0;pointer-events:none;opacity:0;transition:opacity .4s}
.dm-art.dm-live .dm-art-head,.dm-art.dm-fin .dm-art-head{opacity:1}
.dm-art-head i{position:absolute;top:16px;bottom:0;left:-.75px;width:1.5px;border-radius:1px;background:var(--dm-accent-mark,var(--dm-accent));transition:background .4s}
.dm-art-now{position:absolute;top:0;left:0;transform:translateX(-50%);display:inline-flex;align-items:center;gap:3px;height:16px;padding:0 5px;border-radius:5px;background:var(--dm-accent);color:var(--dm-on-accent);font-size:11px;font-weight:500;line-height:1;white-space:nowrap;transition:background .4s,color .4s}
.dm-art-now .dm-i{width:10px;height:10px}
.dm-art.dm-fin .dm-art-head i{background:var(--dm-ok)}
.dm-art.dm-fin .dm-art-now{background:var(--dm-ok-bg);color:var(--dm-ok)}
.dm-art-f{display:flex;flex-wrap:wrap;gap:var(--dm-s-2) var(--dm-s-5);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);font-size:12.5px}
.dm-art-f>span{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.dm-art-f b{color:var(--dm-fg);font-weight:500}
@media (max-width:600px){.dm-art{--dm-lab:112px;--dm-dur:38px}.dm-art-lab{gap:6px;padding-right:var(--dm-s-2)}.dm-art-lab .dm-tag{display:none}.dm-art-f{gap:var(--dm-s-2) var(--dm-s-4)}}
:root:not([data-style="soft"]) .dm-art-ic:not(:where([data-style="soft"],[data-style="soft"] *)){--dm-ring:inset 0 0 0 1px color-mix(in srgb,var(--dm-k) 35%,transparent)}
:root:not([data-style="soft"]) .dm-art-row[data-k="app"] .dm-art-ic:not(:where([data-style="soft"],[data-style="soft"] *)){--dm-ring:inset 0 0 0 1px color-mix(in srgb,var(--dm-rail) 50%,transparent)}
:root:not([data-style="soft"]) .dm-art-row[data-s="run"] .dm-art-ic:not(:where([data-style="soft"],[data-style="soft"] *)){animation:dm-artbp 1s steps(1,end) infinite}
@keyframes dm-artbp{0%,55%{box-shadow:inset 0 0 0 1px var(--dm-k)}56%,100%{box-shadow:var(--dm-ring)}}
:root:not([data-style="soft"]) .dm-art-row.dm-bump .dm-art-ic:not(:where([data-style="soft"],[data-style="soft"] *)){animation:none}
:root:not([data-style="soft"]) .dm-art-row[data-s="fail"] .dm-art-bar:not(:where([data-style="soft"],[data-style="soft"] *)){background:repeating-linear-gradient(135deg,var(--dm-c5) 0 3px,color-mix(in srgb,var(--dm-c5) 45%,var(--dm-surface)) 3px 6px)}
:root:not([data-style="soft"]) .dm-art-bar:not(:where([data-style="soft"],[data-style="soft"] *)){background:var(--dm-k)}
:root:not([data-style="soft"]) .dm-art-row[data-s="run"] .dm-art-bar:not(:where([data-style="soft"],[data-style="soft"] *)){background:linear-gradient(135deg,var(--dm-k) 25%,color-mix(in srgb,var(--dm-k) 50%,var(--dm-surface)) 25% 50%,var(--dm-k) 50% 75%,color-mix(in srgb,var(--dm-k) 50%,var(--dm-surface)) 75%) 0 0/8px 8px;animation:dm-artmarch .6s steps(4) infinite}
@keyframes dm-artmarch{to{background-position:8px 0}}
:root:not([data-style="soft"]) .dm-art-bar i:not(:where([data-style="soft"],[data-style="soft"] *)){width:1px;right:0;box-shadow:none}
:root:not([data-style="soft"]) .dm-art-link:not(:where([data-style="soft"],[data-style="soft"] *)){border-left-style:solid;border-bottom-style:solid}
:root:not([data-style="soft"]) .dm-art-row:not([data-s="wait"]) .dm-art-link:not(:where([data-style="soft"],[data-style="soft"] *)){animation:dm-artdrawbp .3s steps(3,end) backwards}
@keyframes dm-artdrawbp{from{clip-path:inset(0 100% 100% 0)}to{clip-path:inset(0)}}
:root:not([data-style="soft"]) .dm-art-past:not(:where([data-style="soft"],[data-style="soft"] *)){background:repeating-linear-gradient(135deg,transparent 0 5px,var(--dm-grid) 5px 6px)}
:root:not([data-style="soft"]) .dm-art-head i:not(:where([data-style="soft"],[data-style="soft"] *)){width:1px;left:-.5px}
agent-run-timeline.htmlHTML · 16 lines
<div class="card wide bare" data-c="agent-run-timeline">
<div class="stage-in">
<div class="art panel">
<div class="art-h"><div class="art-hd"><div class="art-t">Run 4821, update the pricing page</div><div class="art-s muted num">Starting</div></div><span class="art-tag tag"><span class="spin" aria-hidden="true"></span>Running</span></div>
<div class="art-key num" role="list" aria-label="Time by kind of step"></div>
<div class="art-grid">
<div class="art-axis" aria-hidden="true"><span></span><div class="art-ticks"></div><span></span></div>
<div class="art-past" aria-hidden="true"></div>
<div class="art-rows"></div>
<div class="art-head" aria-hidden="true"><i></i><span class="art-now num">0.0s</span></div>
</div>
<div class="art-f muted num"></div>
</div>
</div>
</div>
agent-run-timeline.cssHTML · 92 lines
/* ---------- Agent run timeline ---------- */
/* each kind of step has a hue: --k paints its bar, --kt its text and icon, --kb the icon's tint. The model is purple, tools
are teal and apps orange, each app with its own mark. The playhead runs in the accent with the time on it, and the lanes it
has passed take a faint wash; once the run is done it stops at the finish and turns green */
.art{padding:var(--s-4);--lab:176px;--dur:44px}
.art-h{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s-3)}
.art-hd{min-width:0}
.art-t{font-weight:500}
.art-s{font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.art-s.sw{display:block}
.art-tag{flex:none;transition:background .3s,color .3s}
.art-tag .spin{width:9px;height:9px}
.art.live .art-tag{background:var(--accent-bg);color:var(--accent-fg,var(--fg))}
/* the key: how long each kind has run so far */
.art-key{display:flex;flex-wrap:wrap;gap:2px var(--s-4);margin-top:10px;font-size:12px;color:var(--muted)}
.art-key span{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.art-key i{width:8px;height:8px;border-radius:2px;background:var(--k);flex:none}
.art-key b{font-weight:500;color:var(--fg2)}
.art-grid{position:relative;margin-top:var(--s-3)}
.art-axis,.art-row{display:grid;grid-template-columns:var(--lab) minmax(0,1fr) var(--dur);align-items:center}
.art-axis{height:20px;font-size:11px;color:var(--muted)}
.art-ticks{position:relative;height:16px}
.art-ticks span{position:absolute;top:0;transform:translateX(-50%);transition:opacity .2s}
.art-ticks span:first-child{transform:none}
.art-ticks span:last-child{transform:translateX(-100%)}
.art-ticks span.hid{opacity:0}
.art-rows{position:relative}
.art-row{height:32px}
.art-lab{display:flex;align-items:center;gap:var(--s-2);min-width:0;padding-right:var(--s-3);font-size:12.5px;color:var(--fg2);white-space:nowrap;transition:color .3s}
.art-lab .t{min-width:0;overflow:hidden;text-overflow:ellipsis}
.art-lab .tag{flex:none}
.art-ic{--ring:0 0 0 0 transparent;width:22px;height:22px;border-radius:7px;display:grid;place-items:center;flex:none;background:var(--kb);color:var(--kt);box-shadow:var(--ring);transition:opacity .3s}
.art-ic .brand{width:13px;height:13px}
/* an app's own mark sits on a plain tile */
.art-row[data-k="app"] .art-ic{--ring:inset 0 0 0 1px color-mix(in srgb,var(--rail) 50%,transparent);background:var(--surface);color:var(--fg)}
/* a step that hasn't started waits quietly; the one at work breathes a ring of its hue round its icon */
.art-row[data-s="wait"] .art-lab{color:var(--muted)}
.art-row[data-s="wait"] .art-ic{opacity:.45}
.art-row[data-s="run"] .art-lab{color:var(--fg)}
.art-row[data-s="run"] .art-ic{animation:artbreathe 1.8s var(--ease-io) infinite}
@keyframes artbreathe{0%,100%{box-shadow:var(--ring),0 0 0 0 color-mix(in srgb,var(--k) 0%,transparent)}50%{box-shadow:var(--ring),0 0 0 3px color-mix(in srgb,var(--k) 24%,transparent)}}
.art-row.bump .art-ic{animation:artpop .45s var(--ease)}
@keyframes artpop{40%{transform:scale(1.14)}}
/* the lanes: quarter lines at half strength, a bar for each step */
.art-lane{position:relative;height:100%;background:linear-gradient(90deg,color-mix(in srgb,var(--line) 50%,transparent) 1px,transparent 1px) 0 0/25% 100%;box-shadow:inset -1px 0 color-mix(in srgb,var(--line) 50%,transparent)}
.art-bar{position:absolute;top:9px;height:14px;width:0;border-radius:5px;background:linear-gradient(90deg,var(--k),color-mix(in oklab,var(--k) 82%,#fff));cursor:default;transition:filter .2s}
@media (hover:hover) and (pointer:fine){.art-bar:hover{filter:brightness(1.08) saturate(1.1)}}
/* the bar at work carries a passing light and a bright leading edge */
.art-row[data-s="run"] .art-bar{background:linear-gradient(100deg,transparent 30%,rgba(255,255,255,.42) 46%,transparent 62%) 0 0/250% 100% no-repeat,var(--k);animation:artsheen 1.3s linear infinite}
.art-bar i{display:none;position:absolute;right:-1px;top:-3px;bottom:-3px;width:2px;border-radius:1px;background:var(--k);box-shadow:0 0 8px 1px color-mix(in srgb,var(--k) 60%,transparent)}
.art-row[data-s="run"] .art-bar i{display:block}
@keyframes artsheen{from{background-position:120% 0,0 0}to{background-position:-120% 0,0 0}}
.art-row[data-s="fail"] .art-bar{background:repeating-linear-gradient(135deg,var(--c5) 0 3px,color-mix(in srgb,var(--c5) 45%,var(--surface)) 3px 6px)}
/* a retry hangs off the end of the step that failed */
.art-link{position:absolute;top:-9px;height:25px;min-width:6px;border-left:1px dashed var(--warn);border-bottom:1px dashed var(--warn);pointer-events:none;display:none}
.art-row:not([data-s="wait"]) .art-link{display:block;animation:artdraw .5s var(--ease) backwards}
@keyframes artdraw{from{clip-path:inset(0 100% 100% 0)}to{clip-path:inset(0)}}
/* how long each step took, counting while it runs */
.art-d{font-size:12px;color:var(--muted);text-align:right;white-space:nowrap}
.art-row[data-s="run"] .art-d{color:var(--kt)}
.art-row[data-s="done"] .art-d{color:var(--fg2)}
.art-row[data-s="fail"] .art-d{color:var(--bad)}
/* the playhead, with the time on the axis, and the wash over what it has passed */
.art-past{position:absolute;top:20px;bottom:0;left:var(--lab);width:calc((100% - var(--lab) - var(--dur)) * var(--p,0));background:linear-gradient(90deg,transparent,var(--sel));pointer-events:none;opacity:0;transition:opacity .5s}
.art.live .art-past{opacity:1}
.art-head{position:absolute;top:2px;bottom:0;left:calc(var(--lab) + (100% - var(--lab) - var(--dur)) * var(--p,0));width:0;pointer-events:none;opacity:0;transition:opacity .4s}
.art.live .art-head,.art.fin .art-head{opacity:1}
.art-head i{position:absolute;top:16px;bottom:0;left:-.75px;width:1.5px;border-radius:1px;background:var(--accent-mark,var(--accent));transition:background .4s}
.art-now{position:absolute;top:0;left:0;transform:translateX(-50%);display:inline-flex;align-items:center;gap:3px;height:16px;padding:0 5px;border-radius:5px;background:var(--accent);color:var(--on-accent);font-size:11px;font-weight:500;line-height:1;white-space:nowrap;transition:background .4s,color .4s}
.art-now .i{width:10px;height:10px}
.art.fin .art-head i{background:var(--ok)}
.art.fin .art-now{background:var(--ok-bg);color:var(--ok)}
.art-f{display:flex;flex-wrap:wrap;gap:var(--s-2) var(--s-5);margin-top:var(--s-3);padding-top:var(--s-3);border-top:1px solid color-mix(in srgb,var(--line) 50%,transparent);font-size:12.5px}
.art-f>span{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.art-f b{color:var(--fg);font-weight:500}
@media (max-width:600px){.art{--lab:112px;--dur:38px}.art-lab{gap:6px;padding-right:var(--s-2)}.art-lab .tag{display:none}.art-f{gap:var(--s-2) var(--s-4)}}
:root:not([data-style="soft"]) .art-ic:not(:where([data-style="soft"],[data-style="soft"] *)){--ring:inset 0 0 0 1px color-mix(in srgb,var(--k) 35%,transparent)}
:root:not([data-style="soft"]) .art-row[data-k="app"] .art-ic:not(:where([data-style="soft"],[data-style="soft"] *)){--ring:inset 0 0 0 1px color-mix(in srgb,var(--rail) 50%,transparent)}
:root:not([data-style="soft"]) .art-row[data-s="run"] .art-ic:not(:where([data-style="soft"],[data-style="soft"] *)){animation:artbp 1s steps(1,end) infinite}
@keyframes artbp{0%,55%{box-shadow:inset 0 0 0 1px var(--k)}56%,100%{box-shadow:var(--ring)}}
:root:not([data-style="soft"]) .art-row.bump .art-ic:not(:where([data-style="soft"],[data-style="soft"] *)){animation:none}
:root:not([data-style="soft"]) .art-row[data-s="fail"] .art-bar:not(:where([data-style="soft"],[data-style="soft"] *)){background:repeating-linear-gradient(135deg,var(--c5) 0 3px,color-mix(in srgb,var(--c5) 45%,var(--surface)) 3px 6px)}
:root:not([data-style="soft"]) .art-bar:not(:where([data-style="soft"],[data-style="soft"] *)){background:var(--k)}
:root:not([data-style="soft"]) .art-row[data-s="run"] .art-bar:not(:where([data-style="soft"],[data-style="soft"] *)){background:linear-gradient(135deg,var(--k) 25%,color-mix(in srgb,var(--k) 50%,var(--surface)) 25% 50%,var(--k) 50% 75%,color-mix(in srgb,var(--k) 50%,var(--surface)) 75%) 0 0/8px 8px;animation:artmarch .6s steps(4) infinite}
@keyframes artmarch{to{background-position:8px 0}}
:root:not([data-style="soft"]) .art-bar i:not(:where([data-style="soft"],[data-style="soft"] *)){width:1px;right:0;box-shadow:none}
:root:not([data-style="soft"]) .art-link:not(:where([data-style="soft"],[data-style="soft"] *)){border-left-style:solid;border-bottom-style:solid}
:root:not([data-style="soft"]) .art-row:not([data-s="wait"]) .art-link:not(:where([data-style="soft"],[data-style="soft"] *)){animation:artdrawbp .3s steps(3,end) backwards}
@keyframes artdrawbp{from{clip-path:inset(0 100% 100% 0)}to{clip-path:inset(0)}}
:root:not([data-style="soft"]) .art-past:not(:where([data-style="soft"],[data-style="soft"] *)){background:repeating-linear-gradient(135deg,transparent 0 5px,var(--grid) 5px 6px)}
:root:not([data-style="soft"]) .art-head i:not(:where([data-style="soft"],[data-style="soft"] *)){width:1px;left:-.5px}
agent-run-timeline.jsHTML · 75 lines
DM.def('agent-run-timeline', (r, c) => {
// each kind of step has a hue (a chart colour for its bar, the palette slot for its text and icon tint) and a name in the key.
// Teal and orange take a little of their text slot, so the bars keep 3:1 on a light stage
const K = { model:['Model', 'var(--c2)', 'pl'], tool:['Tools', 'color-mix(in srgb,var(--c6) 70%,var(--pt))', 'pt'], app:['Apps', 'color-mix(in srgb,var(--c4) 75%,var(--pp))', 'pp'] };
// an icon starting with @ is an app's own mark
const S = [
{ l:'Plan the change', ic:'brain', k:'model', s:0, d:1.6, cost:.03 },
{ l:'Read 6 files', ic:'file-text', k:'tool', s:1.6, d:2.2, cost:.02 },
{ l:'Search Figma', ic:'@figma', k:'app', s:1.9, d:1.4, cost:.01 },
{ l:'Edit PricingCard', ic:'edit', k:'model', s:4, d:2.4, cost:.06 },
{ l:'Run tests', ic:'term', k:'tool', s:6.5, d:1.5, cost:.01, fail:'2 failed' },
{ l:'Run tests again', ic:'refresh', k:'tool', s:8.1, d:1.9, cost:.01, retry:true },
{ l:'Open a PR on GitHub', ic:'@github', k:'app', s:10.1, d:1, cost:.02 },
{ l:'Post to Slack', ic:'@slack', k:'app', s:11.1, d:.6, cost:.01 }
];
const T = 12, END = Math.max(...S.map(s => s.s + s.d)), PACE = 1.6, TK = [0, 3, 6, 9, 12];
const art = $('.art', r), grid = $('.art-grid', r), rows = $('.art-rows', r), now = $('.art-now', r), st = $('.art-s', r), foot = $('.art-f', r);
$('.art-ticks', r).innerHTML = TK.map(t => `<span style="left:${t / T * 100}%">${t}s</span>`).join('');
const ticks = $$('.art-ticks span', r);
$('.art-key', r).innerHTML = Object.entries(K).map(([id, k]) => `<span role="listitem" style="--k:${k[1]}"><i></i>${k[0]}<b data-k="${id}">0.0s</b></span>`).join('');
const kb = Object.fromEntries($$('.art-key b', r).map(b => [b.dataset.k, b]));
const pct = v => v / T * 100 + '%';
S.forEach((s, i) => {
const k = K[s.k], prev = S[i - 1];
const link = s.retry && prev ? `<span class="art-link" aria-hidden="true" style="left:${pct(prev.s + prev.d)};width:${pct(s.s - prev.s - prev.d)}"></span>` : '';
s.el = html(`<div class="art-row" data-k="${s.k}" data-s="wait" style="--k:${k[1]};--kt:var(--${k[2]});--kb:var(--${k[2]}-bg)"><span class="art-lab"><span class="art-ic" aria-hidden="true">${s.ic[0] === '@' ? BRAND[s.ic.slice(1)] : I(s.ic, 'xs')}</span><span class="t">${s.l}</span></span><span class="art-lane">${link}<span class="art-bar" style="left:${pct(s.s)}" data-tip="${s.l}: ${s.d.toFixed(1)}s, $${s.cost.toFixed(2)}${s.fail ? ', 2 tests failed' : ''}"><i></i></span></span><span class="art-d num"></span></div>`);
s.bar = $('.art-bar', s.el); s.dur = $('.art-d', s.el); s.st = 'wait';
rows.appendChild(s.el);
});
stagger($$('.art-row', r), 40);
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
let line = '', ft = '';
// the run as it stands t seconds in: bars, states, times, the key, the footer and the line under the title
function draw(t){
grid.style.setProperty('--p', (t / T).toFixed(4));
let cost = 0, n = 0, re = 0; const per = { model:0, tool:0, app:0 }, live = [];
S.forEach(s => {
const w = Math.max(0, Math.min(s.d, t - s.s)), was = s.st;
s.st = t < s.s ? 'wait' : t < s.s + s.d ? 'run' : s.fail ? 'fail' : 'done';
s.bar.style.width = pct(w);
s.dur.textContent = t >= s.s ? w.toFixed(1) + 's' : '';
if (s.st !== was){
s.el.dataset.s = s.st;
if (s.st === 'fail'){ const tg = html(`<span class="tag bad">${s.fail}</span>`); $('.art-lab', s.el).appendChild(tg); enter(tg); }
if (s.st === 'done'){ s.el.classList.remove('bump'); void s.el.offsetWidth; s.el.classList.add('bump'); }
}
if (t >= s.s){ n++; cost += s.cost * w / s.d; per[s.k] += w; }
if (s.st === 'fail') re++;
if (s.st === 'run') live.push(s.l);
});
for (const k in per) kb[k].textContent = per[k].toFixed(1) + 's';
const f = `<span>${I('clock','xs')}<span><b>${Math.min(t, END).toFixed(1)}s</b> total</span></span><span>${I('dollar','xs')}<span><b>$${cost.toFixed(2)}</b> cost</span></span><span>${I('layers','xs')}<span><b>${n}</b> ${n === 1 ? 'step' : 'steps'}</span></span><span>${I('refresh','xs')}<span><b>${re}</b> ${re === 1 ? 'retry' : 'retries'}</span></span>`;
if (f !== ft){ foot.innerHTML = f; ft = f; }
// between two steps the line keeps the last one's name, so it never flickers
if (live.length && live.join(', ') !== line){ line = live.join(', '); st.textContent = line; }
now.textContent = t.toFixed(1) + 's';
// the time on the playhead covers the axis labels it passes
const b = now.getBoundingClientRect();
ticks.forEach(el => { const x = el.getBoundingClientRect(); el.classList.toggle('hid', x.right > b.left - 4 && x.left < b.right + 4); });
}
draw(0);
c.run(async () => {
await c.wait(500);
art.classList.add('live'); st.classList.remove('muted'); st.classList.add('shim');
// time moves on only while the demo isn't held, so a mouse resting on it stops the playhead where it is: a long gap
// between two frames is a hold (or the card being away) ending, and counts as one frame
await new Promise(res => { let t = 0, last = 0; c.loop(ts => { const dt = last && ts - last < 250 ? ts - last : 16; last = ts; t = Math.min(END, t + dt / 1000 * PACE); draw(t); if (t >= END){ res(); return false; } }); });
art.classList.remove('live'); art.classList.add('fin');
st.classList.remove('shim'); st.classList.add('muted');
now.innerHTML = I('check','xs') + END.toFixed(1) + 's';
const tag = $('.art-tag', r); tag.className = 'art-tag tag ok'; tag.innerHTML = I('check','xs') + 'Done'; enter(tag);
swap(st, `Finished in ${END.toFixed(1)}s, ${plural(S.filter(s => s.fail).length, 'retry', 'retries')}`);
});
});
Questions
What people ask about it.
- How do I follow a run live?
- Keep the steps you know about in
stepsand updatetimewith the seconds since the run started, for example on each animation frame. When the run ends, leavetimeout. - How do I show a failed step and its retry?
- Mark the failed step with
failed: true, a shorttaglike "2 failed" and an optionalnotefor its tooltip, then add the retry as its own step after it withretry: true. The footer counts the failed steps as retries. - Which app marks can a step show?
- Set
brandto one of the marks the components share: google, github, figma, slack, notion, linear, gmail or drive. For anything else, useicon. - Can I replay a finished run?
- Yes. Pass the full
stepslist and movetimefrom 0 to the end of the run. The demo plays it back at 1.6 times real speed, and holds while a mouse rests on it.
More in Agent status
Show what the agent is doing, how far along it is, and what it is working with.