@blackbelt-technology/pi-agent-dashboard 0.4.5 → 0.5.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 (133) hide show
  1. package/AGENTS.md +342 -267
  2. package/README.md +51 -2
  3. package/docs/architecture.md +266 -25
  4. package/package.json +14 -4
  5. package/packages/extension/package.json +2 -2
  6. package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
  7. package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
  8. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
  9. package/packages/extension/src/__tests__/prompt-bus.test.ts +44 -0
  10. package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
  11. package/packages/extension/src/__tests__/server-launcher.test.ts +24 -1
  12. package/packages/extension/src/__tests__/vcs-info-jj.test.ts +145 -0
  13. package/packages/extension/src/__tests__/{git-info.test.ts → vcs-info.test.ts} +6 -6
  14. package/packages/extension/src/bridge-context.ts +7 -0
  15. package/packages/extension/src/bridge.ts +142 -4
  16. package/packages/extension/src/command-handler.ts +6 -0
  17. package/packages/extension/src/markdown-image-inliner.ts +268 -0
  18. package/packages/extension/src/model-tracker.ts +35 -1
  19. package/packages/extension/src/prompt-bus.ts +4 -3
  20. package/packages/extension/src/prompt-expander.ts +50 -2
  21. package/packages/extension/src/provider-register.ts +117 -0
  22. package/packages/extension/src/server-launcher.ts +18 -1
  23. package/packages/extension/src/session-sync.ts +6 -1
  24. package/packages/extension/src/vcs-info.ts +184 -0
  25. package/packages/server/package.json +4 -4
  26. package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
  27. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
  28. package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
  29. package/packages/server/src/__tests__/build-auth-status.test.ts +190 -0
  30. package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -0
  31. package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
  32. package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +87 -0
  33. package/packages/server/src/__tests__/has-openspec-dir.test.ts +64 -0
  34. package/packages/server/src/__tests__/health-shape.test.ts +43 -0
  35. package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
  36. package/packages/server/src/__tests__/is-unread-trigger.test.ts +4 -2
  37. package/packages/server/src/__tests__/jj-routes.test.ts +93 -0
  38. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -0
  39. package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +114 -0
  40. package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +177 -0
  41. package/packages/server/src/__tests__/process-manager-codes.test.ts +80 -0
  42. package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
  43. package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
  44. package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +54 -0
  45. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
  46. package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
  47. package/packages/server/src/__tests__/session-diff-vcs.test.ts +61 -0
  48. package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
  49. package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
  50. package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
  51. package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +166 -0
  52. package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
  53. package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
  54. package/packages/server/src/__tests__/system-routes-restart.test.ts +4 -4
  55. package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
  56. package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
  57. package/packages/server/src/bootstrap-install-from-list.ts +232 -0
  58. package/packages/server/src/bootstrap-state.ts +18 -0
  59. package/packages/server/src/browser-gateway.ts +58 -21
  60. package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
  61. package/packages/server/src/browser-handlers/session-action-handler.ts +60 -2
  62. package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
  63. package/packages/server/src/cli.ts +22 -0
  64. package/packages/server/src/directory-service.ts +31 -0
  65. package/packages/server/src/event-wiring.ts +57 -2
  66. package/packages/server/src/home-lock.d.ts +124 -0
  67. package/packages/server/src/home-lock.js +330 -0
  68. package/packages/server/src/home-lock.js.map +1 -0
  69. package/packages/server/src/idle-timer.ts +15 -1
  70. package/packages/server/src/openspec-tasks.ts +50 -19
  71. package/packages/server/src/pi-core-updater.ts +65 -9
  72. package/packages/server/src/pi-gateway.ts +6 -0
  73. package/packages/server/src/process-manager.ts +62 -11
  74. package/packages/server/src/provider-auth-handlers.ts +9 -0
  75. package/packages/server/src/provider-auth-storage.ts +83 -51
  76. package/packages/server/src/provider-catalogue-cache.ts +41 -0
  77. package/packages/server/src/routes/doctor-routes.ts +140 -0
  78. package/packages/server/src/routes/jj-routes.ts +386 -0
  79. package/packages/server/src/routes/provider-auth-routes.ts +9 -0
  80. package/packages/server/src/routes/session-routes.ts +12 -3
  81. package/packages/server/src/routes/system-routes.ts +38 -1
  82. package/packages/server/src/server.ts +16 -9
  83. package/packages/server/src/session-bootstrap.ts +27 -12
  84. package/packages/server/src/session-diff.ts +118 -1
  85. package/packages/server/src/session-discovery.ts +10 -3
  86. package/packages/server/src/session-scanner.ts +4 -2
  87. package/packages/server/src/spawn-failure-log.ts +130 -0
  88. package/packages/server/src/spawn-preflight.ts +82 -0
  89. package/packages/server/src/spawn-register-watchdog.ts +236 -0
  90. package/packages/server/src/terminal-manager.ts +12 -1
  91. package/packages/shared/package.json +1 -1
  92. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +1 -0
  93. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +1 -0
  94. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
  95. package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
  96. package/packages/shared/src/__tests__/config.test.ts +48 -0
  97. package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
  98. package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
  99. package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
  100. package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
  101. package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
  102. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
  103. package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
  104. package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
  105. package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
  106. package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
  107. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
  108. package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
  109. package/packages/shared/src/__tests__/platform-jj.test.ts +339 -0
  110. package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
  111. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +18 -2
  112. package/packages/shared/src/bootstrap-install.ts +196 -2
  113. package/packages/shared/src/browser-protocol.ts +112 -1
  114. package/packages/shared/src/config.ts +29 -0
  115. package/packages/shared/src/dashboard-starter.ts +33 -0
  116. package/packages/shared/src/diff-types.ts +17 -0
  117. package/packages/shared/src/doctor-core.ts +821 -0
  118. package/packages/shared/src/index.ts +9 -0
  119. package/packages/shared/src/installable-list.ts +152 -0
  120. package/packages/shared/src/launch-source-flag.ts +14 -0
  121. package/packages/shared/src/launch-source-types.ts +18 -0
  122. package/packages/shared/src/openspec-activity-detector.ts +25 -7
  123. package/packages/shared/src/platform/detached-spawn.ts +13 -2
  124. package/packages/shared/src/platform/jj.ts +405 -0
  125. package/packages/shared/src/platform/managed-node-path.ts +77 -0
  126. package/packages/shared/src/protocol.ts +60 -2
  127. package/packages/shared/src/rest-api.ts +4 -0
  128. package/packages/shared/src/skill-block-parser.ts +115 -0
  129. package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
  130. package/packages/shared/src/tool-registry/definitions.ts +19 -5
  131. package/packages/shared/src/tool-registry/strategies.ts +42 -0
  132. package/packages/shared/src/types.ts +91 -0
  133. package/packages/extension/src/git-info.ts +0 -55
@@ -12,8 +12,15 @@
12
12
  *
13
13
  * See change: unified-bootstrap-install.
14
14
  */
15
- import { spawn as cpSpawn } from "./platform/exec.js";
16
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
15
+ import { spawn as cpSpawn, spawnSync as cpSpawnSync } from "./platform/exec.js";
16
+ import {
17
+ cpSync,
18
+ existsSync,
19
+ mkdirSync,
20
+ readFileSync,
21
+ rmSync,
22
+ writeFileSync,
23
+ } from "node:fs";
17
24
  import path from "node:path";
18
25
  import { getManagedDir } from "./managed-paths.js";
19
26
  import { getDefaultRegistry, type ToolRegistry } from "./tool-registry/index.js";
@@ -210,3 +217,190 @@ export async function bootstrapInstallDefaults(
210
217
  progress,
211
218
  });
212
219
  }
220
+
221
+ // ── Managed Node runtime install ───────────────────────────────────────
222
+ //
223
+ // See change: embed-managed-node-runtime (spec: managed-node-runtime).
224
+ //
225
+ // `installManagedNode` copies a bundled Node distribution into
226
+ // `<managedDir>/node/` and writes a `<managedDir>/node/.version` marker.
227
+ // Idempotent: skip when marker matches the bundled version, replace on
228
+ // mismatch, no-op when the bundled source isn't available (standalone
229
+ // CLI install with no Electron resources).
230
+
231
+ export interface InstallManagedNodeOptions {
232
+ /**
233
+ * Source directory containing the bundled Node distribution.
234
+ * Layout matches the upstream Node zip/tar:
235
+ * Windows: `<dir>/node.exe`, `<dir>/npm.cmd`, `<dir>/npx.cmd`
236
+ * Unix: `<dir>/bin/node`, `<dir>/bin/npm`, `<dir>/bin/npx`
237
+ *
238
+ * Caller resolves this (Electron uses `path.dirname(getBundledNodePath())`
239
+ * after stripping the platform-specific suffix). Pass `null` /
240
+ * `undefined` for the standalone CLI install case — the function
241
+ * no-ops without error.
242
+ */
243
+ bundledNodeDir?: string | null;
244
+ /** Root of the managed install. Defaults to `getManagedDir()`. */
245
+ managedDir?: string;
246
+ /** Called on every progress tick. Mirrors `bootstrapInstall` shape. */
247
+ progress?: ProgressCallback;
248
+ /**
249
+ * Test seam: override how the bundled Node version is read. Default
250
+ * spawns `<bundledNodeDir>/bin/node --version` (or `node.exe` on
251
+ * Windows) and trims stdout. Tests inject a fake to avoid real spawns.
252
+ */
253
+ _readVersion?: (sourceBinary: string) => string | null;
254
+ }
255
+
256
+ export interface InstallManagedNodeResult {
257
+ /** True iff the operation succeeded (including the no-op cases). */
258
+ ok: boolean;
259
+ /** Did we actually copy files? false when no-op or skipped. */
260
+ copied: boolean;
261
+ /** Resolved managed Node directory (`<managedDir>/node`). */
262
+ managedNodeDir: string;
263
+ /** Bundled Node version (e.g. `v22.12.0`) when known. */
264
+ version?: string;
265
+ /** Reason for skip / failure. Always set when `copied === false`. */
266
+ reason?: string;
267
+ /** Error message when `ok === false`. */
268
+ error?: string;
269
+ }
270
+
271
+ /** Path to the source `node` / `node.exe` binary inside `bundledNodeDir`. */
272
+ function sourceNodeBinary(bundledNodeDir: string): string {
273
+ return process.platform === "win32" // platform-branch-ok: Node distribution layout differs Windows vs Unix
274
+ ? path.join(bundledNodeDir, "node.exe")
275
+ : path.join(bundledNodeDir, "bin", "node");
276
+ }
277
+
278
+ /**
279
+ * Spawn `<nodeBinary> --version` and return the trimmed stdout (e.g.
280
+ * `v22.12.0`). Returns null on failure or when the binary is missing.
281
+ * Synchronous because bootstrap is naturally serial.
282
+ */
283
+ function readNodeVersion(nodeBinary: string): string | null {
284
+ if (!existsSync(nodeBinary)) return null;
285
+ try {
286
+ const r = cpSpawnSync(nodeBinary, ["--version"], {
287
+ stdio: ["ignore", "pipe", "pipe"],
288
+ timeout: 5_000,
289
+ encoding: "utf-8",
290
+ });
291
+ if (r.status !== 0) return null;
292
+ const out = (r.stdout ?? "").toString().trim();
293
+ return out || null;
294
+ } catch {
295
+ return null;
296
+ }
297
+ }
298
+
299
+ /** Read the `<managedNodeDir>/.version` marker if present, else null. */
300
+ function readManagedMarker(managedNodeDir: string): string | null {
301
+ const markerPath = path.join(managedNodeDir, ".version");
302
+ if (!existsSync(markerPath)) return null;
303
+ try {
304
+ return readFileSync(markerPath, "utf-8").trim() || null;
305
+ } catch {
306
+ return null;
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Idempotently copy the bundled Node distribution into `<managedDir>/node/`.
312
+ *
313
+ * - First-run: full recursive copy + write `.version` marker.
314
+ * - Re-run with matching marker: no-op.
315
+ * - Mismatched marker (or missing marker with dir present): replace +
316
+ * rewrite marker.
317
+ * - Bundled source absent or `bundledNodeDir == null`: no-op.
318
+ * - Failed copy mid-flight: marker NOT written, so next call retries.
319
+ */
320
+ export async function installManagedNode(
321
+ opts: InstallManagedNodeOptions = {},
322
+ ): Promise<InstallManagedNodeResult> {
323
+ const managedDir = opts.managedDir ?? getManagedDir();
324
+ const managedNodeDir = path.join(managedDir, "node");
325
+ const step = "node-runtime";
326
+
327
+ const bundledDir = opts.bundledNodeDir ?? null;
328
+ if (!bundledDir) {
329
+ return {
330
+ ok: true,
331
+ copied: false,
332
+ managedNodeDir,
333
+ reason: "no bundled source",
334
+ };
335
+ }
336
+
337
+ const sourceBinary = sourceNodeBinary(bundledDir);
338
+ const sourceVersion = (opts._readVersion ?? readNodeVersion)(sourceBinary);
339
+ if (!sourceVersion) {
340
+ return {
341
+ ok: true,
342
+ copied: false,
343
+ managedNodeDir,
344
+ reason: `bundled node binary missing or unreadable: ${sourceBinary}`,
345
+ };
346
+ }
347
+
348
+ const existingMarker = readManagedMarker(managedNodeDir);
349
+ if (existingMarker === sourceVersion) {
350
+ return {
351
+ ok: true,
352
+ copied: false,
353
+ managedNodeDir,
354
+ version: sourceVersion,
355
+ reason: "version matches bundled — no copy needed",
356
+ };
357
+ }
358
+
359
+ opts.progress?.({
360
+ step,
361
+ status: "running",
362
+ output: `Installing Node ${sourceVersion} runtime`,
363
+ });
364
+
365
+ try {
366
+ // Replace any existing dir (handles the mismatch + missing-marker
367
+ // cases) so the copy is from a clean slate.
368
+ if (existsSync(managedNodeDir)) {
369
+ rmSync(managedNodeDir, { recursive: true, force: true });
370
+ }
371
+ mkdirSync(path.dirname(managedNodeDir), { recursive: true });
372
+
373
+ cpSync(bundledDir, managedNodeDir, {
374
+ recursive: true,
375
+ force: true,
376
+ // dereference: false keeps symlinks-as-symlinks (Unix npm shim).
377
+ // verbatimSymlinks would also work in newer Node; default is fine.
378
+ });
379
+
380
+ // Marker last — partial copy on failure leaves no marker, so the
381
+ // next invocation treats the dir as missing and retries.
382
+ writeFileSync(
383
+ path.join(managedNodeDir, ".version"),
384
+ sourceVersion + "\n",
385
+ "utf-8",
386
+ );
387
+
388
+ opts.progress?.({ step, status: "done", output: `Node ${sourceVersion}` });
389
+ return {
390
+ ok: true,
391
+ copied: true,
392
+ managedNodeDir,
393
+ version: sourceVersion,
394
+ };
395
+ } catch (err) {
396
+ const message = err instanceof Error ? err.message : String(err);
397
+ opts.progress?.({ step, status: "error", error: message });
398
+ return {
399
+ ok: false,
400
+ copied: false,
401
+ managedNodeDir,
402
+ version: sourceVersion,
403
+ error: message,
404
+ };
405
+ }
406
+ }
@@ -127,6 +127,33 @@ export interface SpawnResultBrowserMessage {
127
127
  message: string;
128
128
  }
129
129
 
130
+ /**
131
+ * Failure classification codes for spawn errors.
132
+ * Set on every `{ success: false }` path inside process-manager and the
133
+ * session-action-handler. Additive — clients that do not know a code fall
134
+ * back to the `message` string.
135
+ * See change: spawn-failure-diagnostics.
136
+ */
137
+ export type SpawnFailureCode =
138
+ | "DIR_MISSING"
139
+ | "PI_NOT_FOUND"
140
+ | "WIN_PI_CMD_ONLY"
141
+ | "WT_MISSING"
142
+ | "TMUX_MISSING"
143
+ | "PI_CRASHED"
144
+ | "SPAWN_ERRNO"
145
+ | "PREFLIGHT_FAILED"
146
+ | "REGISTER_TIMEOUT";
147
+
148
+ /**
149
+ * A single reason from the synchronous spawn preflight check.
150
+ * See change: spawn-failure-diagnostics.
151
+ */
152
+ export interface PreflightReason {
153
+ code: string;
154
+ message: string;
155
+ }
156
+
130
157
  /**
131
158
  * Emitted when a session spawn fails — either because `spawnPiSession` threw,
132
159
  * returned `{ success: false }`, or the spawned child crashed immediately.
@@ -140,6 +167,37 @@ export interface SpawnErrorMessage {
140
167
  message: string;
141
168
  /** Up to ~2 KB tail of stderr captured from the failed child, if any. */
142
169
  stderr?: string;
170
+ /** Structured failure classifier. Additive — old clients ignore this field. See change: spawn-failure-diagnostics. */
171
+ code?: SpawnFailureCode;
172
+ /** Preflight failure reasons. Only set when code === "PREFLIGHT_FAILED". See change: spawn-failure-diagnostics. */
173
+ reasons?: PreflightReason[];
174
+ }
175
+
176
+ /**
177
+ * Emitted when a spawned pi session never sends `session_register` within
178
+ * the configured `spawnRegisterTimeoutMs` window.
179
+ * See change: spawn-failure-diagnostics.
180
+ */
181
+ export interface SpawnRegisterTimeoutMessage {
182
+ type: "spawn_register_timeout";
183
+ cwd: string;
184
+ /** Present for headless spawns; absent for tmux/wt/wsl-tmux. */
185
+ pid?: number;
186
+ /** Last 4 KB of the per-session stderr log, if available. */
187
+ stderrTail?: string;
188
+ /** The effective watchdog timeout in ms — so the UI can render e.g. "30s". See change: spawn-failure-diagnostics (fix W2). */
189
+ timeoutMs?: number;
190
+ }
191
+
192
+ /**
193
+ * Emitted when pi finally registers AFTER the watchdog already fired.
194
+ * The UI uses it to auto-clear the timeout banner for the given cwd.
195
+ * See change: spawn-failure-diagnostics.
196
+ */
197
+ export interface SpawnRegisterRecoveredMessage {
198
+ type: "spawn_register_recovered";
199
+ cwd: string;
200
+ pid?: number;
143
201
  }
144
202
 
145
203
  export interface SessionsReorderedMessage {
@@ -148,6 +206,28 @@ export interface SessionsReorderedMessage {
148
206
  sessionIds: string[];
149
207
  }
150
208
 
209
+ /**
210
+ * Atomic on-connect snapshot of the server's full session registry and
211
+ * per-cwd ordering. Replaces the legacy per-session `session_added` loop
212
+ * + per-cwd `sessions_reordered` loop that the gateway used to emit on
213
+ * each browser WS connect. Client SHALL replace its `sessions` Map and
214
+ * `sessionOrderMap` with this payload (no merging) so stale ids from a
215
+ * previous server lifetime are dropped atomically.
216
+ *
217
+ * Live updates after the snapshot continue using the incremental
218
+ * `session_added` / `session_updated` / `session_removed` /
219
+ * `sessions_reordered` messages.
220
+ *
221
+ * See change: fix-stale-sessions-on-reconnect.
222
+ */
223
+ export interface SessionsSnapshotMessage {
224
+ type: "sessions_snapshot";
225
+ /** Every session known to the server at construction time, alive AND ended. */
226
+ sessions: DashboardSession[];
227
+ /** cwd → ordered session ids. Only non-empty arrays are included. */
228
+ orders: Record<string, string[]>;
229
+ }
230
+
151
231
  export interface PinnedDirsUpdatedMessage {
152
232
  type: "pinned_dirs_updated";
153
233
  paths: string[];
@@ -362,6 +442,23 @@ export interface BrowserExtUiDecoratorMessage {
362
442
  removed?: boolean;
363
443
  }
364
444
 
445
+ /**
446
+ * Server → browser: register a base64-encoded image asset under a content
447
+ * hash for the given session. Forwarded verbatim from the bridge's
448
+ * `asset_register` message and replayed to reconnecting browsers (in
449
+ * chronological position relative to its referencing `message_update` /
450
+ * `message_end`). The client populates a per-session `Map<hash,{data,mime}>`
451
+ * consumed by the `MarkdownContent` `pi-asset:` resolver.
452
+ * See change: chat-markdown-local-images-and-math.
453
+ */
454
+ export interface BrowserAssetRegisterMessage {
455
+ type: "asset_register";
456
+ sessionId: string;
457
+ hash: string;
458
+ mimeType: string;
459
+ data: string;
460
+ }
461
+
365
462
  /** Sent when a plugin's config changes; carries only that plugin's namespace. */
366
463
  export interface PluginConfigUpdateMessage {
367
464
  type: "plugin_config_update";
@@ -392,7 +489,10 @@ export type ServerToBrowserMessage =
392
489
  | ResumeResultBrowserMessage
393
490
  | SpawnResultBrowserMessage
394
491
  | SpawnErrorMessage
492
+ | SpawnRegisterTimeoutMessage
493
+ | SpawnRegisterRecoveredMessage
395
494
  | SessionsReorderedMessage
495
+ | SessionsSnapshotMessage
396
496
  | PinnedDirsUpdatedMessage
397
497
  | TerminalAddedMessage
398
498
  | TerminalRemovedMessage
@@ -416,7 +516,8 @@ export type ServerToBrowserMessage =
416
516
  | BootstrapTicketCompleteMessage
417
517
  | BrowserUiModulesListMessage
418
518
  | BrowserUiDataListMessage
419
- | BrowserExtUiDecoratorMessage;
519
+ | BrowserExtUiDecoratorMessage
520
+ | BrowserAssetRegisterMessage;
420
521
 
421
522
  // ── Browser → Server ────────────────────────────────────────────────
422
523
 
@@ -476,6 +577,15 @@ export interface RequestModelsBrowserMessage {
476
577
  sessionId: string;
477
578
  }
478
579
 
580
+ /**
581
+ * Browser asks the server to forward `request_providers` to the bridge.
582
+ * See change: replace-hardcoded-provider-lists.
583
+ */
584
+ export interface RequestProvidersBrowserMessage {
585
+ type: "request_providers";
586
+ sessionId: string;
587
+ }
588
+
479
589
  export interface SetThinkingLevelBrowserMessage {
480
590
  type: "set_thinking_level";
481
591
  sessionId: string;
@@ -769,6 +879,7 @@ export type BrowserToServerMessage =
769
879
  | OpenSpecRefreshBrowserMessage
770
880
  | RenameSessionBrowserMessage
771
881
  | RequestModelsBrowserMessage
882
+ | RequestProvidersBrowserMessage
772
883
  | SetThinkingLevelBrowserMessage
773
884
  | SetModelBrowserMessage
774
885
  | ShutdownBrowserMessage
@@ -134,6 +134,13 @@ export interface DashboardConfig {
134
134
  editor: EditorConfig;
135
135
  /** OpenSpec background polling behavior (interval, concurrency, change detection, jitter) */
136
136
  openspec: OpenSpecPollConfig;
137
+ /**
138
+ * Timeout for ask_user prompts in seconds.
139
+ * Default: 300 (5 minutes).
140
+ * Set to -1 (or any value <= 0) for no timeout (waits indefinitely).
141
+ * If the key is absent from config.json the default of 300 s applies.
142
+ */
143
+ askUserPromptTimeoutSeconds: number;
137
144
  /** Networks trusted for full access without authentication (CIDR, wildcard, exact IP) */
138
145
  trustedNetworks: string[];
139
146
  /** Merged trustedNetworks + auth.bypassHosts (deduplicated). Computed at load time. */
@@ -152,6 +159,12 @@ export interface DashboardConfig {
152
159
  reattachPlacement: ReattachPlacement;
153
160
  /** Persisted list of known remote servers */
154
161
  knownServers: KnownServer[];
162
+ /**
163
+ * How long (ms) to wait for a spawned pi session to send `session_register`
164
+ * before emitting a timeout warning. Default 30000 (30s). Clamped [5000, 120000].
165
+ * See change: spawn-failure-diagnostics.
166
+ */
167
+ spawnRegisterTimeoutMs: number;
155
168
  /**
156
169
  * Per-plugin config namespaces. Reserved top-level key.
157
170
  * Each plugin's config lives at plugins.<id>.*
@@ -168,6 +181,16 @@ export interface CorsConfig {
168
181
 
169
182
  const VALID_SPAWN_STRATEGIES: SpawnStrategy[] = ["tmux", "headless"];
170
183
 
184
+ /** Default ask_user prompt timeout: 300 seconds (5 minutes). */
185
+ export const DEFAULT_ASK_USER_PROMPT_TIMEOUT_SECONDS = 300;
186
+
187
+ /** Default + clamp for spawnRegisterTimeoutMs. See change: spawn-failure-diagnostics. */
188
+ export const DEFAULT_SPAWN_REGISTER_TIMEOUT_MS = 30000;
189
+ export function clampSpawnRegisterTimeoutMs(v: unknown): number {
190
+ if (typeof v !== "number" || isNaN(v)) return DEFAULT_SPAWN_REGISTER_TIMEOUT_MS;
191
+ return Math.max(5000, Math.min(120000, v));
192
+ }
193
+
171
194
  const DEFAULTS: DashboardConfig = {
172
195
  plugins: {},
173
196
  port: 8000,
@@ -187,7 +210,9 @@ const DEFAULTS: DashboardConfig = {
187
210
  cors: { allowedOrigins: [] },
188
211
  electronMode: false,
189
212
  knownServers: [],
213
+ askUserPromptTimeoutSeconds: DEFAULT_ASK_USER_PROMPT_TIMEOUT_SECONDS,
190
214
  reattachPlacement: DEFAULT_REATTACH_PLACEMENT,
215
+ spawnRegisterTimeoutMs: 30000,
191
216
  };
192
217
 
193
218
  /**
@@ -385,6 +410,10 @@ export function loadConfig(): DashboardConfig {
385
410
  knownServers: parseKnownServers(parsed.knownServers),
386
411
  reattachPlacement: parseReattachPlacement(parsed.reattachPlacement),
387
412
  plugins: parsePluginsConfig(parsed.plugins),
413
+ askUserPromptTimeoutSeconds: typeof parsed.askUserPromptTimeoutSeconds === "number"
414
+ ? parsed.askUserPromptTimeoutSeconds
415
+ : defaults.askUserPromptTimeoutSeconds,
416
+ spawnRegisterTimeoutMs: clampSpawnRegisterTimeoutMs(parsed.spawnRegisterTimeoutMs),
388
417
  };
389
418
 
390
419
  // Compute resolvedTrustedNetworks: merge trustedNetworks + auth.bypassHosts
@@ -0,0 +1,33 @@
1
+ /**
2
+ * DashboardStarter — identifies who launched the dashboard server process.
3
+ *
4
+ * "Bridge" — spawned by the pi bridge extension (server-launcher.ts).
5
+ * "Standalone" — invoked directly via CLI (`pi-dashboard` or `pi-dashboard start`).
6
+ * "Electron" — spawned by the Electron main process.
7
+ *
8
+ * The value is injected via the DASHBOARD_STARTER env var at spawn time.
9
+ * When unset or empty the default is "Standalone" (direct CLI invocation).
10
+ */
11
+
12
+ export type DashboardStarter = "Bridge" | "Standalone" | "Electron";
13
+
14
+ const VALID: ReadonlySet<string> = new Set<DashboardStarter>(["Bridge", "Standalone", "Electron"]);
15
+
16
+ /**
17
+ * Parse `env.DASHBOARD_STARTER` into a `DashboardStarter`.
18
+ *
19
+ * - Unset or empty → `"Standalone"` (direct CLI invocation default).
20
+ * - Valid value → that value.
21
+ * - Invalid value → logs `console.warn`, returns `"Standalone"`.
22
+ */
23
+ export function parseDashboardStarter(
24
+ env: Record<string, string | undefined>,
25
+ ): DashboardStarter {
26
+ const raw = env["DASHBOARD_STARTER"];
27
+ if (!raw) return "Standalone";
28
+ if (VALID.has(raw)) return raw as DashboardStarter;
29
+ console.warn(
30
+ `[dashboard-starter] Unknown DASHBOARD_STARTER value "${raw}"; defaulting to "Standalone".`,
31
+ );
32
+ return "Standalone";
33
+ }
@@ -38,4 +38,21 @@ export interface SessionDiffResponse {
38
38
  files: FileDiffEntry[];
39
39
  /** Whether the session cwd is a git repository */
40
40
  isGitRepo: boolean;
41
+ /**
42
+ * VCS regime used to compute the per-file diffs. Optional for
43
+ * backwards compatibility — absent on responses produced before
44
+ * change `add-jj-workspace-plugin`.
45
+ */
46
+ vcsKind?: "git" | "jj";
47
+ /**
48
+ * The literal revset / ref used as the diff base (e.g. "HEAD",
49
+ * "@-", "fork_point(@, trunk())"). Optional.
50
+ */
51
+ diffBase?: string;
52
+ /**
53
+ * Human-readable label for `diffBase` (e.g. "develop", "trunk()",
54
+ * "HEAD"). Optional. Renders as "Diffing against \<baseLabel\>"
55
+ * in the client when `vcsKind === "jj"`.
56
+ */
57
+ baseLabel?: string;
41
58
  }