Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Agent queue

New

Keep queuing tasks for a coding agent while it works: drag to reorder, pause to add context, answer what it asks.

agent-queue/agent-queue.tsx
Live · interactive
  1. keep the composer pinned to the bottom of the panel, done

Alt and the arrow keys move a task in the queue; Delete removes it.

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 coding or writing agent that works through requests one at a time, while the reader keeps adding more.
  • Requests the reader may want to reorder, hold back or add to before the agent reaches them.
  • Agents that sometimes need a decision first, where asking inline beats stopping the whole session.
  • A side panel next to the thing being built, about 340 to 420 px wide.

When not to use

  • A single request and its answer. A plain chat thread is enough.
  • Several agents working in parallel. Use lanes or a board, one column per agent.
  • Long-running jobs measured in hours, with retries and logs. Use a jobs table with statuses.
  • Questions that block everything else, like approving a payment. Use a modal approval step.

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

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

Terminal
npx shadcn@latest add https://brunodelorence.com/r/agent-queue.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 { AgentQueue, type Task } from './agent-queue';const tasks: Task[] = [  { id: 'nav', text: 'make the nav sticky on scroll', status: 'running' },  {    id: 'empty',    text: 'add an empty state to the projects list',    status: 'queued',    questions: [{ prompt: 'What should it show?', options: ['A short line', 'An illustration'] }],  },  { id: 'dark', text: 'check the cards in the dark theme', status: 'queued' },];export function AgentPanel() {  return (    <AgentQueue      tasks={tasks}      onAdd={(task) => console.log('add', task.text)}      onReorder={(ids) => console.log('order', ids)}      onAnswer={(task, answers) => console.log(task.id, answers)}      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.

The side panel of a coding agent that works through a queue of requests while you keep adding more. Each task hangs on a thin line with a dot: blue and shimmering while it runs, hollow while queued, orange when paused, violet when the agent needs answers, green once done. Drag a task and the others move out of its way; pause one to add context; when the agent reaches a task it can’t do yet, it asks a few questions inline and moves on to the next.

  • The agent is a timer. It takes the first queued task from the top, runs it for taskMs and moves it to the done group; it passes over paused tasks. A task with questions runs for a moment, then turns violet and opens its questions while the agent goes on with the next one.
  • Dragging measures every row once, when the drag starts. As the pointer moves, the order the rows would take is worked out from those measurements, and each row’s translate is set to the distance to its new slot, with a short transition: the others slide out of the way while the dragged row follows the hand. On release the order is committed, and the dragged row eases from where it was dropped into its slot.
  • The running task holds its slot: the new order is built from the movable slots only, so it cannot be dragged and nothing lands in its place. Alt and the arrow keys move a task the same way from the keyboard.
  • Rows that change place for any other reason, a task done or removed, a question card opening, slide from where they were: their old offsets are kept after each layout and played back with the Web Animations API on translate (FLIP).
  • The running task’s words are painted with a gradient clipped to the text, three times their width, whose position sweeps across them in a loop.
  • The done group folds with a grid row going from 1fr to 0fr while the pill is open, and the thread keeps to its bottom, like a chat, unless the reader scrolls up.
  • The demo types a task, drags it up two places, pauses another to add context and answers the agent’s three questions, then watches the queue finish. Any input of the reader’s stops it.
PropTypeDefaultDescription
tasksreadonly Task[]TASKSThe turn’s tasks, in queue order: an id, the text, a status (queued, running, done, paused, needs), an optional context line, and the questions the agent will ask before doing it. Done tasks go to the green group.
taskMsnumber5200How long the agent spends on a task, in ms.
workingbooleantrueWhether the agent works through the queue; false holds everything still.
previousstring | nullPREVIOUS_REPLYThe agent’s reply to the turn before, above the pill; null leaves it out.
placeholderstring'Ask the agent to do something'The composer’s placeholder.
defaultExpandedbooleanfalseOpens with the pill listing the whole turn.
defaultEditingstring—Opens with the context field of this paused task open (its id).
onAdd(task: Task) => void—A task was added from the composer, at the end of the queue.
onReorder(ids: string[]) => void—The reader moved a task: the queue’s ids in their new order.
onPause(task: Task) => void—The reader paused a task; the agent passes over it.
onResume(task: Task) => void—The reader let a paused task run again.
onContext(task: Task, context: string) => void—The reader added context to a task, shown as the indented line under it.
onRemove(task: Task) => void—The reader removed a task.
onAnswer(task: Task, answers: string[]) => void—The reader answered the agent’s questions about a task, one answer per question; the task is queued again.
autoplaybooleantrueWhile on screen and untouched, a demo pointer shows the queue 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.
classNamestring—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.

agent-queue.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useCallback,  useEffect,  useEffectEvent,  useId,  useLayoutEffect,  useRef,  useState,  type CSSProperties,  type KeyboardEvent as ReactKeyboardEvent,  type PointerEvent as ReactPointerEvent,} from 'react';import { DEMO_ANSWERS, DEMO_CONTEXT, DEMO_TASK, PREVIOUS_REPLY, TASKS, type Task } from './data';import { DemoPointer, pointerAt, runDemo, type Point } from './demo-pointer';import { Glyph } from './glyphs';import { useAutoplay } from './use-autoplay';import styles from './agent-queue.module.css';/* * Agent queue: the side panel of a coding agent, where the reader keeps lining up requests while
agent-queue.module.css
/* Agent queue (agent-queue.tsx): a light grey stage with a faint sketch of the app being built on   the left, bleeding off the edge, and the agent's panel on the right, 376 px wide: the thread   (the last reply, the pill, the done group, the queue) over the composer. Under 640 px of stage   the sketch goes and the panel takes the width.   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 and the sketch of the app. */  --stage: #f3f3f3;  --window: #ffffff;  --window-edge: #e6e6e6;  --canvas: #f6f6f5;  --skel: #e4e3e0;  --skel-2: #efeeec;  /* Surfaces: the pill, the composer, the question card and its choices. */  --surface: #ffffff;  --edge: #e5e5e5;  --edge-strong: #d4d4d4;  --hover: #fbfbfb;  --shadow: rgb(0 0 0 / 0.045);  --lift-shadow: rgb(0 0 0 / 0.1);
data.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The shapes the queue works with, and the queue the lab shows: a coding agent halfway through a * turn of small requests about the very panel it sits in. One is done, one runs, three wait, and * one of those will need answers before the agent can do it. *//** * Where a task stands. The agent runs one at a time, top to bottom; it skips a paused task, and * one it needs answers for, which it marks `needs` and leaves for the reader. */export type TaskStatus = 'queued' | 'running' | 'done' | 'paused' | 'needs';/** A question the agent asks before it can do a task: a few choices, or the reader’s own words. */export type Question = { prompt: string; options: readonly string[] };export type Task = {  id: string;  text: string;  status: TaskStatus;  /** What the reader added: shown as an indented line under the task. */  context?: string;  /** What the agent will ask once it reaches the task; once answered, the answers are its context. */
glyphs.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The small line glyphs of the queue and its composer, drawn on a 16 px grid in currentColor. * Every one is decorative: the button around it carries the name. */import type { ReactNode } from 'react';export type GlyphName =  | 'plus'  | 'attach'  | 'chevron'  | 'mic'  | 'send'  | 'pause'  | 'play'  | 'info'  | 'close'  | 'left'  | 'right'  | 'pulse'  | 'up'  | 'down'
demo-pointer.tsx
// 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 };/** The pointer's looks. */export type Glyph = 'arrow' | 'hand' | 'grab' | 'text';/** A glide's path and timing, for an animation that plays along with it. */
use-autoplay.ts
// 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 panel is a region named Agent; the done group and the queue are ordered lists. Each task is a button announced as a movable task, with its status and its place (“Queued, 3 of 5”).
  • From the keyboard, the arrow keys walk the tasks, Alt with Up or Down moves one past its neighbours (never past the running task), and Delete removes it. Moves, additions, pauses and the agent’s progress are announced in a polite live region.
  • Pause, info and remove appear on hover and on focus, are always shown on touch screens, and name the task they act on. On touch, a grip next to them starts a drag, so the panel still scrolls.
  • Pausing a task opens its context field; Enter keeps the words, Escape closes it and returns the focus to the task. The agent’s questions are a group named by their question, with the choices as toggle buttons, a field for the reader’s own answer, and Previous, Next and Submit.
  • Every control has a visible focus ring. Under reduced motion the demo, the shimmer and the rows’ sliding are off.
  • The sketch of the app and the demo pointer are decorative and hidden from assistive technology.