HextaUI

Agent Todos

Show an agent’s plan as it works. Every step moves from backlog to to do, in progress and done, with live timings, failures and the tool calls behind it. A status pill for above the composer, plan changes you can see, and a review step to edit the plan before it runs.

Coding agents like Claude Code, Codex and Gemini CLI keep a written plan while they work, through tools such as TodoWrite, update_plan and write_todos. Agent Todos turns that plan into one line per step, with the step in progress spelled out (“Running the tests”) and how long each one took.

Every step moves from backlog to to do, in progress, and then done, failed or cancelled. When the agent rewrites its plan, new steps highlight, removed ones collapse out and a short note says what changed. A failed step keeps its reason in view, and long runs of finished steps fold away so the current work stays on screen.

AgentTodosStatus puts the current step and a count in one line above the composer, and AgentTodosReview lets people edit, reorder, add and remove steps before anything runs, then start with ⌘↵. Adapters read each agent’s tool format, including input that is still streaming, and Tool Calls can show the work inside any step.

  1. Add the Pro registry to components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Add your token

    Create a token on your account page and put it in .env.local as HEXTAUI_PRO_TOKEN.

  3. Add the block

    pnpm dlx shadcn@latest add @hextaui-pro/agent-todos

With the AI SDK

getTodosFromParts reads the latest TodoWrite, update_plan or write_todos call in a message, even while its input is still streaming. Show the list in the message and the status pill above the composer.

"use client"

import { useChat } from "@ai-sdk/react"

import { AgentTodos, AgentTodosStatus } from "@/components/blocks/agent-todos/agent-todos"
import { getTodosFromParts } from "@/components/blocks/agent-todos/todos"

export function Chat() {
  const { messages, status } = useChat()
  const busy = status === "submitted" || status === "streaming"
  const last = messages.at(-1)
  const current = last?.role === "assistant" ? getTodosFromParts(last.parts) : null

  return (
    <div className="flex flex-col gap-4">
      {messages.map((message) => {
        const plan = message.role === "assistant" ? getTodosFromParts(message.parts) : null
        return plan ? (
          <AgentTodos
            key={message.id}
            todos={plan.todos}
            explanation={plan.explanation}
            running={busy && message.id === last?.id}
          />
        ) : null
      })}
      {current ? <AgentTodosStatus todos={current.todos} running={busy} /> : null}
    </div>
  )
}

Review before running

Let people edit, reorder, add and remove steps before the agent starts. Run plan or ⌘↵ hands back the cleaned-up list.

"use client"

import * as React from "react"

import { AgentTodosReview } from "@/components/blocks/agent-todos/todo-review"
import type { Todo } from "@/components/blocks/agent-todos/todos"

export function PlanApproval({
  proposed,
  onRun,
}: {
  proposed: Todo[]
  onRun: (plan: Todo[]) => void
}) {
  const [draft, setDraft] = React.useState(proposed)

  return <AgentTodosReview todos={draft} onChange={setDraft} onApprove={onRun} />
}

With incremental task tools

For tools that create and update one task at a time, like Claude Code’s TaskCreate and TaskUpdate, fold each call into the list with applyTaskEvent.

"use client"

import * as React from "react"

import { AgentTodos } from "@/components/blocks/agent-todos/agent-todos"
import { applyTaskEvent, type TaskEvent, type Todo } from "@/components/blocks/agent-todos/todos"

export function useTaskList() {
  const [todos, setTodos] = React.useState<Todo[]>([])
  const apply = React.useCallback(
    (event: TaskEvent) => setTodos((current) => applyTaskEvent(current, event)),
    []
  )
  return { todos, apply }
}

export function Tasks({ todos, running }: { todos: Todo[]; running: boolean }) {
  return <AgentTodos todos={todos} running={running} />
}

Anatomy

The parts you compose, from the outside in.

PartDescription
AgentTodosThe full list with its header, progress bar, explanation and plan-change note.
AgentTodosStatusA one-line pill with the current step, opening the list in a popover.
AgentTodosReviewThe editable plan shown before the agent starts.
TodoMarkThe status mark on its own, for custom layouts.
getTodosFromParts, fromTodoWrite, fromUpdatePlan, fromWriteTodos, applyTaskEventAdapters from each agent’s tool format to Todo items.
PropTypeDefault
todosSteps in order.
Todo[]–
explanationWhy the plan looks the way it does, shown under the progress bar. Codex sends this with update_plan.
string–
runningWhether the agent is working. Defaults to true while any step is in progress.
boolean–
titleHeader label and region name.
string"Tasks"
collapsibleLet the header fold the list away.
booleantrue
defaultOpenStart open.
booleantrue
foldAfterFold the finished steps at the top once the list is longer than this.
number6
PropTypeDefault
idStable id. Plan changes are detected by id.
string–
contentWhat to do, in the imperative: “Run the tests”.
string–
activeFormWhat it’s doing, shown while in progress: “Running the tests”.
string–
statusThe stage. pending reads as To do.
"backlog" | "pending" | "in_progress" | "completed" | "failed" | "cancelled"–
errorShown under a failed step.
string–
durationStored seconds, for history.
number–
detailsOpens under the step, usually Tool Calls.
ReactNode–
PropTypeDefault
todosThe same list. Renders nothing when empty.
Todo[]–
runningAnimates the mark and shimmer while true.
boolean–
explanationPassed to the list in the popover.
string–
PropTypeDefault
todosThe proposed plan.
Todo[]–
onChangeCalled on every edit.
(todos: Todo[]) => void–
onApproveCalled from Run plan or ⌘↵, with empty steps removed and text trimmed.
(todos: Todo[]) => void–
onCancelShows Cancel and handles Escape.
() => void–
titleHeading.
string"Review the plan"
approveLabelButton label.
string"Run plan"

getTodosFromParts(parts)

Returns { todos, explanation? } from the newest TodoWrite, update_plan or write_todos call in an AI SDK message, or null.

PropTypeDefault
partsWorks on static and dynamic tool parts, including input that is still streaming.
UIMessage["parts"]–

applyTaskEvent(todos, event)

Folds one TaskCreate or TaskUpdate call into the list. A status of deleted removes the task.

PropTypeDefault
eventThe call, normalised.
{ type: "create", id, subject, activeForm? } | { type: "update", id, status?, subject?, activeForm? }–
KeyAction
EnterSpaceOpens or folds the list from its header, or a step’s details.
⌘↵Runs the plan while it’s being reviewed, unless you’re typing in another field.
EnterIn review, adds a step below the current one.
⌫In review, removes an empty step and moves to the one above.
⌥↑In review, moves the step up. ⌥↓ moves it down.
↑↓In review, moves between steps.
EscCancels the review when onCancel is set.
  • The list is a labelled region with an ordered list, and each step’s name includes its stage, such as “Running the tests, In progress”.
  • A hidden progressbar reports how many steps are done.
  • Starting, finishing and failing a step are announced through a polite live region, along with plan changes. Timer ticks are not announced.
  • The status pill’s label reads the current step and the count, and it opens the list in a focus-managed popover.
  • Review fields are labelled by step number, moves are announced (“Moved to step 2 of 5”), and Enter waits while an input method is composing.
  • With reduced motion, the in-progress ring stops spinning, checks appear without drawing, and steps slide into place instantly.

Code

4 files, added to components/blocks/agent-todos.