用途
Plan Canvas 是一个面向计划与可视化产物的审阅回路(review loop)。你负责编写产物,人类在浏览器中审阅它——对确切所指的元素添加批注、聊天,并给出“Approve plan(批准计划)”或“Request changes(请求修改)”的结论;与此同时,你阻塞在一次 CLI 调用上,该调用以 JSON 返回他们的反馈。
它受 lavish-axi 启发,围绕 /plan 确认关卡以 ECC 原生方式重建,零依赖。
何时使用
- 你刚写完一个计划产物(来自 /plan 的 .claude/plans/*.plan.md),需要 CONFIRM/批准决策——画布结论可替代手动输入的“yes/proceed”。
- 用户应当指出要改什么:审阅设计、对比、报告,或任何本地 .md / .html 产物。
- 用户要求 /plan-canvas、可视化审阅,或“在浏览器中打开它”。
不要用于:代码 diff 的代码审阅(/code-review)、运行 Web 应用,或远程 URL。画布仅服务本地产物文件。
工作原理
以 ecc-plan-canvas 调用 CLI——这是 ecc-universal 包附带的 bin(全局/插件安装后在 PATH 上;插件安装时 node "$CLAUDE_PLUGIN_ROOT/scripts/plan-canvas.js" 也可用)。在你正在审阅的项目中运行它;它可从任意工作目录工作。它管理一个分离的环回服务器(127.0.0.1:4517),由所有会话共享,以产物路径为键——无需跟踪会话 ID。
工作流是一个纯 CLI 加 JSON 的循环,因此与模型和 harness 无关:任何能运行 shell 命令并读取 stdout 的 agent 都以相同方式驱动它(Claude Code、Codex、Cursor、Gemini、OpenCode、Copilot)。按你的 harness 暴露技能的方式触发它——例如 Claude Code 中的 /plan-canvas、Codex 中的 $plan-canvas——或直接运行 ecc-plan-canvas 命令。
# 1. 在用户浏览器中打开产物(立即返回)
ecc-plan-canvas open .claude/plans/feature.plan.md
# 2. 阻塞直到人类响应。保持运行;若被中断可重新运行:
# 排队的反馈永不丢失。
ecc-plan-canvas await .claude/plans/feature.plan.md
保持监听,否则人类是在对空椅子说话
只有当某个 await 实际停驻在该会话上时,反馈才能到达你。如果你的回合结束时没有任何监听者,消息会留在队列中,而从人类那一侧看,发送似乎毫无反应。
因此,当你的 harness 支持后台任务时,把 await 作为后台任务运行(在 Claude Code 中,是带 run_in_background: true 的 Bash 调用)。反馈一到它就退出,harness 把 JSON 交给你,从而让循环跨回合存活,而不是随前台调用一起消亡。前台 await 也可以,但只在 harness 对其施加时间限制之前有效。
存在两个兜底机制,但两者都不是跳过上述做法的借口:
- ecc-plan-canvas pending 列出已排队但无监听者的反馈。每当不确定是否漏掉什么时检查它。
- stop:plan-canvas-pending 钩子会在画布反馈未送达时阻止你的回合结束,并把消息交给你。如果你是从该钩子读取反馈,说明你停止监听太早了。
当人类操作时,await 打印 JSON:
{
"status": "feedback",
"items": [
{ "kind": "annotation", "text": "Split this into two phases",
"anchor": { "selector": "h2:nth-of-type(3)", "tag": "h2", "snippet": "Phase 2: Migration" } },
{ "kind": "verdict", "verdict": "request-changes" }
]
}
- kind: "chat" —— 自由形式消息;在画布中回答,而不是在终端。
- kind: "annotation" —— 锚定到某元素的反馈(anchor.selector、anchor.snippet 显示他们所指的内容;当他们高亮一段文字时为 anchor.textRange.text)。
- kind: "verdict" —— approve 表示计划已 CONFIRMED:停止轮询、结束会话并开始实现。request-changes 表示修订产物(画布会实时重载它)并继续循环。
3. 始终在画布中回应,然后继续监听。一条命令同时完成两件事:
ecc-plan-canvas await <file> --reply "Split Phase 2 as requested. Take a look."
每条人类消息都要在画布中得到回复,哪怕只是一句“On it, rewriting the risk table now.”。聊天面板中的沉默与画布损坏无法区分,而这正是此循环存在所要防止的失败。在那里回答,不要只在终端回答。
在你工作时,用活动指示器保持聊天诚实:
# 动画的 "agent is thinking..." 气泡;长时间工作时刷新它
ecc-plan-canvas typing <file> --state thinking
# 在回复落地前切换到 "agent is typing..."
ecc-plan-canvas typing <file> --state typing
await 在把一批反馈交给你的那一刻会为你设置 thinking,而 --reply 会清除它。两种状态都会自行过期,因此崩溃的 agent 会衰减为诚实的“queued”,而不是让人类永远盯着点点。如果一次修订超过一分钟,请刷新 thinking。
4. 审阅结束时结束:ecc-plan-canvas end <file>。
图表(Mermaid)
当计划的一部分是流程、架构、时序、状态机、ER 模型或依赖图时,把它写成围栏 ```mermaid 代码块,而不是 ASCII 艺术或大段文字——画布会将其渲染为人类可以指向的主题化图表。当图比段落读起来更快时就用它;简单列表或表格则跳过。
flowchart LR
A[Market resolves] --> B{Watchers?}
B -->|yes| C[Enqueue jobs] --> D[Fan-out worker]
图表以 ECC 深色主题和强调色调色板渲染。Mermaid 在浏览器中从固定 CDN 加载;若不可用(离线),该代码块会降级为显示其源码,因此审阅永远不会被阻塞。可将本地镜像指向 ECC_PLAN_CANVAS_MERMAID_URL 以用于气隙环境。
规则
- Markdown 产物以 ECC 的计划模板渲染(包括 Mermaid 代码块);.html 产物按原样渲染,并注入批注层。HTML 编写指引请使用 frontend-design-direction 和 artifact-design 技能。
- 通过编辑产物文件来修订——画布在保存时实时重载。切勿重新运行 open 来刷新。
- {"status": "ended", "endedBy": "user"}(或反馈批次上的 sessionEnded: true)表示用户关闭了审阅:停止轮询,在聊天中交付剩余更新,且不要重新打开。对该会话的普通 open 会被拒绝;仅当用户要求恢复时才传 --reopen。
- 同级资源(图片、CSS)必须放在产物旁边,并以相对路径引用。
- 服务器仅限环回,并在空闲 30 分钟后退出(ECC_PLAN_CANVAS_IDLE_MS);stop 会显式关闭它。状态位于 ~/.claude/plan-canvas/(ECC_PLAN_CANVAS_STATE_DIR)。
示例
计划批准流程——/plan 写入 .claude/plans/notifications.plan.md 且必须等待确认:
ecc-plan-canvas open .claude/plans/notifications.plan.md
ecc-plan-canvas await .claude/plans/notifications.plan.md
# → {"status":"feedback","items":[{"kind":"verdict","verdict":"approve"}]}
ecc-plan-canvas end .claude/plans/notifications.plan.md
# 计划已确认——开始实现
修订循环——反馈到达,你编辑文件、回复、继续监听:
# await 返回批注 → 编辑 .plan.md(画布实时重载)
ecc-plan-canvas await <file> --reply "Reworked the risk table."
# → 再次阻塞直到下一次响应
反模式
- 在循环中用 --timeout-ms 轮询。它存在是为了测试。请改为让普通 await 保持运行。
- 在审阅仍打开时,结束你的回合却没有 await 在监听。这是人类会体验为“我发了消息但什么都没发生”的唯一失败。
- 读取了反馈却只在终端回答。人类正看着画布。
- 在用户主动结束后“只是为了展示”而重新打开。
- 把整个计划粘贴到聊天中,同时又打开画布——选择画布,并把终端摘要保持在一行。
- 从状态文件解析画布聊天——你需要的一切都通过 await 到达。
设计说明与起源:docs/design/plan-canvas.md。