旧的视觉头脑风暴流程会通过 wait-for-feedback.sh 脚本阻塞 TUI,导致用户在头脑风暴期间无法与 Claude 交互。新的非阻塞设计采用“浏览器显示,终端命令”模型:浏览器只负责展示 mockup 和记录用户选择(写入 .events 文件),终端则始终作为用户输入反馈的主通道。Claude 在下一个回合中读取 .events 文件和终端文本,从而完成输入的合并与处理。这种架构解耦了显示与交互,提升了 TUI 的响应性并简化了跨平台兼容性。

Visual Brainstorming 的非阻塞设计:为什么浏览器只做显示,终端仍是对话通道

在 SuperPowers 的视觉头脑风暴流程中,一个核心的设计原则是浏览器只做显示,终端仍是对话通道。这种非阻塞架构源于对早期阻塞式设计的彻底反思与重构。如果你在使用 visual-companion.md 技能时曾好奇为何不能直接在浏览器里输入大段文字反馈,或者想知道 .events 文件是如何工作的,本文将结合源码与设计文档,详细拆解背后的逻辑。

旧设计的痛点:为什么阻塞模型行不通

在重构之前,视觉头脑风暴依赖一个名为 wait-for-feedback.sh 的脚本。当 Claude 需要展示 mockup 时,它会将此脚本作为后台任务运行,并通过 TaskOutput(block=true, timeout=600s) 来等待用户反馈。这个设计的根本问题在于它模拟了事件驱动的行为,而 Claude Code 的执行模型是基于回合的

docs/superpowers/specs/2026-02-19-visual-brainstorming-refactor-design.mdProblem 部分明确指出了这一点:“Claude Code’s execution model is turn-based. There is no way for Claude to listen on two channels simultaneously within a single turn.” 这意味着,当 TaskOutput 阻塞时,Claude 的整个 TUI(文本用户界面)会被“冻结”。用户无法在终端继续打字提问或补充信息,浏览器成了唯一的输入渠道。这破坏了编码代理工作流中“对话”这一核心交互模式。

新架构的核心模型:职责分离

新设计引入了一个清晰且稳固的核心模型,将交互职责分离:

Browser = interactive display. 浏览器成为纯粹的交互式显示器。它的职责是呈现 mockup、选项卡片或可视化对比,允许用户通过点击进行选择。所有交互都被服务器记录下来。

Terminal = conversation channel. 终端始终是对话通道。它永不阻塞,用户可以随时在此输入文字反馈、提出新问题或给出上下文。这是用户与 AI 代理进行自然语言交流的主要场所。

这种分离使得工作流的每一步都保持 TUI 的响应能力,用户不会因为查看一个设计选项而失去与代理的对话权。

工作流闭环:从阻塞循环到非阻塞 .events 循环

新的“The Loop”在 visual-companion.md 中有详细描述,其步骤如下,每个环节都体现了非阻塞特性:

  1. Claude 写入 HTML 文件:Claude 使用 Write 工具将一个内容片段(如 layout.html)写入指定的 screen_dir 目录。文件使用语义化命名。
  2. 服务器检测并推送:Brainstorm 服务器通过 chokidar(Node.js 文件监听库)监测到新文件,通过 WebSocket 向浏览器客户端推送 reload 消息,浏览器自动刷新显示最新内容。这个过程在 server.cjsfs.watch 处理函数中实现。
  3. Claude 结束回合,引导用户:Claude 不再阻塞等待,而是主动结束当前回合。它在终端消息中告知用户浏览器已更新,并给出简单的操作指引:“Take a look and let me know what you think. Click to select an option if you’d like.”
  4. 用户在浏览器和终端双通道交互:用户查看浏览器,可以点击选择选项(例如,点击选项卡片)。同时,他们可以在终端直接输入文字,表达想法、提出疑问或要求迭代。
  5. Claude 在下一回合读取输入:当用户发送终端消息后,Claude 在新的回合开始。此时,它需要读取来自两个通道的信息:
    • 读取 $STATE_DIR/.events 文件:这个文件记录了用户在浏览器中的所有点击交互(JSONL 格式)。它由 server.cjs 在 WebSocket message 处理函数中写入。
    • 读取用户的终端文本:这是来自 standard input 的主要反馈。
    • 合并分析:Claude 合并结构化的浏览器事件流和自由形式的终端文本,获得对用户意图的完整理解。
  6. 迭代或前进:根据合并后的反馈,Claude 决定是修改当前屏幕(写入一个新版本文件,如 layout-v2.html)还是进入下一个设计问题。

整个循环中,没有后台任务,没有 TaskOutput 阻塞,没有轮询脚本.events 文件成为连接浏览器交互与 Claude 认知的关键桥梁。

关键实现:.events 文件机制详解

.events 文件是这个非阻塞设计的核心技术实现。在 server.cjs 中,其写入和清理逻辑如下:

  • 写入用户交互事件:在 handleMessage 函数中,当 WebSocket 收到来自浏览器 helper.js 的消息时,如果事件包含 choice 字段(即这是一个用户的选择点击),服务器会执行:

    // server.cjs - handleMessage function
    if (event.choice) {
      const eventsFile = path.join(STATE_DIR, 'events');
      fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
    }

    这确保了只有用户产生的选择事件(如 click)被记录,而服务器的生命周期事件(如 server-started)不会写入,保持文件的小而专注。测试 server.test.js 中的 'writes choice events to state/events' 测试用例验证了这一行为。

  • 新屏幕时清理事件:当文件监听器检测到一个新的 HTML 文件(代表一个新的屏幕)被添加时,会删除旧的 .events 文件,防止上一屏幕的点击数据污染新屏幕的决策。逻辑在 server.cjs 的文件监听回调中:

    // server.cjs - fs.watch callback
    if (!knownFiles.has(filename)) {
      knownFiles.add(filename);
      const eventsFile = path.join(STATE_DIR, 'events');
      if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
      // ... log screen-added
    }

    测试用例 'clears state/events on new screen' 精确地验证了此清理逻辑。

  • 事件格式与解读.events 文件是 JSONL(JSON Lines)格式,每一行是一个用户交互对象。从 visual-companion.md 的示例可以看到,它记录了完整的探索路径,而不仅仅是最终选择。Claude 可以分析整个点击序列来理解用户的犹豫或偏好。

实现计划中的具体改动

从阻塞模型迁移到非阻塞模型,实施计划(2026-02-19-visual-brainstorming-refactor.md)涉及了多个文件的协同改动,这些改动共同确保了新模型的落地:

  1. frame-template.html:移除了旧的 feedback-feedback div(包含文本输入框和发送按钮),替换为一个简洁的选择指示栏。这个栏会显示默认提示“Click an option above, then return to the terminal”,并在用户选择后更新为“Option B selected — return to terminal to continue”。这视觉上引导用户回到终端继续对话。
  2. helper.js:进行了大幅简化。删除了 sendToClaude() 函数、表单提交处理程序等所有与“从浏览器发送文本反馈”相关的代码。将点击事件监听器的作用范围缩窄,只捕获带有 [data-choice] 属性的元素的点击。同时,更新了 window.brainstorm API,移除了 sendToClaude,保留了 sendchoice 方法供高级自定义使用。测试 'helper.js defines required APIs' 确认了新的 API 表面。
  3. index.js:实现了 .events 文件的写入和清理逻辑(如上文所述),并更换了内容注入方式,使用简单的 <!-- CONTENT --> 注释占位符替换旧的正则匹配,使 wrapInFrame 函数更健壮。
  4. wait-for-feedback.sh:被完全删除。它的唯一用途——作为“服务器将事件记录到 stdout”和“Claude 需要接收这些事件”之间的桥梁——已被 .events 文件彻底取代。
  5. visual-companion.md:技能指令被重写,移除了所有关于 TaskOutput、超时、重试的描述,并用新的非阻塞循环流程和 .events 文件文档取而代之。

测试套件 server.test.js 的更新(如检查 indicator-bar 替代 feedback-feedback,验证 .events 文件写入与清理)为整个重构提供了安全网,确保新旧功能切换平滑且可靠。

跨平台兼容性与权衡

这种非阻塞设计带来了显著的跨平台优势。服务器代码(server.cjshelper.jsframe-template.html)是纯 Node.js 和浏览器 JavaScript,完全平台无关。技能指令层(visual-companion.md)是适配不同平台(Claude Code, Codex, Gemini CLI 等)的唯一地方,每个平台的代理只需用其原生方式启动服务器、读取 .events 文件即可。

当然,这种设计也伴随着明确的权衡(What This Drops):

  • 用户必须从浏览器窗口切换回终端才能继续对话,虽然选择指示栏提供了明确指引,但比旧模式下直接在浏览器点击“发送并等待”多了一个步骤。
  • 纯浏览器的文本输入通道被移除,所有文本反馈都通过终端进行。这被有意设计,因为终端是比框架内小文本框更高效的文本输入环境。
  • 不存在浏览器“发送”后的即时响应,用户需要切换并输入终端消息。实践中这个间隔只有几秒,但用户可以在消息中加入更多上下文。

总结

Visual Brainstorming 的非阻塞重构,本质上是将一个试图在回合制系统中模拟事件驱动的架构,纠正为一个严格遵守回合制交互模型的架构。通过明确“浏览器为显示终端,终端为对话通道”的职责分离,并引入 .events 文件作为可靠的交互记录层,SuperPowers 确保了在进行复杂的视觉设计讨论时,用户与 AI 编码代理之间的对话流不会中断,为高质量、高参与度的协作式头脑风暴奠定了技术基础。

FAQ

Q: 为什么不能在浏览器里直接输入文字反馈给 Claude? A: 这是架构的有意设计。终端是更强大、更自然的文本输入环境,允许用户一次性提供复杂的、带上下文的反馈。浏览器内的文本输入框会限制输入能力并破坏交互模型。所有文本反馈都通过终端进行,浏览器只负责记录点击选择。

Q: .events 文件如果不存在意味着什么? A: 如果 Claude 在读取 $STATE_DIR/.events 时发现文件不存在,这表明在上一轮浏览器展示期间,用户没有进行任何点击交互。在这种情况下,Claude 会完全依赖用户在终端输入的文字信息来做出决策和推进流程。