跳到內容

瀏覽器檢視

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

瀏覽器檢視

這一節分成六組:地圖、基地與單位;警示與提問;單位頁面顯示的內容;關閉、開啟與信任;提供頁面(遠端與 Wi-Fi 存取、server);以及檢視、主題、手機與設定。

任何 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)。

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))也會詢問。
  • 確認時,按鍵板用頁面自己的話說明原因,由伺服器的 confirm token 選出(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(頁面仍可使用,沒有東西困住焦點)。文件則改以並排區塊開在目前檢視旁,共用它的寬度;自行到達的區塊不取得焦點,狹窄的視窗把區塊顯示為分頁。對話框顯示單位、它的最後一行,以及它正在等待的工具呼叫,並附上通往它通訊畫面的連結。

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,顯示時截短),更長的則說明它太長而無法讀取。

每個擴充功能的 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)不會被攔下,所以頁面照常捲動。

用 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,包括隱藏頁面的,並透過它們自己對聽過什麼的記錄:警示佇列的記錄只在頁面顯示時才前進,所以它仍會把隱藏期間到達的排入佇列。頁面開啟時已經在等待的,兩者都不發出。

設定頁的一個音效開關和音量變成兩個,在這個裝置之下各有自己的開關和音量:音效(事件的提示:標準與佈景主題的問題、完成和失敗片段,單位就緒的鈴聲,禪的缽聲與敲擊聲)和背景音(在頁面底下播放的:禪的音樂,鴉閣的雨聲和雷聲;雷聲屬於天氣)。兩者預設都關,並且在標準、極客、禪和鴉閣檢視中都有作用;禪的角落選單和它的調色盤也都帶有兩者(各一個開關和兩段音量調整)。

細節

音量是 0 到 100(預設 60),以平方的形式只作為它那一類的增益進入頁面的音訊核心;每個開關只由它那一類的來源檢查,所以關掉音效會讓提示音靜音、雨聲還在,關掉背景音則讓雨聲、雷聲和音樂靜音、提示音還在。音效有試聽按鈕,並在開啟或放開音量時播放完成的片段;背景音沒有樣本,因為它播放的東西本來就聽得到。

舊的 sb.sound 和 sb.volume 在某一類沒有自己儲存的值時,被當作兩類的起點讀取(舊的開是兩者都開、用舊的音量,舊的關是兩者都關),而且絕不寫入,所以同一個瀏覽器上較舊的頁面仍讀它自己的。

使用者訊息或工具結果中的圖片(png、jpeg、gif 或 webp,如 pi 存在 session 檔中的)在對話中顯示為縮圖:在訊息下方,或在工具卡片下方,不論它是開著或關著。頁面從 api/image/<key>/<entry id>/<n> 載入每一張,它讀取那一筆項目並以圖片的位元組回答,快取一天,因為項目絕不改變;歷史本身只帶數量。縮圖開啟燈箱:圖片縮放到符合螢幕,點按則顯示它自己的尺寸,← → 或方向鍵切換同一則訊息的其他圖片,Escape、×,或點它旁邊來關閉。

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 個分開計算)。

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 自己的小工具在那裡顯示它。

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 for within 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 檔(已加入的 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 解除配對的裝置。

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 顯示手機是否信任這個名稱的某張憑證。它分不出是這台電腦的還是攻擊者的。

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 未量測 未量測 未量測 未量測 未量測 未量測 未量測 未量測

設定頁的 伺服器區塊只在這台電腦的頁面上(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 項目就會寫它。

除了 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 模式(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 編碼器是套件自己的(src/qr.ts),部分改編自 Project Nayuki 的 QR Code generator library(MIT,見 NOTICE.md);仍然沒有任何執行期相依套件。

一個沒人動的頁面會以影格消耗手機的電池,所以那些永不停止的動畫(旋轉的環、等待單位的 ping、工作徽章的轉圈、連線點的閃爍、地圖連線上的封包)會在 30 秒沒有輸入後停止,並以同一狀態的靜止標示取而代之(凍結的環或轉圈、等待單位的淡淡光暈);任何觸碰、按鍵、滑鼠移動或捲動都會讓它們回來。連線的封包只在它兩端的塔台之一正在工作時才行進,而手機上位於全螢幕通訊畫面底下的檔案不會被繪製。在 prefers-reduced-motion 下,頁面沒有這類動態,這些都不會執行。

手機版面(未發行;螢幕寬度最多 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)是標準檢視的哥德式外觀,版面與互動都相同(只在頁面顯示它時才載入)。只有深色:帶冷紫調的近黑底色、骨白文字、等待用燭金、失敗用血紅、休息用霧灰,每種文字顏色的對比至少 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 首虛構曲目的音樂資料夾;沒有音訊檔,所以開啟的曲目寫著點按以播放)。

標準與極客中的主塔台(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),匯流排沿著欄與欄之間的間隙,然後沿著每一欄左邊的間隙往下,再橫跨到每座塔台的中心,所以一欄讀起來就是一條主幹加分支,而且沒有任何線段穿過卡片。連線的封包在任一端工作時沿著同一條路徑行進,以及手機抽屜的第一列。沒有任何基地再有皇冠。主塔台的頁面:附轉送內容的對話、以 chip 呈現的詢問,以及對話框

在圓的畫面上(沒有項目、沒有停頓、沒有同伴),主塔台以一條帶子的形式位於呼吸的圓之下:它的行在老闆叫出它們之後顯示(高度容得下多少完整的行就顯示多少,最新的在腳邊、靠近輸入框;安靜時是三行,手機上是兩行;圓會先讓出空間,絕不低於 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 邊框。你取走的項目先占據螢幕,而帶子隨圓一起離開。禪的帶子在圓之下:主塔台的最後幾行與收起的輸入 帶子展開輸入並有草稿,行在指標下清晰 手機上的帶子

?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)。

協調者是舵輪,整合員是兩支箭頭匯合,值守是燈塔,火車是原本的火車頭,被派出的工作者、偵察、規劃、審查、監控者依序是扳手、望遠鏡、鉛筆加尺、放大鏡與獵犬。塔台(控制塔)與主塔台(皇冠)保留自己的,不知道種類的單位仍用位置的圖示(主智慧體、成員、子智慧體)。圖示由 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、字型或執行期相依套件。

瀏覽器檢視切換狀態