Files
superwork/vscode/src/codeReader/fixtures/planChecklist.tsx.txt
T
cruldra 6ce4ef7ee2 ✨ feat(vscode): React 文件照源码一块一张卡片,组件卡片只留名称、说明、构造参数
- 组件体的语句不再并进组件卡片,照源码顺序是它的子块;状态、引用、数据源、派生值、副作用、事件处理函数、渲染输出各成一张卡片,回调和函数里的语句是这些卡片下面的子卡片
- 普通变量、提前返回的 if、注释照语言默认的卡片;表格格子里只写名字和位置,去掉「改它的」「来自」格子里单独打开源码的那条路
- 用词改成书面叫法(更新位置、使用位置、计算表达式、依赖项、触发时机、清理函数、绑定事件、样式取决于……),新增 toggle-right、anchor、download、sigma、layout-template 五个种类图标
- 夹具加上 luffy 的 QueuedBubble.tsx、PlanChecklist.tsx,规格的 React 一节照新样子改写
2026-10-08 07:17:25 +08:00

149 lines
5.9 KiB
Plaintext

"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 (
<Collapsible
open={!collapsed}
onOpenChange={toggle}
className={cn(CARD_SURFACE, "overflow-hidden")}
>
<CollapsibleTrigger
data-testid="plan-checklist-toggle"
className={cn(FLUSH_ROW_INTERACTIVE, "block w-full text-left")}
>
<div data-testid="plan-progress" className="flex gap-1 px-3.5 pt-3">
{todos.map((todo) => (
<span
key={todo.title}
className={cn("h-[3px] flex-1 rounded-full", segmentClass(todo.status, whole))}
/>
))}
</div>
{collapsed ? (
<div className="flex items-center gap-2.5 px-3.5 pt-2.5 pb-3">
<StatusIcon status={current ? "in_progress" : "done"} active={active} />
<span
className={cn(
"min-w-0 flex-1 truncate text-ui leading-[22px] font-medium",
whole === "done" ? "text-muted-foreground" : "text-foreground",
)}
>
{headline}
</span>
<span className="text-caption text-ink-4 tabular-nums">{counter}</span>
<span className="flex items-center gap-0.5 text-caption text-ink-4">
展开
<CollapsibleChevron strokeWidth={1.8} />
</span>
</div>
) : (
<div className="flex items-center gap-2 px-3.5 pt-2.5 pb-2">
<span className="text-caption font-medium tracking-wide text-ink-2">计划清单</span>
<span className="text-caption text-ink-4 tabular-nums">{counter}</span>
<span className="flex-1" />
<WholeStatusLabel whole={whole} />
<span className="flex items-center gap-0.5 text-caption text-ink-4">
收起
<CollapsibleChevron strokeWidth={1.8} />
</span>
</div>
)}
</CollapsibleTrigger>
<CollapsiblePanel>
<ol className="max-h-[40vh] overflow-y-auto border-t border-border px-2 pt-1.5 pb-2">
{todos.map((todo, i) => {
const focus = todo.status === "in_progress";
const last = i === todos.length - 1;
return (
<li
key={todo.title}
className={cn("flex gap-2.5 rounded-md px-2", focus && "bg-surface-2")}
>
{/* 轨道: 首行顶端留空, 末行不画下段; 9px 上段让 16px 图标对齐 22px 行高的第一行 */}
<span className="flex w-4 shrink-0 flex-col items-center">
<span
className={cn("h-[9px] w-0.5", i > 0 && railClass(todos[i - 1].status === "done"))}
/>
<StatusIcon status={todo.status} active={active} />
{!last && (
<span className={cn("w-0.5 flex-1", railClass(todo.status === "done"))} />
)}
</span>
<span
className={cn(
"min-w-0 flex-1 py-1.5 text-ui leading-[22px]",
titleClass(todo.status),
)}
>
{todo.title}
</span>
</li>
);
})}
</ol>
</CollapsiblePanel>
</Collapsible>
);
};