24 / Agent UI
Agent Checklist
A task list whose items change state as work proceeds, announcing what changed instead of re-reading the whole list.
Fixing the migration
0/4- Read the migration, in progress
- Check the column constraints, waiting
- Reorder the statements, waiting
- Run the test suite, waiting
"use client"
import { DemoVariants } from "@/components/demos/demo-variants"
import { RestartButton } from "@/components/demos/restart-button"
import {
useScriptedTimeline,
type TimelineStep,
} from "@/components/demos/use-scripted-timeline"
import {
AgentChecklist,
type AgentChecklistItem,
} from "mischief-ui/agent-checklist"
const labels = [
"Read the migration",
"Check the column constraints",
"Reorder the statements",
"Run the test suite",
]
function frame(activeIndex: number): AgentChecklistItem[] {
return labels.map((label, index) => ({
id: String(index),
label,
status:
index < activeIndex
? "done"
: index === activeIndex
? "active"
: "pending",
}))
}
const steps: [
TimelineStep<AgentChecklistItem[]>,
...TimelineStep<AgentChecklistItem[]>[],
] = [
{ state: frame(0), holdMs: 1600 },
{ state: frame(1), holdMs: 1600 },
{ state: frame(2), holdMs: 1600 },
{ state: frame(3), holdMs: 1600 },
{ state: frame(4), holdMs: 0 },
]
function LiveRun() {
const { state, isFinished, restart, runId } = useScriptedTimeline(steps)
return (
<div className="grid gap-4">
<AgentChecklist key={runId} items={state} title="Fixing the migration" />
{isFinished ? <RestartButton onClick={restart} /> : null}
</div>
)
}
export function AgentChecklistDemo() {
return (
<DemoVariants
label="Checklist state"
variants={[
{ id: "live", label: "Live run", render: () => <LiveRun /> },
{
id: "failure",
label: "With a failure",
render: () => (
<AgentChecklist
title="Fixing the migration"
items={[
{ id: "0", label: labels[0], status: "done" },
{ id: "1", label: labels[1], status: "done" },
{
id: "2",
label: labels[2],
status: "error",
detail: "Permission denied on migrations/.",
},
{ id: "3", label: labels[3], status: "skipped" },
]}
/>
),
},
]}
/>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/agent-checklistimport { AgentChecklist } from "mischief-ui/agent-checklist"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { Check, Circle, Loader, Minus, TriangleAlert } from "lucide-react"import { cn } from "@/lib/utils" export type ChecklistItemStatus = "pending" | "active" | "done" | "error" | "skipped" export type AgentChecklistItem = { id: string label: React.ReactNode status: ChecklistItemStatus detail?: React.ReactNodeUsage
const items = [
{ id: "read", label: "Read the changelog", status: "done" },
{ id: "diff", label: "Compare versions", status: "active" },
{ id: "write", label: "Draft the summary", status: "pending" },
]
export function Plan() {
return <AgentChecklist items={items} title="Plan" />
}The five states
Every item is in exactly one state, and the wording each maps to is what a screen reader hears alongside the label.
| Status | Read as |
|---|---|
pending | waiting |
active | in progress |
done | done |
error | failed |
skipped | skipped |
skipped exists so a plan that changed does not have to lie. An agent that decided a step was unnecessary should mark it skipped rather than done, which is the difference between a truthful record and a tidy one.
Announcing progress
With announce on, each change is read out as it happens. That is genuinely helpful for a plan of five or six steps and unbearable for a plan of forty, so turn it off for long lists and let the progress count carry the story instead.
Write labels as the thing being done, short enough to be heard in one breath: Reading the invoice, not Now attempting to read the uploaded invoice document. Detail is for detail.
<AgentChecklist
title="Extracting the invoice"
items={[
{ id: "read", label: "Reading the file", status: "done" },
{ id: "fields", label: "Finding the fields", status: "active" },
{ id: "verify", label: "Checking the totals", status: "pending" },
]}
/>API
itemsAgentChecklistItem[]Id, label, status, and optional detail per step. Fully controlled.titleReactNodeAn optional heading above the list.announcebooleanAnnounces status transitions politely. Defaults to true.showProgressbooleanShows the settled count in the header. Defaults to true.AgentChecklistItem
idstringUnique within the list.labelReactNodeThe step, phrased as the thing being done.statusChecklistItemStatuspending, active, done, error, or skipped.detailReactNodeA second line, for what the step actually found.Accessibility
The list is an ordered list and every item states its status in text for screen readers, not through colour or icon alone. When a status changes, only the difference is announced along with a running count, so a long list does not get re-read on every update. Spinners stop under reduced motion.