{"slug":"task-list","category":"agents","job":"oversee","added":"2026-11-04T14:20Z","title":"Task List","description":"A live, agent-authored plan — an ordered list of steps marked pending, in progress, or completed as the agent works through them.","url":"https://ai-patterns.app/patterns/agents/task-list","markdown":"https://ai-patterns.app/patterns/agents/task-list.md","json":"https://ai-patterns.app/api/patterns/agents/task-list","seenIn":[{"team":"Claude","source":"https://code.claude.com/docs/en/interactive-mode#task-list"},{"team":"Cursor","source":"https://cursor.com/changelog/1-2"}],"spec":"# Task List\n\n## Summary\nA compact, agent-authored plan: a short ordered list of steps the agent intends to take, each marked with a status glyph — a dashed ring while pending, a spinning arc while in progress, and a filled check once completed. It updates live as the agent works, so the user can see at a glance what's done, what's happening now, and what's next.\n\n## When to use\n- An agent is executing a multi-step task (coding, research, a migration) and has broken it into a plan up front.\n- The user benefits from seeing the remaining scope — to judge how long the run will take, or to interrupt before a step they disagree with.\n- Steps are coarse (3–8 items), each a meaningful unit of work rather than an individual tool call.\n\n## When not to use\n- A single undifferentiated operation — use Thinking Loader.\n- Showing the raw sequence of tool calls or reasoning — use Expandable Trace; a task list is the plan, not the log.\n- Steps the user must complete themselves — use Setup Checklist, which makes rows actionable.\n- Parallel sub-agents each with their own steps — use Multi-Agent Trace.\n\n## Anatomy\n- Container: a bordered, rounded card holding an ordered list.\n- Row: status glyph + label, top-aligned so wrapped labels stay readable.\n- Status glyph — pending: a dashed, muted ring (\"not started\", without implying failure).\n- Status glyph — in progress: a spinning partial arc over a faint track, in the foreground color.\n- Status glyph — completed: a filled muted circle with a check.\n- Label: one line where possible; the in-progress row is emphasised (medium weight) and completed rows fade to muted text.\n\n## Behavior\n- At most one task is in progress at a time in a sequential plan; the next pending task flips to in progress as the previous one completes.\n- Status changes swap the glyph with a short spring (scale + fade) in place — rows never reorder or shift.\n- The agent may revise the plan mid-run: new tasks are appended or inserted where they'll execute, and obsolete ones removed, without resetting completed rows.\n- Optionally show a present-continuous label (\"Running the build\") while a task is in progress, reverting to the imperative label (\"Run the build\") once done.\n- When every task is completed, the list stays visible as a record of what was done; no extra success state is needed.\n\n## Content guidelines\n- Write each label as an imperative verb phrase describing an outcome: \"Add a Task List pattern\", not \"Adding stuff\" or \"Step 2\".\n- Keep labels concrete and scoped — the user should be able to tell what changes when a row turns complete.\n- Prefer 3–8 tasks. Fewer means the plan isn't worth showing; more means the steps are too granular.\n- Don't put timing, file paths, or tool names in the label — those belong in the trace.\n\n## Accessibility\n- Render as an ordered list (`<ol>`) with an accessible name that includes progress (\"Agent plan, 1 of 3 completed\").\n- Each glyph is decorative (`aria-hidden`); expose status as visually hidden text before the label (\"Completed: …\", \"In progress: …\").\n- Mark the in-progress row with `aria-current=\"step\"`.\n- Don't make the whole list a live region — announce only meaningful transitions (a task completing) if needed, to avoid chatter.\n- Respect `prefers-reduced-motion`: stop the arc spinning; the partial arc alone still reads as \"in progress\".\n- Status is never conveyed by color alone — each state has a distinct glyph shape.\n\n## Related patterns\n- **Setup Checklist** — Use it instead when the rows are steps the person completes rather than steps the agent works through.\n- **Multi-Agent Trace** — Use it instead when the plan is split across parallel sub-agents.\n- **Expandable Trace** — Use it as well when people need the detailed log of what happened during each step.\n- **Background Tasks Drawer** — Use it instead when the work runs long and outside the conversation.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { Check } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"completed\";\n\nexport interface Task {\n  id: string;\n  /** Imperative label, e.g. \"Run the build\". */\n  label: string;\n  /** Optional present-continuous label shown while the task is in progress, e.g. \"Running the build\". */\n  activeLabel?: string;\n  status: TaskStatus;\n}\n\nexport interface TaskListProps {\n  tasks: Task[];\n  /** Accessible name for the list. */\n  label?: string;\n  className?: string;\n}\n\nconst STATUS_TEXT: Record<TaskStatus, string> = {\n  pending: \"Pending\",\n  in_progress: \"In progress\",\n  completed: \"Completed\",\n};\n\nexport function TaskList({ tasks, label = \"Task list\", className }: TaskListProps) {\n  const done = tasks.filter((t) => t.status === \"completed\").length;\n\n  return (\n    <div className={cn(\"w-full rounded-2xl border bg-card px-4 py-3\", className)}>\n      <ol aria-label={`${label}, ${done} of ${tasks.length} completed`} className=\"space-y-2.5\">\n        {tasks.map((task) => (\n          <TaskRow key={task.id} task={task} />\n        ))}\n      </ol>\n    </div>\n  );\n}\n\nfunction TaskRow({ task }: { task: Task }) {\n  const completed = task.status === \"completed\";\n  const active = task.status === \"in_progress\";\n  const text = active && task.activeLabel ? task.activeLabel : task.label;\n\n  return (\n    <li\n      aria-current={active ? \"step\" : undefined}\n      className=\"flex items-start gap-2.5\"\n    >\n      <span className=\"relative mt-px flex size-4 shrink-0 items-center justify-center\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            key={task.status}\n            initial={{ scale: 0.5, opacity: 0 }}\n            animate={{ scale: 1, opacity: 1 }}\n            exit={{ scale: 0.5, opacity: 0 }}\n            transition={{ type: \"spring\", stiffness: 420, damping: 26 }}\n            className=\"absolute inset-0 flex items-center justify-center\"\n          >\n            <StatusIcon status={task.status} />\n          </motion.span>\n        </AnimatePresence>\n        <span className=\"sr-only\">{STATUS_TEXT[task.status]}: </span>\n      </span>\n\n      <span\n        className={cn(\n          \"min-w-0 flex-1 text-sm leading-snug transition-colors duration-300\",\n          completed ? \"text-muted-foreground\" : \"text-foreground\",\n          active && \"font-medium\"\n        )}\n      >\n        {text}\n      </span>\n    </li>\n  );\n}\n\nfunction StatusIcon({ status }: { status: TaskStatus }) {\n  if (status === \"completed\") {\n    return (\n      <span className=\"flex size-4 items-center justify-center rounded-full bg-muted-foreground\" aria-hidden>\n        <Check className=\"size-2.5 text-background\" strokeWidth={3.5} />\n      </span>\n    );\n  }\n\n  if (status === \"in_progress\") {\n    return (\n      // A CSS spin, not a Motion `animate`: inside `AnimatePresence initial={false}`\n      // Motion skips the first animation of every nested motion element, so a\n      // task that is already in progress on mount would never start spinning.\n      <svg\n        viewBox=\"0 0 16 16\"\n        className=\"size-4 text-foreground motion-safe:animate-spin motion-safe:[animation-duration:0.9s]\"\n        fill=\"none\"\n        aria-hidden\n      >\n        <circle cx=\"8\" cy=\"8\" r=\"6.25\" stroke=\"currentColor\" strokeOpacity={0.15} strokeWidth=\"1.5\" />\n        <path d=\"M8 1.75a6.25 6.25 0 0 1 6.25 6.25\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n      </svg>\n    );\n  }\n\n  return (\n    <svg viewBox=\"0 0 16 16\" className=\"size-4 text-muted-foreground/60\" fill=\"none\" aria-hidden>\n      <circle\n        cx=\"8\"\n        cy=\"8\"\n        r=\"6.25\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.25\"\n        strokeDasharray=\"2 2\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n","dependencies":["lucide-react","motion"],"localImports":["@/lib/utils"]}