跳到內容

pi extension

pi-better-show 是一個 pi 套件:它加入 show_doc,把 agent 的文件顯示在你的預設瀏覽器或編輯器,並把你在那裡做的事當成訊息送回給 agent。它取代 pi-herdr-doc-review(同名工具,不需要 Herdr,也沒有終端機 pane)。

終端機視窗
pi uninstall npm:pi-herdr-doc-review # same tool name: do not load both
pi install npm:pi-better-show

接著在執行中的 session 裡 /reload。

show_doc({paths, reason, view}) 會立刻回傳,不等你;你在頁面或編輯器裡做的事,之後會以訊息的形式送到 agent。

sequenceDiagram
  participant A as Agent
  participant E as show_doc
  participant S as 頁面伺服器
  participant B as 瀏覽器頁面
  A->>E: show_doc(paths, reason, "page")
  E->>S: 只啟動一次,放行這些檔案
  E->>B: 開啟 URL
  E-->>A: 立刻回傳「Opened ... and returned」
  Note over B: 使用者作答,<br/>或在面板留言
  B->>S: POST /answer {answerId, answer, page}
  S->>E: 檢查形狀,檢查來源
  E->>A: 以 encodeAnswer 寫成訊息<br/>(steer 進去,或開始一輪)
  A->>E: write 或 edit 檔案
  S-->>B: "reload" 事件
兩種檢視
檢視 開啟
page 預設瀏覽器(open、xdg-open,Windows 上是 %SystemRoot% 底下的 rundll32 url.dll,FileProtocolHandler),一頁一個分頁,在由隨機 token 保護的 loopback 頁面伺服器上。.md 連同區塊一起渲染;.html、.tsx、.jsx 在沙箱 frame 中執行;圖片照原樣顯示。
edit(預設) 在你的編輯器開啟文字檔:PI_BETTER_SHOW_EDITOR(一行指令,例如 code -r 或 subl),否則 code,再否則 open -t / xdg-open / Windows 的記事本(Windows 上 code 只算 code.exe:VS Code 的 code.cmd 需要 shell,所以請在 PI_BETTER_SHOW_EDITOR 用完整路徑指定 Code.exe;那裡的反斜線是字面的)。它必須立刻回傳;終端機編輯器行不通。

你關掉分頁的頁面,在 agent 再次顯示它時會重新開啟。view: "page" 搭配不是 Markdown、React、HTML 或圖片的檔案會被拒絕,二進位檔在兩種檢視都會被拒絕。

show_doc 會告訴 agent 文件的區塊哪裡有問題,所以剖析不了的區塊下一輪就會被修好。

檢查什麼、在哪裡回報

show_doc 會把它開啟的每份 Markdown 文件的問題(checkBlocks,見 Core API)附在平常的回覆後面;agent 的 write 與 edit 工具,對含有區塊 fence 的 .md 檔,也會在自己的結果裡拿到:Block problems in questions.md: 接著 - line 40 form question "o5" choice 1 label (warning): ...。沒有區塊 fence 的檔案檢查器不會讀;YAML 剖析器與 Mermaid 在第一個需要它們的檔案才載入。Mermaid 在 extension 裡用 beautiful-mermaid 檢查,它會驗證圖的種類並剖析它畫得出來的東西;它抓不到流程圖的每一個語法小錯。

Markdown 頁面是文章加區塊:文章是 Markdown,原始 HTML 保留為文字,上面的每種區塊都會變成它的元素。表單、挑選、chip、checklist 或 review 會以訊息的形式送到 agent:steer 進進行中的一輪,agent 閒置時則開始新的一輪(encodeAnswer)。

頁面的限制

圖在伺服器上畫,依頁面的配色。圖片與連結依頁面所在的目錄解析:不會從它上層、從點開頭的檔案或目錄,或從其他網站載入任何東西。每個回應都帶 CSP,伺服器只回應 loopback 主機。對 /answer 的 POST 必須來自頁面本身(看它的 Origin)且帶 JSON,並由伺服器指明是哪份文件,所以頁面不能冒充別人作答。

選取一段文字就能留言,也可以在流程圖或狀態圖裡點節點、邊或 subgraph 標題(用鍵盤時 Tab 到這張圖,用方向鍵移到元素,再按 Enter 或空白鍵;在面板裡按 Escape 會回到它,除非這個 Escape 是在結束輸入法的組字)。面板會收集留言與整體意見,在一則訊息中送出;它的草稿能撐過 agent 修改造成的重新載入。它是一般的 feedback 答案:每則 quote 加上它的留言,最後是 Overall: …。

圖上的元素會以圖自己的用語被引用,agent 才能在它寫的原始碼中找到它。

flowchart TD
  svg["圖的 SVG<br/>來自 /diagram.svg"] -->|"fetchDiagramMap"| map["目標:節點、邊、<br/>群組,連同它們的形狀"]
  map -->|"圖上的按鈕"| press["點擊,或 Tab、方向鍵,<br/>再按 Enter 或空白鍵"]
  press -->|"pbs-diagram-target"| quote["diagramQuote:<br/>[Diagram 2, type] kind: text"]
  quote --> panel["意見面板<br/>每個元素一筆"]
  panel -->|"Send:quote 與留言"| msg["encodeAnswer:<br/>> quote,接著是留言"]
quote 怎麼讀

quote 以圖在頁面所有圖中的位置與它的種類稱呼這張圖,再接元素:> [Diagram 2, stateDiagram-v2] edge: Paid → Refunded "money back"、> [Diagram 1, flowchart] node: Check、> [Diagram 1, flowchart] group: Review。節點就是它的標籤(狀態圖的 [*] 寫成 start 與 end);邊是 <from> → <to>,兩端用它們各自 quote 的名稱,有標籤時加上用雙引號括起來的邊標籤;群組是 subgraph 的標題。標籤裡的 Mermaid 實體碼(#quot;、#35;)會寫成那個字元。quote 是給人讀的,不是要剖析的文法。

quote 只指一個元素:只有圖裡兩個元素讀起來一樣時,才會加後綴,也就是節點或群組的 id(node: Check (B)、group: One (S1)),以及旁邊有名為 start 或 end 的狀態時,[*] 加上的 ([*])(node: start ([*])),那個狀態則保留原名。邊的兩端照它們的 quote 來讀(edge: Check (A) → Done);只有兩端跨種類讀起來一樣(節點與群組同名)的邊,才加上兩端的 id(edge: One → B (S → B)),仍然一樣的邊再加上依繪製順序的編號(edge: A → B "retry" #2)。編號接在一串圖裡沒有任何標籤或 id 含有的 # 後面,所以絕不會讀成標籤的一部分:旁邊有標籤為 B #1 的節點時,兩條 A → B 邊讀成 edge: A → B ##1 與 edge: A → B ##2。狀態圖有多個 [*] 時,複合狀態裡面的每一個會照那個狀態的 quote 指名它:node: start (in Active)、edge: Busy → end (in Active),兩個狀態同名時則是 node: start (in Work (W1));最外層的仍是 start。它是 feedback 答案裡一般的 quote,所以訊息的形狀與選取文字時相同,點同一個元素會打開它原本的草稿,不會再加一則。草稿為每則圖的 quote 記住它的圖的原始碼,所以 agent 的修改造成重新載入後,前面多了或少了圖時,quote 會指出那張圖的新位置;自己的原始碼改了的圖,則保留 quote 原本的位置。這則 quote 若與另一則草稿的 quote 讀起來一樣,就加上 (from an earlier version of the page)(仍然一樣時再加 #2、 #3),所以兩則圖表草稿絕不共用一個 quote,對現在位在那個位置的元素新留的言,也會自成一則草稿。在 frame 中執行的頁面(.html、.tsx)只能對選取的文字留言。只有流程圖與狀態圖有目標;其他圖種維持純圖片。

agent 需要區塊以外的東西時,會自己寫一頁:.html,或 .tsx/.jsx。兩者都在 <iframe sandbox="allow-scripts allow-forms"> 中執行,沒有 allow-same-origin:沒有儲存空間、沒有 cookie,也沒有自己作答的辦法。<form action="cgi-bin:pi-answer"> 或 piBetterShow.send(fields) 透過 frame 外面的頁面作答。

答案如何離開 frame

伺服器會拒絕 Origin 不是自己的 POST(frame 的是 null)或不是 JSON 的 POST,也拒絕不是從自己的頁面開啟的事件串流。frame 以 postMessage 把答案交給外面的頁面;那個頁面把它當成自己的送出,agent 讀到的訊息以 Agent page <name> sent: 開頭,因為 agent 寫的頁面執行的是自己的腳本,它的送出並不是使用者的點擊。

frame 外的頁面帶著與 Markdown 頁面相同的意見面板:frame 用 postMessage({source: "pbs-frame", type: "selection", text}) 向它的父頁面回報使用者選取的文字(截成 2000 字元,只接受來自頁面自己的 frame、origin 為 null 者),使用者對段落留言並加上整體意見,一次 Send 就送出一份 feedback 答案,名稱取 HTML 的 <title>,否則取檔名。

TSX 頁面的運作方式相同。它的預設匯出是一個 React 元件,每次載入都由 esbuild 用這個套件自帶的 React 建置;import {PbsBlock, PbsForm, ..., parse, encodeAnswer, send} from "pi-kit" 會給它這些包裝元件。

在編輯模式中,你的編輯器開啟檔案,extension 監看它;agent 讀到的是淨變化,而不是每一次儲存。

flowchart TD
  save["你儲存檔案"] --> watch["extension 看到變化,<br/>並保留 agent 上次<br/>看到的版本"]
  watch --> idle{"agent 閒置?"}
  idle -->|"是"| held["只告訴你一次:<br/>編輯會等你的下一則訊息"]
  held -->|"你的下一個 prompt"| start["這一輪開始時帶著<br/>每個變動檔案的淨 diff"]
  idle -->|"否,正在工作"| quiet["3 秒沒有儲存"]
  quiet --> steer["steer 進進行中的一輪"]
  idle -->|"否,且 agent 即將<br/>寫入你改過的檔案"| first["你的變更先送出,<br/>立刻"]
agent 收到什麼,以及它的上限

儲存不會一次一次送出:agent 拿到的是每個檔案自它上次看到的版本以來的淨變化,寫成零上下文的 diff(@@ 標頭帶行號),一則訊息、每個檔案一段。閒置時,它會附在你的下一則訊息;agent 工作時,則在你停止儲存 3 秒後 steer 進去;agent 要寫你改過的檔案之前,你的變更會先送出。中間的儲存永遠不會到達 agent,在回報前被復原的變更什麼都不送,超過 200 行的 diff 會被截斷並註明省略的行數。與 agent 自己的寫入在同一瞬間發生的儲存不會被回報。

訊息寫成 User edited notes.md:,接著是 diff fence 裡的 diff。你儲存時若有 shell 指令正在執行,則改寫成 notes.md changed while a shell command ran; this may include the command's own changes, not only the user's edits:。

開啟的分頁跟著檔案走:agent 編輯顯示中的文件(write、edit 或 bash)時,分頁會重新載入。你關掉最後一個分頁後,伺服器 30 秒後停止,下一次 show_doc 會啟動新的並再次開啟它的頁面;顯示一個你關掉分頁的頁面,會重新開啟它。

agent 寫了標頭是 Status: proposed 的 docs/specs/<slug>/spec.md 時,該資料夾的 decision.md、spec.md、design.md 與 verification.md 會自己以頁面開啟(只在 pi 有 UI 時);之後才寫的階段檔會跟著加入。PI_BETTER_SHOW_AUTO_OPEN=off 可以停用。

其他檢視器(pi-straw-boss 的 GUI)透過 pi.events 讀取同一批文件,所以契約有版本:每個 payload 都帶 version: 2。

四個事件
事件 Payload
better-show:documents {version, documents},每次變動(文件被顯示、被 agent 修改或被你儲存、頁面被關閉)都完整送出,session 結束時送 []。每一項是 {id, kind: "blocks" | "html" | "text", display, reason, revision, onScreen, capability?, assets?}。沒有路徑:display 是相對於工作目錄,否則是 ~ 路徑,再否則是檔名。頁面伺服器執行期間,對在畫面上或在編輯器中開啟的文件,capability 是提供該文件的 URL(blocks 與 text 是 Markdown 文字;html 是 frame 載入的頁面,要放在帶 sandbox="allow-scripts allow-forms" 的 iframe 中顯示),assets 是它的相對參照所解析的目錄 URL。清單保留最近的 32 份文件。
better-show:documents-request {version} 要求再送一次清單;它會在 better-show:documents 上送出一次。別的 version,或 session 結束後的請求,會被忽略。
better-show:answer {version, id, answerId, answer},answer 是一個 Answer。它到達 agent 時,與頁面作答產生的訊息相同。
better-show:answer-result {version, id, answerId, accepted, reason?};reason 是 closed、invalid(版本或形狀錯誤,或是 text 文件)、empty,或傳遞錯誤。重複的 answerId 會拿回第一次的結果,且不會再傳遞一次。沒有字串 id 與 answerId 的 payload 得不到回覆。

在 frame 中顯示 html 文件的檢視器,會以 frame 傳來的 postMessage({source: "pbs-frame", type: "answer", answerId, answer})(origin 為 "null")收到它的答案,並回覆 {source: "pbs-host", type: "answer-result", answerId, accepted, reason?}。使用者在 frame 中的選取一有變化(清除時 text 為空),它也會收到 {source: "pbs-frame", type: "selection", text, top?, left?},並可以用它提供自己的意見面板。

移除它並安裝這個套件;show_doc 保留名稱與參數。不再有的:Herdr pane、nvim 與 vim、Chawan 與 terminal-browser,以及 <pi-*> page-kit 標籤(改寫 form、review、… 區塊,或使用 pbs-* 元素)。事件名稱從 doc-review:*(版本 1)改為 better-show:*(版本 2)。

不用 pi 也能試:bun run show 在 examples/ 上啟動頁面伺服器,並附一個假的 pi,印出 agent 會收到的每則訊息。