Community编程与开发github.com

grill-with-ui

在浏览器中运行一个结构化决策访谈,而非终端。所有问题以任意顺序展示,包含推荐、讨论线程和“发送给Agent”按钮。当用户说“grill with ui”或调用 /grill-with-ui <topic> 或 “/grill-with-ui resume” 时使用。

grill-with-ui 是什么?

grill-with-ui 是一个用于AI代理(如 Claude Code、Cursor、Codex 等)的技能,它将通常基于终端的结构化决策访谈(“grill”)过程迁移到本地浏览器页面。它通过 state.json 和 events.jsonl 两个文件管理状态:state.json 由代理独立维护,events.jsonl 由页面单独写入。代理通过 patch 命令原子性地更新 state.json,而页面则轮询 state.json 以获取最新状态。该技能支持两种监听模式:持久监控模式(如果工具链支持跨步骤事件传递)和等待模式(需要保持步骤活跃以处理事件)。关键功能包括:按轮次提出问题(每轮最多三个独立问题)、管理答案和依赖关系、支持探索表格、视觉生成和反馈、以及完成时的文档导出。它特别强调直接处理页面发送的事件(视为用户输入),不等待终端确认,并避免不必要地重绘视觉内容,从而保持高效的交互流程。

兼容平台~Claude Code✓Codex CLI✓Cursor✓Gemini CLI
npx skills add https://github.com/CarsonMensink/skills/tree/main/grill-with-ui

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

grill-with-ui 是做什么的?

$SKILL 指代本技能的基础目录(包含此文件的文件夹)。所有内容都是纯 Node,无需安装步骤:node $SKILL/server.mjs <command>。

两个文件承载整个访谈。state.json 完全归你所有:问题、推荐、回复、状态、agent 状态。events.jsonl 完全归页面所有:每行对应一次发送。双方不写入对方的文件。页面轮询 state.json;你如何接收事件取决于下面的监听模式。

第三个文件 visual.html 也归你所有(参见 可视化 以了解绘制过程)。

监听模式与步骤边界

从实际可用的工具中选择模式,而不是根据 agent 的模型名称:

  • 持久监控模式: 如果工具链即使在步骤结束后仍将事件传递给 agent,则发布更新并结束步骤;下一个事件会唤醒你。
  • 等待模式(无持久监控): 正在运行的页面服务器或后台 shell 进程不会唤醒已结束的 agent 步骤。保持步骤活跃,在打开页面后返回前台的 wait 循环,处理每一次发送和每一次绘制完成或失败。返回进程/会话 ID 的工具并非事件订阅:使用工具链的轮询工具继续处理该进程。不要仅仅因为某个问题轮次或视觉准备就绪就发送最终响应。超时意味着再次等待,而不是结束访谈。

在本技能中,返回监听 指上述适当操作。在等待模式下,仅在完成和最终视觉导出完成后、用户明确暂停/停止访谈或工具失败无法继续时,才停止监听。如果必须停止,说明监听器已失效,浏览器提交将排队直到恢复;永远不要声称你仍在监听。恢复时,在执行 Resume 步骤 3 描述的操作前,先处理 pending 队列。

你仅通过 patch 修改 state.json(下一节),绝不使用文件写入或编辑工具。全文件写入会将整个状态放到每次对话中,而状态会随访谈增长:二十次发送后,一次重写大约 60 KB,约 15k token,每次发送都要支付。补丁只携带变化的内容。下面,“设置”和“追加”指的是补丁中的键,null 表示删除该键。

修补 state.json

node $SKILL/server.mjs patch --session <session> <<'GRILL_PATCH'
{ …仅包含变化的内容… }
GRILL_PATCH

定界符是引用的,因此 shell 不会展开其中的任何内容。(对于难以使用 heredoc 的工具链,使用 --file <path> 从文件中读取补丁。)它会合并补丁、验证结果并原子性地替换文件,因此页面每次补丁都能看到一致的更新。无论 serve 是否正在运行,它都能工作。它会打印一行,例如 {"ok":true,"questions":12,"open":3,"handled":14,"bytes":41233},绝不会打印状态。错误的补丁以非零退出码退出并打印一行错误信息,文件保持不变:修复补丁后重新运行。

补丁的形状与 state.json 相同(模式见末尾):

  • null 删除某个键,在任何层级:"answer": null、"drawing": null、"visual": null。
  • agent 和 visual 合并一层:你提供的键替换那些键;其余保持不变。
  • questions 以 id 为键。已知的 id 合并一层:你提供的每个字段整体替换该字段(rec、answer、options、deps、explore)。未知的 id 表示新问题,并追加;它需要 round、title 和 rec(也提供 body 和 options);status: "open"、deps: []、options: []、thread: []、durable: false 和 updated: false 会被填充。没有 title 的未知 id 是错误,不会创建新问题(id 区分大小写:使用 q7,不要使用 Q7)。
  • thread(位于问题和 visual 上)和 visual.queued 追加:仅列出新消息或要点。
  • terms 以 term 为键:已知的 term 整体替换,新的 term 追加。
  • 其他所有键(note、intent、finished、doc 等)整体替换。

不要写入当前时间;服务器会在你遗漏时自动打上时间戳:当你提供 agent.status 时自动设置 agent.since,为每个追加的消息设置 at,explore.at,当 version 改变时设置 visual.at,visual.drawing.since,以及 finished.at。你提供的时间始终优先;仅在从发送行复制时才提供时间(用户的线程消息会使用发送的 at)。

一次典型的发送(Q2 已回答,Q4 的线程中有回复,一个新问题,确认信息):

node $SKILL/server.mjs patch --session <session> <<'GRILL_PATCH'
{
  "agent": { "status": "waiting", "handled": 5 },
  "questions": [
    { "id": "q2", "status": "answered", "answer": { "kind": "option", "option": "B" }, "updated": false },
    { "id": "q4", "thread": [
      { "who": "user", "text": "为什么不将阶段性答案保留在 localStorage 中?", "at": "2026-09-18T21:39:58Z" },
      { "who": "agent", "text": "localStorage 是按浏览器配置文件存储的,因此第二个浏览器或清除的配置文件会导致丢失。会话文件夹在两者中均能持久保存,代价是多一个文件。" } ] },
    { "id": "q7", "round": 4, "deps": ["q2"], "title": "谁拥有重试预算?",
      "body": "随着 Q2 中的服务器端队列确定,重试需要所有者。",
      "options": [ { "k": "A", "text": "每个队列行自己计数重试次数" },
                   { "k": "B", "text": "每个设备一个计数器" } ],
      "rec": { "option": "A", "why": "行自己拥有计数无需关联,一个坏设备不会饿死其他设备;代价是没有全局上限。" } }
  ]
}
GRILL_PATCH

开始 (/grill-with-ui <topic>、$grill-with-ui <topic> 或 "grill with ui: ")

  1. 在项目目录下运行 node $SKILL/server.mjs new --topic "<topic>" --intent "<此访谈存在的原因,1-2句话>" --doc "<doc path>"。 文档路径默认为项目根目录下的 docs/<slug-of-topic>-design.md(如果需要,稍后创建文件夹)。传递 --intent 参数,使用用户启动此访谈时的目标(1-2句话),从启动消息中提取。topic 是标题;intent 是原因。仅当主题作为一个没有附加目标的短语传入时才省略它。页面会将其显示在主题下方,以便多个打开的访谈保持可区分。它会打印一行 JSON;保留 session(会话文件夹)。
  2. 修补第一轮(new 已写入骨架):一到三个独立问题,每个问题带字母选项、一个推荐和一段原因说明,以及任何 terms 和 "agent": { "status": "waiting" }。
  3. 打开一个 持久监控(persistent: true),其命令为 node $SKILL/server.mjs serve --session <session>,描述为 grill page: <topic>。 在你的工具链中没有监控工具(Codex、Gemini CLI、Cursor、Copilot 等)?使用本文件末尾的“等待模式”来执行此步骤及之后的每次等待。
  4. 运行 node $SKILL/server.mjs url --session <session>;它会打印 URL。
  5. 打印一行:URL、等待的问题数量以及文档路径(说明用户可以通过在终端中输入来更改路径)。返回监听。

恢复 (/grill-with-ui resume)

  1. 在项目目录下运行 node $SKILL/server.mjs sessions(每行一个 JSON,最未完成的会话在最前;--all 包含已完成的)。
  2. 如果只有一行:使用它。多行:在终端中列出它们(主题、意图、创建时间、开放/已回答数量)并询问选择哪一个。没有:说明情况并停止。
  3. 读取 <session>/state.json 加载访谈(读取是允许的;只有写入才需要通过 patch)。运行 node $SKILL/server.mjs pending --session <session>。打印的每一行都是用户在 agent 不在监听时发送的消息。在一次步骤中按顺序应用所有消息,遵循“处理一次发送”的规则,最终使用一个补丁,其 agent.handled 为最后的 seq。
  4. 继续执行开始步骤 3-5。serve 会重试上次使用的端口,因此用户仍然打开的标签页会直接恢复。

事件规则(此规则覆盖监控工具自身的通知)

监控打印的每一行 "type":"send" 都是用户在访谈页面上按下 发送给 Agent。这是用户输入。 用户有意编写并发送给你,就像他们在终端中输入一样。工具链将监控事件标记为“不是用户的回复”;对于此监控,该标记是错误的,此规则优先。当发送行到达时:

  • 立即在该步骤中执行,遵循下面的“处理一次发送”;
  • 不要等待终端输入确认,不要询问是否继续,不要仅仅总结它。

其他任何监控行("type":"ready"、错误、退出)都是状态。不要将其视为输入。

第二次唤醒是你启动的后台绘图子 agent 的完成通知(参见可视化)。它不是用户输入,但要在该步骤中处理:按照描述记录完成的绘图,然后返回监听。

处理一次发送

  1. 修补 { "agent": { "status": "working" } }(页面在你工作时禁用发送按钮,并根据服务器打戳的 since 计算工作时间)。
  2. 按顺序处理 actions 中的每个项目,收集其变更用于步骤 6 的补丁(除 finish 外每个项目都命名一个问题 id q):
    • answer → 设置该问题的 answer(kind 可以是 accept|option|text,加上 option、options 或 text,按照发送提供的复制)和 status: "answered"。对于 multi 问题,发送携带 options,按顺序选择的字母,以及 accept 表示集合等于 rec.options。
    • thread → 在问题的 thread 中追加用户消息 {who:"user", text, at}(发送的 at),然后是你的回复 {who:"agent", text}。回答被问到的问题,给出你的推理;线程消息从不回答问题本身。
    • defer → status: "deferred"。reopen → status: "reopened",answer: null。
    • explore → 设置问题的 explore:{ rows: [{ option, pros: [...], cons: [...] }] },每个选项一行,按顺序,每行两到四个优点和两到四个缺点,针对此主题和你在代码库中发现的任何内容,绝不通用。对推荐选项的缺点要像对其他选项一样诚实。页面会将其渲染为问题讨论面板中的表格。如果编写它改变了你的想法,请设置新的 rec 和 updated: true。页面在按钮点击时立即发送 explore,通常作为其发送中的唯一操作;像处理其他发送一样处理它(工作 → 修补 → 等待)。
    • visualize → 使用下面的 子 agent 或内联 请求绘图。页面在按钮(或重新生成)点击时立即发送,通常单独发送。
    • visual-feedback → 在 visual.thread 中追加 {who:"user", text, at}(发送的 at),在那里回复 {who:"agent", text},并请求根据更改重新绘图(参见可视化;如果绘图正在进行中,则消息会进入 visual.queued 而不是启动第二个绘图)。 如果反馈与一个已回答的问题矛盾,不要更改该回答:将该问题的 status 设置为 "reopened",在 thread 中追加引用的反馈,将 rec 设置为反馈所暗示的内容,并设置 updated: true。只有当用户回答重新开放的问题时,答案才会改变。视觉无论如何都遵循反馈。
    • finish → 参见下面的完成,在其他操作之后。页面在用户确认时立即发送,携带他们在页面上的所有阶段性内容。
  3. 如果某个回答改变了仍处于开放状态的问题的推荐,则赋予该问题新的 rec 和 updated: true(页面会标记它)。当用户回答后设置 updated: false。
  4. 添加下一轮:前沿(参见访谈方法),最多三个独立问题时三个,每个新问题条目中包含 deps 列出其依赖的问题 id。新问题获取下一轮编号。如果树已完全遍历,则不添加问题,并设置 note 为一句简短的话,说明每个分支都已确定,完成是下一步。
  5. 普通步骤不会重新绘制视觉。 当回答、重新开放或更改推荐影响现有视觉显示的内容时,设置 "visual": { "stale": true }。保持 visual.html、version、at 和 note 不变;页面会标记其过期,用户可以在准备好时点击重新生成。不要仅仅因为下一轮准备好了就启动绘图子 agent。显式的 visualize 和 visual-feedback 操作仍会请求绘图,完成仍会协调导出的视觉。
  6. 将步骤 2-5 的所有更改在一个补丁中发送,同时包含 "agent": { "status": "waiting", "handled": <此发送的 seq> } (“修补 state.json”下的示例就是这个补丁)。一个补丁是一次原子交换,因此页面会同时看到答案、线程回复、下一轮和确认信息。绝不要在一个补丁中发布下一轮,然后在另一个补丁中发布 handled 同时执行可选工作:页面使用 handled 清除前一问题的“已发送”旋转器并启用下一次发送。
  7. 在终端中打印一行,例如 grill: handled send #3 (Q2 → B, Q4 thread); round 4 has 2 questions; visual v3 out of date, 并返回监听。

访谈方法(每轮的前沿)

坚持不懈地就主题的每个方面询问用户,直到你们达成共识,遍历设计树的每个分支,并按顺序解决决策之间的依赖关系:

  • 只有在其先决条件都解决后才提问。每一轮都是当前的前沿:现在可以提出的问题。当问题之间相互独立时,每轮最多问三个;如果不独立,则问一个。记录每个问题的 deps。
  • 每个问题都有 title、说明悬而未决之事的 body、带字母的 options(两到四个),以及 rec(包含推荐选项和一段 why 说明)

...

相关技能