架構
瀏覽器、Node.js 伺服器、tmux 與 Claude CLI 是如何拼在一起的。
purplemux 是三層拼裝起來的:瀏覽器前端、:8022 上的 Node.js 伺服器,以及主機上的 tmux + Claude CLI。它們之間的傳輸不是二進位 WebSocket,就是小型 HTTP POST。
三層
Browser Node.js server (:8022) Host
───────── ──────────────────────── ──────────────
xterm.js ◀──ws /api/terminal──▶ terminal-server.ts ──node-pty──▶ tmux (purple socket)
Timeline ◀──ws /api/timeline──▶ timeline-server.ts │
Status ◀──ws /api/status────▶ status-server.ts └─▶ shell ─▶ claude
Sync ◀──ws /api/sync──────▶ sync-server.ts
status-manager.ts ◀──POST /api/status/hook── status-hook.sh
rate-limits-watcher.ts ◀──POST /api/status/statusline── statusline.sh
JSONL watcher ──reads── ~/.claude/projects/**/*.jsonl
每個 WebSocket 都只負責單一目的,並不多工。認證方式是在 WS upgrade 時驗證 NextAuth JWT cookie。
瀏覽器
前端是 Next.js(Pages Router)App。會與伺服器通訊的部分:
| 元件 | 函式庫 | 用途 |
|---|---|---|
| 終端機窗格 | xterm.js |
渲染來自 /api/terminal 的位元組。送出按鍵、調整大小事件、標題變化(onTitleChange)。 |
| 工作階段時間軸 | React + useTimeline |
渲染來自 /api/timeline 的 Claude 回合。沒有 cliState 衍生 — 那都在伺服器端。 |
| 狀態指示器 | Zustand useTabStore |
由 /api/status 訊息驅動的分頁徽章、側邊欄圓點、通知計數。 |
| 多裝置同步 | useSyncClient |
透過 /api/sync 監看其他裝置上對工作區 / 版面所做的編輯。 |
分頁標題與前景程序來自 xterm.js 的 onTitleChange 事件 — tmux 被設定(src/config/tmux.conf)為每兩秒送出 #{pane_current_command}|#{pane_current_path},由 lib/tab-title.ts 解析。
Node.js 伺服器
server.ts 是一個自訂 HTTP 伺服器,在同一連接埠上同時掛載 Next.js 與四個 ws WebSocketServer 實例。
WebSocket endpoints
| 路徑 | 處理者 | 方向 | 用途 |
|---|---|---|---|
/api/terminal |
terminal-server.ts |
雙向、二進位 | 透過 node-pty 連到 tmux 工作階段的終端機 I/O |
/api/timeline |
timeline-server.ts |
伺服器 → 客戶端 | 串流從 JSONL 解析的 Claude 工作階段條目 |
/api/status |
status-server.ts |
雙向、JSON | 伺服器送出 status:sync / status:update / status:hook-event,客戶端送出 status:tab-dismissed / status:ack-notification / status:request-sync |
/api/sync |
sync-server.ts |
雙向、JSON | 跨裝置工作區狀態 |
外加 /api/install 用於首次執行的安裝程式(不需要認證)。
終端機二進位協定
/api/terminal 使用一個定義於 src/lib/terminal-protocol.ts 的精簡二進位協定:
| 代碼 | 名稱 | 方向 | Payload |
|---|---|---|---|
0x00 |
MSG_STDIN |
客戶端 → 伺服器 | 鍵碼 |
0x01 |
MSG_STDOUT |
伺服器 → 客戶端 | 終端機輸出 |
0x02 |
MSG_RESIZE |
客戶端 → 伺服器 | cols: u16, rows: u16 |
0x03 |
MSG_HEARTBEAT |
雙向 | 30 秒間隔,90 秒 timeout |
0x04 |
MSG_KILL_SESSION |
客戶端 → 伺服器 | 結束底層 tmux 工作階段 |
0x05 |
MSG_WEB_STDIN |
客戶端 → 伺服器 | 網頁輸入列文字(在 copy-mode 結束後送達) |
背壓:當 WS bufferedAmount > 1 MB 時 pty.pause,低於 256 KB 時恢復。每個伺服器最多 32 個並發連線,超過時最舊的會被丟棄。
狀態管理器
src/lib/status-manager.ts 是 cliState 的單一真實來源。Hook 事件透過 /api/status/hook(以 token 認證的 POST)流入、依分頁排序(eventSeq),並由 deriveStateFromEvent 縮減為 idle / busy / needs-input / ready-for-review / unknown。JSONL 監看器只更新 metadata,但會發出一個合成的 interrupt 事件。
完整狀態機請見 Session status (STATUS.md)。
tmux 層
purplemux 在專屬 socket 上執行隔離的 tmux — -L purple — 並使用自己的設定 src/config/tmux.conf。你的 ~/.tmux.conf 永遠不會被讀取。
工作階段命名為 pt-{workspaceId}-{paneId}-{tabId}。瀏覽器中的一個終端機窗格對應一個 tmux 工作階段,透過 node-pty 連接。
tmux socket: purple
├── pt-ws-MMKl07-pa-1-tb-1 ← 瀏覽器分頁 1
├── pt-ws-MMKl07-pa-1-tb-2 ← 瀏覽器分頁 2
└── pt-ws-MMKl07-pa-2-tb-1 ← 分割窗格、分頁 1
prefix 已停用,狀態列關閉(xterm.js 自繪外框),set-titles 開啟,mouse on 把滾輪交給 copy-mode。tmux 是讓工作階段能撐過瀏覽器關閉、Wi-Fi 斷線或伺服器重啟的關鍵。
完整 tmux 設定、指令封裝與程序偵測細節,請見 tmux & process detection (TMUX.md)。
Claude CLI 整合
purplemux 並不 fork 或包裝 Claude — claude 二進位就是你已經安裝的那一個。新增的兩件事是:
- Hook settings — 啟動時,
ensureHookSettings()寫入~/.purplemux/hooks.json、status-hook.sh與statusline.sh。每個 Claude 分頁都以--settings ~/.purplemux/hooks.json啟動,因此SessionStart、UserPromptSubmit、Notification、Stop、PreCompact、PostCompact都會 POST 回伺服器。 - JSONL 讀取 —
~/.claude/projects/**/*.jsonl由timeline-server.ts解析以呈現即時對話檢視,並由session-detection.ts監看,透過~/.claude/sessions/下的 PID 檔來偵測正在執行的 Claude 程序。
Hook 指令稿讀取 ~/.purplemux/port 與 ~/.purplemux/cli-token,並以 x-pmux-token POST。在伺服器離線時它們會靜默失敗,所以在 Claude 執行中關閉 purplemux 不會崩潰。
啟動順序
server.ts:start() 依序執行:
acquireLock(port)— 透過~/.purplemux/pmux.lock的單例守衛initConfigStore()+initShellPath()(解析使用者 login shell 的PATH)initAuthCredentials()— 將 scrypt 雜湊密碼與 HMAC secret 載入 envscanSessions()+applyConfig()— 清除已死的 tmux 工作階段、套用tmux.confinitWorkspaceStore()— 載入workspaces.json與每工作區的layout.jsonautoResumeOnStartup()— 在儲存的目錄重新啟動 shell,嘗試 Claude resumegetStatusManager().init()— 啟動 metadata 輪詢app.prepare()(Next.js dev)或require('.next/standalone/server.js')(prod)- 在
bindPlan.host:port(依存取政策為0.0.0.0或127.0.0.1)執行listenWithFallback() ensureHookSettings(result.port)— 以實際連接埠寫入或更新 hook 指令稿getCliToken()— 讀取或產生~/.purplemux/cli-tokenwriteAllClaudePromptFiles()— 重新整理每工作區的claude-prompt.md
連接埠解析到第 10 步之間的視窗,正是 hook 指令稿每次啟動都重新產生的原因:它們需要把實際生效的連接埠寫進去。
自訂伺服器與 Next.js 模組圖
延伸閱讀
docs/TMUX.md— tmux 設定、指令封裝、程序樹走訪、終端機二進位協定。docs/STATUS.md— Claude CLI 狀態機、hook 流程、合成 interrupt 事件、JSONL 監看器。docs/DATA-DIR.md— purplemux 寫入的每個檔案。