Skip to content
AI Interfaces block

Live Agent Task List

Run status as rows — done, running, failed, queued — with native progress elements, per-task retry buttons that name their task, and progress announced from one small status region instead of every row.

234 lineslucide-reactAdded 13 Aug 2026
  • tasks
  • run status
  • progress
  • queue
  • agent

What's included

  • components/agent-task-list.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add agent-task-list

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Quarterly churn review

2 of 5 complete · 1 failed

  • Complete: Pull cancellations for Q3

    2,481 rows from warehouse.subscriptions

    2.7s
  • Complete: Join to account seat counts

    Matched 2,463 of 2,481 — 18 accounts deleted

    1.4s
  • Failed: Fetch exit-survey responses

    CRM returned 429 — rate limited after 40 of 300

  • Running: Score accounts by churn risk

    Running the model over the joined cohort

    62%
  • Queued: Draft the summary

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/agent-task-list.tsx
/**
 * <AgentTaskList> — the run queue: what the agent is doing now, what it has
 * finished, and what failed.
 *
 * The block exists because a long-running agent is otherwise invisible. A
 * user who kicked off a job and sees nothing will start it again, and the
 * second run is usually the one that does damage.
 *
 * Live-region handling, which is the hard part:
 *
 *  - The list is `aria-live="polite"` but `aria-relevant="additions"`, so a
 *    task changing from Running to Done does not re-read every row. Only a
 *    genuinely new task is announced.
 *  - The *summary* line above the list is a `role="status"` and is where
 *    "3 of 5 complete" lives. Progress belongs in one small region that
 *    changes rarely, not scattered across rows that change constantly.
 *  - A failed row exposes its retry as a real button with the task name in
 *    its accessible name — five buttons all called "Retry" is a menu nobody
 *    can use.
 *  - `<progress>` is the native element, so the value is exposed without
 *    `role="progressbar"` and its ARIA triplet. Its bar is restyled but its
 *    semantics are untouched.
 */

'use client'

import * as React from 'react'
import { AlertCircle, Check, Loader2, Pause, Play, RotateCw, Square } from 'lucide-react'

export type TaskState = 'done' | 'running' | 'failed' | 'queued'

export interface AgentTask {
  id: string
  title: string
  detail?: string
  state: TaskState
  /** 0–100, only meaningful while running. */
  progress?: number
  duration?: string
}

export interface AgentTaskListProps {
  heading?: string
  tasks?: AgentTask[]
  className?: string
}

const DEFAULT_TASKS: AgentTask[] = [
  {
    id: '1',
    title: 'Pull cancellations for Q3',
    detail: '2,481 rows from warehouse.subscriptions',
    state: 'done',
    duration: '2.7s',
  },
  {
    id: '2',
    title: 'Join to account seat counts',
    detail: 'Matched 2,463 of 2,481 — 18 accounts deleted',
    state: 'done',
    duration: '1.4s',
  },
  {
    id: '3',
    title: 'Fetch exit-survey responses',
    detail: 'CRM returned 429 — rate limited after 40 of 300',
    state: 'failed',
    duration: '12.0s',
  },
  {
    id: '4',
    title: 'Score accounts by churn risk',
    detail: 'Running the model over the joined cohort',
    state: 'running',
    progress: 62,
  },
  {
    id: '5',
    title: 'Draft the summary',
    state: 'queued',
  },
]

const STATE_LABEL = {
  done: 'Complete',
  running: 'Running',
  failed: 'Failed',
  queued: 'Queued',
} as const

export function AgentTaskList({
  heading = 'Quarterly churn review',
  tasks = DEFAULT_TASKS,
  className = '',
}: AgentTaskListProps) {
  const [paused, setPaused] = React.useState(false)

  const done = tasks.filter((t) => t.state === 'done').length
  const failed = tasks.filter((t) => t.state === 'failed').length
  const active = tasks.some((t) => t.state === 'running')

  return (
    <div className={`mx-auto w-full max-w-2xl p-6 ${className}`}>
      <div
        aria-busy={active && !paused}
        className="overflow-y-hidden rounded-2xl border border-border/60 bg-card"
      >
        {/* -- Header ---------------------------------------------------- */}
        <div className="flex items-center gap-3 border-b border-border/60 px-4 py-3">
          <div className="min-w-0 flex-1">
            {heading ? <h3 data-stress-ignore className="truncate text-sm font-semibold">{heading}</h3> : null}

            {/* The one place progress is announced. */}
            <p role="status" className="mt-0.5 text-xs text-muted-foreground">
              {done} of {tasks.length} complete
              {failed > 0 ? ` · ${failed} failed` : ''}
              {paused ? ' · paused' : ''}
            </p>
          </div>

          <button
            type="button"
            onClick={() => setPaused((v) => !v)}
            className="inline-flex items-center gap-1.5 rounded-lg border border-border/60 px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
          >
            {paused ? (
              <Play aria-hidden className="h-3.5 w-3.5" />
            ) : (
              <Pause aria-hidden className="h-3.5 w-3.5" />
            )}
            {paused ? 'Resume' : 'Pause'}
          </button>

          <button
            type="button"
            className="inline-flex items-center gap-1.5 rounded-lg border border-border/60 px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
          >
            <Square aria-hidden className="h-3 w-3" />
            Stop
          </button>
        </div>

        {/* -- Rows ------------------------------------------------------ */}
        <ul
          aria-live="polite"
          aria-relevant="additions"
          aria-label="Tasks"
          className="divide-y divide-border/40"
        >
          {tasks.map((task) => (
            <li key={task.id} className="flex items-start gap-3 px-4 py-3">
              <StateIcon state={task.state} paused={paused} />

              <div className="min-w-0 flex-1">
                <p
                  className={`text-sm ${
                    task.state === 'queued' ? 'text-muted-foreground' : 'font-medium'
                  }`}
                >
                  <span className="sr-only">{STATE_LABEL[task.state]}: </span>
                  {task.title}
                </p>

                {task.detail ? (
                  <p
                    className={`mt-0.5 truncate text-xs ${
                      task.state === 'failed' ? 'text-rose-500' : 'text-muted-foreground'
                    }`}
                  >
                    {task.detail}
                  </p>
                ) : null}

                {task.state === 'running' && typeof task.progress === 'number' ? (
                  <progress
                    value={task.progress}
                    max={100}
                    aria-label={`${task.title} progress`}
                    className="mt-2 h-1 w-full appearance-none overflow-hidden rounded-full [&::-moz-progress-bar]:bg-primary [&::-webkit-progress-bar]:rounded-full [&::-webkit-progress-bar]:bg-muted [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:bg-primary"
                  >
                    {task.progress}%
                  </progress>
                ) : null}
              </div>

              {task.state === 'failed' ? (
                <button
                  type="button"
                  className="inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border/60 px-2.5 py-1 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                >
                  <RotateCw aria-hidden className="h-3 w-3" />
                  Retry
                  {/* Distinguishes this Retry from every other Retry. */}
                  <span className="sr-only"> {task.title}</span>
                </button>
              ) : task.duration ? (
                <span className="shrink-0 pt-0.5 font-mono text-[11px] text-muted-foreground">
                  {task.duration}
                </span>
              ) : null}
            </li>
          ))}
        </ul>
      </div>
    </div>
  )
}

function StateIcon({ state, paused }: { state: TaskState; paused: boolean }) {
  const base = 'mt-0.5 h-4 w-4 shrink-0'

  if (state === 'done') return <Check aria-hidden className={`${base} text-emerald-500`} />
  if (state === 'failed') return <AlertCircle aria-hidden className={`${base} text-rose-500`} />
  if (state === 'queued') {
    return (
      <span
        aria-hidden
        className={`${base} rounded-full border-2 border-dashed border-muted-foreground/40`}
      />
    )
  }

  return (
    <Loader2
      aria-hidden
      // Slowed, not stopped, under reduced motion — and genuinely stopped
      // when the run is paused, where a still icon is the correct signal.
      className={`${base} text-primary motion-reduce:[animation-duration:2.4s] ${
        paused ? '' : 'animate-spin'
      }`}
    />
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/agent-task-list.tsx and import it where you need the section:

import { AgentTaskList } from '@/components/agent-task-list'

Customize

1 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
headingstring'Quarterly churn review'
tasksAgentTask[]DEFAULT_TASKS
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

agent-task-list.html
<!--
  Live Agent Task List — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<div class="mx-auto w-full max-w-2xl p-6 ">
  <div aria-busy="true" class="overflow-y-hidden rounded-2xl border border-border/60 bg-card">
    <div class="flex items-center gap-3 border-b border-border/60 px-4 py-3">
      <div class="min-w-0 flex-1">
        <h3 data-stress-ignore="true" class="truncate text-sm font-semibold">Quarterly churn review</h3>
        <p role="status" class="mt-0.5 text-xs text-muted-foreground">2 of 5 complete · 1 failed</p>
      </div>
      <button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-border/60 px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pause h-3.5 w-3.5" aria-hidden="true"><rect x="14" y="4" width="4" height="16" rx="1"></rect><rect x="6" y="4" width="4" height="16" rx="1"></rect></svg>Pause</button>
      <button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-border/60 px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square h-3 w-3" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2"></rect></svg>Stop</button>
    </div>
    <ul aria-live="polite" aria-relevant="additions" aria-label="Tasks" class="divide-y divide-border/40">
      <li class="flex items-start gap-3 px-4 py-3">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check mt-0.5 h-4 w-4 shrink-0 text-emerald-500" aria-hidden="true">
          <path d="M20 6 9 17l-5-5"></path>
        </svg>
        <div class="min-w-0 flex-1">
          <p class="text-sm font-medium"><span class="sr-only">Complete: </span>Pull cancellations for Q3</p>
          <p class="mt-0.5 truncate text-xs text-muted-foreground">2,481 rows from warehouse.subscriptions</p>
        </div>
        <span class="shrink-0 pt-0.5 font-mono text-[11px] text-muted-foreground">2.7s</span>
      </li>
      <li class="flex items-start gap-3 px-4 py-3">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check mt-0.5 h-4 w-4 shrink-0 text-emerald-500" aria-hidden="true">
          <path d="M20 6 9 17l-5-5"></path>
        </svg>
        <div class="min-w-0 flex-1">
          <p class="text-sm font-medium"><span class="sr-only">Complete: </span>Join to account seat counts</p>
          <p class="mt-0.5 truncate text-xs text-muted-foreground">Matched 2,463 of 2,481 — 18 accounts deleted</p>
        </div>
        <span class="shrink-0 pt-0.5 font-mono text-[11px] text-muted-foreground">1.4s</span>
      </li>
      <li class="flex items-start gap-3 px-4 py-3">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-alert mt-0.5 h-4 w-4 shrink-0 text-rose-500" aria-hidden="true">
          <circle cx="12" cy="12" r="10"></circle>
          <line x1="12" x2="12" y1="8" y2="12"></line>
          <line x1="12" x2="12.01" y1="16" y2="16"></line>
        </svg>
        <div class="min-w-0 flex-1">
          <p class="text-sm font-medium"><span class="sr-only">Failed: </span>Fetch exit-survey responses</p>
          <p class="mt-0.5 truncate text-xs text-rose-500">CRM returned 429 — rate limited after 40 of 300</p>
        </div>
        <button type="button" class="inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border/60 px-2.5 py-1 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw h-3 w-3" aria-hidden="true"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path></svg>Retry<span class="sr-only"> Fetch exit-survey responses</span></button>
      </li>
      <li class="flex items-start gap-3 px-4 py-3">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-loader-circle mt-0.5 h-4 w-4 shrink-0 text-primary motion-reduce:[animation-duration:2.4s] animate-spin" aria-hidden="true">
          <path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
        </svg>
        <div class="min-w-0 flex-1">
          <p class="text-sm font-medium"><span class="sr-only">Running: </span>Score accounts by churn risk</p>
          <p class="mt-0.5 truncate text-xs text-muted-foreground">Running the model over the joined cohort</p>
          <progress value="62" max="100" aria-label="Score accounts by churn risk progress" class="mt-2 h-1 w-full appearance-none overflow-hidden rounded-full [&amp;amp;::-moz-progress-bar]:bg-primary [&amp;amp;::-webkit-progress-bar]:rounded-full [&amp;amp;::-webkit-progress-bar]:bg-muted [&amp;amp;::-webkit-progress-value]:rounded-full [&amp;amp;::-webkit-progress-value]:bg-primary">62%</progress>
        </div>
      </li>
      <li class="flex items-start gap-3 px-4 py-3">
        <span aria-hidden="true" class="mt-0.5 h-4 w-4 shrink-0 rounded-full border-2 border-dashed border-muted-foreground/40"></span>
        <div class="min-w-0 flex-1">
          <p class="text-sm text-muted-foreground"><span class="sr-only">Queued: </span>Draft the summary</p>
        </div>
      </li>
    </ul>
  </div>
</div>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

6 more blocks in Agent Reasoning

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Agent Reasoning

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Agent Reasoning blocks

View category
Open the full page for this block

Collapsible Thinking Trace

The "thought for 5s" panel: a native details/summary outside, individually expandable steps inside — buttons rather than nested disclosures, which are announced as a disclosure within a disclosure.

Agent Reasoning256 lines1 dep
Open the full page for this block

Tool Call Chips

Dense tool invocations that open into their arguments and result, with status carried by icon and word rather than colour, and errors marked as alerts only once the user opens them.

Agent Reasoning258 lines1 dep
Open the full page for this block

Working Indicator with Elapsed Time

The gap before the first token: a shimmer grid, a phase label that advances as the wait lengthens, and a running counter — which is the only honest answer to "is it stuck". The timer is hidden from the live region on purpose.

Agent Reasoning120 lines1 dep
Open the full page for this block

Failed Run Trace

The trace after something broke: retries counted in the open with what each one cost, a step that was silently worked around drawn in amber rather than green, and the raw error kept apart from the agent’s reading of it.

Agent Reasoning337 lines1 dep
Open the full page for this block

Agent Retry Log

What the agent tried, what failed, and what it did about it — the run history a user needs before they trust a second attempt.

Agent Reasoning153 linesNo deps
Open the full page for this block

Agent Cost Breakdown

What one run cost, per step, so an expensive answer is attributable rather than a monthly surprise.

Agent Reasoning149 linesNo deps