@blackbelt-technology/pi-agent-dashboard 0.2.1 → 0.2.3
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 +12 -6
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/docs/architecture.md +79 -26
- package/package.json +4 -2
- package/packages/extension/package.json +1 -1
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +50 -0
- package/packages/extension/src/__tests__/dashboard-default-adapter.test.ts +77 -0
- package/packages/extension/src/__tests__/dev-build.test.ts +2 -2
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +18 -18
- package/packages/extension/src/__tests__/prompt-bus-wiring.test.ts +791 -0
- package/packages/extension/src/__tests__/prompt-bus.test.ts +469 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +35 -34
- package/packages/extension/src/__tests__/tui-prompt-adapter.test.ts +207 -0
- package/packages/extension/src/ask-user-tool.ts +26 -6
- package/packages/extension/src/bridge-context.ts +1 -1
- package/packages/extension/src/bridge.ts +214 -59
- package/packages/extension/src/command-handler.ts +2 -2
- package/packages/extension/src/dashboard-default-adapter.ts +37 -0
- package/packages/extension/src/flow-event-wiring.ts +6 -23
- package/packages/extension/src/pi-env.d.ts +13 -0
- package/packages/extension/src/prompt-bus.ts +240 -0
- package/packages/extension/src/server-launcher.ts +2 -2
- package/packages/extension/src/session-sync.ts +2 -1
- package/packages/server/package.json +1 -1
- package/packages/server/src/__tests__/bridge-register-nondestructive.test.ts +108 -0
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +39 -0
- package/packages/server/src/__tests__/extension-register.test.ts +26 -22
- package/packages/server/src/__tests__/known-servers-routes.test.ts +129 -0
- package/packages/server/src/__tests__/process-manager.test.ts +4 -1
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +5 -5
- package/packages/server/src/__tests__/tunnel.test.ts +2 -2
- package/packages/server/src/browser-gateway.ts +55 -16
- package/packages/server/src/cli.ts +1 -1
- package/packages/server/src/editor-manager.ts +1 -1
- package/packages/server/src/event-status-extraction.ts +7 -0
- package/packages/server/src/event-wiring.ts +20 -22
- package/packages/server/src/package-manager-wrapper.ts +1 -1
- package/packages/server/src/process-manager.ts +8 -69
- package/packages/server/src/routes/known-servers-routes.ts +110 -0
- package/packages/server/src/routes/system-routes.ts +3 -1
- package/packages/server/src/server.ts +8 -4
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/bridge-register.test.ts +136 -0
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +62 -0
- package/packages/shared/src/__tests__/tool-resolver.test.ts +164 -0
- package/packages/shared/src/bridge-register.ts +95 -0
- package/packages/shared/src/browser-protocol.ts +47 -1
- package/packages/shared/src/config.ts +23 -0
- package/packages/shared/src/managed-paths.ts +15 -0
- package/packages/shared/src/mdns-discovery.ts +1 -1
- package/packages/shared/src/openspec-activity-detector.ts +8 -6
- package/packages/shared/src/protocol.ts +46 -0
- package/packages/shared/src/rest-api.ts +28 -0
- package/packages/shared/src/tool-resolver.ts +201 -0
- package/packages/shared/src/types.ts +24 -0
- package/packages/extension/src/__tests__/ui-proxy.test.ts +0 -583
- package/packages/extension/src/ui-proxy.ts +0 -269
- package/packages/server/src/extension-register.ts +0 -92
package/AGENTS.md
CHANGED
|
@@ -56,7 +56,7 @@ make clean # Destroy all cloned VMs
|
|
|
56
56
|
| File | Purpose |
|
|
57
57
|
|------|---------|
|
|
58
58
|
| `src/shared/protocol.ts` | Extension↔Server WebSocket messages |
|
|
59
|
-
| `src/shared/browser-protocol.ts` | Server↔Browser WebSocket messages |
|
|
59
|
+
| `src/shared/browser-protocol.ts` | Server↔Browser WebSocket messages (all message types including PromptBus `prompt_request`/`prompt_dismiss`/`prompt_cancel` must be in the `ServerToBrowserMessage` union — `as any` switch cases are stripped by esbuild in production) |
|
|
60
60
|
| `src/shared/types.ts` | Data models (Session, Workspace, Event) |
|
|
61
61
|
| `src/shared/config.ts` | Shared config loader (`~/.pi/dashboard/config.json`) |
|
|
62
62
|
| `src/extension/bridge.ts` | Main extension entry point (composes sync/tracker/flow modules, tracks `isAgentStreaming` in persistent BridgeState) |
|
|
@@ -83,7 +83,9 @@ make clean # Destroy all cloned VMs
|
|
|
83
83
|
| `src/client/components/BranchPicker.tsx` | Typeahead branch picker with keyboard navigation |
|
|
84
84
|
| `src/client/components/BranchSwitchDialog.tsx` | Checkout orchestration: dirty-state stash, pop prompt |
|
|
85
85
|
| `src/client/lib/git-api.ts` | Client-side fetch helpers for git API endpoints |
|
|
86
|
-
| `src/extension/
|
|
86
|
+
| `src/extension/prompt-bus.ts` | PromptBus — unified prompt routing to registered adapters (TUI, dashboard, custom). First-response-wins, cross-adapter dismissal. |
|
|
87
|
+
| `src/extension/dashboard-default-adapter.ts` | Built-in PromptBus adapter that renders prompts as generic interactive dialogs in dashboard chat |
|
|
88
|
+
| `src/client/lib/prompt-component-registry.ts` | Client-side component registry mapping prompt type strings to render metadata (placement, component) |
|
|
87
89
|
| `src/extension/ask-user-tool.ts` | `ask_user` tool registration (bundled in bridge, registered at session_start to avoid static tool-name conflicts with other extensions) |
|
|
88
90
|
| `src/shared/openspec-activity-detector.ts` | Detects OpenSpec activity from tool events; auto-attach requires only changeName (phase optional) |
|
|
89
91
|
| `src/shared/openspec-poller.ts` | OpenSpec CLI polling (shared, used by server DirectoryService) |
|
|
@@ -155,7 +157,11 @@ make clean # Destroy all cloned VMs
|
|
|
155
157
|
| `src/client/hooks/useAuthStatus.ts` | Client auth status hook and login redirect helper |
|
|
156
158
|
| `src/server/localhost-guard.ts` | Network access guard: `createNetworkGuard` (loopback/trusted/authenticated), `isBypassedHost` (CIDR/wildcard/exact), netmask-to-CIDR helpers |
|
|
157
159
|
| `src/server/server-pid.ts` | PID file management for daemon mode |
|
|
158
|
-
| `src/client/components/ServerSelector.tsx` | Server selector dropdown
|
|
160
|
+
| `src/client/components/ServerSelector.tsx` | Server selector dropdown showing persisted known servers with availability probing |
|
|
161
|
+
| `src/client/components/KnownServersSection.tsx` | Settings section: list/add/remove persisted known remote servers |
|
|
162
|
+
| `src/client/components/NetworkDiscoverySection.tsx` | Settings section: mDNS network scan with "Add" action and label prompt |
|
|
163
|
+
| `src/client/lib/known-servers-api.ts` | Client-side fetch helpers for known servers CRUD and discovery endpoints |
|
|
164
|
+
| `src/server/routes/known-servers-routes.ts` | REST routes: known servers CRUD, on-demand mDNS discovery scan |
|
|
159
165
|
| `src/server/terminal-manager.ts` | PTY lifecycle, ring buffer, spawn/attach/kill terminals |
|
|
160
166
|
| `src/server/terminal-gateway.ts` | Binary WebSocket upgrade handler for `/ws/terminal/:id` |
|
|
161
167
|
| `scripts/fix-pty-permissions.cjs` | Postinstall: fix node-pty spawn-helper execute permissions |
|
|
@@ -200,8 +206,8 @@ make clean # Destroy all cloned VMs
|
|
|
200
206
|
| `src/client/hooks/useZoomPan.ts` | Reusable zoom/pan hook (wheel, drag, pinch, buttons) |
|
|
201
207
|
| `src/client/hooks/useMessageHandler.ts` | WebSocket message dispatch hook (extracted from App.tsx) |
|
|
202
208
|
| `src/client/hooks/useSessionActions.ts` | Session action callbacks hook (send, abort, resume, spawn, etc.) |
|
|
203
|
-
| `src/client/hooks/useOpenSpecActions.ts` | OpenSpec action callbacks hook (refresh, archive, attach, detach) |
|
|
204
|
-
| `src/client/hooks/useContentViews.ts` | Content view state + fetch (pi resources, readme, file preview) |
|
|
209
|
+
| `src/client/hooks/useOpenSpecActions.ts` | OpenSpec action callbacks hook (refresh, archive, attach, detach); calls `clearAllContentViews` before opening preview |
|
|
210
|
+
| `src/client/hooks/useContentViews.ts` | Content view state + fetch (pi resources, readme, file preview); `clearAll()` resets all hook-owned states; `onBeforeOpen` callback for cross-component clearing |
|
|
205
211
|
| `src/client/lib/event-reducer.ts` | Event-sourced state reducer (delegates flow events to flow-reducer); extracts LLM errors from `agent_end` into `lastError` |
|
|
206
212
|
| `src/client/hooks/usePendingPromptTimeout.ts` | 30-second safety timeout for stuck `pendingPrompt` spinners |
|
|
207
213
|
| `src/client/lib/flow-reducer.ts` | Flow state machine: all flow_* event handling |
|
|
@@ -260,7 +266,7 @@ make clean # Destroy all cloned VMs
|
|
|
260
266
|
| `packages/electron/scripts/test-electron-install.sh` | Full e2e Docker test: install, wizard, server launch, health check |
|
|
261
267
|
| `packages/electron/scripts/test-electron-install-inner.sh` | Inner test script run inside Docker container |
|
|
262
268
|
| `packages/electron/resources/icon.png` | Master 1024×1024 app icon (π on dark navy) |
|
|
263
|
-
| `.github/workflows/
|
|
269
|
+
| `.github/workflows/publish.yml` | CI: builds DMG (macOS), DEB+AppImage (Linux), NSIS+ZIP+portable (Windows) on native runners; publishes npm + GitHub Release |
|
|
264
270
|
|
|
265
271
|
## Build & Restart Workflow
|
|
266
272
|
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Csakany
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -428,7 +428,7 @@ curl -X POST http://localhost:8000/api/restart -H 'Content-Type: application/jso
|
|
|
428
428
|
src/
|
|
429
429
|
├── shared/ # Shared TypeScript types
|
|
430
430
|
│ ├── protocol.ts # Extension ↔ Server messages
|
|
431
|
-
│ ├── browser-protocol.ts # Server ↔ Browser messages
|
|
431
|
+
│ ├── browser-protocol.ts # Server ↔ Browser messages (incl. PromptBus types)
|
|
432
432
|
│ ├── types.ts # Data models
|
|
433
433
|
│ ├── config.ts # Shared config loader
|
|
434
434
|
│ └── rest-api.ts # REST API types
|
|
@@ -534,7 +534,7 @@ Output by platform:
|
|
|
534
534
|
|----------|--------|----------|
|
|
535
535
|
| macOS | `.dmg` | `packages/electron/out/make/` |
|
|
536
536
|
| Linux | `.deb` + `.AppImage` | `packages/electron/out/make/` |
|
|
537
|
-
| Windows | `.exe` (NSIS installer) | `packages/electron/out/make/` |
|
|
537
|
+
| Windows | `.exe` (NSIS installer) + `.zip` + portable `.exe` | `packages/electron/out/make/` |
|
|
538
538
|
|
|
539
539
|
### Cross-Platform Builds (via Docker)
|
|
540
540
|
|
package/docs/architecture.md
CHANGED
|
@@ -31,12 +31,13 @@ A global pi extension that runs in every pi session. It:
|
|
|
31
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
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
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
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
34
|
+
- Routes `ctx.ui` dialog methods (confirm, select, input, editor, notify) through `PromptBus` (`prompt-bus.ts`)
|
|
35
|
+
- Adapters register to handle prompts: `DashboardDefaultAdapter` renders generic dialogs inline; extensions (e.g. pi-flows) can register custom adapters via `prompt:register-adapter` event
|
|
36
|
+
- First-response-wins: multiple adapters (TUI, dashboard, custom) can claim a prompt; the first to respond resolves it, others are dismissed
|
|
37
|
+
- Bridge's TUI adapter is registered inline (captures original `ctx.ui` methods before patching) and presents prompts in the terminal with AbortController-based cancellation
|
|
38
|
+
- Patched `ctx.ui` methods forward the `message` field (from opts) via `metadata` in the PromptBus request
|
|
39
|
+
- Client-side `prompt-component-registry.ts` maps component type strings to render placement (inline, widget-bar, overlay)
|
|
40
|
+
- Protocol messages: `prompt_request`, `prompt_dismiss`, `prompt_cancel`, `prompt_response`
|
|
40
41
|
|
|
41
42
|
### 2. Dashboard Server (`src/server/`)
|
|
42
43
|
A Node.js HTTP + WebSocket server that:
|
|
@@ -78,7 +79,7 @@ A React-based responsive web UI that:
|
|
|
78
79
|
### 4. Shared Types (`src/shared/`)
|
|
79
80
|
TypeScript type definitions shared across all components:
|
|
80
81
|
- `protocol.ts` - Extension↔Server WebSocket messages
|
|
81
|
-
- `browser-protocol.ts` - Server↔Browser WebSocket messages
|
|
82
|
+
- `browser-protocol.ts` - Server↔Browser WebSocket messages (includes PromptBus messages: `prompt_request`, `prompt_dismiss`, `prompt_cancel`)
|
|
82
83
|
- `types.ts` - Data models (Session, Workspace, Event, etc.)
|
|
83
84
|
|
|
84
85
|
## Data Flow
|
|
@@ -90,25 +91,38 @@ TypeScript type definitions shared across all components:
|
|
|
90
91
|
4. Server broadcasts to all subscribed browsers via `event` message
|
|
91
92
|
5. Browser's event reducer processes event, React renders update
|
|
92
93
|
|
|
93
|
-
### Interactive UI Flow (extension dialog → browser → response)
|
|
94
|
+
### Interactive UI Flow (PromptBus — extension dialog → browser → response)
|
|
94
95
|
1. Extension calls `ctx.ui.confirm()` / `select()` / `input()` / `editor()`
|
|
95
|
-
2. Bridge
|
|
96
|
-
3.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
96
|
+
2. Bridge PromptBus intercepts via patched `ctx.ui` methods, creates a `PromptRequest` with a unique `promptId` and `pipeline` tag (e.g. `"command"`, `"architect"`)
|
|
97
|
+
3. Registered adapters claim the prompt:
|
|
98
|
+
- `DashboardDefaultAdapter` (always registered) returns a `PromptClaim` with `component: { type: "generic-dialog", props }` and `placement: "inline"`
|
|
99
|
+
- Custom adapters (e.g. `ArchitectUIAdapter` from pi-flows) can claim with custom component types and widget-bar placement
|
|
100
|
+
- TUI adapters (registered via `prompt:register-adapter` event) can claim to show a terminal dialog
|
|
101
|
+
4. Bus sends `prompt_request` to server with the winning adapter's component info
|
|
102
|
+
5. Server forwards to subscribed browsers
|
|
103
|
+
6. Browser's `prompt-component-registry.ts` resolves the component type to a React renderer and placement
|
|
104
|
+
7. User responds in browser → `prompt_response` sent to server → routed to bridge
|
|
105
|
+
8. Bus resolves the original dialog promise and calls `onResponse()` on all adapters for cleanup
|
|
106
|
+
|
|
107
|
+
**First-response-wins (multi-adapter):**
|
|
108
|
+
- Multiple adapters can claim the same prompt (e.g. TUI + dashboard)
|
|
109
|
+
- The first adapter to respond wins; the bus sends `prompt_dismiss` to the server for the losing adapter's dashboard component
|
|
110
|
+
- Adapters implement `onCancel()` for cleanup when another adapter wins
|
|
111
|
+
|
|
112
|
+
**Custom UI components:**
|
|
113
|
+
- Extensions register adapters via `pi.events.emit("prompt:register-adapter", adapter)`
|
|
114
|
+
- Adapters return custom `PromptClaim` with arbitrary component types (e.g. `"architect-prompt"`)
|
|
115
|
+
- Client-side registry maps type strings to render placement; unknown types fall back to `"generic-dialog"`
|
|
116
|
+
|
|
117
|
+
**Message passthrough:**
|
|
118
|
+
- The `message` field from `ask_user` tool (and other `ctx.ui` callers) is forwarded via `metadata.message` in the PromptBus request, through the `prompt_request` protocol message, and extracted by the client into the interactive renderer's `params.message`.
|
|
119
|
+
|
|
120
|
+
**Type safety:**
|
|
121
|
+
- `prompt_request`, `prompt_dismiss`, and `prompt_cancel` **must** be in the `ServerToBrowserMessage` union in `browser-protocol.ts`. If they are only handled via `case "..." as any:` in switch statements, esbuild's dead-code elimination will strip the handlers in production builds, silently breaking the interactive UI.
|
|
108
122
|
|
|
109
123
|
**Resilience:**
|
|
110
|
-
- **Page refresh**: Server replays pending `
|
|
111
|
-
- **
|
|
124
|
+
- **Page refresh**: Server replays pending `prompt_request` messages when a browser subscribes. Client deduplicates by `requestId` or pending title match.
|
|
125
|
+
- **Bridge reconnect**: Bridge replays pending PromptBus requests on WebSocket reconnect so dashboard dialogs survive server restarts.
|
|
112
126
|
|
|
113
127
|
### Command Flow (browser → pi)
|
|
114
128
|
1. User types prompt or command in browser
|
|
@@ -149,7 +163,7 @@ Inline stop buttons also appear on running tool cards in `ToolCallStep`, providi
|
|
|
149
163
|
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
164
|
|
|
151
165
|
**Fork decisions and subagent ask_user:**
|
|
152
|
-
-
|
|
166
|
+
- Work through PromptBus — `TuiFlowIOAdapter` calls `ctx.ui.select/confirm/input` which the bridge routes through the bus to registered adapters (dashboard, TUI, or custom)
|
|
153
167
|
|
|
154
168
|
**Flow launcher:**
|
|
155
169
|
- Available flows detected from session commands list (heuristic: `source: "extension"`, excluding management commands)
|
|
@@ -279,6 +293,18 @@ The web client includes a generic `MarkdownPreviewView` component that replaces
|
|
|
279
293
|
### Archive Browser
|
|
280
294
|
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
295
|
|
|
296
|
+
### Content View Management
|
|
297
|
+
|
|
298
|
+
The content area (right panel) shows one view at a time: ChatView, ArchiveBrowserView, SpecsBrowserView, PiResourcesView, MarkdownPreviewView (readme, pi resource file, flow YAML, OpenSpec artifact), FileDiffView, FlowArchitectDetail, or FlowAgentDetail. Each view is controlled by independent state in `App.tsx` and `useContentViews`. A priority chain in the JSX determines which view renders (first truthy state wins).
|
|
299
|
+
|
|
300
|
+
**Mutual exclusivity**: A `clearAllContentViews()` helper resets all content view states. It is called before opening any new content view, ensuring the previous view is always dismissed. This combines `clearAppContentViews()` (App-level states: preview, specs browser, archive browser, diff view, flow YAML, architect detail, flow agent detail) with `clearContentViews()` from `useContentViews` (pi resources, pi resource file preview, readme preview).
|
|
301
|
+
|
|
302
|
+
**Session switch**: When the selected session changes, `clearAllContentViews()` is called to dismiss any open content view.
|
|
303
|
+
|
|
304
|
+
**Sub-navigation**: `handleViewPiResourceFile` (viewing a file within PiResourcesView) does not clear other views — it's sub-navigation within an already-active content view.
|
|
305
|
+
|
|
306
|
+
**`onBeforeOpen` callback**: `useContentViews` accepts an optional `onBeforeOpen` callback. When `handleOpenPiResources` or `handleViewReadme` opens a new top-level view, it calls `onBeforeOpen` first so App.tsx can clear its own states, then clears the hook's sibling states internally.
|
|
307
|
+
|
|
282
308
|
### Network Access Control
|
|
283
309
|
|
|
284
310
|
The server has a two-layer access model:
|
|
@@ -499,12 +525,26 @@ The dashboard uses mDNS (via `bonjour-service`) for zero-config server discovery
|
|
|
499
525
|
- `isDashboardRunning(port)` replaces `isPortOpen(port)` for identity-verified detection
|
|
500
526
|
- After auto-starting, the bridge waits up to 10s for the server's mDNS advertisement
|
|
501
527
|
|
|
528
|
+
### Known Servers
|
|
529
|
+
- Users can persist remote servers in `config.json` via `knownServers: KnownServer[]`
|
|
530
|
+
- Each entry has `host`, `port`, optional `label`, and `addedAt` timestamp
|
|
531
|
+
- REST API: `GET/POST/DELETE /api/known-servers` for CRUD, `POST /api/discover-servers` for on-demand mDNS scan
|
|
532
|
+
- Localhost is always implicitly available (not stored)
|
|
533
|
+
- The data model is extensible for future key exchange / auth tokens
|
|
534
|
+
|
|
502
535
|
### Server Selector UI
|
|
503
|
-
-
|
|
504
|
-
- Each entry shows hostname, port, Local/Remote badge, and
|
|
536
|
+
- The header dropdown shows persisted known servers (from config) plus localhost, not raw mDNS results
|
|
537
|
+
- Each entry shows label (or hostname), host:port, Local/Remote badge, and availability status
|
|
538
|
+
- Non-current servers are probed via health check when the dropdown opens
|
|
505
539
|
- Switching closes the current WebSocket and connects to the selected server
|
|
506
540
|
- Last-used server persisted in `localStorage` (`pi-dashboard-last-server`)
|
|
507
541
|
|
|
542
|
+
### Server Management (Settings Panel)
|
|
543
|
+
- **Known Servers section**: lists persisted servers with remove buttons and an inline add form (host, port, label)
|
|
544
|
+
- **Network Discovery section**: "Scan network" triggers `POST /api/discover-servers`, shows results with "Add" button that prompts for a label
|
|
545
|
+
- Already-known servers show "Already added" badge in discovery results
|
|
546
|
+
- Electron loading page shows known servers as fallback when primary server is unreachable
|
|
547
|
+
|
|
508
548
|
## Provider Authentication
|
|
509
549
|
|
|
510
550
|
The dashboard supports browser-based authentication with pi's LLM providers, enabling login from phones, tablets, or remote tunnel access without needing terminal access.
|
|
@@ -612,6 +652,19 @@ All code-server traffic is proxied through `/editor/:id/*` on the dashboard serv
|
|
|
612
652
|
|
|
613
653
|
Binary auto-detection order: config override → `code-server` on PATH → `openvscode-server` on PATH.
|
|
614
654
|
|
|
655
|
+
### Known Servers Configuration
|
|
656
|
+
|
|
657
|
+
```json
|
|
658
|
+
{
|
|
659
|
+
"knownServers": [
|
|
660
|
+
{ "host": "office-mac.local", "port": 8000, "label": "Office Mac", "addedAt": "2024-01-15T10:30:00Z" },
|
|
661
|
+
{ "host": "build-server", "port": 8000, "addedAt": "2024-01-20T14:00:00Z" }
|
|
662
|
+
]
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Managed via REST API (`/api/known-servers`) or Settings panel. Localhost is always implicit.
|
|
667
|
+
|
|
615
668
|
## Bundled Skill: pi-dashboard
|
|
616
669
|
|
|
617
670
|
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.
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackbelt-technology/pi-agent-dashboard",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Web dashboard for monitoring and interacting with pi agent sessions",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/BlackBeltTechnology/pi-agent-dashboard"
|
|
8
8
|
},
|
|
9
|
+
"license": "MIT",
|
|
9
10
|
"publishConfig": {
|
|
10
11
|
"access": "public"
|
|
11
12
|
},
|
|
@@ -41,7 +42,8 @@
|
|
|
41
42
|
"packages/dist/",
|
|
42
43
|
"docs/architecture.md",
|
|
43
44
|
"AGENTS.md",
|
|
44
|
-
"README.md"
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
45
47
|
],
|
|
46
48
|
"scripts": {
|
|
47
49
|
"dev": "npm run dev --workspace=@blackbelt-technology/pi-dashboard-web",
|
|
@@ -81,5 +81,55 @@ describe("registerAskUserTool", () => {
|
|
|
81
81
|
await tool.execute("id", { method: "input", title: "Q" }, undefined, undefined, ctx);
|
|
82
82
|
expect(ctx.ui.input).toHaveBeenCalledWith("Q", undefined, undefined);
|
|
83
83
|
});
|
|
84
|
+
|
|
85
|
+
it("falls back to message when title is missing", async () => {
|
|
86
|
+
const { tool, ctx } = getToolAndMockCtx();
|
|
87
|
+
await tool.execute("id", { method: "input", message: "Detailed question" }, undefined, undefined, ctx);
|
|
88
|
+
expect(ctx.ui.input).toHaveBeenCalledWith("Detailed question", undefined, { message: "Detailed question" });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("falls back to 'Question' when both title and message are missing", async () => {
|
|
92
|
+
const { tool, ctx } = getToolAndMockCtx();
|
|
93
|
+
await tool.execute("id", { method: "confirm" }, undefined, undefined, ctx);
|
|
94
|
+
expect(ctx.ui.confirm).toHaveBeenCalledWith("Question", "");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("parses options from JSON string", async () => {
|
|
98
|
+
const { tool, ctx } = getToolAndMockCtx();
|
|
99
|
+
await tool.execute("id", { method: "select", title: "Pick", options: '["A", "B"]' }, undefined, undefined, ctx);
|
|
100
|
+
expect(ctx.ui.select).toHaveBeenCalledWith("Pick", ["A", "B"], undefined);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("handles malformed options string gracefully", async () => {
|
|
104
|
+
const { tool, ctx } = getToolAndMockCtx();
|
|
105
|
+
await tool.execute("id", { method: "select", title: "Pick", options: "not json" }, undefined, undefined, ctx);
|
|
106
|
+
expect(ctx.ui.select).toHaveBeenCalledWith("Pick", [], undefined);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("prepareArguments", () => {
|
|
111
|
+
function getTool() {
|
|
112
|
+
const pi = createMockPi();
|
|
113
|
+
registerAskUserTool(pi as any);
|
|
114
|
+
return pi.registerTool.mock.calls[0][0];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
it("parses stringified options array", () => {
|
|
118
|
+
const tool = getTool();
|
|
119
|
+
const result = tool.prepareArguments({ method: "select", title: "Pick", options: '["A", "B"]' });
|
|
120
|
+
expect(result.options).toEqual(["A", "B"]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("leaves real array options unchanged", () => {
|
|
124
|
+
const tool = getTool();
|
|
125
|
+
const result = tool.prepareArguments({ method: "select", title: "Pick", options: ["A", "B"] });
|
|
126
|
+
expect(result.options).toEqual(["A", "B"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("leaves malformed string as-is", () => {
|
|
130
|
+
const tool = getTool();
|
|
131
|
+
const result = tool.prepareArguments({ method: "select", title: "Pick", options: "not json" });
|
|
132
|
+
expect(result.options).toBe("not json");
|
|
133
|
+
});
|
|
84
134
|
});
|
|
85
135
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { DashboardDefaultAdapter } from "../dashboard-default-adapter.js";
|
|
3
|
+
import type { PromptRequest } from "../prompt-bus.js";
|
|
4
|
+
|
|
5
|
+
function makePrompt(overrides: Partial<PromptRequest> = {}): PromptRequest {
|
|
6
|
+
return {
|
|
7
|
+
id: "test-1",
|
|
8
|
+
pipeline: "command",
|
|
9
|
+
type: "select",
|
|
10
|
+
question: "Pick one:",
|
|
11
|
+
options: ["A", "B"],
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("DashboardDefaultAdapter", () => {
|
|
17
|
+
it("has name 'dashboard-default'", () => {
|
|
18
|
+
const adapter = new DashboardDefaultAdapter();
|
|
19
|
+
expect(adapter.name).toBe("dashboard-default");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("claims all prompts with generic-dialog component", () => {
|
|
23
|
+
const adapter = new DashboardDefaultAdapter();
|
|
24
|
+
const claim = adapter.onRequest(makePrompt());
|
|
25
|
+
|
|
26
|
+
expect(claim).toEqual({
|
|
27
|
+
component: {
|
|
28
|
+
type: "generic-dialog",
|
|
29
|
+
props: {
|
|
30
|
+
question: "Pick one:",
|
|
31
|
+
type: "select",
|
|
32
|
+
options: ["A", "B"],
|
|
33
|
+
defaultValue: undefined,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
placement: "inline",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("claims input prompts with correct props", () => {
|
|
41
|
+
const adapter = new DashboardDefaultAdapter();
|
|
42
|
+
const claim = adapter.onRequest(makePrompt({
|
|
43
|
+
type: "input",
|
|
44
|
+
question: "Name:",
|
|
45
|
+
options: undefined,
|
|
46
|
+
defaultValue: "default",
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
expect(claim.component!.type).toBe("generic-dialog");
|
|
50
|
+
expect(claim.component!.props.type).toBe("input");
|
|
51
|
+
expect(claim.component!.props.defaultValue).toBe("default");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("claims confirm prompts", () => {
|
|
55
|
+
const adapter = new DashboardDefaultAdapter();
|
|
56
|
+
const claim = adapter.onRequest(makePrompt({ type: "confirm", question: "Sure?" }));
|
|
57
|
+
|
|
58
|
+
expect(claim.component!.type).toBe("generic-dialog");
|
|
59
|
+
expect(claim.component!.props.type).toBe("confirm");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("placement is always inline", () => {
|
|
63
|
+
const adapter = new DashboardDefaultAdapter();
|
|
64
|
+
const claim = adapter.onRequest(makePrompt({ pipeline: "architect-new" }));
|
|
65
|
+
expect(claim.placement).toBe("inline");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("onResponse does not throw", () => {
|
|
69
|
+
const adapter = new DashboardDefaultAdapter();
|
|
70
|
+
expect(() => adapter.onResponse({ id: "x", answer: "A", source: "tui" })).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("onCancel does not throw", () => {
|
|
74
|
+
const adapter = new DashboardDefaultAdapter();
|
|
75
|
+
expect(() => adapter.onCancel("x")).not.toThrow();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -42,7 +42,7 @@ describe("runDevBuild", () => {
|
|
|
42
42
|
it("should log progress messages", () => {
|
|
43
43
|
run();
|
|
44
44
|
|
|
45
|
-
const logs = logSpy.mock.calls.map((c) => c[0]);
|
|
45
|
+
const logs = logSpy.mock.calls.map((c: any) => c[0]);
|
|
46
46
|
expect(logs).toContain("🔨 Dashboard: building client...");
|
|
47
47
|
expect(logs).toContain("✅ Dashboard: client built");
|
|
48
48
|
expect(logs).toContain("🛑 Dashboard: stopping server...");
|
|
@@ -54,7 +54,7 @@ describe("runDevBuild", () => {
|
|
|
54
54
|
|
|
55
55
|
run();
|
|
56
56
|
|
|
57
|
-
const logs = logSpy.mock.calls.map((c) => c[0]);
|
|
57
|
+
const logs = logSpy.mock.calls.map((c: any) => c[0]);
|
|
58
58
|
expect(logs).toContain("❌ Dashboard: build failed — build error");
|
|
59
59
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
60
60
|
"http://localhost:8000/api/shutdown",
|
|
@@ -93,28 +93,28 @@ describe("detectOpenSpecActivity", () => {
|
|
|
93
93
|
const result = detectOpenSpecActivity("bash", {
|
|
94
94
|
command: 'openspec status --change "session-sync" --json',
|
|
95
95
|
});
|
|
96
|
-
expect(result).toEqual({ changeName: "session-sync" });
|
|
96
|
+
expect(result).toEqual({ changeName: "session-sync", isActive: true });
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
it("detects change name from openspec instructions command", () => {
|
|
100
100
|
const result = detectOpenSpecActivity("bash", {
|
|
101
101
|
command: 'openspec instructions apply --change "my-feature" --json',
|
|
102
102
|
});
|
|
103
|
-
expect(result).toEqual({ changeName: "my-feature" });
|
|
103
|
+
expect(result).toEqual({ changeName: "my-feature", isActive: true });
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
it("detects change name without quotes", () => {
|
|
107
107
|
const result = detectOpenSpecActivity("bash", {
|
|
108
108
|
command: "openspec status --change session-sync --json",
|
|
109
109
|
});
|
|
110
|
-
expect(result).toEqual({ changeName: "session-sync" });
|
|
110
|
+
expect(result).toEqual({ changeName: "session-sync", isActive: true });
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
it("detects change name from openspec archive command", () => {
|
|
114
114
|
const result = detectOpenSpecActivity("bash", {
|
|
115
115
|
command: "openspec archive session-sync",
|
|
116
116
|
});
|
|
117
|
-
expect(result).toEqual({ changeName: "session-sync" });
|
|
117
|
+
expect(result).toEqual({ changeName: "session-sync", isActive: true });
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
it("returns null for non-openspec bash commands", () => {
|
|
@@ -128,21 +128,21 @@ describe("detectOpenSpecActivity", () => {
|
|
|
128
128
|
const result = detectOpenSpecActivity("bash", {
|
|
129
129
|
command: 'openspec new change "add-auth"',
|
|
130
130
|
});
|
|
131
|
-
expect(result).toEqual({ changeName: "add-auth" });
|
|
131
|
+
expect(result).toEqual({ changeName: "add-auth", isActive: true });
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
it("detects change name from openspec new change with unquoted name", () => {
|
|
135
135
|
const result = detectOpenSpecActivity("bash", {
|
|
136
136
|
command: "openspec new change add-auth",
|
|
137
137
|
});
|
|
138
|
-
expect(result).toEqual({ changeName: "add-auth" });
|
|
138
|
+
expect(result).toEqual({ changeName: "add-auth", isActive: true });
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
it("detects change name from openspec new change with cd prefix", () => {
|
|
142
142
|
const result = detectOpenSpecActivity("bash", {
|
|
143
143
|
command: 'cd /Users/dev/project && openspec new change "my-feature"',
|
|
144
144
|
});
|
|
145
|
-
expect(result).toEqual({ changeName: "my-feature" });
|
|
145
|
+
expect(result).toEqual({ changeName: "my-feature", isActive: true });
|
|
146
146
|
});
|
|
147
147
|
|
|
148
148
|
it("returns null for openspec list (no change name)", () => {
|
|
@@ -154,18 +154,18 @@ describe("detectOpenSpecActivity", () => {
|
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
describe("change name detection from file reads", () => {
|
|
157
|
-
it("detects change name from openspec change file read", () => {
|
|
157
|
+
it("detects change name from openspec change file read as passive", () => {
|
|
158
158
|
const result = detectOpenSpecActivity("read", {
|
|
159
159
|
path: "openspec/changes/session-sync/tasks.md",
|
|
160
160
|
});
|
|
161
|
-
expect(result).toEqual({ changeName: "session-sync" });
|
|
161
|
+
expect(result).toEqual({ changeName: "session-sync", isActive: false });
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
it("detects change name from absolute path", () => {
|
|
164
|
+
it("detects change name from absolute path as passive", () => {
|
|
165
165
|
const result = detectOpenSpecActivity("read", {
|
|
166
166
|
path: "/Users/dev/project/openspec/changes/my-feature/proposal.md",
|
|
167
167
|
});
|
|
168
|
-
expect(result).toEqual({ changeName: "my-feature" });
|
|
168
|
+
expect(result).toEqual({ changeName: "my-feature", isActive: false });
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
it("returns null for non-openspec file reads", () => {
|
|
@@ -177,18 +177,18 @@ describe("detectOpenSpecActivity", () => {
|
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
describe("change name detection from file writes", () => {
|
|
180
|
-
it("detects change name from openspec change file write", () => {
|
|
180
|
+
it("detects change name from openspec change file write as active", () => {
|
|
181
181
|
const result = detectOpenSpecActivity("write", {
|
|
182
182
|
path: "openspec/changes/session-sync/proposal.md",
|
|
183
183
|
});
|
|
184
|
-
expect(result).toEqual({ changeName: "session-sync" });
|
|
184
|
+
expect(result).toEqual({ changeName: "session-sync", isActive: true });
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
it("detects change name from absolute path write", () => {
|
|
187
|
+
it("detects change name from absolute path write as active", () => {
|
|
188
188
|
const result = detectOpenSpecActivity("write", {
|
|
189
189
|
path: "/Users/dev/project/openspec/changes/my-feature/spec.md",
|
|
190
190
|
});
|
|
191
|
-
expect(result).toEqual({ changeName: "my-feature" });
|
|
191
|
+
expect(result).toEqual({ changeName: "my-feature", isActive: true });
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
it("returns null for non-openspec file writes", () => {
|
|
@@ -203,15 +203,15 @@ describe("detectOpenSpecActivity", () => {
|
|
|
203
203
|
it("handles capitalized tool names (backward compatibility)", () => {
|
|
204
204
|
expect(detectOpenSpecActivity("Read", {
|
|
205
205
|
path: "openspec/changes/my-feature/proposal.md",
|
|
206
|
-
})).toEqual({ changeName: "my-feature" });
|
|
206
|
+
})).toEqual({ changeName: "my-feature", isActive: false });
|
|
207
207
|
|
|
208
208
|
expect(detectOpenSpecActivity("Bash", {
|
|
209
209
|
command: 'openspec status --change "add-auth" --json',
|
|
210
|
-
})).toEqual({ changeName: "add-auth" });
|
|
210
|
+
})).toEqual({ changeName: "add-auth", isActive: true });
|
|
211
211
|
|
|
212
212
|
expect(detectOpenSpecActivity("Write", {
|
|
213
213
|
path: "openspec/changes/my-feature/design.md",
|
|
214
|
-
})).toEqual({ changeName: "my-feature" });
|
|
214
|
+
})).toEqual({ changeName: "my-feature", isActive: true });
|
|
215
215
|
});
|
|
216
216
|
|
|
217
217
|
it("returns null for unknown tool names", () => {
|