Elements

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
  • Read the failing test
  • Fix the converter
  • Re-run the suite

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>
  );
}