65 / 122 · Agents
Todo list
The agent's own working list, rewritten mid-run as it discovers what else is needed.
Todos0/3 · rev 1
Installation
npx shadcn@latest add "@assistant-ui/elements-todo-list"
Usage
import { TodoList } from "@/components/elements/todo-list";
<TodoList
items={[
{ id: "read", text: "Read the failing test", status: "done" },
{ id: "fix", text: "Fix the converter", status: "active" },
{ id: "verify", text: "Re-run the suite", status: "pending" },
]}
revision={2}
/>Props
TodoList
items*TodoItem[]The list as it stands right now. Each item carries its own status, so the agent can add, reorder, and complete items in any order between renders.
revisionnumberWhich rewrite of the list this is. Omit to show a plain done/total count instead.
classNamestringExtra classes merged onto the root.
TodoItem
id*stringStable identity across revisions. Reusing an id keeps an item in place while its text or status changes.
text*stringThe item as the agent phrased it.
status*"pending" | "active" | "done"Per-item state. More than one item may be active if the agent works in parallel.
Source
todo-list.tsx"use client";
import { CheckIcon, Loader2Icon } from "lucide-react";
import { cn } from "@/lib/utils";
import { mono } from "./surfaces";
export type TodoStatus = "pending" | "active" | "done";
export interface TodoItem {
id: string;
text: string;
status: TodoStatus;
}
export function TodoList({
items,
revision,
className,
}: {
items: readonly TodoItem[];
revision?: number;
className?: string;
}) {
const done = items.filter((item) => item.status === "done").length;
return (
<div className={cn("flex w-full max-w-sm flex-col gap-3", className)}>
<div className="flex items-baseline justify-between">
<span className="text-[13.5px] font-medium">Todos</span>
<span className={cn(mono, "text-foreground/35 tabular-nums")}>
{revision === undefined
? `${done}/${items.length}`
: `${done}/${items.length} · rev ${revision}`}
</span>
</div>
<ul className="flex flex-col gap-1">
{items.map((item) => (
<li
key={item.id}
className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both flex items-start gap-2.5 py-0.5 text-[13.5px] duration-300"
>
<span className="flex size-4 h-5 shrink-0 items-center justify-center">
{item.status === "done" ? (
<span className="border-foreground/20 bg-foreground/[0.06] flex size-3.5 items-center justify-center rounded-[5px] border">
<CheckIcon className="text-foreground/45 size-2.5" />
</span>
) : item.status === "active" ? (
<Loader2Icon className="size-3.5 animate-spin text-blue-500 motion-reduce:animate-none dark:text-blue-400" />
) : (
<span
aria-hidden
className="border-foreground/15 size-3.5 rounded-[5px] border"
/>
)}
</span>
<span
className={cn(
"leading-5",
item.status === "done" &&
"text-foreground/35 line-through decoration-[1.5px]",
item.status === "active" && "text-foreground/90",
item.status === "pending" && "text-foreground/50",
)}
>
{item.text}
</span>
</li>
))}
</ul>
</div>
);
}