@blackbelt-technology/pi-agent-dashboard 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/AGENTS.md +342 -0
  2. package/README.md +619 -0
  3. package/docs/architecture.md +646 -0
  4. package/package.json +92 -0
  5. package/packages/extension/package.json +33 -0
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
  7. package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
  8. package/packages/extension/src/__tests__/connection.test.ts +344 -0
  9. package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
  10. package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
  11. package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
  12. package/packages/extension/src/__tests__/git-info.test.ts +112 -0
  13. package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
  14. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
  15. package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
  16. package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
  17. package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
  18. package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
  19. package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
  20. package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
  21. package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
  22. package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
  23. package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
  24. package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
  25. package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
  26. package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
  27. package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
  28. package/packages/extension/src/ask-user-tool.ts +63 -0
  29. package/packages/extension/src/bridge-context.ts +64 -0
  30. package/packages/extension/src/bridge.ts +926 -0
  31. package/packages/extension/src/command-handler.ts +538 -0
  32. package/packages/extension/src/connection.ts +204 -0
  33. package/packages/extension/src/dev-build.ts +39 -0
  34. package/packages/extension/src/event-forwarder.ts +40 -0
  35. package/packages/extension/src/flow-event-wiring.ts +102 -0
  36. package/packages/extension/src/git-info.ts +65 -0
  37. package/packages/extension/src/git-link-builder.ts +112 -0
  38. package/packages/extension/src/model-tracker.ts +56 -0
  39. package/packages/extension/src/pi-env.d.ts +23 -0
  40. package/packages/extension/src/process-metrics.ts +70 -0
  41. package/packages/extension/src/process-scanner.ts +396 -0
  42. package/packages/extension/src/prompt-expander.ts +87 -0
  43. package/packages/extension/src/provider-register.ts +276 -0
  44. package/packages/extension/src/server-auto-start.ts +87 -0
  45. package/packages/extension/src/server-launcher.ts +82 -0
  46. package/packages/extension/src/server-probe.ts +33 -0
  47. package/packages/extension/src/session-sync.ts +154 -0
  48. package/packages/extension/src/source-detector.ts +26 -0
  49. package/packages/extension/src/ui-proxy.ts +269 -0
  50. package/packages/extension/tsconfig.json +11 -0
  51. package/packages/server/package.json +37 -0
  52. package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
  53. package/packages/server/src/__tests__/auth.test.ts +224 -0
  54. package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
  55. package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
  56. package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
  57. package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
  58. package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
  59. package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
  60. package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
  61. package/packages/server/src/__tests__/config-api.test.ts +104 -0
  62. package/packages/server/src/__tests__/cors.test.ts +48 -0
  63. package/packages/server/src/__tests__/directory-service.test.ts +240 -0
  64. package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
  65. package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
  66. package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
  67. package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
  68. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
  69. package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
  70. package/packages/server/src/__tests__/extension-register.test.ts +61 -0
  71. package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
  72. package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
  73. package/packages/server/src/__tests__/git-operations.test.ts +251 -0
  74. package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
  75. package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
  76. package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
  77. package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
  78. package/packages/server/src/__tests__/json-store.test.ts +70 -0
  79. package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
  80. package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
  81. package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
  82. package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
  83. package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
  84. package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
  85. package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
  86. package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
  87. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
  88. package/packages/server/src/__tests__/package-routes.test.ts +172 -0
  89. package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
  90. package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
  91. package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
  92. package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
  93. package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
  94. package/packages/server/src/__tests__/process-manager.test.ts +184 -0
  95. package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
  96. package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
  97. package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
  98. package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
  99. package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
  100. package/packages/server/src/__tests__/server-pid.test.ts +89 -0
  101. package/packages/server/src/__tests__/session-api.test.ts +244 -0
  102. package/packages/server/src/__tests__/session-diff.test.ts +138 -0
  103. package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
  104. package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
  105. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
  106. package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
  107. package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
  108. package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
  109. package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
  110. package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
  111. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
  112. package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
  113. package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
  114. package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
  115. package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
  116. package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
  117. package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
  118. package/packages/server/src/__tests__/tunnel.test.ts +206 -0
  119. package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
  120. package/packages/server/src/auth-plugin.ts +302 -0
  121. package/packages/server/src/auth.ts +323 -0
  122. package/packages/server/src/browse.ts +55 -0
  123. package/packages/server/src/browser-gateway.ts +495 -0
  124. package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
  125. package/packages/server/src/browser-handlers/handler-context.ts +45 -0
  126. package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
  127. package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
  128. package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
  129. package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
  130. package/packages/server/src/cli.ts +347 -0
  131. package/packages/server/src/config-api.ts +130 -0
  132. package/packages/server/src/directory-service.ts +162 -0
  133. package/packages/server/src/editor-detection.ts +60 -0
  134. package/packages/server/src/editor-manager.ts +352 -0
  135. package/packages/server/src/editor-proxy.ts +134 -0
  136. package/packages/server/src/editor-registry.ts +108 -0
  137. package/packages/server/src/event-status-extraction.ts +131 -0
  138. package/packages/server/src/event-wiring.ts +589 -0
  139. package/packages/server/src/extension-register.ts +92 -0
  140. package/packages/server/src/git-operations.ts +200 -0
  141. package/packages/server/src/headless-pid-registry.ts +207 -0
  142. package/packages/server/src/idle-timer.ts +61 -0
  143. package/packages/server/src/json-store.ts +32 -0
  144. package/packages/server/src/localhost-guard.ts +117 -0
  145. package/packages/server/src/memory-event-store.ts +193 -0
  146. package/packages/server/src/memory-session-manager.ts +123 -0
  147. package/packages/server/src/meta-persistence.ts +64 -0
  148. package/packages/server/src/migrate-persistence.ts +195 -0
  149. package/packages/server/src/npm-search-proxy.ts +143 -0
  150. package/packages/server/src/oauth-callback-server.ts +177 -0
  151. package/packages/server/src/openspec-archive.ts +60 -0
  152. package/packages/server/src/package-manager-wrapper.ts +200 -0
  153. package/packages/server/src/pending-fork-registry.ts +53 -0
  154. package/packages/server/src/pending-load-manager.ts +110 -0
  155. package/packages/server/src/pending-resume-registry.ts +69 -0
  156. package/packages/server/src/pi-gateway.ts +419 -0
  157. package/packages/server/src/pi-resource-scanner.ts +369 -0
  158. package/packages/server/src/preferences-store.ts +116 -0
  159. package/packages/server/src/process-manager.ts +311 -0
  160. package/packages/server/src/provider-auth-handlers.ts +438 -0
  161. package/packages/server/src/provider-auth-storage.ts +200 -0
  162. package/packages/server/src/resolve-path.ts +12 -0
  163. package/packages/server/src/routes/editor-routes.ts +86 -0
  164. package/packages/server/src/routes/file-routes.ts +116 -0
  165. package/packages/server/src/routes/git-routes.ts +89 -0
  166. package/packages/server/src/routes/openspec-routes.ts +99 -0
  167. package/packages/server/src/routes/package-routes.ts +172 -0
  168. package/packages/server/src/routes/provider-auth-routes.ts +244 -0
  169. package/packages/server/src/routes/provider-routes.ts +101 -0
  170. package/packages/server/src/routes/route-deps.ts +23 -0
  171. package/packages/server/src/routes/session-routes.ts +91 -0
  172. package/packages/server/src/routes/system-routes.ts +271 -0
  173. package/packages/server/src/server-pid.ts +84 -0
  174. package/packages/server/src/server.ts +554 -0
  175. package/packages/server/src/session-api.ts +330 -0
  176. package/packages/server/src/session-bootstrap.ts +80 -0
  177. package/packages/server/src/session-diff.ts +178 -0
  178. package/packages/server/src/session-discovery.ts +134 -0
  179. package/packages/server/src/session-file-reader.ts +135 -0
  180. package/packages/server/src/session-order-manager.ts +73 -0
  181. package/packages/server/src/session-scanner.ts +233 -0
  182. package/packages/server/src/session-stats-reader.ts +99 -0
  183. package/packages/server/src/terminal-gateway.ts +51 -0
  184. package/packages/server/src/terminal-manager.ts +241 -0
  185. package/packages/server/src/tunnel.ts +329 -0
  186. package/packages/server/tsconfig.json +11 -0
  187. package/packages/shared/package.json +15 -0
  188. package/packages/shared/src/__tests__/config.test.ts +358 -0
  189. package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
  190. package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
  191. package/packages/shared/src/__tests__/protocol.test.ts +243 -0
  192. package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
  193. package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
  194. package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
  195. package/packages/shared/src/archive-types.ts +11 -0
  196. package/packages/shared/src/browser-protocol.ts +534 -0
  197. package/packages/shared/src/config.ts +245 -0
  198. package/packages/shared/src/diff-types.ts +41 -0
  199. package/packages/shared/src/editor-types.ts +18 -0
  200. package/packages/shared/src/mdns-discovery.ts +248 -0
  201. package/packages/shared/src/openspec-activity-detector.ts +109 -0
  202. package/packages/shared/src/openspec-poller.ts +96 -0
  203. package/packages/shared/src/protocol.ts +369 -0
  204. package/packages/shared/src/resolve-jiti.ts +43 -0
  205. package/packages/shared/src/rest-api.ts +255 -0
  206. package/packages/shared/src/server-identity.ts +51 -0
  207. package/packages/shared/src/session-meta.ts +86 -0
  208. package/packages/shared/src/state-replay.ts +174 -0
  209. package/packages/shared/src/stats-extractor.ts +54 -0
  210. package/packages/shared/src/terminal-types.ts +18 -0
  211. package/packages/shared/src/types.ts +351 -0
  212. package/packages/shared/tsconfig.json +8 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * WebSocket connection manager with exponential backoff reconnection
3
+ * and message buffering during disconnection.
4
+ */
5
+
6
+ export interface ConnectionManagerOptions {
7
+ url: string;
8
+ WebSocketImpl?: any;
9
+ maxBufferSize?: number;
10
+ /** Server liveness watchdog: force reconnect after this many ms without any received message. Default 60000. Set 0 to disable. */
11
+ watchdogTimeout?: number;
12
+ onMessage?: (data: unknown) => void;
13
+ onReconnect?: () => void;
14
+ }
15
+
16
+ export class ConnectionManager {
17
+ private url: string;
18
+ private WS: any;
19
+ private ws: any | null = null;
20
+ private buffer: string[] = [];
21
+ private maxBufferSize: number;
22
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
23
+ private backoff = 0;
24
+ private intentionalClose = false;
25
+ private hasConnectedBefore = false;
26
+ private onMessage?: (data: unknown) => void;
27
+ private onReconnect?: () => void;
28
+
29
+ private static readonly INITIAL_BACKOFF = 1000;
30
+ private static readonly MAX_BACKOFF = 30000;
31
+ private static readonly WATCHDOG_CHECK_INTERVAL = 15_000;
32
+ private static readonly DEFAULT_WATCHDOG_TIMEOUT = 60_000;
33
+
34
+ private lastMessageAt = 0;
35
+ private watchdogTimer: ReturnType<typeof setInterval> | null = null;
36
+ private watchdogTimeout: number;
37
+
38
+ constructor(options: ConnectionManagerOptions) {
39
+ this.url = options.url;
40
+ this.WS = options.WebSocketImpl ?? (globalThis as any).WebSocket;
41
+ this.maxBufferSize = options.maxBufferSize ?? 10000;
42
+ this.watchdogTimeout = options.watchdogTimeout ?? ConnectionManager.DEFAULT_WATCHDOG_TIMEOUT;
43
+ this.onMessage = options.onMessage;
44
+ this.onReconnect = options.onReconnect;
45
+ }
46
+
47
+ connect(): void {
48
+ this.intentionalClose = false;
49
+ this.createConnection();
50
+ this.startWatchdog();
51
+ }
52
+
53
+ disconnect(): void {
54
+ this.intentionalClose = true;
55
+ this.stopWatchdog();
56
+ if (this.reconnectTimer) {
57
+ clearTimeout(this.reconnectTimer);
58
+ this.reconnectTimer = null;
59
+ }
60
+ if (this.ws) {
61
+ this.ws.onclose = null;
62
+ this.ws.close();
63
+ this.ws = null;
64
+ }
65
+ }
66
+
67
+ send(message: unknown): void {
68
+ const data = JSON.stringify(message);
69
+
70
+ if (this.ws?.readyState === 1) {
71
+ try {
72
+ this.ws.send(data);
73
+ } catch {
74
+ // Connection died between readyState check and send — buffer instead
75
+ this.bufferMessage(data);
76
+ }
77
+ } else {
78
+ this.bufferMessage(data);
79
+ }
80
+ }
81
+
82
+ private bufferMessage(data: string): void {
83
+ this.buffer.push(data);
84
+ if (this.buffer.length > this.maxBufferSize) {
85
+ this.buffer.shift();
86
+ }
87
+ }
88
+
89
+ get isConnected(): boolean {
90
+ return this.ws?.readyState === 1;
91
+ }
92
+
93
+ /**
94
+ * Update the WebSocket URL and reconnect.
95
+ * Used when mDNS discovers the server on a different address/port.
96
+ */
97
+ updateUrl(newUrl: string): void {
98
+ if (newUrl === this.url) return;
99
+ this.url = newUrl;
100
+ // Force reconnect to new URL
101
+ if (this.ws) {
102
+ this.handleDisconnect();
103
+ }
104
+ }
105
+
106
+ private createConnection(): void {
107
+ try {
108
+ this.ws = new this.WS(this.url);
109
+ } catch {
110
+ // Constructor failed — schedule reconnect
111
+ this.ws = null;
112
+ if (!this.intentionalClose) {
113
+ this.scheduleReconnect();
114
+ }
115
+ return;
116
+ }
117
+
118
+ this.ws.onopen = () => {
119
+ // Reset backoff on successful connection
120
+ this.backoff = 0;
121
+ this.lastMessageAt = Date.now();
122
+
123
+ // Notify reconnect if this isn't the first connection
124
+ if (this.hasConnectedBefore) {
125
+ this.onReconnect?.();
126
+ }
127
+ this.hasConnectedBefore = true;
128
+
129
+ // Flush buffer
130
+ const buffered = [...this.buffer];
131
+ this.buffer = [];
132
+ for (const data of buffered) {
133
+ this.ws?.send(data);
134
+ }
135
+ };
136
+
137
+ this.ws.onmessage = (ev: { data: string }) => {
138
+ this.lastMessageAt = Date.now();
139
+ try {
140
+ const parsed = JSON.parse(ev.data);
141
+ this.onMessage?.(parsed);
142
+ } catch {
143
+ // Ignore malformed messages
144
+ }
145
+ };
146
+
147
+ this.ws.onclose = () => {
148
+ this.handleDisconnect();
149
+ };
150
+
151
+ this.ws.onerror = () => {
152
+ // Node 22's built-in WebSocket may fire onerror WITHOUT onclose
153
+ // on connection failure. Handle once and prevent re-entrant calls
154
+ // (ws.close() can re-trigger onerror synchronously).
155
+ this.handleDisconnect();
156
+ };
157
+ }
158
+
159
+ private handleDisconnect(): void {
160
+ if (!this.ws) return; // Already handled — idempotent guard
161
+ const ws = this.ws;
162
+ this.ws = null;
163
+ // Detach handlers to prevent re-entrant calls from ws.close()
164
+ ws.onclose = null;
165
+ ws.onerror = null;
166
+ ws.onopen = null;
167
+ ws.onmessage = null;
168
+ try { ws.close(); } catch { /* ignore — may already be closed */ }
169
+ if (!this.intentionalClose) {
170
+ this.scheduleReconnect();
171
+ }
172
+ }
173
+
174
+ private startWatchdog(): void {
175
+ this.stopWatchdog();
176
+ if (this.watchdogTimeout <= 0) return;
177
+ this.watchdogTimer = setInterval(() => {
178
+ if (this.ws && this.lastMessageAt > 0 && Date.now() - this.lastMessageAt >= this.watchdogTimeout) {
179
+ // Server has gone silent — force close to trigger reconnect
180
+ this.handleDisconnect();
181
+ }
182
+ }, ConnectionManager.WATCHDOG_CHECK_INTERVAL);
183
+ }
184
+
185
+ private stopWatchdog(): void {
186
+ if (this.watchdogTimer) {
187
+ clearInterval(this.watchdogTimer);
188
+ this.watchdogTimer = null;
189
+ }
190
+ }
191
+
192
+ private scheduleReconnect(): void {
193
+ if (this.backoff === 0) {
194
+ this.backoff = ConnectionManager.INITIAL_BACKOFF;
195
+ } else {
196
+ this.backoff = Math.min(this.backoff * 2, ConnectionManager.MAX_BACKOFF);
197
+ }
198
+
199
+ this.reconnectTimer = setTimeout(() => {
200
+ this.reconnectTimer = null;
201
+ this.createConnection();
202
+ }, this.backoff);
203
+ }
204
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Dev build-on-reload helper.
3
+ * Builds the Vite client and requests server shutdown.
4
+ */
5
+ import { execSync as defaultExecSync } from "node:child_process";
6
+
7
+ export interface DevBuildOptions {
8
+ packageRoot: string;
9
+ serverPort: number;
10
+ /** @internal for testing */
11
+ _execSync?: typeof defaultExecSync;
12
+ /** @internal for testing */
13
+ _fetch?: typeof fetch;
14
+ }
15
+
16
+ /**
17
+ * Run the dev build and shutdown sequence.
18
+ * Errors are caught and logged — never throws.
19
+ */
20
+ export function runDevBuild(opts: DevBuildOptions): void {
21
+ const execSyncFn = opts._execSync ?? defaultExecSync;
22
+ const fetchFn = opts._fetch ?? fetch;
23
+
24
+ try {
25
+ console.log("🔨 Dashboard: building client...");
26
+ execSyncFn("npm run build", { cwd: opts.packageRoot, stdio: "inherit" });
27
+ console.log("✅ Dashboard: client built");
28
+ } catch (err: any) {
29
+ console.log(`❌ Dashboard: build failed — ${err.message}`);
30
+ }
31
+
32
+ try {
33
+ console.log("🛑 Dashboard: stopping server...");
34
+ fetchFn(`http://localhost:${opts.serverPort}/api/shutdown`, { method: "POST" }).catch(() => {});
35
+ console.log("✅ Dashboard: server stopped");
36
+ } catch {
37
+ // Server may not be running — that's fine
38
+ }
39
+ }
@@ -0,0 +1,40 @@
1
+ import type { EventForwardMessage } from "@blackbelt-technology/pi-dashboard-shared/protocol.js";
2
+
3
+ /**
4
+ * Extract only JSON-serializable fields from an event object.
5
+ * Strips functions, AbortSignals, and other non-serializable values.
6
+ */
7
+ function extractSerializable(obj: Record<string, unknown>): Record<string, unknown> {
8
+ const result: Record<string, unknown> = {};
9
+ for (const [key, value] of Object.entries(obj)) {
10
+ if (value === undefined || value === null) {
11
+ result[key] = value;
12
+ continue;
13
+ }
14
+ if (typeof value === "function") continue;
15
+ if (value instanceof AbortSignal) continue;
16
+ if (typeof value === "object" && "aborted" in (value as object)) continue;
17
+ result[key] = value;
18
+ }
19
+ return result;
20
+ }
21
+
22
+ /**
23
+ * Map a pi event object to an event_forward protocol message.
24
+ */
25
+ export function mapEventToProtocol(
26
+ sessionId: string,
27
+ piEvent: Record<string, unknown>,
28
+ ): EventForwardMessage {
29
+ const serializable = extractSerializable(piEvent);
30
+
31
+ return {
32
+ type: "event_forward",
33
+ sessionId,
34
+ event: {
35
+ eventType: (piEvent.type as string) ?? "unknown",
36
+ timestamp: Date.now(),
37
+ data: serializable,
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Flow event wiring: registers listeners for pi-flows events
3
+ * and forwards them as protocol messages to the dashboard server.
4
+ */
5
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import type { BridgeContext } from "./bridge-context.js";
7
+ import { filterHiddenCommands } from "./bridge-context.js";
8
+ import type { FlowInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
9
+
10
+ /** Map of pi-flows event names to dashboard protocol event types */
11
+ export const FLOW_EVENT_MAP: Record<string, string> = {
12
+ "flow:flow-started": "flow_started",
13
+ "flow:agent-started": "flow_agent_started",
14
+ "flow:agent-complete": "flow_agent_complete",
15
+ "flow:subagent-tool-call": "flow_tool_call",
16
+ "flow:subagent-tool-result": "flow_tool_result",
17
+ "flow:assistant-text": "flow_assistant_text",
18
+ "flow:thinking-text": "flow_thinking_text",
19
+ "flow:loop-iteration": "flow_loop_iteration",
20
+ "flow:auto-decision": "flow_auto_decision",
21
+ "flow:complete": "flow_complete",
22
+ "flow:summary-started": "flow_summary_started",
23
+ "flow:summary-ready": "flow_summary_ready",
24
+ "flow:summary-dismissed": "flow_summary_dismissed",
25
+ // Architect lifecycle events
26
+ "flow:architect-started": "architect_started",
27
+ "flow:architect-tool-call": "architect_tool_call",
28
+ "flow:architect-tool-result": "architect_tool_result",
29
+ "flow:architect-text": "architect_text",
30
+ "flow:architect-preview": "architect_preview",
31
+ "flow:architect-complete": "architect_complete",
32
+ "flow:architect-replan": "architect_replan",
33
+ "flow:architect-cancelled": "architect_cancelled",
34
+ "flow:architect-saved": "architect_saved",
35
+ "flow:architect-error": "architect_error",
36
+ "flow:architect-context-generating": "architect_context_generating",
37
+ "flow:architect-context-ready": "architect_context_ready",
38
+ "flow:architect-run-handoff": "architect_run_handoff",
39
+ };
40
+
41
+ /** Map of pi-subagents event names to dashboard protocol event types */
42
+ export const SUBAGENT_EVENT_MAP: Record<string, string> = {
43
+ "subagents:created": "subagent_created",
44
+ "subagents:started": "subagent_started",
45
+ "subagents:completed": "subagent_completed",
46
+ "subagents:failed": "subagent_failed",
47
+ };
48
+
49
+ /**
50
+ * Register flow event listeners on pi.events.
51
+ * Must be called after session_start when pi.events is available.
52
+ *
53
+ * @param bc - Bridge context (mutable state)
54
+ * @param isSessionReady - Function that returns whether session is ready
55
+ * @param getFlowsList - Function to get current flows list
56
+ */
57
+ export function registerFlowEventListeners(
58
+ bc: BridgeContext,
59
+ isSessionReady: () => boolean,
60
+ getFlowsList: () => FlowInfo[],
61
+ ): void {
62
+ const { pi, connection } = bc;
63
+ if (!pi.events) return;
64
+
65
+ // Re-send commands and flows list when pi-flows discovers new flows or completes
66
+ const resendCommandsAndFlows = () => {
67
+ if (!isSessionReady()) return;
68
+ const commands = filterHiddenCommands(pi.getCommands());
69
+ connection.send({ type: "commands_list", sessionId: bc.sessionId, commands });
70
+ const flows = getFlowsList();
71
+ connection.send({ type: "flows_list", sessionId: bc.sessionId, flows });
72
+ };
73
+ pi.events.on("flow:rediscover", resendCommandsAndFlows);
74
+ pi.events.on("flow:complete", resendCommandsAndFlows);
75
+
76
+ // Note: event_forward sending for flow and subagent events is handled by
77
+ // the EventBus emit intercept in bridge.ts (catch-all forwarding).
78
+
79
+ // Forward architect prompt requests directly to the dashboard widget bar.
80
+ // Non-architect prompts still go through ui-proxy (flow-tui -> ctx.ui.select -> extension_ui_request).
81
+ // Both paths race -- first response wins via emitPromptAndAwait's resolved guard.
82
+ pi.events.on("flow:prompt-request", (data: unknown) => {
83
+ if (!isSessionReady()) return;
84
+ const req = data as { id?: string; pipeline?: string; type?: string; question?: string; options?: string[]; defaultValue?: string };
85
+ if (!req.id || !req.pipeline?.startsWith("architect-")) return;
86
+ connection.send({
87
+ type: "event_forward",
88
+ sessionId: bc.sessionId,
89
+ event: {
90
+ eventType: "architect_prompt_request",
91
+ timestamp: Date.now(),
92
+ data: {
93
+ id: req.id,
94
+ promptType: req.type,
95
+ question: req.question,
96
+ options: req.options,
97
+ defaultValue: req.defaultValue,
98
+ },
99
+ },
100
+ });
101
+ });
102
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Git info gathering — detects branch, remote URL, and PR number.
3
+ */
4
+ import { execSync } from "node:child_process";
5
+ import { buildGitLinks, type GitLinks } from "./git-link-builder.js";
6
+
7
+ export interface GitInfo {
8
+ gitBranch: string;
9
+ gitBranchUrl?: string;
10
+ gitPrNumber?: number;
11
+ gitPrUrl?: string;
12
+ }
13
+
14
+ /** Run a shell command and return trimmed stdout, or undefined on failure. */
15
+ function runGit(command: string, cwd: string): string | undefined {
16
+ try {
17
+ return execSync(command, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+
23
+ /** Detect the current git branch. Returns short SHA for detached HEAD. */
24
+ export function detectBranch(cwd: string): string | undefined {
25
+ const ref = runGit("git rev-parse --abbrev-ref HEAD", cwd);
26
+ if (!ref) return undefined;
27
+ if (ref === "HEAD") {
28
+ // Detached HEAD — return short commit SHA
29
+ return runGit("git rev-parse --short HEAD", cwd) ?? "HEAD";
30
+ }
31
+ return ref;
32
+ }
33
+
34
+ /** Detect the remote origin URL. */
35
+ export function detectRemoteUrl(cwd: string): string | undefined {
36
+ return runGit("git remote get-url origin", cwd);
37
+ }
38
+
39
+ /** Detect the PR number via gh CLI (best effort). */
40
+ export function detectPrNumber(cwd: string): number | undefined {
41
+ const result = runGit("gh pr view --json number -q .number", cwd);
42
+ if (result) {
43
+ const num = parseInt(result, 10);
44
+ if (!isNaN(num)) return num;
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ /** Gather all git info for a directory. Returns undefined if not a git repo. */
50
+ export function gatherGitInfo(cwd: string): GitInfo | undefined {
51
+ const branch = detectBranch(cwd);
52
+ if (!branch) return undefined;
53
+
54
+ const remoteUrl = detectRemoteUrl(cwd);
55
+ const prNumber = detectPrNumber(cwd);
56
+
57
+ const links: GitLinks = remoteUrl ? buildGitLinks(remoteUrl, branch, prNumber) : {};
58
+
59
+ return {
60
+ gitBranch: branch,
61
+ gitBranchUrl: links.branchUrl,
62
+ gitPrNumber: prNumber,
63
+ gitPrUrl: links.prUrl,
64
+ };
65
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Git link builder — parses remote URLs and builds platform-specific links.
3
+ */
4
+
5
+ export interface GitLinks {
6
+ branchUrl?: string;
7
+ prUrl?: string;
8
+ }
9
+
10
+ export interface ParsedRemote {
11
+ host: string;
12
+ user: string;
13
+ repo: string;
14
+ }
15
+
16
+ type Platform = "github" | "gitlab" | "bitbucket" | "gitea" | "codeberg" | "sourcehut";
17
+
18
+ const HOST_TO_PLATFORM: Record<string, Platform> = {
19
+ "github.com": "github",
20
+ "gitlab.com": "gitlab",
21
+ "bitbucket.org": "bitbucket",
22
+ "gitea.com": "gitea",
23
+ "codeberg.org": "codeberg",
24
+ "sr.ht": "sourcehut",
25
+ };
26
+
27
+ /** Parse an SSH or HTTPS remote URL into host/user/repo. */
28
+ export function parseRemoteUrl(url: string): ParsedRemote | undefined {
29
+ // SSH: git@host:user/repo.git
30
+ const sshMatch = url.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
31
+ if (sshMatch) {
32
+ const [, host, path] = sshMatch;
33
+ const parts = path!.split("/");
34
+ if (parts.length >= 2) {
35
+ return { host: host!, user: parts.slice(0, -1).join("/"), repo: parts[parts.length - 1]! };
36
+ }
37
+ }
38
+
39
+ // HTTPS: https://host/user/repo.git
40
+ const httpsMatch = url.match(/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/);
41
+ if (httpsMatch) {
42
+ const [, host, path] = httpsMatch;
43
+ const parts = path!.split("/");
44
+ if (parts.length >= 2) {
45
+ return { host: host!, user: parts.slice(0, -1).join("/"), repo: parts[parts.length - 1]! };
46
+ }
47
+ }
48
+
49
+ return undefined;
50
+ }
51
+
52
+ /** Detect the hosting platform from a hostname. */
53
+ export function detectPlatform(host: string): Platform | undefined {
54
+ return HOST_TO_PLATFORM[host];
55
+ }
56
+
57
+ /** Build branch and PR URLs for a given platform. */
58
+ export function buildGitLinks(remoteUrl: string, branch: string, prNumber?: number): GitLinks {
59
+ const parsed = parseRemoteUrl(remoteUrl);
60
+ if (!parsed) return {};
61
+
62
+ const platform = detectPlatform(parsed.host);
63
+ if (!platform) return {};
64
+
65
+ const baseUrl = `https://${parsed.host}/${parsed.user}/${parsed.repo}`;
66
+ const encodedBranch = encodeURIComponent(branch);
67
+
68
+ const links: GitLinks = {};
69
+
70
+ // Don't generate branch URL for detached HEAD
71
+ if (branch !== "HEAD") {
72
+ switch (platform) {
73
+ case "github":
74
+ case "sourcehut":
75
+ links.branchUrl = `${baseUrl}/tree/${encodedBranch}`;
76
+ break;
77
+ case "gitlab":
78
+ links.branchUrl = `${baseUrl}/-/tree/${encodedBranch}`;
79
+ break;
80
+ case "bitbucket":
81
+ links.branchUrl = `${baseUrl}/src/${encodedBranch}`;
82
+ break;
83
+ case "gitea":
84
+ case "codeberg":
85
+ links.branchUrl = `${baseUrl}/src/branch/${encodedBranch}`;
86
+ break;
87
+ }
88
+ }
89
+
90
+ if (prNumber !== undefined) {
91
+ switch (platform) {
92
+ case "github":
93
+ links.prUrl = `${baseUrl}/pull/${prNumber}`;
94
+ break;
95
+ case "gitlab":
96
+ links.prUrl = `${baseUrl}/-/merge_requests/${prNumber}`;
97
+ break;
98
+ case "bitbucket":
99
+ links.prUrl = `${baseUrl}/pull-requests/${prNumber}`;
100
+ break;
101
+ case "gitea":
102
+ case "codeberg":
103
+ links.prUrl = `${baseUrl}/pulls/${prNumber}`;
104
+ break;
105
+ case "sourcehut":
106
+ links.prUrl = `${baseUrl}/patches/${prNumber}`;
107
+ break;
108
+ }
109
+ }
110
+
111
+ return links;
112
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Model and thinking-level change detection.
3
+ * Sends model_update only when values actually change.
4
+ */
5
+ import type { BridgeContext } from "./bridge-context.js";
6
+ import { getCurrentModelString } from "./bridge-context.js";
7
+ import { gatherGitInfo } from "./git-info.js";
8
+
9
+ /**
10
+ * Send model_update if model or thinking level has changed since last send.
11
+ */
12
+ export function sendModelUpdateIfChanged(bc: BridgeContext): void {
13
+ const model = getCurrentModelString(bc);
14
+ const thinkingLevel = (bc.pi as any).getThinkingLevel?.() ?? undefined;
15
+ if (model === bc.lastModel && thinkingLevel === bc.lastThinkingLevel) return;
16
+ bc.lastModel = model;
17
+ bc.lastThinkingLevel = thinkingLevel;
18
+ if (model) {
19
+ bc.connection.send({
20
+ type: "model_update",
21
+ sessionId: bc.sessionId,
22
+ model,
23
+ thinkingLevel,
24
+ });
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Send session_name_update if name has changed since last send.
30
+ */
31
+ export function sendSessionNameIfChanged(bc: BridgeContext): void {
32
+ const name = bc.pi.getSessionName() ?? "";
33
+ if (name === bc.lastSessionName) return;
34
+ bc.lastSessionName = name;
35
+ bc.connection.send({
36
+ type: "session_name_update",
37
+ sessionId: bc.sessionId,
38
+ name,
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Send git_info_update if branch or PR has changed since last send.
44
+ */
45
+ export function sendGitInfoIfChanged(bc: BridgeContext, cwd: string): void {
46
+ const info = gatherGitInfo(cwd);
47
+ if (!info) return;
48
+ if (info.gitBranch === bc.lastGitBranch && info.gitPrNumber === bc.lastGitPrNumber) return;
49
+ bc.lastGitBranch = info.gitBranch;
50
+ bc.lastGitPrNumber = info.gitPrNumber;
51
+ bc.connection.send({
52
+ type: "git_info_update",
53
+ sessionId: bc.sessionId,
54
+ ...info,
55
+ });
56
+ }
@@ -0,0 +1,23 @@
1
+ // Ambient declarations for pi runtime packages.
2
+ // The actual types are provided by whichever host (pi or OMP) loads this extension.
3
+ // tsconfig paths handles resolution when one of the packages is installed;
4
+ // these declarations serve as fallback when neither is available (e.g. CI, dev without pi).
5
+ declare module "@mariozechner/pi-coding-agent" {
6
+ export type ExtensionAPI = import("@oh-my-pi/pi-coding-agent").ExtensionAPI;
7
+ }
8
+ declare module "@oh-my-pi/pi-coding-agent" {
9
+ export interface ModelRegistry {
10
+ getAvailable(): Array<{ provider: string; id: string }>;
11
+ refresh(): void;
12
+ }
13
+
14
+ export interface ExtensionAPI {
15
+ on(event: string, handler: (...args: any[]) => any): void;
16
+ getCommands(): any[];
17
+ sendUserMessage(message: string | any[]): void;
18
+ setSessionName(name: string): void;
19
+ getSessionName(): string | undefined;
20
+ registerCommand(name: string, options: { description?: string; handler: (args: string, ctx: any) => Promise<void> }): void;
21
+ exec(command: string, args: string[], options?: { timeout?: number }): Promise<{ stdout: string; stderr: string; exitCode: number }>;
22
+ }
23
+ }