@blackbelt-technology/pi-agent-dashboard 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/AGENTS.md +342 -0
  2. package/README.md +619 -0
  3. package/docs/architecture.md +646 -0
  4. package/package.json +92 -0
  5. package/packages/extension/package.json +33 -0
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
  7. package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
  8. package/packages/extension/src/__tests__/connection.test.ts +344 -0
  9. package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
  10. package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
  11. package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
  12. package/packages/extension/src/__tests__/git-info.test.ts +112 -0
  13. package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
  14. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
  15. package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
  16. package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
  17. package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
  18. package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
  19. package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
  20. package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
  21. package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
  22. package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
  23. package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
  24. package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
  25. package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
  26. package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
  27. package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
  28. package/packages/extension/src/ask-user-tool.ts +63 -0
  29. package/packages/extension/src/bridge-context.ts +64 -0
  30. package/packages/extension/src/bridge.ts +926 -0
  31. package/packages/extension/src/command-handler.ts +538 -0
  32. package/packages/extension/src/connection.ts +204 -0
  33. package/packages/extension/src/dev-build.ts +39 -0
  34. package/packages/extension/src/event-forwarder.ts +40 -0
  35. package/packages/extension/src/flow-event-wiring.ts +102 -0
  36. package/packages/extension/src/git-info.ts +65 -0
  37. package/packages/extension/src/git-link-builder.ts +112 -0
  38. package/packages/extension/src/model-tracker.ts +56 -0
  39. package/packages/extension/src/pi-env.d.ts +23 -0
  40. package/packages/extension/src/process-metrics.ts +70 -0
  41. package/packages/extension/src/process-scanner.ts +396 -0
  42. package/packages/extension/src/prompt-expander.ts +87 -0
  43. package/packages/extension/src/provider-register.ts +276 -0
  44. package/packages/extension/src/server-auto-start.ts +87 -0
  45. package/packages/extension/src/server-launcher.ts +82 -0
  46. package/packages/extension/src/server-probe.ts +33 -0
  47. package/packages/extension/src/session-sync.ts +154 -0
  48. package/packages/extension/src/source-detector.ts +26 -0
  49. package/packages/extension/src/ui-proxy.ts +269 -0
  50. package/packages/extension/tsconfig.json +11 -0
  51. package/packages/server/package.json +37 -0
  52. package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
  53. package/packages/server/src/__tests__/auth.test.ts +224 -0
  54. package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
  55. package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
  56. package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
  57. package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
  58. package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
  59. package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
  60. package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
  61. package/packages/server/src/__tests__/config-api.test.ts +104 -0
  62. package/packages/server/src/__tests__/cors.test.ts +48 -0
  63. package/packages/server/src/__tests__/directory-service.test.ts +240 -0
  64. package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
  65. package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
  66. package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
  67. package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
  68. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
  69. package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
  70. package/packages/server/src/__tests__/extension-register.test.ts +61 -0
  71. package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
  72. package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
  73. package/packages/server/src/__tests__/git-operations.test.ts +251 -0
  74. package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
  75. package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
  76. package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
  77. package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
  78. package/packages/server/src/__tests__/json-store.test.ts +70 -0
  79. package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
  80. package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
  81. package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
  82. package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
  83. package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
  84. package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
  85. package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
  86. package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
  87. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
  88. package/packages/server/src/__tests__/package-routes.test.ts +172 -0
  89. package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
  90. package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
  91. package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
  92. package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
  93. package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
  94. package/packages/server/src/__tests__/process-manager.test.ts +184 -0
  95. package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
  96. package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
  97. package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
  98. package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
  99. package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
  100. package/packages/server/src/__tests__/server-pid.test.ts +89 -0
  101. package/packages/server/src/__tests__/session-api.test.ts +244 -0
  102. package/packages/server/src/__tests__/session-diff.test.ts +138 -0
  103. package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
  104. package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
  105. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
  106. package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
  107. package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
  108. package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
  109. package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
  110. package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
  111. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
  112. package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
  113. package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
  114. package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
  115. package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
  116. package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
  117. package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
  118. package/packages/server/src/__tests__/tunnel.test.ts +206 -0
  119. package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
  120. package/packages/server/src/auth-plugin.ts +302 -0
  121. package/packages/server/src/auth.ts +323 -0
  122. package/packages/server/src/browse.ts +55 -0
  123. package/packages/server/src/browser-gateway.ts +495 -0
  124. package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
  125. package/packages/server/src/browser-handlers/handler-context.ts +45 -0
  126. package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
  127. package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
  128. package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
  129. package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
  130. package/packages/server/src/cli.ts +347 -0
  131. package/packages/server/src/config-api.ts +130 -0
  132. package/packages/server/src/directory-service.ts +162 -0
  133. package/packages/server/src/editor-detection.ts +60 -0
  134. package/packages/server/src/editor-manager.ts +352 -0
  135. package/packages/server/src/editor-proxy.ts +134 -0
  136. package/packages/server/src/editor-registry.ts +108 -0
  137. package/packages/server/src/event-status-extraction.ts +131 -0
  138. package/packages/server/src/event-wiring.ts +589 -0
  139. package/packages/server/src/extension-register.ts +92 -0
  140. package/packages/server/src/git-operations.ts +200 -0
  141. package/packages/server/src/headless-pid-registry.ts +207 -0
  142. package/packages/server/src/idle-timer.ts +61 -0
  143. package/packages/server/src/json-store.ts +32 -0
  144. package/packages/server/src/localhost-guard.ts +117 -0
  145. package/packages/server/src/memory-event-store.ts +193 -0
  146. package/packages/server/src/memory-session-manager.ts +123 -0
  147. package/packages/server/src/meta-persistence.ts +64 -0
  148. package/packages/server/src/migrate-persistence.ts +195 -0
  149. package/packages/server/src/npm-search-proxy.ts +143 -0
  150. package/packages/server/src/oauth-callback-server.ts +177 -0
  151. package/packages/server/src/openspec-archive.ts +60 -0
  152. package/packages/server/src/package-manager-wrapper.ts +200 -0
  153. package/packages/server/src/pending-fork-registry.ts +53 -0
  154. package/packages/server/src/pending-load-manager.ts +110 -0
  155. package/packages/server/src/pending-resume-registry.ts +69 -0
  156. package/packages/server/src/pi-gateway.ts +419 -0
  157. package/packages/server/src/pi-resource-scanner.ts +369 -0
  158. package/packages/server/src/preferences-store.ts +116 -0
  159. package/packages/server/src/process-manager.ts +311 -0
  160. package/packages/server/src/provider-auth-handlers.ts +438 -0
  161. package/packages/server/src/provider-auth-storage.ts +200 -0
  162. package/packages/server/src/resolve-path.ts +12 -0
  163. package/packages/server/src/routes/editor-routes.ts +86 -0
  164. package/packages/server/src/routes/file-routes.ts +116 -0
  165. package/packages/server/src/routes/git-routes.ts +89 -0
  166. package/packages/server/src/routes/openspec-routes.ts +99 -0
  167. package/packages/server/src/routes/package-routes.ts +172 -0
  168. package/packages/server/src/routes/provider-auth-routes.ts +244 -0
  169. package/packages/server/src/routes/provider-routes.ts +101 -0
  170. package/packages/server/src/routes/route-deps.ts +23 -0
  171. package/packages/server/src/routes/session-routes.ts +91 -0
  172. package/packages/server/src/routes/system-routes.ts +271 -0
  173. package/packages/server/src/server-pid.ts +84 -0
  174. package/packages/server/src/server.ts +554 -0
  175. package/packages/server/src/session-api.ts +330 -0
  176. package/packages/server/src/session-bootstrap.ts +80 -0
  177. package/packages/server/src/session-diff.ts +178 -0
  178. package/packages/server/src/session-discovery.ts +134 -0
  179. package/packages/server/src/session-file-reader.ts +135 -0
  180. package/packages/server/src/session-order-manager.ts +73 -0
  181. package/packages/server/src/session-scanner.ts +233 -0
  182. package/packages/server/src/session-stats-reader.ts +99 -0
  183. package/packages/server/src/terminal-gateway.ts +51 -0
  184. package/packages/server/src/terminal-manager.ts +241 -0
  185. package/packages/server/src/tunnel.ts +329 -0
  186. package/packages/server/tsconfig.json +11 -0
  187. package/packages/shared/package.json +15 -0
  188. package/packages/shared/src/__tests__/config.test.ts +358 -0
  189. package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
  190. package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
  191. package/packages/shared/src/__tests__/protocol.test.ts +243 -0
  192. package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
  193. package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
  194. package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
  195. package/packages/shared/src/archive-types.ts +11 -0
  196. package/packages/shared/src/browser-protocol.ts +534 -0
  197. package/packages/shared/src/config.ts +245 -0
  198. package/packages/shared/src/diff-types.ts +41 -0
  199. package/packages/shared/src/editor-types.ts +18 -0
  200. package/packages/shared/src/mdns-discovery.ts +248 -0
  201. package/packages/shared/src/openspec-activity-detector.ts +109 -0
  202. package/packages/shared/src/openspec-poller.ts +96 -0
  203. package/packages/shared/src/protocol.ts +369 -0
  204. package/packages/shared/src/resolve-jiti.ts +43 -0
  205. package/packages/shared/src/rest-api.ts +255 -0
  206. package/packages/shared/src/server-identity.ts +51 -0
  207. package/packages/shared/src/session-meta.ts +86 -0
  208. package/packages/shared/src/state-replay.ts +174 -0
  209. package/packages/shared/src/stats-extractor.ts +54 -0
  210. package/packages/shared/src/terminal-types.ts +18 -0
  211. package/packages/shared/src/types.ts +351 -0
  212. package/packages/shared/tsconfig.json +8 -0
@@ -0,0 +1,646 @@
1
+ # PI Dashboard Architecture
2
+
3
+ ## Overview
4
+
5
+ The PI Dashboard is a web-based dashboard for monitoring and interacting with pi agent sessions. It consists of three components:
6
+
7
+ ```
8
+ ┌─────────────┐ WebSocket ┌──────────────┐ WebSocket ┌─────────────┐
9
+ │ Bridge │ ◄─────────────────► │ Dashboard │ ◄───────────────► │ Web Client │
10
+ │ Extension │ (port 9999) │ Server │ (port 8000) │ (React) │
11
+ │ (per pi) │ │ (Node.js) │ │ (Browser) │
12
+ └─────────────┘ └──────────────┘ └─────────────┘
13
+
14
+ ┌─────┴─────┐
15
+ │ In-Memory │
16
+ │ + JSON │
17
+ └───────────┘
18
+ ```
19
+
20
+ ## Components
21
+
22
+ ### 1. Bridge Extension (`src/extension/`)
23
+ A global pi extension that runs in every pi session. It:
24
+ - Detects session source (TUI, Zed, tmux, dashboard-spawned) via `.meta.json` sidecar files and environment variables
25
+ - Forwards all pi events to the dashboard server via WebSocket
26
+ - Relays commands from the dashboard back to pi
27
+ - Handles reconnection with exponential backoff and event buffering
28
+ - Sends heartbeats every 15s with process metrics (CPU%, RSS, heap, event loop max delay, load average); server responds with `heartbeat_ack`
29
+ - Server liveness watchdog: forces reconnect if no message received for 60s
30
+ - Server-side WS ping/pong (60s interval) detects dead TCP connections; requires 2 consecutive missed pongs before killing (tolerates long-running bash commands that block the event loop)
31
+ - Detects OpenSpec activity (phase/change) from tool events; server auto-attaches the change when `changeName` is detected (phase is not required — skills loaded via prompt templates don't emit a SKILL.md read event). The session card's OpenSpec activity badge displays when either `openspecPhase` or `openspecChange` is detected (not just phase).
32
+ - **Duplicate bridge prevention**: Uses `process`-level shared state (not `globalThis`) with a monotonic generation counter. When the extension is loaded multiple times (e.g., local + global npm package), only the latest instance's event handlers are active — stale listeners bail out immediately. All previous connections and timers are tracked and cleaned up on re-init.
33
+ - **Subagent re-entry guard**: When pi-subagents launches an Agent tool, the subagent creates its own `AgentSession` which loads extensions (including the bridge) in the same process. Without protection, this would overwrite the parent bridge's global state, disconnect its WebSocket, and prevent `tool_execution_end`/`agent_end` from being forwarded — leaving the parent session stuck at "streaming" forever. The bridge stores a reference to its owning `pi` instance and skips initialization when called from a different instance (subagent).
34
+ - Proxies `ctx.ui` dialog methods (confirm, select, input, editor) to the dashboard via `ui-proxy.ts`
35
+ - TUI sessions: races terminal dialog against dashboard response (first wins)
36
+ - Race cancellation: when dashboard wins, TUI dialog is aborted via `AbortSignal`; when TUI wins, dashboard dialog is dismissed via `extension_ui_dismiss` message
37
+ - Headless sessions: only dashboard can respond
38
+ - Fire-and-forget methods (notify) are forwarded alongside the original call
39
+ - Re-sends pending UI requests on WebSocket reconnect (server restart resilience)
40
+
41
+ ### 2. Dashboard Server (`src/server/`)
42
+ A Node.js HTTP + WebSocket server that:
43
+ - Accepts connections from bridge extensions (Pi Gateway, port 9999)
44
+ - Accepts connections from web browsers (Browser Gateway, port 8000)
45
+ - Stores events in an in-memory buffer with LRU eviction (max 100 sessions, 5000 events per session)
46
+ - Truncates large event payloads (tool results, file content, thinking blocks) to bound memory
47
+ - Applies WebSocket backpressure on browser connections (drops messages when send buffer > 4MB)
48
+ - Manages sessions in a pure in-memory registry (populated from bridge connections and direct disk discovery)
49
+ - Persists global preferences (pinned directories, session order) in `~/.pi/dashboard/preferences.json`
50
+ - Discovers historical sessions directly from disk via `SessionManager.list()` (DirectoryService)
51
+ - Loads session events on demand directly from disk via `SessionManager.open()` (DirectoryService)
52
+ - Polls OpenSpec CLI per directory every 30s, broadcasting changes to browsers (DirectoryService)
53
+ - Serves the built web client as static files (production) or proxies to Vite dev server (dev mode)
54
+ - Writes per-session `.meta.json` sidecar files with dashboard state and cached stats
55
+ - Exposes REST API for session management, event content fetch, pinned directories, and file reading
56
+ - Provides session control REST endpoints (`/api/session/:id/*`) wrapping WebSocket-only operations (prompt, abort, spawn, resume, rename, hide, flow-control, model, thinking-level, attach/detach-proposal) — see `src/server/session-api.ts`
57
+
58
+ **Server decomposition:** The server is split into focused modules:
59
+ - `server.ts` — Orchestrator: creates services, composes modules, manages lifecycle
60
+ - `routes/` — REST API routes grouped by domain (session, git, file, openspec, system)
61
+ - `event-wiring.ts` — Pi gateway → browser gateway event forwarding
62
+ - `idle-timer.ts` — Auto-shutdown idle timer
63
+ - `session-bootstrap.ts` — Startup session discovery and OpenSpec polling init
64
+ - `extension-register.ts` — Auto-registers bundled bridge extension in pi's global settings (`~/.pi/agent/settings.json`) on startup; no-op in dev mode
65
+ - `browser-handlers/` — Browser WebSocket message handlers by domain (subscription, session-actions, session-meta, terminal, directory)
66
+
67
+ ### 3. Web Client (`src/client/`)
68
+ A React-based responsive web UI that:
69
+ - Shows all active sessions organized by directory, with pinned directories always visible at the top
70
+ - Renders chat messages with markdown, syntax highlighting, and streaming
71
+ - Persists scroll position per session — switching sessions restores exact scroll position if locked, or scrolls to bottom if following
72
+ - Displays collapsed tool call steps with lazy-loaded content and elapsed time badges
73
+ - Shows live ticking elapsed counters on running operations (thinking, tool calls) and final duration on completed ones
74
+ - Provides command autocomplete with `/` prefix
75
+ - Supports bidirectional interaction (send prompts, run commands)
76
+ - Works on mobile with responsive layout and swipe gestures
77
+
78
+ ### 4. Shared Types (`src/shared/`)
79
+ TypeScript type definitions shared across all components:
80
+ - `protocol.ts` - Extension↔Server WebSocket messages
81
+ - `browser-protocol.ts` - Server↔Browser WebSocket messages
82
+ - `types.ts` - Data models (Session, Workspace, Event, etc.)
83
+
84
+ ## Data Flow
85
+
86
+ ### Event Flow (pi → browser)
87
+ 1. Pi emits event (e.g., `message_update`)
88
+ 2. Bridge extension converts to `event_forward` protocol message
89
+ 3. Server receives, stores in in-memory buffer, assigns sequence number
90
+ 4. Server broadcasts to all subscribed browsers via `event` message
91
+ 5. Browser's event reducer processes event, React renders update
92
+
93
+ ### Interactive UI Flow (extension dialog → browser → response)
94
+ 1. Extension calls `ctx.ui.confirm()` / `select()` / `input()` / `editor()`
95
+ 2. Bridge UI proxy intercepts, sends `extension_ui_request` to server
96
+ 3. Server tracks the request in `pendingUiRequests` map and forwards to subscribed browsers
97
+ 4. Browser renders interactive card inline in chat (renderers in `interactive-renderers/`)
98
+ 5. User clicks Allow/Deny/option/submits text
99
+ 6. Browser sends `extension_ui_response` to server, optimistically clears "Waiting for input" on session card
100
+ 7. Server clears the request from `pendingUiRequests` and routes response to bridge extension
101
+ 8. Bridge UI proxy resolves the original dialog promise
102
+
103
+ **Race cancellation (TUI sessions):**
104
+ - TUI and dashboard both show the dialog simultaneously via `Promise.race`
105
+ - When dashboard answers first: TUI dialog is dismissed via `AbortSignal` (passed in `ExtensionUIDialogOptions.signal`)
106
+ - When TUI answers first: bridge sends `extension_ui_dismiss` to server → forwarded as `ui_dismiss` to browsers → dashboard transitions dialog to "dismissed" ("Answered in terminal")
107
+ - Pending Map entry is cleaned up immediately when TUI wins, preventing memory leaks
108
+
109
+ **Resilience:**
110
+ - **Page refresh**: Server replays pending `extension_ui_request` messages when a browser subscribes, so interactive dialogs survive page refreshes.
111
+ - **Server restart**: Bridge UI proxy re-sends all pending requests on WebSocket reconnect (`resendPending()`), so dialogs survive server restarts.
112
+
113
+ ### Command Flow (browser → pi)
114
+ 1. User types prompt or command in browser
115
+ 2. Browser sends `send_prompt` via WebSocket
116
+ 3. Server routes to correct bridge extension by sessionId
117
+ 4. Bridge extension's command handler parses input for pi command prefixes:
118
+ - `!!<cmd>` → silent bash execution via `pi.exec()`, result as `bash_output` event
119
+ - `!<cmd>` → bash execution via `pi.exec()`, result as `bash_output` event + send to LLM
120
+ - `/compact [instructions]` → `ctx.compact()`, feedback as `command_feedback` event
121
+ - `/<command>` → `session.prompt()` for extension commands/skills/templates (fallback to `sendUserMessage()`)
122
+ - Colon-to-hyphen aliasing: `/opsx:continue` resolves to `opsx-continue.md` template (both `:` and `-` forms work)
123
+ - Plain text → `pi.sendUserMessage()` (default)
124
+ 5. Pi processes the command, events flow back via event flow
125
+
126
+ ### Flow Dashboard Data Flow (pi-flows → browser)
127
+ pi-flows runs multi-agent workflows in-process. Subagent sessions use `SessionManager.inMemory()` and don't bootstrap the bridge, so flow data must be explicitly forwarded by the parent session's bridge.
128
+
129
+ 1. pi-flows `EventEmitObserver` emits `flow:*` events on `pi.events` (all 10 `FlowObserver` callbacks)
130
+ 2. Bridge extension listens to `flow:*` events and forwards as `event_forward` messages with `flow_*` event types
131
+ 3. Server stores events, extracts flow metadata to `DashboardSession` fields (`activeFlowName`, `flowAgentsDone`, `flowAgentsTotal`, `flowStatus`)
132
+ 4. Browser event reducer builds client-side `FlowState` (agents map, tool history, detail entries)
133
+ 5. React renders `FlowDashboard` (sticky card grid above ChatView), `FlowAgentDetail` (replaces chat), `FlowSummary` (post-completion)
134
+
135
+ **Flow controls (browser → pi-flows):**
136
+ - Abort: browser sends `flow_control { action: "abort" }` → server → bridge → `pi.events.emit("flow:abort")` → `flowManager.abort()`
137
+ - Autonomous toggle: browser sends `flow_control { action: "toggle_autonomous" }` → same path → `setAutonomousMode()`
138
+
139
+ ### Force Kill Escalation
140
+ The Stop button supports two-click escalation for stuck sessions:
141
+ 1. **Click 1 (Abort)**: Sends `abort` → bridge → `ctx.abort()`. Button transitions to orange pulsing "Force Stop".
142
+ 2. **Click 2 (Force Kill)**: Sends `force_kill` → server kills the process via SIGTERM → 2s wait → SIGKILL (with PID safety check). Session marked "ended" (not removed), resumable via fork/continue.
143
+
144
+ The bridge includes `process.pid` in `session_register` so the server can kill the process. The server also force-closes the bridge WebSocket and uses the headless PID registry as a fallback. If no PID is available, only the WebSocket is closed.
145
+
146
+ Inline stop buttons also appear on running tool cards in `ToolCallStep`, providing contextual abort access right where the stuck command is visible.
147
+
148
+ ### Repeated Tool Call Collapsing
149
+ Consecutive tool calls with the same name and identical args (e.g. health check polling loops) are collapsed into a single expandable group showing a count badge (e.g. "×24"). Implemented via `groupConsecutiveToolCalls()` in the chat rendering pipeline. Groups require 3+ calls; running tools are never grouped.
150
+
151
+ **Fork decisions and subagent ask_user:**
152
+ - Already work through existing UI proxy — `TuiFlowIOAdapter` calls `ctx.ui.select/confirm/input` which the bridge wraps and races between TUI and dashboard
153
+
154
+ **Flow launcher:**
155
+ - Available flows detected from session commands list (heuristic: `source: "extension"`, excluding management commands)
156
+ - Launch dispatched as `send_prompt` with `/<flow-name> <task>`
157
+ - Commands list auto-refreshed on `flow:rediscover` and `flow:complete` events
158
+
159
+ **pi-flows local patches required** (upstream report prepared):
160
+ - `EventEmitObserver`: 5 missing methods added (flow-started, agent-started, agent-complete, assistant-text, thinking-text)
161
+ - `index.ts`: `flow:abort` and `flow:toggle-autonomous` event listeners added
162
+ - `flow-tui.ts`: `autonomousMode` included in `flow:flow-started` event data
163
+
164
+ ### Auto-Resume on Prompt
165
+ When a user sends a prompt to an ended session, the server automatically resumes it:
166
+ 1. Server detects `send_prompt` for a session with `status === "ended"` and a valid `sessionFile`
167
+ 2. Prompt is queued in `PendingResumeRegistry` (keyed by cwd, 30s expiry)
168
+ 3. Session is set to `resuming: true`, card shows pulsing yellow dot + "Resuming…"
169
+ 4. Server spawns `pi --session <file>` (continue mode)
170
+ 5. `pi --session` reconnects with the same session ID — `session_register` sets status back to `"active"`
171
+ 6. Server flushes queued prompt to the session and clears `resuming` flag
172
+ 7. No navigation needed — user is already viewing the same session
173
+ 8. On timeout (30s) or spawn failure, `resuming` flag is cleared and session returns to normal ended state
174
+ 9. If user sends another prompt while already resuming, the queued prompt is updated without spawning a second process
175
+
176
+ ### Model & Thinking Level Flow
177
+ 1. Bridge sends current model and thinking level in `session_register` on connect
178
+ 2. When user changes model (via `/model`), pi emits `model_select` event
179
+ 3. Bridge enriches the event with current `thinkingLevel` from context before forwarding
180
+ 4. Bridge also sends a `model_update` protocol message for session-level tracking
181
+ 5. Server extracts model/thinkingLevel from events and `model_update`, broadcasts to browsers
182
+ 6. Thinking level changes (via pi keybinding) are detected when `model_select` events fire, on reconnect, and immediately after `set_thinking_level` commands
183
+ 7. Browser can send `set_thinking_level` to change thinking level remotely
184
+
185
+ ### Context Usage Tracking
186
+ 1. On each `turn_end`, the bridge calls pi's `ctx.getContextUsage()` API to get real-time context usage (tokens used + actual context window from the provider)
187
+ 2. Bridge enriches the `turn_end` event with this `contextUsage` data before forwarding to the server
188
+ 3. Server extracts `contextUsage` from the event data and passes it to `extractTurnStats()`, which includes it in the synthesized `stats_update` event
189
+ 4. Server updates `session.contextTokens` and `session.contextWindow` and broadcasts to browsers
190
+ 5. The `onChange` handler persists these values to `.meta.json` (debounced 1s)
191
+ 6. On server restart, the scanner restores `contextTokens`/`contextWindow` from `.meta.json`
192
+ 7. Client's event reducer stores `contextUsage` from `stats_update` events; `App.tsx` falls back to `session.contextTokens/contextWindow` for sessions without live reducer state
193
+ 8. When real data is unavailable (e.g., old sessions without persisted context data), `state-replay.ts` and `session-stats-reader.ts` use `inferContextWindow()` to estimate context window from the model name
194
+
195
+ ### Git Polling
196
+ 1. Bridge polls git info every 30s (`git-info.ts`): branch, remote URL, PR number
197
+ 2. Changes are sent to the server only when values differ from last poll
198
+ 3. Server broadcasts updates to subscribed browsers
199
+
200
+ ### Child Process Scanning
201
+ 1. Bridge scans child processes every 10s via `process-scanner.ts` (two-phase: capture new PGIDs during active bash calls, then check tracked PGIDs)
202
+ 2. Only processes running ≥30s are reported (filters out short-lived commands)
203
+ 3. Bash/sh wrapper processes are excluded (only leaf commands shown)
204
+ 4. Bridge sends `process_list` to server only when the PID set changes (dedup)
205
+ 5. Server stores processes on the session object and forwards to subscribed browsers as `process_list_update`
206
+ 6. New browser connections receive current processes via the initial `session_added` message
207
+ 7. Session cards display processes with elapsed time and a kill button (sends SIGTERM to process group)
208
+
209
+ ### OpenSpec Polling (Server-Side)
210
+ 1. Server's DirectoryService polls `openspec` CLI every 30s for each known directory (union of pinned dirs + session cwds)
211
+ 2. OpenSpec data is keyed by directory (cwd), not by session — one poll per directory regardless of session count
212
+ 3. Changes are broadcast to all connected browsers via `openspec_update { cwd, data }`
213
+ 4. Browsers can request immediate refresh via `openspec_refresh { cwd }`
214
+ 5. New directories (pinned or from new sessions) trigger immediate discovery + polling
215
+
216
+ ### File Read API
217
+ The server exposes `GET /api/file?cwd=...&path=...` for reading files or listing directories from session working directories. Guards: localhost-only, cwd must match a known session, resolved path must stay inside cwd. Returns `{ type: "file", content }` or `{ type: "directory", entries }`.
218
+
219
+ ### Pi Resources Browser
220
+
221
+ The dashboard can display pi extensions, skills, and prompts installed for each workspace. The server-side scanner (`pi-resource-scanner.ts`) discovers resources from three sources:
222
+
223
+ 1. **Local**: `<cwd>/.pi/extensions/`, `.pi/skills/`, `.pi/prompts/`
224
+ 2. **Global**: `~/.pi/agent/extensions/`, `skills/`, `prompts/`
225
+ 3. **Packages**: Resolved from `packages[]` in both `<cwd>/.pi/settings.json` and `~/.pi/agent/settings.json` — supports npm, git, and local path packages with pi manifest or conventional directory fallback
226
+
227
+ Metadata is parsed from SKILL.md YAML frontmatter (`name`, `description`), prompt frontmatter, and `package.json`. Results are cached in DirectoryService and polled every 30s alongside OpenSpec.
228
+
229
+ **API endpoints:**
230
+ - `GET /api/pi-resources?cwd=...` — returns grouped resources (local, global, packages) from cache
231
+ - `GET /api/pi-resource-file?path=...` — reads resource files from allowed locations (`.pi/`, `~/.pi/agent/`, `node_modules/`, `.pi/git/`)
232
+
233
+ **Package Management:**
234
+ - `GET /api/packages/search?q=&type=` — proxied npm search for `keywords:pi-package`, cached 5min
235
+ - `GET /api/packages/readme?pkg=` — fetch package README from npm registry
236
+ - `GET /api/packages/installed?scope=global|local&cwd=` — list installed packages via pi's `PackageManager`
237
+ - `POST /api/packages/install` — install package (returns 202 + operationId, streams progress via WS)
238
+ - `POST /api/packages/remove` — remove package (same async pattern)
239
+ - `POST /api/packages/update` — update packages (same async pattern)
240
+ - `POST /api/packages/check-updates` — check for available updates (on-demand)
241
+
242
+ Package operations use pi's `DefaultPackageManager` API on the server, serialized (one at a time, 409 on concurrent). Progress events are forwarded to browsers via `package_progress` WebSocket messages. After any successful operation, the server sends `/reload` to all connected pi sessions.
243
+
244
+ **Client navigation stack:**
245
+ - Puzzle icon button in folder header → PiResourcesView (content area, "Installed" / "Packages" tabs)
246
+ - "View" button on resource → MarkdownPreviewView (`.md` as markdown, `.ts` as code block)
247
+ - Settings → Packages tab → inline PackageBrowser for global package management
248
+ - Back buttons pop the stack: Preview → Resources → Chat
249
+
250
+ ### Git Branch Selector
251
+
252
+ The dashboard provides a git branch selector at the folder group level. Clicking the branch icon in `GroupGitInfo` opens a typeahead `BranchPicker` dialog. The flow supports three states:
253
+
254
+ 1. **No git repo**: Dimmed icon labeled "Init git" — clicking triggers `POST /api/git/init`
255
+ 2. **Detached HEAD**: Shows short commit SHA — clicking opens the branch picker
256
+ 3. **Normal branch**: Shows branch name — clicking opens the branch picker
257
+
258
+ **Server API endpoints** (all localhost-only in `git-operations.ts`):
259
+ - `GET /api/git/branches?cwd=...` — lists local + remote branches sorted by committer date
260
+ - `POST /api/git/checkout` — switches branch; returns 409 with dirty file list if working tree is dirty
261
+ - `POST /api/git/init` — initializes a git repository
262
+ - `POST /api/git/stash-pop` — pops the most recent stash, reports conflicts
263
+
264
+ **Checkout flow**: Clean checkout closes immediately. Dirty working tree → client shows file list + "Stash & Switch" button → stash + checkout → asks "Pop stash on new branch?" with explicit Yes/No. Remote branches auto-create local tracking branches.
265
+
266
+ ### Session File Diff View
267
+
268
+ The dashboard provides a GitHub-style file diff viewer for sessions. It shows what files a session has changed, with per-change drill-down.
269
+
270
+ **Data flow**: `GET /api/session-diff?sessionId=xxx` (localhost-only) scans session events for Write/Edit tool calls, extracts file paths and change data, optionally enriches with `git diff HEAD` output. Returns `SessionDiffResponse` with files, per-file change events (timestamps + context messages), and optional git diffs.
271
+
272
+ **UI**: Split-pane content-area view (replaces ChatView when active). Left panel shows a two-level file tree — files with status indicators, expandable to show individual change events with timestamps and assistant message context. Right panel renders diffs via `@git-diff-view/react` with `@git-diff-view/lowlight` syntax highlighting. Supports split/unified diff modes and a file content view toggle.
273
+
274
+ **Entry point**: "Changed Files" button in SessionHeader (only visible when Write/Edit tool events exist). Works for both active and ended sessions.
275
+
276
+ ### Markdown Preview View
277
+ The web client includes a generic `MarkdownPreviewView` component that replaces the chat area. It supports a back button, title, optional tab bar, and loading/error states. For OpenSpec artifacts, the `useOpenSpecReader` hook maps artifact IDs (P/S/D/T) to file paths, fetches content via the file API, and concatenates specs from subdirectories.
278
+
279
+ ### Archive Browser
280
+ The `ArchiveBrowserView` provides a searchable, date-grouped listing of archived OpenSpec changes. It uses a dedicated `GET /api/openspec-archive?cwd=<path>` endpoint that scans `openspec/changes/archive/` and returns entry metadata (name, date, artifacts). The view uses two-level navigation: the list is the first level, and clicking an artifact letter (P/D/S/T) opens the reader as the second level. Back from the reader returns to the list (preserving search and scroll), and back from the list returns to the session view. Entry point is the `[Archive]` button in `FolderOpenSpecSection`.
281
+
282
+ ### Network Access Control
283
+
284
+ The server has a two-layer access model:
285
+
286
+ **Layer 1: Network Guard (`createNetworkGuard`)** — Fastify `preHandler` on all sensitive routes. Allows requests via three paths:
287
+ 1. **Loopback** — `127.0.0.1`, `::1`, `::ffff:127.0.0.1` (always allowed)
288
+ 2. **Trusted networks** — IPs matching `resolvedTrustedNetworks` (CIDR, wildcard, exact). Configured via top-level `trustedNetworks` in config, merged with `auth.bypassHosts` at load time.
289
+ 3. **Authenticated** — `request.isAuthenticated === true` (set by auth `onRequest` hook via `decorateRequest`)
290
+
291
+ Otherwise → 403. The guard strips `::ffff:` IPv4-mapped prefixes before matching.
292
+
293
+ **Layer 2: Auth Plugin (`onRequest` hook)** — Only registered when `auth` is configured. Skips loopback, trusted networks, `/auth/*`, `/api/health`, and `bypassUrls`. Validates JWT cookie for all other requests. Tags valid requests with `request.isAuthenticated = true`.
294
+
295
+ **Execution order**: `onRequest` (auth) → `preHandler` (guard) → handler. This means the auth hook tags the request before the guard checks it.
296
+
297
+ **WebSocket upgrades** follow the same logic: loopback → trusted network → JWT cookie validation.
298
+
299
+ **Zrok tunnel** connections appear as `127.0.0.1` (zrok proxies to localhost), so both layers pass automatically.
300
+
301
+ **`GET /api/network-interfaces`** returns detected non-internal IPv4 interfaces with computed CIDRs. Used by the Settings UI "Add Local Network" button. This endpoint uses the legacy `localhostGuard` (localhost-only, not network-guard-aware) since it exposes machine network topology.
302
+
303
+ ### OAuth Authentication Flow
304
+
305
+ Optional OAuth2 authentication protects the dashboard when accessed remotely.
306
+
307
+ 1. Server loads `auth` config from `~/.pi/dashboard/config.json` at startup
308
+ 2. If `auth.providers` has entries, the auth plugin registers routes, the `isAuthenticated` request decorator, and an `onRequest` hook
309
+ 3. The `onRequest` hook skips localhost requests (`isLoopback`), trusted network IPs (`resolvedTrustedNetworks`), `/auth/*` paths, `/api/health`, and configured `bypassUrls` path prefixes
310
+ 4. External requests without a valid `pi_dash_token` JWT cookie are redirected to `/auth/login`
311
+ 5. `/auth/login` shows a provider picker (or auto-redirects if single provider)
312
+ 6. OAuth callback exchanges code for token, fetches user info, validates against `allowedEmails`
313
+ 7. On success, a signed JWT cookie is set (7-day expiry) and user is redirected back
314
+ 8. WebSocket upgrade requests are also validated — external connections without valid cookie or trusted network get 401
315
+ 9. Supported providers: GitHub (hardcoded endpoints), Google/Keycloak/OIDC (via OIDC discovery)
316
+
317
+ ### Settings Panel
318
+ The web client includes a Settings panel (gear icon in sidebar header → `/settings` route) that lets users view and edit all dashboard configuration. The panel:
319
+ 1. Loads config via `GET /api/config` (secrets redacted as `***`)
320
+ 2. Renders grouped form fields: Server, Sessions, Tunnel, Trusted Networks, Authentication, Developer
321
+ 3. Sends only changed fields via `PUT /api/config` (partial merge)
322
+ 4. Server preserves `***` secrets (doesn't overwrite real values), writes to disk, and applies runtime-safe changes
323
+ 5. Port/piPort changes flag `restartRequired` in the response
324
+
325
+ ### Reconnection Flow
326
+ 1. Browser reconnects with `subscribe` message including `lastSeq`
327
+ 2. Server replays missed events from in-memory buffer in async batches of 50 with backpressure handling
328
+ 3. Browser's event reducer processes replay, rebuilding state
329
+
330
+ ### Bridge Reconnection (State Reset)
331
+ When a bridge extension reconnects (e.g., after `npm run reload` or network recovery):
332
+ 1. Bridge sends `session_register` with `eventCount` to re-register the session
333
+ 2. Server checks `canSkipWipe`: if the bridge's `eventCount` matches the server's `lastEntryCount` and events exist in the store, the wipe is skipped (fast reconnect path)
334
+ 3. **Full replay path** (`canSkipWipe = false`): Server clears the in-memory event store, broadcasts `session_state_reset` to browsers, stores replayed events, and sends them as `event_replay` batch after `replay_complete`
335
+ 4. **Skip replay path** (`canSkipWipe = true`): Server keeps existing events in the store, marks the session in `skipReplayInsert` set so replayed events are NOT re-inserted (preventing exponential duplication). Status updates are still processed for session state accuracy. After `replay_complete`, the `event_replay` batch is skipped since browsers already have the events.
336
+ 5. Bridge replays full session history as individual `event_forward` messages
337
+ 6. Bridge sends `replay_complete` to signal replay is done
338
+ 7. If the agent is currently mid-turn (bridge tracks `isAgentStreaming` flag in persistent `BridgeState`), a synthetic `agent_start` event is sent after `replay_complete` so the session card shows "Thinking…" instead of "Waiting for input"
339
+ 8. Server clears the replaying flag, broadcasts the final accumulated session status
340
+ 9. Browser rebuilds state cleanly from the replayed events (full replay) or continues with existing state (skip replay)
341
+
342
+ Without the `session_state_reset` message (full replay path), replayed events would duplicate existing messages in the browser's accumulated state.
343
+
344
+ **Replay status suppression**: During step 5, replayed events like `agent_start`/`agent_end` would normally trigger rapid `session_updated` broadcasts (e.g., `status: "streaming"` → `status: "idle"` for each turn), causing visible flicker on session cards. The server suppresses these status broadcasts while replaying, accumulating them in the session manager. Only the final status is broadcast after `replay_complete`. A 5-second safety timeout ensures the flag is cleared even if `replay_complete` never arrives (e.g., older bridge versions).
345
+
346
+ **Agent streaming state recovery**: The bridge tracks `isAgentStreaming` in process-level `BridgeState` (survives reload). Set `true` on `agent_start`, `false` on `agent_end`/`session_shutdown`. Since the replay doesn't include `agent_start`/`agent_end` events, the session status would otherwise stay "active" (displayed as "Waiting for input") when the agent is mid-turn during reconnect.
347
+
348
+ ### Session File Deduplication
349
+ When pi continues a session via `--session <file>`, it reuses the same JSONL file but may create a new session ID. The server detects this: when a new session registers with a `sessionFile` already associated with another session, the old session's `sessionFile` is cleared. This prevents the Resume button from loading the wrong conversation.
350
+
351
+ ### Ghost Session Cleanup
352
+ When the bridge extension is loaded multiple times (e.g., local project + global npm package), duplicate connections can create "ghost" sessions — active sessions with no sessionFile and no events. The server detects and removes these:
353
+ - **Pi gateway**: When a `session_register` changes the connection's session ID, the old session is cleaned up if it has `source: "unknown"` or no `sessionFile`
354
+ - **Event wiring**: When `session_register` arrives, any active sessions in the same cwd that have no sessionFile, no events, aren't connected, and were created within 30s are removed as ghosts
355
+
356
+ ### On-Demand Session Loading (Server-Side)
357
+ When a browser subscribes to a session whose events have been evicted from memory:
358
+ 1. Server sends empty `event_replay` with `isLast: false` to indicate loading
359
+ 2. Server's DirectoryService loads the session file directly via `SessionManager.open(sessionFile).getBranch()`
360
+ 3. Entries are converted via `replayEntriesAsEvents()` and stored in the event buffer (truncated, capped at 5000/session)
361
+ 4. Server sends `event_replay` in async batches with backpressure to all waiting browsers
362
+ 5. If the session file is missing or corrupt, server sends `dataUnavailable: true`
363
+ 6. Concurrent loads for the same session are deduplicated
364
+
365
+ ### Flows Refresh Deduplication
366
+ When a session sends `flows_list`, the server notifies other sessions in the same cwd to rediscover flows. To prevent infinite loops (A→refresh B→B sends flows→refresh A→...), a per-session 5-second cooldown (`recentFlowsRefresh` set) suppresses duplicate refresh requests.
367
+
368
+ ### Event Broadcast During Replay
369
+ During bridge session replay (while `replayingSessions` set contains the session), `event_forward` messages are stored but NOT broadcast individually to browser subscribers. Instead, when `replay_complete` arrives (or the 5s safety timeout fires), the server sends all accumulated events as a single `event_replay` batch to subscribers. This prevents per-event serialization overhead during replay while still delivering the full history to browsers.
370
+
371
+ ## Persistence
372
+
373
+ | Data | Storage | Details |
374
+ |------|---------|---------|
375
+ | Events | In-memory Map | LRU eviction, max 100 sessions. Pinned if active bridge or browser subscribers. |
376
+ | Sessions | In-memory Map + `.meta.json` | In-memory registry. Each session's state cached in per-session `.meta.json` sidecar next to `.jsonl`. On startup, `session-scanner.ts` scans `~/.pi/agent/sessions/*/` to restore all sessions from cached meta. |
377
+ | Session meta | `~/.pi/agent/sessions/…/<id>.meta.json` | Per-session sidecar: dashboard-owned state (name, attachedProposal, hidden, source) + cached stats (tokens, cost, model, status). Debounced per-session writes (max 1/sec). Stale cache detected via `cachedAt` vs `.jsonl` mtime. |
378
+ | Pinned directories | `~/.pi/dashboard/preferences.json` | Ordered array of cwd paths. Pinned dirs always visible in sidebar. |
379
+ | Session order | `~/.pi/dashboard/preferences.json` | Per-cwd ordering managed by `session-order-manager.ts`. |
380
+ | Server PID | `~/.pi/dashboard/server.pid` | Tracks running server process for daemon management. |
381
+ | Headless PIDs | `~/.pi/dashboard/headless-pids.json` | Maps spawned headless processes to sessions. Unix: `tail -f /dev/null \| pi --mode rpc` (uses tail instead of sleep to avoid stdin pipeline bug). Windows: `pi.cmd --mode rpc` with `shell: true` and quoted paths for spaces in usernames. |
382
+ | Bridge extension | `~/.pi/agent/settings.json` | On bundled installs (Electron DEB/DMG), the server auto-registers the bridge extension path in pi's global settings so all spawned pi sessions discover and load it. No-op in dev mode. |
383
+ | Session files | `~/.pi/agent/sessions/` (pi's own) | Source of truth. Bridge loads on demand. |
384
+
385
+ ## Configuration
386
+
387
+ Precedence: CLI flags → environment variables → config file (`~/.pi/dashboard/config.json`)
388
+
389
+ | Setting | Default | Description |
390
+ |---------|---------|-------------|
391
+ | `port` | 8000 | HTTP + Browser WebSocket port |
392
+ | `piPort` | 9999 | Pi extension WebSocket port |
393
+ | `autoStart` | true | Bridge extension auto-starts server if not running |
394
+ | `autoShutdown` | false | Server shuts down after idle period (disabled by default; enable for TUI auto-start scenarios) |
395
+ | `shutdownIdleSeconds` | 300 | Idle timeout before auto-shutdown |
396
+ | `spawnStrategy` | `"headless"` | How to spawn new sessions: `"headless"` or `"tmux"` |
397
+ | `tunnel.enabled` | true | Enable zrok tunnel for remote access |
398
+ | `tunnel.reservedToken` | _(auto)_ | Reserved zrok share token for persistent URL (auto-created on first run) |
399
+
400
+ ### Tunnel Lifecycle
401
+
402
+ The tunnel is **enabled by default** (`tunnel.enabled: true`). When the server starts:
403
+
404
+ 1. **Binary detection** — `detectZrokBinary()` checks if `zrok` is on PATH via `which`/`where`
405
+ 2. **Environment check** — `loadZrokEnv()` reads zrok's own config (`~/.zrok2/environment.json` or `~/.zrok/environment.json`) to verify enrollment. The dashboard never stores zrok API keys — they live entirely in zrok's config directory, created by `zrok enable <token>`.
406
+ 3. **Stale cleanup** — `cleanupStaleZrok()` reads `~/.pi/dashboard/zrok.pid`, kills orphaned zrok processes from previous crashes
407
+ 4. **Reserved share** — If `tunnel.reservedToken` is not set, `zrok reserve public` is called to create a persistent share token. The token is saved to config so the URL stays the same across restarts. If a saved token fails (e.g., expired), a new reservation is created automatically.
408
+ 5. **Subprocess spawn** — `createTunnel(port, reservedToken?)` spawns `zrok share reserved <token> --headless` (or `zrok share public --headless` as fallback) as a child process
409
+ 6. **URL parsing** — The public URL is parsed from stdout/stderr (30s timeout)
410
+ 7. **PID tracking** — The subprocess PID is written to `~/.pi/dashboard/zrok.pid`
411
+ 8. **Shutdown** — `deleteTunnel()` kills the subprocess and removes the PID file. The reserved token is preserved for next restart.
412
+
413
+ To disable: set `tunnel.enabled` to `false` in `~/.pi/dashboard/config.json` or pass `--no-tunnel` on the CLI.
414
+
415
+ The client can query `GET /api/tunnel-status` which returns `{ status: "active"|"inactive"|"unavailable", url?, serverOs }`.
416
+ The client can connect/disconnect the tunnel via `POST /api/tunnel-connect` and `POST /api/tunnel-disconnect`.
417
+
418
+ ### PWA Support
419
+
420
+ The dashboard is installable as a Progressive Web App on mobile devices:
421
+
422
+ - **Manifest** (`public/manifest.json`) — app name, icons, standalone display mode
423
+ - **Service Worker** (`public/sw.js`) — minimal fetch pass-through for installability
424
+ - **Tunnel/QR Button** — unified sidebar button: shows tunnel icon when zrok is not installed (click → setup guide), QR code icon when set up but disconnected (click → setup guide), green QR code icon when connected (click → QR dialog with disconnect and setup buttons)
425
+
426
+ | `devBuildOnReload` | false | Rebuild Vite client + restart server on `/reload` |
427
+
428
+ ## Shared Config
429
+
430
+ Both the server CLI and bridge extension read from `~/.pi/dashboard/config.json` via a shared module (`src/shared/config.ts`). On first access, the config file is auto-created with defaults.
431
+
432
+ ### Dev Mode with Production Fallback
433
+
434
+ When started with `--dev`, the server proxies client requests to the Vite dev server for HMR. If Vite is not running, it falls back to serving the production build from `dist/client/`. This means:
435
+ - `pi-dashboard start --dev` **always works** — no 502 errors
436
+ - If Vite is running → hot module replacement, fast iteration
437
+ - If Vite is not running → serves last production build silently
438
+ - Vite can be started/stopped independently without restarting the dashboard
439
+
440
+ ### Graceful Restart
441
+
442
+ The `POST /api/restart` endpoint and `pi-dashboard restart` command perform fault-tolerant restarts:
443
+ 1. Flush all pending state (meta persistence, preferences)
444
+ 2. Spawn new server process
445
+ 3. Wait for old server's port to become free (up to 10s)
446
+ 4. Start new server with the same (or overridden) flags
447
+ 5. Verify health via `/api/health` (up to 10s)
448
+ 6. `pi-dashboard stop` also kills any stale processes holding the port (via `lsof`)
449
+
450
+ The restart endpoint accepts `{ dev: boolean }` to switch between dev/production mode.
451
+
452
+ ### Auto-Start Flow
453
+
454
+ When `autoStart` is `true` (default), the bridge extension automatically starts the dashboard server:
455
+
456
+ ```
457
+ pi session_start
458
+
459
+
460
+ ensureConfig() → create ~/.pi/dashboard/config.json if missing
461
+ loadConfig() → read piPort, port, autoStart
462
+
463
+
464
+ TCP probe localhost:{piPort}
465
+
466
+ ┌────┴────┐
467
+ │ open │ closed & autoStart=true
468
+ │ │
469
+ ▼ ▼
470
+ connect spawn server (detached)
471
+ silently pass --port & --pi-port
472
+
473
+
474
+ notify user:
475
+ "🌐 Dashboard started at http://localhost:{port}"
476
+
477
+
478
+ connect
479
+ ```
480
+
481
+ The server is spawned detached (`child_process.spawn` with `detached: true`, stdout/stderr redirected to `~/.pi/dashboard/server.log`), so it outlives the pi session. If multiple pi sessions start simultaneously, duplicate spawn attempts fail harmlessly with EADDRINUSE. After a failed launch, the bridge re-probes the port — if another agent started the server concurrently, the warning is suppressed. The auto-start logic is extracted into `server-auto-start.ts` for testability.
482
+
483
+ ## mDNS Server Discovery
484
+
485
+ The dashboard uses mDNS (via `bonjour-service`) for zero-config server discovery:
486
+
487
+ ### Discovery Chain
488
+ 1. **mDNS browse** (2s timeout) — discover `_pi-dashboard._tcp` services on the local network
489
+ 2. **Health check fallback** — `GET /api/health` on configured port, verifies `{ ok: true, pid }` response
490
+ 3. **Auto-start** — if no server found and `autoStart` is enabled, spawn detached server
491
+
492
+ ### Server Advertisement
493
+ - On startup, the server publishes a `_pi-dashboard._tcp` mDNS service with TXT record: `{ version, pid, piPort }`
494
+ - On shutdown, the service is unpublished
495
+ - A continuous mDNS browser discovers peer servers and broadcasts updates to connected browsers via `servers_discovered`/`servers_updated` WebSocket messages
496
+
497
+ ### Bridge Discovery
498
+ - Bridge extensions use the mDNS discovery chain instead of bare TCP port probes
499
+ - `isDashboardRunning(port)` replaces `isPortOpen(port)` for identity-verified detection
500
+ - After auto-starting, the bridge waits up to 10s for the server's mDNS advertisement
501
+
502
+ ### Server Selector UI
503
+ - A dropdown in the sidebar header shows all discovered servers (local + LAN)
504
+ - Each entry shows hostname, port, Local/Remote badge, and connection status
505
+ - Switching closes the current WebSocket and connects to the selected server
506
+ - Last-used server persisted in `localStorage` (`pi-dashboard-last-server`)
507
+
508
+ ## Provider Authentication
509
+
510
+ The dashboard supports browser-based authentication with pi's LLM providers, enabling login from phones, tablets, or remote tunnel access without needing terminal access.
511
+
512
+ ### Flow
513
+
514
+ 1. **Settings UI** shows OAuth providers (Anthropic, Codex, GitHub Copilot, Gemini CLI, Antigravity) and API key providers
515
+ 2. **Auth-code flow** (Anthropic, Codex, Gemini, Antigravity): browser opens popup → provider consent → callback HTML relays code via `postMessage`/`BroadcastChannel`/`localStorage` → server exchanges code for tokens using PKCE
516
+ 3. **Device-code flow** (GitHub Copilot): server requests device code → UI shows user code + verification URL → server polls until authorized
517
+ 4. **API key flow**: user pastes key in Settings → saved directly
518
+ 5. All credentials written to `~/.pi/agent/auth.json` with lockfile + atomic write (`0600` permissions)
519
+ 6. Server broadcasts `credentials_updated` to all connected bridges → bridges call `authStorage.reload()` so running pi sessions pick up new tokens immediately
520
+
521
+ ### Key Files
522
+
523
+ | File | Purpose |
524
+ |------|--------|
525
+ | `src/server/provider-auth-handlers.ts` | Per-provider OAuth logic (PKCE, token exchange, project discovery) |
526
+ | `src/server/provider-auth-storage.ts` | auth.json read/write with file locking |
527
+ | `src/server/routes/provider-auth-routes.ts` | REST API for authorize, exchange, callback, device-code, API keys |
528
+ | `src/client/components/ProviderAuthSection.tsx` | Settings UI component |
529
+
530
+ ## Terminal Emulator
531
+
532
+ The dashboard includes a browser-based terminal emulator for direct shell access.
533
+
534
+ ### Architecture
535
+
536
+ ```
537
+ Browser Server
538
+ ┌────────────────┐ ┌──────────────────┐
539
+ │ xterm.js │ │ TerminalManager │
540
+ │ (per terminal)│◄──binary──►│ ├─ node-pty │
541
+ │ FitAddon │ WS │ ├─ RingBuffer │
542
+ │ AttachAddon │ │ └─ clients Set │
543
+ └────────────────┘ └──────────────────┘
544
+ ```
545
+
546
+ ### WebSocket Protocol
547
+
548
+ Each terminal has a dedicated binary WebSocket at `/ws/terminal/:id`:
549
+ - **Binary frames**: Raw terminal I/O (keystrokes client→server, PTY output server→client)
550
+ - **Text frames**: JSON control messages (`{ "type": "resize", "cols": N, "rows": N }`)
551
+
552
+ This is separate from the main JSON dashboard WebSocket (`/ws`).
553
+
554
+ ### Terminal Lifecycle
555
+
556
+ 1. Browser sends `create_terminal` on main WS → server spawns PTY via `node-pty`
557
+ 2. Server broadcasts `terminal_added` to all browsers
558
+ 3. Browser opens binary WS to `/ws/terminal/:id`, attaches `xterm.js`
559
+ 4. Shell exit → PTY `onExit` → server broadcasts `terminal_removed` → card removed
560
+
561
+ ### Output Buffering
562
+
563
+ Each terminal maintains a 256KB ring buffer of raw PTY output. When a new WebSocket connects (reconnect, new tab), the buffer is replayed before live streaming. Combined with client-side 10,000-line scrollback.
564
+
565
+ ### Keep-Alive
566
+
567
+ Terminal xterm.js instances stay mounted in the DOM (CSS hidden/shown) for instant switching without replay flicker. The binary WebSocket stays open while mounted.
568
+
569
+ ### Folder-Scoped View
570
+
571
+ Terminals are displayed in a tabbed `TerminalsView` per folder, accessed via the folder action bar's `Terminals(N)` button or `+Terminal` button. Terminal cards no longer appear in the sidebar — the sidebar shows only pi session cards. The tab bar supports switching, closing, renaming, and creating new terminals.
572
+
573
+ ## Embedded Editor (code-server)
574
+
575
+ The dashboard supports embedding VS Code in the browser via code-server.
576
+
577
+ ### Architecture
578
+
579
+ ```
580
+ Browser Dashboard Server code-server
581
+ ┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
582
+ │ EditorView │ │ EditorManager │ │ VS Code │
583
+ │ (iframe) │◄─HTTP──►│ EditorProxy │◄─HTTP──►│ :10001 │
584
+ │ │ same │ /editor/:id/* │ local │ (per folder)│
585
+ └──────────────┘ origin └─────────────────┘ └──────────────┘
586
+ ```
587
+
588
+ ### Lifecycle
589
+
590
+ 1. User clicks `Editor` button in folder action bar → navigates to `/folder/:encodedCwd/editor`
591
+ 2. `EditorView` sends `POST /api/editor/start` with `{ cwd }`
592
+ 3. `EditorManager` spawns code-server on a free port with `--auth none --bind-addr 127.0.0.1:<port>`
593
+ 4. Waits for TCP ready probe → returns `{ id, proxyPath }` → iframe loads
594
+ 5. Browser sends heartbeat every 30s → resets idle timer
595
+ 6. No heartbeat for 10 min → instance killed via SIGTERM
596
+
597
+ ### Reverse Proxy
598
+
599
+ All code-server traffic is proxied through `/editor/:id/*` on the dashboard server. This provides same-origin access (no CORS/iframe issues) and works transparently through zrok tunnels.
600
+
601
+ ### Configuration
602
+
603
+ ```json
604
+ {
605
+ "editor": {
606
+ "binary": "/usr/local/bin/code-server",
607
+ "idleTimeoutMinutes": 10,
608
+ "maxInstances": 3
609
+ }
610
+ }
611
+ ```
612
+
613
+ Binary auto-detection order: config override → `code-server` on PATH → `openvscode-server` on PATH.
614
+
615
+ ## Bundled Skill: pi-dashboard
616
+
617
+ The `.pi/skills/pi-dashboard/` directory is both a local project skill (discovered by pi from `.pi/skills/`) and shipped with the npm package (discovered via `pi.skills` in `package.json`). This means any pi session in the dashboard project or any project that installs the dashboard package gets access to the skill.
618
+
619
+ ### Session Control REST API
620
+
621
+ `src/server/session-api.ts` registers REST wrappers for operations that were previously WebSocket-only:
622
+
623
+ | Endpoint | Description |
624
+ |----------|-------------|
625
+ | `POST /api/session/:id/prompt` | Send a text prompt to a session |
626
+ | `POST /api/session/:id/abort` | Abort current operation |
627
+ | `POST /api/session/:id/shutdown` | Shutdown a pi session |
628
+ | `POST /api/session/:id/rename` | Rename a session |
629
+ | `POST /api/session/:id/hide` | Hide session |
630
+ | `POST /api/session/:id/unhide` | Unhide session |
631
+ | `POST /api/session/spawn` | Spawn new session in a directory |
632
+ | `POST /api/session/:id/resume` | Resume or fork ended session |
633
+ | `POST /api/session/:id/flow-control` | Abort flow or toggle autonomous |
634
+ | `POST /api/session/:id/model` | Set provider + model |
635
+ | `POST /api/session/:id/thinking-level` | Set thinking level |
636
+ | `POST /api/session/:id/attach-proposal` | Attach OpenSpec change |
637
+ | `POST /api/session/:id/detach-proposal` | Detach OpenSpec change |
638
+
639
+ These call the same internal methods as the browser-gateway WebSocket handlers — no duplicated logic.
640
+
641
+ ### Skill Contents
642
+
643
+ - `SKILL.md` — Auto-discovers dashboard port from `~/.pi/dashboard/config.json`, organized by capability, auth-aware
644
+ - `references/api-reference.md` — Complete REST API documentation
645
+ - `references/recipes.md` — Multi-step orchestration patterns (spawn→prompt→monitor, batch operations, health checks)
646
+ - `scripts/dashboard-api.sh` — curl wrapper with port detection, optional auth token, graceful jq fallback