瀏覽器檢視
/boss gui 會在你的預設瀏覽器顯示整個網:一張遊戲風格的地圖,即時且帶動畫,你可以讀任何單位的通訊畫面,也可以對它的 session 打字。

這一節分成六組:地圖、基地與單位;警示與提問;單位頁面顯示的內容;關閉、開啟與信任;提供頁面(遠端與 Wi-Fi 存取、server);以及檢視、主題、手機與設定。
地圖、基地與單位
Section titled “地圖、基地與單位”任何 session 都能開啟它
Section titled “任何 session 都能開啟它”任何 session 都能開啟它,不論在 Herdr 工作區或子工作區,不論有沒有加入。它不改變任何角色,也不寫入 token。已加入的 session 透過它的分頁頻道讀取各 session;無法加入的(子工作區)則用它自己的頻道(straw-boss/view/<hex of the pane id>,絕不具備 owner 資格),這個頻道不參與任何分頁。在 Herdr 之外,擴充功能不啟用,所以沒有這個指令。
網所涵蓋的每個工作區都是一個基地:有存活塔台,或有已加入 session 的工作區。兩者皆無的工作區,以及沒有加入的 session,都不顯示。基地有它的塔台(主塔台戴著皇冠;沒有塔台的基地顯示暗色的幽靈)、每個分頁的主智慧體和成員(作為單位)、啟動它們的 session 底下的子智慧體、塔台底下塔台自己的子智慧體(連結方式與儀表板相同,不論它們在哪裡執行),以及作為父層底下前哨的子工作區。
細節
單位卡片顯示它在目前狀態已經多久(從伺服器看到它進入該狀態起算)、它的 todo(完成數/總數與進行中的任務,來自 rpiv-todo,由該 session 的 bridge 轉送),以及它最近幾步,以一排標記顯示在卡片底部。地圖下方的時間軸標出網在最近 30 分鐘內的加入、離開和狀態變化(參考 zoetrope):等待和錯誤在最上,工作在中間,其餘在下,滑鼠移上去顯示名稱,並連到它的單位。伺服器保存這些資料,所以重新載入的頁面仍然有;手機則把這條時間軸收合成只顯示最新的事件。
基地像 masonry 一樣依閱讀順序排列。地圖超出視窗時,角落的總覽以各基地的狀態顏色顯示每個基地,點擊哪裡就捲動到哪裡。
狀態在各處看起來都一樣:
| 狀態 | 外觀 |
|---|---|
| 執行中 | 旋轉的環和旋轉符號徽章 |
| 等待問題回覆 | 琥珀色的 ? 會跳動,帶脈動環 |
| 閒置 | 暗色的 z |
| 完成 | 綠色的 ✓ |
| 錯誤 | 紅色的 ! |
細節
變化會有動畫;頁面遵守 prefers-reduced-motion。通訊畫面開著的那個 session 正在做什麼(思考、使用工具、看著 Monitor、指揮、等待老闆)不是紀錄中的一列:它工作時,輸入框外圍有一圈環繞著跑,等待老闆時環靜止並呈琥珀色,同樣的意思也在 live region 裡用文字說出。
| 檢視 | 路由 |
|---|---|
| 地圖 | /<token>/ |
| 基地 | /<token>/w/<workspace id> |
| 單位 | /<token>/w/<workspace id>/s/<session key>,可選擇加上 ?at=<entry id>,把它的通訊畫面開在某一筆項目 |
細節
連結是真正的連結(中鍵點擊和複製連結都能用);一般點擊、上一頁和下一頁會在不重新載入的情況下切換檢視,Escape 則上移一層。標頭中的 Straw Net 標題會回到地圖。深層連結會直接載入它的檢視。
工作區路由把基地大幅畫在地圖的格線上:它的塔台、每個分頁作為一個由單位 token 組成的小隊(圖示和狀態徽章都相同)、繫在啟動它們的單位或塔台底下的子智慧體、前哨,以及工作區裡其他的 pi pane,作為未編列的單位(在網之外,灰色,帶虛線環)。每個單位顯示它最後一行助理輸出,從它的 session 檔結尾讀取。進入基地時,會從它在地圖上的位置縮小離開(zoom out)。
單位的通訊畫面
Section titled “單位的通訊畫面”session 路由把該 session 的對話顯示為通訊紀錄,側欄顯示單位的檔案(頭像、角色、狀態、模型、context 用量、目錄、基地、小隊、啟動者、派出的子智慧體)。它的終端機按鈕會把單位的終端機鏡像開成紀錄旁的一格。
你的訊息在右邊,金色;單位的傳送在左邊,帶著它的圖示。每個工具呼叫是一張收合的動作卡片,帶狀態徽章(執行中、完成、失敗),思考內容折疊在它的長度後面,壓縮(compaction)顯示為分隔線。助理訊息的標頭寫的是單位的角色(塔台、主智慧體、成員、子智慧體)加上模型和時間,而不是 session 的名稱(頁面標頭已經顯示);轉送來的訊息仍然寫出它的寄件者。
細節
新的項目在 pi 寫入時出現;還在串流中的文字還不在檔案裡,所以會顯示在回覆的結尾。紀錄開在最新的項目,並固定在那裡。往上捲時它留在原處,一顆浮動按鈕(跳到最新,有新項目進來後則是 N 則新的)會捲到結尾並再次固定。往上捲時會載入較舊的頁面;頁面中最多保留 600 筆項目。
終端機按鈕會在紀錄旁開一格該單位 pane 的即時鏡像。伺服器只送出顏色與屬性,頁面用文字畫出各列;pane 有變動時,鏡像在一秒內更新。
細節
這一格顯示單位的 pane 所顯示的內容,帶顏色和屬性,在內容變動時一秒內更新(每秒最多讀 4 次,由所有顯示它的頁面共用,沒有頁面顯示時則不讀)。伺服器只送出顏色和屬性的跳脫序列,頁面用文字建立各列。符合寬度把它縮放到符合該格並捲到底部,也就是 pi 編輯器所在處;否則它保持原尺寸並可橫向捲動,和手機上一樣。session 在等待 secret(secret_drop)時不讀取任何東西,該格也會這樣說明。某一格在它的瀏覽器分頁被隱藏 5 秒後暫停,顯示時恢復。唯讀的問題面板也提供顯示終端機。在手機上,鏡像僅供顯示:在裡面打字或點按不會送出任何東西。
鏡像下方的按鍵板每點一下送出一個按鍵:esc、tab、⇧tab、方向鍵、enter、space、⌫、home、end、pgup、pgdn,以及 1–9(收合起來)。它沒有字母、/、Ctrl 或 Alt,伺服器也拒絕把它們當按鍵。
細節
- 它只在該格即時顯示終端機時(最近 3 秒內有一格畫面)才作用,每秒最多 8 個按鍵。
- session 在等待 secret,或停在它的面板可回答的問題時,只有 Escape 有作用。已配對的裝置同樣只送 Escape,而且在
link_request開著時不送提示。 - 送出 enter 之前,伺服器會問 session:這個 Enter 會提交什麼。如果那是結束或取代 session 的指令(
/reload、/new……,從歷史叫回來的也算)、另一個/或!指令,或是讀不到的編輯器,就會先要求確認。在閒置的 session 上一秒內第二次按 Escape(會開啟 pi 的樹狀(或 fork))也會詢問。 - 確認時,按鍵板用頁面自己的話說明原因,由伺服器的
confirmtoken 選出(gui/confirmwords.ts;只有頁面不認識的 token 才用伺服器的英文),上方是兩顆等高的按鈕:送出的那顆帶著它的標籤(Send Enter),實心填色;Cancel 只有外框,焦點落在它上面。 - 按鍵板的按鈕和打字的按鍵走同一個寫入佇列:寫入進行中按下按鈕,會輪流等待,期間按鈕保持可用。Escape 送到 pane,不是送到頁面。
在桌面(細緻指標)上,點進鏡像會讓它取得鍵盤,直到點到別處;拖曳選取文字以便複製則不會,而把 session 檢視切換到它的終端機時會取得一次。按鍵落在鏡像角落的一個隱形文字框裡,所以輸入法在那裡顯示它的候選字。
細節
在手機上沒有東西會自己取得鍵盤,因為點一下 pane 不會開啟螢幕鍵盤。終端機的鍵盤按鈕(鍵盤)會把焦點放到同一個文字框並開啟螢幕鍵盤(再點一下,或離開終端機,就會關閉)。在裡面打的字會以文字送出(輸入法的組字在結束時以一次寫入送出),Backspace 和 Enter 在螢幕鍵盤沒有送出按鍵時,改從它的 beforeinput 讀取。切換到終端機時,輸入框也會放掉焦點,所以按鍵絕不會落到隱藏的對話裡。
送出的有以下這些,全都走同一個 keys 端點、受同樣的防護:
- 按鍵板的按鍵以它們自己的按鍵送出(方向鍵、Enter、Escape、Tab、Shift+Tab、Backspace、Home、End、Page Up、Page Down),Delete 則以它的序列送出;
- Ctrl 或 Alt 加字母、Alt+Enter、Alt+Backspace,以及 Ctrl 或 Alt 加 ← 或 →,用 Herdr 的命名(字母取自按鍵的位置,因為 Mac 上的 Alt 打 A 會打出 å)。Ctrl+Z 絕不送出:pi 會在背景停住,而這裡沒有任何東西能把它帶回來。Ctrl+D 會先詢問(在空的編輯器上它會離開 pi),一秒內第二次 Ctrl+C 也會(pi 在第二次離開)。⌘ 仍歸瀏覽器,非 Mac 上對選取文字的 Ctrl+V 和 Ctrl+C 也是;
- 字元(包括
/、!和輸入法的文字)作為打字文字({text}):一次原始寫入,1 到 64 個可列印字元,所以不會夾帶換行、tab 或跳脫序列,Enter 仍然單獨送出並先詢問; - 貼上作為一次貼上(
{paste}):最多 4,000 個字元,可含換行和 tab 但不含其他控制字元,伺服器會把它包進終端機的括號式貼上標記,所以 pi 把它當成貼上,其中的換行不會提交任何東西。
寫入一次一筆,最快每 130 毫秒一次,同樣受每秒 8 次的限制。這期間打的字元併入下一次寫入,按住的按鍵只會在佇列為空時重複(Enter、Escape、Tab 和組合鍵絕不重複),被拒絕或要求確認時,會丟掉仍在佇列裡的內容。
紀錄下方的輸入框透過 Herdr 打進單位的終端機,就像在那裡打字一樣:
- Send(Enter;Shift+Enter 換行;輸入框空時停用)在單位閒置時送出提示,在它工作時送出引導訊息;
- After this(Alt+Enter,在它工作時顯示)把一則訊息排到目前任務完成之後;
- Stop 在確認後送出 Escape。
細節
輸入框裡只有 / 和正在打的名稱時,上方的選單會列出該 session 中相符的指令(擴充功能指令、提示範本和 skill,照它自己的 pi 所列;pi 內建指令不列出):↑ 和 ↓ 移動,Enter、Tab 或點擊把 /name 放進輸入框,Escape 關閉它。這份清單由該 session 的對話框 bridge 回答,所以是該 session 自己的集合,一秒內沒回答時選單什麼都不顯示。
開頭的 / 或 ! 會執行 pi 或 shell 指令;結束或取代 session 的指令(/quit、/exit、/new、/resume、/fork、/clone、/import、/reload)會先詢問。文字會落在終端機編輯器裡尚未送出的任何內容之後,並一起送出。輸入框只在 Herdr 確認收到輸入後才清空,並保留這期間打的任何內容;沒有被確認的送出絕不重送(請查看終端機)。答案到達時頁面無法說的事(你在別的 session,或頁面重新載入了)會保留下來,在你回來時顯示:為什麼某次送出被拒絕、它沒有被確認,或者,如果你編輯了已經送出的文字,原本的已經送出。
串流中斷、session 不在了,或它在等對話框時,輸入框會停用並說明原因(能在上方面板回答的就在那裡回答,否則在終端機)。已關閉的頻道不接受打字:輸入框說明原因(「This session is no longer live; its history stays readable.」),通訊畫面標頭顯示「channel closed」,伺服器對它拒絕輸入和按鍵(409 not_live)。session 離開網,或它是沒有任何 pane 為它執行 pi 的已完成子智慧體(它的 pane 已不在,或回到 shell)時,頻道就關閉;pi 仍在執行的已完成子智慧體(互動式、沒有自動結束的),在已配對裝置的頁面上和在這台電腦上一樣保有輸入框。其他子智慧體像任何 session 一樣直接打字。
Ctrl+C 和 Ctrl+D 絕不從瀏覽器送出。在觸控螢幕上,Enter 換行,Send 才送出;草稿在重新載入後仍在。Markdown 渲染成 DOM 節點,絕不當成 HTML;只有 http、https 和 mailto 連結是連結;引言區塊和清單最多巢狀 16 層,更深的層級維持為文字。
flowchart TD
q["單位等待問題<br/>或對話框"] --> a["標頭佇列裡的<br/>警示(問題優先)"]
a --> d["你打開它:一個對話框"]
d --> s["回答"]
d --> l["稍後:回到佇列"]
d --> o["開啟通訊"]
f["單位失敗,或<br/>子智慧體完成"] --> n["通知按鈕底下的通知"]
只有需要你回答的單位才會發出警示。警示在標頭排成佇列,絕不自己打開:你把它打開成對話框,然後回答、用稍後放回佇列,或用開啟通訊跳到該單位的通訊畫面。失敗和已完成的子智慧體是通知:它們另外放在通知按鈕底下,絕不搶走焦點。
細節
只有需要你回答的單位才會發出顯眼的警示:正在等待問題或對話框的單位(標記為等待,或有待處理的對話框)。失敗的單位(標記為錯誤,或失敗、或沉默的 Monitor)以及完成的子智慧體是通知:它們絕不佔用標頭的警示、不會排入對話框佇列,也不搶焦點。警示絕不會自己開啟任何東西:它加入標頭的佇列(問題在前,其次是通知,各自依最舊的在前),並在單位繼續往下走時離開佇列。
標頭的警示、Space 和對話框佇列的計數只算問題;通知等在它旁邊的通知按鈕底下,只在有通知時顯示,其中有失敗時呈紅色。頁面開啟之前發出的通知不算新消息。你從佇列開啟警示,或點擊正在等待的單位;它們會在側欄上方的 dock 中以對話框開啟。
你再開啟的第二個對話框疊在上面,下面那個露出一部分;點擊露出的部分可以把它帶到上面(真正落在條帶下方某個控制項上的點擊,例如設定連結,會交給那個控制項)。你正在閱讀的對話框絕不會被你沒開啟的對話框蓋住:警示只會排隊,沒有經過你的操作而開啟的對話框,會放在最上層對話框下面,不取得焦點。關閉最上層會顯露下面那個,下面沒有時,則顯露佇列中下一個問題或錯誤。
稍後把警示放回佇列,不開啟任何其他東西;如果單位在對話框開著時已經繼續往下走,警示就已過期,改為離開。對話框絕不會在你面前被關掉:如果它的單位在對話框開著時離開了網,對話框會顯示已離開(「left the net」),不再有它的問題或開啟通訊。佇列在標頭(它的提示)與每個對話框(「Also queued: 1 needs you · 1 finished」)中的計算方式相同:問題和錯誤需要你處理,也是關閉對話框後下一個會顯露的;只是完成的單位則留在佇列裡等你開啟,另外計算。
開啟通訊會前往該單位的通訊畫面,停在警示所指的那筆項目(警示到達時讀取:附上它所等待的工具呼叫的回覆,否則是它最後一則回覆),必要時載入較舊的頁面,並讓那筆項目亮一下;通訊紀錄會停在那裡,並帶著跳到最新,直到你往下捲。它會關閉警示,而不開啟下一個排隊中的,所以不會有東西蓋住你前往的通訊畫面。項目記在 URL 裡(?at=),所以上一頁、下一頁和連結本身都會再次落在它上面;已不在歷史中的項目會開在最新的一筆,以其他任何方式開啟單位時也一樣。
Escape 只關閉最上層的對話框,焦點回到原處,對話框不是 modal(頁面仍可使用,沒有東西困住焦點)。文件則改以並排區塊開在目前檢視旁,共用它的寬度;自行到達的區塊不取得焦點,狹窄的視窗把區塊顯示為分頁。對話框顯示單位、它的最後一行,以及它正在等待的工具呼叫,並附上通往它通訊畫面的連結。
問題與選擇器
Section titled “問題與選擇器”flowchart TD
w["單位在等一個對話框"] --> p["頁面顯示它<br/>(警示對話框、session 檢視)"]
p --> a["送出答案"]
a --> k["開啟該對話框的<br/>套件來結算"]
k --> e["終端機的對話框關閉,<br/>智慧體只收到一次答案"]
p --> r["其他情況:唯讀,<br/>附顯示終端機"]
單位在等對話框時,頁面會在警示對話框和 session 的輸入框上方顯示它。有四種可以從頁面回答:pi-robot-hand 的選擇器、rpiv 的問卷、pi-code 的 hook gate,以及擴充功能自己的 select 或 confirm。送出答案會交給開啟該對話框的套件,由它像終端機的答案一樣結算,所以智慧體只收到一次答案。其他一律唯讀,而 secret_drop 永遠不能從頁面回答。
細節
單位在等待對話框時,它的警示對話框和它的 session 檢視(輸入框上方)都會顯示它。有四種可以從頁面回答:pi-robot-hand 的選擇器(robot_hand ≥ 0.3.0:照原樣顯示它的標題,每個選項附上它作用的位置和終端機的編號,以及複製指令,它透過瀏覽器複製,不回答任何東西);@juicesharp/rpiv-ask-user-question 的問卷(ask_user_question:一到四個問題,每題有標頭標籤、附說明的選項(單選為選項鈕,可複選時為核取方塊)、「Or type your own answer」和「Note (optional)」);pi-code 的 hook gate(任何工具受管控的呼叫之前的確認,例如「Allow bash?」:Allow 或 Deny);以及任何擴充功能自己的 ctx.ui.select 或 ctx.ui.confirm(見任何擴充功能的 select 與 confirm)。
送出答案交給開啟該對話框的套件,由它像終端機答案一樣處理,所以終端機的對話框會關閉,agent 只收到一次答案。面板接著顯示「Answered.」;終端機先答時顯示「It was already answered in the terminal.」;套件拒絕時顯示擁有者的理由(對話框仍可回答);沒有收到確認時顯示「Not confirmed. Check the terminal」和再檢查一次(重送同一個答案,絕不送新的)。
取消(在送出旁邊;在標準、極客和手機版面中,手機上是 44 px 的點按目標)以和終端機中按 Escape 相同的方式結束對話框,不選任何東西:rpiv 收到 {cancel: true},它的工具呼叫以它自己的已取消結果結束;ctx.ui 的 select 解析為 undefined,confirm 解析為 false;link gate 與 link 核可把它當成拒絕;pi-robot-hand 0.8.0 的 robot_hand 收到帶著對話框 digest 的 {cancel: true},只限 pending 說了 cancellable: true 的對話框(0.7.x 不說,也就不收),它的工具呼叫以已取消返回,不結束這一輪。它走和答案相同的答案路由,所以第一個答案獲勝、重送會重播,卡片顯示「Cancelled.」。沒有取消路徑的擁有者(pi-code 的 gate、secret_drop、0.8.0 之前的 robot_hand)沒有這顆按鈕:什麼都不送、沒有按鍵、不讀畫面。
頁面上的 Escape 仍然只關閉對話框檢視,不會取消問題。取消是一種拒絕,所以已配對的裝置在它能選某個選項的地方都可以取消。選項全是 hereOnly 的對話框(/boss link 核可、它的 confirm 和它的措辭 select,以及 gui rotate、gui unpair all、server stop 和 restart 的 confirm)會被拒絕來自已配對裝置的請求(403 local_only),在那裡也不顯示取消。信任、移除、角色、輪替或離開的 link 核可保有一個已配對裝置可以選的拒絕,所以它可以取消(也就是拒絕)。
問題可以只用鍵盤回答:對話框開啟時,焦點在第一題已選或第一個選項上,而不是在開啟通訊上。方向鍵在選項之間移動(單選在焦點移動時就選取;複選只移動,用 Space 切換);Tab 和 Shift+Tab 在題目之間移動(最後一題之後,依序到它自己的話的輸入框、它的備註和送出);Enter,或單選上的 Space,確認一個選項並前往下一個未回答的題目,再到送出,在那裡按 Enter 就送出。在題目最後一個選項再往下,會到它自己的話的輸入框,在那裡 Enter 做同樣的事,Shift+Enter 換行。
焦點在文字框時,頁面自己的按鍵停用,Escape 除外。作用在這台電腦上的選項(robot_hand 的剪貼簿)在已配對裝置的頁面上也提供,並附上說明它作用在哪裡的備註,也接受從那裡送出的答案。不送任何按鍵,也絕不讀畫面:每個 session 的對話框 bridge(在 Herdr 裡的每個 pi,子智慧體也一樣)把它的套件發布在 pi 事件匯流排上的對話框,經由只有該 session 才能擁有的 intercom 擴充功能頻道,轉送給 GUI 伺服器的 session。伺服器只在對話框是該 session 唯一開著的對話框、屬於 session 檔裡開著的呼叫之一(擁有者自己的工具呼叫,hook gate 則是它所指名的呼叫),而且與頁面所顯示的相符(對話框 id 與摘要;見回答 pi 對話框)時,才讓頁面回答。
其他一切都維持唯讀,並附上原因和顯示終端機,它會在頁面上開啟它的畫面:ask_user(pi-ask-user)、多個開著的對話框(只有在 bridge 轉送超過一個,或它轉送的那個沒有指名開啟中的呼叫時,頁面才會這樣說;最後一則訊息留下多個未回答呼叫的 session,若它唯一轉送的對話框指名其中一個,仍可回答)、已在途中的答案、太長而無法轉送的對話框,或與 session 檔不一致的對話框。
secret_drop 絕不能從頁面回答:secret 只在終端機輸入。pi-intercom 的身分是自行申報的,所以同一使用者的行程可以假冒提問 session 的 bridge,在自己的 key 下轉送 secret 對話框,並收到頁面封給它的值。在 intercom 能驗證 bridge 是誰之前,bridge 根本不轉送 secret 對話框(標籤、步驟、目的地和 key 都不離開 session),並拒絕對它的任何答案;伺服器不從任何請求取用 value(這台電腦的頁面回 400,已配對裝置回 403),也不送出任何值。
在每個頁面上,面板都是唯讀的等待,沒有欄位,也沒有顯示終端機(鏡像保持隱藏,按鍵板只剩 Escape):這台電腦的頁面說明機密只在終端機輸入,已配對裝置的頁面則寫成「For security, entering a secret is not supported remotely」(安全考量,遠端不支援);兩者都指名終端機。加密的程式碼留在樹裡,等這條路徑恢復的那一天;規格說明了什麼擋住它(回答 pi 對話框,「Why a secret is terminal only」)。
secret_drop 呼叫本身仍會連同它的引數(標籤、步驟與目的地)顯示在裝置的通訊歷史中,和任何工具呼叫一樣;值絕不會。在另一個開著的對話框旁邊,單位只顯示它在等待機密。有開著對話框的單位立刻算作等待;否則在 Herdr 回報它的 pane 為 blocked 期間算作等待,這由 pi 根據 herdr:blocked 事件回報。Straw Boss 把 rpiv 的 rpiv:ask-user:blocked 轉送進去,每份開著的問卷一次,pi 自己的對話框也一樣(見被擋住的對話框)。
顯示的問題是 session 目前分支上最新未回答的呼叫(對在多個開啟呼叫中被回答的 hook gate,則是它管控的那個呼叫);超過 256 KB 的呼叫會從檔案更前面讀取(最多 16 MB,顯示時截短),更長的則說明它太長而無法讀取。
任何擴充功能的 select 與 confirm
Section titled “任何擴充功能的 select 與 confirm”每個擴充功能的 ctx.ui 是同一個物件,所以 Straw Boss 包住 select 和 confirm(input、editor 和 custom 不動,因為那裡可能打入機密)。被包住的呼叫會開 pi 自己的終端機對話框,同時把同一個問題轉送到頁面;先到的答案勝出,另一邊隨即關閉。對話框絕不會變差:無法轉送的呼叫就是原本的呼叫。
細節
每個擴充功能的 ctx.ui 是同一個物件,所以 Straw Boss(在 Herdr 裡的每個 pi,子智慧體也一樣)取代它的 select 和 confirm,而不動 input、editor 和 custom(那裡可能打進 secret)。被包裝的呼叫照原樣開啟 pi 自己的終端機對話框,並在同一次呼叫裡,把對話框以 straw-boss:ui 底下的 dialog-contract v1 擁有者身分發布在 pi 的事件匯流排上,所以 bridge、hub 和答案路由把它當成任何擁有者的對話框處理,所有既有的檢查都適用(只有一個開著的對話框、摘要、答案已在途中、secret_drop 開著、太長)。
select 連同它的選項轉送;confirm 則是一個對話框,文字是它的標題、一個空行,再加它的訊息,附上是與否。第一個答案獲勝,另一邊被關閉:來自頁面的答案透過它的 signal(與呼叫者自己的合併)中止終端機的對話框,擴充功能的呼叫以該答案解析(選到的選項,Yes 為 true,No 為 false),只有一次;在終端機的答案則關閉頁面的對話框(stale,已在終端機回答)。
對話框絕不會變得更糟:無法轉送、或轉送啟動失敗的呼叫,就是什麼都沒留下的原始呼叫,拋出或拒絕的對話框則原樣傳遞它的錯誤。
何時綁定。 對話框只在剛好有一個工具呼叫進行中時才被轉送,並綁定到那個呼叫(hub 需要它在 session 檔開著的呼叫之中,和 pi-code 的 gate 一樣);頁面把它寫成「opened while {tool} was running」,因為它可能與那個呼叫無關。沒有呼叫、有兩個以上,或有 secret_drop 進行中時,它留在終端機;呼叫的工具有契約擁有者(robot_hand)而擁有者什麼都沒轉送時也一樣。頁面顯示的文字中的 Unicode 格式字元(雙向覆寫、零寬標記)以 U+FFFD 畫出。
只轉送最外層的對話框(input、editor 和 custom 只計數,原樣通過;計數在 agent_end 和 pi 更換它的 ui 物件時重設):在另一個對話框開著時開啟的對話框,會撤回已轉送的那個(頁面無法分辨一個答案會關掉哪個對話框),而它自己不被轉送。超過 100 個選項的 select,或標題與選項超過 12,000 個字元的,不轉送。擁有者優先。 有契約擁有者的對話框(robot_hand、pi-code 的 gate)由它的擁有者轉送,同一個工具呼叫上被攔截的孿生對話框會被丟掉,不論哪個先開。ask_user_question 透過 ctx.ui.custom 開啟它的問卷,那個沒有被包裝。遠端。 已配對的裝置回答 select 和 confirm 的方式和這台電腦的頁面相同:轉送的對話框帶 confirm: true(由它的摘要區分,所以轉送者無法把其中一種冒充成另一種),每個頁面都顯示 Yes 和 No。機密是例外:沒有任何頁面接受。取消遵循和任何選項相同的規則:已配對的裝置可以取消 select 或 confirm,除非它的每個選項都是 hereOnly。
- 生命週期。 包裝會在它包裝過的
ui物件上做記號,所以載入了套件的多份副本時只會包裝一次。pi 在綁定新的 UI context(模式變更)時會取代該物件,所以記號在 session 啟動時和每次工具呼叫之前檢查,並把包裝放回新的物件上;關閉時,轉送的對話框會被關掉,原始方法也會還原。沒有 UI 的 session 有 pi 共用的 no-op 物件,絕不去動它。
需要終端機的答案會開啟終端機
Section titled “需要終端機的答案會開啟終端機”flowchart TD
a["你在頁面上選了<br/>需要終端機的答案"] --> c["終端機卡片開啟"]
c --> p["放進 pi 輸入列的指令:<br/>送出或取消"]
c --> n["在新 pane 的指令:<br/>該 pane 自己的畫面"]
p --> x["這一步結束時<br/>卡片關閉"]
n --> x
c --> f["完整終端機<br/>(ssh、vim 之類)"]
有些選擇把下一步留在終端機:robot_hand 可以把指令放進 pi 的輸入列,或在新的 Herdr pane 裡執行或輸入。擁有者接受這種答案後,頁面會開一張小的終端機卡片放著那條指令,這一步結束時卡片就關閉。需要更多時,完整終端機會開出整個終端機。
細節
有些選項把下一步留在終端機:robot_hand 的 Put it in my prompt(指令在 pi 的提示字元等著 Enter)、Type it into a new Herdr pane 和 Run it in a new Herdr pane(在 pi 下方的新 pane 顯示或執行指令;該 pane 會自己關閉,它的輸出以訊息傳回)。選項以可選的 next 這樣說明("terminal" 或 "pane",和 note 一樣是提示,不納入雜湊);pi-robot-hand 不送,所以它的 adapter 從它顯示的標籤判讀,自己送的擁有者則保留它自己的。
擁有者接受這類答案時,答案路由的回覆會帶 then(來自已儲存對話框的選項,絕不來自請求),頁面在每個檢視中為它開啟終端機卡片:一張小卡片,裡面有答案放在那裡的指令(pane 則是它畫面最後四列)、一行附有隱藏我輸入的內容切換的輸入,以及一顆按鈕:提示字元中等著的指令是 Run(執行),pane 是 Send Enter。卡片送出按鍵板送出的內容,走同樣的寫入、受同樣的伺服器檢查。
對 pi 提示字元中的指令,第一次點按會遇到伺服器對指令的確認,卡片把它畫成一對內嵌按鈕,Send it 與 Cancel(頁面依伺服器的 token 用自己的話;Cancel 取得焦點,所以第二次或按住的 Enter 無法確認)。最後一列以冒號結尾,且在 password、passphrase、passcode、PIN、密碼或口令之後時,會自己把該行變成密碼欄位並說明;開關可手動遮住,手動遮住在它所回答的那個 Enter 之後結束。
這個欄位在任何表單之外,送出時清除;提交輸入法候選字的 Enter 不送出任何東西。attach 被拒絕或失敗的卡片可以從它的按鈕重新 attach(不送按鍵),最多每秒一次,持續失敗時倍增到 8 秒,並在這期間說明它正在連線。只是回顯上方顯示的指令的列(一個提示字元,然後是指令)會被省略。
按鍵或鏡像的拒絕,以伺服器的代碼(no_mirror、blocked、rate、standing、not_found 等)用頁面的語言說明;頁面不認識的代碼則顯示伺服器自己的文字。頁面對每個帶有 then 的被接受答案都這樣做,在這台電腦和已配對裝置上都一樣;只有一條規則,沒有 robot_hand 的特例,在一次呼叫中完成(openThen,gui/terminalthen.ts),每個檢視都從答案的結果到達它(gui/ask.ts,所以警示對話框、session 檢視和禪都一樣),任何其他以終端機結束的東西也可以呼叫它。
它交給頁面一個步驟,{kind: "prompt" | "pane", key, command},由檢視只決定卡片擺在哪裡:標準和極客放在覆蓋層的 dock(非互動視窗,手機上是底部的面板,上面開啟的對話框會蓋住它,並立刻關閉把老闆送到那裡的警示對話框),禪則作為它終端機層的卡片模式(用墨色,在頁面中央,右上角有「回到禪」 / Back to Zen)。較新答案的卡片取代正在顯示的那張。
卡片在它的步驟結束時關閉,依它送出了什麼或它自己的 pane 發生了什麼,絕不依單位的標記:提示字元的卡片在它自己的 Enter 被接受時,pane 的卡片在 pane 結束時(ended 畫面、404 或 409 pane_closed);在標準中 × 和 Esc 會關閉它,在禪中則由該層的 Esc Esc 返回。
完整終端機開啟卡片出現之前的終端機,用於 ssh、vim 之類:標準的 session 終端機模式或該 pane 的區塊,在禪中則是同一層的完整模式(頁面唯一的終端機,和標準相同的鏡像、按鍵板與鍵盤,只 attach 一次;它取得鍵盤時 Esc 送給它,焦點在該層其他地方時 Esc Esc 返回,沒有別的東西會結束它)。該層絕不會自己關閉:pane 關閉或它的授權結束、提示字元自己的 Enter 被接受,或單位又開始工作時,該層列上有一行安靜的字說明步驟已完成,老闆用回到禪返回。
禪的開啟終端機按鈕(被擋住的提示字元、唯讀對話框)直接開啟那個完整模式,並處於待命:單位的第一件工作就是那個步驟的結束,這一點由該列說明。沒有執行中塔台的塔台,在卡片上顯示它的句子而不是各列。標準絕不會從禪造訪,原本在那裡的畫面(項目、同伴或閒置)會照原樣再次顯示。pane 由伺服器找出:答案送出之前先記下該分頁的 pane,答案被接受後,再以猜不到的 id 授予一個新 pane,連同它的終端機 id 一起保存(重複使用的 pane id 會被拒絕),最多 8 個、6 小時。哪個 pane 取決於擁有者。
選項說明 tagged 的擁有者,會用 Herdr 的 pane token sb_answer=<the answer's answerId> 標記它開啟的 pane;伺服器只對分頁中恰好帶有該值的新 pane(3 秒內)授權,不授權給其他任何 pane,不論未標記的 pane 多新,兩個 pane 帶有它則什麼都不授權。不標記的擁有者(pi-robot-hand 0.3.x)則由伺服器依時間判斷:3 秒內恰好一個沒有執行智慧體、也沒有 sb_role token 的新 pane。
它的殘餘風險是:把 pane 與答案連在一起的只有時間,所以同一分頁中在那個時間窗內單獨出現的 pane(Herdr 還沒偵測到它的 pi 的子智慧體或 /boss open 分割、老闆自己的分割、另一個 session 的答案)會被授權,並在 Herdr 回報它的智慧體之前接受裸按鍵(之後授權結束);Herdr 的偵測可能延遲,這對 pi 沒有量測過。標記的擁有者沒有這種殘餘風險。兩個新 pane(未標記的擁有者)、沒有,或讀不到的版面,都不授權(then.pane 為 null;卡片說明 Herdr 中開了一個 pane)。
授權絕不會到達執行智慧體或帶有 sb_role 的 pane,並在它的 pane 一變成這樣時就結束:下一個按鍵被拒絕(409 pane_closed),鏡像每個畫面都重讀該 pane,一旦它成為 pi session 或塔台就不再送出,授權連同它的讀取器一起被丟棄。
POST /api/panes/<grant>/mirror 和 /keys 對它做鏡像(「裸」讀取器,背後沒有 session)並對它送按鍵(按鍵板的按鍵、組合鍵、打字文字與貼上,受同樣的速率與即時鏡像前提,但沒有 pi session 的防護、探測或確認),對老闆的頁面和已配對的裝置與對 session 的終端機按鍵相同;hereOnly 的選項仍然拒絕已配對的裝置。不開啟的:唯讀的對話框(secret_drop、同時多個對話框、太長的)保留它的顯示終端機連結,不會自己開啟任何東西,而「未確認」或「另一個答案在途中」的結果,在面板中提供再次確認,終端機檢視會把它隱藏。
-
一鍵送出提示字元。 當選項把 shell 指令放進 session 的提示字元時(robot_hand 把它放成
! <command>),伺服器為該答案保留一次點按的授權(答案 id、session、它自己的隨機 nonce,以及! <command>這一行的 MAC),而then說run: true。卡片的「執行」以run: <answer id>送出它的 Enter。只有對那個 Enter,伺服器才會連同探測把授權的 nonce 交給 session 的 bridge,而 bridge 對
command草稿回答 HMAC-SHA-256(nonce, 它的提示字元行)(沒有run的 Enter 不帶 nonce,所以不會為它產生草稿的 MAC;草稿本身絕不離開,但在本機 intercom broker 上、同時看到那一個 Enter 的 nonce 與 MAC 的監聽者,仍可以對短指令測試猜測)。只有 MAC 等於該指令的 MAC 時,Enter 才通過command確認。run的 Enter 絕不會送出不是那個指令的一行: 被編輯、清空,或變成文字或結束型指令的提示字元,不論編輯器是在哪裡讀到的,都會被拒絕(run_changed,什麼都不送);唯一改為要求確認的情況,是來自不送 MAC 的 bridge(較舊的)的command行。結束型指令(/reload、/quit 等)和讀不到的編輯器,不論 MAC 為何仍會詢問。一鍵只涵蓋沒有可補全 token 的!行。含有 pi 補全選單可能在其上開啟的 token(
/路徑或 pi 指令、~、@、#、引號、以.開頭的 token,或結尾的空格)的指令,得不到一鍵:選單可能在行不變的情況下開著,而對/token,Enter 這時會提交選單的選項。殘餘風險:在一個單純的字(
ls foo)上按 Tab 之後,沒有任何規則能把它和指令字區分,選單可能是開著的;一鍵的 Enter 這時只會填入選項而不執行任何東西,授權用掉了,卡片像指令執行了一樣關閉,被改過的行留在提示字元裡等著。pi 不告訴擴充功能它的選單是否開著,所以探測看不到。它的 Enter 保留確認,而then沒有run。授權只回答一次,不論它回答什麼,屬於它的 session,2 分鐘後過期;被改過的行、另一個 session 的或未知/重播的答案 id,以及過期的授權,會以
run_changed、run_unknown、run_expired被拒絕(用頁面的話說明),下一個不帶run的 Enter 則照常要求確認。只有 pane 的答案、沒有next的選項,或擁有者沒有接受的答案,不留下授權。只保存或記錄 MAC。
通知一經開啟就算讀過:它們沒有稍後,關閉一則也不會把它放回佇列。它們與需要你處理的東西分開等著,放在標頭的通知 N 按鈕底下:它會開啟已完成或失敗單位的清單,每個附上它的基地和最後一行;點一列會前往該單位的通訊畫面並清除它,全部清除則清除每一則(問題仍在,等著回答)。
細節
pane 已關閉的已完成單位,以及pi-herdr-agents 記錄為完成的子智慧體(它的 subagent-activity 檔中的 phase: "done",即使它的 pane 仍在執行 pi),在它的聲音和提醒之後會自己從清單清除:它的父層有結果。地圖仍會把已完成的子智慧體顯示十分鐘。
警示對話框的 ⤢ 會把對話框停靠區變成視窗中央一個寬面板,適合長的問題;⤡ 則放回去。這個選擇依瀏覽器保存。
細節
警示對話框在 × 旁邊的 ⤢ 會把對話框 dock 變成視窗中央一個寬的面板(最寬 880 px,幾乎和視窗一樣高),供長的問題使用;⤡ 放回去。這個選擇按瀏覽器保存(sb.dialog-wide),所以下一個對話框以同樣的大小開啟。手機沒有放大:它的對話框本來就佔滿寬度。三個按鍵驅動對話框:Space 開啟下一則警示,和標頭的警示按鈕一樣,F 按下最上層對話框的 ⤢ 或 ⤡,Escape 關閉最上層對話框。你在打字時(輸入欄位、輸入框及它的 / 選單、終端機的按鍵接收器)、按住 Ctrl、Alt 或 ⌘ 時、輸入法組字時,或確認對話框、圖片檢視器或彈出視窗開著時,頁面不處理按鍵;答案選項上的 Space(它會選取或切換),或你用鍵盤把焦點移到的按鈕或連結上的 Space,則保留它自己的啟用行為。
被滑鼠留下焦點的按鈕或連結(點擊之後的標誌)不會扣住 Space:它仍會開啟下一個警示。沒有作用的按鍵(沒有排隊時的 Space、沒有對話框時的 F)不會被攔下,所以頁面照常捲動。
單位頁面顯示的內容
Section titled “單位頁面顯示的內容”用 pi-herdr-doc-review 顯示文件的 session,會把文件列在它檔案的文件底下,點一下就在目前檢視旁開成一格。HTML 文件在沒有腳本的沙盒框裡執行;文字文件以唯讀方式顯示。
細節
用 pi-herdr-doc-review 0.4.0 或更新版本顯示文件的 session(show_doc,或自行開啟的 Mini SDD 規格),會在 2 秒內把它們列在它的檔案的文件之下,而單位 token 上的羊皮紙徽章會計算仍顯示在它終端機畫面上的文件數(塔台只在它的檔案中列出)。在該 session 的通訊畫面上,到達的文件會在紀錄旁以並排區塊開啟且不取得焦點,除非已經開了四個區塊(它絕不會擠掉你開啟的);在其他任何地方(地圖、基地、另一個 session)只有徽章改變,而在有文件的 session 上開啟的頁面則一個都不開。
在檔案中點擊會開啟一個,show_doc 卡片上的在旁開啟在它的文件被列出時也會(較舊的卡片維持文字)。HTML 文件是沒有腳本的 sandbox 框架:頁面的屬性與伺服器的政策都讓它成為不透明的 origin,所以它不執行任何腳本、不能讀取 API 或 cookie,只從它自己的目錄載入圖片、樣式表和字型。它的連結在新分頁開啟,而帶有 action="cgi-bin:pi-answer" 的表單,回答 session 的方式與 Chawan 提交完全相同(由擁有者送達):每次頁面檢視只能送出一次,所以上一頁再送出會顯示「Already sent」,重新載入則再送一次。
答案放在 GET 的 URL 裡,和 Chawan 一樣,並留在瀏覽器的歷史中:不要把 secret 放進文件表單。在新分頁開啟顯示同一個沙箱頁面。文字文件以唯讀方式轉譯,.md 為 markdown,其他為純文字。擁有者回報新的修訂版時,正在顯示的區塊會重新載入,隱藏的(另一個區塊分頁)會說明並在顯示時載入;在它的 session 中被關閉的文件,或已結束的 session,會讓它的區塊顯示它已不在清單中。
一個 session 最多列出 32 份文件;太大而無法放進對話框頻道的清單會被丟棄(檔案說明請在終端機中閱讀),而文件絕不會扣住它的對話框。
在輸入框貼上、拖放,或用 + 挑選的檔案,會以它的位元組 POST 到 sessions/<key>/upload,受和打字輸入相同的 origin 與 CSRF 檢查(已配對的手機也一樣),最多 20 MB。伺服器把它存在暫存目錄的 pi-straw-boss-uploads 之下(資料夾 700,檔案 600),名稱經過清理,頁面把路徑放進游標所在處的訊息中,就像貼進 pi 終端機的圖片會變成暫存檔路徑那樣;session 用它自己的工具讀取它。文字則以文字貼上。
設定頁(這個裝置)上有兩個開關,各是一顆標示清楚、以文字顯示開或關的按鈕,按瀏覽器存在 localStorage:提醒(預設開)和音效(預設關,所以開啟的頁面不會發出聲音;音效其實是兩個開關,音效與背景音,在下一則說明)。它們作用在 snapshot 所引發的警示:單位轉為等待回答、轉為錯誤,或已完成。
細節
提醒是系統通知(問題的可能發出嗶聲;失敗或完成的則無聲,是安靜的通知),只在頁面隱藏或沒有焦點時發出,每個警示一則,以單位標記,點擊會把頁面帶到那個單位;有焦點的頁面本來就顯示警示佇列。第一次按提醒會請求瀏覽器的權限;在獲得授權之前,開關顯示琥珀色,瀏覽器封鎖時顯示紅色。一批只播放一個聲音,是其中標記最緊急的(等待,然後錯誤,然後完成;錯誤保有它自己的音訊片段,與問題的分開發聲),來自三個 Kenney CC0 音訊片段,打包成帶雜湊的 MP3 檔,在第一次播放時才取得。
音效有 0 到 100 的音量(預設 60,以 sb.volume 存在 localStorage),底下有試聽按鈕;每次變更都會播放完成的聲音。裝置的偏好設定存放在同一處;另一個分頁的音效開關或音量會立刻套用到開著的頁面。這些片段透過 Web Audio 以音量的平方播放(響度跟隨平方,而且 iPhone 會忽略 audio 元素的 volume)。兩者都跟隨每個 snapshot,包括隱藏頁面的,並透過它們自己對聽過什麼的記錄:警示佇列的記錄只在頁面顯示時才前進,所以它仍會把隱藏期間到達的排入佇列。頁面開啟時已經在等待的,兩者都不發出。
音效與背景音
Section titled “音效與背景音”設定頁的一個音效開關和音量變成兩個,在這個裝置之下各有自己的開關和音量:音效(事件的提示:標準與佈景主題的問題、完成和失敗片段,單位就緒的鈴聲,禪的缽聲與敲擊聲)和背景音(在頁面底下播放的:禪的音樂,鴉閣的雨聲和雷聲;雷聲屬於天氣)。兩者預設都關,並且在標準、極客、禪和鴉閣檢視中都有作用;禪的角落選單和它的調色盤也都帶有兩者(各一個開關和兩段音量調整)。
細節
音量是 0 到 100(預設 60),以平方的形式只作為它那一類的增益進入頁面的音訊核心;每個開關只由它那一類的來源檢查,所以關掉音效會讓提示音靜音、雨聲還在,關掉背景音則讓雨聲、雷聲和音樂靜音、提示音還在。音效有試聽按鈕,並在開啟或放開音量時播放完成的片段;背景音沒有樣本,因為它播放的東西本來就聽得到。
舊的 sb.sound 和 sb.volume 在某一類沒有自己儲存的值時,被當作兩類的起點讀取(舊的開是兩者都開、用舊的音量,舊的關是兩者都關),而且絕不寫入,所以同一個瀏覽器上較舊的頁面仍讀它自己的。
使用者訊息或工具結果中的圖片(png、jpeg、gif 或 webp,如 pi 存在 session 檔中的)在對話中顯示為縮圖:在訊息下方,或在工具卡片下方,不論它是開著或關著。頁面從 api/image/<key>/<entry id>/<n> 載入每一張,它讀取那一筆項目並以圖片的位元組回答,快取一天,因為項目絕不改變;歷史本身只帶數量。縮圖開啟燈箱:圖片縮放到符合螢幕,點按則顯示它自己的尺寸,← → 或方向鍵切換同一則訊息的其他圖片,Escape、×,或點它旁邊來關閉。
排隊中的訊息
Section titled “排隊中的訊息”flowchart TD
t["智慧體工作時你打字"] --> q["pi 的佇列:<br/>引導或後續"]
q --> b["對話下方變淡的氣泡"]
b --> e["編輯:把整個佇列<br/>取回輸入框"]
q --> n["某個 turn 取走它們"]
智慧體工作時打的內容會在 pi 的佇列裡等著:引導訊息在智慧體的下一個 turn 到達,後續訊息在這次執行結束時到達。session 檢視在對話下方把它們顯示為變淡的氣泡,直到某個 turn 取走;編輯會把整個佇列取回輸入框。底下是這次取回如何保持安全的細節。
細節
智慧體工作時打的內容會在 pi 的佇列裡等著:引導訊息在智慧體的下一個 turn 到達它,後續訊息在這次執行結束時到達。session 檢視在對話下方把它們顯示為變淡的氣泡,標示為引導或後續,直到某個 turn 取走它們。pi 只告訴擴充功能有沒有訊息在等,所以每個 session 的對話框 bridge 記下它看到到達的訊息,並在對應的使用者訊息開始時移除一則(文字相同的那則,否則最舊的);pi 用別的方式(Esc、它的取回按鍵)清空的佇列,會在 bridge 下一次檢查時清除,2 秒內。
bridge 連同對話框轉送最舊的五則,每則截到 200 字元。每個排隊中的列最多顯示三行,以省略號截斷;被截斷的列點擊、按 Enter 或 Space 會展開成完整文字,再按一次收回。編輯(佇列下方的一顆按鈕,附提示說明所有排隊的訊息會合成一段文字取回,已經送達的無法取回)把整個佇列取回輸入框。
它就是 pi 自己的取回按鍵(app.message.dequeue):Alt+Up,但在 pi 使用它的 Windows 按鍵綁定的地方(Windows,以及 WSL 下的 Linux)是 Alt+Q,或使用者的 keybindings.json 綁定的按鍵。pi 讓擴充功能讀取它的即時按鍵綁定,卻不讓它執行這個動作,所以在 pi 內執行的 session bridge 說出該按鍵:pi 對它的即時綁定中,伺服器願意按的第一個(一個 Alt 或 Ctrl 組合鍵,絕不是 Ctrl+C、Ctrl+D 或 Ctrl+Z);即時管理器沒有這個 id 時,改讀 <agent dir>/keybindings.json(讀法和 pi 相同,無效的值整個丟掉),否則用平台的預設值。伺服器對同一份清單再檢查一次,然後送出。
這個鍵會清空每個佇列,並把 steering 訊息,然後是 follow-up,以空行連接,放在 pi 的編輯器原本內容的前面。頁面為每次取回產生一個 id(存在分頁的 sessionStorage 中,直到某個答案結案),並 POST 到 POST …/sessions/<key>/dequeue(從這台電腦的頁面和已配對的裝置都一樣,不需要鏡像,因為打字也不需要)。
伺服器先問 session 的 bridge(有沒有東西排隊、編輯器裡有什麼、bridge 自己的鏡像說排了什麼),只有在有東西時才把那個按鍵送到 pane(送之前再看一次 pane,所以這期間改變的 session 不會收到按鍵),然後再問一次(最多等 1.5 秒讓佇列清空,讀取編輯器,檢查草稿前面的東西恰好就是鏡像的訊息,先引導再後續,把草稿放回去,只交出訊息)。
終端機的草稿絕不離開 session:草稿前面任何不是恰好等於鏡像訊息的東西(開始與按鍵之間在終端機打的文字、pi 展開過的訊息,例如 /template 或 /skill:、被擴充功能改寫的)都會被以 changed 拒絕,訊息連同草稿留在 pi 的編輯器中。pi 自己的還原會忘記草稿中貼上區塊的內容(只留下它的標記);此時 bridge 把編輯器設成這些訊息加上完整的草稿,貼上的內容放回原處。
hub 告訴 finish 它的期限(從請求起 2.5 秒,用這台機器的時鐘)並等 4 秒;超過期限減 0.3 秒後,bridge 什麼都不清,所以訊息留在 pi 的編輯器中,答案是 not_cleared。交出的訊息在 bridge 中以取回的 id 保留十分鐘:同一個 id 再問一次(答案遺失了:頁面離開、網路中斷、頁面重新載入)不用按鍵就會拿到它們,所以再按一次「編輯」就能取回。
頁面把回覆放在它的框裡原有內容之前,或在它的輸入框已不在時,放在該 session 已儲存草稿之前(輸入框回來時附上一則說明),所以沒有東西取決於輸入框是否掛載;pi 的編輯器保留它自己的草稿,不再持有這些訊息,所以從頁面送出不會重複它們。
空的佇列不送按鍵({empty: true},「nothing is queued any more」),在檢查與按鍵之間被送達的佇列給出同樣的答案,而對話框等待時的按下,與任何按鍵一樣被拒絕,只有 Escape 除外,且在 link_request 開著時,來自已配對裝置的也被拒絕(403 local_only,在按鍵之前再檢查一次)。
讓訊息留在 pi 編輯器中的拒絕會這樣說明:pi 的 dequeue 鍵只綁定到頁面不會送的按鍵(no_key,什麼都沒送)、pi 沒有取回它的佇列(訊息指名送出的那個鍵,它可能不是它的 dequeue 鍵;較舊的 bridge 不指名任何鍵,伺服器於是送出它平台的預設)、編輯器中途改變了、文字超過 12,000 位元組、編輯器無法清空(當它也會留在編輯器中時,不交出任何東西),或按鍵送出後取回逾時(stale)。沒有 pull 的 bridge(該 session 中較舊的 pi-straw-boss)不回答,頁面說明它不確定。使用者開啟的列按 session 和按列記住(相同列中的第 n 個分開計算)。
Monitor
Section titled “Monitor”session 來自 pi-loop-monitor 的背景 monitor(MonitorCreate)顯示在它的 todo 之下:收合時是有幾個在執行、幾個失敗,以及最新執行中那個的最後一行輸出;展開時是每個 monitor(最新的在前,執行中的在已結束的之前,最多六個),附上它的描述或指令、它的狀態(執行中、已完成、失敗、沒有輸出而停止,或已停止)、它的結束碼、它的行數和它的最後一行。每個 session 的對話框 bridge 跟隨 pi-loop-monitor 在 session 匯流排上的事件(monitor:started、最多每 2 秒一次的 monitor:output、monitor:finished),去掉終端機跳脫序列和控制字元,把文字截到 160 個字元,並連同它的對話框轉送清單;已結束的 monitor 會留 15 分鐘,與 pi-loop-monitor 保留它的記錄一樣久。
細節
這份清單是唯讀的:pi-loop-monitor 不給其他擴充功能任何停止 monitor 的方法。失敗(結束碼不是 0)或沉默(被它的閒置逾時停止)的 monitor 會發出它自己的錯誤通知,與它單位的狀態分開(它歸在通知之下,而不是標頭的警示):它的對話框指名這個 monitor,而開啟通訊、它的對話、它的終端機和提醒的點擊都前往該單位。完成或被停止的 monitor 不發出任何東西。當 session 不再列出該 monitor 時,警示離開佇列。
自身閒置、但它的某個子智慧體正在工作、等待或指揮中,或它的某個 monitor 正在執行的單位,會顯示它自己的標記,指揮中(zh-TW 指揮中),而不是閒置。它自己的狀態優先:工作和等待維持原樣,已完成的子智慧體不算數。這對主智慧體、成員、塔台,或任何有子智慧體或 monitor 的單位都成立,在塔台儀表板和瀏覽器檢視中皆然(由伺服器決定;monitor 還會保留紅色的「看著 monitor」備註)。
細節
它有自己的顏色(紫羅蘭)、符號(◈,一顆菱形)和呼吸般的光暈,出現在地圖上單位的名牌、檔案、小隊的 token、通訊畫面標頭,以及 HUD 的指揮中數字上(在 prefers-reduced-motion 下和頁面靜止後不再動),圖例裡也有一行。含有指揮中單位的基地或小隊把它彙總為工作中(它的子智慧體本來就在工作;只看著 monitor 的單位會讓它的基地變成工作中);HUD 和統計把每個標記各自計算,所以工作中不包含指揮中。
警示佇列把指揮中當作工作中:它不引發任何警示。pi-herdr-agents 記錄為已完成、但它的 pi 還活著且有 monitor 在執行的子智慧體還沒有完成:它不是 ended,所以顯示為指揮中(備註:看著 monitor)而不是完成,沒有移除按鈕,不在「移除已完成」之內(伺服器也拒絕移除它),也不引發完成通知。monitor 停止且 pi 閒置時,它又回到完成。
任何單位,不論是不是子智慧體,只要它的 pi 還活著,都一樣:在 monitor 執行期間被 Herdr 標為完成時,它顯示為指揮中而不是完成,在 monitor 停止之前不引發完成通知或完成音效。塔台自己的 TUI 儀表板沒有 monitor 的資料(monitor 只到達托管 GUI 的 session),所以它對這樣的單位的字符不變。
用 @juicesharp/rpiv-todo 維護 todo 清單的 session,會把它顯示在輸入框上方:收合時是 Todos done/total 和進行中的任務,展開時是每個任務(○ 等待、◐ 進行中、✓ 完成;已刪除的不列出)。伺服器從 session 檔讀取它,和讀取歷史一樣:目前分支上最新的 rpiv-todo-state 項目,否則是最新 todo 結果的 details(這些項目出現之前的 session),隨歷史的第一頁、每次重設,以及每個加入 snapshot 的變更一起送出。終端機模式把它隱藏,因為 pi 自己的小工具在那裡顯示它。
關閉、開啟與信任
Section titled “關閉、開啟與信任”flowchart TD
q["在單位、小隊或基地上按關掉"] --> a["先詢問"]
a --> b["詢問每個智慧體的 bridge<br/>是否退出,pane 保留"]
b --> r{"有拒絕<br/>或沒回應?"}
r -->|"是"| k["什麼都不關"]
r -->|"否"| c["分頁或工作區關閉"]
在單位、小隊或基地上按關掉會先詢問,再請每個 session 自己的 bridge 退出,和 /boss close 一樣。只要有一個拒絕或沒回應就什麼都不關;每個智慧體都離開後,分頁或工作區才關閉。已完成的子智慧體改用 Dismiss,基地則可以 Stash 收起來再 Pop 回來。
細節
地圖上、基地、小隊或單位上的關掉(不含子智慧體:由啟動它的單位關閉),以及基地名稱下方只關掉塔台的關掉塔台,會先詢問。× 只關閉對話框。關掉也出現在單位自己的通訊畫面標頭,桌面和手機都一樣:就是地圖上該單位的同一顆按鈕(塔台的是關掉塔台),只在地圖顯示它的地方才顯示(子智慧體、已完成子智慧體的移除,或頁面無法控制的單位都不顯示),確認、請求和拒絕都相同;取消或被拒絕時留在通訊畫面,關掉成功則回到該單位的基地。
接著它對基地 POST close/all(基地的關掉會先像 /boss close all 一樣檢查,只要有執行中的東西會遺失,就會連同所有原因一併拒絕)、close/tab 或 close/unit。基地的收起(在它的 +主智慧體旁邊)會先詢問,並在基地的標籤下 POST close/stash;HUD 的已收起(n) 列出被收起的東西,每個都有接回,它 POST close/pop,並從伺服器的 session 開啟它。已配對的裝置可以收起和接回,如同它可以關掉。關閉是每個 session 自己的事:伺服器請 session 的 bridge 像 /boss close 那樣讓它結束(pi 的 shutdown,原因 quit,所以它的 token 清除,而塔台的結束會把主智慧體降為成員)。
sb server 所執行的塔台(塔台服務)沒有 pi,也沒有可詢問的 bridge:它的關掉塔台POST 同樣的 close/unit,伺服器像 close-tower 那樣結束該服務(分頁關閉,token 隨 sb_tower_quit=<members> 消失,基地的主智慧體變成成員);小隊或基地被關閉時,不詢問那個身分,也不詢問服務 pane 中執行的 pi(老闆對話、事件 run、手動執行),而服務隨分頁或工作區結束(正在工作的執行的 pi 仍會拒絕基地的關掉);收起會拒絕塔台是該服務的基地,因為接回不會再產生服務,它的佇列和對話留在舊的工作區 id 之下。
沒有回答的 session 會在對話框中被告知,並提示它可能執行較舊的 pi-straw-boss(對它 /reload)。pi 結束後,單位隨即從一個脫離的行程關閉它自己的 pane。在關掉之前,session 會用 token sb_closing 標記它的 pane(TTL 10 分鐘,session 絕不清除):pi 還在關閉時它就離開 intercom,而 Herdr 仍列為沒有 session 的 pi 的 pane,在那幾秒會顯示成「網之外」下一張已完成的未加入 pi 的綠色卡片,所以網會把帶有這個 token 的 pane 排除在外,被關掉的主智慧體從它的分頁直接消失。
對小隊或基地,其中網的每個智慧體(主智慧體、成員和基地的塔台)都被詢問,並保留它的 pane;一次拒絕或沉默就什麼都不關,30 秒後仍在執行的智慧體會讓小隊或基地保持開啟,否則分頁或工作區連同裡面剩下的東西(shell、子智慧體)一起關閉。
已完成的子智慧體有移除而不是關掉(它會在每個頁面隱藏它,直到它離開網),位置列中 HUD 下方的移除已完成(n)則經由同一條路徑一次隱藏網上每個已完成的子智慧體(按鈕只在有已完成的子智慧體時顯示,這台電腦的頁面和已配對裝置的頁面都一樣)。盟友 box 的已完成子智慧體也一併隱藏,各用自己的 key,與該列的「移除」相同(伺服器的 all 不會到 box,否則會隱藏那裡每個團隊的已完成單位),而且只計入連線中且列出 dismiss 的 box 的子智慧體;手機的左窗格也有它,在底部、緊貼在「回到地圖」上方,拇指碰得到兩者。顯示著被關閉內容的頁面會上移到剩下的東西:從被關閉的基地到地圖,從被關閉的小隊或單位的通訊畫面到它的基地。
flowchart LR
f["選資料夾"] --> c["建立"] --> s["啟動"] --> j["加入"] --> d["送達"]
+ New base 在你選的資料夾開一個 Herdr 工作區,裡面有一位成員、沒有塔台;+ Member 和 + Main 則在新分頁多開一個智慧體。頁面會顯示開啟的各步驟(建立、啟動、加入,有第一則訊息時還有送達),伺服器強制執行的規則和指令相同。
細節
+新基地是地圖上最後一張卡片,它在你挑選的資料夾中開啟一個 Herdr 工作區,第一個分頁有一個成員,沒有塔台:基地絕不是空的,開啟失敗時會再把工作區關掉。沒有塔台的基地只接受成員:基地上的 +成員在挑選的資料夾中的新分頁開啟一個,而在小隊上則把一個分割進該小隊的分頁,在它主智慧體的資料夾(或它第一個 pane 的資料夾)。它的塔台依塔台的規則開啟,絕不從頁面開。
有塔台的基地只接受主智慧體:+主智慧體在挑選的資料夾中的新分頁開啟一個,而且它一加入就自己升任;塔台自己的通訊畫面在它的標頭帶有同樣的 +主智慧體(手機和桌面都一樣),它為塔台的基地開啟那個區塊,並前往新主智慧體的通訊畫面。每個表單都可以帶一則可選的第一則訊息,在新智慧體加入後打給它。
資料夾以 Finder 風格的欄位挑選:有主目錄、桌面、文件,以及開著的基地所在的資料夾等捷徑;選取資料夾的每一層各有一欄,方向鍵在其間移動(欄內上下、欄間左右、Enter 選定);有上一頁與下一頁的麵包屑;以及新資料夾,它在選取的資料夾內建立一個資料夾(單純可見的名稱,沒有分隔符號、沒有開頭的點、不含 Windows 拒絕的字元;已存在的就只是選取它)並選取它。選取資料夾的真實路徑一律顯示,基地的名稱預設為資料夾本身的名稱,直到被改打。
單位開啟時,區塊以部署面板取代表單:伺服器在 open/progress 以頁面隨開啟一起送出的 id 回報開啟的步驟(建立、啟動、加入,有第一則訊息時還有遞送),頁面每秒輪詢兩次,執行中步驟的計時會跳動,完成的開啟會閃現「unit ready」(音效開啟時帶完成音效),然後頁面前往新單位;失敗時會指出它失敗的步驟和伺服器的理由,回到表單則回到原樣的表單。
伺服器強制同樣的規則(基地在它的塔台 session 存活時算作有塔台),讓挑選器留在主目錄內、不進隱藏的資料夾(開頭的點在各處都會隱藏;Windows 上還有 Hidden 或 System 檔案屬性,全新的設定檔會把它放在 AppData 和舊的相容性 junction,例如 Application Data、Cookies、My Documents 與在地化的開始功能表:它們不列出、不可挑選、不能在其中建立,也不能經由經過其中之一的路徑抵達;所列資料夾的屬性用一次 cmd.exe 呼叫(dir /a:h 和 /a:s)讀取並保留 10 秒,資料夾讀不了時,請求會帶著理由失敗,而不是憑猜測列出),一次只執行一個開啟(整個伺服器行程共用一把鎖,所以 GUI 在其中停止再啟動、或 token 輪替,都不會讓重試和仍在加入中的開啟並行:重試會被告知 429 busy),並在開啟失敗時關閉它建立的東西。
帶有 deploy id 的 open/base,在工作區一建立就會被記錄下來(它建立的工作區、分頁和 pane、它的資料夾與標籤,以及第一則訊息進行到哪裡;最近 64 個 id),所以這個 id 在伺服器重啟後仍然有效:用同一個 id 重複請求,會找到那個工作區並用它回答,它的 pi 還沒加入就等它加入,而且不會為該資料夾建立第二個工作區。
Herdr 會重用已關閉工作區的 id,所以記錄只在它的工作區還開著、記錄的 pane 還在而且在記錄的資料夾中執行、而且重複請求指名同一個資料夾時才算數。沒有回報資料夾的 pane(Herdr 的 cwd 是可選的,Windows 的 pane 在剛啟動時可能不回報)只在記錄的工作區 id、pane id 和工作區標籤全都仍然相符時才算數;更弱的條件一律丟掉記錄,因為錯的工作區比重複的更糟。有回報但不同的資料夾會讓記錄過期。被丟掉的記錄讓重複請求成為一次新的開啟(每個帶 id 的 open/base 也會忘掉工作區已不在的記錄)。
第一則訊息按 id 保存為 none(這次開啟沒有)、pending(從未嘗試)、sending(在呼叫打出它之前設定)和 sent。重複請求只在 none 或 pending 時打出它,這是伺服器在 join 期間重新啟動的情況;在 sending 或 sent 時它絕不再打。有訊息的 open/base 的回覆和重複請求的回覆帶有 delivery:sent(由這次呼叫打出,或由完成的較早一次打出)、unknown(較早的一次開始打它,而伺服器無法判斷它是否到達),或 not_sent(較早的一次有一則從未打出的訊息,而這次呼叫沒有帶訊息);open/progress 在開啟完成後帶有同樣的 delivery,而重複請求的 deliver 步驟在每種情況下都是完成的,所以完成的開啟絕不顯示待處理的步驟。
沒有到達的第一則訊息(502 prompt_failed,或對無法打字的伺服器 503 unavailable)會留下工作區和它已加入的 pi,並保留該 id,所以重複請求會找到它,不會建立第二個基地;在第一則訊息打出之前就拋出的開啟(pi 從未加入,或記錄無法寫入)會關閉工作區並釋放該 id。自己的 pi 沒有加入的重複請求會在 join 失敗(502 open_failed),並留下工作區和記錄,因為這兩者都不是它建立的:下一次重複請求再等一次。
無法寫入的記錄,在建立工作區時或在訊息要被打出時,會以固定的原因「The opening could not be recorded, so it was not kept.」讓開啟失敗(在 create 或 deliver;原因和它的路徑以 opening-record-failed 寫入 server.log,絕不進入回覆),而建立了工作區的開啟會在任何東西被打出之前把它關掉,而不是讓重試去建立第二個;這樣失敗的重複請求會留下工作區和記錄,那不是它建立的。
等待新 pi 加入預設是 60 秒;open/base 接受可選的 joinWaitMs(毫秒的整數,10000 到 300000;其他任何值都在建立任何東西之前被以 400 bad_request 拒絕),供 Windows 之類的慢電腦使用,而在這段時間內沒有加入的 pi 會讓開啟在 join 步驟失敗(502 open_failed,指名等待的秒數)。信任問題開著的時間不算在它之內,和以前一樣。open/member 和 open/main 保持 60 秒。
在帶有專案資源(.pi/settings.json、mcp.json、extensions、skills、prompts、themes、SYSTEM.md、APPEND_SYSTEM.md,或該資料夾或其上層的 .agents/skills)的資料夾中啟動的 pi,會在載入該資料夾的任何東西之前,停在 pi 自己的「Trust project folder?」,而這個提示只繪在 pane 的終端機裡。Straw Boss 在 Herdr 中的每個 pi(子智慧體也一樣)註冊一個 project_trust handler,它也會詢問正在執行的 GUI,所以只使用 GUI 的人會看到警示,而不是卡住的開啟。沒有 GUI 時,它回答 undecided,pi 照舊自己詢問。
細節
-
何時詢問。 handler 立刻回答
undecided,讓 pi 自己的順序保持不變(先是--approve或--no-approve覆寫,再是trust.json中已儲存的決定,再是defaultProjectTrust),除非同時符合:pi 在終端機上以它的 TUI 執行;這是行程的第一個 runtime(/new、/resume或/fork到另一個資料夾,是 pi 自己的提示);該資料夾或上層沒有儲存的決定(讀不到的trust.json算作一個決定:pi 會回報它);defaultProjectTrust是ask;而且有一個讀取信任請求的 GUI 在為這個 Herdr 執行(gui.json帶有trust: 1,指名這個 Herdr 的 socket,沒有停止,並在 1 秒內回答)。頁面只有在記錄的伺服器被證明就是正在執行的行程之後才取得 token:它的 pid 還活著,且它的啟動時間就是記錄的那個。在 macOS 和 Linux 上這是
ps,立刻完成。在 Windows 上,啟動時間是一次 CIM 讀取,快取冷的時候要 3.5 到 5 秒,所以新的 pi 可能要等到 6 秒,問題才會到達 GUI(如果gui.json指名一個存活、且讀取信任請求的伺服器,這個讀取在 pi 載入時就開始,所以到那時多半已在進行;6 秒內沒有拿到的啟動時間是未知,什麼都不送,pi 在終端機詢問)。指名已死亡 pid、已停止的伺服器,或什麼都沒有的gui.json,不花任何等待,也不啟動 PowerShell。 -
頻道。 pi 還沒有加入網,所以它透過
<state>/trust/中的檔案(模式 700,檔案 600)與伺服器溝通:它的請求<id>.json(它的行程、pane、角色、資料夾、信任會載入什麼,以及對資料夾和那些發現的 digest)、伺服器的<id>.answer.json,以及 pi 的<id>.result.json(哪一邊回答的),全都以 rename 寫入。在 Windows 上,資料夾必須位於使用者的設定檔之內(見 Windows);其他地方 pi 在它的終端機詢問。伺服器只在 Herdr 是它自己的、它的行程還活著、而且 Herdr 列出它的 pane 時才列出請求;行程已結束的請求會被移除。是連結、屬於別人、太大,或 digest 與它的發現不符的檔案不會被讀取。 -
競爭。 pi 的啟動提示一旦繪出就無法關閉,所以終端機印出三行(資料夾、「The question is in the Straw Boss window.」、「Press any key to answer here instead.」)並讀一個按鍵。GUI 的答案作為決定傳回給 pi(pi 的提示永遠不會出現);按鍵則交給 pi 自己帶有全部選項的提示,警示則以在終端機回答而關閉;15 秒內沒有伺服器回答也同樣交出。等待期間的
SIGINT或SIGTERM會移除請求。沒有任何東西逾時:智慧體不應因為沒有人看就信任或不信任一個資料夾。 -
警示。 每個資料夾和 digest 一個,不論有多少智慧體在等它,排序視為問題(它計入 HUD,Space 開啟它,提醒和聲音是等待的那種)。它指名資料夾和智慧體在哪裡執行,說明信任會載入資料夾的設定、擴充功能、skill、提示、佈景主題和 MCP 伺服器,並安裝它的設定所指名的套件(擴充功能和 MCP 伺服器在這台電腦上作為程式執行),列出資料夾裡有什麼(超過五項就收合),並提供信任(儲存在
trust.json中該資料夾之下)、只信任這一次和不信任(只限這個行程,所以下一個智慧體會再問)。pi 的「信任上層資料夾」留在終端機。 -
誰能回答。
POST /<token>/api/trust/answer {id, digest, choice, answerId},受頁面慣用的檢查(origin、Sec-Fetch-Site、CSRF、64 KB):這台電腦的頁面,以及遠端 listener 上已配對的裝置(如同轉送的確認);Straw Link 的對等端絕不行。答案必須帶有請求的 digest,所以它針對的恰好是顯示出來的內容;頁面送來的任何東西都不是路徑。每個請求只接受一個答案(另一個得到busy;同一個答案再送一次會聽到它的結果),寫入該資料夾和 digest 的每個請求,回覆是accepted(某個 pi 取用了它)、stale(終端機或 pi 的結束先發生),或unknown(5 秒內沒有結果;再次確認會再看一次)。 -
開啟。 等待信任問題的新 pi 還沒有加入,所以每個等待新 pi 的等候(頁面的開啟、
boss_open_main、塔台的啟動、接回)在它的 pane 有請求時停止計時,並在十分鐘之後以「No trust decision forwithin 10 minutes」失敗,關閉它建立的東西。部署面板在 start 與 join 之間有一個信任步驟,內嵌同樣的三個選項;該頁面的警示佇列於是不為它開第二個對話框,而已配對的裝置或另一個分頁仍然收到警示,而面板的 120 秒放棄不計這段時間。 -
桌面模式與團隊設定檔。 不需要更多東西:桌面視窗就是這台電腦的頁面,而桌面堆疊上的每個 pane 都執行這個 handler。把
defaultProjectTrust留在ask,這樣每個新資料夾問人一次,而信任會記住它(always會不經詢問就載入任何複製來的儲存庫的擴充功能;never會丟掉團隊儲存庫自己的.agents/skills)。
伺服器一次在一個 session 中執行,也就是開啟它的那個,或接手它的倖存者(見每台機器一個伺服器),在 127.0.0.1 上以兩個隨機的空閒 port 執行:本機 listener 供這台電腦的瀏覽器使用,遠端 listener 則是 tailscale serve 唯一指向的目標。哪個請求是本機的,由它到達的 listener 決定,而不是它自己宣稱的:遠端 listener 上的請求不論它的 Host 或標頭怎麼說都是遠端的,而本機 listener 只放行帶有 Host: 127.0.0.1:<port>、且沒有 tailscale serve 加上的任何轉送標頭(X-Forwarded-*、Tailscale-*、Forwarded、Via)的請求,其餘以 403 拒絕(第二道防護)。兩個 listener 執行同一個 handler。
細節
兩個 listener 執行同一個 handler。每個 URL 都以隨機 token 開頭(http://127.0.0.1:<port>/<token>/);沒有它或帶有另一個 Host 標頭的請求會被以 403 拒絕,所以其他本機頁面讀不到網。唯一的寫入是 /<token>/api/sessions/<key>/ 底下的 POST:input;keys,它接受來自即時顯示該終端機的頁面的一個按鍵板按鍵,或 Stop 的 Escape;commands,它向 session 的 bridge 詢問符合所打 /name 的指令,不呼叫任何 Herdr 方法;answer,它把對話框的答案交給它的擁有者,不送任何按鍵;以及 mirror,它在頁面自己的串流上顯示 pane 的畫面。只有這台電腦的頁面還可以送出(已配對裝置會得到 403)server/stop、remote/pair(新的一次性配對碼:配對屬於桌面)、remote/on、remote/off、remote/devices、remote/unpair、remote/unpair-all,以及 /<token>/api/dismiss(一個已完成的子智慧體,或 all)。每一個都需要頁面自己的 Origin、有送時的 Sec-Fetch-Site: same-origin、最多 64 KB 的 JSON 內容,以及寫進頁面的 CSRF secret;對 input、keys 和 answer,伺服器還會當場向 Herdr 確認該 pane 仍在該 session 的檔案上執行 pi(鏡像在第一次讀取之前和每 2 秒檢查一次,session 移到另一個 pane 時重新開始)。
打字的文字絕不記錄。頁面是建置好的靜態 bundle;任何路由都回答同一個頁面,它帶有 <base href>,所以從任何深度都能載入。串流(/<token>/events)先報上自己的名字(hello,一個頁面用來附掛終端機鏡像的隨機 id;還沒讀過網的伺服器在第一次讀取後才報名),然後在連線時和每次變動時送出完整快照,每 15 秒一個 ping,Herdr 或 intercom 讀不到時送出 problem 事件;events?watch=<session key> 加上該 session 的歷史,events?watch=w:<workspace id> 加上那裡每個單位的最後一行。
/<token>/api/history/<key> 和 /<token>/api/entry/<key>/<entry id> 分頁讀取 session,以及完整取得一筆項目(上限 1 MB)。文件以 capability 提供,也就是伺服器給每份被列出文件的隨機 128 位元值,絕不用路徑:/<token>/doc/<cap>/ 是 HTML 文件(最大 2 MB),其中每個 cgi-bin:pi-answer 表單指向一次性的 nonce(30 分鐘,綁定該文件),/<token>/doc/<cap>/<file> 是文件自己目錄之下的圖片、樣式表或字型(最大 8 MB,沒有 ..,沒有指向目錄之外的 symlink),/<token>/doc/<cap>/answer/<nonce> 是一個表單答案(最多 64 個欄位,16 KB 的 query,12 KB 的答案,除了 tab 和換行之外沒有控制字元),而 /<token>/api/doc/<cap> 是以 JSON 提供的文字文件。
只提供 session 目前列出的文件。每個 /doc/ 回應都帶有沒有 allow-scripts 或 allow-same-origin 的 CSP sandbox,而它的來源指名伺服器的 origin(WebKit 在 sandbox 文件中不符合 'self');app 只能框住它自己的頁面,並送出 Cross-Origin-Opener-Policy: same-origin。重寫也會丟掉文件自己的 <base>、referrer policy,以及它按鈕的 formaction、formmethod、formtarget;這是對 HTML 文字盡力而為,真正守住的是 sandbox 和政策。
頁面在監聽時每 2 秒讀一次網;沒有頁面時仍每 10 秒讀一次,所以時間軸會保留手機睡著期間發生的事。頁面可以安裝成 app:bundle 帶有 manifest.webmanifest(它的 id、start_url 和 scope 是 ./,所以它們在 token 路徑之下解析)、PNG 與 SVG 圖示,以及 apple-touch-icon.png;頁面的政策加上 manifest-src 'self',並以 crossorigin="use-credentials" 連結 manifest,所以已配對裝置的 cookie 會一起送出。沒有 service worker:每個頁面都來自正在執行的伺服器自己的建置。
Session 檔
Section titled “Session 檔”伺服器只讀它剛讀取的網中單位的 session 檔(已加入的 session、塔台、透過它們的 pane 或啟動腳本找到的子智慧體,以及基地的其他 pi pane);路徑絕不來自瀏覽器。檔案只從執行 pi 的 pane 讀取,只讀 pi 的 sessions 目錄之下的 .jsonl(PI_CODING_AGENT_DIR 或 ~/.pi/agent 中的 sessions/,兩側的 symlink 都已解析),而對 intercom session,只有在檔案是以那個 session 命名時才讀,所以在 /new 期間讀取的網不會把舊 session 和下一個檔案配成一對;被監看的 session 的檔案改變時,會重新 attach 到新檔案。
細節
檔案以 1 MB 為單位,依每一行的頭尾位元組建立索引,記憶體裡只留索引;要送出的頁面則依位移讀取項目。建立索引、附加和讀取依序一次執行一個,所以 pi 在第一次建立索引期間寫入的項目會等它,而讀取絕不使用已被取代的檔案的索引。被重寫的檔案或分支移動會重設頁面。讀不到的第一頁會在下一次網讀取時再試。
離開網的單位仍可讀取十分鐘,如果有的話,附上通往該 pane 新 session 的連結。session 檔開啟時,在最後一步不跟隨 symlink(O_NOFOLLOW):在網解析它的路徑之後被換成連結的檔案不會被讀取。在平台沒有 O_NOFOLLOW 的地方(Windows),會在開啟之前才檢查連結,這在檢查與開啟之間留下一個短暫的空窗;能在 pi 的 sessions 目錄中換檔案的同一使用者的行程可以利用它,讓 GUI 顯示該目錄之下另一個 .jsonl,而讀到結果的只有使用者和他已配對的裝置。存放在其他地方的 session(--session-dir、PI_CODING_AGENT_SESSION_DIR,或 pi 的 sessionDir 設定)顯示在地圖上,但沒有歷史,也不能對它打字。
sequenceDiagram
participant P as pi session
participant S as sb server
participant T as Tailscale
participant M as 手機
P->>S: /boss gui remote
S->>T: tailscale serve 項目(HTTPS 8443)
S-->>P: QR 與一次性連結
M->>S: 掃描、開啟連結
S-->>M: 裝置 cookie,完成配對
/boss gui remote 請 sb server 用一條 tailscale serve 項目把 GUI 放上你的 tailnet(HTTPS、port 8443,只指向遠端 listener,絕不用 Funnel)。session 會在 pi overlay 顯示 QR;以這台機器的使用者登入 Tailscale 的手機掃描後,會拿到一個裝置 cookie。每個遠端請求還必須帶著這台機器的 Tailscale 身分。
細節
在任何 pi session 裡執行 /boss gui remote,會請 sb server 把 GUI 放上你的 tailnet:它新增一筆 tailscale serve 項目,在 8443 連接埠提供 HTTPS,代理到 server 的遠端 listener(絕不使用 Funnel,也絕不指向本機 port),然後 session 在 pi 的覆蓋層顯示一個 QR code。這個 QR code 是在本機根據 server 的回應繪製的,所以 QR code 與它的一次性連結都不會進入 server 的 log 或模型的對話。用以這台機器的使用者身分登入 Tailscale 的手機掃描即可(若是已安裝的 app,則輸入 QR code 下方印出的短代碼:見下方 已安裝 app 的入口)。
在覆蓋層裡,c 複製連結,o 在這台電腦的瀏覽器開啟連結(桌面堆疊 desk stack 中的 session,STRAW_BOSS_DESK=1,沒有 o 鍵,也不列出連結:由 shell 自己顯示頁面);每一次 QR、複製與開啟都是各自獨立的一次性連結,十分鐘內只能使用一次,Esc 或 q 會關閉它。覆蓋層開啟期間,這個 session 視為等待中,所以來自瀏覽器的輸入會被拒絕,而不是在裡面遺失。設定頁的「手機與裝置」區塊也會在頁面中顯示新的 QR code;它只顯示在這台電腦的頁面(127.0.0.1),不會顯示在已配對裝置的頁面,也不會顯示在 desk 視窗中(頁面中的 Desk 模式)。
已配對的裝置會拿到一個 session cookie(HttpOnly、Secure、SameSite=Strict、90 天,範圍限定在 token 路徑),並一直維持配對,直到桌面端解除配對、GUI 換了新 token(配對屬於 token:重啟會保留,但連接埠被佔用、gui-secrets.json 遺失或損毀,或 /boss gui rotate 都會產生新的 token,需要新的 QR code)、這台機器的 Tailscale 身分(host 或 login)改變,或連續 90 天沒有使用為止;每次使用都會續期,回應會再次設定 cookie(儲存的到期時間最多每小時移動一次)。
server 只保留 cookie 的 SHA-256,連同裝置名稱(配對時從其 User-Agent 讀取的平台與瀏覽器)、配對時間與上次使用時間,存放在 gui.json 旁的 paired.json(mode 600);它在遠端關閉再開啟、server 重啟之後都會保留,但新的 GUI token 會讓所有裝置解除配對,因為它們的 cookie 範圍限定在舊 token 的路徑,反正也無法連線;身分與要開啟遠端的意願會保留,log 會寫 devices-cleared,手機再掃新的 QR code(見每台機器一個 GUI)。
每個遠端請求都從遠端 listener 進來,而且必須同時帶有這台機器的 Tailscale 使用者,放在 tailscale serve 設定的 Tailscale-User-Login 中,並符合確切的 MagicDNS host。機密只在本機 listener 的頁面輸入,所以丟掉轉送標頭的 proxy,或仍指向本機 port 的 serve 項目,都無法把遠端請求變成本機請求。文件自己的請求(/<token>/doc/…)需要該身分與文件的 capability,但不需要 cookie,因為 Chrome 不會在沙箱 frame 的請求中帶上 SameSite=Strict cookie:在 tailnet 上,同一個 Tailscale 使用者的裝置只要持有文件的連結,不必配對就能讀取文件並回答其中的表單。
8443 port 只有在空閒或已經是這個 server 的時候才會取用,包括遠端 listener 出現之前、指向這個 server 本機 port 的舊項目:開啟遠端(或頁面上的「開啟遠端存取」)會把它改指向遠端 listener;如果 Tailscale 沒有接受這項變更,遠端維持關閉,訊息為 “tailscale serve did not take the entry as asked”,且不記錄任何東西,所以經由舊目標的請求會被本機 listener 以 403 拒絕,而不是被當成本機請求處理。指向其他地方的項目絕不會動。
關閉遠端,以及啟動一個不恢復遠端的 server,都會移除舊目標的項目;以「想要遠端開啟」狀態啟動的 server 則會把它改指向遠端 listener(同樣遵守「空閒或已經是自己的」規則),只有在維持關閉時才移除。/boss gui remote off(或這台電腦設定頁「手機與裝置」區塊中的關閉遠端存取)只移除那一筆項目(其他 serve 項目絕不會動),結束每一條遠端串流,並忘記曾經想要開啟遠端;已配對的裝置仍維持配對。
/boss gui stop 不會動它(該項目指向遠端 listener,會隨 GUI 一起停止),但 server 結束時會在 server 不在的期間移除該項目,而遠端仍維持「想要開啟」:下一個啟動的 server(來自 /boss gui、cli.mjs up、重啟、升級,或 pi session 的 look)會自己再把遠端開啟,沿用同樣的安全檢查,也不需要 QR code;做不到時(Tailscale 關閉、8443 被佔用),它會在 cli.mjs status(remote.note)與 up 的 remote_off 警告中說明原因,並在下次啟動時再試。
解除配對:這台電腦設定頁的「手機與裝置」區塊列出已配對的裝置(名稱、配對時間、上次使用),每一台都有「解除配對」,另有「全部解除配對」,這兩者都會立刻結束該裝置已開啟的串流;裝置清單絕不會提供給已配對的裝置。有終端機存取權的已配對裝置,等同於老闆自己的鍵盤一樣受信任:頁面路由拒絕它進行配對、遠端、Wi-Fi 與 server 管理,它的輸入框與新 session 的第一則訊息會拒絕對應的 /boss 指令(以及 /boss gui view <mode>)以防誤觸,但按鍵板的終端機按鍵不會被過濾,所以它可以輸入 pi 接受的任何東西,包括 ! shell 指令,而按鍵板的 Escape 可以關閉任何已開啟的對話框,包括所有 hereOnly 的;這是拒絕,絕不是核准。
Tailscale 身分改變、已 90 天沒使用,或 GUI token 被取代的裝置,需要新的 QR code(在 app 裡則是新的代碼)。恢復時會檢查身分:新的 server 只有在 Tailscale 身分(host 與 login)與裝置配對時一致(paired.json 記得它)的情況下,才會自己開啟遠端;若是另一個身分(登入了另一位使用者或另一個 tailnet),它維持關閉、移除找到的任何 serve 項目、說明原因(一則警告),並保留意願與舊裝置不動,所以下次以被記住的身分啟動時會再開啟遠端,在那之前每次都會重複同樣的說明;以新身分手動開啟遠端(/boss gui remote)才會讓舊裝置失效。
無法寫入的 paired.json(磁碟已滿、沒有寫入權限的資料夾)不會讓請求丟出例外,變更會在記憶體中持續生效直到 server 停止(解除配對的裝置維持解除配對,遠端關閉維持關閉),但會回報:解除配對、全部解除配對、開啟與關閉遠端會回應 500 save_failed 並附上原因(「手機與裝置」區塊會顯示它),session 也會顯示警告,因為重啟後被撤銷的裝置會再度配對,或遠端會再度開啟。access.saveError 存放原始錯誤。舊版的 paired.json只有在其中某個 session 尚未過期時才算「想要」。
遠端關閉時,如果它的項目仍存在於另一個 host 名稱下(Tailscale 身分改變),會保留 remote.json,讓之後的關閉能移除它。當 Tailscale 不存在、已登出、是帶標籤的節點、沒有 MagicDNS,或 8443 提供的是別的東西時,它會說明原因,且不改變任何東西。
版面可在手機上使用:沒有橫向捲動、觸控目標較大、有安全區域內縮,且輸入框停留在螢幕鍵盤上方。自 0.26.0 起,手機上的 session 占滿整個螢幕(‹ 回到它的基地,資料以全螢幕開啟它的檔案,且自 0.26.1 起,塔台的檔案在每種螢幕上都列出它基地的小隊與單位),輸入框固定在鍵盤上方的底部,圖塊也以同樣方式在其上開啟;輸入框把輸入欄放在上方,+ 放在送出那一列,備註行在空白時不占空間。
當手指按住或甩動通訊記錄時,沒有任何東西會把它拉回最新的一筆。網的記錄可從標題處收合,在手機上只顯示最新的兩筆。要當成 app 使用,先在瀏覽器配對,再加到主畫面:iOS 只在安裝時把瀏覽器的 cookie 複製進 app 一次,之後兩者各自保有獨立的 cookie,所以配對連結(會在瀏覽器開啟)永遠到不了 app;在 app 自己的入口輸入配對代碼則可以。在 Android 上,從 Chrome 安裝的 app 與 Chrome 共用 cookie,但啟動它是從另一個網站發起的導覽,會讓 SameSite=Strict cookie 被留在後面:沒帶 cookie 且 Sec-Fetch-Site 不是 same-origin 的頁面載入,會得到一個自己重新載入一次的頁面,而那次同源的重新載入會帶上 cookie。
已安裝 app 的入口。 已安裝的 app 無法跟隨會變動的 token(連接埠被佔用、儲存檔遺失或 /boss gui rotate 都會換新的),而且在 iOS 上它有自己的 cookie jar,在 Safari 開啟的連結永遠到不了那裡。所以遠端 listener(僅限遠端;在本機 listener 上 /sb-app/ 會被視為錯誤的 token 而回答 403)還會回應一個固定、不含 token 的路徑 /sb-app/,而它對遠端頁面提供的 manifest,start_url 是 /sb-app/、scope 是 /;本機頁面的 manifest 維持原樣。
這個入口執行與頁面相同的身分檢查(遠端已開啟、確切的 MagicDNS host、沒有 Funnel 標記、這台機器的 Tailscale-User-Login),它的頁面不含 token,也不含任何機密。帶有效裝置 cookie 的 GET 會回應 302 到目前的 /<token>/ 並再次設定 cookie;沒有 cookie 的則顯示一個有八個格子、每格一個字元的頁面,用來輸入配對代碼(它有自己的 form-action 'self' 與 Referrer-Policy: same-origin,因為在其他頁面 no-referrer 之下的表單 post 會帶 Origin: null 而被拒絕;/sb-app/entry.js 提供的腳本在同樣嚴格的 script-src 'self' 下負責在格子間移動並接受貼上,沒有它時,這些格子就是 server 會合併的 c1 到 c8 欄位,或是單一的 code 欄位),或者,對於不是 Sec-Fetch-Site: same-origin 的文件載入(Android 啟動 app 時,會讓 SameSite=Strict cookie 被留在後面),則回應與帶 token 的頁面相同的一次性自我重新載入。
POST /sb-app/pair 接收代碼:它必須是來自入口自己頁面的表單 post(Origin 等於 https 名稱、送出時 Sec-Fetch-Site 為 same-origin、application/x-www-form-urlencoded、最多 1 KB),所以來自其他網站的 post 永遠碰不到代碼,也不會產生任何作用。代碼是取自 32 個符號字母表(不含 I、L、O 與 U)的 8 個字元(40 位元,顯示為兩組各四個字元並以空格分開,ABCD EFGH,絕不使用連字號;大小寫、全形字母(NFKC)、任何空格或破折號與外觀相似的字元都會被寬容處理),在 /boss gui remote 執行時產生(在它的覆蓋層中的 QR code 下方,絕不出現在對話顯示的文字中),以及「手機與裝置」區塊要求時(在 QR code 旁)產生;它只能使用一次,十分鐘內有效,並在遠端關閉或身分改變時失效;最多同時有十六組等待。
輸錯五次之後,所有等待中的代碼都作廢,入口回應 429,直到這台電腦要求新的代碼(這會把五次機會還回來),所以 tailnet 上的裝置無法暴力猜測那 40 位元。用代碼完成的配對與掃描完成的配對相同(有名稱的裝置、90 天、使用即續期),它的回應會設定兩次 session cookie:頁面用 Path=/<token>/,入口用 Path=/sb-app/,各自都是 HttpOnly、Secure、SameSite=Strict,所以配對絕不會被送到該 host 上的其他路徑;QR 配對與續期也會同時設定這兩個。
這台機器的使用者對失效 token 發出的文件載入,或對目前 token 發出、沒有配對且 Sec-Fetch-Site: same-origin 的文件載入,會回應 302 到 /sb-app/,所以在入口出現之前就安裝的 app(它的起始 URL 是舊的帶 token 位址)會自己找到入口;其他任何請求則照舊得到拒絕。其餘情況的 401 文字會告訴你在已安裝的 app 中輸入代碼。重啟會保留 token 與配對,所以已配對的 app 會自己繼續運作;代碼用於第一次配對,以及被新 token 解除配對的裝置。
Wi-Fi 存取
Section titled “Wi-Fi 存取”flowchart TD
on["/boss gui lan"] --> w["讀過風險,<br/>回答 y"]
w --> ca["這台電腦自己產生 CA"]
ca --> ph["每支手機安裝一次"]
ph --> pr["配對,在這台<br/>電腦上核准"]
/boss gui lan 讓與這台電腦同一個 Wi-Fi 的手機不用 Tailscale 就能開啟 GUI。它預設關閉,開啟後在重啟之間保持開啟,直到 /boss gui lan off。Tailscale 存取不變,仍是獨立的開關;兩者可以同時開啟,手機也可以透過兩者各自配對。終端機使用英文;手機看到的頁面依瀏覽器的第一語言使用英文或繁體中文。
細節
先讀風險。 Wi-Fi 存取需要一個由這台電腦自己建立、每支手機安裝一次的憑證授權單位(CA)。信任該 CA 的手機會接受 CA 簽署的任何東西。任何拿到 CA 金鑰副本的人,都能在手機連上的任何 Wi-Fi 上,對該手機冒充這台電腦,也能冒充任何私有網路位址(路由器或 NAS 的 192.168.x.x 頁面),直到手機上的憑證被移除為止。
金鑰放在 lan-ca.json,所有以你身分執行的程式都讀得到,包括你執行的智慧體,而且它可能出現在 agent 資料夾的備份中。macOS 會把這個檔案排除在 Time Machine 之外,但 Time Machine 的本機快照可能仍保有被取代的金鑰,最長一天。CA 受名稱限制所約束(見下),而這只在手機的軟體會強制執行時才有效。所以請在手機的熱點上使用 Wi-Fi 存取,或在讓手機直接連到這台電腦的 Wi-Fi 上使用,這樣網路上就沒有其他人。
第一次 /boss gui lan 會顯示這段警告,且只有在你回答 y 之後才建立 CA;頁面的 Wi-Fi 分頁在開啟任何東西之前,也會顯示同樣的文字。
這台電腦每個符合條件的位址(僅限私有 IPv4)各有自己的 listener,預設為 8445。IPv6、Tailscale 位址、link-local 位址和公開位址一律不提供。
細節
- 這台電腦每個符合資格的位址都有自己的 listener(預設
8445,記錄在lan.json,因為 port 是已安裝 app 的 origin 的一部分)。 - 位址符合資格的條件是:它是
10/8、172.16/12或192.168/16內的 IPv4,位於真實的網路介面上(不是 loopback、不是utun、WireGuard 這類通道,也不是/31或/32的連結),且前綴合理。 - 絕不提供:IPv6、Tailscale 位址、link-local 與公開位址。
- VM 與容器的橋接會被提供,因為沒有任何東西能把它們與 Wi-Fi 區分開來;第二層 VPN 介面卡也可能被提供。覆蓋層、頁面與
cli.mjs status都會列出每個被提供的位址與它的介面,讓你看得到它,也能關閉 Wi-Fi 存取。 - 每個連線都必須來自與被提供位址同一子網路的私有位址;其他任何連線都會在讀取任何位元組之前關閉。
- listener 以第一個位元組分辨 TLS 與純 HTTP。TLS 得到 GUI;純 HTTP 只回應憑證頁面。
- server 每 5 秒監看網路介面:新位址得到 listener,消失的位址失去它,從睡眠喚醒時會重新綁定全部位址。
- 位址上的 port 被其他程式佔用,會讓該位址不被提供並附上說明;只有此時
/boss gui lan才會移動 port,而安裝在舊 port 上的 app 必須重新加入。
CA 只產生一次,存在 lan-ca.json;每個提供的位址各有一張短效的 server 憑證,只寫這台電腦的 .local 名稱和該位址。CA 的名稱限制為私有 IPv4。
細節
lan-ca.json存放一個有效期兩年的 ECDSA P-256 CA。每個被提供的位址都得到一張 90 天的 server 憑證(保存在記憶體中,剩不到 30 天時更新),其中只列出<computer name>.local與該位址,沒有別的。- CA 只能簽署那個名稱、私有 IPv4 位址,不含 IPv6、電子郵件位址與 URI。若讀不到電腦名稱,CA 就只簽署位址,QR code 帶的是位址。
- 到期前六十天,
status與頁面會提醒;到期後,Wi-Fi 存取會自己關閉,直到你更新為止。 /boss gui lan reset會詢問:r 產生新憑證並保留配對(舊的即將到期,或電腦改過名稱);u 另外把所有 Wi-Fi 裝置解除配對(金鑰或手機可能被偷)。不論哪一種,每支手機都必須移除舊憑證並安裝新的;移除它才能讓舊金鑰的副本失效。/boss gui rotate不會動 CA。- 損毀、屬於他人、是連結或權限過寬的
lan-ca.json會讓 Wi-Fi 存取維持關閉並附上說明;沒有任何東西會在你背後建立新的 CA。
讓手機信任它(每支手機一次)
Section titled “讓手機信任它(每支手機一次)”每支手機要信任這個 CA 一次。最好的方式是從這台電腦用 AirDrop 或 Quick Share 把設定檔(iPhone)或 .crt(Android)傳給手機;憑證頁面是後備。
細節
- 最好:從這台電腦用 AirDrop 或 Quick Share 把描述檔(iPhone)或
.crt(Android)傳給手機。覆蓋層的 f 鍵會把這些公開檔案寫到lan-ca-files/並顯示出來;頁面的分享按鈕會下載它們,每個檔案一個連結。Wi-Fi 上沒有人碰得到這條路徑,而且除非手機問你要不要接受來自未知寄件者的檔案,否則不需要任何檢查。 - 否則掃描憑證 QR code,或開啟
http://<name>.local:8445/sb-ca/,這是一個純 HTTP 頁面,提供檔案以及該手機平台的步驟。任何控制 Wi-Fi 的人都能掉包那個頁面提供的內容,而且頁面本身不顯示指紋,因為掉包頁面的攻擊者會印出真正的那個。所以檢查在這台電腦的螢幕上進行,它顯示真正憑證的 SHA-256 開頭,以及在手機上要看哪裡:- iPhone,在按安裝之前:描述檔的「包含」列只寫一張憑證,「更多詳細資訊」恰好列出一張憑證,沒有別的,且它的 SHA-256 以顯示的位元組開頭。
- Android,安裝之後:「信任的憑證」、「使用者」恰好顯示一個 Straw Boss LAN CA,沒有你沒加入的任何東西,且 SHA-256 相同。
- Mac:「鑰匙圈存取」恰好顯示一張,且 SHA-256 相同。
- Windows:匯入期間恰好出現一個安全性警告,且它的 SHA-1 憑證指紋與顯示的相符。任何其他情況都代表要把加入的東西移除。
- 描述檔與檔案各自恰好只含一張憑證,所以數量不同就表示被掉包了。
- 在 iPhone 上,安裝描述檔後,要到「設定」、「一般」、「關於本機」、「憑證信任設定」中開啟該憑證。在 Android 上請使用 Chrome;它信任你安裝的憑證,而 Firefox 預設不會。
- 12 版之前的 Android 手機可能無法解析
.local;在覆蓋層按 i 改用位址。從位址安裝的 app 會在位址改變時失效。 - 頁面
/sb-ca/check顯示手機是否信任這個名稱的某張憑證。它分不出是這台電腦的還是攻擊者的。
配對,並在這台電腦上核准
Section titled “配對,並在這台電腦上核准”overlay 的 QR 帶的是不含 token 的連結,手機要先按配對;一個代碼只能用一次,兌換後只會產生待核准的裝置,必須由這台電腦核准。
細節
- 覆蓋層的 QR code 帶有不含 token 的連結
https://<name>.local:8445/sb-app/pair/<code>,它會在手機使用代碼之前,要求手機按下配對,所以連結預覽無法消耗它。已安裝 app 用的輸入式代碼只在被要求時才顯示。 - 代碼只能使用一次,十分鐘內有效,且每個介面(一個覆蓋層、一個頁面分頁)最多只有一組有效代碼;產生新的會讓先前顯示的作廢。
- 兌換過的代碼只會產生一個待核准的裝置,並在手機上顯示四位數字。你要在這台電腦上輸入那個數字來允許它,可以在覆蓋層或頁面的 Wi-Fi 分頁中輸入。兩處都不會印出那個數字,也沒有按鈕可以允許列出的請求,所以用到被看見的代碼的旁觀者,不可能被你反射性地允許。覆蓋層用
d拒絕一個請求(有多個等待時,上下鍵標記是哪一個;頁面上則是「拒絕」),或用n全部拒絕;拒絕不需要數字。輸錯三次數字,會拒絕所有待處理的請求;兩分鐘沒有回應就捨棄該請求;每個結果都會在覆蓋層、session、頁面與 log 中公告。 - 輸入的代碼依對端位址鎖定(五次錯誤),而且在所有位址累計 200 次錯誤後作廢;有人似乎在猜測時,頁面會說明。
- 在
/boss gui lan畫面開啟時,已配對的裝置(不論哪個網路)只能對該 session 送出 Escape,而且每個畫面在開啟後的 1.5 秒內只接受 Escape。對該 session 的/boss send也受同樣的時間點限制(該 post 會寫guarded),因為已配對的裝置可以讓另一個 session 送出它。手機無法回答它正在看的那個畫面。 - 在沒有畫面開啟時到達的請求,會在每個 pi session 與頁面中發出通知。
- 核准無法擋住被掉包的憑證:中間人攻擊者會轉送真正的數字。指紋檢查才擋得住。
核准後手機持有一個 Wi-Fi cookie,只設給 GUI 的路徑,和遠端的 cookie 分開。Wi-Fi 裝置能做的和已配對的 tailnet 裝置一樣,沒有更多。
細節
-
cookie 是
__Secure-sb_lan(HttpOnly、Secure、SameSite=Strict、90 天、使用即續期),只為/<token>/與/sb-app/設定。遠端與 Wi-Fi 的 cookie 位於不同的 host,絕不會混用。 -
允許的裝置存放在
lan.json,綁定 GUI token:新 token(連接埠被佔用、儲存檔遺失、/boss gui rotate)會讓它們解除配對。它們在 Wi-Fi 存取關閉再開啟、重啟、新位址與 CA 更新之後都會保留。 -
Wi-Fi 裝置能做的事與已配對的 tailnet 裝置相同,不會更多。server、遠端、Wi-Fi 與大部分 Straw Link 的控制項仍只在這台電腦的頁面上。
-
在 Wi-Fi 裝置上開啟的文件,需要裝置針對文件本身的 cookie,而文件載入的一切(圖片、樣式、回答)都經過頁面
<base>中一張短效的票證,所以外洩的連結什麼也開不了。解除配對、關閉 Wi-Fi 存取或新 token 都會讓票證失效。 -
這台電腦頁面上的設定頁「手機與裝置」區塊有 Tailscale 分頁與「同一個 Wi-Fi」分頁,以及一份兩者共用的裝置清單,每台裝置都標示它的網路。Wi-Fi 分頁顯示風險與熱點建議,只有在你按下它的按鈕時才開啟 Wi-Fi 存取,並提供憑證檔案(每個檔案一個連結,標示它適用的平台)、指紋步驟、配對 QR code、代碼、位址切換、待處理的請求,以及輸入手機數字的欄位。
來自 server 的說明與事件(連接埠被佔用、防火牆、電腦改名、沒有位址、憑證到期、裝置配對或被拒絕)都附有代碼與它的參數,頁面用英文或繁體中文呈現;防火牆說明中的 PowerShell 與 macOS 指令則完全照 server 給的內容。表示手機現在無法連線的說明(Wi-Fi 存取被擋下、連接埠被佔用、憑證已到期、防火牆封鎖)以錯誤顯示;移動過的 port、VPN、即將到期的憑證與事件則只是告知。
來自同一位址被拒絕的配對代碼合併成一行,附上次數與最近一次的時間。當配對請求在等待而 Wi-Fi 分頁沒有開啟時,頁面的標頭會說明,按下它就會開啟該分頁。這個圖塊不會顯示在 desk 視窗或已配對裝置的頁面上。
-
Straw Link 不變:加入 tailnet 仍只透過 Tailscale,而且 link 的任何部分都不在 Wi-Fi 上監聽。
-
只有預設 Herdr 的 sb server 提供 Wi-Fi 存取;其他 slot 的 server 會這麼說明。
已安裝 app 在 Wi-Fi 上的入口。 Wi-Fi listener 也回應固定的 /sb-app/,使用 Wi-Fi 自己的代碼、cookie 與等待頁面。輸入的 Wi-Fi 代碼顯示為 ABCD EFGH,並在與 Tailscale 入口相同的八個格子中輸入,同樣寬容地解讀(大小寫、全形字母、空格、任何破折號與不可見字元)。入口的頁面會送出 Referrer-Policy: same-origin:在 listener 慣用的 no-referrer 之下,表單 post 會帶 Origin: null,而兌換會拒絕它。從名稱安裝的 app 在電腦位址改變時仍能繼續運作。
防火牆。 server 絕不更改防火牆;它讀取不需提權就能讀到的資訊,並附上修正方式的說明。
- macOS:當應用程式防火牆開啟,而且封鎖所有連入連線或封鎖
node時,說明會要你到「系統設定」、「網路」、「防火牆」、「選項」中允許它。 - Windows:見 Windows。
- Linux:不檢查;允許該子網路連到該 port,例如
ufw allow from <subnet> to any port 8445 proto tcp。 - 封鎖本機流量的全通道 VPN 會讓位址被提供卻連不到;說明會要你在 VPN 的設定中允許本機網路存取。
手機是否會對你安裝的憑證強制執行 CA 的名稱限制,是各平台自己的行為,要量測,不能假設。目前還沒有量測任何東西:每個平台都算未量測,並得到強烈的警告。憑證頁面、同一個 Wi-Fi 分頁和 /boss gui lan 覆蓋層都明白說出,手機是否限制該憑證還沒有被驗證,而金鑰的副本可以對它假冒成任何網站。下表在這道關卡(見發布說明)執行之前一直維持「未量測」;通過的平台會得到較窄的文字。
| 用戶端 | 作業系統與瀏覽器版本 | 允許位址之外 | 另一個 .local 名稱 |
IPv6 | 已到期的 CA | 無名稱 CA 之下的另一個 DNS 名稱 (f) | 無名稱 CA 的真實葉憑證 | 郵件與程式碼簽署的葉憑證 (g) |
|---|---|---|---|---|---|---|---|---|
| iPhone、Safari 與主畫面 app | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 |
| Android、Chrome 與已安裝的 app | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 |
| macOS、Safari 與 Chrome | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 |
| Windows、Edge 與 Chrome | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 | 未量測 |
Server 面板
Section titled “Server 面板”設定頁的 伺服器區塊只在這台電腦的頁面上(127.0.0.1;已配對的裝置看不到,server 對它回答 403 local_only)顯示,打開後呈現 sb server 的狀態(運作中、重啟中或已停止;版本;模式;運作時間;最後一個錯誤)、重新啟動伺服器(就地確認,說明頁面會保留它的位址並自己重新連線,接著 server 重啟,回來時頁面重新連線,若沒有就附註請開啟 /boss gui:只有 port 被佔用時位址才會改變)以及 顯示記錄(server.log 的最後 200 行,其中沒有訊息文字或機密)。從頁面重啟時,log 中請求者的名稱是 “GUI”;原因是一行、最多 200 個字元。
每台機器一個 GUI,由 sb server 提供
Section titled “每台機器一個 GUI,由 sb server 提供”GUI 執行在 sb server(sb server)中,而不是在 pi session 裡:server 在登記好它的 intercom id 之後,綁定它的兩個 loopback listener(本機與遠端),而在 Wi-Fi 存取開啟時,每個被提供的私有位址再各多一個 listener(Wi-Fi 存取),而 /boss gui、/boss gui stop、/boss gui remote、/boss stash 與 /boss close all 都透過控制 socket 請 server 處理。pi session 不承載任何東西、不接管任何東西、不取代任何東西,也沒有對話框 hub,所以頁面不依賴哪個 pi 還活著。server 寫入 gui.json,並把同樣的 port 與 token 記錄在 server.json(gui: { port, token, stopped })。token 比 listener 活得更久(D27)。 老闆決定在重啟之間保留 GUI token、CSRF 機密與已配對的裝置。
細節
老闆選擇讓 GUI token、CSRF secret 和已配對的裝置在重啟之後仍然保留。token 與 CSRF secret 放在 gui-secrets.json(模式 600,以 rename 寫入,和 paired.json 一起在僅限擁有者的資料夾;在 Windows 上由狀態資料夾的 ACL 涵蓋;ACL 回讀只在主要 slot 的資料夾列出它,次要 slot 的副本繼承資料夾的保護而不回讀),連同它們綁定的那一組 port。
server 的重啟或升級、崩潰或不正常結束後啟動的 server、重新開機,以及 /boss gui stop 之後的啟動,都會再次綁定記錄的 port(舊 server 仍在釋放的 port 最多等 5 秒),並保留 token、CSRF 機密與 paired.json 中的裝置:開著的頁面與 desk shell 會自己重新連線,手機帶 token 的起始 URL 與已安裝 app 的固定入口繼續運作,而遠端存取在曾經被想要開啟的情況下,會隨著 serve 項目指向新的遠端 listener 而恢復。兩份記錄都在 restarting 被清除之前帶有 token。
新的 token 會讓每台裝置解除配對(log 會寫 devices-cleared;身分與意願會保留),發生在三種情況:gui-secrets.json 遺失、損毀或過期(一行 log,newToken=true why=missing、corrupt 或 stale;從 0.47 升級後的第一次啟動沒有它;由另一個帳號擁有的連結或檔案取代它的位置算 corrupt;stale 是 token 與記錄所指的不同的儲存檔,在忽略儲存檔的版本在這段期間用這些 port 執行時留下:降版,或 0.45 host)、listener 無法取回它的兩個 port 而移動了(why=moved;遠端 port 也算),以及 /boss gui rotate(確認之後;同樣的 port 換新 token 與 CSRF 機密,每支手機都解除配對,已停止的 GUI 被啟動,並開啟新位址;why=rotate)。
接著舊 token 與 CSRF 機密會得到 403,開著的頁面會說這個位址在此無法使用(在舊位址載入頁面會得到同樣的說明,是一個簡短的 HTML 403,當 Accept-Language 要求時會是繁體中文),而手機要掃新的 QR code(已安裝的 app 則輸入配對代碼)。老闆接受的風險: 另一個帳號若在兩個 server 之間的空檔綁定了 port,就會收到開著的頁面或 app 送出的東西(URL 中的 token、請求中的 CSRF 機密),而且在它釋放 port 之後這些機密仍然有效。
在遠端 port 上,空檔更寬:正常離開(以及嚴重的 JavaScript 錯誤)會把 serve 項目拿掉,但 SIGKILL、記憶體不足被終止或斷電,會讓它仍指向已死的 port,所以佔住那裡的人會收到已配對手機的請求,路徑中帶有 token。它因此得到的比那支手機更多:有了 token,它就能在本機 port 上開啟這台電腦的頁面(本機 listener 根據 token 與行程自己設定的 Host 標頭來放行),並讀取 CSRF 機密,那就是本機頁面的完整權力(重啟、link 設定、spawn、輸入機密)。
把手機解除配對不會終結它;只有 /boss gui rotate 會,而「port 被佔用」規則只抓得到下一個 server 啟動時仍佔著 port 的佔用者,抓不到已經放手的。/boss gui rotate 就是面對你不信任的空檔時的答案。升級後的第一個 server 可能發現 0.45 或更舊的 pi session 仍在承載 GUI:它請那個 session 交接,這是新版一直都能做的(server/stop 搭配 take;它的 serve 項目會隨之移除),等待最多 5 秒直到沒有東西接受它的 port 上的連線(等待期間不會對該 port 送任何東西),並在取回時,用自己的 token(舊 host 從未儲存過)綁定同一個 port(只有在 gui.json 指名的行程仍在執行、而且它的啟動時間已知並與 gui.json 記錄的一致時:記錄的行程已不在的 port 不會收到任何東西,行程啟動時間尚未知或沒有記錄的也不會,因為在 Windows 上崩潰 host 的 pid 常是另一個行程的,它可能佔著那個 port。
啟動時間最多等 6 秒;等不到時,交接會被略過並記錄為 gui-reclaim-skipped,server 的 GUI 移到另一個 port),所以遠端存取會恢復在 server 的 listener 上(舊 host 提供的頁面停止運作:它的 token 屬於舊 listener)。server 本身絕不被接管:對它要求的接管會得到 409,舊的 session 接著把 GUI 回報為過期並放著不管(只有該版本單純的 /boss gui stop 會被接受,當成刻意的停止)。
/boss gui 是老闆自己的啟動:沒有 server 在執行時啟動 sb server,啟動刻意停止的 GUI,開啟瀏覽器,並印出 URL。/boss gui stop 只停止 GUI listener(gui.json 中的 stopped 與 server.json 中的 gui.stopped);server 繼續執行它的其他服務,而重啟會保留這個停止,老闆自己的 /boss server restart 也一樣(它只清除 server 自己的刻意停止,/boss server 與 cli.mjs serve 也是如此;只有 /boss gui 與 cli.mjs up 的 gui 步驟會啟動刻意停止的 GUI)。
沒有任何自動請求會啟動刻意停止的 GUI:/boss stash 與 /boss close all 會說它被停止,並留給 /boss gui 或 cli.mjs up 處理。當遠端存取開啟時,remote.json 記錄 serve 項目的目標、server 的 PID,以及該行程何時啟動(paired.json 比 server 活得更久:遠端是否想要開啟、身分,以及裝置,這些只會隨新的 GUI token 消失),所以下一個 server 會移除崩潰的那個留下的項目:只有當記錄的 PID 已不在,或屬於在另一個時間啟動的行程時,記錄的項目才算過期,所以真正在執行的 server 會保住它的項目。
如果遠端 port 被佔用(兩個 server,或另一個行程),server 會取得另一個空閒的 port,而 remote.json 與下一筆 serve 項目就會寫它。
gui.json 是一份契約
Section titled “gui.json 是一份契約”除了 session 之外,顯示 GUI 的程式(straw-boss-desk shell)會讀取 state 資料夾 $PI_CODING_AGENT_DIR/straw-boss/ 中的 gui.json(沒有設定該變數的 pi 使用 ~/.pi/agent/straw-boss/)。它可以讀取 port、token、pid、started、stopped、version、herdr、owner 與 view;csrf 與其餘欄位是給 session 用的(trust: 1 告訴 pi 由 server 讀取它的專案信任詢問,專案信任)。
細節
自從 GUI 移進 sb server 之後,pid 與 started 指的是 sb server,也就是提供 GUI 的行程,owner 是 sb server (pid N),ownerSession 是 server 的 intercom id,view(選填,standard、geek 或 zen,老闆沒設定預設值時不存在)是 GUI 的預設檢視(/boss gui view;該值改變時會重寫記錄,GUI 停止期間也一樣,不認識該欄位的 shell 會忽略它),而 stopped 表示 GUI 被刻意停止(/boss gui stop)而 server 持續執行;整個 server 被刻意停止的情況則是 server.json 的 stopped。
這個檔案含有 token,所以它是機密:絕不要寫進 log。URL 是 http://127.0.0.1:<port>/<token>/,結尾帶斜線。就緒:記錄存在、不是 stopped,而且對 URL 的 GET 回應 200。已移動:重啟、升級、崩潰或不正常結束後的啟動、/boss gui stop 之後的啟動,以及重新開機,都會用同樣的 token 與 CSRF 機密以及同樣的 port 重寫記錄(D27:token 與它所屬的那一對 port 一起保存在 gui-secrets.json)。
新的 token 會伴隨被佔用的 port(遠端 port 也算:被佔用的那個會把 listener 一併移到新 token)、遺失、損毀或過期的 gui-secrets.json、從 0.45 host 的交接(它從未儲存過,並讓在它之前的儲存檔過期),以及老闆的 /boss gui rotate 而來。舊 token 與 CSRF 機密此後會得到 403,而且在讀取者可以把 server 當成運作中之前,兩份記錄都帶有新 token。監看該資料夾(或每 2 秒讀一次檔案),並在新 URL 回應之後,於 port 或 token 改變時重新載入;載入舊 URL 的 shell 沒有需要保留的頁面。
消失已不再依賴 pi session:沒有停止就掛掉的 server 會被下一個 pi session 的 look(任何 pi 在執行時約 6 秒)、它的 supervisor,或 shell 在啟動時執行的 cli.mjs up 重新啟動;各種狀態,以及 shell 在每種狀態下的做法,見給 shell 的狀態。每次寫入都是寫到同一資料夾的暫存檔(mode 600、以隨機名稱新建)再 rename 到位,所以讀取者永遠不會看到寫到一半的檔案。
server 重啟期間,gui.json 仍指名它,但沒有 started:舊版本只在記錄有 started 時才接管 GUI,所以 0.45 或更舊的 pi session 都不會在重啟中途接管它,而下一個 server 會再寫入 started。這份記錄沒有 schema 版本:請釘選 pi-straw-boss 版本,這些欄位的改動只會隨新的釘選到達你。server.json(在它旁邊)描述整個堆疊,是 shell 要移往的後繼者:它可以讀取 pid、started、version、herdr、mode、desk、stopped、restarting、lastError、log,以及 gui: { port, token, stopped };socket、secret、intercom 與 intercomId 只給 session 使用。
gui.json 會留在磁碟上,沒有在發布說明中註明就絕不會被移除。環境中帶有 STRAW_BOSS_DESK=1 啟動的 session(desk shell 自己顯示頁面)會為 /boss gui 印出 URL,且不開啟瀏覽器。
頁面與 session 中的 Desk 模式
Section titled “頁面與 session 中的 Desk 模式”頁面與 session 中的 Desk 模式(STRAW_BOSS_DESK=1;desk shell 帶著它啟動自己的 Herdr server,所以底下的一切都有它,而 server.json 記錄 desk)。帶著這個開關執行的 server,會在它提供的頁面中,把 <meta name="sb-desk" content="1"> 放在 sb-csrf 旁邊,而頁面在載入時讀取一次它,就省略 GUI 遠端存取(設定頁的手機與裝置區塊)與收起(每個基地的收起按鈕、HUD 的已收起按鈕與它的清單);這在每次重啟之後仍成立,因為每個 server 都從自己的環境讀取該開關(up 會重啟沒帶它執行的 server)。
細節
隱藏只是呈現層:路由仍在,而 URL 不論如何都是憑證。desk stack 中的 session 會為 /boss gui 印出 URL 並且不開啟瀏覽器,而且它的配對覆蓋層沒有 o 鍵。當 shell 的 preload 暴露 globalThis.strawBossDesk.pickFolder({ defaultPath, title })(回答資料夾路徑或 null)時,資料夾選擇器(+新基地、+成員、+主智慧體,以及 Straw Link 的資料夾步驟)會顯示一個選擇… 按鈕:選到的路徑會經過與點擊麵包屑相同的 POST api/dirs,所以 server 仍是它的裁判(只限家目錄、沒有任何隱藏項目,拒絕會顯示在選擇器的備註中),而 null 不改變任何東西。瀏覽器沒有這樣的 hook,所以沒有按鈕。
每個 session,不論是否為 desk,在啟動 20 秒後都會檢查 Herdr 的 pi 整合是否已為它的 agent 資料夾就位:當 Herdr 回報它的 pane 中有智慧體但沒有 agent_session 時,它會警告一次:“Herdr’s pi integration is missing for this agent directory, so this session cannot join the net; run herdr integration install pi and restart pi.”(src/integration.ts;列表失敗則什麼也不說)。
頁面在 45 秒沒有訊息之後,把串流視為已死,並以從 1 秒加倍到 30 秒(±20 %)的退避重新連線,在 8 次失敗後放棄,直到分頁再次被使用。回來時(可見、取得焦點、pageshow、online),若串流已關閉或已安靜 20 秒,就立刻重新連線,並取得全新的完整快照,而不是重播錯過的內容。斷線期間,地圖會變暗,橫幅會說明並附上最後一次更新的時間。
細節
一個強制回應的覆蓋層(gui/connect.ts)從第一個位元組起到串流的第一份快照為止,以及每當串流中斷時,都會蓋住頁面:「連線中…」或「重新連線中…」,在 8 次失敗之後則是放棄的說明,附上「重試」。app 根節點是 inert,沒有任何點擊或按鍵能到達它後面的頁面。隱藏的分頁會暫停它的動畫,回來時只繪製最新的快照,而且除了卡住偵測之外沒有任何計時器。
QR 編碼器
Section titled “QR 編碼器”QR 編碼器是套件自己的(src/qr.ts),部分改編自 Project Nayuki 的 QR Code generator library(MIT,見 NOTICE.md);仍然沒有任何執行期相依套件。
一個沒人動的頁面會以影格消耗手機的電池,所以那些永不停止的動畫(旋轉的環、等待單位的 ping、工作徽章的轉圈、連線點的閃爍、地圖連線上的封包)會在 30 秒沒有輸入後停止,並以同一狀態的靜止標示取而代之(凍結的環或轉圈、等待單位的淡淡光暈);任何觸碰、按鍵、滑鼠移動或捲動都會讓它們回來。連線的封包只在它兩端的塔台之一正在工作時才行進,而手機上位於全螢幕通訊畫面底下的檔案不會被繪製。在 prefers-reduced-motion 下,頁面沒有這類動態,這些都不會執行。
檢視、主題、手機與設定
Section titled “檢視、主題、手機與設定”手機版面(未發行;螢幕寬度最多 640 px,桌面不變)。紀錄佔滿螢幕:在主智慧體有三則 steer 排隊或塔台正在等老闆時,紀錄至少佔 390 × 844 視窗的 55%(原本是 29% 和 33%),而第一個基地從地圖頂端 160 px 以內開始。
細節
地圖沒有位置列,也沒有單位的關掉:單位的關掉只在它的通訊畫面標頭中;基地和小隊的關掉(同一個按鈕,同樣的確認和拒絕)在它標頭的 ⋯ 後面,而標頭自己的 ⋯ 在存在時容納移除已完成和已收起。塔台的通訊畫面標頭是一列(返回、名稱、資料、⋯),+主智慧體和關掉塔台在它的 ⋯ 後面,位於通訊畫面|終端機的切換之上。老闆的對話是一行,附一個 ⋯,容納離開、新建、該行截掉的資訊,以及事件 run的標籤(只有事件 run時,它寫成「Event runs · n」)。
排隊中的訊息在有兩則以上時收合成一列「n queued」;點按會開啟清單,並可編輯所有 steer。助理的訊息沒有頭像欄,小隊的單位是底板在名稱旁邊的列,而手機的左側窗格寫出塔台和它的狀態。⋯ 是一個 44 px 的按鈕,開啟 popover(Escape 或點擊外面會關閉它,焦點回到按鈕;方向鍵、Home 和 End 在它的控制項之間移動;有作用的控制項會關閉它,離開和新建則為了它們的結果而保持開著),而在 640 px 以上,這個按鈕不存在,它的控制項回到它們一直所在的列,方向鍵也再次屬於頁面(gui/menu.ts)。
已完成子智慧體的移除佔它自己在文字下方的一列,所以它的狀態和註記保有卡片的寬度。問題的卡片作為一個整體捲動,在下面還有內容時,送出列上方有「More below」提示和漸層。在 360 px 的螢幕上,老闆對話的那一行可以佔兩行(顯示它結束的時間),輸入框是一列(+、框、送出),直到框被點按或有文字;有排隊 steer 的主智慧體和正在等待的塔台,紀錄至少佔 360 × 740 的 55%。開著的對話 ⋯ 在頁面重繪時保持開著,並帶著離開或新建的結果。
檢視:自動、標準、極客、禪、鴉閣
Section titled “檢視:自動、標準、極客、禪、鴉閣”檢視:自動、標準、極客、禪、鴉閣(每台裝置各自設定,預設值來自 server)。設定頁的「這台裝置」群組有一個五個選項的控制項(自動、標準、極客、禪、鴉閣;鴉閣是下一筆項目的同系,在禪之後說明;禪在下一筆項目中說明,它的設計是規格 docs/specs/2026-11-01-zen-view.md),保存在這個瀏覽器的 localStorage(sb.view);gui/view.ts 最先讀取它,並在任何東西繪製之前,於 <html> 上設定 data-view(以及瀏覽器的 theme-color)。標準就是上面的色盤。
細節
極客是一個終端機:黑色表面、一種 13 px 的等寬字型、每種文字顏色在每個表面上至少 7 : 1,每條邊框至少 3 : 1(tests/geek-contrast.test.ts 對樣式表的 [data-view="geek"] 區塊做這個運算,包括色調),沒有動態、光暈、漸層或按下凹陷(「動態會休息」的靜止標示一律開啟),四邊都是一條單純的邊框,而每個狀態也以單位名稱前的文字標籤呈現([WORK]、[WAIT?]、[CMD◈]、[IDLE]、[DONE✓]、[ERR!]、[UNKN]),連線用文字表示,提醒、音效與背景音的開關顯示為 [On] 與 [Off](標準在圓點旁顯示同樣的字),而被選取的東西(分頁、模式開關、選取的資料夾)以反白顯示。
自動,以及遺失或無效的儲存值,在老闆設定了預設值時跟隨 server 的預設值(見下),否則是標準,而在系統要求更高對比時是極客(prefers-contrast: more,即時跟隨);沒有淺色版本。server 的預設值只能從 pi 設定:/boss gui view geek(或 standard、zen、raven,或會清除它的 auto;單獨的 /boss gui view 會告訴你目前的值)透過控制 socket(gui-view)請 sb server 處理,設定它是老闆自己的指令,所以它會啟動沒有在執行的 server,但不會啟動被停止的 GUI,而告知只是讀取:它什麼也不啟動,所以被刻意停止的 server 會維持停止),server 把它保存在 gui.json 旁的 gui-prefs.json({"v":1,"view":"geek"},mode 600、以 rename 寫入;遺失、損毀或屬於他人的檔案,或這個版本不認識的值,都是 auto,寫入失敗會被告知且不改變任何東西;明確設定記憶體中已有的值仍會重寫沒有寫著它的檔案,所以 /boss gui view auto 會取代這個版本讀不懂的值,而啟動 server 不會重寫任何東西)。
退回比禪更舊的版本,之後再升級:舊的 server 把 zen(與 raven)讀成 auto 並提供自動,但它不動這個檔案,直到被告知不同的值,所以升級會把 zen 帶回來,除非老闆在舊版本上設定了另一個預設值(在那裡明確設定自動對它是 no-op,因為記憶體中的值已經是 auto)。頁面絕不寫入它,而已配對的裝置(Tailscale 或 Wi-Fi)也沒有任何路由能寫入:唯一的入口是控制 socket。
server 會把它寫進它提供的每個頁面,作為 <meta name="sb-view" content="auto|standard|geek|zen|raven">,所以跟隨 server 的裝置在第一個影格就畫出預設值(沒有閃爍,在 desk 視窗或已安裝的 app 中也不必設定任何東西),把 view 放進每條串流的 hello,並在它改變時對開著的串流送出 view 事件,所以開著的頁面立刻切換而不需重新載入(錯過事件的串流會在它重新連線的 hello 中得知)。處於自動的裝置跟隨它,在設定頁標示為跟隨伺服器(極客);選了標準或極客的裝置保留它的選擇,而預設值絕不存放在裝置上。
當預設值是 auto 時,每台裝置都回到它自己螢幕的對比偏好。gui.json 把預設值當成選填的 view 帶著(standard、geek、zen 或 raven;auto 時不存在),給在頁面載入之前就繪製視窗的 shell 使用。在 forced-colors 下,系統的顏色接手,而狀態形狀會得到外框;QR code 與終端機保留它們自己的顏色。calm.ts 在極客中不執行任何計時器。
鴉閣(Raven Keep)
Section titled “鴉閣(Raven Keep)”鴉閣(Raven Keep)是標準檢視的哥德式外觀,版面與互動都相同(只在頁面顯示它時才載入)。只有深色:帶冷紫調的近黑底色、骨白文字、等待用燭金、失敗用血紅、休息用霧灰,每種文字顏色的對比至少 4.5 : 1。
細節
它帶來自己的十個角色圖示(每個圖示都透過 iconRef;標準的保留作為後備)、自己的英文與繁體中文角色名稱(主塔台是 Raven Keep / 鴉閣,老闆是 the Count / 伯爵,塔台是 Steward(管家),主智慧體是 Spawn(血僕),成員是 Wolf,子智慧體是 Bat,前哨是 Coffin chamber,未編列的單位是 Ghost,通訊是 Ravens(中文為通信)),以及在繁體中文裡隨名稱一起切換的一組句子(每則說到塔台的訊息改說管家,說到主塔台的改說鴉閣,沒有塔台的基地改說自主活動;英文句子沿用基本用詞)。它還有一個給分頁圖示用的紋章,以及三個取代標準片段的聲音,仍在同一個音效開關和音量之後:問題是一聲鐘響(鐘是在要求答案)、完成是烏鴉叫、失敗是門的嘎吱聲(bell.mp3、caw.mp3、door.mp3,單聲道,第一次播放時才取得)。
主塔台頁面與地圖在它們後面畫一幅插圖(霧上的城堡與地窖門),只用 CSS 做動態:飄動的霧、亮著的窗與燈籠、蝙蝠、一隻烏鴉,以及遠方的閃電,最多每 61 秒一次(第一次在圖片顯示後 7 秒:天空中一道分岔的閃電,以不到 0.3 秒的雙脈衝出現,接著圖片亮一秒,然後是雷聲);減少動態時既不畫閃電也不畫光暈。地圖的面板與圖例在圖片上疊 0.66 的黑,所以文字在最亮的閃電畫格仍保持 4.5 : 1;聊天、工作區與設定頁維持素面。單位的聊天頁面依單位的角色在通訊畫面後畫出城堡的一個房間(塔台是書房,主智慧體是大廳,子智慧體是地窖,其他是圖書室),罩著一層深色紗,燭光以透明度呼吸。
標準會掃雷達、轉環與 ping,城堡則在同一座塔、名牌和徽章上畫出別的圖形,而且沒有東西旋轉:工作中的單位得到一個印記,一個環從底部兩側向上畫到頂端的菱形,然後淡去(4.2 秒),盤下有兩層呼吸的霧,小徽章上有一隻拍翅的蝙蝠(0.9 秒);等待中的單位得到在盤內脹縮的魂火(3.2 秒),上方是呼吸的點狀環,它的小徽章上下浮動(2.4 秒)。兩者不只在色調上不同,形狀也不同。每個圖形只移動或淡化一張固定的圖(只用 transform 和 opacity),所以不論多少單位在工作,一個畫格都不需要重繪任何東西。這一切與天氣,在 prefers-reduced-motion 下、頁面靜止後(data-calm),以及強制色彩下都會停止,每種狀態此時保留一個靜止的圖形:帶菱形的實心印記,或帶魂火的點狀環。連線覆蓋層的環同樣是帶魂火的點狀環。在細緻指標上,靜止不會停掉閃電、烏鴉的光與轉頭,、雷聲與雨。
手機(600 px 以下)把圖片畫成覆蓋螢幕高度、靠左固定讓城堡露出,沒有蝙蝠、飛行者和主塔台頁面上棲息的烏鴉;瀏覽器支援 backdrop-filter 時,大型面板(基地、通訊畫面、檔案與時間軸、設定群組、面板)是蓋在圖上的毛玻璃(#0a080e 的 72%、8 px 模糊,面板為 88%;主塔台頁面的通訊畫面較薄,2 px 模糊下 18%,所以城堡能在後面讀出來),文字在閃電高峰時圖片最亮處仍保持 4.5 : 1。
每個檢視都把 pi-better-show 的 --pbs-* token 對應到它自己的色盤(tests/pbs-tokens.test.ts),所以文件或聊天項目中的 pbs 元素跟隨檢視。/boss gui view raven 和其他的一樣設定 server 的預設值;當預設值移到 raven 時,顯示禪的頁面維持禪,而帶 prefers-contrast: more 的自動仍是極客。
stateDiagram-v2
[*] --> rest
rest: 靜止(呼吸圓圈)
rest --> card: Enter,有項目在等
card --> answered: 回答或稍後再說
answered --> card: 下一個項目
answered --> rest: 沒有了
禪一次只顯示一件事。項目就是在等你的事,例如單位的問題、信任問題或塔台的等待,會以一張卡片出現。禪靜止時是一個呼吸圓圈,沒有卡片會自己佔住畫面:它們等你按 Enter。你讀著的卡片絕不會被換掉或抽走。
細節
禪一次只顯示一件事(gui/zen.ts,一個只在檢視顯示時才載入的 chunk;選擇存放在純模型 gui/zenmodel.ts)。項目就是在等你的東西:等著回答的單位、待處理的對話框、專案信任詢問,或塔台對你的等待,依它們開始等待的順序排列(會卡住智慧體的問題,例如對話框、信任詢問、盒子轉送的問題或隊友的確認,排在塔台的等待、配對請求或記憶提案之前;沒有任何卡片會在任何狀態下自己占滿螢幕,而是等待 Enter,卡住的優先,而螢幕上的卡片絕不會被取代或抽走;標準的順序不變),所以 HUD 的警示清單是同一組。
另外四種問題,標準把它們顯示在 Link 按鈕、Wi-Fi 通知與塔台的通知中,在禪中各是一張卡片(gui/cardalerts.ts、gui/zendecide.ts):隊友的塔台向這台電腦提出的確認(允許或拒絕,下面附工具的完整參數)、Straw Link 盒子轉送的對話框(共用的問題面板)、配對請求(手機顯示的四位數字,允許或拒絕),以及塔台 run的記憶提案(確認或駁回)。
每一種都透過與標準那一列相同的函式回答(gui/asks.ts),由 server 的清單決定卡片何時結束(它不再列出的卡片會留在螢幕上,附上已經在別處回好了,或在它的單位離開網或關閉時附上這個單位已離開,它的回答關閉,直到你按 Enter、Esc 或它的按鈕關閉它;卡片的敲擊聲會響一次,之後的拒絕或失敗仍會響低沉的缽聲,而在轉送之前就被取走、之後才得到對話框的卡片會變成可回答,而不是結束),而已配對的裝置保有它在標準中擁有的權限:它不會得到配對請求卡片,因為 server 對它回答 local_only。加入請求留在標準。
頁面只顯示目前的項目,連同它的問題卡片(標準對話框的卡片,以同樣方式回答,包括需要終端機的回答),絕不顯示數量,也不暗示還有更多在排隊。有多個問題的對話框放在同一欄中(gui/zenquestions.ts):一個問題被聚焦,展開並維持完整強度,其他的各折成一行淡化的線(標題或問題,一旦有答案就在一個點之後附上答案),其控制項不繪製,所以只有點按、Enter 或 Tab 在該行上才會被聽見,並把那個問題帶進來。
一次滾輪手勢或一次滑動移動一個問題(聚焦問題自己的捲動優先,甩動的慣性不會移動第二個),指標選取單一選項、Tab 與 Enter,以及點按某一行也是如此;比空間更高的問題會在自己裡面捲動,並且總是從頂端開啟。送出、取消、結果與晚點再說無論視窗多大都留在頁面上,是問題區域讓出空間。
當它因你自己的回答或取消而離開螢幕時,回答被接受時已經在等待的下一張卡片,會在 1.2 秒的結果之後接著出現,帶著缽聲而沒有停頓(回答之後才到的卡片等 Enter;在最初的 400 毫秒內,那張卡片會忽略重複的或更早的 Enter,以及任何 Enter、點按或 Esc,所以回答用的那個按鍵不能按下它;隱藏的頁面上、沒有焦點的頁面上、終端機層、調色盤或選單底下,或在回答之後才開始的禪訪視中,都不會連鎖:每次訪視都從呼吸開始)。
最後一個回答、晚點再說(讓卡片休息 10 分鐘或直到它的問題改變),以及問題已在別處結束的卡片被關閉(點按可略過它)之後,會有 3 秒的呼吸停頓,而下一個項目等待 Enter。Enter 按下你取走的那張卡片的主要按鈕(絕不在文字欄位中),而終端機層的確認也接受 Enter,不過在它顯示時你輸入的按鍵會把焦點移到取消而不是確認,而它顯示後 400 毫秒內的 Enter(雙擊 Enter 的第二下)什麼也不確認。
禪中沒有任何東西會從你手上奪走螢幕:從卡片開啟的終端機會留著,直到你回去;結束或失去控制的作伴 session 會帶著一則安靜的說明(已結束、已失去控制)留著,它的輸入框唯讀、送出關閉,直到你離開它;server 預設檢視的改變會在禪頁面的下一次載入時套用,而不是立刻(而它開啟設定頁所留住的頁面,離開設定頁時會再顯示禪);頁面的新版本(較新的 pi-straw-boss 接管了 server)也不會在你手上載入:當卡片、終端機層、作伴 session、主塔台的對話或展開的輸入、調色盤或選單開著時,一行安靜的文字會說新版本已就緒,而頁面會在禪回到呼吸的圓、沒有任何東西開著時(每秒檢查一次)重新載入一次,或在頁面隱藏、或離開禪時立刻重新載入;標準與極客如先前那樣立刻重新載入。
沒有項目時,就是一個呼吸的圓與一行有出處的正念句子(gui/zenlines.ts):每次訪視都從那裡開始,訪視之間不記得任何東西。[ 與 ] 逐一切換到一位同伴,一個有即時記錄與輸入框的作用中 session,經由核心的候選清單,清單在尾端繞回到圓,而調色盤的回到呼吸讓訪視的其餘時間回到圓;沒有 session,包括塔台,能取代它。
選單與音樂清單在一層會接住關閉它們的按壓的紗幕上開啟,所以那一下絕不會到達它底下的卡片。Esc 是一張表(gui/zenkeys.ts):角落選單先關閉,接著有文字的欄位只失去焦點,接著按下項目的取消(無法取消時則是晚點再說),接著當主塔台的條帶的行都已清除時讓它休息,否則第一次 Esc 顯示提示,600 毫秒內的第二次 Esc 回到標準;角落選單的最後一列則立刻做同樣的事,而離開絕不改變裝置的選擇。角落有一個全螢幕按鈕與一個選單(語言、檢視、聲音、音量、回到標準)。
選單只由按壓開啟(點擊、點按、Enter 或空白鍵),絕不由 hover,並由 Esc、在外面按壓,或選擇語言、檢視或回去的路徑而關閉。卡片立刻取得內容所需的大小;它所容納內容的替換(新的對話框、結果行)以 0.3 秒淡入,而沒有任何東西緩動卡片的高度或寬度。色盤只有深色墨色。
聲音有兩件事,都放在頁面自己的開關之後。音效以 Web Audio 合成(gui/zensound.ts,沒有音訊檔):項目占據螢幕時是一聲頌缽,完成時是一聲木魚敲擊,出錯時是一聲較低的缽;它們是音效:只在設定頁的音效開關開啟時才播放(預設關閉),音量是其音量的平方,每次播放時讀取,而且只在頁面的第一次手勢之後才響(瀏覽器讓音訊從暫停開始;在那之前到期的音效會被丟棄,絕不排隊)。
角落選單的試聽音效會播放它們。標準與極客保有自己的片段,而禪絕不播放那些。音樂是你的:agent 資料夾中 straw-boss-music/ 的音訊檔(mp3、m4a、aac、ogg、oga、opus、wav、flac)(~/.pi/agent/straw-boss-music/,與 state 資料夾並列而不在其中);GUI 的 server 在 GET api/music 列出它們,並以 range 請求串流(src/music.ts),放在與每個其他 api 讀取相同的 token 與 cookie 之後,而套件不附帶任何東西。每次禪顯示時都會讀取該資料夾;沒有檔案時,角落沒有 ♪。
音樂是背景音:角落的 ♪ 選擇禪是否播放它(預設關閉,存放在 sb.zen.music),而它只在設定頁(或角落選單)的背景音開關開啟時才發聲,音量是該開關的音量;背景音關閉時,♪ 保留位置並寫著音樂:開啟,但背景音已關閉,不取得任何東西,而正在播放的曲目立刻停止。
♪ 與點按喚醒音樂的頁面是瀏覽器要求的手勢,所以從角落選單開啟背景音會立刻開始一首曲目,而從另一個分頁開啟則等待一次點按。它旁邊的 ⌄ 在有兩首以上時選擇曲目(sb.zen.track)。播放器(gui/zenmusic.ts,自己的 chunk,只在音樂開啟後才取得)使用兩個 audio 元素,並在最後 6 秒把曲目與它自己交叉淡出,所以迴圈沒有空隙;音效響起時會把它壓低 6 dB。
播放從手勢開始:已儲存為開啟的開關會等第一次點按並寫著點按以播放,被鎖定的手機或拒絕的瀏覽器所停止的也一樣。在觸控螢幕上,頁面隱藏時音樂暫停;在桌面上它持續播放。離開禪會停止它。iPhone 的鈴聲開關會把它靜音,如同任何網頁音訊。由回答開啟的終端機(robot_hand 的Put it in my prompt或新的 Herdr pane,或被擋下的提示的開啟終端機)在禪中顯示為全螢幕層,角落有回到禪(上面的「需要終端機的回答會開啟它」一段)。
示範以 ?zen=item|questions|secret|words|trust|idle|alone|terminal|blocking 開啟一個狀態(gui/fixture.ts;blocking 是塔台的等待,一個盒子的問題在 &box=<ms> 之後才到(&box=<ms> 可用於任何狀態),settled 是 item,它的問題在 &settle=<ms> 後於別處被回答;terminal 是 item,附一個在顯示 20 秒後自己關閉的 pane;四個 link 狀態是 gui/demo-link.ts,&refuse 讓 server 拒絕回答,而配對數字是 4821)。
禪中的按鍵:[ 與 ] 在你能交談的 session 之間逐一切換同伴(閒置的與塔台優先;也可以用 Option 或 AltGr 輸入,絕不在文字框內),而 ⌘K(非 Mac 上是 Ctrl+K,文字框中也可用)開啟命令調色盤:頁面的動作(同伴的 session 或終端機、設定、回到標準)與角落選單的偏好(檢視、語言、聲音、音量)。
頂端一條安靜的帶子說明有多少單位在工作、多少閒置;它絕不計算在等待的。H 在卡片上開啟 Context(脈絡):卡片內的單位 session 記錄,唯讀,只在第一次開啟時取得,同時仍可回答;H 或 Esc 關閉它(它是 Esc 表的第二步,在角落選單之後)。Shift+Esc 讓鍵盤離開禪中的終端機,否則終端機會接管每個 Esc(pi 會因它而中斷);Enter 或空白鍵提早結束呼吸停頓,點按也一樣。指標留下淡淡的漣漪,而一切會動的東西在 prefers-reduced-motion 下停止,漂移的底色在強制色彩下停止;與標準不同,禪不論頁面休息多久都持續呼吸(圓、同伴記錄後面淡淡的圓,以及以半分鐘為潮汐漂移的底色)。
同伴的記錄與輸入框沒有方框:訊息、工具呼叫、程式碼與表格靠空白與較安靜的灰燼色區隔,而輸入框是一行。禪的其餘部分遵循同一條規則(規格的視覺語言):選項是一行帶圓點的文字,選取時填滿,hover 或按鍵聚焦時變亮;選單、音樂清單與調色盤浮在模糊的紗幕上,沒有面板;你說的話以灰燼色靠右;朱砂印只標記鍵盤所在,作為圓點的環或一個字下的一條線,絕不是方框外的環。
在手機上,卡片、停頓與角落保有 44 px 的目標,選單與音樂清單開在頁面上方,而角落的全螢幕按鈕,在瀏覽器沒有 Fullscreen API 的地方讓位給一次性的「加到主畫面」提示。任何狀態上的 &upgrade=<ms> 會在頁面啟動那麼久之後宣告新版本(每個瀏覽器 session 一次)。示範為了畫面開啟更多狀態:history(已開啟脈絡的項目)、palette(開著調色盤的同伴)、pause(被擱置的項目,所以停頓會顯示),以及 ?music=<n>(有 n 首虛構曲目的音樂資料夾;沒有音訊檔,所以開啟的曲目寫著點按以播放)。
標準與極客中的主塔台
Section titled “標準與極客中的主塔台”標準與極客中的主塔台(gui/headpage.ts;設計見 docs/specs/2026-11-04-head-tower.md)。路由 head(/head)是以鍵 sb-head 呈現的塔台通訊畫面:記錄中每個章節開始處有章節分隔線,輸入框(「叫醒主塔台」;/quit 與 /new 不需確認)、附有離開對話與開新對話的對話框、記憶面板、附有重試、重新送達與捨棄的通知,以及終端機模式。
細節
不同之處在標頭(通訊畫面標誌所在處是一頂金色皇冠、標題主塔台、狀態行)、對話框下方的廣播卡片(規格:主塔台轉送的每一項指示一張卡片,它收合的那行是帶標籤的部分:你說(老闆自己的話,由 server 從 relay 指名的事件保存)、主塔台送出(它的第一行,不含老闆的引用副本)與送給,接著是 11m ago · 2/3 handled;點按會展開完整的文字與每座塔台的狀態(answered、handled、received、waiting、no answer)、它最新的回答文字,以及通往它通訊畫面的連結;被主塔台指名為由較晚的一項指示所更正的指令,會帶著刪除線折疊在最新的卡片之下,附上 corrects N earlier / corrected by the next),以及對話框 ⋯ 選單中的關閉主塔台,它會詢問一次。在主塔台存在之前,頁面顯示輸入框與一行「第一則訊息會開啟主塔台」。
三種入口:每個路由上標頭的皇冠按鈕(當主塔台最新的回覆比這台裝置看過的更新時,帶一個小圓點,絕不是數字)、地圖的主塔台橫帶(規格;gui/headband.ts,由 gui/render.ts 中的 fillHeadNode 繪製:地圖裡的第一個東西,置中在基地網格上方,最寬一個網格欄寬、高 44 px,在版面流中所以隨地圖捲動;皇冠、主塔台、最新的一行,以及一個帶有主塔台等待老闆的數量的 chip,為 0 時隱藏,計算方式與主塔台頁面的列相同;整條橫帶是一個通往 /head 的連結,它的狀態放在無障礙名稱與 hover 中。
最新的一行是最新廣播的一行摘要(當它在最近一天內有活動時),否則是主塔台最新助理文字的第一行(最多 160 個字元),由 server 依主塔台記錄的篩選條件從對話檔的尾端讀取(HeadUnit.line);兩者都沒有時為空。當主塔台未開啟時,橫帶是淡淡的虛線,寫著未開啟,沒有那一行或 chip。在手機上它是至少 44 px 的一行;最新的一行以省略號截斷,當橫帶窄於 240 px 時捨棄,而皇冠、名稱與 chip 一律保留。
極客與鴉閣會為它換樣式;禪沒有地圖),它是地圖連線的樞紐,連線是虛線,且只有正交線:從橫帶底部中央往下一條線到網格上方空隙中的匯流排(橫帶下方約 32 px),匯流排沿著欄與欄之間的間隙,然後沿著每一欄左邊的間隙往下,再橫跨到每座塔台的中心,所以一欄讀起來就是一條主幹加分支,而且沒有任何線段穿過卡片。連線的封包在任一端工作時沿著同一條路徑行進,以及手機抽屜的第一列。沒有任何基地再有皇冠。
禪中的主塔台
Section titled “禪中的主塔台”在圓的畫面上(沒有項目、沒有停頓、沒有同伴),主塔台以一條帶子的形式位於呼吸的圓之下:它的行在老闆叫出它們之後顯示(高度容得下多少完整的行就顯示多少,最新的在腳邊、靠近輸入框;安靜時是三行,手機上是兩行;圓會先讓出空間,絕不低於 80 px),老闆的以灰燼色靠右,它的回覆以紙色靠左,而轉送(→ Harbor API tower)與回答各有一行簡短的註記。
細節
每一行從它第一次出現在螢幕上起清晰 20 秒,然後在三分鐘內淡到幾乎看不見並停在那裡;淡化是每一行一個 CSS 動畫,所以沒有計時器重繪。在帶子上 hover 或聚焦,或在手機上點按它(10 秒),會在 0.6 秒內把每一行緩和到清晰,並在指標離開 1.5 秒後回到它淡化所在的位置(淡化的時鐘從不停止:hover 只是把行抬起,gui/zen.css 中的 --lift);等待老闆的等待事項會讓它的那一行保持清晰。
正念句子只在每一行都淡去時顯示,所以同一時間只有一樣東西是紙色:行變清晰時它在 0.6 秒內淡出,再以它自己緩慢的步調淡入,而在休息時它與全部對話連結保有它們的空間,hover 或點按只會抬起那些行,絕不填滿頁面,所以指標底下沒有任何東西會移動(醒著的帶子的行會填滿空間,沒有正念句子)。連結在輸入提示的安靜灰燼色中指出它的按鍵,全部對話 · H,而觸控螢幕(沒有鍵盤)不顯示該按鍵。
在行的下方,輸入是一條細線,提示是「按 Enter 和主塔台說話」;在圓上點擊、點按或按 Enter 會展開它(Enter 送出,Shift+Enter 換行,Esc 收起它並保留草稿,拒絕時以陶土色顯示 server 的話並保留草稿)。H、全部對話連結、向上的滾輪手勢或向上滑動,會在淡淡的圓之上以同伴的形式開啟完整的歷史(捲動時載入較舊的頁面,轉送讀起來是安靜的一行,「已送給 Harbor API 塔台」),而 Esc、H、回到呼吸或向下的手勢會關閉它。
帶子預設是休息的:每次進入禪的訪視都從呼吸的圓、正念句子與收起的輸入開始,不顯示主塔台的任何一行。點按或點擊收起的輸入、對它聚焦、Enter 或 H(第一次 H 把行帶上來,第二次開啟全部對話)會把行叫出來,從那時起清晰 20 秒;Esc、點按或點擊空白處,或焦點離開帶子(失去焦點的空輸入)會讓它再度休息,而關閉全部對話則回到休息。休息中的帶子對主塔台自己的行(塔台的回答、它自己開始的回報)以及對你的等待或問題(那是一張卡片,算在下面的提示中)保持休息,只對你在這次訪視中送出的東西的回覆浮現;休息是頁面自己的,每次訪視都以它開始。
卡片等待 Enter:當禪停在呼吸的圓上休息時,不論種類(單位的對話框、信任詢問、盒子轉送的問題、隊友的確認、塔台的等待、配對請求、提案)或主塔台自己的等待,沒有卡片會自己占滿螢幕;輸入上方一行安靜的文字說明有多少張在等(「1 張卡等 Enter。不急,等你準備好。」),而 Enter 或點按那一行會取走下一張,卡住的問題優先。完成時,禪再度呼吸,下一張等待 Enter,只有一個例外:你自己被接受的回答之後,下一張卡片會立刻接著出現,沒有呼吸。螢幕上的卡片絕不會被打斷。當卡片在等待時,輸入的提示寫「點一下和主塔台說話」,因為 Enter 會先取走卡片。
絕不打斷,而且 Enter 就是行動:卡片在任何狀態下都不會自己占滿螢幕,包括作伴 session、主塔台的全部對話與終端機層;在那裡,提示放在名稱之下,或層保持原樣,而在等待期間就解決的卡片會離開計數,從未被繪製。當你閱讀時問題結束的卡片會留著,說明這件事,並由 Enter、Esc 或它的按鈕回到呼吸關閉;單位的下一個問題等待 Enter。
取走卡片時,焦點落在它的主要控制項(對話框的第一個選項、開啟終端機、信任、等待卡片的第一列、允許、確認、配對數字、終端機步驟的執行),而下一次 Enter 會按下它;Enter 絕不在文字欄位中觸發,而終端機按鍵板的確認(送出 Enter、送出 Escape、送出按鍵)在禪中只由 Enter 確認,絕不由按住的按鍵,或在確認顯示之前就開始的按鍵確認。沒有人在看的頁面(隱藏,或在前面但沒有焦點)會為每張卡片得到一則系統通知,只要 server 仍列出它就只通知一次。
帶子在每種狀態下都為連結保留一列、為提示保留一列,而頁面較矮時圓會讓出空間,所以卡片來去時沒有任何東西移動,而圓絕不會坐在工作中 · 閒置那一行後面。隊友基地的單位狀態回報與 Link 通知絕不是禪中的卡片。調色盤新增和主塔台說話、主塔台的全部對話與在標準檢視開啟主塔台。
減少動態時沒有淡化與緩動(清晰 20 秒,然後立刻休息;hover 立刻抬起各行);強制色彩顯示未淡化的行,能容納多少就顯示多少(醒著時最多 24 行),並帶有 CanvasText 邊框。你取走的項目先占據螢幕,而帶子隨圓一起離開。

主塔台的示範狀態
Section titled “主塔台的示範狀態”?head=absent|talk|answers|waiting|busy 讓主塔台上網(未開啟;一段對話;工作交給三座塔台,其中一座已回答;對老闆的等待;以及一個拒絕老闆所送內容的,用於帶子的拒絕),而 ?zen=head、?zen=head-open(輸入展開並有草稿)、?zen=head-history 與 ?zen=head-rest(頁面啟動後一會兒按 Esc,然後主塔台自己做出一份回報,休息的帶子不會顯示它)在禪中顯示帶子。
設定頁(尚未發布;路由 settings,gui/settings.ts,GUI 改版的第 5 步)。標頭的齒輪是通往它的連結,任何人都能複製的連結也是(settings,或用來開啟某個區塊的 settings/phone、settings/wifi、settings/teams、settings/server;server 對每一個都回應該頁面,與任何路由一樣放在 token 之後)。
細節
它取代了設定彈出視窗,以及原本開在地圖旁邊的手機、團隊與 server 圖塊,所以地圖保有它的欄。這台裝置放的是人設定一次、而且一直都在的東西:檢視(自動、標準、極客;server 的預設值仍是 pi 指令)、寫著開或關的有標籤按鈕——提醒與音效(提醒還會說允許?或已封鎖,並在下面附一行說明它的意思)、音效的音量與試聽,以及語言。手機與裝置(Tailscale 與「同一個 Wi-Fi」分頁、標題旁有「全部解除配對」的已配對裝置)、團隊與伺服器是關閉的區塊,打開時才填入,並在關閉或離開頁面時捨棄,所以關閉的區塊背後沒有任何東西在輪詢或產生:打開手機區塊才會要求配對代碼,如同手機按鈕原本那樣。
誰可以使用各區塊與先前相同:手機與伺服器只在這台電腦的頁面上(已配對裝置的頁面有「這台裝置」與團隊,而 server 仍對它回答 local_only),而 desk 視窗沒有手機區塊。標頭的等待請求通知會開啟 settings/wifi;Escape、頁面的返回箭頭與齒輪會回到開啟頁面前所在的位置(若它是第一個頁面則回到地圖)。在「同一個 Wi-Fi」分頁中,當 Wi-Fi 存取關閉時,完整的警告(風險、尚未驗證、熱點)留在開啟按鈕上方,該按鈕是實心按鈕;只有在 Wi-Fi 存取開啟之後,它才折疊到「詳細說明」之下。在手機上每個控制項都是 44 px(tests/phone-targets.test.ts)。
單位的圖示是它種類的
Section titled “單位的圖示是它種類的”協調者是舵輪,整合員是兩支箭頭匯合,值守是燈塔,火車是原本的火車頭,被派出的工作者、偵察、規劃、審查、監控者依序是扳手、望遠鏡、鉛筆加尺、放大鏡與獵犬。塔台(控制塔)與主塔台(皇冠)保留自己的,不知道種類的單位仍用位置的圖示(主智慧體、成員、子智慧體)。圖示由 unitIcon(role, duty)(gui/icons.ts)決定,畫在原本畫單位角色圖示的每個地方:地圖上的 token 與卡片、放大的基地、抽屜、檔案資料、警示的臉孔、聊天頭像,以及隊友卡上盒子主智慧體的那一列(用盒子送來的種類)。
細節
被派出的子智慧體的種類以 Unit.duty 傳到頁面,讀自它 session 檔前 64 KB 中第一個 straw-boss { agent } 條目(在它的第一個 turn 之前,由啟動腳本的 PI_SUBAGENT_AGENT 代替)。被續跑的子智慧體會從 pi-herdr-agents 拿到新的 id 和一支不帶角色的 <name>-resume-<ts>.sh 腳本,它靠帶有其 PI_SUBAGENT_ID 的那支腳本找到,再經由那支腳本的 session 檔與執行它的 pane,所以它保有自己的名稱、運作中的標記和種類。鴉閣對這些種類畫標準檢視的符號,如同對火車,極客也畫標準的。
圖示來自 Lorc 與 Delapouite 的 game-icons.net,CC BY 3.0(詳情見 gui/ICONS.md)。沒有任何東西從網路載入:沒有 CDN、字型或執行期相依套件。
