Agent lanes
NewA team of agents on one time axis: each task a bar that runs, finishes, fails or waits for you, with handoffs between them.
agent-lanes/agent-lanes.tsxAgents
2 running1 need you
Last 30m- ResearcherModel A large
- WriterModel A medium
- ReviewerIdleModel B pro
- OpsNeeds youModel C fast
The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.
When to use
- A team of agents working at once, where the reader needs to see who is busy, who is stuck and who is waiting on them.
- Recent work on a shared clock: the last 15 minutes to a couple of hours, ending at now.
- Agents that pause for approval, where a bar that asks is better than a separate inbox.
- Work that passes from one agent to another, so the handoff is worth drawing.
- A side panel or a dashboard tile, about 560 to 670 px wide, with two to six lanes.
When not to use
- One agent, or one task at a time. Use a progress line or a step list in the chat.
- Long history, days or weeks of runs. Use a table or a log with filters and dates.
- Exact timing to the second, as in a trace. Use a waterfall or a profiler view.
- A queue of approvals with nothing else to show. Use an approval card or an inbox.
- Many agents, past eight or so: the lanes get too thin. Group them, or use a table with a status column.
You can add this component with the shadcn CLI or by hand:
Into components/agent-lanes of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/agent-lanes.json- Copy the component files
Copy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
- Import it
As shown under Usage, below.
Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.
Usage
'use client';import { AgentLanes, type Task } from './agent-lanes';const tasks: Task[] = [ { id: 'deploy', agent: 'ops', label: 'Deploy release', tools: ['code', 'key'], status: 'needs', start: -6, notes: ['Built and tested', 'Waiting for your go-ahead'], ask: { title: 'Deploy release', body: 'Ship build 214 to production?' }, },];export function TeamBoard() { return ( <AgentLanes agents={[{ id: 'ops', name: 'Ops', model: 'Model C fast', mark: 'diamond' }]} tasks={tasks} onDecision={(task, decision) => console.log(task.id, decision)} autoplay={false} /> );}Each state below is a live instance, locked so you can study it.
The pieces it is built from, each with its measurements read live.
A card that shows what a team of agents did over the last half hour. Each agent has a lane on a shared axis that ends at now; its tasks are bars, grey when done, red when they failed, blue and still growing while they run, orange while they wait for you. Time keeps flowing, so finished work drifts left. Hover a bar for what it did, open a waiting task to approve or reject it, and follow the dashed curve where one agent hands its work to another.
- Time is a clock in minutes from the moment the board mounts, and it moves a single custom property, --now, on the lanes. --k is the lanes’ px per minute, measured from their width; each bar is placed at (start + window − now) × k by a transform, and a running bar is (now − start) × k wide, so its right end stays on now while finished bars drift left.
- The clock writes --now every frame only while that moves a bar by a quarter of a pixel or more; at real speed that is about once a second, so a board left open costs almost nothing. React re-renders only when the data changes, and for a tooltip’s minutes while one is open.
- A bar’s status is a data attribute: its background, text and the bright right edge of a running bar cross-fade with transitions, so approving a waiting task or finishing one turns it in place. A running bar has a soft band of light sweeping across it.
- Handoffs are SVG curves on a track of their own over all the lanes, from the end of one bar to the start of the next, sliding with time like the bars. A new one draws itself in through a mask whose stroke dash runs from 0 to the whole path.
- Lanes clip their bars and fade them out at the left edge with a mask, as they slide out of the window. Under 560 px of card, each agent sits over its lane instead of beside it.
- The demo hovers bars, opens the waiting task and approves it, then lets the writer hand its draft to the reviewer, on a clock 30 times faster than real time. Any input of the reader’s stops it, and time goes back to the speed prop.
| Prop | Type | Default | Description |
|---|---|---|---|
agents | readonly Agent[] | AGENTS | The agents, one lane each: an id, a name, the model under it, and the mark on its tile (petal, rings, diamond, hex). |
tasks | readonly Task[] | TASKS | Their tasks: the agent, a label, the tools it used, a status (running, needs, done, failed), a start and, when finished, an end, in minutes from mount (negative: before it); notes for the tooltip, and for a task that needs you, the question it asks. |
handoffs | readonly Handoff[] | HANDOFFS | Which finished task handed its work to which: a dashed curve between them. |
window | number | 30 | How many minutes the axis shows, up to now. |
speed | number | 1 | How fast time runs on the board: 1 is real time. The demo runs at 30. |
title | string | 'Agents' | The header’s title. |
onDecision | (task: Task, decision: 'approve' | 'reject') => void | — | Called when the reader approves a waiting task (it runs) or rejects it (it ends as failed). |
autoplay | boolean | true | While on screen and untouched, a demo pointer shows the board at work; any input hands it to the reader. |
colorScheme | 'light' | 'dark' | 'system' | — | Light or dark colours, or the reader’s; without it, the page’s color-scheme. |
defaultOpenTask | string | — | Opens with the popover of this waiting task showing (its id), to show the question in a static view; turns the demo off. |
defaultTooltip | string | — | Opens with the tooltip of this task showing (its id); turns the demo off. |
className | string | — | Classes for the stage. |
The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useEffectEvent, useId, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent,} from 'react';import { AGENTS, HANDOFFS, REVIEW_TASK, TASKS, type Agent, type Handoff, type Task } from './data';import { DemoPointer, runDemo, type Point } from './demo-pointer';import { AgentGlyph, Sparkle, ToolGlyph, Warning } from './glyphs';import { useAutoplay } from './use-autoplay';import styles from './agent-lanes.module.css';/* * Agent lanes: what a team of agents did over the last half hour, one lane per agent on a shared/* Agent lanes (agent-lanes.tsx): a quiet grey stage with the card in its middle, or at night a near-black one with the card a step darker still. The card is laid out for 666 px: a 140 px column of agents, then lanes on a shared axis that ends at now. Under 560 px of card, each agent sits over its lane instead of beside it. Time: the script sets --k (px per minute, from the lanes' width) and --now (minutes since mount) on .body; --window is the axis's span. A bar sits at (start + window) × k on its lane's track, which slides left by now × k; a running bar is (now − start) × k wide, so it ends on now. Colours are the tokens below, light-dark(light, dark) where the two differ: the component takes the color-scheme it inherits from the page (light unless the page says otherwise), or the one its colorScheme prop sets (data-color-scheme). */.root { /* The stage, the card, its edge and the lines across it. */ --stage: #f7f7f7; --card: #fdfdfd; --edge: #e6e6e6; --line: #efefef; --grid: rgb(0 0 0 / 0.035); --card-shadow: rgb(0 0 0 / 0.03); /* Text: names, models and quiet counts, the axis. */ --ink: #111113; --ink-2: #737376;// Copyright (c) 2026 Bruno Delorence. MIT License./* * The shapes the lanes draw, and the board the lab shows: four agents, their tasks of the last * half hour and the one handoff between them. Times are in minutes from the moment the board * mounts: -12 is twelve minutes ago. *//** The tools a task used: a small glyph each, drawn in glyphs.tsx. */export type Tool = 'search' | 'doc' | 'chat' | 'design' | 'code' | 'ticket' | 'key' | 'sync';/** An agent's mark on its tile: one per model family. */export type AgentMark = 'petal' | 'rings' | 'diamond' | 'hex';/** * Where a task stands. A running task, or one waiting for you, reaches up to now; a done or * failed one has its end. */export type TaskStatus = 'running' | 'needs' | 'done' | 'failed';export type Agent = { id: string; name: string; /** The model it runs on, under its name. */// Copyright (c) 2026 Bruno Delorence. MIT License./* * The small drawings: a glyph for each tool a task uses, a mark for each agent's tile, the * header's sparkle and the warning triangle, drawn on a 16-unit grid and sized by * the stylesheet. Ink-coloured strokes take --glyph-ink, so they turn light at night; the * coloured ones keep their colour on both schemes. */import type { AgentMark, Tool } from './data';const INK = { stroke: 'var(--glyph-ink)' };function ToolPath({ tool }: { tool: Tool }) { switch (tool) { case 'search': return ( <> <circle cx="6.8" cy="6.8" r="4.3" fill="none" stroke="#2f7cf6" strokeWidth="2.2" /> <path d="M10.2 10.2l3.6 3.6" stroke="#18a46b" strokeWidth="2.4" strokeLinecap="round" /> </> ); case 'doc': return (// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import type { CSSProperties, Ref } from 'react';/* * The demo's pointer, and the steps that move it. A demo is a script of awaited steps: wait, or * glide the pointer somewhere. Stopping the demo (the reader takes over, or it scrolls away) ends * the step in progress at once: its promise rejects, which ends the script right there, and the * pointer fades out. * * Each glide is a curved flight played by the Web Animations API, so nothing runs frame by frame. * The points of the flight are handed to whoever wants to move along with it. The arrow and the * hand are HTML boxes * that swap by their visibility: a change of style inside an SVG would lay the SVG out again. */export type Point = { x: number; y: number };/** A glide's path and timing, for an animation that plays along with it. */export type Flight = { points: Point[]; timing: KeyframeAnimationOptions };export type Demo = {// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) { const ref = useRef<T>(null); const [visible, setVisible] = useState(false); const [reduced, setReduced] = useState(true); const [taken, setTaken] = useState(false); const timer = useRef(0); useEffect(() => { const element = ref.current; if (!element) return; let onScreen = false; const update = () => setVisible(onScreen && !document.hidden); const observer = new IntersectionObserver(- The card is a section named by its title; the agents are a list, and each lane’s tasks a list of buttons named by their label and status.
- Hovering, focusing with the keyboard or tapping a bar shows its tooltip, tied to the bar with aria-describedby; Escape hides it.
- The “Needs you” chip is a button with aria-haspopup and aria-expanded. Its popover is a non-modal dialog named by its title: opening it from the chip puts the focus on Approve, Escape closes it and gives the focus back, and a decision returns the focus to the bar.
- Approving, rejecting and a handoff are announced in a polite live region. The header’s counts say how many agents run and how many need you in words, not only with coloured dots.
- Every control has a visible focus ring. Under reduced motion, the demo, the sweeping light, the chip’s breathing and the curves’ drawing are off; time still moves, at the speed prop.
- The demo pointer is decorative and hidden from assistive technology.