Skip to content

待办事项看板 (Todo list) ​

在前端 UI 中通过实时 Todo 列表追踪多步骤智能体任务规划进度

当智能体启用了任务规划中间件(TodoListMiddleware)时,智能体在执行复杂长任务期间会主动创建并维护一个结构化的待办任务清单(Todo List)。 前端可以通过订阅 stream.values.todos,实时渲染出一个动态滚动的任务进度看板与进度条。

工作原理解析 (How it works) ​

  1. 后端规划:智能体调用内置的 write_todos 工具,写入形如 { content: "...", status: "pending" | "in_progress" | "completed" } 的待办任务列表。
  2. 状态同步:该列表自动保存在智能体图状态中,并通过流式事件实时下发到前端。
  3. 前端渲染:前端组件响应式监听 todos 的状态变更,动态更新复选框、当前正在执行的高亮任务项与整体完成百分比进度条。

前端 useStream 接入 ​

在前端定义具有类型推导的流连接:

ts
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";

const agent = await createDeepAgent({
  model: "google:gemini-3.6-flash",
  middleware: [todoListMiddleware()],
});

构建 TodoList 核心组件 ​

实现一个包含进度统计与任务项列表的 React/Vue 看板组件:

ts
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";

const agent = await createDeepAgent({
  model: "openai:gpt-5.5",
  middleware: [todoListMiddleware()],
});

进度条可视化 (Progress bar) ​

基于完成数占总任务数的比例动态渲染平滑过渡的进度条组件:

ts
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";

const agent = await createDeepAgent({
  model: "anthropic:claude-sonnet-5",
  middleware: [todoListMiddleware()],
});

待办事项状态渲染 (Individual todo items) ​

根据不同的任务状态渲染专属视觉样式:

  • completed:绿色对勾、文字带有删除线(Strikethrough)。
  • in_progress:蓝色脉冲动画或正在加载的 Spinner 图标。
  • pending:灰色空心待办圈。

计算总体进度 (Calculating progress) ​

通过对 todos 数组进行简单聚合计算百分比:

ts
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";

const agent = await createDeepAgent({
  model: "openrouter:z-ai/glm-5.2",
  middleware: [todoListMiddleware()],
});

结合聊天消息的混合布局 (Combining with chat messages) ​

在现代生产级应用中,通常将 Todo 看板固定悬浮在右侧边栏(Sidebar)或消息输入框上方,左侧为主聊天流,形成兼具对话感与项目管理感的双重视图:

ts
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";

const agent = await createDeepAgent({
  model: "fireworks:accounts/fireworks/models/glm-5p2",
  middleware: [todoListMiddleware()],
});

核心业务适用场景 (Use cases) ​

  • 复杂代码重构与迁移:明确列出“扫描文件 -> AST 分析 -> 重写模块 -> 运行测试”的执行链条。
  • 深度调研与研报撰写:直观展示当前处于文献检索、数据整理还是报告排版阶段。
  • 跨服务批量工单处理:让客服运营人员清晰知晓批处理进度。

异常与空状态处理 (Empty and loading states) ​

在智能体尚未制定计划前,展示骨架屏(Skeleton)或友好的占位文案;在全部任务达成后,触发彩带特效或成功完成提示卡。

最佳实践准则 ​

  1. 确保后端智能体原子更新:要求智能体在真正开始某项子任务时才将其置为 in_progress,完成后立刻标记 completed。
  2. 保持状态响应式纯净:直接从 stream.values.todos 派生视图,避免在前端引入冗余的可变本地状态。
ts
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";

const agent = await createDeepAgent({
  model: "baseten:zai-org/GLM-5.2",
  middleware: [todoListMiddleware()],
});
ts
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";

const agent = await createDeepAgent({
  model: "ollama:north-mini-code-1.0",
  middleware: [todoListMiddleware()],
});

Then point useStream at that agent and read the todos from stream.values.

NOTE

The code examples use useStream<typeof myAgent> for type-safe stream state. See Type inference for Python or JavaScript backends.

tsx
import { useStream } from "@langchain/react";

const AGENT_URL = "http://localhost:2024";

export function TodoAgent() {
  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "deep_agent_todo_list",
  });

  const todos = stream.values?.todos ?? [];

  return (
    <div>
      <TodoList todos={todos} />
      {stream.messages.map((msg) => (
        <Message key={msg.id} message={msg} />
      ))}
    </div>
  );
}
vue
<script setup lang="ts">
import { useStream } from "@langchain/vue";
import { computed } from "vue";

const AGENT_URL = "http://localhost:2024";

const stream = useStream<typeof myAgent>({
  apiUrl: AGENT_URL,
  assistantId: "deep_agent_todo_list",
});

const todos = computed(() => stream.values.value?.todos ?? []);
</script>

<template>
  <div>
    <TodoList :todos="todos" />
    <Message
      v-for="msg in stream.messages.value"
      :key="msg.id"
      :message="msg"
    />
  </div>
</template>
svelte
<script lang="ts">
  import { useStream } from "@langchain/svelte";

  const AGENT_URL = "http://localhost:2024";

  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "deep_agent_todo_list",
  });

  const todos = $derived(stream.values?.todos ?? []);
</script>

<div>
  <TodoList {todos} />
  {#each stream.messages as msg (msg.id)}
    <Message message={msg} />
  {/each}
</div>
ts
import { Component, computed } from "@angular/core";
import { injectStream } from "@langchain/angular";

const AGENT_URL = "http://localhost:2024";

@Component({
  selector: "app-todo-agent",
  template: `
    <div>
      <app-todo-list [todos]="todos()" />
      @for (msg of stream.messages(); track msg.id) {
        <app-message [message]="msg" />
      }
    </div>
  `,
})
export class TodoAgentComponent {
  stream = injectStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "deep_agent_todo_list",
  });

  todos = computed(() => this.stream.values()?.todos ?? []);
}

Building the TodoList component ​

The todo list renders each item with a status icon, color coding, and visual styling that reflects the current state:

tsx
function TodoList({ todos }: { todos: Todo[] }) {
  const completed = todos.filter((t) => t.status === "completed").length;
  const percentage = todos.length
    ? Math.round((completed / todos.length) * 100)
    : 0;

  return (
    <div className="rounded-lg border bg-white p-4 shadow-sm">
      <div className="mb-4 flex items-center justify-between">
        <h2 className="text-lg font-semibold">Agent Progress</h2>
        <span className="text-sm text-gray-500">
          {completed}/{todos.length} tasks
        </span>
      </div>

      <ProgressBar percentage={percentage} />

      <ul className="mt-4 space-y-2">
        {todos.map((todo, i) => (
          <TodoItem key={i} todo={todo} />
        ))}
      </ul>
    </div>
  );
}

Progress bar ​

A visual progress bar gives users an at-a-glance summary of overall completion:

tsx
function ProgressBar({ percentage }: { percentage: number }) {
  return (
    <div className="space-y-1">
      <div className="flex items-center justify-between text-xs text-gray-500">
        <span>Progress</span>
        <span>{percentage}%</span>
      </div>
      <div className="h-2 overflow-hidden rounded-full bg-gray-200">
        <div
          className="h-full rounded-full bg-green-500 transition-all duration-500"
          style={{ width: `${percentage}%` }}
        />
      </div>
    </div>
  );
}

Individual todo items ​

Each item gets a status icon, color-coded text, and strikethrough styling for completed tasks:

tsx
function TodoItem({ todo }: { todo: Todo }) {
  const config = {
    pending: {
      icon: "○",
      textClass: "text-gray-600",
      bgClass: "bg-gray-50",
      iconClass: "text-gray-400",
    },
    in_progress: {
      icon: "◉",
      textClass: "text-amber-800",
      bgClass: "bg-amber-50 border-amber-200",
      iconClass: "text-amber-500 animate-pulse",
    },
    completed: {
      icon: "✓",
      textClass: "text-green-800 line-through",
      bgClass: "bg-green-50 border-green-200",
      iconClass: "text-green-500",
    },
  };

  const style = config[todo.status];

  return (
    <li
      className={`flex items-start gap-3 rounded-md border px-3 py-2 ${style.bgClass}`}
    >
      <span className={`mt-0.5 text-lg leading-none ${style.iconClass}`}>
        {style.icon}
      </span>
      <span className={`text-sm ${style.textClass}`}>{todo.content}</span>
    </li>
  );
}

The in_progress icon uses animate-pulse to draw attention to the currently active task.

Calculating progress ​

Derive progress metrics directly from the todos array:

ts
const todos = stream.values?.todos ?? [];

const completed = todos.filter((t) => t.status === "completed").length;
const inProgress = todos.filter((t) => t.status === "in_progress").length;
const pending = todos.filter((t) => t.status === "pending").length;
const percentage = todos.length
  ? Math.round((completed / todos.length) * 100)
  : 0;

These values update reactively as the agent modifies its state, keeping the progress bar and counters in sync.

Combining with chat messages ​

The todo list works alongside the regular chat interface. A practical layout shows the todo list as a persistent sidebar or header panel, with chat messages below:

tsx
function TodoAgentLayout() {
  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "deep_agent_todo_list",
  });

  const todos = stream.values?.todos ?? [];

  return (
    <div className="flex h-screen flex-col">
      {todos.length > 0 && (
        <div className="border-b bg-gray-50 p-4">
          <TodoList todos={todos} />
        </div>
      )}

      <main className="flex-1 overflow-y-auto p-6">
        <div className="mx-auto max-w-2xl space-y-4">
          {stream.messages.map((msg) => (
            <Message key={msg.id} message={msg} />
          ))}
        </div>
      </main>

      <ChatInput
        onSubmit={(text) =>
          stream.submit({ messages: [{ type: "human", content: text }] })
        }
        isLoading={stream.isLoading}
      />
    </div>
  );
}

TIP

Show the todo list only when todos.length > 0. Before the agent creates its plan, there's nothing to display. Showing an empty component wastes space.

Use cases ​

The todo list pattern fits any scenario where an agent executes a structured plan:

  • Project planning: agent breaks a project into tasks and works through them sequentially
  • Research workflows: each research question becomes a todo that the agent investigates and completes
  • Data processing: steps like ingestion, validation, transformation, and export each get their own todo
  • Onboarding flows: agent walks through setup steps, checking off each one as it configures services
  • Report generation: sections of a report become todos: gather data, analyze trends, write summary, format output

Handling empty and loading states ​

Handle the initial state before the agent has created its plan:

tsx
function TodoList({ todos, isLoading }: { todos: Todo[]; isLoading: boolean }) {
  if (todos.length === 0 && !isLoading) {
    return null;
  }

  if (todos.length === 0 && isLoading) {
    return (
      <div className="rounded-lg border bg-white p-4 shadow-sm">
        <div className="flex items-center gap-2 text-sm text-gray-500">
          <span className="animate-spin">⟳</span>
          Agent is creating a plan...
        </div>
      </div>
    );
  }

  return (
    <div className="rounded-lg border bg-white p-4 shadow-sm">
      {/* ... full todo list rendering */}
    </div>
  );
}

Best practices ​

  • Show the todo list prominently. It's the primary progress indicator for plan-based agents. Don't bury it below the fold.
  • Animate status transitions. Smooth transitions make the agent feel more responsive. Use CSS transitions on background color, text decoration, and opacity.
  • Only highlight one in_progress item. Agents typically work on one task at a time. If multiple items show as in_progress, the UI gets noisy. Consider only pulsing the first one.
  • Collapse or dim completed items. As the list grows, completed items become less relevant. Reduce their visual weight so users focus on what's still happening.
  • Show the progress percentage. A single number like "67% complete" is immediately understandable, even from across the room.
  • Keep the todo list in sync. Because stream.values updates reactively, the todo list stays current automatically. Don't add manual polling or refresh logic.

The todo list pattern is a specialization of the broader LangChain approach to rendering structured agent state. These guides cover related techniques that pair well with plan-based agents:

Structured output

Render any structured agent state—not just todos—with custom UI components instead of plain text.

Join & rejoin streams

Reconnect to a running plan after a page reload or tab switch without losing progress.

Message queues

Queue follow-up tasks while the agent is still working through its current plan.

Human-in-the-Loop

Pause and resume plan execution when the agent needs user approval or input.


Connect these docs to your agent of choice via MCP for real-time answers.

Edit this page on GitHub or file an issue.