Agent plan
A checklist the agent works through, with progress you can glance.
- Read existing composer state in progressTrace the current data flow before changing it.
- Design the draft store not started
- Wire runtime persistence not started
- Add regression tests not started
- Update the docs not started
Installation
Install manually
npx shadcn@latest add "@assistant-ui/elements-agent-plan"First time? Set up a runtime
Runtime components read their state from an assistant-ui runtime. Add one to an existing project:
npx assistant-ui@latest initThen wrap your app in a runtime provider:
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";
export default function App() {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({ api: "/api/chat" }),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
{/* your components */}
</AssistantRuntimeProvider>
);
}The installation guide covers new projects, templates, and API routes.
npx shadcn@latest add "@assistant-ui/elements-agent-plan"Props-driven: no runtime or provider required.
A checklist with a title, header count, accessible progress bar, and each step marked done, active, or still ahead. With a runtime the plan comes from a tool call the model drives; standalone you hold the step list and the active index yourself.
Getting started
A plan like this is a natural shape for a tool call: the model calls a tool with the full step list and the index of the step it is currently on.
Render the tool call
Register a toolkit entry whose render maps the call straight onto AgentPlan. The title, step list, and active index come from args, so the card needs nothing beyond what the model already sent.
"use client";
import { defineToolkit } from "@assistant-ui/react";
import { AgentPlan } from "@/components/assistant-ui/elements/agent-plan";
export const toolkit = defineToolkit({
update_plan: {
type: "backend",
render: ({ args }) => (
<AgentPlan
title={args.title}
steps={args.steps}
activeIndex={args.activeIndex}
/>
),
},
});The model calls update_plan again each time it checks a step off, so a plan reads down the transcript as a short sequence of cards rather than one card mutating in place. The schema and the tool that produces these calls live on your server; only the renderer is shown here.
Register the toolkit
import { AssistantRuntimeProvider, AuiConfig, Tools } from "@assistant-ui/react";
import { toolkit } from "./toolkit";
const config = AuiConfig({ tools: Tools({ toolkit }) });
export function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
return (
<AssistantRuntimeProvider runtime={runtime} config={config}>
{children}
</AssistantRuntimeProvider>
);
}Every update_plan call now renders as an AgentPlan card wherever it lands in the message.
Standalone, the element is a controlled display: you own the step list and move activeIndex forward as work completes.
Hold the plan state
"use client";
import { useState } from "react";
import { AgentPlan } from "@/components/assistant-ui/elements/agent-plan";
const STEPS = [
{ id: "read", label: "Read existing composer state" },
{
id: "design",
label: "Design the draft store",
description: "Keep pending changes local until they are ready.",
},
{ id: "wire", label: "Wire runtime persistence" },
{ id: "test", label: "Add regression tests" },
{ id: "docs", label: "Update the docs" },
];
export function Plan() {
const [activeIndex, setActiveIndex] = useState(0);
return <AgentPlan title="Composer draft" steps={STEPS} activeIndex={activeIndex} />;
}Advance it as work finishes
function completeStep() {
setActiveIndex((i) => i + 1);
}Once activeIndex reaches steps.length, every step renders as done and the header reads n of n.
Anatomy
<div data-slot="agent-plan">
<div>
<span>{/* title */}</span>
<span>{/* n of m */}</span>
</div>
<div role="progressbar">{/* accessible progress */}</div>
<ul>
<li>
<span>{/* check, spinner, or dot */}</span>
<span>{/* step label, active description, and screen-reader status */}</span>
</li>
</ul>
</div>activeIndex is clamped into 0โฆsteps.length before anything is drawn from it, so an out-of-range value never breaks the layout: a value at or past steps.length marks every step done, and NaN falls back to 0. A step is done when its index is before the clamped active index or every step is already done, active when it sits exactly at that index, and otherwise still ahead. Object steps may add a stable id and a description; only the active step shows its description. Every row includes screen-reader status text, while the progress bar announces both percentage and the completed-step count. With an empty steps array the header reads 0 of 0, the bar sits at 0%, and the list renders nothing.
Examples
Add detail to the active step
Object steps use id for stable keys, label for the visible row, and an optional description beneath the active label. A custom title names both the heading and the progress bar for screen readers.
<AgentPlan
title="Release checklist"
steps={[
{ id: "review", label: "Review the release notes" },
{
id: "publish",
label: "Publish the package",
description: "Wait for the release job to finish.",
},
{ id: "announce", label: "Share the release" },
]}
activeIndex={1}
/>Restarting a plan
A plan restarts with another update_plan call whose activeIndex is 0. There is no separate reset action.
render: ({ args }) => (
<AgentPlan title={args.title} steps={args.steps} activeIndex={args.activeIndex} />
),function restart() {
setActiveIndex(0);
}API reference
Render props
| Prop | Type | Description |
|---|---|---|
args.title | string | undefined | Optional title for the card and its progress bar. |
args.steps | (string | AgentPlanStep)[] | The plan's steps, in order. |
args.activeIndex | number | Index of the step currently in progress. |
AgentPlan
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | "Plan" | Title for the card and its progress bar. |
steps | readonly (string | AgentPlanStep)[] | required | The plan's steps, in order. Strings render as labels. |
activeIndex | number | required | Index of the step in progress. Out-of-range values clamp; NaN falls back to 0. |
className | string | Merged onto the root. |
AgentPlanStep is { id?: string; label: string; description?: string }. Its id is the row key when present, and its description appears only while that step is in progress.
All other div props are forwarded to the root.