"use client"; import { Circle, CircleDot, Pause } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { CARD_SURFACE } from "@/components/ui/card"; import { cn } from "@/lib/utils"; import { Collapsible, CollapsibleChevron, CollapsiblePanel, CollapsibleTrigger } from "@/components/ui/collapsible"; import { FLUSH_ROW_INTERACTIVE } from "@/components/ui/interactive"; import { StatusMark } from "@/components/ui/status-mark"; import type { PlanTodo } from "./events"; interface PlanChecklistProps { /** 执行期计划清单 (覆盖式刷新); 空数组时不渲染. */ todos: PlanTodo[]; /** * 本轮是否仍在流式执行中. ``in_progress`` 仅在 ``active`` 时转圈; * turn 结束/报错后转 ``false``, 当前步停转 (显示"停在此步"), 不再空转误导. */ active: boolean; } /** * 执行期"计划清单"实时勾选 (TodoWrite 式): code-agent 批准计划后边执行边刷新各步 * 状态, 用户看到"做到哪一步了". 渲染在 composer 上方, 由 planTodos 驱动. * * 进度叙事三件套: 顶部分段进度条 (一段一步) · 左侧轨道串起各步 · 当前步整行铺中性底, * 不看文字也知道走到哪. 设计稿见 docs/prototype/todolist/openpencil. * * 可折叠: 七八步的清单会把对话区挤成一小截, 点标题栏折成一行 (分段条 + 当前步 + 计数). * 偏好全局粘性 (localStorage): 折过一次以后所有会话默认折叠, 展开同理. * SSR / 首屏恒为展开, 水合后 effect 读偏好校正 (与 ComposerSizeProvider 同款). */ export const PlanChecklist = ({ todos, active }: PlanChecklistProps) => { const [collapsed, setCollapsed] = useState(false); useEffect(() => { try { if (localStorage.getItem(PLAN_CHECKLIST_COLLAPSED_KEY) === "1") { queueMicrotask(() => setCollapsed(true)); } } catch { /* localStorage 不可用 (隐私模式等) 时静默用默认 */ } }, []); const toggle = useCallback(() => { const next = !collapsed; setCollapsed(next); try { localStorage.setItem(PLAN_CHECKLIST_COLLAPSED_KEY, next ? "1" : "0"); } catch { /* 忽略写入失败 */ } }, [collapsed]); if (todos.length === 0) return null; const whole = wholeStatusOf(todos, active); const doneCount = todos.filter((t) => t.status === "done").length; const counter = `${doneCount} / ${todos.length}`; const current = todos.find((t) => t.status === "in_progress"); // 折叠行永远带一步的名字: 跑着是当前步, 做完了是最后一步, 不留光秃秃的计数 const headline = (current ?? todos[todos.length - 1]).title; return (
{todos.map((todo) => ( ))}
{collapsed ? (
{headline} {counter} 展开
) : (
计划清单 {counter} 收起
)}
    {todos.map((todo, i) => { const focus = todo.status === "in_progress"; const last = i === todos.length - 1; return (
  1. {/* 轨道: 首行顶端留空, 末行不画下段; 9px 上段让 16px 图标对齐 22px 行高的第一行 */} 0 && railClass(todos[i - 1].status === "done"))} /> {!last && ( )} {todo.title}
  2. ); })}
); };