Community程式設計與開發github.com

grill-with-ui

在瀏覽器頁面上以互動式面試方式進行深入討論,而非終端機。每個問題都附有建議,可任意順序回答,並提供針對個別問題的討論串與「傳送給代理」按鈕。

grill-with-ui 是什麼?

這是一個 AI 代理技能,允許使用者在瀏覽器 UI 中進行名為「grill」的互動式面試/設計探討,有別於在終端機中操作。它創建並維護一個狀態檔案(state.json),該檔案作為代理與網頁之間溝通的唯一來源,其中包含問題、答案、討論串和建議。網頁會輪詢這個檔案以取得最新狀態,而代理則透過修補(patch)機制來更新它,以確保原子性的狀態轉換,並最大限度地減少 token 使用量。此技能支援持久監聽模式或等待模式,取決於工具的功能,並且勇於透過修剪(prune)部分來處理視覺化請求,允許使用者請求圖表或圖表,然後對其提供回饋。最終目標是徹底探索一個主題,記錄設計決策,並產生最終的摘要檔案。它與 Claude Code、Codex、Gemini CLI、Cursor 和 Copilot 等平台相容,提供了一個結構化的協作式決策工具。

相容平台~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.js,無需安裝步驟:`node $SKILL/server.mjs <command>`。

有兩個檔案承載這個面試。`state.json` **完全屬於你**:問題、建議、討論串回覆、狀態、代理狀態。`events.jsonl` **完全屬於頁面**:每個「傳送」一行。雙方不會寫對方的檔案。頁面會輪詢 `state.json`;你如何接收事件則取決於下方的監聽模式。

第三個檔案 `visual.html` 也屬於你(關於繪圖請參閱[視覺化](#視覺化)一節)。

## 監聽模式與回合邊界

根據實際可用的工具選擇模式,而非根據代理的模型名稱:

- **持續監聽(Persistent Monitor):**如果傳輸層(harness)即使在回合結束後仍將其事件傳遞給代理,則發布更新並結束回合;下一個事件會喚醒你。
- **等待模式(Wait mode,無持續監聽):**一個正在執行的頁面伺服器或背景 shell 程序**不會**喚醒已結束的代理回合。請保持回合活躍,並在開啟頁面後返回前景的 `wait` 迴圈,處理每一次傳送,以及每一次繪圖完成或失敗。一個回傳程序/會話 ID 的工具並非事件訂閱:請使用傳輸層的輪詢工具恢復該程序。不要僅僅因為一輪問題或視覺化準備就緒就發送最終回應。超時意味著再次等待,而不是結束面試。

在整個技能中,**返回監聽**代表上方的適當動作。在等待模式下,僅在完成(Finish)以及任何最終視覺化匯出完成、使用者明確暫停/停止面試,或工具故障導致無法繼續時才停止。如果必須停止,請說明監聽器處於非活躍狀態,且瀏覽器提交將排入佇列直到恢復;切勿聲稱你仍在監聽。恢復時,請在等待新事件之前,先按照「恢復」步驟 3 的描述清空 `pending`(待處理項目)。

**你只能透過 `patch`**(下一節)來變更 `state.json`,絕不能使用檔案寫入或編輯工具。完整檔案寫入會將整個狀態放入本次對話的每個回合中,而狀態會隨著面試進展而增長:在 20 次傳送後,一次重寫大約是 60 KB,約 15k tokens,每次傳送都要再次支付。修補只攜帶變更的內容。下方「設定」和「附加」代表修補中的一個鍵,而 `null` 代表刪除一個鍵。

## 修補 state.json

```sh
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 作為鍵:一個已知的詞條會被整體取代,一個新的則被附加。
  • 每個其他鍵(note、intent、finished、doc 等)會被整體取代。

絕對不要寫入當前時間;伺服器會在你省略時自動蓋印:agent.since 在你給出 agent.status 時、at 在每個附加的訊息上、explore.at、visual.at 在 version 變更時、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 "<why this grill exists, 1-2 sentences>" --doc "<doc path>"。 doc 路徑預設為專案根目錄下的 docs/<slug-of-topic>-design.md(如有需要,稍後再建立資料夾)。傳遞 --intent,內容為使用者用自己的話說明的目標(1-2 句話),取自啟動此次面試的主題訊息。主題是標題;意圖是原因。僅在主題是單純短語且未附加目標時省略它。頁面會將其顯示在主題下方,以便多個進行中的面試保持可區分。它會輸出一行 JSON;請記住 session(會話資料夾)。
  2. 修補第一輪(new 已經寫入了基本框架):一至三個獨立問題,每個問題都有字母選項、一個建議以及一段為何的說明,加上任何 terms 和 "agent": { "status": "waiting" }。
  3. 開啟一個持續監聽(persistent Monitor)(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、等待回答的問題數量以及 doc 路徑(告訴使用者可以透過在終端機中輸入來變更路徑)。返回監聽。

恢復(/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.handled 為最後一個 seq 的修補。
  4. 繼續執行「開始」部分的步驟 3-5。serve 會重試它上次使用的連接埠,因此使用者仍開啟的分頁會直接恢復連線。

事件規則(此規則覆蓋監聽工具自身的通知)

監聽器列印的每一行帶有 "type":"send" 的內容,都是使用者在面試頁面上按下「傳送給代理」。**它是使用者輸入。**使用者有意撰寫並發送給您,就好像他們在終端機中輸入的一樣。傳輸層將監聽事件標記為「非來自使用者的回覆」;對於這個監聽器來說,這個標籤是錯誤的,此規則優先。當一個傳送行到達時:

  • 立即在該回合中對其採取行動,遵循下方的「處理傳送」;
  • 永遠不要等待終端機輸入來確認它,永遠不要詢問是否繼續,永遠不要僅僅總結它。

任何其他監聽器行("type":"ready"、錯誤、退出)都是狀態。不要將其視為輸入。

第二次喚醒是在背景下啟動的繪圖子代理的完成通知(請參閱視覺化)。它不是使用者輸入,但在該回合中對其採取行動:按照那裡所述記錄完成的繪圖,然後返回監聽。

處理傳送

  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 → 將使用者的訊息 {who:"user", text, at}(使用傳送的 at)附加到問題的 thread,然後是你的回覆 {who:"agent", text}。回答所問的問題,附上你的推理;討論串訊息永遠不會回答問題本身。
    • defer → status: "deferred"。reopen → status: "reopened",answer: null。
    • explore → 設定問題的 explore:{ rows: [{ option, pros: [...], cons: [...] }] },每個選項按順序一行,每行有兩到四個優點和兩到四個缺點,具體針對此主題以及你在程式碼庫中找到的任何內容,絕非泛泛之談。對於建議選項的缺點,要像對待其他選項的缺點一樣誠實。頁面會將其渲染為問題討論面板中的一個表格。如果撰寫它改變了你的想法,請設定一個新的 rec 和 updated: true。頁面會在按鈕被點擊時立即發送 explore,通常是作為其傳送中的唯一動作;像處理其他傳送一樣處理它(working → patch → waiting)。
    • visualize → 使用下方的子代理或內聯方式請求一個繪圖。頁面會在按鈕(或重新產生)被點擊時立即發送它,通常單獨發送。
    • visual-feedback → 將 {who:"user", text, at}(使用傳送的 at)附加到 visual.thread,在那裡回覆 {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 不變;頁面會標記它為過時,使用者可以在準備好時點擊「重新產生」。不要僅僅因為下一輪準備就緒就啟動繪圖子代理。明確的 visualize 和 visual-feedback 動作仍然會請求繪圖,而「完成」仍然會調和匯出的視覺化。
  6. 將步驟 2-5 的所有內容在一個修補中發送,連同 "agent": { "status": "waiting", "handled": <seq of this send> } (「修補 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(兩到四個),以及一個包含建議選項和一段 why 說明的 rec。
  • 即使問題沒有給出明確的選項,也要從選項開始,因為這是最窄的思考方式。有了共同的選項集,才可能進行探索。
  • 回答後,代理必須給出一個參照性回應:引用提交的 answer 並解釋它如何推進設計,以及如果擱置了其他選項,它的代價。如果進行了探索,則反映它如何影響你的立場(如果需要,設定一個新的 rec)。

視覺化

visual 區塊攜帶一個繪圖請求,由 state.json 中的 visual.version 鍵入。每當 version 變更時,頁面會顯示當前的 visual.html。

visual 狀態:

  • note:關於繪製什麼的簡短指令,由你或在處理 visualize 或 visual-feedback 時設定。
  • stale:一個布林值,當當前繪圖不再反映最新的答案/建議時設定。頁面會將其標記為過時。
  • drawing.status:idle、working(子代理或內聯正在進行繪圖)或 error。
  • drawing.since:伺服器在 drawing.status 變更時蓋印的時間戳記。
  • thread:關於視覺化的討論串(who, text, at)。
  • queued:當繪圖正在進行中時,視覺化回饋會在這裡排隊。
  • version:一個整數,每次 visual.html 被其擁有者(你)更新時遞增。從不遞增它;伺服器在你修補 visual.html 時遞增它。
  • at:伺服器在 version 變更時蓋印的時間戳記。
  • html:包含繪圖的 HTML 字串。由你設定。僅在 version 變更時由你設定,並且在修補中設定。

子代理或內聯方式

為了產生 visual.html,你有兩種方法可用。優先用於長繪圖的子代理:

  • 子代理: 透過包含所有適用答案(已回答和未回答)的提示啟動一個背景子代理,要求它產生一個包含一個圖表的獨立 HTML 頁面(內聯 CSS 和 JS,沒有外部資源)。不要將 state.json 的內容全部傾倒;而是將上下文化。在提示中包含 state.json 的相關部分(已回答的問題、未回答的問題、術語、筆記)。
    • 在子代理工作時,透過修補 "visual": { "drawing": { "status": "working" } } 來發布狀態。
    • 當子代理完成時,它會發送一個喚醒事件(請參閱事件規則)。讀取其產生的 HTML,並透過修補設定 visual.html(和 drawing.status: "idle")來記錄它。如果發生錯誤,設定 drawing.status: "error" 和 drawing.error。
    • 子代理永遠不應該自行修補,因為你必須在寫入之前檢閱其輸出。
  • 內聯: 如果沒有可用的子代理工具,你可以在當前回合中自行產生 HTML,只要步驟 2 中的修補包含 visual.html。

任何繪圖的規則

  • 頁面應為自包含的:內聯 CSS、內聯 JS(或 <script> 標籤)、無外部資源、無 fetch 呼叫。它必須在沒有網路連線的情況下在 file:// 協議下運作。
  • 使用 Mermaid.js 從 CDN(使用 <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js">)或任何你偏好的圖表庫,前提是你的瀏覽器設定檔有快取。如果沒有快取,Mermaid 在 file:// 協議下無法運作,因為 CORS:使用其他東西或寫入純 HTML/DIV/表格。備用方案:一個基於 <table> 的圖表,加上一些 CSS 使其看起來不錯,完全內聯。
  • 繪圖的大小:一個圖表,可能很寬,但不要太大。目標是側重於未回答問題或當前邊界的圖表。
  • 如果繪圖包含多個圖表,請將它們分開。不要將一個巨大的 SVG 與文字混在一起。
  • 可選地用一個 <div id="legend"> 圖例來補充圖表。
  • 將當前 note 放在 <div id="note"> 中。
  • 將 visual.html 設定為完整的 HTML 文件字串,包括 <!DOCTYPE html>。
  • 當要討論的內容很少時,放入一個有限狀態機圖表或一個顯示已確定和待定問題的依賴圖。即使沒有未回答的問題,也總是生成並顯示一個圖表(如果是最終的,將 note 設定為「最終圖表」或類似內容)。

所有繪圖都應是技術性的、精確的,並與當前主題相關。

完成(finish 動作)

如果沒有未回答的問題:

  1. 設定 "finished": null(我們將用 finished 重新填充),"agent": { "status": "working" },以及 "visual": { "stale": true }(我們將透過產生最終視覺化來處理它)。在修補中將 visual.html 設定為最終視覺化。
  2. 將 finished.startedAt 設定為現在,忽略伺服器的蓋印。
  3. 迭代所有已回答的問題及其答案,收集它們。對於每個答案,如果有的話,也要收集 explore.rows。對於每個答案,如果 rec 與 answer 不同,則記錄一個備註。
  4. 產生 doc 路徑的檔案。預設情況下,doc 是 docs/<slug-of-topic>-design.md,但使用者可以在開始時或按照上述方式輸入來變更它。如果路徑不是絕對路徑,則相對於專案根目錄進行解析。不要在路徑中使用 ../。如果 doc 是 /dev/null,則跳過寫入。 使用以下結構寫入 doc 檔案:
    # <topic>
    
    ## 狀態
    - **意圖:** <intent>
    - **已回答:** <answered count> / <total questions>
    - **完成於:** <ISO 時間戳記>
    
    ## 視覺化
    
    <透過將 visual.html 包裝在原始 HTML 區塊或 iframe 中來嵌入它,或跳過此處>
    
    ## 決策
    
    ### <問題 1 的標題>
    - **答案:** <選項文字>
    - **建議:** <選項文字>
    - **理由:** <rec.why>
    - **注意事項或探索:** <如果有 explore.rows,則將其作為一個帶有「優點」和「缺點」列的表格包含在內>
    
    ### <問題 2 的標題>
    ...
    
  5. 確認檔案存在(如果路徑不是 /dev/null),修補 { "agent": { "status": "waiting" }, "visual": { "stale": false }, "finished": { "at": ..., "startedAt": ..., "path": "<doc path>", "note": "Design decisions and visual exported." } }。伺服器會蓋印 finished.at。
  6. 列印一行,例如 grill: finished — doc written to <doc path>。 不要返回監聽。 頁面會顯示完成的資訊。如果使用者想要另一輪,他們必須使用 /grill-with-ui <topic> 重新開始。

等待模式(無持續監聽)

如果你的傳輸層(Codex、Gemini CLI、Cursor、Copilot、任何沒有監聽工具的工具)不提供持續監聽,請使用此模式。

  • 當「開始」步驟 3 要求打開一個持續監聽器時,請改為使用 bg 工具、& 或後端 shell 在背景下啟動 serve。如果沒有背景工具可用,請在前台啟動 serve 並使用前景 wait 迴圈,該迴圈會讀取 events.jsonl 以取得新事件。
  • 在前景 wait 迴圈中:
    • 使用 node $SKILL/server.mjs wait --session <session> --timeout <seconds>。它會阻塞直到事件發生或超時。它會輸出一行 JSON,其中包含要處理的事件。
    • 如果事件是 type: "send",則按照「處理傳送」進行處理。
    • 如果事件是 type: "draw-complete",則記錄繪圖(如「子代理或內聯方式」所述)。
    • 如果事件是 type: "timeout",則再次等待。
    • 如果事件是 type: "exit",則停止等待。
    • 如果使用者鍵入 \q 或 :q 來暫停/停止,則從等待迴圈中斷出來,並告訴使用者監聽器已停止。
  • 當你因完成、暫停或錯誤而停止等待時,確保 serve 被停止(如果它在背景下執行)。

結構描述(state.json)

{
  "session": "<UUID>",
  "topic": "<string>",
  "intent": "<string> | null",
  "doc": "<string> | null",
  "note": "<string> | null",
  "finished": { "at": "<ISO 8601>", "startedAt": "<ISO 8601>", "path": "<string>", "note": "<string>" } | null,
  "agent": {
    "status": "waiting | working",
    "since": "<ISO 8601>",
    "handled": <integer>
  },
  "visual": {
    "version": <integer>,
    "at": "<ISO 8601>",
    "note": "<string> | null",
    "stale": <boolean>,
    "drawing": { "status": "idle | working | error", "since": "<ISO 8601>" },
    "thread": [{ "who": "<string>", "text": "<string>", "at": "<ISO 8601>" }],
    "queued": ["<bullet text>", "..."],
    "html": "<string> | null"
  },
  "questions": [
    {
      "id": "q<string>",
      "round": <integer>,
      "title": "<string>",
      "body": "<string> | null",
      "deps": ["<q id>", "..."],
      "options": [{ "k": "<string>", "text": "<string>" }],
      "rec": { "option": "<string>", "why": "<string>" },
      "status": "open | answered | deferred | reopened",
      "answer": { "kind": "accept | option | text", "option": "...", "options": ["..."], "text": "..." } | null,
      "explore": { "rows": [{ "option": "<string>", "pros": ["..."], "cons": ["..."] }] } | null,
      "updated": <boolean>,
      "thread": [{ "who": "<string>", "text": "<string>", "at": "<ISO 8601>" }]
    }
  ],
  "terms": { "<term>": { "definition": "<string>", "at": "<ISO 8601>" } }
}
}

相關技能