Agent statusAgent lanes
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callAgent lanes
Several agents working in parallel, each with its own lane and hand-offs. 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-lanesReact
npx designme add agent-lanes --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 lanes shows several agents working on one job, each in its own row and its own colour, with a name, what it is doing right now, a step track and a status tag. A relay line joins the agents' badges. When one agent passes work to another, a light runs down that line to the next badge, a line like "Researcher handed the brief to Designer" opens above the lane that takes it, and the stretch of line it crossed stays lit.
- When your product runs several agents with different roles, like a researcher, a designer and a reviewer, on one task.
- When some agents wait on others and people need to see who is blocked and why.
- When people should be able to pause every agent at once and resume them later.
- When each agent's own log should be one click away without crowding the overview.
- Each lane has a
tone(blue, purple, green, orange, red, yellow, pink or teal) that colours its badge, its track, its Working tag and its stretch of the relay line, in light and dark. - Each lane has a
status: queued, waiting, working, paused or done. A working lane breathes a soft ring round its badge, shimmers its activity and carries a wash of its colour; paused holds; done fills the badge with its colour. - The
activityline crossfades when it changes. The track is cut intosteps(3 by default). The step afterdonefills asprogressgoes from 0 to 1, with a light running along it; withoutprogressit fills part way. - Setting
handoffto{ from, to, what }opens a line above the receiving lane and sends a light down the relay line from the sender's badge to the receiver's, which rings once. Set it back to undefined and the line slides away; the stretch the light crossed stays lit. - The header reads "3 agents working", "All agents paused" or "All 3 agents finished" unless you pass
title, with a m:ss clock that counts while agents work and, wider than a phone, how many steps of all the lanes are done. - Clicking a lane opens its
log, with an optional time for each entry. Open lanes can be controlled withexpandedandonExpandedChange. onPausedChangeadds a Pause all / Resume button that turns working lanes into Paused and stops the clock. It disappears once every lane is done.
- Each lane row is a button with
aria-expandedfor its log. - Closed logs and a hand-off line that is leaving are
inertandaria-hidden. - The track, the relay line, the light and the working spinner are
aria-hidden: each status is written out as text, like Working, Paused or Done, and the header counts the steps. - With reduced motion the light doesn't run: the stretch of line lights at once, and the breathing and shimmer stop.
Usage
Import it and drive it from your own state.
Import
import { AgentLanes } from '@/components/designme/agent-lanes/AgentLanes';Example
The demo above as a React component, 63 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 { AgentLanes, type AgentHandoff, type AgentLane, type LaneTone } from './AgentLanes';
import type { IconName } from '../lib/icons';
import { useScript } from '../lib/useScript';
/* ---------- Agent lanes: three agents work in turn, each step filling as it goes, and hand the work along; Pause all holds them ---------- */
const PLAN: { key: string; name: string; icon: IconName; tone: LaneTone; wait?: string; acts: string[]; log: string[] }[] = [
{ key: 'researcher', name: 'Researcher', icon: 'search', tone: 'blue', acts: ['Reading competitor sites', 'Comparing 5 pricing pages', 'Writing the brief'], log: ['Read stripe.com/pricing', 'Read linear.app/pricing', 'Noted 3 patterns'] },
{ key: 'designer', name: 'Designer', icon: 'palette', tone: 'purple', wait: 'Waiting for the brief', acts: ['Laying out pricing cards', 'Setting type and spacing', 'Exporting frames'], log: ['Made 3 card variants', 'Applied spacing tokens', 'Exported 4 frames'] },
{ key: 'reviewer', name: 'Reviewer', icon: 'eye', tone: 'green', wait: 'Waiting for the designer', acts: ['Checking contrast', 'Checking copy', 'Signing off'], log: ['Contrast passes AA', 'Fixed 2 lines of copy', 'Approved'] }
];
export default function AgentLanesDemo() {
const [lanes, setLanes] = useState<AgentLane[]>(() => PLAN.map(p => ({ key: p.key, name: p.name, icon: p.icon, tone: p.tone, status: 'queued', activity: p.wait ?? 'Queued', done: 0, log: [] })));
const [handoff, setHandoff] = useState<AgentHandoff>();
const [paused, setPaused] = useState(false);
const [secs, setSecs] = useState(0);
const [finished, setFinished] = useState(false);
// the script reads these while it runs
const live = useRef({ paused, secs });
useEffect(() => { live.current = { paused, secs }; });
// the clock ticks on a steady beat and skips the beats that fall in a pause
useEffect(() => {
if (finished) return;
const id = setInterval(() => { if (!live.current.paused) setSecs(s => s + 1); }, 1000);
return () => clearInterval(id);
}, [finished]);
useScript(async ({ wait }) => {
const set = (key: string, patch: Partial<AgentLane> | ((l: AgentLane) => Partial<AgentLane>)) =>
setLanes(ls => ls.map(l => l.key === key ? { ...l, ...(typeof patch === 'function' ? patch(l) : patch) } : l));
// work only moves on while the agents are not paused
const hold = async (ms: number, each?: (p: number) => void) => { for (let t = 0; t < ms;) { await wait(100); if (!live.current.paused) { t += 100; each?.(t / ms); } } };
const work = async (p: (typeof PLAN)[number]) => {
for (let i = 0; i < 3; i++) {
set(p.key, { status: 'working', activity: p.acts[i], done: i, progress: 0 });
await hold(1500, f => set(p.key, { progress: f }));
const time = live.current.secs + 's';
set(p.key, l => ({ done: i + 1, log: [...(l.log ?? []), { text: p.log[i], time }] }));
}
set(p.key, { status: 'done', activity: p.log[2] });
};
// the line opens, the light runs down to the next badge (about 1.5s), then the line closes
const hand = async (from: string, to: string, what: string) => { setHandoff({ from, to, what }); await wait(2750); setHandoff(undefined); await wait(700); };
await wait(600);
const first = work(PLAN[0]); set('designer', { status: 'waiting' });
await first; await hand('researcher', 'designer', 'the brief');
set('reviewer', { status: 'waiting' });
await work(PLAN[1]); await hand('designer', 'reviewer', '4 frames');
await work(PLAN[2]);
setFinished(true);
});
return (
<div style={{ width: '100%' }}>
<AgentLanes lanes={lanes} handoff={handoff} seconds={secs} paused={paused} onPausedChange={setPaused} />
</div>
);
}
Props
Everything AgentLanes takes.
| Prop | Type | What it does |
|---|---|---|
lanesrequired | AgentLane[] | |
title | string | Header text. Defaults to "3 agents working", "All agents paused" or "All 3 agents finished" |
seconds | number | Seconds to show as m:ss. Leave it out to let the header count while agents work |
paused | boolean | Whether every agent is paused. Working lanes then show Paused and the clock stops |
defaultPaused | boolean | |
onPausedChange | (paused: boolean) => void | Adds the Pause all / Resume button, until every lane is done |
handoff | AgentHandoff | The handoff to show. Set it back to undefined and its line slides away; the stretch of relay line it crossed stays lit |
expanded | string[] | Keys of the lanes that are open. Leave it out to let each row toggle itself |
defaultExpanded | string[] | |
onExpandedChange | (keys: string[]) => void | |
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.
AgentLanes.tsxReact · 209 lines
'use client';
import { Fragment, useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
import { cx } from '../lib/cx';
import { useControllable, useReducedMotion, useSeconds } from '../lib/hooks';
import { Icon, type IconName } from '../lib/icons';
import { enter, Fold, Swap } from '../lib/motion';
import '../designme.css';
import './agent-lanes.css';
export type LaneStatus = 'queued' | 'waiting' | 'working' | 'paused' | 'done';
export type LaneTone = 'blue' | 'purple' | 'green' | 'orange' | 'red' | 'yellow' | 'pink' | 'teal';
export interface AgentLane {
key: string;
/** Like "Researcher" */
name: string;
icon: IconName;
/** The lane's hue: its badge, its track, its Working tag and its stretch of the relay line. Defaults to blue */
tone?: LaneTone;
/** working breathes, shimmers and fills the next step, paused holds it, done fills the badge with its hue */
status: LaneStatus;
/** What the agent is doing now, or what it is waiting for. It crossfades when it changes, and shimmers while the agent works */
activity: string;
/** How many steps the track is cut into. Defaults to 3 */
steps?: number;
/** How many steps are finished. The next one fills while the agent works */
done?: number;
/** How far the agent is through the step in hand, from 0 to 1. Leave it out and that step fills part way */
progress?: number;
/** What the agent did, shown when the lane is opened. New entries slide in */
log?: { text: string; time?: string }[];
}
/** One agent passing work to another: a light runs down the relay line between their badges, with a line saying what changed hands */
export interface AgentHandoff {
/** Key of the lane handing over */
from: string;
/** Key of the lane taking it */
to: string;
/** What changes hands, like "the brief" */
what: string;
}
export interface AgentLanesProps {
lanes: AgentLane[];
/** Header text. Defaults to "3 agents working", "All agents paused" or "All 3 agents finished" */
title?: string;
/** Seconds to show as m:ss. Leave it out to let the header count while agents work */
seconds?: number;
/** Whether every agent is paused. Working lanes then show Paused and the clock stops */
paused?: boolean;
defaultPaused?: boolean;
/** Adds the Pause all / Resume button, until every lane is done */
onPausedChange?: (paused: boolean) => void;
/** The handoff to show. Set it back to undefined and its line slides away; the stretch of relay line it crossed stays lit */
handoff?: AgentHandoff;
/** Keys of the lanes that are open. Leave it out to let each row toggle itself */
expanded?: string[];
defaultExpanded?: string[];
onExpandedChange?: (keys: string[]) => 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;
}
const TONE: Record<LaneTone, string> = { blue: 'pb', purple: 'pl', green: 'pm', orange: 'pp', red: 'pr', yellow: 'py', pink: 'pk', teal: 'pt' };
const hue = (l?: AgentLane) => TONE[l?.tone ?? 'blue'];
/** 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;
}
/** Keeps the last value on screen for `ms` after it is cleared, so it can play its exit */
function usePresence<T>(value: T | undefined, ms = 700) {
const [kept, setKept] = useState(value);
if (value !== undefined && value !== kept) setKept(value);
useEffect(() => {
if (value !== undefined || kept === undefined) return;
const t = setTimeout(() => setKept(undefined), ms);
return () => clearTimeout(t);
}, [value, kept, ms]);
return { shown: value ?? kept, leaving: value === undefined && kept !== undefined };
}
/** The hand-off line: it mounts shut and opens on the next frame, so the lanes below glide down, then closes before it goes */
function Handoff({ leaving, lit, style, children }: { leaving: boolean; lit: boolean; style: CSSProperties; children: ReactNode }) {
const [open, setOpen] = useState(false);
useEffect(() => { let b = 0; const a = requestAnimationFrame(() => { b = requestAnimationFrame(() => setOpen(true)); }); return () => { cancelAnimationFrame(a); cancelAnimationFrame(b); }; }, []);
const e = enter(80);
return (
<div className={cx('dm-fold dm-aln-hf', !(open && !leaving) && 'dm-shut', lit && 'dm-lit')} style={style} inert={!(open && !leaving)} aria-hidden={!(open && !leaving)}>
<div><div className={cx('dm-aln-hand', e.className)} style={e.style}>{children}</div></div>
</div>
);
}
function StatusTag({ status }: { status: LaneStatus }) {
const moved = useChanges(status) > 0 ? 'dm-enter' : undefined;
if (status === 'working') return <span key="working" className={cx('dm-tag dm-aln-tg', moved)}><span className="dm-spin" aria-hidden="true" />Working</span>;
if (status === 'paused') return <span key="paused" className={cx('dm-tag dm-warn', moved)}>Paused</span>;
if (status === 'done') return <span key="done" className={cx('dm-tag dm-ok', moved)}><Icon name="check" size="xs" />Done</span>;
if (status === 'waiting') return <span key="waiting" className={cx('dm-tag', moved)}><Icon name="clock" size="xs" />Waiting</span>;
return <span key="queued" className={cx('dm-tag', moved)}>Queued</span>;
}
function Lane({ lane, paused, open, sent, got, ping, onToggle }: { lane: AgentLane; paused: boolean; open: boolean; sent: boolean; got: boolean; ping: boolean; onToggle: () => void }) {
const status = paused && lane.status === 'working' ? 'paused' : lane.status;
const tone = hue(lane), steps = lane.steps ?? 3, done = lane.done ?? 0;
const live = status === 'working' || status === 'paused';
return (
<div className={cx('dm-aln-lane', sent && 'dm-sent', got && 'dm-got')} data-s={status} data-lane={lane.key} style={{ '--dm-c': `var(--dm-${tone})`, '--dm-cb': `var(--dm-${tone}-bg)`, '--dm-cm': `var(--dm-aln-${tone},var(--dm-${tone}))` } as CSSProperties}>
<button type="button" className="dm-aln-row" aria-expanded={open} onClick={onToggle}>
<span className={cx('dm-aln-av', ping && 'dm-ping')}>
<Icon name={lane.icon} />
</span>
<span className="dm-aln-who"><span className="dm-aln-n">{lane.name}</span><Swap className={cx('dm-aln-a', live && 'dm-live', status === 'working' && 'dm-shim')}>{lane.activity}</Swap></span>
<span className="dm-aln-tr" aria-hidden="true">
{Array.from({ length: steps }, (_, i) => {
const run = i === done && live;
return <i key={i} className={cx(i < done && 'dm-on', run && 'dm-run', run && lane.progress === undefined && 'dm-ind')}
style={run && lane.progress !== undefined ? { '--dm-p': Math.max(0, Math.min(1, lane.progress)) } as CSSProperties : undefined} />;
})}
</span>
<span className="dm-aln-st"><StatusTag status={status} /></span>
</button>
<Fold open={open}>
<div className="dm-aln-det">
{lane.log?.map((l, i) => <div key={i} className="dm-enter"><span>{l.text}</span>{l.time && <span className="dm-muted">{l.time}</span>}</div>)}
</div>
</Fold>
</div>
);
}
const clock = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
/** Several agents working side by side, each in its own lane and hue, handing work to one another down a relay line. */
export function AgentLanes({ lanes, title, seconds, paused, defaultPaused = false, onPausedChange, handoff, expanded, defaultExpanded = [], onExpandedChange, bare = true, className }: AgentLanesProps) {
const [isPaused, setPaused] = useControllable(paused, defaultPaused, onPausedChange);
const [open, setOpen] = useControllable(expanded, defaultExpanded, onExpandedChange);
const finished = lanes.length > 0 && lanes.every(l => l.status === 'done');
const counted = useSeconds(seconds === undefined && !isPaused && !finished);
const { shown: hand, leaving } = usePresence(handoff);
const reduce = useReducedMotion();
const rows = useRef<HTMLDivElement>(null);
// the hand-offs whose light has arrived: their stretch of the relay line stays lit
const [relayed, setRelayed] = useState<string[]>([]);
const [ping, setPing] = useState<string>();
const handKey = hand ? `${hand.from}>${hand.to}` : '';
useEffect(() => {
if (!handoff) return;
const { from, to } = handoff, key = `${from}>${to}`, box = rows.current;
let an: Animation | undefined, pk: HTMLSpanElement | undefined, t2 = 0;
const arrive = () => { pk?.remove(); setRelayed(r => r.includes(key) ? r : [...r, key]); setPing(to); t2 = window.setTimeout(() => setPing(p => p === to ? undefined : p), 950); };
// the line opens first, then the light runs from the sender's badge down to the taker's
const t1 = window.setTimeout(() => {
const a = box?.querySelector(`[data-lane="${CSS.escape(from)}"] .dm-aln-av`), b = box?.querySelector(`[data-lane="${CSS.escape(to)}"] .dm-aln-av`);
if (!box || !a || !b || reduce) return arrive();
const rr = box.getBoundingClientRect(), y0 = a.getBoundingClientRect().bottom - rr.top + 4, y1 = b.getBoundingClientRect().top - rr.top - 4;
const f = lanes.find(l => l.key === from), g = lanes.find(l => l.key === to);
pk = document.createElement('span'); pk.className = 'dm-aln-pk'; pk.setAttribute('aria-hidden', 'true');
pk.style.setProperty('--dm-from', `var(--dm-${hue(f)})`); pk.style.setProperty('--dm-to', `var(--dm-${hue(g)})`);
box.appendChild(pk);
an = pk.animate([{ transform: `translateY(${y0}px)`, opacity: 0 }, { opacity: 1, offset: .12 }, { opacity: 1, offset: .88 }, { transform: `translateY(${y1}px)`, opacity: 0 }], { duration: 950, easing: 'cubic-bezier(.65,0,.35,1)' });
an.finished.then(arrive, () => {});
}, reduce ? 0 : 500);
return () => { clearTimeout(t1); clearTimeout(t2); an?.cancel(); pk?.remove(); };
}, [handoff?.from, handoff?.to]); // eslint-disable-line react-hooks/exhaustive-deps
const nameOf = (key: string) => lanes.find(l => l.key === key)?.name ?? key;
const toneOf = (key: string) => hue(lanes.find(l => l.key === key));
const head = title ?? (finished ? `All ${lanes.length} agents finished` : isPaused ? 'All agents paused' : `${lanes.length} agents working`);
const total = lanes.reduce((n, l) => n + (l.steps ?? 3), 0), stepsDone = lanes.reduce((n, l) => n + Math.min(l.done ?? 0, l.steps ?? 3), 0);
return (
<div data-dm="" className={cx('dm-aln', !bare && 'dm-panel', finished && 'dm-fin', className)} style={bare ? { paddingInline: 0 } : undefined}>
<div className="dm-aln-h">
<span className="dm-aln-ic">{finished ? <span key="ok" {...enter()} style={{ display: 'grid' }}><Icon name="ok" /></span> : <Icon name="users" />}</span>
<Swap className="dm-aln-t">{head}</Swap>
<span className="dm-aln-m dm-muted dm-num">{clock(seconds ?? counted)}</span>
<Swap className="dm-aln-ct dm-num">{`${stepsDone} of ${total} steps`}</Swap>
{onPausedChange && !finished && (
<button type="button" className="dm-pill dm-soft dm-sm" onClick={() => setPaused(!isPaused)}>
<Icon name={isPaused ? 'play' : 'pause'} size="xs" />{isPaused ? 'Resume' : 'Pause all'}
</button>
)}
</div>
<div className="dm-aln-rows" ref={rows}>
{lanes.map(l => (
<Fragment key={l.key}>
{hand?.to === l.key && (
<Handoff key={handKey} leaving={leaving} lit={relayed.includes(handKey)} style={{ '--dm-from': `var(--dm-${toneOf(hand.from)})`, '--dm-to': `var(--dm-${toneOf(hand.to)})` } as CSSProperties}>
<b className="dm-f">{nameOf(hand.from)}</b> handed {hand.what} to <b className="dm-t">{nameOf(hand.to)}</b>
</Handoff>
)}
<Lane lane={l} paused={isPaused} open={open.includes(l.key)} ping={ping === l.key}
sent={relayed.some(k => k.startsWith(l.key + '>'))} got={relayed.some(k => k.endsWith('>' + l.key))}
onToggle={() => setOpen(open.includes(l.key) ? open.filter(k => k !== l.key) : [...open, l.key])} />
</Fragment>
))}
</div>
</div>
);
}
agent-lanes.cssReact · 95 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Agent lanes ---------- */
/* each agent has its own hue (--c, with its tint --cb): its badge, its track, its tag and its stretch of the relay line that
joins the badges. A hand-off sends a light down that line from one agent to the next, and the stretch it crossed stays lit */
.dm-aln{padding:var(--dm-s-2)}
/* the lanes' marks (tracks, lines, a finished badge) in livelier versions of the agents' hues, each 3:1 on the surface; their
text keeps the agent's text colour. Dark mode's agent colours are light already, so they are used as they are */
.dm-aln{--dm-aln-pb:#2a7bff;--dm-aln-pm:#1fa66a;--dm-aln-pl:#7a5af5}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .dm-aln{--dm-aln-pb:var(--dm-pb);--dm-aln-pm:var(--dm-pm);--dm-aln-pl:var(--dm-pl)}}
:root[data-theme="dark"] .dm-aln{--dm-aln-pb:var(--dm-pb);--dm-aln-pm:var(--dm-pm);--dm-aln-pl:var(--dm-pl)}
/* on a card the lanes sit centred in the room kept for their tallest moment, and glide as a hand-off line opens and shuts */
.dm-stage-in:has(>.dm-aln){display:flex;flex-direction:column;justify-content:center}
.dm-aln-h{display:flex;align-items:center;gap:10px;padding:var(--dm-s-2) 10px 10px}
.dm-aln-ic{color:var(--dm-fg2);display:grid;transition:color .3s}
.dm-aln.dm-fin .dm-aln-ic{color:var(--dm-ok)}
.dm-aln-t{font-weight:500}
.dm-aln-m{font-size:12.5px}
.dm-aln-ct{font-size:12.5px;color:var(--dm-muted);margin-right:auto;white-space:nowrap}
.dm-aln-ct::before{content:"·";margin-right:6px}
.dm-aln-rows{position:relative}
.dm-aln-rows>*{position:relative}
.dm-aln-lane{border-radius:14px;transition:background .3s}
@media (hover:hover) and (pointer:fine){.dm-aln-lane:hover{background:var(--dm-row)}}
/* the lane at work carries a wash of its hue from the left */
.dm-aln-lane[data-s="working"]{background:linear-gradient(90deg,color-mix(in srgb,var(--dm-c) 8%,transparent),transparent 72%)}
@media (hover:hover) and (pointer:fine){.dm-aln-lane[data-s="working"]:hover{background:linear-gradient(90deg,color-mix(in srgb,var(--dm-c) 8%,transparent),transparent 72%),var(--dm-row)}}
/* the relay line runs from badge to badge, through the lanes and the hand-offs between them, and stops short of each badge */
.dm-aln-rows>*::before{content:"";position:absolute;left:26px;width:2px;top:0;bottom:0;border-radius:1px;background:var(--dm-line);transition:background .5s var(--dm-ease);pointer-events:none}
.dm-aln-lane::before{top:48px}
.dm-aln-lane:last-child::before{display:none}
.dm-aln-lane::after{content:"";position:absolute;left:26px;width:2px;top:0;height:6px;border-radius:1px;background:var(--dm-line);transition:background .5s var(--dm-ease);pointer-events:none}
.dm-aln-lane:first-child::after{display:none}
.dm-aln-lane.dm-sent::before{background:linear-gradient(var(--dm-cm),color-mix(in srgb,var(--dm-cm) 45%,var(--dm-line)))}
.dm-aln-lane.dm-got::after{background:var(--dm-cm)}
.dm-aln-hf.dm-lit::before{background:linear-gradient(color-mix(in srgb,var(--dm-from) 45%,var(--dm-line)),var(--dm-to))}
.dm-aln-row{display:grid;grid-template-columns:34px minmax(0,1.2fr) minmax(0,1fr) auto;align-items:center;gap:var(--dm-s-3);width:100%;padding:10px;text-align:left}
.dm-aln-av{position:relative;width:34px;height:34px;border-radius:11px;display:grid;place-items:center;color:var(--dm-c);background:var(--dm-cb);transition:color .4s,background .4s}
.dm-aln-av::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none}
.dm-aln-lane[data-s="working"] .dm-aln-av{animation:dm-alnbreathe 2.4s var(--dm-ease-io) infinite}
@keyframes dm-alnbreathe{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--dm-c) 0%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--dm-c) 18%,transparent)}}
/* the badge that takes a hand-off rings once as the light reaches it */
.dm-aln-av.dm-ping::before{animation:dm-alnping .9s var(--dm-ease)}
@keyframes dm-alnping{from{box-shadow:0 0 0 0 color-mix(in srgb,var(--dm-c) 50%,transparent)}to{box-shadow:0 0 0 12px color-mix(in srgb,var(--dm-c) 0%,transparent)}}
/* a finished agent's badge fills with its hue */
.dm-aln-lane[data-s="done"] .dm-aln-av{background:var(--dm-cm);color:var(--dm-surface)}
.dm-aln-who{min-width:0}
.dm-aln-n{display:block;font-weight:500;font-size:13.5px}
.dm-aln-a{display:block;font-size:12.5px;color:var(--dm-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-aln-a.dm-live{color:var(--dm-fg2)}
/* the track: one part a step. Finished parts are solid, the one in hand fills as the agent works, with a light running along it */
.dm-aln-tr{display:flex;gap:3px}
.dm-aln-tr i{flex:1;height:10px;border-radius:5px;background:color-mix(in srgb,var(--dm-cm) 13%,var(--dm-line));position:relative;overflow:hidden}
.dm-aln-tr i::after{content:"";position:absolute;inset:0;border-radius:inherit;background:var(--dm-cm);transform:scaleX(var(--dm-p,0));transform-origin:left;transition:transform .25s linear}
.dm-aln-tr i.dm-on::after{transform:scaleX(1);transition:transform .5s var(--dm-ease)}
.dm-aln-tr i.dm-run::after{background:linear-gradient(90deg,var(--dm-cm) 0 30%,color-mix(in srgb,var(--dm-cm) 30%,var(--dm-surface)) 50%,var(--dm-cm) 70% 100%) 0 0/250% 100%;animation:dm-alnsheen 1.6s linear infinite}
/* given no step progress, the part in hand fills part way and keeps its light */
.dm-aln-tr i.dm-run.dm-ind::after{transform:scaleX(.6);transition:transform 2.4s var(--dm-ease)}
@keyframes dm-alnsheen{from{background-position:100% 0}to{background-position:-150% 0}}
.dm-aln-st{min-width:92px;display:flex;justify-content:flex-end}
.dm-aln-tg{background:var(--dm-cb);color:var(--dm-c)}
.dm-aln-tg .dm-spin{width:9px;height:9px}
.dm-aln-det{padding:0 10px 10px 56px;font-size:12.5px;color:var(--dm-fg2)}
.dm-aln-det div{display:flex;align-items:center;gap:var(--dm-s-2);padding:3px 0}
.dm-aln-det div::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--dm-c);flex:none}
.dm-aln-det .dm-muted{margin-left:auto}
/* a hand-off opens a line between the lanes, glides the lanes below down and closes again */
.dm-aln-hf{transition-duration:var(--dm-t-slow)}
.dm-aln-hand{padding:6px 10px 6px 56px;font-size:12.5px;line-height:18px;color:var(--dm-fg2)}
.dm-aln-hand b{font-weight:500}
.dm-aln-hand .dm-f{color:var(--dm-from)}.dm-aln-hand .dm-t{color:var(--dm-to)}
/* the light that carries the work down the line: a bright head with a tail in the sender's hue */
.dm-aln-pk{position:absolute;left:27px;top:0;width:8px;height:8px;margin:-4px 0 0 -4px;border-radius:50%;background:var(--dm-to);box-shadow:0 0 0 3px color-mix(in srgb,var(--dm-to) 22%,transparent),0 0 12px 2px color-mix(in srgb,var(--dm-to) 45%,transparent);pointer-events:none;z-index:2;opacity:0}
.dm-aln-pk::before{content:"";position:absolute;left:3px;bottom:4px;width:2px;height:30px;border-radius:1px;background:linear-gradient(transparent,var(--dm-from))}
@media (max-width:600px){
.dm-aln-row{grid-template-columns:34px minmax(0,1fr) auto;row-gap:var(--dm-s-2)}
.dm-aln-tr{grid-column:2 / -1;grid-row:2}
.dm-aln-lane::before{top:48px}
.dm-aln-ct{display:none}
.dm-aln-m{margin-right:auto}
}
@media (max-width:400px){.dm-aln-ic{display:none}}
:root:not([data-style="soft"]) .dm-aln-rows>*:not(:where([data-style="soft"],[data-style="soft"] *))::before,:root:not([data-style="soft"]) .dm-aln-lane:not(:where([data-style="soft"],[data-style="soft"] *))::after{width:1px;left:26.5px;background:repeating-linear-gradient(var(--dm-rail) 0 3px,transparent 3px 6px)}
:root:not([data-style="soft"]) .dm-aln-lane.dm-sent:not(:where([data-style="soft"],[data-style="soft"] *))::before,:root:not([data-style="soft"]) .dm-aln-lane.dm-got:not(:where([data-style="soft"],[data-style="soft"] *))::after{background:var(--dm-c)}
:root:not([data-style="soft"]) .dm-aln-hf.dm-lit:not(:where([data-style="soft"],[data-style="soft"] *))::before{background:var(--dm-to)}
:root:not([data-style="soft"]) .dm-aln-av:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-c) 35%,transparent)}
:root:not([data-style="soft"]) .dm-aln-lane[data-s="working"] .dm-aln-av:not(:where([data-style="soft"],[data-style="soft"] *)){animation:dm-alnbp 1.1s steps(1,end) infinite}
@keyframes dm-alnbp{0%,55%{box-shadow:inset 0 0 0 1px var(--dm-c)}56%,100%{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-c) 35%,transparent)}}
:root:not([data-style="soft"]) .dm-aln-lane[data-s="working"]:not(:where([data-style="soft"],[data-style="soft"] *)){background:color-mix(in srgb,var(--dm-c) 5%,transparent)}
:root:not([data-style="soft"]) .dm-aln-tr i:not(:where([data-style="soft"],[data-style="soft"] *)){height:8px}
:root:not([data-style="soft"]) .dm-aln-tr i.dm-run:not(:where([data-style="soft"],[data-style="soft"] *))::after{background:repeating-linear-gradient(135deg,var(--dm-cm) 0 4px,color-mix(in srgb,var(--dm-cm) 55%,var(--dm-surface)) 4px 7px);animation:none}
:root:not([data-style="soft"]) .dm-aln-pk:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:none;width:7px;height:7px;margin:-3px 0 0 -3px}
:root:not([data-style="soft"]) .dm-aln-pk:not(:where([data-style="soft"],[data-style="soft"] *))::before{width:1px}
:root:not([data-style="soft"]) .dm-aln-av.dm-ping:not(:where([data-style="soft"],[data-style="soft"] *))::before{animation:dm-alnpingbp .6s steps(3,end)}
@keyframes dm-alnpingbp{from{box-shadow:0 0 0 1px var(--dm-c)}to{box-shadow:0 0 0 7px transparent}}
agent-lanes.htmlHTML · 9 lines
<div class="card wide bare" data-c="agent-lanes">
<div class="stage-in">
<div class="aln panel">
<div class="aln-h"><span class="aln-ic"><svg class="i s"><use href="#i-users"/></svg></span><span class="aln-t">3 agents working</span><span class="aln-m muted num">0:00</span><span class="aln-ct num">0 of 9 steps</span><button class="pill soft sm aln-pause"><svg class="i xs"><use href="#i-pause"/></svg>Pause all</button></div>
<div class="aln-rows"></div>
</div>
</div>
</div>
agent-lanes.cssHTML · 94 lines
/* ---------- Agent lanes ---------- */
/* each agent has its own hue (--c, with its tint --cb): its badge, its track, its tag and its stretch of the relay line that
joins the badges. A hand-off sends a light down that line from one agent to the next, and the stretch it crossed stays lit */
.aln{padding:var(--s-2)}
/* the lanes' marks (tracks, lines, a finished badge) in livelier versions of the agents' hues, each 3:1 on the surface; their
text keeps the agent's text colour. Dark mode's agent colours are light already, so they are used as they are */
.aln{--aln-pb:#2a7bff;--aln-pm:#1fa66a;--aln-pl:#7a5af5}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .aln{--aln-pb:var(--pb);--aln-pm:var(--pm);--aln-pl:var(--pl)}}
:root[data-theme="dark"] .aln{--aln-pb:var(--pb);--aln-pm:var(--pm);--aln-pl:var(--pl)}
/* on a card the lanes sit centred in the room kept for their tallest moment, and glide as a hand-off line opens and shuts */
.stage-in:has(>.aln){display:flex;flex-direction:column;justify-content:center}
.aln-h{display:flex;align-items:center;gap:10px;padding:var(--s-2) 10px 10px}
.aln-ic{color:var(--fg2);display:grid;transition:color .3s}
.aln.fin .aln-ic{color:var(--ok)}
.aln-t{font-weight:500}
.aln-m{font-size:12.5px}
.aln-ct{font-size:12.5px;color:var(--muted);margin-right:auto;white-space:nowrap}
.aln-ct::before{content:"·";margin-right:6px}
.aln-rows{position:relative}
.aln-rows>*{position:relative}
.aln-lane{border-radius:14px;transition:background .3s}
@media (hover:hover) and (pointer:fine){.aln-lane:hover{background:var(--row)}}
/* the lane at work carries a wash of its hue from the left */
.aln-lane[data-s="working"]{background:linear-gradient(90deg,color-mix(in srgb,var(--c) 8%,transparent),transparent 72%)}
@media (hover:hover) and (pointer:fine){.aln-lane[data-s="working"]:hover{background:linear-gradient(90deg,color-mix(in srgb,var(--c) 8%,transparent),transparent 72%),var(--row)}}
/* the relay line runs from badge to badge, through the lanes and the hand-offs between them, and stops short of each badge */
.aln-rows>*::before{content:"";position:absolute;left:26px;width:2px;top:0;bottom:0;border-radius:1px;background:var(--line);transition:background .5s var(--ease);pointer-events:none}
.aln-lane::before{top:48px}
.aln-lane:last-child::before{display:none}
.aln-lane::after{content:"";position:absolute;left:26px;width:2px;top:0;height:6px;border-radius:1px;background:var(--line);transition:background .5s var(--ease);pointer-events:none}
.aln-lane:first-child::after{display:none}
.aln-lane.sent::before{background:linear-gradient(var(--cm),color-mix(in srgb,var(--cm) 45%,var(--line)))}
.aln-lane.got::after{background:var(--cm)}
.aln-hf.lit::before{background:linear-gradient(color-mix(in srgb,var(--from) 45%,var(--line)),var(--to))}
.aln-row{display:grid;grid-template-columns:34px minmax(0,1.2fr) minmax(0,1fr) auto;align-items:center;gap:var(--s-3);width:100%;padding:10px;text-align:left}
.aln-av{position:relative;width:34px;height:34px;border-radius:11px;display:grid;place-items:center;color:var(--c);background:var(--cb);transition:color .4s,background .4s}
.aln-av::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none}
.aln-lane[data-s="working"] .aln-av{animation:alnbreathe 2.4s var(--ease-io) infinite}
@keyframes alnbreathe{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--c) 0%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--c) 18%,transparent)}}
/* the badge that takes a hand-off rings once as the light reaches it */
.aln-av.ping::before{animation:alnping .9s var(--ease)}
@keyframes alnping{from{box-shadow:0 0 0 0 color-mix(in srgb,var(--c) 50%,transparent)}to{box-shadow:0 0 0 12px color-mix(in srgb,var(--c) 0%,transparent)}}
/* a finished agent's badge fills with its hue */
.aln-lane[data-s="done"] .aln-av{background:var(--cm);color:var(--surface)}
.aln-who{min-width:0}
.aln-n{display:block;font-weight:500;font-size:13.5px}
.aln-a{display:block;font-size:12.5px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.aln-a.live{color:var(--fg2)}
/* the track: one part a step. Finished parts are solid, the one in hand fills as the agent works, with a light running along it */
.aln-tr{display:flex;gap:3px}
.aln-tr i{flex:1;height:10px;border-radius:5px;background:color-mix(in srgb,var(--cm) 13%,var(--line));position:relative;overflow:hidden}
.aln-tr i::after{content:"";position:absolute;inset:0;border-radius:inherit;background:var(--cm);transform:scaleX(var(--p,0));transform-origin:left;transition:transform .25s linear}
.aln-tr i.on::after{transform:scaleX(1);transition:transform .5s var(--ease)}
.aln-tr i.run::after{background:linear-gradient(90deg,var(--cm) 0 30%,color-mix(in srgb,var(--cm) 30%,var(--surface)) 50%,var(--cm) 70% 100%) 0 0/250% 100%;animation:alnsheen 1.6s linear infinite}
/* given no step progress, the part in hand fills part way and keeps its light */
.aln-tr i.run.ind::after{transform:scaleX(.6);transition:transform 2.4s var(--ease)}
@keyframes alnsheen{from{background-position:100% 0}to{background-position:-150% 0}}
.aln-st{min-width:92px;display:flex;justify-content:flex-end}
.aln-tg{background:var(--cb);color:var(--c)}
.aln-tg .spin{width:9px;height:9px}
.aln-det{padding:0 10px 10px 56px;font-size:12.5px;color:var(--fg2)}
.aln-det div{display:flex;align-items:center;gap:var(--s-2);padding:3px 0}
.aln-det div::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--c);flex:none}
.aln-det .muted{margin-left:auto}
/* a hand-off opens a line between the lanes, glides the lanes below down and closes again */
.aln-hf{transition-duration:var(--t-slow)}
.aln-hand{padding:6px 10px 6px 56px;font-size:12.5px;line-height:18px;color:var(--fg2)}
.aln-hand b{font-weight:500}
.aln-hand .f{color:var(--from)}.aln-hand .t{color:var(--to)}
/* the light that carries the work down the line: a bright head with a tail in the sender's hue */
.aln-pk{position:absolute;left:27px;top:0;width:8px;height:8px;margin:-4px 0 0 -4px;border-radius:50%;background:var(--to);box-shadow:0 0 0 3px color-mix(in srgb,var(--to) 22%,transparent),0 0 12px 2px color-mix(in srgb,var(--to) 45%,transparent);pointer-events:none;z-index:2;opacity:0}
.aln-pk::before{content:"";position:absolute;left:3px;bottom:4px;width:2px;height:30px;border-radius:1px;background:linear-gradient(transparent,var(--from))}
@media (max-width:600px){
.aln-row{grid-template-columns:34px minmax(0,1fr) auto;row-gap:var(--s-2)}
.aln-tr{grid-column:2 / -1;grid-row:2}
.aln-lane::before{top:48px}
.aln-ct{display:none}
.aln-m{margin-right:auto}
}
@media (max-width:400px){.aln-ic{display:none}}
:root:not([data-style="soft"]) .aln-rows>*:not(:where([data-style="soft"],[data-style="soft"] *))::before,:root:not([data-style="soft"]) .aln-lane:not(:where([data-style="soft"],[data-style="soft"] *))::after{width:1px;left:26.5px;background:repeating-linear-gradient(var(--rail) 0 3px,transparent 3px 6px)}
:root:not([data-style="soft"]) .aln-lane.sent:not(:where([data-style="soft"],[data-style="soft"] *))::before,:root:not([data-style="soft"]) .aln-lane.got:not(:where([data-style="soft"],[data-style="soft"] *))::after{background:var(--c)}
:root:not([data-style="soft"]) .aln-hf.lit:not(:where([data-style="soft"],[data-style="soft"] *))::before{background:var(--to)}
:root:not([data-style="soft"]) .aln-av:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--c) 35%,transparent)}
:root:not([data-style="soft"]) .aln-lane[data-s="working"] .aln-av:not(:where([data-style="soft"],[data-style="soft"] *)){animation:alnbp 1.1s steps(1,end) infinite}
@keyframes alnbp{0%,55%{box-shadow:inset 0 0 0 1px var(--c)}56%,100%{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--c) 35%,transparent)}}
:root:not([data-style="soft"]) .aln-lane[data-s="working"]:not(:where([data-style="soft"],[data-style="soft"] *)){background:color-mix(in srgb,var(--c) 5%,transparent)}
:root:not([data-style="soft"]) .aln-tr i:not(:where([data-style="soft"],[data-style="soft"] *)){height:8px}
:root:not([data-style="soft"]) .aln-tr i.run:not(:where([data-style="soft"],[data-style="soft"] *))::after{background:repeating-linear-gradient(135deg,var(--cm) 0 4px,color-mix(in srgb,var(--cm) 55%,var(--surface)) 4px 7px);animation:none}
:root:not([data-style="soft"]) .aln-pk:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:none;width:7px;height:7px;margin:-3px 0 0 -3px}
:root:not([data-style="soft"]) .aln-pk:not(:where([data-style="soft"],[data-style="soft"] *))::before{width:1px}
:root:not([data-style="soft"]) .aln-av.ping:not(:where([data-style="soft"],[data-style="soft"] *))::before{animation:alnpingbp .6s steps(3,end)}
@keyframes alnpingbp{from{box-shadow:0 0 0 1px var(--c)}to{box-shadow:0 0 0 7px transparent}}
agent-lanes.jsHTML · 65 lines
DM.def('agent-lanes', (r, c) => {
r.innerHTML = r.innerHTML.replace('{users}', I('users')).replace('{pause}', I('pause','xs'));
// each agent has a hue of its own (t names the palette slot): its badge, its track, its tag and its stretch of the relay line
const L = [
{ n:'Researcher', ic:'search', t:'pb', acts:['Reading competitor sites','Comparing 5 pricing pages','Writing the brief'], log:['Read stripe.com/pricing','Read linear.app/pricing','Noted 3 patterns'] },
{ n:'Designer', ic:'palette', t:'pl', wait:'Waiting for the brief', acts:['Laying out pricing cards','Setting type and spacing','Exporting frames'], log:['Made 3 card variants','Applied spacing tokens','Exported 4 frames'] },
{ n:'Reviewer', ic:'eye', t:'pm', wait:'Waiting for the designer', acts:['Checking contrast','Checking copy','Signing off'], log:['Contrast passes AA','Fixed 2 lines of copy','Approved'] }
];
const root = $('.aln', r), rows = $('.aln-rows', r), ct = $('.aln-ct', r), STEPS = L.length * 3;
L.forEach(l => {
l.el = html(`<div class="aln-lane" data-s="queued" style="--c:var(--${l.t});--cb:var(--${l.t}-bg);--cm:var(--aln-${l.t},var(--${l.t}))"><button class="aln-row" aria-expanded="false"><span class="aln-av">${I(l.ic)}</span><span class="aln-who"><span class="aln-n">${l.n}</span><span class="aln-a">${l.wait || 'Queued'}</span></span><span class="aln-tr" aria-hidden="true"><i></i><i></i><i></i></span><span class="aln-st"><span class="tag">Queued</span></span></button><div class="fold shut"><div><div class="aln-det"></div></div></div></div>`);
rows.appendChild(l.el); const b = $('.aln-row', l.el), f = $('.fold', l.el);
b.onclick = () => { const s = f.classList.toggle('shut'); b.setAttribute('aria-expanded', !s); };
});
const TAG = { queued:'<span class="tag">Queued</span>', waiting:`<span class="tag">${I('clock','xs')}Waiting</span>`, working:'<span class="tag aln-tg"><span class="spin"></span>Working</span>', paused:'<span class="tag warn">Paused</span>', done:`<span class="tag ok">${I('check','xs')}Done</span>` };
const state = (l, s) => { l.el.dataset.s = s; const t = $('.aln-st', l.el); t.innerHTML = TAG[s]; enter(t.firstElementChild); };
const act = (l, t, live) => { const a = $('.aln-a', l.el); a.classList.toggle('live', !!live); a.classList.toggle('shim', !!live); swap(a, t); };
let paused = false, secs = 0, finished = false, steps = 0;
const count = () => swap(ct, `${steps} of ${STEPS} steps`);
const pb = $('.aln-pause', r);
pb.onclick = () => { paused = !paused; pb.innerHTML = I(paused ? 'play' : 'pause','xs') + (paused ? 'Resume' : 'Pause all'); swap($('.aln-t', r), paused ? 'All agents paused' : '3 agents working');
L.forEach(l => { if (l.running){ state(l, paused ? 'paused' : 'working'); $('.aln-a', l.el).classList.toggle('shim', !paused); } }); };
c.every(1000, () => { if (!paused && !finished){ secs++; $('.aln-m', r).textContent = Math.floor(secs / 60) + ':' + String(secs % 60).padStart(2, '0'); } });
// time only passes for the agents while they aren't paused; each() hears how far through the wait they are
const hold = async (ms, each) => { let t = 0; while (t < ms){ await c.wait(100); if (!paused){ t += 100; each && each(t / ms); } } };
async function work(l){
l.running = true; state(l, 'working');
const bars = $$('.aln-tr i', l.el), det = $('.aln-det', l.el);
for (let i = 0; i < 3; i++){
bars[i].classList.add('run'); act(l, l.acts[i], true);
await hold(1500, p => bars[i].style.setProperty('--p', p.toFixed(3)));
bars[i].classList.replace('run', 'on'); steps++; count();
const d = html(`<div><span>${l.log[i]}</span><span class="muted">${secs}s</span></div>`); det.appendChild(d); enter(d);
}
l.running = false; act(l, l.log[2]); state(l, 'done');
}
// a light runs down the relay line from one badge to the next, the badge it reaches rings, and the stretch stays lit
function carry(from, to){
const rr = rows.getBoundingClientRect(), a = $('.aln-av', from.el).getBoundingClientRect(), b = $('.aln-av', to.el).getBoundingClientRect();
const pk = html(`<span class="aln-pk" aria-hidden="true" style="--from:var(--${from.t});--to:var(--${to.t})"></span>`); rows.appendChild(pk);
const y0 = a.bottom - rr.top + 4, y1 = b.top - rr.top - 4;
const an = REDUCE ? null : pk.animate([{ transform:`translateY(${y0}px)`, opacity:0 }, { opacity:1, offset:.12 }, { opacity:1, offset:.88 }, { transform:`translateY(${y1}px)`, opacity:0 }], { duration:950, easing:'cubic-bezier(.65,0,.35,1)' });
c.offs.push(() => pk.remove());
return (an ? an.finished.catch(() => {}) : Promise.resolve()).then(() => pk.remove());
}
async function handoff(from, to, txt){
const h = html(`<div class="fold shut aln-hf" style="--from:var(--${from.t});--to:var(--${to.t})"><div><div class="aln-hand"><b class="f">${from.n}</b> handed ${txt} to <b class="t">${to.n}</b></div></div></div>`);
to.el.before(h); void h.offsetWidth; h.classList.remove('shut'); enter($('.aln-hand', h), 80);
await c.wait(REDUCE ? 0 : 500); await carry(from, to);
from.el.classList.add('sent'); h.classList.add('lit'); to.el.classList.add('got');
const av = $('.aln-av', to.el); av.classList.add('ping'); c.later(950, () => av.classList.remove('ping'));
await c.wait(1300); h.classList.add('shut'); await c.wait(REDUCE ? 0 : 700); h.remove();
}
c.run(async () => {
await c.wait(600);
const r1 = work(L[0]); act(L[1], L[1].wait); state(L[1], 'waiting');
await r1; await handoff(L[0], L[1], 'the brief');
act(L[2], L[2].wait); state(L[2], 'waiting');
await work(L[1]); await handoff(L[1], L[2], '4 frames');
await work(L[2]);
finished = true; root.classList.add('fin'); $('.aln-ic', r).innerHTML = I('ok'); enter($('.aln-ic', r).firstElementChild);
swap($('.aln-t', r), 'All 3 agents finished'); pb.remove();
});
});
Questions
What people ask about it.
- Does Pause all stop my agents?
- No. It changes how the lanes look and calls
onPausedChange. Your code pauses the agents, and the lanes' ownstatusvalues stay as you set them. - How do I show one agent handing work to another?
- Set
handoffwith thefromandtolane keys and a shortwhat, like "the brief". The light takes about a second and a half to arrive; clearhandoffafter that and the line closes before it goes. - Can a lane have more than three steps?
- Yes. Set
stepson the lane to cut its track into as many parts as you need, movedoneforward as each one finishes, and setprogressto fill the one in hand.
More in Agent status
Show what the agent is doing, how far along it is, and what it is working with.