Plan Canvas(计划画板)
计划和可视化工件的审查循环:你编写工件,人类在浏览器中审查 —— 标注他们所指的确切元素、聊天、并给出 批准计划 / 请求更改 的裁决 —— 而你则阻塞在一个 CLI 调用上,该调用将他们的反馈作为 JSON 返回。
灵感来自 lavish-axi;围绕 /plan 确认门控进行了 ECC 原生重构,零依赖。
何时使用
- 你刚刚编写了一个计划工件(来自
/plan的.claude/plans/*.plan.md),需要 CONFIRM/批准决策 —— 画板的裁决取代了键盘输入的“是/继续”。 - 用户应该 指向 要更改的内容:审查设计、对比、报告或任何本地
.md/.html工件。 - 用户请求
/plan-canvas、可视化审查,或“在浏览器中打开它”。
请勿用于:差异审查(/code-review)、运行 Web 应用或远程 URL。画板仅服务于本地工件文件。
工作方式
调用 CLI 命令 ecc-plan-canvas —— 由 ecc-universal 包提供的二进制文件(在全局/插件安装后位于 PATH 中;对于插件安装,node "$CLAUDE_PLUGIN_ROOT/scripts/plan-canvas.js" 也同样有效)。在你正在审查的项目目录中运行它;它可以从任何工作目录运行。它管理一个由所有会话共享的分离式环回服务器(127.0.0.1:4517),以工件路径为键 —— 无需跟踪会话 ID。
这个工作流是一个纯 CLI 加 JSON 的循环,因此与模型和工具链无关:任何能够运行 shell 命令并读取标准输出的代理都可以以相同的方式驱动它(Claude Code、Codex、Cursor、Gemini、OpenCode、Copilot)。通过你的工具链支持技能的任何方式触发它 —— 例如 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 实际在会话上驻留时,反馈才能到达你。如果你的回合结束时没有人在监听,消息会留在队列中,从人类的角度看,发送似乎没有任何作用。
因此,当你的工具链支持时,将 await 作为后台任务运行(在 Claude Code 中,使用 run_in_background: true 的 Bash 调用)。它会在反馈到达时立即退出,工具链将 JSON 交给你,这样循环就能在回合之间保持活跃,而不是随着前台调用而终止。前台的 await 也可以工作,但只能持续到工具链超时为止。
存在两个后盾,但都不能成为忽略上述规则的借口:
ecc-plan-canvas pending列出没有监听器而排队的反馈。每当你不确定是否遗漏了某些内容时检查它。stop:plan-canvas-pending钩子会在画板反馈未送达时阻止你的回合结束,并将消息交给你。如果你从该钩子读取反馈,说明你过早停止了监听。
await 在人类操作时打印 JSON:
{
"status": "feedback",
"items": [
{ "kind": "annotation", "text": "把这分成两个阶段",
"anchor": { "selector": "h2:nth-of-type(3)", "tag": "h2", "snippet": "阶段2: 迁移" } },
{ "kind": "verdict", "verdict": "request-changes" }
]
}
kind: "chat"—— 自由格式消息;在画板中回答,而不是终端。kind: "annotation"—— 锚定到元素的反馈(anchor.selector、anchor.snippet显示他们指向的内容;anchor.textRange.text在他们高亮了一段文字时提供)。kind: "verdict"——approve表示计划已确认:停止轮询,结束会话,开始实施。request-changes表示修订工件(画板将实时重新加载它)并保持循环进行。
3. 始终在画板中响应,然后继续监听。一个命令同时完成两件事:
ecc-plan-canvas await <file> --reply "已经按照要求拆分阶段2,请查看。"
人类的每条消息都要在画板中得到回复,即使只是一句“正在处理,现在重写风险表。”画板聊天面板中的沉默与破损的画板无法区分,而这正是此循环要防止的故障。在那里回答,而不仅仅是终端中。
在你工作时,用活动指示器保持聊天的真实性:
# 动画“代理正在思考...”气泡;在长时间工作时刷新它
ecc-plan-canvas typing <file> --state thinking
# 在回复即将到来之前切换到“代理正在输入...”
ecc-plan-canvas typing <file> --state typing
await 在将一批结果交给你时会自动设置为 thinking,而 --reply 会清除它。两种状态都会自动过期,因此崩溃的代理会诚实地显示为“已排队”,而不会让人类永远看着那些点。如果修订需要超过一分钟,请刷新 thinking。
4. 结束 当审查结束时:ecc-plan-canvas end <file>。
图表(Mermaid)
当计划的某部分涉及流程图、架构图、时序图、状态机、ER 模型或依赖关系图时,将其编写为围栏 ```mermaid 块,而不是 ASCII 艺术或一大段文字 —— 画板会将其渲染为带主题的图表,人类可以指向它。当图片比段落更容易阅读时使用它;对于简单的列表或表格则跳过。
```mermaid
flowchart LR
A[市场解决] --> B{有观察者?}
B -->|是| C[入队任务] --> D[扇出工作进程]
```
图表以 ECC 深色主题和强调色板渲染。Mermaid 从固定的 CDN 加载到浏览器中;如果 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 并且必须 WAIT 等待确认:
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 "已重做风险表。"
# → 再次阻塞,直到下一次响应
反模式
- 使用
--timeout-ms在循环中进行轮询。它仅用于测试。保留普通的await运行。 - 在审查仍然打开时,没有
await监听就结束你的回合。这是人类体验为“我发送了一条消息,但什么也没发生”的唯一故障。 - 读取反馈但仅在终端中回答。人类正在看着画板。
- 在用户发起的结束之后重新打开,“只是为了展示”某些东西。
- 将整个计划粘贴到聊天中 并且 打开画板 —— 选择画板,终端摘要保持在一行。
- 从状态文件中解析画板聊天 —— 你需要的一切都通过
await到达。
设计说明和起源: docs/design/plan-canvas.md。