Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Agent progress

A coding agent at work: it reads, searches, thinks, writes and reviews, each step in the colour of its kind of work.

agent-progress/agent-progress.tsx
Live · interactive

Coding Agent

Ready

Ship dark mode toggle

  1. To do: Read theme tokens
  2. To do: Search open-source examples
  3. To do: Pick a token strategy
  4. To do: Implement the toggle
  5. To do: Fix contrast on hover

Ready to run5 steps · Sable 3.2

  • Repo
  • Files
  • Design
  • Issues
  • Packages

0 tokens$0.0000:00

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

  • An agent working through a task of a few known steps, where the reader waits for it and wants to see where it is.
  • Work that takes seconds to minutes: long enough to need a sign of life, short enough to watch.
  • When the work itself is worth showing as it happens: the file read, the search, the code typed, the change made.
  • When tokens, cost and time matter to the reader, and they may want to pause or stop the run.

When not to use

  • A wait of a second or two. A spinner or a busy button says enough.
  • Open-ended work with no plan up front. Use a streaming log or a chat transcript instead.
  • Background jobs nobody watches. Use a notification or a status in a list when they are done.
  • Many runs at once. Use a table of jobs with a status column, and open this card for one of them.

You can add this component with the shadcn CLI or by hand:

Into components/agent-progress of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/agent-progress.json

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

agent-panel.tsx
'use client';import { AgentProgress } from './agent-progress';import { STEPS } from './run';export function AgentPanel() {  // Waits for the reader's Run; reports each finished run.  return (    <AgentProgress      agent="Coding Agent"      task="Ship dark mode toggle"      steps={STEPS}      onComplete={({ tokens, ms }) => console.log(`${tokens} tokens in ${ms / 1000} s`)}      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 follows a coding agent through a task. The header says what it is doing now, in the colour of that kind of work, over a progress line; the steps tick off below, and a panel shows the work as it happens: a file read line by line, a search and its results, thoughts pinned one by one, text typed in, a review’s fixes. The footer counts tokens, cost and time, with Pause, Stop and Run again.

  • The run is a clock. Everything on the card is worked out from the time the run has been going: the step in progress, how far into it, the tokens spent, and how much of the step’s work has come in (lines read, characters typed, results found).
  • An animation frame loop moves the clock only while a run plays on screen in a visible tab. It redraws the card only when something it shows changes: a character, a result, a tenth of a thousand tokens, a second.
  • Each kind of work sets one colour, --accent. The icon, the status, the progress line, the ticks and the tinted pieces pick it up with a transition, so the whole card turns together from violet to blue, orange, green and red.
  • When the step changes, the panel’s height eases from the old content’s height to the new one’s with the Web Animations API, while the new step’s pieces come in one by one.
  • It is built for a phone’s GPU. The progress line, reading lines, results and notes come in as clip-path wipes, the tick draws its stroke, and labels fade their colour. Only the bobbing icon and the spinner need layers of their own, and only while the clock runs.
  • Every colour is a light-dark() token on the root, the five kinds’ colours included; the script only points --accent at the step’s kind (var(--read) and so on), so the kinds, their tints and the whole card follow the page’s color-scheme, or the colorScheme prop. At night the kinds are lighter, their ticks turn dark, and Run turns light.
PropTypeDefaultDescription
agentstring'Coding Agent'The agent’s name, in the header.
taskstring'Ship dark mode toggle'The task it works on, over the steps.
stepsreadonly AgentStep[]STEPSThe run. Each step has a kind (read, search, think, write, review), a label, a duration in ms, the tokens it spends, the tool it lights, and its detail: a file and its lines, a query and results, notes, text, or a diff.
doneTitlestring'Dark mode shipped'What the card says once the run is done.
toolsreadonly string[]TOOLSThe tools along the footer; a step lights the one it works in.
modelsreadonly string[]MODELSThe models in the picker; the first is picked at first.
defaultStateAgentProgressState—Opens the card at a moment of a run, with no demo: status ('idle', 'running', 'paused' or 'complete'), elapsed ms, held (a running clock drawn still until a button is pressed) and the model picked.
onComplete(run: { tokens: number; ms: number }) => void—Called when a run is done, with the tokens it spent and how long it took.
onStatusChange(status: AgentProgressStatus) => void—Called when the reader or the demo starts, pauses, resumes or stops a run, or it ends, with the new status.
autoplaybooleantrueWhile on screen and untouched, starts a run on its own, and another a few seconds after each one ends. A run the reader paused or stopped is left alone.
colorScheme'light' | 'dark' | 'system'—Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one.
classNamestring—Extra class for the root, the card’s stage.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

agent-progress.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useEffect,  useLayoutEffect,  useRef,  useState,  type CSSProperties,  type ReactNode,  type RefObject,} from 'react';import {  CheckIcon,  ChevronIcon,  CloseIcon,  DoneIcon,  FileIcon,  PauseIcon,  PlayIcon,  RerunIcon,  SearchIcon,  SpinnerIcon,
agent-progress.module.css
/* Agent progress (agent-progress.tsx): a white stage with the card in its middle, or at night a   near-black one with the card a step lighter. The root's --accent is the colour of the work in   progress; data-status says where the run is, and data-live is set while its clock moves, which   is when the icon and the spinner animate.   Colours are the tokens below, each light-dark(light, dark): 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 colour of each kind of work, of a finished run and of the card at rest: the script sets     --accent to one of them. At night they are lighter, to read on the dark card. */  --read: #7a5af0;  --search: #4a5df0;  --think: #e0782a;  --write: #2d9467;  --review: #d93f5d;  --done: #2d9467;  --idle: #a1a1aa;  --accent: var(--idle);  /* The tick on the accent, the tinted pieces, the reading lines, and the query typed in. */  --accent-ink: #fff;  --tint: color-mix(in srgb, var(--accent) 11%, #fff);  --skeleton: color-mix(in srgb, var(--accent) 20%, #fff);
run.ts
/* * The run: what the agent does, step by step, and where it stands at any moment of it. A run is * a list of steps, each of a kind (reading, searching, thinking, writing, reviewing) with its * colour, its status line and what its detail panel shows. Everything the card draws is worked * out from the time the run has been going: which step is in progress, how far into it, how many * tokens it has spent, and how much of the step's detail has come in. *//** What a step does. Each kind has its own colour, status line and detail panel. */export type StepKind = 'read' | 'search' | 'think' | 'write' | 'review';/** A search hit: a title and where it comes from. */export type SearchResult = { title: string; source: string };/** A piece of a reviewed sentence: text that stays, or a change (text removed, text added). */export type DiffPart = string | { removed: string; added: string };type StepBase = {  /** What the step does, as the list shows it. */  label: string;  /** How long it takes, in ms. */  ms: number;  /** The tokens it spends. */  tokens: number;
icons.tsx
/* * The card's small icons, drawn on a 16 × 16 grid in the current colour. All are decorative: the * buttons and the steps carry their names in text. */type IconProps = { className?: string };const svg = {  viewBox: '0 0 16 16',  fill: 'none',  stroke: 'currentColor',  strokeLinecap: 'round',  strokeLinejoin: 'round',  'aria-hidden': true,} as const;export function CheckIcon({ className }: IconProps) {  return (    <svg {...svg} className={className} strokeWidth={2}>      <path d="m4.6 8.3 2.3 2.3 4.6-4.9" pathLength={1} />    </svg>  );}
use-autoplay.ts
'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(      ([entry]) => {        onScreen = entry.isIntersecting;
  • The steps are an ordered list, each prefixed for screen readers with “Done”, “In progress” or “To do”. The progress line is a progressbar (“2 of 5 steps done”).
  • A run the reader starts, pauses or resumes is announced politely, a step at a time (“Searching web: Search open-source examples. Step 2 of 5.”), then at the end with its totals. Tokens and the timer are never announced, and the demo’s own runs stay silent.
  • Run, Pause, Resume and Run again are one button whose name follows the run, so focus stays put. “Stop the run” is a separate button, and focus returns to the main button when it goes. The model picker is a native select.
  • Under reduced motion the demo never starts. A run the reader starts shows each step whole, without typing, growing lines or easing.