@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.
- package/AGENTS.md +342 -0
- package/README.md +619 -0
- package/docs/architecture.md +646 -0
- package/package.json +92 -0
- package/packages/extension/package.json +33 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
- package/packages/extension/src/__tests__/connection.test.ts +344 -0
- package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
- package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
- package/packages/extension/src/__tests__/git-info.test.ts +112 -0
- package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
- package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
- package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
- package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
- package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
- package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
- package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
- package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
- package/packages/extension/src/ask-user-tool.ts +63 -0
- package/packages/extension/src/bridge-context.ts +64 -0
- package/packages/extension/src/bridge.ts +926 -0
- package/packages/extension/src/command-handler.ts +538 -0
- package/packages/extension/src/connection.ts +204 -0
- package/packages/extension/src/dev-build.ts +39 -0
- package/packages/extension/src/event-forwarder.ts +40 -0
- package/packages/extension/src/flow-event-wiring.ts +102 -0
- package/packages/extension/src/git-info.ts +65 -0
- package/packages/extension/src/git-link-builder.ts +112 -0
- package/packages/extension/src/model-tracker.ts +56 -0
- package/packages/extension/src/pi-env.d.ts +23 -0
- package/packages/extension/src/process-metrics.ts +70 -0
- package/packages/extension/src/process-scanner.ts +396 -0
- package/packages/extension/src/prompt-expander.ts +87 -0
- package/packages/extension/src/provider-register.ts +276 -0
- package/packages/extension/src/server-auto-start.ts +87 -0
- package/packages/extension/src/server-launcher.ts +82 -0
- package/packages/extension/src/server-probe.ts +33 -0
- package/packages/extension/src/session-sync.ts +154 -0
- package/packages/extension/src/source-detector.ts +26 -0
- package/packages/extension/src/ui-proxy.ts +269 -0
- package/packages/extension/tsconfig.json +11 -0
- package/packages/server/package.json +37 -0
- package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
- package/packages/server/src/__tests__/auth.test.ts +224 -0
- package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
- package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
- package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
- package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
- package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
- package/packages/server/src/__tests__/config-api.test.ts +104 -0
- package/packages/server/src/__tests__/cors.test.ts +48 -0
- package/packages/server/src/__tests__/directory-service.test.ts +240 -0
- package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
- package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
- package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
- package/packages/server/src/__tests__/extension-register.test.ts +61 -0
- package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
- package/packages/server/src/__tests__/git-operations.test.ts +251 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
- package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
- package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
- package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
- package/packages/server/src/__tests__/json-store.test.ts +70 -0
- package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
- package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
- package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
- package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
- package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
- package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
- package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
- package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
- package/packages/server/src/__tests__/package-routes.test.ts +172 -0
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
- package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
- package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
- package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
- package/packages/server/src/__tests__/process-manager.test.ts +184 -0
- package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
- package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
- package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
- package/packages/server/src/__tests__/server-pid.test.ts +89 -0
- package/packages/server/src/__tests__/session-api.test.ts +244 -0
- package/packages/server/src/__tests__/session-diff.test.ts +138 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
- package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
- package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
- package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
- package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
- package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
- package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
- package/packages/server/src/__tests__/tunnel.test.ts +206 -0
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
- package/packages/server/src/auth-plugin.ts +302 -0
- package/packages/server/src/auth.ts +323 -0
- package/packages/server/src/browse.ts +55 -0
- package/packages/server/src/browser-gateway.ts +495 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
- package/packages/server/src/browser-handlers/handler-context.ts +45 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
- package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
- package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
- package/packages/server/src/cli.ts +347 -0
- package/packages/server/src/config-api.ts +130 -0
- package/packages/server/src/directory-service.ts +162 -0
- package/packages/server/src/editor-detection.ts +60 -0
- package/packages/server/src/editor-manager.ts +352 -0
- package/packages/server/src/editor-proxy.ts +134 -0
- package/packages/server/src/editor-registry.ts +108 -0
- package/packages/server/src/event-status-extraction.ts +131 -0
- package/packages/server/src/event-wiring.ts +589 -0
- package/packages/server/src/extension-register.ts +92 -0
- package/packages/server/src/git-operations.ts +200 -0
- package/packages/server/src/headless-pid-registry.ts +207 -0
- package/packages/server/src/idle-timer.ts +61 -0
- package/packages/server/src/json-store.ts +32 -0
- package/packages/server/src/localhost-guard.ts +117 -0
- package/packages/server/src/memory-event-store.ts +193 -0
- package/packages/server/src/memory-session-manager.ts +123 -0
- package/packages/server/src/meta-persistence.ts +64 -0
- package/packages/server/src/migrate-persistence.ts +195 -0
- package/packages/server/src/npm-search-proxy.ts +143 -0
- package/packages/server/src/oauth-callback-server.ts +177 -0
- package/packages/server/src/openspec-archive.ts +60 -0
- package/packages/server/src/package-manager-wrapper.ts +200 -0
- package/packages/server/src/pending-fork-registry.ts +53 -0
- package/packages/server/src/pending-load-manager.ts +110 -0
- package/packages/server/src/pending-resume-registry.ts +69 -0
- package/packages/server/src/pi-gateway.ts +419 -0
- package/packages/server/src/pi-resource-scanner.ts +369 -0
- package/packages/server/src/preferences-store.ts +116 -0
- package/packages/server/src/process-manager.ts +311 -0
- package/packages/server/src/provider-auth-handlers.ts +438 -0
- package/packages/server/src/provider-auth-storage.ts +200 -0
- package/packages/server/src/resolve-path.ts +12 -0
- package/packages/server/src/routes/editor-routes.ts +86 -0
- package/packages/server/src/routes/file-routes.ts +116 -0
- package/packages/server/src/routes/git-routes.ts +89 -0
- package/packages/server/src/routes/openspec-routes.ts +99 -0
- package/packages/server/src/routes/package-routes.ts +172 -0
- package/packages/server/src/routes/provider-auth-routes.ts +244 -0
- package/packages/server/src/routes/provider-routes.ts +101 -0
- package/packages/server/src/routes/route-deps.ts +23 -0
- package/packages/server/src/routes/session-routes.ts +91 -0
- package/packages/server/src/routes/system-routes.ts +271 -0
- package/packages/server/src/server-pid.ts +84 -0
- package/packages/server/src/server.ts +554 -0
- package/packages/server/src/session-api.ts +330 -0
- package/packages/server/src/session-bootstrap.ts +80 -0
- package/packages/server/src/session-diff.ts +178 -0
- package/packages/server/src/session-discovery.ts +134 -0
- package/packages/server/src/session-file-reader.ts +135 -0
- package/packages/server/src/session-order-manager.ts +73 -0
- package/packages/server/src/session-scanner.ts +233 -0
- package/packages/server/src/session-stats-reader.ts +99 -0
- package/packages/server/src/terminal-gateway.ts +51 -0
- package/packages/server/src/terminal-manager.ts +241 -0
- package/packages/server/src/tunnel.ts +329 -0
- package/packages/server/tsconfig.json +11 -0
- package/packages/shared/package.json +15 -0
- package/packages/shared/src/__tests__/config.test.ts +358 -0
- package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
- package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
- package/packages/shared/src/__tests__/protocol.test.ts +243 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
- package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
- package/packages/shared/src/archive-types.ts +11 -0
- package/packages/shared/src/browser-protocol.ts +534 -0
- package/packages/shared/src/config.ts +245 -0
- package/packages/shared/src/diff-types.ts +41 -0
- package/packages/shared/src/editor-types.ts +18 -0
- package/packages/shared/src/mdns-discovery.ts +248 -0
- package/packages/shared/src/openspec-activity-detector.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +96 -0
- package/packages/shared/src/protocol.ts +369 -0
- package/packages/shared/src/resolve-jiti.ts +43 -0
- package/packages/shared/src/rest-api.ts +255 -0
- package/packages/shared/src/server-identity.ts +51 -0
- package/packages/shared/src/session-meta.ts +86 -0
- package/packages/shared/src/state-replay.ts +174 -0
- package/packages/shared/src/stats-extractor.ts +54 -0
- package/packages/shared/src/terminal-types.ts +18 -0
- package/packages/shared/src/types.ts +351 -0
- 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
|