@blackbelt-technology/pi-agent-dashboard 0.2.1 → 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.
- package/AGENTS.md +3 -1
- package/docs/architecture.md +30 -23
- package/package.json +1 -1
- package/packages/extension/package.json +1 -1
- 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__/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 +1 -1
- 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__/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 +16 -19
- package/packages/server/src/package-manager-wrapper.ts +1 -1
- package/packages/server/src/process-manager.ts +8 -69
- package/packages/server/src/routes/system-routes.ts +3 -1
- package/packages/server/src/server.ts +6 -4
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/bridge-register.test.ts +136 -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 +10 -0
- package/packages/shared/src/managed-paths.ts +15 -0
- package/packages/shared/src/mdns-discovery.ts +1 -1
- package/packages/shared/src/protocol.ts +46 -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
|
@@ -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) |
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
-
-
|
|
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 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
|
|
96
|
-
3.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
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 `
|
|
111
|
-
- **Server restart**:
|
|
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
|
-
-
|
|
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
|
@@ -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",
|