@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.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 (197) hide show
  1. package/AGENTS.md +67 -116
  2. package/README.md +93 -7
  3. package/docs/architecture.md +408 -9
  4. package/package.json +6 -4
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
  7. package/packages/extension/src/__tests__/git-info.test.ts +67 -55
  8. package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
  9. package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
  10. package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
  11. package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
  12. package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
  13. package/packages/extension/src/bridge.ts +69 -2
  14. package/packages/extension/src/dev-build.ts +1 -1
  15. package/packages/extension/src/git-info.ts +9 -19
  16. package/packages/extension/src/pi-env.d.ts +1 -0
  17. package/packages/extension/src/process-scanner.ts +72 -38
  18. package/packages/extension/src/provider-register.ts +304 -16
  19. package/packages/extension/src/server-auto-start.ts +27 -1
  20. package/packages/extension/src/server-launcher.ts +71 -27
  21. package/packages/server/package.json +16 -2
  22. package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
  23. package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
  24. package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
  25. package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
  26. package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
  27. package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
  28. package/packages/server/src/__tests__/config-api.test.ts +68 -0
  29. package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
  30. package/packages/server/src/__tests__/directory-service.test.ts +234 -8
  31. package/packages/server/src/__tests__/editor-registry.test.ts +28 -15
  32. package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
  33. package/packages/server/src/__tests__/extension-register.test.ts +3 -1
  34. package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
  35. package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
  36. package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
  37. package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
  38. package/packages/server/src/__tests__/home-lock.test.ts +308 -0
  39. package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
  40. package/packages/server/src/__tests__/node-guard.test.ts +85 -0
  41. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
  42. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
  43. package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
  44. package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
  45. package/packages/server/src/__tests__/process-manager.test.ts +45 -18
  46. package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
  47. package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
  48. package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
  49. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
  50. package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
  51. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
  52. package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
  53. package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
  54. package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
  55. package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
  56. package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
  57. package/packages/server/src/__tests__/tunnel.test.ts +13 -7
  58. package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
  59. package/packages/server/src/bootstrap-queue.ts +130 -0
  60. package/packages/server/src/bootstrap-state.ts +131 -0
  61. package/packages/server/src/browse.ts +8 -3
  62. package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
  63. package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
  64. package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
  65. package/packages/server/src/cli.ts +256 -32
  66. package/packages/server/src/config-api.ts +16 -0
  67. package/packages/server/src/directory-service.ts +270 -39
  68. package/packages/server/src/editor-detection.ts +12 -9
  69. package/packages/server/src/editor-manager.ts +19 -4
  70. package/packages/server/src/editor-pid-registry.ts +9 -8
  71. package/packages/server/src/editor-registry.ts +22 -25
  72. package/packages/server/src/git-operations.ts +1 -1
  73. package/packages/server/src/headless-pid-registry.ts +7 -20
  74. package/packages/server/src/home-lock-release.ts +72 -0
  75. package/packages/server/src/home-lock.ts +389 -0
  76. package/packages/server/src/node-guard.ts +52 -0
  77. package/packages/server/src/package-manager-wrapper.ts +207 -47
  78. package/packages/server/src/pi-core-checker.ts +1 -1
  79. package/packages/server/src/pi-core-updater.ts +7 -1
  80. package/packages/server/src/pi-resource-scanner.ts +5 -8
  81. package/packages/server/src/pi-version-skew.ts +196 -0
  82. package/packages/server/src/preferences-store.ts +17 -3
  83. package/packages/server/src/process-manager.ts +403 -222
  84. package/packages/server/src/provider-probe.ts +234 -0
  85. package/packages/server/src/restart-helper.ts +130 -0
  86. package/packages/server/src/routes/bootstrap-routes.ts +88 -0
  87. package/packages/server/src/routes/openspec-routes.ts +25 -1
  88. package/packages/server/src/routes/pi-core-routes.ts +24 -1
  89. package/packages/server/src/routes/provider-auth-routes.ts +8 -8
  90. package/packages/server/src/routes/provider-routes.ts +43 -0
  91. package/packages/server/src/routes/recommended-routes.ts +10 -12
  92. package/packages/server/src/routes/system-routes.ts +20 -33
  93. package/packages/server/src/routes/tool-routes.ts +153 -0
  94. package/packages/server/src/server-pid.ts +5 -9
  95. package/packages/server/src/server.ts +211 -10
  96. package/packages/server/src/session-api.ts +77 -8
  97. package/packages/server/src/session-bootstrap.ts +17 -3
  98. package/packages/server/src/session-diff.ts +21 -21
  99. package/packages/server/src/terminal-manager.ts +61 -20
  100. package/packages/server/src/tunnel.ts +42 -28
  101. package/packages/shared/package.json +10 -3
  102. package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
  103. package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
  104. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
  105. package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
  106. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
  107. package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
  108. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
  109. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
  110. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
  111. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
  112. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
  113. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
  114. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
  115. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
  116. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
  117. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
  118. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
  119. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
  120. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
  121. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
  122. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
  123. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
  124. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
  125. package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
  126. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
  127. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
  128. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
  129. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
  130. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
  131. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
  132. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
  133. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
  134. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
  135. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
  136. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
  137. package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
  138. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
  139. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
  140. package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
  141. package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
  142. package/packages/shared/src/__tests__/config.test.ts +56 -0
  143. package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
  144. package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
  145. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
  146. package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
  147. package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
  148. package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
  149. package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
  150. package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
  151. package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
  152. package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
  153. package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
  154. package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
  155. package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
  156. package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
  157. package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
  158. package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
  159. package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
  160. package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
  161. package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
  162. package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
  163. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
  164. package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
  165. package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
  166. package/packages/shared/src/bootstrap-install.ts +212 -0
  167. package/packages/shared/src/bridge-register.ts +87 -20
  168. package/packages/shared/src/browser-protocol.ts +71 -1
  169. package/packages/shared/src/config.ts +87 -15
  170. package/packages/shared/src/managed-paths.ts +31 -4
  171. package/packages/shared/src/openspec-poller.ts +63 -46
  172. package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
  173. package/packages/shared/src/platform/commands.ts +100 -0
  174. package/packages/shared/src/platform/detached-spawn.ts +305 -0
  175. package/packages/shared/src/platform/exec.ts +220 -0
  176. package/packages/shared/src/platform/git.ts +155 -0
  177. package/packages/shared/src/platform/index.ts +15 -0
  178. package/packages/shared/src/platform/npm.ts +162 -0
  179. package/packages/shared/src/platform/openspec.ts +91 -0
  180. package/packages/shared/src/platform/paths.ts +276 -0
  181. package/packages/shared/src/platform/process-identify.ts +126 -0
  182. package/packages/shared/src/platform/process-scan.ts +94 -0
  183. package/packages/shared/src/platform/process.ts +168 -0
  184. package/packages/shared/src/platform/runner.ts +369 -0
  185. package/packages/shared/src/platform/shell.ts +44 -0
  186. package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
  187. package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
  188. package/packages/shared/src/recommended-extensions.ts +18 -2
  189. package/packages/shared/src/resolve-jiti.ts +62 -3
  190. package/packages/shared/src/rest-api.ts +26 -0
  191. package/packages/shared/src/semaphore.ts +83 -0
  192. package/packages/shared/src/tool-registry/definitions.ts +342 -0
  193. package/packages/shared/src/tool-registry/index.ts +56 -0
  194. package/packages/shared/src/tool-registry/overrides.ts +118 -0
  195. package/packages/shared/src/tool-registry/registry.ts +262 -0
  196. package/packages/shared/src/tool-registry/strategies.ts +198 -0
  197. package/packages/shared/src/tool-registry/types.ts +180 -0
package/AGENTS.md CHANGED
@@ -18,8 +18,10 @@ See [docs/architecture.md](docs/architecture.md) for full details.
18
18
 
19
19
  ```bash
20
20
  npm install # Install dependencies
21
- npm test # Run all tests (vitest) — ISOLATED HOME, safe for live sessions
22
- npm run test:watch # Watch mode — also isolated
21
+ npm test # Run all tests (vitest)
22
+ npm run test:watch # Watch mode
23
+ npm run test:bootstrap # Run the bootstrap resolution harness only
24
+ npm run test:bootstrap:watch # Bootstrap harness in watch mode
23
25
  npm run build # Build web client (Vite)
24
26
  npm run dev # Start Vite dev server
25
27
  npm run reload # Reload all connected pi sessions
@@ -28,69 +30,6 @@ pi-dashboard # Start dashboard server
28
30
  pi-dashboard --dev # Start with Vite proxy
29
31
  ```
30
32
 
31
- ## Test Isolation (READ BEFORE RUNNING VITEST)
32
-
33
- **Always run tests via `npm test` or `npm run test:watch`.** Never run `npx vitest` / `vitest` directly without also setting `HOME` to an ephemeral directory first.
34
-
35
- ### Why this matters
36
-
37
- Integration tests call `createServer()`, which on startup runs destructive sweeps against `$HOME/.pi/dashboard/`:
38
- - `headlessPidRegistry.cleanupOrphans()` — reads `headless-pids.json` and may SIGTERM PIDs it considers orphans (including the **live pi process running the bridge**).
39
- - `headlessPidRegistry.killAll()` — SIGTERMs every tracked PID on server shutdown.
40
- - `editorPidRegistry.cleanupOrphans()` — SIGTERMs orphan code-server processes.
41
-
42
- If tests run against your real `$HOME`, these sweeps can **kill the pi session that launched the tests**. They also write `.meta.json` sidecars into `~/.pi/agent/sessions/`, racing live bridges and corrupting real session state.
43
-
44
- ### How isolation works
45
-
46
- Three independent layers (defense-in-depth):
47
-
48
- 1. **Process-level HOME override** (`package.json` `test` / `test:watch` scripts): `HOME=$(mktemp -d -t pi-test-XXXXXX) vitest ...` — vitest starts with a fresh HOME under `os.tmpdir()`.
49
- 2. **globalSetup tripwire** (`packages/shared/src/test-support/setup-home.ts`, wired via `globalSetup` in every `vitest.config.ts`): runs ONCE at vitest boot, throws if `process.env.HOME === os.userInfo().homedir` — aborts the run before any test file loads. Pre-creates `.pi/agent/sessions/` and `.pi/dashboard/` subdirs.
50
- 3. **Production-code guards** (`packages/server/src/test-env-guard.ts`): `headlessPidRegistry.cleanupOrphans`/`killAll` and `editorPidRegistry.cleanupOrphans` no-op with `console.warn` when `process.env.VITEST === "true"` AND HOME matches the real user home.
51
-
52
- ### Running tests safely
53
-
54
- ```bash
55
- npm test # Preferred — all isolation layers active
56
- npm run test:watch # Also safe
57
- ```
58
-
59
- If you must invoke vitest directly (e.g. for a single file):
60
-
61
- ```bash
62
- HOME=$(mktemp -d -t pi-test-XXXXXX) npx vitest run path/to/test.ts
63
- ```
64
-
65
- Without the `HOME=$(mktemp -d)` prefix, the globalSetup tripwire will abort the run with an instructive error — but **don't rely on that alone**: if you stash or delete the tripwire code, the prefix is your only remaining safety net.
66
-
67
- ### Writing integration tests
68
-
69
- Tests that boot a real `DashboardServer` SHOULD use the `createTestServer()` helper:
70
-
71
- ```ts
72
- import { createTestServer } from "@blackbelt-technology/pi-dashboard-server/test-support/test-server.js";
73
-
74
- const { server, httpPort, piPort, stop } = await createTestServer();
75
- // httpPort and piPort are OS-assigned (port: 0) — no hard-coded port ranges,
76
- // no risk of colliding with the live dashboard on :8000 / :9999.
77
- // ...
78
- await stop();
79
- ```
80
-
81
- Avoid hard-coding ports like `19070`, `19080`, `19090`, etc. in new tests — the helper handles port allocation and safe defaults (`autoShutdown: false`, `tunnel: false`).
82
-
83
- ### Verifying isolation after test changes
84
-
85
- ```bash
86
- find ~/.pi/agent/sessions -name "*.meta.json" -exec md5 -q {} \; | sort > /tmp/before.txt
87
- npm test
88
- find ~/.pi/agent/sessions -name "*.meta.json" -exec md5 -q {} \; | sort > /tmp/after.txt
89
- diff /tmp/before.txt /tmp/after.txt | grep -v "pi-agent-dashboard" # should be empty
90
- ```
91
-
92
- Files under the **current** session's directory (`~/.pi/agent/sessions/--Users-...-pi-agent-dashboard--/`) will legitimately change during the test run because the live bridge keeps writing heartbeats. Only files in **other** session directories indicate a leak.
93
-
94
33
  ## Cross-Platform QA Testing
95
34
 
96
35
  VM-based QA testing for verifying clean-state installation and runtime across platforms.
@@ -121,7 +60,8 @@ make clean # Destroy all cloned VMs
121
60
  | `src/shared/protocol.ts` | Extension↔Server WebSocket messages |
122
61
  | `src/shared/browser-protocol.ts` | Server↔Browser WebSocket messages (all message types including PromptBus `prompt_request`/`prompt_dismiss`/`prompt_cancel` must be in the `ServerToBrowserMessage` union — `as any` switch cases are stripped by esbuild in production) |
123
62
  | `src/shared/types.ts` | Data models (Session, Workspace, Event) |
124
- | `src/shared/config.ts` | Shared config loader (`~/.pi/dashboard/config.json`) |
63
+ | `src/shared/config.ts` | Shared config loader (`~/.pi/dashboard/config.json`). Includes `openspec: OpenSpecPollConfig` block (`pollIntervalSeconds` 5–3600, `maxConcurrentSpawns` 1–16, `changeDetection` `"mtime"\|"always"`, `jitterSeconds` 0–60) with clamping via `parseOpenSpecPollConfig`. See change: optimize-openspec-poll-burst |
64
+ | `src/shared/semaphore.ts` | Tiny FIFO semaphore (`createSemaphore(max)` → `{run, setMax, size}`). Used by `directory-service.ts` to cap concurrent `openspec` CLI spawns. Supports live resize via `setMax(n)` for runtime reconfig. |
125
65
  | `src/extension/bridge.ts` | Main extension entry point (composes sync/tracker/flow modules, tracks `isAgentStreaming` in persistent BridgeState) |
126
66
  | `src/extension/bridge-context.ts` | Shared mutable state type + helpers for bridge modules |
127
67
  | `src/extension/session-sync.ts` | Session register, replay, and switch/fork handling |
@@ -131,7 +71,7 @@ make clean # Destroy all cloned VMs
131
71
  | `src/extension/server-probe.ts` | TCP probe to detect running server |
132
72
  | `src/shared/server-identity.ts` | Identity-verified health check (`isDashboardRunning`) replacing bare TCP probes |
133
73
  | `src/shared/mdns-discovery.ts` | mDNS advertise/discover/browse for `_pi-dashboard._tcp` services |
134
- | `src/extension/server-launcher.ts` | Auto-start server as detached process |
74
+ | `src/extension/server-launcher.ts` | Auto-start server as detached process; captures **both stdout AND stderr** to `~/.pi/dashboard/server.log` (append mode) by passing `stdoutFd: logFd` alongside `logFd` — parity with `pi-dashboard start`'s `stdio: ["ignore", logFd, logFd]`. Exports pure `buildSpawnDetachedOptions` and `buildReadyTimeoutMessage`; the latter appends a `nodejs/node#58515` upgrade hint when `isKnownBadNode(process.version)` is true. |
135
75
  | `src/extension/command-handler.ts` | Command routing: `!`/`!!` bash, `/compact`, slash commands |
136
76
  | `src/extension/prompt-expander.ts` | Slash command → prompt template expansion (supports colon-to-hyphen aliasing: `/opsx:cmd` → `opsx-cmd.md`) |
137
77
  | `src/extension/dev-build.ts` | Dev build-on-reload helper (client build + server shutdown) |
@@ -149,7 +89,7 @@ make clean # Destroy all cloned VMs
149
89
  | `src/extension/prompt-bus.ts` | PromptBus — unified prompt routing to registered adapters (TUI, dashboard, custom). First-response-wins, cross-adapter dismissal. |
150
90
  | `src/extension/dashboard-default-adapter.ts` | Built-in PromptBus adapter that renders prompts as generic interactive dialogs in dashboard chat |
151
91
  | `src/client/lib/prompt-component-registry.ts` | Client-side component registry mapping prompt type strings to render metadata (placement, component) |
152
- | `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). Supports five methods: `confirm`, `select`, `multiselect`, `input`, and `batch` (multiple related questions in one call, answers returned as ordered array; cancel mid-batch returns partial results + `cancelled: true`). `prepareArguments` rescues common LLM drift: stringified `params`/`questions`/`options`, `question`/`header` → `title`, `input_type` wrapper flattening, `{label,value}[]` → `label[]` with warning surfaced via `details.warnings`. |
92
+ | `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) |
153
93
  | `src/shared/openspec-activity-detector.ts` | Detects OpenSpec activity from tool events; auto-attach requires only changeName (phase optional) |
154
94
  | `src/shared/openspec-poller.ts` | OpenSpec CLI polling (shared, used by server DirectoryService) |
155
95
  | `src/shared/state-replay.ts` | Synthesizes events from pi entries (shared, used by server + bridge) |
@@ -165,10 +105,11 @@ make clean # Destroy all cloned VMs
165
105
  | `src/server/idle-timer.ts` | Auto-shutdown idle timer with sleep-wake resilience |
166
106
  | `src/server/session-bootstrap.ts` | Startup session discovery and OpenSpec polling init |
167
107
  | `src/server/pi-gateway.ts` | Extension WebSocket gateway (port 9999) |
168
- | `src/server/browser-gateway.ts` | Browser WebSocket gateway (dispatches to handler modules; logs handler exceptions as `[browser-gw] handler error type=<msg.type>: <err>` instead of silently swallowing them — only malformed JSON frames are dropped silently) |
108
+ | `src/server/browser-gateway.ts` | Browser WebSocket gateway (dispatches to handler modules) |
169
109
  | `src/server/browser-handlers/handler-context.ts` | Shared context type for browser message handlers |
170
110
  | `src/server/browser-handlers/subscription-handler.ts` | Subscribe/unsubscribe with async batched replay, backpressure, lazy loading |
171
- | `src/server/browser-handlers/session-action-handler.ts` | Send prompt, abort, resume, spawn, shutdown, force kill, flow control |
111
+ | `src/server/browser-handlers/session-action-handler.ts` | Send prompt, abort, resume, spawn, shutdown, force kill, flow control. `handleForceKill` delegates the SIGTERM→wait→SIGKILL escalation to `killProcess` from `platform/process.ts` so Windows gets `taskkill /F /T /PID` (genuine tree kill). No direct `process.kill()` anywhere (enforced by `no-direct-process-kill.test.ts`). `handleSendPrompt` also intercepts `/reload` on headless sessions (gated by `headlessPidRegistry.getPid`) and delegates to `handleHeadlessReload`, which SIGTERMs the pi process and respawns with `--session <file> --mode continue` — pi 0.68.0 has no extension-accessible reload path in RPC mode, so kill-and-respawn is the only way to reload settings/extensions/skills for headless sessions. See changes: route-kill-paths-through-platform, headless-reload-via-respawn. |
112
+ | `src/server/browser-handlers/session-action-helpers.ts` | Pure helpers for session-action-handler. `shouldInterceptReload(msg, headlessPidRegistry)` gates the headless-reload interception: exact `/reload` text, no images, PID tracked in registry. Extracted for testability. |
172
113
  | `src/client/components/ImageLightbox.tsx` | Full-size image lightbox with zoom/pan (useZoomPan), Esc/backdrop close |
173
114
  | `src/client/components/CollapsedToolGroup.tsx` | Collapsed group of repeated tool calls with expand toggle |
174
115
  | `src/client/lib/group-tool-calls.ts` | Groups consecutive identical tool calls for chat display |
@@ -181,42 +122,54 @@ make clean # Destroy all cloned VMs
181
122
  | `src/client/components/ArchiveBrowserView.tsx` | Searchable archive browser: date-grouped list, two-level nav to artifact reader |
182
123
  | `src/client/hooks/useArchiveListing.ts` | Fetch hook + pure helpers (groupByDate, filterEntries) for archive endpoint |
183
124
  | `src/server/openspec-archive.ts` | Scans `openspec/changes/archive/` and returns structured ArchiveEntry list |
184
- | `packages/server/src/openspec-tasks.ts` | Parses an OpenSpec change's `tasks.md` (top-level `- [ ]` / `- [x]` lines under `## ` headings) and rewrites a single checkbox line atomically (tmp + rename, byte-for-byte preservation of other lines). Exports `parseTasksMarkdown`, `readTasks`, `toggleTask`, plus typed `NotFoundError` / `LineMismatchError` / `NotACheckboxError` consumed by `routes/openspec-routes.ts` to map to HTTP 404/409/400. |
185
- | `packages/client/src/lib/openspec-tasks-api.ts` | Client fetch helpers for `GET /api/openspec/tasks` and `POST /api/openspec/tasks/toggle`. Throws typed `LineMismatchError` on HTTP 409 so `TasksPopover` can refetch + show a banner without string-matching error text. |
186
- | `packages/client/src/components/StatePill.tsx` | Compact color-coded pill (zinc/blue/amber/green) showing `deriveChangeState(change)` next to the attached-change badge on the session card. Exports `stateToLabel` and the `STATE_PILL_CLASS` map for reuse. |
187
- | `packages/client/src/components/TasksPopover.tsx` | Portal-rendered popover (anchored to the session card's `Tasks N/M` button) listing every parseable `tasks.md` checkbox grouped by `## ` heading. Optimistic toggle; HTTP 409 triggers a refetch + “File changed” banner; broadcast-driven openspec poll keeps the card counts fresh after each tick.
188
125
  | `src/client/components/SessionOpenSpecActions.tsx` | Session-level OpenSpec: searchable attach dialog, action buttons, detach |
189
126
  | `src/client/components/DialogPortal.tsx` | Portal wrapper rendering dialogs at document.body with scroll lock |
190
- | `src/client/components/PinDirectoryDialog.tsx` | Dialog to pin a directory (wraps PathPicker). Mounted once at the app root in `App.tsx` via `DialogPortal`; opened by both the sidebar "Add folder" button and the LandingPage Step ② CTA through a shared `onOpenPinDialog` callback. |
191
- | `packages/client/src/components/LandingPage.tsx` | Empty-state onboarding view. Renders three cards (Setup credentials → Add folder → Start session) that collapse to ✔ rows once each step is satisfied. Accepts `{ providersReady, pinnedCount, sessionsCount, firstPinnedCwd, onOpenPinDialog, onSpawnSession, navigate }`. Falls back to the legacy `π + "Select a session to get started"` placeholder when no onboarding props are supplied (preserves existing test surface). |
192
- | `packages/client/src/hooks/useProvidersReady.ts` | Hook observing `GET /api/providers`: returns `{ loading, ready, count }` where `ready=true` iff at least one provider has a non-empty `apiKey`. Refetches on window `focus` and on `provider-auth-event` custom events. Used by `LandingPage` via `App.tsx` to drive Step ① state. |
127
+ | `src/client/components/PinDirectoryDialog.tsx` | Dialog to pin a directory (wraps PathPicker) |
193
128
  | `src/client/components/PathPicker.tsx` | Reusable keyboard-first path picker with typeahead directory list |
194
129
  | `src/client/lib/browse-api.ts` | Client-side browse API helper for PathPicker |
195
130
  | `src/server/browse.ts` | Directory listing logic for browse API endpoint |
196
131
  | `src/server/pi-resource-scanner.ts` | Discovers pi extensions, skills, prompts from local, global, and package sources |
197
- | `src/server/package-manager-wrapper.ts` | Thin adapter around pi's `DefaultPackageManager` with operation serialization, progress forwarding, and session reload. `loadPiPackageManager()` resolution order: (1) direct import of `@mariozechner/pi-coding-agent`, (2) managed install at `~/.pi-dashboard/node_modules/` (used by Electron portable/standalone on Windows where pi isn't globally installed), (3) `npm root -g`. Without step (2) portable Windows users see a red "pi-coding-agent is not installed" banner in the Packages tab. |
132
+ | `src/server/package-manager-wrapper.ts` | Thin adapter around pi's `DefaultPackageManager` with operation serialization, progress forwarding, and session reload; delegates module resolution to `ToolRegistry.resolveModule("pi-coding-agent")` |
133
+ | `src/shared/tool-registry/registry.ts` | `ToolRegistry` service — single-source resolver for every external binary/module (pi, pi-coding-agent, openspec, npm, node, tsx, git, zrok, pi-dashboard). Ordered strategy chain per tool, per-resolution diagnostic trail, in-memory cache, override-aware |
134
+ | `src/shared/tool-registry/definitions.ts` | Registers the standard tool set. Each definition declares an ordered strategy chain (override → bare-import → managed → npm-global → where) and a classifier (strategy → source) |
135
+ | `src/shared/tool-registry/strategies.ts` | Reusable resolution strategies: `overrideStrategy`, `managedBinStrategy`, `managedModuleStrategy`, `npmGlobalStrategy`, `whereStrategy`, `bareImportStrategy` (uses `createRequire` for sync probe). All take injectable `StrategyDeps` for tests |
136
+ | `src/shared/tool-registry/overrides.ts` | Read/write `~/.pi/dashboard/tool-overrides.json` (machine-local, separate from `config.json`). Lazy-loaded, atomic write via tmp+rename, malformed-file tolerant |
137
+ | `src/shared/tool-registry/types.ts` | `ToolDefinition`, `Strategy`, `StrategyResult`, `Resolution`, `Source`, `UnknownToolError`, `ModuleResolutionError` |
138
+ | `src/shared/tool-registry/index.ts` | Barrel export + `getDefaultRegistry()` singleton accessor |
139
+ | `src/server/routes/tool-routes.ts` | REST routes: `GET /api/tools`, `GET /api/tools/:name`, `POST /api/tools/rescan`, `PUT/DELETE /api/tools/:name`, `POST /api/tools/diagnostics` (text/plain export) |
140
+ | `packages/shared/src/bootstrap-install.ts` | Shared bootstrap installer (`bootstrapInstall({ packages, managedDir?, progress?, npmArgv?, env?, registry? })`, `bootstrapInstallDefaults`, `ensureManagedDir`, `resolveNpmArgv`). Single entry point for pi/openspec/tsx install into `~/.pi-dashboard/` — called from the Electron wizard (via `packages/electron/src/lib/dependency-installer.ts` wrapper that adds bundled-node + offline-cacache concerns) and from the CLI first-run path (`cli.ts runDegradedModeBootstrap`). See change: unified-bootstrap-install. |
141
+ | `packages/server/src/bootstrap-state.ts` | In-memory bootstrap state store (`createBootstrapState()`, `BootstrapState { status, progress, error, version, compatibility, bridgeRegistrationError }`, `BootstrapStateStore` with get/set/subscribe/dispose plus side-channel `setLastInstallPackages` / `getLastInstallPackages` so `POST /api/bootstrap/retry` re-runs the exact failed set instead of a hard-coded default). Partial `set()` supports `undefined` = clear semantics; `setLastInstallPackages` does NOT trigger subscribers (it's not part of the broadcast snapshot). |
142
+ | `packages/server/src/routes/bootstrap-routes.ts` | REST routes: `GET /api/bootstrap/status`, `POST /api/bootstrap/upgrade-pi` (202+ticketId or 409), `POST /api/bootstrap/retry` (202 if failed, else 409). Trigger callbacks are injected so CLI wires them to `bootstrapInstall` while tests wire them to spies. |
143
+ | `packages/server/src/bootstrap-queue.ts` | In-memory ticket queue (`createBootstrapQueue()`, `enqueue(handler)`, `flushAll()`, `size()`, `clear(reason)`, `onTicketComplete(listener)`). `server.ts` flushes on bootstrap-state transition to "ready" and wires `onTicketComplete` → `bootstrap_ticket_complete` WS broadcast so browsers holding a 202 ticketId learn the queued op's outcome. `session-api.ts gateOrEnqueue` uses the queue to defer session spawn during installs. On `clear`, pending tickets are rejected directly (reject closure stored on the entry) so no caller hangs at shutdown. |
144
+ | `packages/server/src/pi-version-skew.ts` | Pi compatibility range reader (`readPiCompatibility` reads `piCompatibility` from `packages/server/package.json`; `readCurrentPiVersion` via `createRequire`), comparator (`parseVersion`, `compareVersions`, `isBelow`, `isAbove` supporting `0.x` wildcard), + `updateBootstrapCompatibility(store, pkgPath)` that writes the result into `bootstrapState.compatibility` with a 60 s cache. Below-minimum adds a 503-blocking `error` message. |
145
+ | `packages/client/src/hooks/useBootstrapStatus.ts` | Client hook for bootstrap state. Fetches `/api/bootstrap/status` on mount, subscribes to `bootstrap-status` `CustomEvent` dispatched by `useMessageHandler` on `bootstrap_status_update` WS broadcasts. Exposes `{ state, isLoading, error, refresh, retry, upgradePi }`. |
146
+ | `packages/client/src/components/BootstrapBanner.tsx` | Banner mounted in `App.tsx` above `<MobileShell>`. Hidden at status="ready" with no compatibility hints; blue "Installing pi…" when installing; red "pi install failed — [Retry]" when failed; amber upgrade hints when `compatibility.upgradeRecommended` or `upgradeDashboard` is true. |
147
+ | `src/client/lib/tools-api.ts` | Client-side fetch helpers for `/api/tools*` (`fetchTools`, `rescanAll`, `rescanOne`, `setOverride`, `clearOverride`, `downloadDiagnostics`) |
148
+ | `src/client/components/ToolsSection.tsx` | Settings → General → **Tools** section. One row per registered tool: status badge, source, truncated path, expand-to-trail, override input, per-row rescan. Top-level: Rescan all / Reset overrides / Export diagnostics |
198
149
  | `src/server/npm-search-proxy.ts` | Cached proxy for npm registry search (`keywords:pi-package`) and README fetch |
199
150
  | `src/server/routes/package-routes.ts` | REST routes: search, readme, installed, install, remove, update, check-updates |
200
- | `packages/server/src/pi-core-checker.ts` | Discovers pi ecosystem CORE CLI packages (pi itself, pi-dashboard, pi-model-proxy, etc.) from global npm + `~/.pi-dashboard/node_modules/` and compares against the npm registry. Complements PackageManagerWrapper which only manages extensions in `settings.json packages[]`. 5-min cache. |
201
- | `packages/server/src/pi-core-updater.ts` | Runs `npm update -g <pkg>` (global install) or `npm update <pkg>` in `~/.pi-dashboard/` (managed install), streams progress via listener, acquires `PackageManagerWrapper.runExclusive()` busy-lock, auto-reloads sessions on any successful update. |
202
- | `packages/server/src/routes/pi-core-routes.ts` | REST routes: `GET /api/pi-core/versions[?refresh=true]`, `POST /api/pi-core/update` with `{ packages?: string[] }` (empty = update all with `updateAvailable`). 409 when busy, 400 on unknown package names. |
203
- | `packages/client/src/hooks/usePiCoreVersions.ts` | Fetch + 30-min poll hook for pi core version status. Refetches (with `?refresh=true`) when `pi_core_update_complete` arrives via the `pi-core-event` window event. |
204
- | `packages/client/src/components/PiCoreVersionsSection.tsx` | Settings → Packages tab section showing core packages with current → latest versions, per-package Update button, Update All, Check Now, live progress/error surfaced from WS `pi_core_update_progress`/`complete`. |
205
- | `packages/client/src/components/PiUpdateBadge.tsx` | Header badge (count of available pi core updates). Hidden when zero. Clicking navigates to `/settings?tab=packages`. Mounted next to `ServerSelector` in `App.tsx` `headerExtra`. |
206
151
  | `src/client/components/SortablePinnedGroup.tsx` | Drag-to-reorder wrapper for pinned directory groups |
207
152
  | `src/server/preferences-store.ts` | Global UI preferences (pinned dirs, session order) in `preferences.json` |
208
153
  | `src/server/meta-persistence.ts` | Per-session debounced `.meta.json` writer |
209
154
  | `src/server/session-scanner.ts` | Startup session discovery by scanning `~/.pi/agent/sessions/` |
210
155
  | `src/server/migrate-persistence.ts` | One-time migration from `sessions.json` + `state.json` to `.meta.json` |
211
156
  | `src/server/session-order-manager.ts` | Per-cwd session ordering with persistence |
212
- | `src/server/directory-service.ts` | Server-side session discovery, event loading, and OpenSpec polling |
157
+ | `src/server/directory-service.ts` | Server-side session discovery, event loading, and OpenSpec polling. Uses mtime-gated per-directory cache (`DirCache`), shared FIFO semaphore, and deterministic per-cwd `phaseOffsetMs(cwd, jitterSeconds)` jitter (FNV-1a 32-bit hash). `refreshOpenSpec(cwd)` bypasses the mtime gate but still acquires the semaphore. `reconfigurePolling(cfg)` applies live config changes without a restart. Pi-resources scan lives on its own 5×-interval timer so it doesn't stack with the openspec burst. See change: optimize-openspec-poll-burst |
213
158
  | `src/server/pending-fork-registry.ts` | Tracks pending fork operations for session placement |
214
159
  | `src/server/pending-resume-registry.ts` | Queues prompts for auto-resume of ended sessions |
215
160
  | `src/server/json-store.ts` | Atomic JSON file read/write helpers |
216
- | `src/server/process-manager.ts` | Session spawning via tmux or headless mode |
161
+ | `src/server/process-manager.ts` | Session spawning: dispatches via `platform/spawn-mechanism.ts` `selectMechanism` → `tmux` / `wt` / `wsl-tmux` / `headless`. All mechanisms forward `sessionFile`/`mode` uniformly via `sessionFlagsToArgv`. Windows headless uses `spawnDetached` primitive with `detached: true` (PGID-equivalent via libuv) and stderr-to-file-fd (crash-visible). |
162
+ | `src/shared/platform/detached-spawn.ts` | Three primitives: `spawnDetached` (libuv-correct detached defaults on every OS), `waitForNoCrash` (negative liveness — did it survive the window?), `waitForReady` (positive liveness — did a probe turn true?). All spawn sites with long-lived detached children delegate here. `SpawnDetachedOptions` exposes optional `stdoutFd` (defaults to `"ignore"`) and `logFd` (stderr, defaults to `"ignore"`); the bridge server-launcher sets both to the same fd for parity with the CLI. |
163
+ | `src/shared/platform/node-version-check.ts` | Pure predicate `isKnownBadNode(version)` + message builder `buildNodeVersionWarning(version)` for Node ranges affected by [nodejs/node#58515](https://github.com/nodejs/node/issues/58515) (22.0.0–22.17.x and 24.1.0–24.2.x). Consumed by `packages/server/src/cli.ts` (preflight warning), `packages/extension/src/server-launcher.ts` (ready-timeout hint), and `packages/electron/src/lib/doctor.ts` (Node runtime compatibility row). |
164
+ | `src/shared/platform/preload-fastify.ts` | Shared resolver `resolvePreloadFastifyPath(): string \| null` — returns absolute native path (never `file://`) to `packages/server/preload-fastify.cjs`. Used by all four server-spawn sites (cli daemon, bridge launcher, Electron lifecycle, restart-helper orchestrator) to inject `--require <preloadPath>` BEFORE `--import <jitiLoader>` so Fastify's CJS chain is cached before any ESM hook runs — sidesteps nodejs/node#58515 race-independently. See change: `preload-fastify-cjs`. |
165
+ | `packages/server/preload-fastify.cjs` | CommonJS preload file — loaded by Node's legacy synchronous CJS loader via `--require`. Populates `require.cache` with `fastify` + `@fastify/ajv-compiler` + `@fastify/ajv-compiler/standalone` so the ESM→CJS translator short-circuits on later imports. MUST stay `.cjs`; MUST NOT use ESM syntax. |
166
+ | `src/shared/platform/spawn-mechanism.ts` | `SpawnMechanism` enum (`tmux`/`wt`/`wsl-tmux`/`headless`) + pure `selectMechanism({ platform, userStrategy, electronMode, available })` selector. `buildWtArgs` builds argv for Windows Terminal `new-tab`. `sessionFlagsToArgv` is the uniform flag builder every mechanism MUST call. |
167
+ | `src/shared/platform/process-identify.ts` | `findPidByMarker` + `isProcessLikePi` + `isPiCommandLine` — consolidates the three `process.platform === "win32"` branches that previously lived inside `session-action-handler.ts`. Windows stubs are documented (command-line lookup goes via `headlessPidRegistry` instead). |
168
+ | `src/shared/platform/process.ts` | **Sole source of process termination + liveness primitives**: `isProcessAlive(pid)` (signal 0), `killProcess(pid, {timeoutMs})` (Windows `taskkill /F /T /PID`, POSIX `SIGTERM` → wait → `SIGKILL` tree kill), `killPidWithGroup(pid, sig)` (POSIX `kill(-pid, sig)`, Windows direct kill). Every `process.kill(...)` call outside this file is banned by `no-direct-process-kill.test.ts`. See change: route-kill-paths-through-platform. |
169
+ | `src/shared/__tests__/no-direct-process-kill.test.ts` | Repo-level lint: scans `packages/*/src/` (excluding platform/ and `__tests__/`) for `process.kill(` calls and fails with file:line if any are found. Mirrors `no-direct-child-process.test.ts`. |
170
+ | `src/shared/__tests__/bootstrap/` | In-memory bootstrap resolution harness (memfs-backed). `harness.ts` (withFakeEnv + layer), `fixtures/` (managed/npm-g/electron/dev-monorepo/settings-json layouts), `assertions.ts` (snapshotTrail + snapshotSettingsDelta with `<HOME>` / `<NPM_ROOT>` normalization), `scenarios.ts` (1080-cell cube: platform × dash × pi × settings × env), `scenarios-skipped.ts` (bulk-skip manifest with documented reasons), `cube.test.ts` (fail-closed sweep), `families/*.test.ts` (30+ registered scenario cells across A-K). Run via `npm run test:bootstrap`. See change: bootstrap-resolution-harness. |
217
171
  | `src/server/editor-registry.ts` | Detects available native editors (running processes + CLI) |
218
172
  | `src/server/editor-manager.ts` | Lifecycle manager for code-server child processes (spawn, stop, idle, heartbeat) |
219
- | `packages/server/src/editor-pid-registry.ts` | Persists spawned code-server PIDs to `~/.pi/dashboard/editor-pids.json` and sweeps orphaned code-server processes on server boot (runs in `server.start()` before `fastify.listen`). Verifies ownership via cmdline check against `~/.pi/dashboard/editors/` prefix to avoid killing unrelated user-run code-server instances. SIGTERM → 1s grace → SIGKILL escalation. |
220
173
  | `src/server/editor-proxy.ts` | Reverse proxy for `/editor/:id/*` to code-server instances |
221
174
  | `src/server/editor-detection.ts` | Auto-detect code-server/openvscode-server binary on PATH |
222
175
  | `src/server/routes/editor-routes.ts` | REST routes: editor start, stop, heartbeat, status, detect |
@@ -226,9 +179,13 @@ make clean # Destroy all cloned VMs
226
179
  | `src/server/provider-auth-handlers.ts` | Pi provider OAuth handlers (Anthropic, Codex, GitHub Copilot, Gemini CLI, Antigravity) |
227
180
  | `src/server/provider-auth-storage.ts` | Read/write ~/.pi/agent/auth.json with lockfile for pi provider credentials |
228
181
  | `src/server/routes/provider-auth-routes.ts` | REST routes: provider OAuth authorize/exchange/callback, device-code, API key CRUD |
182
+ | `src/server/routes/provider-routes.ts` | REST routes: custom LLM provider CRUD (`GET/PUT /api/providers`) + **`POST /api/providers/test`** connection probe (reuses `provider-probe.ts`). See change: `hot-reload-custom-providers` |
183
+ | `src/server/provider-probe.ts` | Pure per-API-type probe builders (`buildProbeRequest` for `openai-completions`/`openai-responses`/`anthropic-messages`/`google-generative-ai`) + `resolveProbeApiKey` (handles literal, `$ENV_VAR`, and `***` REDACTED sentinel via injected providers reader) + I/O-bearing `probeProvider` (8 s timeout, never echoes apiKey in error text, caps body excerpts at 500 chars). Used by the `/api/providers/test` route and shared with the bridge's discovery path |
184
+ | `src/extension/provider-register.ts` | Reads `~/.pi/agent/providers.json`, calls `pi.registerProvider()` with auto-discovered models, exports `reloadProviders(pi)` + `onProviderChanged(cb)`. `reloadProviders` diffs the current file against a module-level `lastRegistered` snapshot and applies add/remove/change via `registerEntry` / `pi.unregisterProvider`. Called from the bridge's `credentials_updated` handler BEFORE `modelRegistry.refresh()` so new providers appear in `/model` without a session restart. Every discovered model is enriched via `enrichModelMetadata(id, api, probe)` where `probe` wraps pi's `modelRegistry.find()` (captured from `ctx.modelRegistry` at the first `session_start` event; `model_select` is a fallback capture point) — this resolves accurate `contextWindow`, `maxTokens`, `reasoning`, `cost`, and `input` for catalog-known models (e.g., `proxy/cc/claude-opus-4-7` → 1M ctx / reasoning / Opus pricing) and falls back to api-appropriate defaults otherwise (`anthropic-messages` → 200k/64k, `google-generative-ai` → 1M/65k, `openai-completions` → 128k/16k). The fallback path keeps `input: ["text", "image"]` so pasted images reach vision-capable models without pi-ai's `downgradeUnsupportedImages` stripping them client-side; text-only models either ignore the image silently or return a 400 that the user sees. See changes: enrich-custom-provider-model-metadata, enable-image-input-custom-providers |
185
+ | `src/client/lib/providers-api.ts` | Client-side fetch helper: `testProvider({ name?, baseUrl, apiKey, api })` → structured `{ ok, status?, modelCount?, sample?, error? }`. Used by the **Test** button on the LLM Providers card |
229
186
  | `src/client/components/ProviderAuthSection.tsx` | Settings section: OAuth login buttons, device-code modal, API key inputs |
230
187
  | `src/server/auth-plugin.ts` | Fastify plugin: auth routes, onRequest hook, WS upgrade validation |
231
- | `src/server/config-api.ts` | Config REST API: read (redacted), write (partial merge), secret preservation |
188
+ | `src/server/config-api.ts` | Config REST API: read (redacted), write (partial merge), secret preservation. `writeConfigPartial` auth-merge propagates `secret`, `providers`, `allowedUsers`, `bypassHosts`, `bypassUrls` (the last two added by change `fix-trusted-networks-no-oauth` — silently dropped before, breaking Trusted Networks saves for users without OAuth). |
232
189
  | `src/client/components/SettingsPanel.tsx` | Settings UI: all dashboard config fields, grouped form, save to server |
233
190
  | `src/client/hooks/useAuthStatus.ts` | Client auth status hook and login redirect helper |
234
191
  | `src/server/localhost-guard.ts` | Network access guard: `createNetworkGuard` (loopback/trusted/authenticated), `isBypassedHost` (CIDR/wildcard/exact), netmask-to-CIDR helpers |
@@ -240,25 +197,22 @@ make clean # Destroy all cloned VMs
240
197
  | `src/server/routes/known-servers-routes.ts` | REST routes: known servers CRUD, on-demand mDNS discovery scan |
241
198
  | `src/server/terminal-manager.ts` | PTY lifecycle, ring buffer, spawn/attach/kill terminals |
242
199
  | `src/server/terminal-gateway.ts` | Binary WebSocket upgrade handler for `/ws/terminal/:id` |
243
- | `scripts/fix-pty-permissions.cjs` | Postinstall: locates `node-pty` via `require.resolve("node-pty/package.json")` (hoist-aware) and chmods every `prebuilds/*/spawn-helper` and `prebuilds/*/pty.node` to `0o755`. Wired up at the workspace-root `postinstall` so it applies regardless of which workspace triggered `npm install`. |
244
- | `src/server/fix-pty-permissions.ts` | Runtime spawn-helper permission fix called once by `createTerminalManager()`, uses `createRequire().resolve("node-pty")` to handle any install layout including Electron bundles |
245
- | `src/server/tunnel.ts` | Zrok tunnel with reserved shares for persistent URLs. Serialized creation via in-flight promise (`pendingCreate`) prevents parallel reservations from UI double-clicks. `releaseShare(token)` + `scavengeOrphanZrokProcesses(port)` clean up leaked reservations and processes on startup (unconditional when zrok binary is present, even in `--no-tunnel`), on `/api/restart`, `/api/shutdown`, and `/api/tunnel-disconnect`. Retry cap of 1 prevents cascade leaks; timeout path SIGTERM→SIGKILL escalation + release just-in-time tokens. |
246
- | `packages/client/vite.config.ts` | Client build. `rollupOptions.output.manualChunks` splits bundle into vendor chunks (`react-vendor`, `markdown`, `syntax`, `diff`, `xterm`, `dnd`, `util`) so the initial chunk is ~570 KB (~150 KB gzipped) instead of a 3 MB monolith — avoids tunnel abort thresholds on large assets. |
247
- | `packages/client/scripts/precompress.mjs` | Post-build step (runs from `build` / `prepare`): zero-dependency Node script that writes `.gz` siblings for every compressible file in `dist/`. `@fastify/static` serves them with `preCompressed: true` so responses ship a stable `Content-Length` header — avoids streaming-compression quirks in intermediate HTTP/2 proxies. |
248
- | `packages/server/src/server.ts` | Fastify server. Registers `@fastify/compress` globally with gzip+deflate (brotli intentionally disabled — zrok free proxy stream-resets `content-encoding: br`). Serves static client with `preCompressed: true`. CORS callback allows localhost, the active zrok tunnel URL (via `getTunnelUrl()`), any `*.share.zrok.io` host, and configured origins; on mismatch returns `cb(null, false)` rather than throwing — throwing causes `@fastify/cors` to surface HTTP 500 on every asset response (the hidden root cause of browser-only 500s over zrok tunnels, since Vite's `<script type="module" crossorigin>` entries always request in CORS mode). |
200
+ | `scripts/fix-pty-permissions.cjs` | Postinstall: fix node-pty spawn-helper execute permissions |
201
+ | `src/server/tunnel.ts` | Zrok tunnel with reserved shares for persistent URLs, binary detection, PID tracking, stale cleanup |
249
202
  | `src/client/components/TunnelButton.tsx` | Unified tunnel/QR button — tunnel icon when not set up, QR icon when inactive, green QR icon when connected; opens QR dialog with disconnect/setup |
250
203
  | `src/client/components/QrCodeDialog.tsx` | QR code dialog showing tunnel URL as scannable QR code with copy, disconnect, and setup buttons |
251
204
  | `public/manifest.json` | PWA web app manifest for installability |
252
205
  | `public/sw.js` | Minimal service worker for PWA installability |
253
206
  | `src/client/components/ZrokInstallGuide.tsx` | OS-aware zrok installation guide view (macOS/Linux/Windows) |
254
- | `src/server/cli.ts` | CLI entry point with subcommands (start/stop/restart/status) |
207
+ | `src/server/cli.ts` | CLI entry point with subcommands (start/stop/restart/status); `findPortHolders` is cross-platform (netstat/taskkill on Windows, lsof on Unix) and `server.log` is opened append-mode with timestamped headers |
208
+ | `src/server/restart-helper.ts` | Cross-platform `/api/restart` orchestrator: spawns a detached `node -e` child using only Node built-ins (net, http) — no sh/lsof/curl dependency; exports pure `buildOrchestratorScript(params)` for testing |
209
+ | `src/shared/resolve-jiti.ts` | Resolves pi's jiti register hook as a `file://` URL (required for `node --import` on Windows); exports pure `buildJitiRegisterUrl(pkgJsonPath)` helper and `resolveJitiFromAnchor(anchorPath)` for managed-install/system-pi callers |
210
+ | `src/shared/platform/paths.ts` | OS-aware path primitives: `normalizePath`, `samePath` (filesystem-level equality), `parsePathInput` (picker input), `withTrailingSep`, `isFilesystemRoot`. All accept optional trailing `platform: NodeJS.Platform` for testability. Windows multi-drive invariant: A:\, B:\, C:\ never merge; bare `B:` input treated as drive root, not cwd-relative. Exported from `platform/index.ts` as `paths.*` namespace alongside `git.*`, `openspec.*`, `npm.*` |
211
+ | `src/client/lib/session-grouping.ts` | `inferPlatform(samples)` heuristic (backslash/drive-letter = Windows, leading `/` = POSIX) + `groupSessionsByDirectory` that uses `normalizePath`-keyed Maps so sessions group under their pinned folder across separator/case/trailing drift |
212
+ | `src/shared/platform/` | Unified cross-OS primitives (see `index.ts` barrel). Sub-modules: `exec.ts` (**the only module that imports `node:child_process`** — wraps `execSync`/`exec`/`execFile`/`spawn`/`spawnSync` with `windowsHide: true` by default; enforced by `no-direct-child-process.test.ts`), `runner.ts` (the Recipe engine — `run(recipe, input)` resolves binaries via `ToolResolver`, applies timeout / tolerate / error normalization, returns `Result<T>`), `git.ts` / `openspec.ts` / `npm.ts` (Recipe-based tool modules — typed functions like `git.diff(...)`, `openspec.list(...)`, `npm.rootGlobal()` that never touch `child_process` or `process.platform`), `binary-lookup.ts` (`where`/`which`, `.cmd` ext, `ToolResolver` class), `process.ts` (`findPortHolders`, `killProcess`, `isProcessAlive`, `killPidWithGroup`, `parseNetstatListeners`), `process-scan.ts` (`isProcessRunning` via pgrep/tasklist, `parseEtime`), `shell.ts` (`detectShell` for SHELL/COMSPEC, `getTerminalEnvHints`), `commands.ts` (`openBrowser`, `isVirtualMachine`). All exported helpers that depend on OS take an optional `platform: NodeJS.Platform` parameter so tests can exercise both branches without mutating `process.platform`. See changes: consolidate-platform-handlers, platform-command-executor. |
255
213
  | `src/shared/rest-api.ts` | REST API type definitions |
256
214
  | `scripts/reload-all.sh` | Build bridge + reload all pi sessions |
257
- | `CHANGELOG.md` | Human-authored release notes per version (Keep a Changelog 1.1.0); source of GitHub Release bodies. Contributors append bullets to `## [Unreleased]`; release author promotes at tag time. |
258
- | `packages/shared/src/test-support/setup-home.ts` | Vitest `globalSetup` module. Runs ONCE at vitest boot. Tripwire: throws if `process.env.HOME === os.userInfo().homedir` (i.e. tests would run against real user home), aborting the entire run before any test file loads. Warns if HOME is outside `os.tmpdir()`. Pre-creates `<HOME>/.pi/agent/sessions/` and `<HOME>/.pi/dashboard/`. Wired via `test.globalSetup` in every package's `vitest.config.ts`. |
259
- | `packages/server/src/test-env-guard.ts` | `isUnsafeTestHomeScan()` — returns true when `VITEST=true` AND HOME matches `os.userInfo().homedir`. Gates destructive sweeps in `headlessPidRegistry.cleanupOrphans/killAll` and `editorPidRegistry.cleanupOrphans` so tests never SIGTERM live pi processes even if higher isolation layers are bypassed. |
260
- | `packages/server/src/test-support/test-server.ts` | `createTestServer(overrides)` helper — boots a real `DashboardServer` on OS-assigned ports (`port: 0`, `piPort: 0`) with safe defaults (no auto-shutdown, no tunnel). Returns `{ server, httpPort, piPort, stop }`. Prefer over hard-coded `19xxx` port constants in new integration tests. |
261
- | `docs/release-process.md` | Canonical how-to for cutting a release: commit conventions, Unreleased promotion, version bump, tag + push, CI behavior, manual fallback. |
215
+ | `scripts/sync-versions.js` | Post-bump release helper. Reads every workspace `package.json`, enforces lockstep versions, rewrites every inter-package dep specifier (e.g. `"@blackbelt-technology/pi-dashboard-shared": "^<ver>"`) to the current bumped version. Called by `release-cut` skill AND by `.github/workflows/publish.yml` (defensively) between `npm version -ws` and `npm run build`. Required because the npm CLI does not implement the pnpm/yarn `workspace:` protocol — we use plain semver ranges and sync them at bump time. Cross-ref specifiers use plain `"^<ver>"`; `packages/electron` is `"private": true` so `npm publish -ws` skips it automatically. |
262
216
  | `src/client/components/PiResourcesView.tsx` | Content area view for browsing pi extensions, skills, and prompts (with Installed/Packages tabs) |
263
217
  | `src/client/components/PackageBrowser.tsx` | Reusable inline package browser: npm search, type filters, install/uninstall, manual URL input |
264
218
  | `src/client/components/PackageCard.tsx` | Package card with type badges, downloads, install/uninstall actions |
@@ -281,9 +235,7 @@ make clean # Destroy all cloned VMs
281
235
  | `src/client/components/TerminalsView.tsx` | Tabbed terminal container per folder (tab bar, keep-alive, rename) |
282
236
  | `src/client/components/EditorView.tsx` | code-server iframe embedding with lazy start and heartbeat |
283
237
  | `src/client/components/EditorInstallGuide.tsx` | Platform-specific code-server installation guide |
284
- | `src/client/components/FolderActionBar.tsx` | Unified action bar per folder: +Session, Terminals(N), Editor, Zed, Pi Resources |
285
- | `packages/client/src/hooks/useImagePaste.ts` | Shared clipboard-image-paste hook (pendingImages, imageError, handlePaste, removeImage, clearImages). Used by `CommandInput` and `ExploreDialog` so behavior is identical across both prompt surfaces. Exports `MAX_IMAGE_SIZE` and `SUPPORTED_IMAGE_TYPES`. |
286
- | `packages/client/src/components/ImagePreviewStrip.tsx` | Shared image preview strip: error banner + horizontal thumbnail row with remove buttons; thumbnails open `ImageLightbox`. Used by `CommandInput` and `ExploreDialog`. |
238
+ | `src/client/components/FolderActionBar.tsx` | Unified action bar per folder: +Session, +Terminal, Terminals(N), Editor, Zed, Pi Resources |
287
239
  | `src/client/lib/folder-encoding.ts` | Base64url encode/decode for folder paths in URL routes |
288
240
  | `src/shared/editor-types.ts` | Editor instance types shared across components |
289
241
  | `src/client/components/TerminalCard.tsx` | Sidebar card for terminal sessions (cyan accent) |
@@ -292,13 +244,11 @@ make clean # Destroy all cloned VMs
292
244
  | `src/client/components/MobileActionMenu.tsx` | Kebab menu for session actions on mobile (includes OpenSpec commands) |
293
245
  | `src/client/components/MobileOverlay.tsx` | Hamburger button and sidebar overlay for mobile |
294
246
  | `src/client/components/SessionHeader.tsx` | Session header with OpenSpec attach/detach, flow launcher, MobileAttachButton |
295
- | `packages/client/src/components/PiLogo.tsx` | Inline-SVG Pi brand-mark component. `fill="currentColor"`, transparent background, themeable via parent `color`. Used in both sidebar headers (`SessionList.tsx`, `SessionSidebar.tsx`). |
296
- | `packages/client/src/components/SessionSidebar.tsx` | Alternate sidebar; header brand button renders `<PiLogo size={24} />` (inherits `text-blue-500 hover:text-blue-400`) instead of a literal `π` glyph. |
297
- | `packages/client/src/components/SessionList.tsx` | Desktop sidebar; header brand button uses `<PiLogo />`. Pin-folder button renders `📌 Add folder` label (tooltip: "Pin a folder to the sidebar") instead of icon-only `📌+`. |
298
- | `packages/client/src/index.css` | Global styles. `.card-working-pulse` layers a 45° repeating-linear-gradient (amber, `background-size: 28.2843px` = one full diagonal period) over a flat amber tint; `background-position` scrolls **horizontally** by `56.5685px` (2 periods) over 2s linear — scroll must be across stripes, diagonal scroll is pattern-invariant. An opacity pulse `0.6 → 1 → 0.6` runs in parallel over 3s ease-in-out (coprime with 2s stripes so they never lock). `prefers-reduced-motion: reduce` disables both animations but keeps the static stripe pattern as a state cue. `.card-input-pulse` (ask_user) intentionally stays pulse-only — stripes = working, pulse-only = waiting on you. |
299
- | `packages/client/vite.config.ts` | Client build config. `publicDir: "../../../public"` — resolved relative to `root: "src"`, three `../` hops reach the project-root `public/` directory (icon-192.png, icon-512.png, manifest.json, sw.js). Earlier `"../../public"` silently resolved to a non-existent `packages/public/` and caused all PWA assets to 404 in production. |
300
247
  | `src/client/hooks/useSwipeBack.ts` | iOS-style left-edge swipe-back gesture (40px edge zone, document-level listeners) |
301
248
  | `src/client/components/ChatView.tsx` | Chat message view with scroll-lock: pauses auto-scroll when user scrolls up, floating scroll-to-bottom button, per-session scroll position persistence |
249
+ | `src/client/components/CommandInput.tsx` | Chat textarea + autocomplete + controlled draft / history. Props: `sessionId`, `draft`, `onDraftChange` (lifted to App.tsx so the draft survives navigation), `history` (newest-first user prompts for this session). `ArrowUp`/`ArrowDown` walk history bash-style (only when no dropdown is open AND caret is on first/last line); `Escape` during history mode restores the in-progress draft. See change: chat-input-draft-and-history |
250
+ | `src/client/lib/draft-storage.ts` | Per-session chat input draft persistence helpers: `readAllDrafts()`, `writeDraft(sid, text)`, `deleteDraft(sid)`, key prefix `chat-draft:`. Wraps `localStorage` in try/catch (private-mode / quota safe). |
251
+ | `src/client/lib/message-history.ts` | `extractUserPromptHistory(messages)` — pure filter+dedup over `ChatMessage[]` returning newest-first user prompts with consecutive duplicates collapsed. Drives `ArrowUp`/`ArrowDown` history recall in `CommandInput`. |
302
252
  | `src/client/lib/mobile-depth.ts` | Pure function computing MobileShell depth from route state |
303
253
  | `src/client/hooks/useZoomPan.ts` | Reusable zoom/pan hook (wheel, drag, pinch, buttons) |
304
254
  | `src/client/hooks/useMessageHandler.ts` | WebSocket message dispatch hook (extracted from App.tsx) |
@@ -329,8 +279,6 @@ make clean # Destroy all cloned VMs
329
279
  | `src/client/lib/diff-tree.ts` | Directory tree builder from flat file paths |
330
280
  | `src/server/session-api.ts` | REST wrappers for WebSocket-only session operations (prompt, abort, spawn, resume, etc.) |
331
281
  | `.pi/skills/pi-dashboard/SKILL.md` | Bundled skill: monitor and control the dashboard from any pi session |
332
- | `.pi/skills/release-cut/SKILL.md` | Skill: cut a new release — pre-flight (clean tree, tests, build), CHANGELOG `[Unreleased]` → versioned promotion, workspace version bump, `chore(release): v<version>` commit, tag + push develop. Stops at the CI-generated draft GitHub Release per `docs/release-process.md`. |
333
- | `.pi/skills/release-revoke/SKILL.md` | Skill: revoke/rollback a release — delete GitHub Release (draft or published) via `gh release delete`, remove git tag locally + on origin, `npm deprecate` the version (never `npm unpublish` — blocked after 72h / with dependents), optionally revert the `chore(release): v<version>` commit. Handles partial CI failures gracefully. |
334
282
  | `.pi/skills/pi-dashboard/references/api-reference.md` | Complete REST API reference for the skill |
335
283
  | `.pi/skills/pi-dashboard/references/recipes.md` | Multi-step orchestration recipes |
336
284
  | `.pi/skills/pi-dashboard/scripts/dashboard-api.sh` | Helper script with port auto-detection and auth |
@@ -359,6 +307,15 @@ make clean # Destroy all cloned VMs
359
307
  | `packages/electron/scripts/docker-make.sh` | Docker entrypoint: platform-aware native module handling, ZIP for Windows |
360
308
  | `packages/electron/scripts/Dockerfile.build` | Docker image for cross-platform builds (node:22-bookworm-slim + build tools) |
361
309
  | `packages/electron/scripts/bundle-server.sh` | Bundles dashboard server source + deps into resources/server/ (--source-only for cross-builds) |
310
+ | `packages/electron/offline-packages.json` | Pinned versions of pi-coding-agent / openspec / tsx that get bundled as an offline npm cacache per release (see change: `electron-offline-bundled-packages`) |
311
+ | `packages/electron/scripts/bundle-offline-packages.sh` | Build-time script that runs `npm install --os=<os> --cpu=<cpu> --ignore-scripts` for the pinned versions, tars the resulting `_cacache/` (pax format — ustar is too narrow), writes `manifest.json` with SHA-256, and enforces a 100 MB hard budget. Opt-in via `BUNDLE_OFFLINE_PACKAGES=1`. |
312
+ | `packages/electron/resources/offline-packages/manifest.json` | Offline-cache manifest (`{bundledAt, targetPlatform, tarball, tarballBytes, sha256, packages}`). Consumed at runtime by `dependency-installer.ts` via `resolveOfflinePackages()`. |
313
+ | `packages/electron/resources/offline-packages/npm-cache.tar.gz` | gzipped npm cacache used by the first-run offline install. Extracted to `~/.pi-dashboard/.offline-cache/`, verified by SHA-256, consumed by `npm install --offline`, then deleted to reclaim ~140 MB. |
314
+ | `packages/electron/src/lib/offline-packages.ts` | Pure helpers: `parseOfflineManifest`, `resolveOfflinePackages`, `fileSha256`, `extractOfflineCache`, `buildOfflineInstallArgs`, `selectInstallStrategy`. 19 unit tests in `packages/electron/src/__tests__/offline-packages.test.ts`. |
315
+ | `packages/electron/scripts/bundle-recommended-extensions.sh` | Opt-in (via `BUNDLE_RECOMMENDED_EXTENSIONS=1`) build-time bundler that clones each id in `BUNDLED_EXTENSION_IDS` into `packages/electron/resources/bundled-extensions/<id>/`, records `.bundled-sha`, enforces SPDX allowlist (MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC) and a 15 MB total size budget. See change: bundle-first-party-extensions. |
316
+ | `packages/electron/src/lib/dependency-installer.ts` → `installBundledExtensions` | First-run activation of pre-bundled extensions. Copies `<resourcesPath>/bundled-extensions/<id>/` into pi's git cache (`~/.pi/agent/git/<host>/<path>/`), runs `npm install --omit=dev` if runtime deps declared, then calls `manager.addSourceToSettings(gitUrl)` + `settingsManager.flush()`. Returns activated ids so `installRecommendedExtensions` can skip them with `output: "Already installed (bundled)"`. |
317
+ | `packages/electron/src/lib/wizard-badge.ts` | Pure `classifyProgressBadge(output)` helper (`bundled` / `system` / null) shared between wizard HTML inline JS and unit tests. |
318
+ | `packages/shared/src/recommended-extensions.ts` → `BUNDLED_EXTENSION_IDS` | Single source of truth for which recommended ids ship bundled in the Electron installer. Currently `["pi-anthropic-messages", "pi-flows"]` (see design.md for pi-flows license blocker). |
362
319
  | `packages/electron/scripts/docker-make.sh` | Docker entrypoint: bundles server, installs native deps, runs Forge make |
363
320
  | `packages/electron/scripts/Dockerfile.build` | Docker image for cross-platform builds (node:22-bookworm-slim + build tools) |
364
321
  | `packages/electron/scripts/test-server-launch.sh` | Docker-based test for server launch on clean Linux |
@@ -366,12 +323,6 @@ make clean # Destroy all cloned VMs
366
323
  | `packages/electron/scripts/test-electron-install-inner.sh` | Inner test script run inside Docker container |
367
324
  | `packages/electron/resources/icon.png` | Master 1024×1024 app icon (π on dark navy) |
368
325
  | `.github/workflows/publish.yml` | CI: builds DMG (macOS), DEB+AppImage (Linux), NSIS+ZIP+portable (Windows) on native runners; publishes npm + GitHub Release |
369
- | `.github/workflows/deploy-site.yml` | CI: builds marketing site in `/site` with Astro, enforces 50 KB gzipped JS budget, deploys to GitHub Pages via `actions/deploy-pages@v4` on push to `develop` when `site/**` changes |
370
- | `site/` | Public marketing site (Astro + Tailwind + MDX + Preact). Self-contained, published at `BlackBeltTechnology.github.io/pi-agent-dashboard` (later `pi-dashboard.dev`). Uses Pi-blue palette, Supabase-style playful bento grid, 4-state storytelling hero animation. |
371
- | `site/src/components/HeroAnimation.tsx` | Preact island — the only hydrated hero component. Cycles through dashboard state screenshots every 6s with motion-one crossfade; pauses on hover/touch; respects `prefers-reduced-motion`. |
372
- | `site/src/content/features.ts` | Data-driven feature list rendered by `BentoGrid.astro` into the 12-card bento layout. |
373
- | `site/scripts/screenshots/capture.ts` | Playwright screenshot pipeline. Two modes: `SCREENSHOT_TARGET_URL=<url> npm run screenshots` captures against an existing dashboard (recommended); plain `npm run screenshots` spawns a temp `pi-dashboard` with fixture sessions, captures desktop (1440) + mobile (390), and cleans up. Outputs to `site/public/screenshots/{desktop,mobile}/`. |
374
- | `site/scripts/check-js-size.mjs` | Enforces the 50 KB gzipped JavaScript budget on `site/dist/**/*.js` (wired to `npm run size` and to the deploy workflow). |
375
326
 
376
327
  ## Build & Restart Workflow
377
328
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # PI Dashboard
2
2
 
3
3
  [![CI](https://github.com/BlackBeltTechnology/pi-agent-dashboard/actions/workflows/ci.yml/badge.svg)](https://github.com/BlackBeltTechnology/pi-agent-dashboard/actions/workflows/ci.yml)
4
- [![npm](https://img.shields.io/npm/v/@blackbelt-technology/pi-dashboard)](https://www.npmjs.com/package/@blackbelt-technology/pi-dashboard)
4
+ [![npm](https://img.shields.io/npm/v/@blackbelt-technology/pi-agent-dashboard)](https://www.npmjs.com/package/@blackbelt-technology/pi-agent-dashboard)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
7
  A web-based dashboard for monitoring and interacting with [pi](https://github.com/badlogic/pi-mono) agent sessions from any browser, including mobile.
@@ -29,6 +29,7 @@ A web-based dashboard for monitoring and interacting with [pi](https://github.co
29
29
  - **Force kill escalation** — Two-click Stop button (in command bar and on running tool cards): first click sends soft abort, second click force-kills the process (SIGTERM → SIGKILL). Session preserved as "ended" for resume/fork. Repeated tool calls (e.g. health check loops) are auto-collapsed with a count badge.
30
30
  - **Searchable select dialogs** — Keyboard-navigable picker with real-time filtering for OpenSpec changes and flow commands
31
31
  - **Browser-based provider auth** — Sign in to Anthropic, OpenAI Codex, GitHub Copilot, Gemini CLI, and Antigravity directly from Settings. Enter API keys for other providers. Credentials saved to `~/.pi/agent/auth.json` and live-synced to running sessions.
32
+ - **Custom LLM providers** — Add OpenAI-compatible, Anthropic-compatible, or Google Generative AI endpoints (Settings → Providers → LLM Providers). Each card has a **Test** button that pings the provider's `/models` endpoint to verify your base URL + API key combination before saving. Adding, editing, or removing a provider takes effect live in every running pi session — no restart required.
32
33
  - **Package management** — Browse, install, update, and remove pi packages from the dashboard. Search the npm registry for pi-package extensions/skills/themes, install from npm or git URL, manage global packages in Settings and local packages per workspace. All active sessions auto-reload after changes.
33
34
  - **OpenSpec integration** — Browse specs, view archive history, manage changes, and create new changes from the session sidebar
34
35
  - **Diff viewer** — Side-by-side and unified diff views with file tree navigation for reviewing agent changes
@@ -105,10 +106,10 @@ On first launch, a setup wizard guides you through:
105
106
 
106
107
  ### Option B: pi Package (recommended for CLI users)
107
108
 
108
- Requires [pi](https://github.com/badlogic/pi-mono) (or [Oh My Pi](https://www.npmjs.com/package/@oh-my-pi/pi-coding-agent)) and Node.js ≥ 20.
109
+ Requires [pi](https://github.com/badlogic/pi-mono) (or [Oh My Pi](https://www.npmjs.com/package/@oh-my-pi/pi-coding-agent)) and Node.js **≥ 22.18.0** (or 24.3.0). Older Node 22.x / 24.x builds are affected by [nodejs/node#58515](https://github.com/nodejs/node/issues/58515) which crashes Fastify at startup.
109
110
 
110
111
  ```bash
111
- pi install npm:@blackbelt-technology/pi-dashboard
112
+ pi install npm:@blackbelt-technology/pi-agent-dashboard
112
113
  pi
113
114
  ```
114
115
 
@@ -175,7 +176,7 @@ Only needed for Option B/C (the Electron app handles everything automatically).
175
176
  | Requirement | Why | Install |
176
177
  |-------------|-----|---------|
177
178
  | **[pi](https://github.com/badlogic/pi-mono)** or **[Oh My Pi](https://www.npmjs.com/package/@oh-my-pi/pi-coding-agent)** | The AI coding agent that the dashboard monitors | `npm i -g @mariozechner/pi-coding-agent` |
178
- | **Node.js ≥ 20** | Runtime for the dashboard server | [nodejs.org](https://nodejs.org/) |
179
+ | **Node.js ≥ 22.18.0** | Runtime for the dashboard server (older 22.x / 24.x affected by [nodejs/node#58515](https://github.com/nodejs/node/issues/58515)) | [nodejs.org](https://nodejs.org/) |
179
180
  | **C++ build tools** | Required by `node-pty` native addon for terminal emulation | Xcode CLI Tools (macOS) / `build-essential` (Linux) |
180
181
 
181
182
  ### Optional tools
@@ -189,6 +190,8 @@ Only needed for Option B/C (the Electron app handles everything automatically).
189
190
 
190
191
  Config file: **`~/.pi/dashboard/config.json`** (auto-created with defaults on first run)
191
192
 
193
+ Tool path overrides (optional, machine-local): **`~/.pi/dashboard/tool-overrides.json`** — see [Tool resolution & overrides](#tool-resolution--overrides) below.
194
+
192
195
  ```json
193
196
  {
194
197
  "port": 8000,
@@ -198,10 +201,61 @@ Config file: **`~/.pi/dashboard/config.json`** (auto-created with defaults on fi
198
201
  "shutdownIdleSeconds": 300,
199
202
  "spawnStrategy": "headless",
200
203
  "tunnel": { "enabled": true, "reservedToken": "auto-created-on-first-run" },
201
- "devBuildOnReload": false
204
+ "devBuildOnReload": false,
205
+ "openspec": {
206
+ "pollIntervalSeconds": 30,
207
+ "maxConcurrentSpawns": 3,
208
+ "changeDetection": "mtime",
209
+ "jitterSeconds": 5
210
+ }
211
+ }
212
+ ```
213
+
214
+ **OpenSpec background polling** (`openspec` block):
215
+
216
+ | Key | Default | Range | Description |
217
+ |-----|---------|-------|-------------|
218
+ | `pollIntervalSeconds` | `30` | `5–3600` | How often each known directory is polled for OpenSpec updates |
219
+ | `maxConcurrentSpawns` | `3` | `1–16` | Cap on concurrent `openspec` CLI invocations across all directories |
220
+ | `changeDetection` | `"mtime"` | `"mtime" \| "always"` | `mtime` skips re-polling unchanged proposals (near-zero steady-state cost); `always` polls unconditionally |
221
+ | `jitterSeconds` | `5` | `0–60` | Per-directory phase offset so polls don't all align on the same tick |
222
+
223
+ Live-reconfigurable via Settings → Advanced → "Background polling (OpenSpec)" or `PUT /api/config` — no server restart needed. See [docs/architecture.md](docs/architecture.md) for the cost model.
224
+
225
+ ### Tool resolution & overrides
226
+
227
+ The dashboard resolves every external tool it calls (`pi`, `pi-coding-agent`, `openspec`, `npm`, `node`, `tsx`, `git`, `zrok`, `pi-dashboard`) through a single `ToolRegistry`. Each tool has an ordered strategy chain (override → managed install → bare-import / npm-global → PATH search), and every resolution records a diagnostic trail of which strategies were tried and why each succeeded or failed.
228
+
229
+ **Inspecting and overriding** — Settings → General → **Tools** shows every resolved tool, its source, and the trail. You can set a per-tool override path, rescan a single tool or all of them, and export the full diagnostic report for bug reports.
230
+
231
+ **Overrides file** — `~/.pi/dashboard/tool-overrides.json`:
232
+
233
+ ```json
234
+ {
235
+ "version": 1,
236
+ "overrides": {
237
+ "pi": { "path": "C:\\custom\\pi.cmd" },
238
+ "pi-coding-agent": { "path": "D:\\dev\\pi-coding-agent\\dist\\index.js" }
239
+ }
202
240
  }
203
241
  ```
204
242
 
243
+ The file is deliberately separate from `config.json` so machine-specific paths don't follow a dotfiles sync to another host. Invalid overrides (path doesn't exist) are recorded in the diagnostic trail and the registry falls through to the next strategy automatically.
244
+
245
+ **Troubleshooting** — if the dashboard says it can't find `pi`, `openspec`, `npm`, or any other tool, open Settings → General → Tools, click the chevron next to the failing tool to see the full `tried[]` trail, then either (a) install the missing tool on PATH / in the managed location shown in the trail, or (b) set an explicit override via the row's path input. Hit **Rescan** to pick up the change without a server restart.
246
+
247
+ **Troubleshooting: sessions don't group under my pinned folder** — since v0.3+, session grouping uses OS-aware path equality (`platform/paths.ts`). Sessions group correctly under a pinned folder even when the path stored on pin and the path reported by the session differ in trailing separator, separator style, or case (on Windows and macOS). If you still see two entries for what should be one folder, the paths are likely on different Windows drives (`A:\Foo` and `B:\Foo` are different filesystems and never merge) — that's correct behavior, not a bug. If the paths really are the same filesystem, file an issue with both the pinned path (visible in Settings → Tools → Export diagnostics) and the session `cwd` as reported in `/api/sessions`.
248
+
249
+ ### Windows session durability
250
+
251
+ **Behavior change**: Since the `consolidate-windows-spawn-and-platform-handlers` release, pi sessions on Windows now **survive dashboard server restart**, matching macOS/Linux behavior. Previously, killing or restarting the dashboard process (Task Manager, Ctrl+C, `/api/restart`, crash) would terminate every running pi session because the children were in the server's libuv kill-on-close Job Object. The fix uses `detached: true` so children are excluded from the parent's job — the Windows equivalent of Unix PGID detachment.
252
+
253
+ If you previously relied on "closing the dashboard cleans everything up," use the per-session **Force Kill** action instead (or `POST /api/session/:id/force-kill` via the REST API).
254
+
255
+ **Recommended: install Windows Terminal** (`wt.exe`) for tabbed interactive sessions on Windows 10/11. The dashboard prefers `wt` when available (new tab in an existing WT window, respects your default profile — cmd / PowerShell / WSL / whatever), and falls back to WSL tmux and then headless mode when absent. Windows Terminal is bundled with Windows 11; on Windows 10 install it from the Microsoft Store (search for "Windows Terminal").
256
+
257
+ **Troubleshooting: Windows Terminal tab doesn't appear** — if `wt.exe` is on PATH but launching does nothing, check Settings → Apps → Advanced app settings → App execution aliases. If the "wt" alias is disabled, `wt.exe` is found but can't be executed. Enable the alias or uninstall/reinstall Windows Terminal.
258
+
205
259
  ### Authentication (Optional)
206
260
 
207
261
  Add an `auth` section to enable OAuth2 authentication for external (tunnel) access. Localhost is always unguarded.
@@ -274,10 +328,10 @@ See [Getting Started — Option A](#option-a-electron-desktop-app-standalone--no
274
328
 
275
329
  ```bash
276
330
  # pi
277
- pi install npm:@blackbelt-technology/pi-dashboard
331
+ pi install npm:@blackbelt-technology/pi-agent-dashboard
278
332
 
279
333
  # Oh My Pi
280
- omp install npm:@blackbelt-technology/pi-dashboard
334
+ omp install npm:@blackbelt-technology/pi-agent-dashboard
281
335
  ```
282
336
 
283
337
  > The package is compatible with both [pi](https://github.com/badlogic/pi-mono) and [Oh My Pi](https://www.npmjs.com/package/@oh-my-pi/pi-coding-agent) — no configuration needed.
@@ -350,6 +404,21 @@ pi-dashboard status # Show daemon status
350
404
 
351
405
  Daemon stdout/stderr is logged to `~/.pi/dashboard/server.log` for crash diagnosis.
352
406
 
407
+ ### Keyboard shortcuts in chat input
408
+
409
+ The chat input supports bash-style history recall and per-session draft persistence:
410
+
411
+ | Key | Action |
412
+ |-----|--------|
413
+ | `Enter` | Send the prompt. |
414
+ | `Shift+Enter` | Insert a newline. |
415
+ | `ArrowUp` | Recall the previous user prompt (only when the caret is on the first line and no autocomplete dropdown is open). Repeat to walk further back. |
416
+ | `ArrowDown` | Walk forward through history (only when the caret is on the last line). Past the newest entry, restores the in-progress draft. |
417
+ | `Escape` | While navigating history, restore the in-progress draft and exit history mode. Also cancels a pending prompt or dismisses the autocomplete dropdown. |
418
+ | `Tab` / `Enter` in dropdown | Accept the highlighted `/command` or `@file` suggestion. |
419
+
420
+ Drafts (typed-but-unsent text) are persisted per session in `localStorage` under `chat-draft:<sessionId>` and survive navigation (Settings, OpenSpec preview, file diffs, ...) as well as full page reloads. Drafts never leak between sessions.
421
+
353
422
  ### Graceful restart via API
354
423
 
355
424
  Restart without CLI — useful from scripts, other sessions, or the dashboard skill:
@@ -596,6 +665,23 @@ Returns:
596
665
 
597
666
  Agent metrics are collected every 15s via heartbeats and include `eventLoopMaxMs` — useful for diagnosing connection drops during long-running operations.
598
667
 
668
+ ### Troubleshooting: dashboard server doesn't start
669
+
670
+ If `pi` launches but the dashboard never becomes reachable (no `http://localhost:8000`, no `🌐 Dashboard started` notification), inspect the launch log:
671
+
672
+ ```bash
673
+ cat ~/.pi/dashboard/server.log # Linux / macOS
674
+ type %USERPROFILE%\.pi\dashboard\server.log # Windows
675
+ ```
676
+
677
+ The log is opened in append mode and prefixed with a timestamped header on every start attempt, so previous crashes are preserved. Common issues:
678
+
679
+ - **`ERR_UNSUPPORTED_ESM_URL_SCHEME` on Windows** — fixed in pi-agent-dashboard 0.2.10+; upgrade the package.
680
+ - **`Port 8000 is occupied by another service`** — another process is bound to the port. On Windows: `netstat -ano | findstr :8000` then `taskkill /F /PID <pid>`. On Unix: `lsof -t -i :8000 | xargs kill`. Or change `port` in `~/.pi/dashboard/config.json`.
681
+ - **`Cannot find pi's TypeScript loader`** — pi is not installed globally. Run `npm install -g @mariozechner/pi-coding-agent`.
682
+
683
+ The `POST /api/restart` endpoint and `pi-dashboard restart` command work identically on Windows, macOS, and Linux (no `sh`/`lsof`/`curl` dependency).
684
+
599
685
  ## Extension UI Events
600
686
 
601
687
  Your own extensions can broadcast UI events to the dashboard: