Todo List

The agent's plan as one line above the composer: the live task and a count, opening into the whole checklist.

Open in

Installation

npx shadcn@latest add miskibin/chat-components/todo-list

Also installs collapsible.

Usage

"use client"

import { TodoPanel, parseTodoItems } from "@/components/ui/todo-list"

export function Plan({ toolInput }: { toolInput?: string }) {
  const items = parseTodoItems(toolInput) ?? []

  return <TodoPanel items={items} />
}

Examples

Above the composer

The panel carries the same max-w-3xl measure as ChatInput, so the two stack without a call-site override. parseTodoItems turns a todo tool call's arguments straight into the list.

When the turn is over

running is what tells the bar the turn behind it has settled. An agent that stops without writing its last step off is the common case, and a spinner that outlives the run reads as work still happening — so an unfinished step goes quiet, and a plan whose steps are all done says Plan done. Live, abandoned, and finished, in that order.

  1. Read the failing test
  2. Patch applyPatch
  3. Re-run the suite
  1. Read the failing test
  2. Patch applyPatch
  3. Re-run the suite
  1. Read the failing test
  2. Patch applyPatch
  3. Re-run the suite

The checklist alone

TodoList is the list with no chrome — for a plan that is already framed by something else, like a tool row or a sidebar section.

  1. Reproduce the failing turn
  2. Bisect the reducer
  3. Add a regression test

Notes

API reference

TodoPanel

items*TodoItem[]
The plan, in order. An empty array renders nothing at all.
defaultOpenboolean = false
Start expanded. Collapsed, the panel is a single status line.
openboolean
Controlled open state; pair with onOpenChange.
onOpenChange(open: boolean) => void
Fired when the disclosure is toggled.
labelReactNode
Replaces the collapsed headline, which is otherwise the in-progress task (or the next unfinished one).
runningboolean = true
False once the turn behind the plan has settled: nothing spins, and a plan with every step done reads as done.

TodoList

items*TodoItem[]
The same list, rendered as a bare ordered list.
runningboolean = true
False draws an unfinished step as the step nobody got to, instead of one still being worked on.

Helpers

parseTodoItems(input?: string | null) => TodoItem[] | null
Parses a todo tool call's JSON arguments into items. Null when the payload holds no list.
isTodoToolName(name: string) => boolean
True for todo_write, TodoWrite, write_todos, update_plan and the like.
todoProgress(items: TodoItem[]) => { completed, total, active, current, done }
Counts plus the in-progress item and the one worth naming in a single line.
normalizeTodoStatus(value: unknown) => TodoStatus
Folds any spelling onto pending | in_progress | completed.

Data slots

Every part below is addressable from a parent, so you can restyle internals without forking the component.

todo-paneltodo-panel-roottodo-panel-triggertodo-panel-headlinetodo-panel-counttodo-panel-listtodo-listtodo-item