@blackbelt-technology/pi-agent-dashboard 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/AGENTS.md +3 -1
  2. package/docs/architecture.md +30 -23
  3. package/package.json +8 -1
  4. package/packages/extension/package.json +1 -1
  5. package/packages/extension/src/__tests__/dashboard-default-adapter.test.ts +77 -0
  6. package/packages/extension/src/__tests__/dev-build.test.ts +2 -2
  7. package/packages/extension/src/__tests__/prompt-bus-wiring.test.ts +791 -0
  8. package/packages/extension/src/__tests__/prompt-bus.test.ts +469 -0
  9. package/packages/extension/src/__tests__/server-launcher.test.ts +35 -34
  10. package/packages/extension/src/__tests__/tui-prompt-adapter.test.ts +207 -0
  11. package/packages/extension/src/ask-user-tool.ts +1 -1
  12. package/packages/extension/src/bridge-context.ts +1 -1
  13. package/packages/extension/src/bridge.ts +214 -59
  14. package/packages/extension/src/command-handler.ts +2 -2
  15. package/packages/extension/src/dashboard-default-adapter.ts +37 -0
  16. package/packages/extension/src/flow-event-wiring.ts +6 -23
  17. package/packages/extension/src/pi-env.d.ts +13 -0
  18. package/packages/extension/src/prompt-bus.ts +240 -0
  19. package/packages/extension/src/server-launcher.ts +2 -2
  20. package/packages/extension/src/session-sync.ts +2 -1
  21. package/packages/server/package.json +1 -1
  22. package/packages/server/src/__tests__/bridge-register-nondestructive.test.ts +108 -0
  23. package/packages/server/src/__tests__/extension-register-appimage.test.ts +39 -0
  24. package/packages/server/src/__tests__/extension-register.test.ts +26 -22
  25. package/packages/server/src/__tests__/process-manager.test.ts +4 -1
  26. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +5 -5
  27. package/packages/server/src/__tests__/tunnel.test.ts +2 -2
  28. package/packages/server/src/browser-gateway.ts +55 -16
  29. package/packages/server/src/cli.ts +1 -1
  30. package/packages/server/src/editor-manager.ts +1 -1
  31. package/packages/server/src/event-status-extraction.ts +7 -0
  32. package/packages/server/src/event-wiring.ts +16 -19
  33. package/packages/server/src/package-manager-wrapper.ts +1 -1
  34. package/packages/server/src/process-manager.ts +8 -69
  35. package/packages/server/src/routes/system-routes.ts +3 -1
  36. package/packages/server/src/server.ts +6 -4
  37. package/packages/shared/package.json +1 -1
  38. package/packages/shared/src/__tests__/bridge-register.test.ts +136 -0
  39. package/packages/shared/src/__tests__/tool-resolver.test.ts +164 -0
  40. package/packages/shared/src/bridge-register.ts +95 -0
  41. package/packages/shared/src/browser-protocol.ts +10 -0
  42. package/packages/shared/src/managed-paths.ts +15 -0
  43. package/packages/shared/src/mdns-discovery.ts +1 -1
  44. package/packages/shared/src/protocol.ts +46 -0
  45. package/packages/shared/src/tool-resolver.ts +201 -0
  46. package/packages/shared/src/types.ts +24 -0
  47. package/packages/extension/src/__tests__/ui-proxy.test.ts +0 -583
  48. package/packages/extension/src/ui-proxy.ts +0 -269
  49. package/packages/server/src/extension-register.ts +0 -92
package/AGENTS.md CHANGED
@@ -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/ui-proxy.ts` | Proxies ctx.ui dialogs to dashboard (confirm/select/input/editor/notify) |
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) |
@@ -31,12 +31,12 @@ 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
- - 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)
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 emits `prompt:ctx-originals` so TUI adapters can capture original ctx.ui methods before patching
38
+ - Client-side `prompt-component-registry.ts` maps component type strings to render placement (inline, widget-bar, overlay)
39
+ - Protocol messages: `prompt_request`, `prompt_dismiss`, `prompt_cancel`, `prompt_response`
40
40
 
41
41
  ### 2. Dashboard Server (`src/server/`)
42
42
  A Node.js HTTP + WebSocket server that:
@@ -90,25 +90,32 @@ TypeScript type definitions shared across all components:
90
90
  4. Server broadcasts to all subscribed browsers via `event` message
91
91
  5. Browser's event reducer processes event, React renders update
92
92
 
93
- ### Interactive UI Flow (extension dialog → browser → response)
93
+ ### Interactive UI Flow (PromptBus — extension dialog → browser → response)
94
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
95
+ 2. Bridge PromptBus intercepts via patched `ctx.ui` methods, creates a `PromptRequest` with a unique `promptId` and `pipeline` tag (e.g. `"command"`, `"architect"`)
96
+ 3. Registered adapters claim the prompt:
97
+ - `DashboardDefaultAdapter` (always registered) returns a `PromptClaim` with `component: { type: "generic-dialog", props }` and `placement: "inline"`
98
+ - Custom adapters (e.g. `ArchitectUIAdapter` from pi-flows) can claim with custom component types and widget-bar placement
99
+ - TUI adapters (registered via `prompt:register-adapter` event) can claim to show a terminal dialog
100
+ 4. Bus sends `prompt_request` to server with the winning adapter's component info
101
+ 5. Server forwards to subscribed browsers
102
+ 6. Browser's `prompt-component-registry.ts` resolves the component type to a React renderer and placement
103
+ 7. User responds in browser → `prompt_response` sent to server → routed to bridge
104
+ 8. Bus resolves the original dialog promise and calls `onResponse()` on all adapters for cleanup
105
+
106
+ **First-response-wins (multi-adapter):**
107
+ - Multiple adapters can claim the same prompt (e.g. TUI + dashboard)
108
+ - The first adapter to respond wins; the bus sends `prompt_dismiss` to the server for the losing adapter's dashboard component
109
+ - Adapters implement `onCancel()` for cleanup when another adapter wins
110
+
111
+ **Custom UI components:**
112
+ - Extensions register adapters via `pi.events.emit("prompt:register-adapter", adapter)`
113
+ - Adapters return custom `PromptClaim` with arbitrary component types (e.g. `"architect-prompt"`)
114
+ - Client-side registry maps type strings to render placement; unknown types fall back to `"generic-dialog"`
108
115
 
109
116
  **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.
117
+ - **Page refresh**: Server replays pending `prompt_request` messages when a browser subscribes.
118
+ - **Server restart**: TODO PromptBus reconnect resend not yet implemented.
112
119
 
113
120
  ### Command Flow (browser → pi)
114
121
  1. User types prompt or command in browser
@@ -149,7 +156,7 @@ Inline stop buttons also appear on running tool cards in `ToolCallStep`, providi
149
156
  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
157
 
151
158
  **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
159
+ - 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
160
 
154
161
  **Flow launcher:**
155
162
  - Available flows detected from session commands list (heuristic: `source: "extension"`, excluding management commands)
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "@blackbelt-technology/pi-agent-dashboard",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Web dashboard for monitoring and interacting with pi agent sessions",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/BlackBeltTechnology/pi-agent-dashboard"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
5
12
  "keywords": [
6
13
  "pi-package"
7
14
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackbelt-technology/pi-dashboard-extension",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Pi bridge extension for pi-dashboard",
5
5
  "type": "module",
6
6
  "pi": {
@@ -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",