@blackbelt-technology/pi-agent-dashboard 0.0.0-test-darwin-x64.1

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 (461) hide show
  1. package/AGENTS.md +405 -0
  2. package/LICENSE +21 -0
  3. package/README.md +741 -0
  4. package/docs/architecture.md +1785 -0
  5. package/package.json +111 -0
  6. package/packages/extension/package.json +48 -0
  7. package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
  8. package/packages/extension/src/__tests__/ask-user-tool.test.ts +508 -0
  9. package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
  10. package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
  11. package/packages/extension/src/__tests__/connection-suppress-auto-start.test.ts +97 -0
  12. package/packages/extension/src/__tests__/connection.test.ts +344 -0
  13. package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
  14. package/packages/extension/src/__tests__/dashboard-default-adapter.test.ts +77 -0
  15. package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
  16. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
  17. package/packages/extension/src/__tests__/event-forwarder.test.ts +119 -0
  18. package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +88 -0
  19. package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
  20. package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
  21. package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
  22. package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
  23. package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
  24. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
  25. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +269 -0
  26. package/packages/extension/src/__tests__/openspec-poller.test.ts +124 -0
  27. package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
  28. package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
  29. package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
  30. package/packages/extension/src/__tests__/prompt-bus-wiring.test.ts +791 -0
  31. package/packages/extension/src/__tests__/prompt-bus.test.ts +513 -0
  32. package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
  33. package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
  34. package/packages/extension/src/__tests__/server-auto-start.test.ts +258 -0
  35. package/packages/extension/src/__tests__/server-launcher.test.ts +61 -0
  36. package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
  37. package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
  38. package/packages/extension/src/__tests__/session-sync.test.ts +135 -0
  39. package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
  40. package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
  41. package/packages/extension/src/__tests__/tui-prompt-adapter.test.ts +207 -0
  42. package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
  43. package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
  44. package/packages/extension/src/__tests__/vcs-info-jj.test.ts +145 -0
  45. package/packages/extension/src/__tests__/vcs-info.test.ts +124 -0
  46. package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
  47. package/packages/extension/src/ask-user-tool.ts +461 -0
  48. package/packages/extension/src/bridge-context.ts +81 -0
  49. package/packages/extension/src/bridge.ts +1413 -0
  50. package/packages/extension/src/command-handler.ts +533 -0
  51. package/packages/extension/src/connection.ts +233 -0
  52. package/packages/extension/src/dashboard-default-adapter.ts +37 -0
  53. package/packages/extension/src/dev-build.ts +39 -0
  54. package/packages/extension/src/event-forwarder.ts +40 -0
  55. package/packages/extension/src/flow-event-wiring.ts +85 -0
  56. package/packages/extension/src/git-link-builder.ts +112 -0
  57. package/packages/extension/src/model-tracker.ts +90 -0
  58. package/packages/extension/src/multiselect-decode.ts +40 -0
  59. package/packages/extension/src/multiselect-list.ts +146 -0
  60. package/packages/extension/src/multiselect-polyfill.ts +73 -0
  61. package/packages/extension/src/pi-env.d.ts +37 -0
  62. package/packages/extension/src/process-metrics.ts +70 -0
  63. package/packages/extension/src/process-scanner.ts +430 -0
  64. package/packages/extension/src/prompt-bus.ts +241 -0
  65. package/packages/extension/src/prompt-expander.ts +108 -0
  66. package/packages/extension/src/provider-register.ts +564 -0
  67. package/packages/extension/src/server-auto-start.ts +129 -0
  68. package/packages/extension/src/server-launcher.ts +138 -0
  69. package/packages/extension/src/server-probe.ts +33 -0
  70. package/packages/extension/src/session-sync.ts +169 -0
  71. package/packages/extension/src/source-detector.ts +26 -0
  72. package/packages/extension/src/ui-modules.ts +272 -0
  73. package/packages/extension/src/vcs-info.ts +184 -0
  74. package/packages/extension/tsconfig.json +11 -0
  75. package/packages/server/package.json +58 -0
  76. package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
  77. package/packages/server/src/__tests__/auth.test.ts +224 -0
  78. package/packages/server/src/__tests__/auto-attach.test.ts +308 -0
  79. package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
  80. package/packages/server/src/__tests__/auto-shutdown.test.ts +142 -0
  81. package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
  82. package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
  83. package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
  84. package/packages/server/src/__tests__/bridge-register-nondestructive.test.ts +108 -0
  85. package/packages/server/src/__tests__/browse-endpoint.test.ts +616 -0
  86. package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -0
  87. package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
  88. package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
  89. package/packages/server/src/__tests__/cli-parse.test.ts +84 -0
  90. package/packages/server/src/__tests__/cli-restart.test.ts +78 -0
  91. package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
  92. package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
  93. package/packages/server/src/__tests__/config-api.test.ts +181 -0
  94. package/packages/server/src/__tests__/cors.test.ts +80 -0
  95. package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
  96. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
  97. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
  98. package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
  99. package/packages/server/src/__tests__/directory-service.test.ts +640 -0
  100. package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
  101. package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
  102. package/packages/server/src/__tests__/editor-manager-pid-registry.test.ts +168 -0
  103. package/packages/server/src/__tests__/editor-manager.test.ts +106 -0
  104. package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
  105. package/packages/server/src/__tests__/editor-registry.test.ts +165 -0
  106. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
  107. package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
  108. package/packages/server/src/__tests__/extension-register-appimage.test.ts +43 -0
  109. package/packages/server/src/__tests__/extension-register.test.ts +67 -0
  110. package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
  111. package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
  112. package/packages/server/src/__tests__/fix-pty-permissions.test.ts +59 -0
  113. package/packages/server/src/__tests__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
  114. package/packages/server/src/__tests__/force-kill-handler.test.ts +158 -0
  115. package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
  116. package/packages/server/src/__tests__/git-operations.test.ts +253 -0
  117. package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
  118. package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
  119. package/packages/server/src/__tests__/health-endpoint.test.ts +33 -0
  120. package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
  121. package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
  122. package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
  123. package/packages/server/src/__tests__/home-lock.test.ts +308 -0
  124. package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
  125. package/packages/server/src/__tests__/is-activity-event.test.ts +78 -0
  126. package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
  127. package/packages/server/src/__tests__/is-unread-trigger.test.ts +166 -0
  128. package/packages/server/src/__tests__/jj-routes.test.ts +93 -0
  129. package/packages/server/src/__tests__/json-store.test.ts +70 -0
  130. package/packages/server/src/__tests__/known-servers-routes.test.ts +129 -0
  131. package/packages/server/src/__tests__/last-activity-broadcast.test.ts +190 -0
  132. package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
  133. package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
  134. package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
  135. package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
  136. package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
  137. package/packages/server/src/__tests__/node-guard.test.ts +85 -0
  138. package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
  139. package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
  140. package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
  141. package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +292 -0
  142. package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
  143. package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
  144. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
  145. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +198 -0
  146. package/packages/server/src/__tests__/package-routes.test.ts +305 -0
  147. package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
  148. package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
  149. package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
  150. package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
  151. package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
  152. package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
  153. package/packages/server/src/__tests__/pi-core-checker.test.ts +238 -0
  154. package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
  155. package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
  156. package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
  157. package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
  158. package/packages/server/src/__tests__/pi-version-skew.test.ts +237 -0
  159. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
  160. package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
  161. package/packages/server/src/__tests__/preferences-store.test.ts +177 -0
  162. package/packages/server/src/__tests__/process-manager.test.ts +214 -0
  163. package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
  164. package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
  165. package/packages/server/src/__tests__/provider-auth-routes.test.ts +153 -0
  166. package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
  167. package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
  168. package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
  169. package/packages/server/src/__tests__/reattach-placement.test.ts +231 -0
  170. package/packages/server/src/__tests__/recommended-routes.test.ts +389 -0
  171. package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
  172. package/packages/server/src/__tests__/restart-helper.test.ts +136 -0
  173. package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
  174. package/packages/server/src/__tests__/server-pid.test.ts +89 -0
  175. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
  176. package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
  177. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
  178. package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
  179. package/packages/server/src/__tests__/session-api.test.ts +244 -0
  180. package/packages/server/src/__tests__/session-diff-vcs.test.ts +61 -0
  181. package/packages/server/src/__tests__/session-diff.test.ts +138 -0
  182. package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
  183. package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
  184. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +144 -0
  185. package/packages/server/src/__tests__/session-order-manager.test.ts +190 -0
  186. package/packages/server/src/__tests__/session-order-reboot.test.ts +359 -0
  187. package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
  188. package/packages/server/src/__tests__/session-scanner.test.ts +274 -0
  189. package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
  190. package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
  191. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +128 -0
  192. package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
  193. package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
  194. package/packages/server/src/__tests__/subscription-handler.test.ts +195 -0
  195. package/packages/server/src/__tests__/system-routes-restart.test.ts +128 -0
  196. package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
  197. package/packages/server/src/__tests__/terminal-manager.test.ts +297 -0
  198. package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
  199. package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
  200. package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
  201. package/packages/server/src/__tests__/trusted-networks-config.test.ts +103 -0
  202. package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
  203. package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
  204. package/packages/server/src/__tests__/tunnel.test.ts +303 -0
  205. package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
  206. package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
  207. package/packages/server/src/__tests__/unread-persistence.test.ts +55 -0
  208. package/packages/server/src/__tests__/unread-trigger-wiring.test.ts +210 -0
  209. package/packages/server/src/__tests__/viewed-session-tracker.test.ts +93 -0
  210. package/packages/server/src/__tests__/ws-ping-pong.test.ts +120 -0
  211. package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
  212. package/packages/server/src/auth-plugin.ts +302 -0
  213. package/packages/server/src/auth.ts +323 -0
  214. package/packages/server/src/bootstrap-queue.ts +130 -0
  215. package/packages/server/src/bootstrap-state.ts +131 -0
  216. package/packages/server/src/browse.ts +259 -0
  217. package/packages/server/src/browser-gateway.ts +602 -0
  218. package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
  219. package/packages/server/src/browser-handlers/directory-handler.ts +158 -0
  220. package/packages/server/src/browser-handlers/handler-context.ts +60 -0
  221. package/packages/server/src/browser-handlers/session-action-handler.ts +431 -0
  222. package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
  223. package/packages/server/src/browser-handlers/session-meta-handler.ts +129 -0
  224. package/packages/server/src/browser-handlers/subscription-handler.ts +199 -0
  225. package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
  226. package/packages/server/src/cli.ts +686 -0
  227. package/packages/server/src/config-api.ts +146 -0
  228. package/packages/server/src/directory-service.ts +534 -0
  229. package/packages/server/src/editor-detection.ts +63 -0
  230. package/packages/server/src/editor-manager.ts +386 -0
  231. package/packages/server/src/editor-pid-registry.ts +199 -0
  232. package/packages/server/src/editor-proxy.ts +134 -0
  233. package/packages/server/src/editor-registry.ts +105 -0
  234. package/packages/server/src/event-status-extraction.ts +235 -0
  235. package/packages/server/src/event-wiring.ts +766 -0
  236. package/packages/server/src/fix-pty-permissions.ts +44 -0
  237. package/packages/server/src/git-operations.ts +200 -0
  238. package/packages/server/src/headless-pid-registry.ts +203 -0
  239. package/packages/server/src/home-lock-release.ts +72 -0
  240. package/packages/server/src/home-lock.ts +389 -0
  241. package/packages/server/src/idle-timer.ts +61 -0
  242. package/packages/server/src/installed-package-enricher.ts +143 -0
  243. package/packages/server/src/json-store.ts +32 -0
  244. package/packages/server/src/localhost-guard.ts +117 -0
  245. package/packages/server/src/memory-event-store.ts +193 -0
  246. package/packages/server/src/memory-session-manager.ts +154 -0
  247. package/packages/server/src/meta-persistence.ts +64 -0
  248. package/packages/server/src/migrate-persistence.ts +195 -0
  249. package/packages/server/src/node-guard.ts +52 -0
  250. package/packages/server/src/npm-search-proxy.ts +214 -0
  251. package/packages/server/src/oauth-callback-server.ts +177 -0
  252. package/packages/server/src/openspec-archive.ts +60 -0
  253. package/packages/server/src/openspec-tasks.ts +189 -0
  254. package/packages/server/src/package-manager-wrapper.ts +688 -0
  255. package/packages/server/src/package-source-helpers.ts +104 -0
  256. package/packages/server/src/pending-attach-registry.ts +112 -0
  257. package/packages/server/src/pending-fork-registry.ts +53 -0
  258. package/packages/server/src/pending-load-manager.ts +110 -0
  259. package/packages/server/src/pending-resume-intent-registry.ts +107 -0
  260. package/packages/server/src/pending-resume-registry.ts +69 -0
  261. package/packages/server/src/pi-core-checker.ts +285 -0
  262. package/packages/server/src/pi-core-updater.ts +172 -0
  263. package/packages/server/src/pi-gateway.ts +444 -0
  264. package/packages/server/src/pi-resource-scanner.ts +366 -0
  265. package/packages/server/src/pi-version-skew.ts +207 -0
  266. package/packages/server/src/preferences-store.ts +130 -0
  267. package/packages/server/src/process-manager.ts +491 -0
  268. package/packages/server/src/proposal-attach-naming.ts +47 -0
  269. package/packages/server/src/provider-auth-handlers.ts +438 -0
  270. package/packages/server/src/provider-auth-storage.ts +200 -0
  271. package/packages/server/src/provider-probe.ts +234 -0
  272. package/packages/server/src/reattach-placement.ts +98 -0
  273. package/packages/server/src/resolve-path.ts +12 -0
  274. package/packages/server/src/restart-helper.ts +180 -0
  275. package/packages/server/src/routes/bootstrap-routes.ts +88 -0
  276. package/packages/server/src/routes/editor-routes.ts +86 -0
  277. package/packages/server/src/routes/file-routes.ts +169 -0
  278. package/packages/server/src/routes/git-routes.ts +89 -0
  279. package/packages/server/src/routes/jj-routes.ts +386 -0
  280. package/packages/server/src/routes/known-servers-routes.ts +110 -0
  281. package/packages/server/src/routes/openspec-routes.ts +205 -0
  282. package/packages/server/src/routes/package-routes.ts +241 -0
  283. package/packages/server/src/routes/pi-core-routes.ts +140 -0
  284. package/packages/server/src/routes/plugin-config-routes.ts +129 -0
  285. package/packages/server/src/routes/provider-auth-routes.ts +246 -0
  286. package/packages/server/src/routes/provider-routes.ts +154 -0
  287. package/packages/server/src/routes/recommended-routes.ts +225 -0
  288. package/packages/server/src/routes/route-deps.ts +23 -0
  289. package/packages/server/src/routes/session-routes.ts +100 -0
  290. package/packages/server/src/routes/system-routes.ts +295 -0
  291. package/packages/server/src/routes/tool-routes.ts +153 -0
  292. package/packages/server/src/server-pid.ts +80 -0
  293. package/packages/server/src/server.ts +1282 -0
  294. package/packages/server/src/session-api.ts +424 -0
  295. package/packages/server/src/session-bootstrap.ts +94 -0
  296. package/packages/server/src/session-diff.ts +295 -0
  297. package/packages/server/src/session-discovery.ts +134 -0
  298. package/packages/server/src/session-file-reader.ts +135 -0
  299. package/packages/server/src/session-order-manager.ts +95 -0
  300. package/packages/server/src/session-scanner.ts +261 -0
  301. package/packages/server/src/session-stats-reader.ts +99 -0
  302. package/packages/server/src/terminal-gateway.ts +51 -0
  303. package/packages/server/src/terminal-manager.ts +286 -0
  304. package/packages/server/src/test-env-guard.ts +26 -0
  305. package/packages/server/src/test-support/test-server.ts +63 -0
  306. package/packages/server/src/tunnel.ts +467 -0
  307. package/packages/server/src/viewed-session-tracker.ts +78 -0
  308. package/packages/server/tsconfig.json +11 -0
  309. package/packages/shared/package.json +29 -0
  310. package/packages/shared/src/__tests__/binary-lookup.test.ts +184 -0
  311. package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
  312. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
  313. package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
  314. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
  315. package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
  316. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
  317. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
  318. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
  319. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
  320. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
  321. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
  322. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
  323. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
  324. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
  325. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
  326. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
  327. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
  328. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
  329. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
  330. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
  331. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
  332. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
  333. package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
  334. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
  335. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
  336. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
  337. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
  338. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
  339. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
  340. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
  341. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
  342. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
  343. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
  344. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
  345. package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
  346. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
  347. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
  348. package/packages/shared/src/__tests__/bridge-register.test.ts +185 -0
  349. package/packages/shared/src/__tests__/browser-protocol-types.test.ts +121 -0
  350. package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
  351. package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
  352. package/packages/shared/src/__tests__/config.test.ts +473 -0
  353. package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
  354. package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
  355. package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
  356. package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
  357. package/packages/shared/src/__tests__/mdns-discovery.test.ts +127 -0
  358. package/packages/shared/src/__tests__/no-bash-on-windows.test.ts +304 -0
  359. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
  360. package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
  361. package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
  362. package/packages/shared/src/__tests__/no-hardcoded-node-modules-paths.test.ts +176 -0
  363. package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
  364. package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
  365. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +107 -0
  366. package/packages/shared/src/__tests__/node-spawn.test.ts +210 -0
  367. package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
  368. package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
  369. package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
  370. package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
  371. package/packages/shared/src/__tests__/openspec-poller.test.ts +44 -0
  372. package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
  373. package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
  374. package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
  375. package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
  376. package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
  377. package/packages/shared/src/__tests__/platform-jj.test.ts +339 -0
  378. package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
  379. package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
  380. package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
  381. package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
  382. package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
  383. package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
  384. package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
  385. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
  386. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
  387. package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
  388. package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
  389. package/packages/shared/src/__tests__/protocol.test.ts +254 -0
  390. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +215 -0
  391. package/packages/shared/src/__tests__/recommended-extensions.test.ts +160 -0
  392. package/packages/shared/src/__tests__/resolve-jiti.test.ts +53 -0
  393. package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
  394. package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
  395. package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
  396. package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
  397. package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
  398. package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
  399. package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
  400. package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
  401. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +255 -0
  402. package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
  403. package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
  404. package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
  405. package/packages/shared/src/archive-types.ts +11 -0
  406. package/packages/shared/src/bootstrap-install.ts +212 -0
  407. package/packages/shared/src/bridge-register.ts +164 -0
  408. package/packages/shared/src/browser-protocol.ts +803 -0
  409. package/packages/shared/src/config.ts +440 -0
  410. package/packages/shared/src/dashboard-plugin/index.ts +11 -0
  411. package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
  412. package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
  413. package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
  414. package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
  415. package/packages/shared/src/diff-types.ts +58 -0
  416. package/packages/shared/src/editor-types.ts +18 -0
  417. package/packages/shared/src/managed-paths.ts +42 -0
  418. package/packages/shared/src/mdns-discovery.ts +279 -0
  419. package/packages/shared/src/openspec-activity-detector.ts +107 -0
  420. package/packages/shared/src/openspec-design-evidence.ts +109 -0
  421. package/packages/shared/src/openspec-poller.ts +232 -0
  422. package/packages/shared/src/openspec-specs-evidence.ts +79 -0
  423. package/packages/shared/src/platform/binary-lookup.ts +396 -0
  424. package/packages/shared/src/platform/commands.ts +100 -0
  425. package/packages/shared/src/platform/detached-spawn.ts +305 -0
  426. package/packages/shared/src/platform/exec.ts +220 -0
  427. package/packages/shared/src/platform/git.ts +155 -0
  428. package/packages/shared/src/platform/index.ts +16 -0
  429. package/packages/shared/src/platform/jj.ts +405 -0
  430. package/packages/shared/src/platform/node-spawn.ts +184 -0
  431. package/packages/shared/src/platform/npm.ts +162 -0
  432. package/packages/shared/src/platform/openspec.ts +91 -0
  433. package/packages/shared/src/platform/paths.ts +276 -0
  434. package/packages/shared/src/platform/process-identify.ts +126 -0
  435. package/packages/shared/src/platform/process-scan.ts +94 -0
  436. package/packages/shared/src/platform/process.ts +168 -0
  437. package/packages/shared/src/platform/runner.ts +369 -0
  438. package/packages/shared/src/platform/shell.ts +44 -0
  439. package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
  440. package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
  441. package/packages/shared/src/plugin-bridge-register.ts +139 -0
  442. package/packages/shared/src/protocol.ts +535 -0
  443. package/packages/shared/src/recommended-extensions.ts +202 -0
  444. package/packages/shared/src/resolve-jiti.ts +102 -0
  445. package/packages/shared/src/rest-api.ts +445 -0
  446. package/packages/shared/src/semaphore.ts +83 -0
  447. package/packages/shared/src/server-identity.ts +51 -0
  448. package/packages/shared/src/session-meta.ts +92 -0
  449. package/packages/shared/src/source-matching.ts +126 -0
  450. package/packages/shared/src/state-replay.ts +193 -0
  451. package/packages/shared/src/stats-extractor.ts +54 -0
  452. package/packages/shared/src/terminal-types.ts +18 -0
  453. package/packages/shared/src/test-support/setup-home.ts +74 -0
  454. package/packages/shared/src/tool-registry/definitions.ts +435 -0
  455. package/packages/shared/src/tool-registry/index.ts +56 -0
  456. package/packages/shared/src/tool-registry/overrides.ts +118 -0
  457. package/packages/shared/src/tool-registry/registry.ts +262 -0
  458. package/packages/shared/src/tool-registry/strategies.ts +212 -0
  459. package/packages/shared/src/tool-registry/types.ts +180 -0
  460. package/packages/shared/src/types.ts +595 -0
  461. package/packages/shared/tsconfig.json +8 -0
package/AGENTS.md ADDED
@@ -0,0 +1,405 @@
1
+
2
+ # PI Dashboard
3
+
4
+ ## Project Overview
5
+
6
+ Web-based dashboard for monitoring and interacting with pi agent sessions remotely. Three-component architecture: bridge extension + Node.js server + React web client.
7
+
8
+ ## Architecture
9
+
10
+ See [docs/architecture.md](docs/architecture.md) for full details.
11
+
12
+ - **Bridge Extension** (`src/extension/`) — Runs in every pi session, forwards events via WebSocket
13
+ - **Dashboard Server** (`src/server/`) — Aggregates events, in-memory + JSON persistence, dual WebSocket servers
14
+ - **Web Client** (`src/client/`) — React + Tailwind responsive UI
15
+ - **Shared Types** (`src/shared/`) — Protocol definitions shared across components
16
+
17
+ ## Commands
18
+
19
+ ```bash
20
+ npm install # Install dependencies
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
25
+ npm run build # Build web client (Vite)
26
+ npm run dev # Start Vite dev server
27
+ npm run reload # Reload all connected pi sessions
28
+ npm run reload:check # Type-check + reload all pi sessions
29
+ pi-dashboard # Start dashboard server
30
+ pi-dashboard --dev # Start with Vite proxy
31
+ ```
32
+
33
+ ## Running Tests
34
+
35
+ Pipe test output to a tmp file, then grep — avoids re-running to inspect errors:
36
+
37
+ ```bash
38
+ npm test 2>&1 | tee /tmp/pi-test.log # run once, capture all output
39
+ grep -nE 'FAIL|Error|✗|✘' /tmp/pi-test.log # find failures
40
+ grep -n -A 20 'FAIL ' /tmp/pi-test.log # failure + context
41
+ ```
42
+
43
+ Always grep the file — never rerun `npm test` just to see errors.
44
+
45
+ ## Cross-Platform QA Testing
46
+
47
+ VM-based QA testing for verifying clean-state installation and runtime across platforms.
48
+
49
+ ```bash
50
+ cd qa
51
+ make build-linux-x86 # Build Ubuntu x86 base image (Packer + VMware)
52
+ make test-linux-x86 # Clone → boot → run tests → destroy
53
+ make manual-linux-x86 # Clone with GUI for manual testing
54
+ make clean # Destroy all cloned VMs
55
+ ```
56
+
57
+ | File | Purpose |
58
+ |------|---------|
59
+ | `qa/Makefile` | Build/test/manual/clean targets for all platforms |
60
+ | `qa/packer/*.pkr.hcl` | Packer templates per platform (Ubuntu, Windows, macOS) |
61
+ | `qa/packer/scripts/` | Provisioning scripts (common, linux, macos, windows) |
62
+ | `qa/packer/vars/` | OS-version-specific variables (ISO URL, checksum, VM specs) |
63
+ | `qa/packer/http/` | Auto-install configs (cloud-init, autounattend.xml) |
64
+ | `qa/scripts/` | VM lifecycle (clone, wait-ssh, destroy, run-test) |
65
+ | `qa/tests/` | Test suite (install, server, websocket, terminal, git) |
66
+ | `qa/README.md` | Full setup and usage documentation |
67
+
68
+ ## Key Files
69
+
70
+ > **Full file map**: see [`docs/file-index.md`](docs/file-index.md) — read it on demand when locating a file or understanding its full responsibilities (incl. change-history annotations).
71
+
72
+ This section lists only the **architectural backbone** — the files agents touch most often or need to know about for any non-trivial change. For everything else (renderers, individual tool cards, narrow helpers, build/CI internals) consult `docs/file-index.md`.
73
+
74
+ ### Protocol & types
75
+ | File | Purpose |
76
+ |------|---------|
77
+ | `src/shared/protocol.ts` | Extension↔Server WebSocket messages. Phase-1 Extension UI System adds `ui_modules_list` / `ui_data_list` (extension → server) and `ui_management` (server → extension); these messages MUST stay in `ExtensionToServerMessage` / `ServerToExtensionMessage` unions or esbuild strips the switch cases in production. See change: add-extension-ui-modal. |
78
+ | `src/shared/browser-protocol.ts` | Server↔Browser WebSocket messages (all message types including PromptBus `prompt_request`/`prompt_dismiss`/`prompt_cancel` and Extension UI System Phase-1 `ui_modules_list`/`ui_data_list` (server → browser) + `ui_management` (browser → server) must be in the `ServerToBrowserMessage` / `BrowserToServerMessage` unions — `as any` switch cases are stripped by esbuild in production). **`prompt_request.metadata.toolCallId`** (change: fix-interactive-ui-reorder): optional field set by the bridge's `ctx.ui.{select, input, confirm, editor}` wrappers when the prompt is bound to a tool execution; consumed by the client reducer's `addInteractiveRequest` to pair the resulting `interactiveUi` row with its parent `toolResult`. The `metadata` field is typed `Record<string, unknown>`, so the addition is opt-in and forward/backward-compatible. **`SessionViewBrowserMessage` / `SessionUnviewBrowserMessage`** (change: session-card-unread-stripes): browser → server messages declaring the currently-displayed session id (`/session/:id`). Both must stay in the `BrowserToServerMessage` union. The browser is required to re-send `session_view` for the active session on every WebSocket reconnect (handled by `useViewDispatcher`). |
79
+ | `src/shared/types.ts` | Data models (Session, Workspace, Event). **`DashboardSession.lastActivityAt?: number`** (epoch ms) is server-stamped on activity events via `isActivityEvent` in `event-status-extraction.ts`; cold-start seeded from `events.jsonl` mtime in `session-scanner.ts`; consumed by client `selectBadgeTimestamp` to render the session-card relative-time badge as time-since-last-activity instead of time-since-spawn. NOT persisted to `.meta.json`. See change: session-card-last-activity-badge. **`DashboardSession.unread?: boolean`** (change: session-card-unread-stripes): server-managed per-session unread bit. Set to `true` by `event-wiring.ts` when `isUnreadTrigger(...)` fires AND no browser is currently viewing the session AND the event is not part of a replay. Cleared when any browser sends `session_view`. Persisted to `.meta.json` so unread sessions stay flagged across server restarts. Bridges SHALL NOT send this field. Consumed by `SessionCard.tsx::getCardPulseClass` to render the cyan-stripes (`card-unread-pulse`, Tailwind `cyan-400`) decoration with lower priority than `card-input-pulse` (purple) and `card-working-pulse` (yellow). |
80
+ | `src/shared/config.ts` | Shared config loader (`~/.pi/dashboard/config.json`). Exports `ReattachPlacement = "preserve" | "streaming-only" | "always"` + `parseReattachPlacement(raw)` validator (default `"always"`); the `reattachPlacement` config field controls how the server places re-registering bridges in `sessionOrder` after a dashboard restart. See change: reattach-move-to-front. 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 |
81
+ | `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. |
82
+ | `src/extension/bridge.ts` | **`hasRegisteredOnce: boolean`** local (default `false`, synced through `BridgeContext`) flips to `true` after the first `sendStateSync`; consumed by `session-sync.ts` to tag the `session_register` message with `registerReason: "spawn" | "reattach"` so the server can apply the configured `reattachPlacement` policy on dashboard-restart reattaches. See change: reattach-move-to-front. Main extension entry point (composes sync/tracker/flow modules, tracks `isAgentStreaming` in persistent BridgeState). **Invariant**: bridge code MUST NOT call `pi.newSession(...)` / `ctx.fork(...)` / `ctx.switchSession(...)` — enforced by `packages/extension/src/__tests__/no-session-replacement-calls.test.ts`. pi 0.69+ invalidates captured `pi`/`ctx` after these calls; the bridge re-captures state in `session_start` keyed on `event.reason ∈ {"new","fork","resume"}`. **PromptBus patch site**: patches `ctx.ui.select/input/confirm/editor/multiselect` in `session_start`; the TUI adapter handles `select/input/confirm/editor` only. `multiselect` is bridge-attached (pi has no native API) and routed exclusively through the bus to the `DashboardDefaultAdapter`'s browser dialog — decoded via `decodeMultiselectAnswer`. There is intentionally NO TUI adapter arm for multiselect: pi 0.70 RPC mode's `ctx.ui.custom` is a no-op, so any TUI arm awaiting it would auto-cancel the dashboard render in <1s (changes: fix-multiselect-auto-cancel-on-dashboard, fix-multiselect-tui-arm-self-cancel; regression-pinned by `no-tui-multiselect-arm-regression.test.ts`). **Per-message-fork entryId stamping** (change: fix-per-message-fork): `message_start` events now stamp a `nonce` (no entryId, since the user entry isn't persisted yet on pi 0.69+); `message_end` enrichment is deferred via `setTimeout(0)` (NOT `queueMicrotask`, which resolves inside pi's awaited extension dispatcher and misses `appendMessage`); a wrapped `ctx.sessionManager.appendMessage` emits `entry_persisted { entryId, nonce }` so the client reducer can back-fill the user-message bubble's `entryId`. |
83
+ | `src/extension/bridge-context.ts` | Shared mutable state type + helpers for bridge modules |
84
+ | `src/extension/session-sync.ts` | Session register, replay, and switch/fork handling. `sendStateSync` tags `session_register.registerReason` as `"spawn"` on first invocation per process and `"reattach"` thereafter, flipping `bc.hasRegisteredOnce`. `handleSessionChange` (new/fork/resume — fresh sessionId) ALWAYS tags `"spawn"` regardless of the flag. See change: reattach-move-to-front. |
85
+ | `src/extension/model-tracker.ts` | Model/thinking-level/git/name change detection |
86
+ | `src/extension/flow-event-wiring.ts` | Flow event listener registration (flow:* → event_forward) |
87
+ | `src/extension/connection.ts` | WebSocket with exponential backoff. **Auto-start suppression** (change: fix-restart-bridge-auto-start-race): exposes `pauseAutoStart(ms)` (idempotent extend-only) + `shouldSuppressAutoStart()`. The bridge calls `pauseAutoStart(quiesceMs)` on receipt of `server_restarting`; `server-auto-start.ts` consults `shouldSuppressAutoStart()` and skips the spawn step while the window is active so bridges never race the `restart-helper.ts` orchestrator. Discovery + reconnection are NOT suppressed. |
88
+ | `src/extension/server-probe.ts` | TCP probe to detect running server |
89
+ | `src/shared/server-identity.ts` | Identity-verified health check (`isDashboardRunning`) replacing bare TCP probes |
90
+ | `src/shared/mdns-discovery.ts` | mDNS advertise/discover/browse for `_pi-dashboard._tcp` services. Exports `pickBestHost(service)` which returns `service.host` only when it matches the DNS-safe pattern `/^[A-Za-z0-9.-]+$/` (no leading/trailing hyphen); otherwise falls back to the first IPv4 address in `service.addresses`, then to any address, finally to the original host. Bonjour on macOS advertises the OS computer-name verbatim (e.g. `"MacBook 242"`) which contains spaces — without this fallback, saved known-server entries are unresolvable in the browser. `serviceToServer` calls `pickBestHost` so discovered servers always carry a host the browser can resolve. |
91
+ | `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. |
92
+ | `src/extension/command-handler.ts` | Command routing: `!`/`!!` bash, `/compact`, slash commands |
93
+ | `src/extension/prompt-expander.ts` | Slash command → prompt template expansion (supports colon-to-hyphen aliasing: `/opsx:cmd` → `opsx-cmd.md`) |
94
+ | `src/extension/dev-build.ts` | Dev build-on-reload helper (client build + server shutdown) |
95
+ | `src/extension/server-auto-start.ts` | mDNS-first discovery → health check fallback → auto-start with concurrent launch detection |
96
+ | `src/shared/session-meta.ts` | Session metadata sidecar (.meta.json) read/write helpers. **`SessionMeta.unread?: boolean`** (change: session-card-unread-stripes) mirrors `DashboardSession.unread`; persisted by `metaPersistence.save(...)` in `server.ts onChange` and restored by `session-scanner.ts::sessionFromMeta` so the unread bit survives server reboot. Backwards compatible — absent field reads as `undefined` (treated as `false`). |
97
+ | `src/extension/process-metrics.ts` | Lightweight CPU/memory/event-loop metrics collector for heartbeats |
98
+ | `src/extension/process-scanner.ts` | Child process detection via ps + PGID tracking (leaf-only, grandchild recursion) and PGID-based kill |
99
+ | `src/client/components/ProcessList.tsx` | Session card process list with elapsed time and red ✕ kill button |
100
+ | `src/extension/git-info.ts` | Git branch/remote/PR detection (polled every 30s) |
101
+ | `src/extension/git-link-builder.ts` | Git remote URL parsing and platform-specific links |
102
+ | `src/server/git-operations.ts` | Server-side git commands: branch listing, checkout, init, stash pop |
103
+ | `src/client/components/BranchPicker.tsx` | Typeahead branch picker with keyboard navigation |
104
+ | `src/client/components/BranchSwitchDialog.tsx` | Checkout orchestration: dirty-state stash, pop prompt |
105
+ | `src/client/lib/git-api.ts` | Client-side fetch helpers for git API endpoints |
106
+ | `src/client/hooks/useImagePaste.ts` | Reusable clipboard-image-paste hook with controlled/uncontrolled modes. Uncontrolled (`useImagePaste()` with no args): hook owns `pendingImages` in local `useState` — used by `ExploreDialog` whose lifetime IS the dialog. Controlled (`useImagePaste({ images, onImagesChange })`): caller owns the array — used by `<CommandInput>` so App can key pending images by `sessionId`. `imageError` stays local in BOTH modes (auto-clears after 3 s, no value in lifting). Supports image/png, image/jpeg, image/gif, image/webp; 10 MB base64 cap. See change: lift-pending-images-to-app |
107
+ | `src/extension/prompt-bus.ts` | PromptBus — unified prompt routing to registered adapters (TUI, dashboard, custom). First-response-wins, cross-adapter dismissal. |
108
+ | `src/extension/dashboard-default-adapter.ts` | Built-in PromptBus adapter that renders prompts as generic interactive dialogs in dashboard chat |
109
+ | `src/extension/ui-modules.ts` | Extension UI System Phases 1+2: `refreshUiModules(ctx)` partitions `probe.modules` by `kind` — `management-modal` → `ui_modules_list` (Phase 1, last-write-wins on duplicate `id`); `footer-segment`/`agent-metric`/`breadcrumb`/`gate`/`toast` → one `ext_ui_decorator` per descriptor (Phase 2, last-write-wins on `(kind,namespace,id)`; `namespace` validated against `/^[a-z0-9-]+$/`; `removed: true` forwarded verbatim). `subscribeUiInvalidate(ctx)` throttles `ui:invalidate` re-probes to 20/sec (50 ms leading + trailing) and emits one warning per offending burst (`INVALIDATE_RATE_CAP_PER_SEC = 20`). `handleUiManagement(ctx, msg)` re-emits browser-originated `ui_management` on `pi.events` with `_reply` injected and forwards synchronous `data.items` as `ui_data_list`. Wired into `bridge.ts` at `session_start` and the `onReconnect` callback. See changes: add-extension-ui-modal, add-extension-ui-decorations. |
110
+ | `src/client/components/extension-ui/GenericExtensionDialog.tsx` | Phase-1 modal renderer for `ExtensionUiModule`. Supports `view.kind ∈ {table, grid, form}`, dispatches `ui_management { action: "list", event: dataEvent }` on mount for table/grid, gates `UiAction.confirm` through `ConfirmDialog`. MDI icons resolved via `mdi-icon-lookup.ts` (unknown keys render no icon). |
111
+ | `src/client/components/extension-ui/decorator-utils.ts` | Phase-2 helper: `decoratorsOfKind(decorators, kind)` — typed filter over `Session.uiDecorators` returning only descriptors of the requested `DecoratorKind`. Used by every Phase-2 slot component. See change: add-extension-ui-decorations. |
112
+ | `src/client/components/extension-ui/FooterSegmentSlot.tsx` | Phase-2 slot. Renders all `kind: "footer-segment"` descriptors as inline pills mounted inside `SessionHeader.tsx` to the right of the model/thinking-level info. Supports MDI `payload.icon` (resolved via `mdi-icon-lookup`) and `payload.tooltip` (HTML title attribute). |
113
+ | `src/client/components/extension-ui/AgentMetricSlot.tsx` | Phase-2 slot. Renders `agent-metric` descriptors whose `payload.agentId` matches the `FlowAgentCard.agent.agentName` of the parent. Mounted inside `FlowAgentCard.tsx`. Orphan descriptors (no matching agent) render nothing. |
114
+ | `src/client/components/extension-ui/BreadcrumbSlot.tsx` | Phase-2 slot. Renders the most-recently-cached `breadcrumb` descriptor as a horizontal step indicator at the top of `FlowDashboard.tsx`. Steps with `status: "done"` show a check; `"error"` show a red alert; the active step is matched against `payload.current` (or first `status: "active"`). |
115
+ | `src/client/components/extension-ui/GateSlot.tsx` | Phase-2 slot. Aggregates `gate` descriptors targeting the same `flowId` via the pure helper `aggregateGateState(decorators, flowId)`: most-restrictive-wins (any `available: false` blocks; reasons concatenated). Mounted inline in `FlowLaunchDialog.tsx`; the dialog also reads `aggregateGateState` directly to disable the Run button. |
116
+ | `src/client/components/extension-ui/ToastSlot.tsx` | Phase-2 slot. Top-right fixed tray mounted in `App.tsx`. Reads toast descriptors across every session in the `sessions` Map, stacks without deduplication, auto-dismisses each toast after `payload.durationMs` (default 5000ms; `0` = sticky), caps simultaneous-display at 5 (FIFO eviction — cache is unaffected). Manual dismiss via the close button latches the descriptor key into a local `dismissed` set so cache replay does not resurrect dismissed toasts. |
117
+ | `src/client/lib/mdi-icon-lookup.ts` | `resolveMdiIcon(key) → string \| null` — looks up an MDI key string against `@mdi/js` exports; allowlists by `mdi`-prefix, returns `null` for unknown keys (no error). XSS-safe icon vocabulary for extension descriptors. |
118
+ | `src/client/lib/prompt-component-registry.ts` | Client-side component registry mapping prompt type strings to render metadata (placement, component) |
119
+ | `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). `multiselect` dispatches through `polyfillMultiselect` (since pi-coding-agent's `ExtensionUIContext` has no native `multiselect` method). Tool description instructs agents: *"UI provides a Select all toggle; do not add one."* **Schema shape**: `parameters` is a single flat `Type.Object` (root `type: "object"`) — NOT a root `Type.Union` — because OpenAI's function-calling validator rejects root-level `anyOf` with *"schema must be a JSON Schema of 'type: \"object\"'"*. To restore Anthropic-friendly per-method strictness after commit a53933f, the root object now carries a body-level `oneOf` discriminator over `method` (confirm/select/multiselect/input/batch) with per-arm `required` + `minItems` constraints; sub-questions use the same flat-object + `oneOf` pattern with no `batch` arm (no nesting). `prepareArguments` still provides runtime rescue/normalization (`params` unwrap, `question`→`title`, stringified `options`/`questions`, batch synthesis/title backfill, `{label,value}` → labels), and `execute` retains empty-options guardrails. See changes: ask-user-multiselect-polyfill, refactor(schema)-restructure-ask-user-tool-schemas, fix-multiselect-auto-cancel-on-dashboard. |
120
+ | `src/extension/multiselect-polyfill.ts` | `polyfillMultiselect(ctx, title, options, opts)` — primary path delegates to bridge-patched `ctx.ui.multiselect` (PromptBus → `DashboardDefaultAdapter` → client `MultiselectRenderer`). Legacy fallback uses `ctx.ui.custom<T>()` + `MultiSelectList` for older / non-bridge contexts; this fallback is **a no-op in pi 0.70 RPC mode** (dashboard headless sessions) — pi-coding-agent defines `custom` as `async () => undefined` there — and is only effective in pure-TUI sessions if a future pi version restores `ctx.ui.custom` in RPC mode. Resolves to `string[]` (confirmed, including empty `[]`) or `undefined` (cancelled). Used for both single-question and batch sub-question `multiselect` paths in `ask-user-tool.ts`. See changes: fix-multiselect-auto-cancel-on-dashboard, fix-multiselect-tui-arm-self-cancel. |
121
+ | `src/extension/multiselect-list.ts` | `MultiSelectList` component implementing pi-tui's `Component` interface. Keyboard contract: `↑↓`/`k`/`j` navigate, `Space` toggles current, `Enter` confirms (selected values in original option order), `Escape` cancels. **No "select all" binding in TUI** — the dashboard adapter provides that affordance. |
122
+ | `src/shared/openspec-activity-detector.ts` | Detects OpenSpec activity from tool events; auto-attach requires only changeName (phase optional). Bash arm rejects flag-shaped capture tokens (`name.startsWith("-")` → `null`) so `openspec archive --help` and similar discovery commands no longer rename unnamed sessions to `--help`. See change: fix-openspec-flag-rename-bug. |
123
+ | `src/shared/openspec-poller.ts` | OpenSpec CLI polling (shared, used by server DirectoryService). `buildOpenSpecData(listResult, statusResults, designProbeFactory?, specsProbeFactory?)` accepts two optional probe factories: a `DesignProbeFactory` that promotes the `design` artifact's status from `"ready"` to `"done"` when local file evidence (R1/R2/R3) satisfies design (see change: fix-openspec-design-detection), and a `SpecsProbeFactory` that promotes the `specs` artifact from `"ready"` to `"done"` when at least one `specs/**/*.md` file exists locally (see change: fix-openspec-specs-mtime-gate-blind-spot). Both overrides are promote-only, single-artifact-only, never demote; the post-override loop re-derives change-level `isComplete`. `pollOpenSpec` (sync) and `pollOpenSpecAsync` (async) wire real-fs probes via `createFsProbeFactory(cwd)` and `createFsSpecsProbeFactory(cwd)`. `directory-service.ts` does the same on the mtime-gated path. |
124
+ | `src/shared/openspec-design-evidence.ts` | Pure rule evaluator + real-fs probe factory for the OpenSpec design-artifact override. `evaluateLocalDesignSatisfaction(changeDir, probe)` short-circuits R1 (`^design.*\.md$` file present) → R2 (`design/` folder with `*.md`) → R3 (`tasks.md` contains a `^\s*-\s+\[[ xX]\]\s` checkbox). `createFsDesignEvidenceProbe()` returns a defensive sync probe (try/catch on every fs call). Probe-injection keeps `buildOpenSpecData` pure and unit-testable without filesystem mocks. See change: fix-openspec-design-detection. |
125
+ | `src/shared/openspec-specs-evidence.ts` | Pure rule evaluator + real-fs probe factory for the OpenSpec specs-artifact override. `evaluateLocalSpecsSatisfaction(changeDir, probe)` returns true iff at least one `*.md` file exists under `<changeDir>/specs/` (iterative DFS, short-circuits on first match). `createFsSpecsEvidenceProbe()` returns a defensive sync probe — every `readdirSync` is wrapped in try/catch so missing dirs / permission errors / symlink loops yield `false` rather than throwing. Mirror-shape of `openspec-design-evidence.ts`; consumed by `buildOpenSpecData` as a defense-in-depth layer over the `directory-service.ts` mtime gate (which now also watches `specs/**`). See change: fix-openspec-specs-mtime-gate-blind-spot. |
126
+ | `.pi/skills/openspec-shared/scripts/effective-status.sh` | Bash wrapper around `openspec status --change <name> --json` that applies the same R1/R2/R3 promotion as the dashboard so OpenSpec workflow skills (`openspec-{continue,ff,apply,verify}-change`) and dashboard session-card buttons cannot disagree about a change's next-ready artifact. Inlines the rule logic via `find` + `grep -E` and uses `jq` for JSON mutation; falls back to raw CLI output if `jq` is absent. **Repo-lint** `packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts` blocks raw `openspec status ... --json` calls in any of the four governed skills (opt-out: `ban:openspec-status-ok`). Parity test: `packages/shared/src/__tests__/openspec-effective-status-script.test.ts`. See change: fix-openspec-design-detection. |
127
+ | `src/shared/state-replay.ts` | Synthesizes events from pi entries (shared, used by server + bridge) |
128
+ | `src/shared/dashboard-plugin/slot-types.ts` | Frozen slot taxonomy: `SlotId` union, `Multiplicity`, `PayloadTier` enums, `SLOT_DEFINITIONS` record. Adding a slot = minor; removing = major. |
129
+ | `src/shared/dashboard-plugin/manifest-types.ts` | `PluginManifest` and `PluginClaim` interfaces; includes `tab` field for `settings-section` claims and `fixture` flag for test-only plugins. |
130
+ | `src/shared/dashboard-plugin/slot-props.ts` | `SlotPropsMap` and `SlotProps<SlotId>` — typed prop contracts per slot id. Type-level test asserts all slot ids are covered. |
131
+ | `src/shared/dashboard-plugin/plugin-status.ts` | `PluginStatus` (for `/api/health`) and `PluginConfigUpdate` (WebSocket broadcast payload). |
132
+ | `src/shared/plugin-bridge-register.ts` | Plugin bridge entry management: `registerPluginBridge`, `deregisterPluginBridge`, `registerAllPluginBridges`. Manages `dashboard-<plugin-id>` keys in `settings.json#dashboardPluginBridges`; never touches user-owned `packages[]` entries. Atomic write (tmp+rename). |
133
+ | `packages/dashboard-plugin-runtime/src/slot-registry.ts` | `createSlotRegistry()` — typed `Map<SlotId, ClaimEntry[]>` pre-sorted by `(priority, pluginId)`. Filter helpers: `forSession`, `forFolder`, `forCommand`, `forTab`, `forToolName`. |
134
+ | `packages/dashboard-plugin-runtime/src/manifest-validator.ts` | Hand-rolled manifest validator. Throws `ManifestValidationError` with `pluginId` and `reason`. No Zod dependency. |
135
+ | `packages/dashboard-plugin-runtime/src/plugin-context.tsx` | `PluginContextProvider`, `CurrentPluginLayer`, `usePluginConfig<T>()`, `useAllSessions`, `useSessionState`, `usePluginLogger`, `usePluginSend`, `usePluginRouter`, `useSlotRegistry`, `applyPluginConfigUpdate`. Per-plugin context layer scopes hooks to the contributing plugin's id. |
136
+ | `packages/dashboard-plugin-runtime/src/slot-consumers.tsx` | One component per slot id: `SidebarFolderSectionSlot`, `SessionCardBadgeSlot`, `SessionCardActionBarSlot`, `ContentViewSlot`, `ContentHeaderStickySlot`, `ContentInlineFooterSlot`, `AnchoredPopoverSlot`, `CommandRouteSlot`, `SettingsSectionSlot`, `ToolRendererSlot`. Each wraps contributions in `SlotErrorBoundary`. |
137
+ | `packages/dashboard-plugin-runtime/src/slot-error-boundary.tsx` | Per-claim React error boundary. Logs `[slot-error-boundary] Plugin "<id>" slot "<slot>" threw:` and renders nothing for the failing claim without suppressing siblings. |
138
+ | `packages/dashboard-plugin-runtime/src/vite-plugin/index.ts` | `viteDashboardPluginsPlugin(repoRoot?)` — generates `packages/client/src/generated/plugin-registry.tsx` with named imports (for tree-shaking). Watches manifests during dev; regenerates + triggers HMR on changes. Filters `fixture:true` plugins in production. |
139
+ | `packages/dashboard-plugin-runtime/src/server/loader.ts` | `discoverPlugins(repoRoot?)` (single-process-cache glob), `loadServerEntries(deps)` (per-plugin dynamic-import + `registerPlugin` invocation, failure-isolated), `getPluginStatusStore()`. |
140
+ | `packages/dashboard-plugin-runtime/src/server/server-context.ts` | `createServerPluginContext(deps, pluginId)` factory — creates a `ServerPluginContext` scoped to a plugin with namespaced logger and typed config accessors. |
141
+ | `packages/dashboard-plugin-runtime/src/server/config-validator.ts` | `validatePluginConfig`, `applySchemaDefaults` — Ajv JSON-Schema 7 validation for plugin config writes. |
142
+ | `packages/dashboard-plugin-runtime/src/server/plugin-status-store.ts` | In-memory `PluginStatusStore` — consumed by `/api/health.plugins[]`. |
143
+ | `src/server/routes/plugin-config-routes.ts` | `POST /api/config/plugins/:id` — validates `:id`, validates body against `configSchema`, merges into `plugins.<id>.*`, atomic-writes, broadcasts `plugin_config_update`. Auth-gated. |
144
+ | `packages/demo-plugin/` | Private workspace (`"private": true`, `"fixture": true`). Exercises `settings-section` (DemoSettings form) and `tool-renderer` (DemoToolRenderer green box for `toolName: "DashboardDemo"`). Excluded from production bundles. |
145
+ | `src/shared/stats-extractor.ts` | Extracts token/cost stats from turn_end events |
146
+ | `src/server/session-stats-reader.ts` | Reads cumulative stats + context usage from session JSONL files at startup |
147
+ | `src/server/server.ts` | HTTP + WebSocket server (composes route modules + wiring). Exports two pure helpers consumed by the bootstrap-state subscribe callback: `makeBootstrapTransitionHandler({ flushQueue, onTransitionToReady })` (stateful gate — fires both callbacks once per `installing → ready` transition, ignores the initial `ready` snapshot) and `runPostInstallRepair({ registry, directoryService, browserGateway })` (full `registry.rescan()` + per-cwd `refreshOpenSpec` with selective `openspec_update` broadcast on prior-empty / data-differs + per-cwd `refreshPiResources`; per-cwd failures isolated). The centralized hook covers all three install entry points (`runDegradedModeBootstrap`, REST `triggerUpgradePi`, REST `triggerRetry`). DEBUG-gated single-line diagnostic on completion. See change: fix-openspec-buttons-after-bootstrap-install. |
148
+ | `src/server/routes/session-routes.ts` | REST routes: sessions, events, session-diff |
149
+ | `src/server/routes/git-routes.ts` | REST routes: git branches, checkout, init, stash-pop |
150
+ | `src/server/routes/file-routes.ts` | REST routes: file read, browse (with `detect=0\|1` opt-in classifier), browse-flags (bulk classifier), browse-mkdir, readme, pinned-dirs. See change: split-browse-flags |
151
+ | `src/server/routes/openspec-routes.ts` | REST routes: openspec-archive, pi-resources, pi-resource-file |
152
+ | `src/server/routes/system-routes.ts` | REST routes: config, health, shutdown, tunnel, editors |
153
+ | `src/server/event-wiring.ts` | Pi gateway → browser gateway event forwarding (replay suppression with `skipReplayInsert` dedup, flows refresh dedup, context usage extraction). Phase-1 Extension UI System: caches `ui_modules_list` on `Session.uiModules` and broadcasts; caches `ui_data_list` on `Session.uiDataMap[event]` with a per-event item cap (default 1000, last-write-wins on overflow) and broadcasts. Phase-2: `ext_ui_decorator` switch arm caches descriptors under `Session.uiDecorators[`${kind}:${namespace}:${id}`]` (upsert, or delete when `removed: true`) and broadcasts the message verbatim to subscribers; deleting an absent key is a no-op but still broadcasts. **Last-activity stamping** (change: session-card-last-activity-badge): every live (non-replay) `event_forward` whose `eventType` passes `isActivityEvent(...)` (`event-status-extraction.ts` allowlist — `prompt_send`, `message_*`, `turn_end`, `tool_execution_*`, `agent_*`, `bash_output`, `flow_*`, `architect_*`) updates `session.lastActivityAt = Date.now()` in memory and broadcasts at most once per 30 s per session via `lastActivityBroadcastAt: Map<sessionId, ms>`; the map entry is dropped on `session_unregister` so a subsequent re-register does not silently suppress its first broadcast. See changes: add-extension-ui-modal, add-extension-ui-decorations, session-card-last-activity-badge. **Unread-trigger evaluation** (change: session-card-unread-stripes): right after the `extractSessionUpdates` block, snapshots `{status, currentTool}` before/after and calls `isUnreadTrigger(eventType, before, after, payload)`; if true AND `viewedSessionTracker.isViewedByAnyone(sessionId) === false` AND `!replayingSessions.has(sessionId)`, stamps `session.unread = true` and broadcasts `session_updated`. The `viewedSessionTracker` dep is optional on `EventWiringDeps` for backward compatibility — wiring is opt-in. |
154
+ | `src/server/idle-timer.ts` | Auto-shutdown idle timer with sleep-wake resilience |
155
+ | `src/server/session-bootstrap.ts` | Startup session discovery and OpenSpec polling init |
156
+ | `src/server/pi-gateway.ts` | Extension WebSocket gateway (port 9999) |
157
+ | `src/server/browser-gateway.ts` | Browser WebSocket gateway (dispatches to handler modules) |
158
+ | `src/server/browser-handlers/handler-context.ts` | Shared context type for browser message handlers |
159
+ | `src/server/browser-handlers/subscription-handler.ts` | Subscribe/unsubscribe with async batched replay, backpressure, lazy loading. Exports `replayUiState(ws, sessionId, ctx)` for the Extension UI System; called immediately after every `replayPendingUiRequests` site (4 sites). Replay sends the cached `ui_modules_list` (Phase 1) → one `ui_data_list` per `(event, items)` entry (Phase 1) → one `ext_ui_decorator` per `Session.uiDecorators` cache entry (Phase 2; never with `removed: true` since deleted entries are absent). Replay ordering: events → pending UI requests → ui_modules_list → ui_data_list → ext_ui_decorator. See changes: add-extension-ui-modal, add-extension-ui-decorations. |
160
+ | `src/server/browser-handlers/session-action-handler.ts` | Send prompt, abort, resume, spawn, shutdown, force kill, flow control. `handleSpawnSession` accepts an optional `SpawnSessionBrowserMessage.attachProposal` and enqueues it into `pendingAttachRegistry` BEFORE awaiting `spawnPiSession(...)` so a fast `session_register` cannot lose the intent (see change: add-folder-task-checker-and-spawn-attach). `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. |
161
+ | `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. |
162
+ | `src/client/components/ImageLightbox.tsx` | Full-size image lightbox with zoom/pan (useZoomPan), Esc/backdrop close |
163
+ | `src/client/components/CollapsedToolGroup.tsx` | Collapsed group of repeated tool calls with expand toggle |
164
+ | `src/client/lib/group-tool-calls.ts` | Groups consecutive identical tool calls for chat display |
165
+ | `src/client/lib/collapse-retried-errors.ts` | Two pure helpers used by `ChatView` to remove duplicate tool cards: `findRetriedErrorIds(messages)` returns ids of error `toolResult`s immediately superseded by a successful retry of the same tool (skipping `assistant`/`thinking`/`turnSeparator`/`rawEvent`/`commandFeedback`); `findActiveInteractiveToolResultIds(messages)` returns ids of `running` `toolResult`s paired with a *pending* `interactiveUi` message that follows them, so the running tool card can be hidden while the `InteractiveUiCard` is the sole interactive surface. See change: collapse-duplicate-tool-cards |
166
+ | `src/client/components/RetriedErrorBadge.tsx` | One-line `⚠ <toolName> failed — retried ›` pill rendered by `ChatView` in place of full `ToolCallStep` for ids returned by `findRetriedErrorIds`. Click toggles an expanded view that re-uses `<ToolCallStep status="error">` with a "Hide failed attempt" toggle so the original validation message + `Received arguments:` JSON is still recoverable. See change: collapse-duplicate-tool-cards |
167
+ | `src/server/browser-handlers/session-meta-handler.ts` | Rename, hide, unhide, attach/detach proposal, fetch, list. Attach/detach apply an **idempotent auto-rename rule** via the pure helpers in `packages/server/src/proposal-attach-naming.ts`: attach renames when name is empty OR equals the current `attachedProposal` (auto-set witness); detach reverts the name only when that equality holds. The same helpers are reused by the REST endpoints in `session-api.ts` to keep WS and REST in lockstep. See change: fix-mobile-attach-proposal-display. |
168
+ | `src/server/proposal-attach-naming.ts` | Pure decision-matrix helpers `attachRenameTarget(session, changeName)` and `detachShouldClearName(session)` used by both the WS handler and the REST endpoint to compute the idempotent auto-rename / auto-revert. Equality witness: `name === attachedProposal`. See change: fix-mobile-attach-proposal-display. |
169
+ | `src/server/browser-handlers/terminal-handler.ts` | Create, kill, rename terminals |
170
+ | `src/server/browser-handlers/directory-handler.ts` | Pin/unpin dirs, reorder, openspec refresh, pi-gateway forwards |
171
+ | `src/server/memory-event-store.ts` | In-memory event buffer with LRU eviction, per-session cap, payload truncation |
172
+ | `src/server/memory-session-manager.ts` | Pure in-memory session registry |
173
+ | `src/client/components/FolderOpenSpecSection.tsx` | Folder-level OpenSpec UI: collapsible change list, refresh, bulk archive, archive button. Each change row's `N/M tasks` indicator is a button that opens the existing `TasksPopover` (one popover at a time; opening another row swaps the popover); a `mdiPlay` icon button rendered after the artifact letters invokes `onSpawnAttached(cwd, changeName)` to spawn a pi session with the change pre-attached. Both buttons stop propagation so the surrounding folder collapse is unaffected. See change: add-folder-task-checker-and-spawn-attach. |
174
+ | `src/server/pending-attach-registry.ts` | In-memory FIFO queue of pending `attachProposal` intents per cwd. `enqueue(cwd, changeName)` is called from `handleSpawnSession` when the browser sets `SpawnSessionBrowserMessage.attachProposal`; `consume(cwd)` is called from `event-wiring.ts`'s `pi-gateway.onSessionRegistered` hook on every `session_register`, and the resolved name is applied via the shared `applyAttachProposal(sessionId, changeName, ctx)` helper in `session-meta-handler.ts` (same code as `handleAttachProposal`). Per-cwd cap = 8 (silent drop + warn), 60 s TTL on every read/write to avoid stranded intents from failed spawns. Cwd is normalized via `safeRealpathSync` + trailing-sep strip so `/p`, `/p/`, and symlink variants share a queue. In-memory only — not persisted. See change: add-folder-task-checker-and-spawn-attach. |
175
+ | `src/client/components/ArchiveBrowserView.tsx` | Searchable archive browser: date-grouped list, two-level nav to artifact reader |
176
+ | `src/client/hooks/useArchiveListing.ts` | Fetch hook + pure helpers (groupByDate, filterEntries) for archive endpoint |
177
+ | `src/server/openspec-archive.ts` | Scans `openspec/changes/archive/` and returns structured ArchiveEntry list |
178
+ | `src/client/components/SessionOpenSpecActions.tsx` | Session-level OpenSpec: searchable attach dialog, action buttons, detach |
179
+ | `src/client/components/DialogPortal.tsx` | Portal wrapper rendering dialogs at document.body with scroll lock |
180
+ | `src/client/components/PinDirectoryDialog.tsx` | Dialog to pin a directory (wraps PathPicker) |
181
+ | `src/client/components/PathPicker.tsx` | Reusable keyboard-first path picker with typeahead directory list |
182
+ | `src/client/lib/browse-api.ts` | Client-side browse API helper for PathPicker |
183
+ | `src/server/browse.ts` | Directory listing + classification for the browse API. Two responsibilities kept deliberately separate: `listDirectories(dir, q, { detect })` does enumeration (cheap, single `readdir`; per-entry `.git`/`.pi` probes only when `detect: true`), and `classifyPaths(paths)` does bulk classification (≤ `MAX_FLAG_PATHS = 100`, `fs.access` fan-out bounded by `createSemaphore(32)`, any error → `{ isGit: false, isPi: false }`). `parseFlagsQuery(rawPaths)` validates the `paths=<json-array>` query for `GET /api/browse/flags`. Worktree-safe: detection uses `fs.access` (never `readdir`) so `.git` regular-files in worktrees still classify correctly. See change: split-browse-flags |
184
+ | `src/server/pi-resource-scanner.ts` | Discovers pi extensions, skills, prompts from local, global, and package sources |
185
+ | `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")`. Includes `move(req)` for scope-to-scope package moves: hybrid execution — npm/git/https sources reinstall at dest then remove from origin (single `busy` lock, coalesced reload), filesystem-path sources settings-edit only (uses `settingsManager.getGlobalSettings/getProjectSettings/setPackages/setProjectPackages` to preserve filter objects). Identity preflight via `computeIdentity` before any side-effect; partial-success (install OK, remove failed) is delivered through the `package_operation_complete` WS event's `partialSuccess` field. See change: unify-package-management-ui. |
186
+ | `src/server/package-source-helpers.ts` | Pure helpers `parseSourceKind(source)` (npm/git/https/abs-path/rel-path) and `computeIdentity(source, settingsDir?)` (npm = bare name, git/https = url-no-ref, path = resolved absolute) mirroring pi's source-kind and dedup rules from `docs/packages.md`. Used by `package-manager-wrapper`'s `move()` arm-selection and identity preflight; safe for cross-platform tests (no `process.platform` branching). See change: unify-package-management-ui. |
187
+ | `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 |
188
+ | `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). Includes build-time tools `electron` and `node-pty` (registered as `kind: "module"` returning the package directory; resolved hoist-aware via `bareImportPackageDirStrategy` with optional `searchPaths`). See change: register-build-time-tools |
189
+ | `packages/shared/bin/pi-dashboard-resolve-tool.cjs` | Shell-callable resolver wrapper for the tool registry. CommonJS, dependency-free, invokable as `node packages/shared/bin/pi-dashboard-resolve-tool.cjs <tool-name> [--json]`. Mirrors the `override` + `bare-import` strategy semantics for build-time tools (electron, node-pty) so CI workflows / Dockerfiles / postinstall scripts can resolve hoisted-vs-nested layouts without depending on the shared package's TS build. **Strategy chain MUST stay in sync with `tool-registry/definitions.ts`** — enforced socially via cross-reference comments and via `no-hardcoded-node-modules-paths.test.ts`. See change: register-build-time-tools |
190
+ | `src/shared/__tests__/no-hardcoded-node-modules-paths.test.ts` | Repo-level lint: scans `.github/workflows/publish.yml`, `.github/workflows/ci.yml`, `packages/electron/scripts/Dockerfile.build`, and the two `fix-pty-permissions.cjs` postinstalls for hardcoded `node_modules/electron` / `node_modules/node-pty` substrings outside the explicit allowlist. Fails with `file:line:col` citation. Replaces hand-rolled inline `require.resolve(...)` patterns with the registered tool registry. Mirrors `no-direct-process-kill.test.ts` and `no-raw-node-import.test.ts`. See change: register-build-time-tools |
191
+ | `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 |
192
+ | `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 |
193
+ | `src/shared/tool-registry/types.ts` | `ToolDefinition`, `Strategy`, `StrategyResult`, `Resolution`, `Source`, `UnknownToolError`, `ModuleResolutionError` |
194
+ | `src/shared/tool-registry/index.ts` | Barrel export + `getDefaultRegistry()` singleton accessor |
195
+ | `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) |
196
+ | `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. |
197
+ | `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). |
198
+ | `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. |
199
+ | `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. |
200
+ | `packages/server/src/pi-version-skew.ts` | Pi compatibility range reader (`readPiCompatibility` reads `piCompatibility` from `packages/server/package.json`; `readCurrentPiVersion` via `createRequire`, with `fs.realpathSync` on the registry-resolved bin path so symlinked npm-global launchers resolve to the real module's `package.json`), 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. **CLI surface**: `cli.ts` calls `logCompatibilityWarning(bootstrapState)` after each `updateBootstrapCompatibility(...)` site and emits a stderr warning — 3-line red block on below-minimum (`⚠ pi X is below the required minimum Y … Run: pi-dashboard upgrade-pi`), single-line advisory on below-recommended, silent when in range. **Currently pinned**: `minimum: "0.70.0"`, `recommended: "0.70.0"`, `maximum: null` (lockstep — no backward compatibility for older pi versions). See changes: pi-zero-seventy-compat, warn-pi-version-skew-in-cli. |
201
+ | `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 }`. |
202
+ | `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. |
203
+ | `src/client/lib/tools-api.ts` | Client-side fetch helpers for `/api/tools*` (`fetchTools`, `rescanAll`, `rescanOne`, `setOverride`, `clearOverride`, `downloadDiagnostics`) |
204
+ | `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 |
205
+ | `src/server/npm-search-proxy.ts` | Cached proxy for npm registry search (`keywords:pi-package`) and README fetch |
206
+ | `src/server/routes/package-routes.ts` | REST routes: search, readme, installed, install, remove, update, check-updates |
207
+ | `src/client/components/SortablePinnedGroup.tsx` | Drag-to-reorder wrapper for pinned directory groups |
208
+ | `src/server/preferences-store.ts` | Global UI preferences (pinned dirs, session order) in `preferences.json` |
209
+ | `src/server/meta-persistence.ts` | Per-session debounced `.meta.json` writer |
210
+ | `src/server/session-scanner.ts` | Startup session discovery by scanning `~/.pi/agent/sessions/`. Each restored `DashboardSession` has `lastActivityAt` cold-start-seeded from the `events.jsonl` mtime (one `fs.statSync` per session, defensive try/catch — returns `undefined` on error so the badge falls back to `startedAt`). See change: session-card-last-activity-badge. The restored session also carries `unread` from `meta.unread` so unread sessions stay flagged across server restart. The `server.ts:273-279` cold-start "force status=ended" override only mutates `status` and `endedAt`, leaving `unread` intact — a session that was unread when the server stopped is still unread when it starts back up, even before its bridge reattaches. See change: session-card-unread-stripes. |
211
+ | `src/server/migrate-persistence.ts` | One-time migration from `sessions.json` + `state.json` to `.meta.json` |
212
+ | `src/server/session-order-manager.ts` | Per-cwd session ordering with persistence. **Move-to-front semantic** (change: top-of-tier-on-status-change): exports `moveToFront(cwd, sessionId)` — idempotent `remove + unshift` used by `server.ts onChange`'s ended→alive user-intent branch so the just-resumed card always surfaces at the top of the alive tier, even on repeated end→resume cycles. Bridge auto-reattach short-circuits before any mutation via `pendingResumeIntents.consume()`. |
213
+ | `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). **Per-change watch set** (`perChangeArtifactPaths`) covers `<change>/`, `tasks.md`, `proposal.md`, `design.md`, plus `specs/`, every immediate `specs/<cap>/`, and every `specs/<cap>/spec.md` — `readdirSync` of `specs/` is try/catch-wrapped so missing dirs yield an empty fan-out (change: fix-openspec-specs-mtime-gate-blind-spot, which also wires `createFsSpecsProbeFactory(cwd)` into `buildOpenSpecData` as a second probe-factory argument so multi-spec authoring lights up green even when the gate misfires). **TOCTOU-safe stamping** (change: fix-openspec-mtime-gate-toctou): the per-change status loop captures `preCallMtime` *before* awaiting `runOpenSpecStatus` and stamps THAT value into the cache; if the post-call effective mtime differs, the entry is racy and the cache is left untouched (next gated tick re-polls). DEBUG-gated `console.warn` cites this change name when the discard branch fires. **Refresh contract**: `refreshOpenSpec(cwd)` is the user-clicked path and bypasses the gate via `pollOne(cwd, true)` — it's the manual escape hatch when the gate's heuristic is wrong. `pollDirectoryGated`, `onDirectoryAdded`, and the post-archive refresh in `handleOpenSpecBulkArchive` all call `pollOne(cwd, false)` so periodic / internal paths stay O(1) status spawns. `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 changes: fix-openspec-specs-mtime-gate-blind-spot, fix-openspec-mtime-gate-toctou, fix-openspec-mtime-gate-blind-spots, optimize-openspec-poll-burst |
214
+ | `src/server/pending-fork-registry.ts` | Tracks pending fork operations for session placement |
215
+ | `src/server/pending-resume-registry.ts` | Queues prompts for auto-resume of ended sessions |
216
+ | `src/server/pending-resume-intent-registry.ts` | In-memory `Map<sessionId, timestamp>` (default 60 s TTL, lazy expiry on read). `record(id)` is called by `handleResumeSession` (WS) and the REST `POST /api/session/:id/resume` handler immediately before `spawnPiSession`; `consume(id)` is called by `server.ts`'s `sessionManager.onChange` hook in the ended→alive branch. **4-way intent contract** (change: reattach-move-to-front): when `consume` returns `null` (no user intent tagged) the branch checks `OnChangeContext.registerReason`; if `"reattach"` it applies the configured `reattachPlacement` policy via `reattach-placement.ts::applyReattachPolicy`, otherwise (legacy bridge or genuine spawn) it returns early without mutation. Registry intents (`"front"` / `"keep"`) always win over `registerReason` per spec. The `if (!order.includes(sessionId))` guard inside the branch keeps drag-to-resume's dropped slot intact when the intent is present. In-memory only — NOT persisted across server restarts. See changes: preserve-session-order-on-reboot, reattach-move-to-front. |
217
+ | `src/server/reattach-placement.ts` | Pure `decideReattachAction(policy, status)` + I/O `applyReattachPolicy(sessionId, cwd, policy, deps, priorStatus?)` for the bridge-reattach placement policy. Mapping: `"always"` → `moveToFront`; `"streaming-only"` → `moveToFront` iff `effectiveStatus === "streaming"` where `effectiveStatus = priorStatus ?? session.status`; `"preserve"` → no-op. **`priorStatus` is required for `streaming-only` to work**: `memory-session-manager.ts::register` unconditionally sets `status: "active"`, so without the prior value the policy would silently behave as `"preserve"`. Captured by `register()` from `existing?.status` BEFORE assembling the new session and forwarded via `OnChangeContext.priorStatus`. `applyReattachPolicy` is wired from `server.ts onChange` at TWO sites: (a) the existing ended→alive branch when the consumed registry intent is `null` and `ctx.registerReason === "reattach"`, and (b) a new alive→alive branch (`!isEnded && !wasEnded && ctx?.registerReason === "reattach"`) that handles the common case where the session was persisted as `"active"` so neither end-state transition fires. See change: reattach-move-to-front. |
218
+ | `src/server/json-store.ts` | Atomic JSON file read/write helpers |
219
+ | `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). |
220
+ | `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. |
221
+ | `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). |
222
+ | `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`. |
223
+ | `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. |
224
+ | `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. |
225
+ | `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). |
226
+ | `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. |
227
+ | `src/shared/platform/node-spawn.ts` | **Sole source of `node --import <loader> <entry>` argv construction**: `toFileUrl(pathOrUrl)` (idempotent path → file:// URL, handles Windows drive letters on POSIX hosts), `spawnNodeScript(opts)` (wraps both loader and entry positions as file:// URLs before delegating to `platform/exec.ts::spawn`). Every `spawn(process.execPath, ["--import", ...])` call outside this file with raw path arguments is banned by `no-raw-node-import.test.ts`. Fixes `ERR_UNSUPPORTED_ESM_URL_SCHEME` on non-C: Windows drives (e.g. `B:\`) where Node's drive-letter heuristic for entry-script paths has known gaps. See change: fix-windows-entry-script-url. **Jiti version contract** (change: fix-electron-windows-installer-and-server-bootstrap, Defect 2): `shouldUrlWrapEntry()`'s Windows-non-tsx arm assumes the jiti loader is from `@mariozechner/pi-coding-agent@0.70.x` (jiti 2.x with the `file:///` triple-slash URL handling fix). Newer jiti versions (e.g. jiti 2.6.5 in pi 0.71.x) misnormalize triple-slash URLs and break the contract. The contract is defended by Defect 1's fix (managed-dir population from the offline cacache pins pi to 0.70.0); regression-pinned by `node-spawn-jiti-contract.test.ts` which asserts `offline-packages.json` keeps the pin in the supported `0.70.x` range AND the docstring documents the contract. |
228
+ | `src/shared/__tests__/no-raw-node-import.test.ts` | Repo-level lint: scans `packages/*/src/` (excluding `platform/node-spawn.ts` + `resolve-jiti.ts` + `__tests__/`) for `spawn(...)` argv with `"--import"` / `"--loader"` followed by raw identifiers not wrapped in `toFileUrl(...)` / `pathToFileURL(...)`. Mirrors `no-direct-process-kill.test.ts` pattern. |
229
+ | `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`. |
230
+ | `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. |
231
+ | `src/server/editor-registry.ts` | Detects available native editors (running processes + CLI) |
232
+ | `src/server/editor-manager.ts` | Lifecycle manager for code-server child processes (spawn, stop, idle, heartbeat) |
233
+ | `src/server/editor-proxy.ts` | Reverse proxy for `/editor/:id/*` to code-server instances |
234
+ | `src/server/editor-detection.ts` | Auto-detect code-server/openvscode-server binary on PATH |
235
+ | `src/server/routes/editor-routes.ts` | REST routes: editor start, stop, heartbeat, status, detect |
236
+ | `src/server/event-status-extraction.ts` | Extracts session status/tool updates from events (incl. flow metadata). Hosts two pure classifiers consumed by `event-wiring.ts`: `isActivityEvent(eventType)` (allowlist driving `lastActivityAt` stamping; see change: session-card-last-activity-badge) and `isUnreadTrigger(eventType, before, after, payload)` (returns true on `streaming→idle\|active`, on `currentTool→"ask_user"`, and on `agent_end` with truthy `payload.error`; see change: session-card-unread-stripes). |
237
+ | `src/server/viewed-session-tracker.ts` | In-memory `Map<sessionId, Set<WebSocket>>` registry of which browser has which session displayed (`/session/:id`). Created by `browser-gateway.ts`, exposed on `BrowserGateway.viewedSessionTracker`, and threaded into `wireEvents({ ..., viewedSessionTracker })`. `view`/`unview` are called from the `session_view`/`session_unview` switch arms; `unviewAll(ws)` is called on every WS `close` so disconnected browsers cannot hold sessions in the viewed state. `isViewedByAnyone(sessionId)` gates the unread-trigger stamp in `event-wiring.ts`. Read state is GLOBAL across browsers — mirrors mail/Slack. In-memory only. See change: session-card-unread-stripes. |
238
+ | `src/server/headless-pid-registry.ts` | Maps headless child PIDs to session IDs |
239
+ | `src/server/auth.ts` | OAuth2 authentication: provider registry, JWT helpers, user allowlist |
240
+ | `src/server/provider-auth-handlers.ts` | Pi provider OAuth handlers (Anthropic, Codex, GitHub Copilot, Gemini CLI, Antigravity) |
241
+ | `src/server/provider-auth-storage.ts` | Read/write ~/.pi/agent/auth.json with lockfile for pi provider credentials |
242
+ | `src/server/routes/provider-auth-routes.ts` | REST routes: provider OAuth authorize/exchange/callback, device-code, API key CRUD |
243
+ | `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` |
244
+ | `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 |
245
+ | `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 |
246
+ | `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 |
247
+ | `src/client/components/ProviderAuthSection.tsx` | Settings section: OAuth login buttons, device-code modal, API key inputs |
248
+ | `src/server/auth-plugin.ts` | Fastify plugin: auth routes, onRequest hook, WS upgrade validation |
249
+ | `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). |
250
+ | `src/client/components/SettingsPanel.tsx` | Settings UI: all dashboard config fields, grouped form, save to server |
251
+ | `src/client/hooks/useAuthStatus.ts` | Client auth status hook and login redirect helper |
252
+ | `src/server/localhost-guard.ts` | Network access guard: `createNetworkGuard` (loopback/trusted/authenticated), `isBypassedHost` (CIDR/wildcard/exact), netmask-to-CIDR helpers |
253
+ | `src/server/server-pid.ts` | PID file management for daemon mode |
254
+ | `src/client/components/ServerSelector.tsx` | Server selector dropdown showing persisted known servers. Availability probing runs **only on dropdown open** (once per open, not on mount, not on a timer, not while closed). Accepts `inFlightSwitchKey` to render a spinner on the entry being switched to. Unreachable entries are `disabled`, rendered with `opacity-50` + `cursor-not-allowed`, and clicks are no-ops. Reachable clicks delegate to the transactional staging-socket switch (see `server-switch.ts`). See change: safe-server-switch |
255
+ | `packages/client/src/lib/staging-socket.ts` | `openStagingSocket(url, {timeoutMs}): Promise<WebSocket>` — single-settle helper that resolves on first `OPEN`, rejects on error/close/timeout, and closes the socket on timeout to avoid leaks. Used by the transactional server-switch. |
256
+ | `packages/client/src/lib/server-switch.ts` | `performServerSwitch(target, deps)` — extracted two-phase transaction (stage → commit) from `App.tsx`'s `handleServerSwitch`. Guarantees the ordering `clearInMemoryState` → `setWsUrl` → `persistLastServer`, and never persists localStorage or clears state on staging failure. Fully unit-tested. |
257
+ | `packages/client/src/components/ConnectionStatusBanner.tsx` | Disconnection banner: appears only after the active WebSocket has been non-`OPEN` for &gt;3s continuously; hidden immediately on reconnect; suppressed during in-flight staging switch. Mounted above `<MobileShell>` in `App.tsx`. |
258
+ | `src/client/components/KnownServersSection.tsx` | Settings section: list/add/remove persisted known remote servers |
259
+ | `src/client/components/NetworkDiscoverySection.tsx` | Settings section: mDNS network scan with "Add" action and label prompt. When the scan finds zero servers (common — Wi-Fi AP isolation, mesh routers, VLANs, VPN, firewall), renders a diagnostic block listing failure causes plus an inline manual-add form that accepts a free-form host input parsed via `parseHostInput()`. Surfaces scan errors instead of swallowing them. See change: diagnose-empty-mdns-scan |
260
+ | `src/client/lib/parse-host-input.ts` | Pure helper `parseHostInput(input, defaultPort = 8000) → {host, port} \| null` accepting full URLs (`http://192.168.16.202:8000`), `host:port`, bare hostnames, and bracketed IPv6 (`[::1]:8000`). Rejects empty input, bare IPv6 (ambiguous with `host:port`), invalid ports, malformed URLs. 12 unit tests in `parse-host-input.test.ts`. See change: diagnose-empty-mdns-scan |
261
+ | `src/client/lib/known-servers-api.ts` | Client-side fetch helpers for known servers CRUD and discovery endpoints |
262
+ | `src/server/routes/known-servers-routes.ts` | REST routes: known servers CRUD, on-demand mDNS discovery scan |
263
+ | `src/server/terminal-manager.ts` | PTY lifecycle, ring buffer, spawn/attach/kill terminals |
264
+ | `src/server/terminal-gateway.ts` | Binary WebSocket upgrade handler for `/ws/terminal/:id` |
265
+ | `scripts/fix-pty-permissions.cjs` | Postinstall: fix node-pty spawn-helper execute permissions |
266
+ | `src/server/tunnel.ts` | Zrok tunnel with reserved shares for persistent URLs, binary detection, PID tracking, stale cleanup |
267
+ | `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 |
268
+ | `src/client/components/QrCodeDialog.tsx` | QR code dialog showing tunnel URL as scannable QR code with copy, disconnect, and setup buttons |
269
+ | `public/manifest.json` | PWA web app manifest for installability |
270
+ | `public/sw.js` | Minimal service worker for PWA installability |
271
+ | `src/client/components/ZrokInstallGuide.tsx` | OS-aware zrok installation guide view (macOS/Linux/Windows) |
272
+ | `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. The local post-install `registry.rescan("pi")` block was removed and ownership of the post-install rescan + force-refresh moved to the centralized `bootstrapState.subscribe` hook in `server.ts`. See change: fix-openspec-buttons-after-bootstrap-install. **`cmdRestart(config, injected?)`** (change: fix-restart-bridge-auto-start-race): probes `isDashboardRunning(port)`; if up, POSTs `/api/restart` with `{dev}` and exits, delegating the entire stop/start to `restart-helper.ts`'s detached orchestrator; if down or HTTP fails, falls back to local `cmdStop()` + `cmdStart()`. Eliminates the in-process race where `cmdStop` killed the daemon and bridges' auto-start beat the subsequent `cmdStart`'s `isServerRunning` check (silently early-returning, leaving the user offline). The optional `injected` arg is for unit tests — production callers always use the real defaults. |
273
+ | `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. **Explicit prior-daemon kill** (change: fix-restart-bridge-auto-start-race): the embedded script reads `~/.pi/dashboard/dashboard.pid`, sends `SIGTERM` to the recorded PID if alive, polls for exit (3 s deadline), then `SIGKILL`. Removes the "wait for self-exit" ambiguity that let bridges race the orchestrator before this change. The `portFree` poll is reduced from 10 s to 5 s since step 0 already guarantees the previous server is dead. |
274
+ | `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 |
275
+ | `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.*` |
276
+ | `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. **Per-session group-key precedence** (change: add-jj-workspace-plugin, Decision 15): exported pure helper `resolveSessionGroupPath(session, pinnedKeys, platform)` resolves the group key as **explicit pin > `jjState.workspaceRoot` > `cwd`** — a session inside a `.shadow/<name>/` jj workspace collapses under its parent repo's group, but an explicit pin on the workspace path still wins. Within a group, sessions are pre-sorted by `clusterByWorkspaceName` so all rows sharing the same `(jjState?.workspaceName ?? "")` cluster adjacently (empty / main-tree first, then ws-A, ws-B, …); the existing `sortSessionsByOrder` ranking still applies inside each cluster. Tests: `packages/client/src/lib/__tests__/session-grouping.test.ts` (5 tests) covers the four scenarios from the spec plus the default-workspace regression guard. |
277
+ | `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; **`isAppImageSelfHit(path, opts?)`** — pure helper that flags a candidate binary path as the running Electron AppImage launcher when `realpath(path) === realpath(process.execPath)`, when `path` lives under `process.env.APPDIR` (squashfs mount), or when `realpath(path) === realpath(process.env.APPIMAGE)`. Defensive try/catch around every `realpath`. Consumed by `whereStrategy` (Layer 2 — every registry-resolved tool inherits the guard) and `detectPiDashboardCli` / `detectPi` / `detectSystemNode` (Layer 1 — belt-and-braces). See change: fix-electron-appimage-cli-self-detection), `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. |
278
+ | `src/shared/rest-api.ts` | REST API type definitions |
279
+
280
+ | `.pi/skills/release-cut/SKILL.md` | Cuts a new release: promotes `## [Unreleased]` in CHANGELOG to a dated section, bumps every workspace package.json, commits, tags, pushes (which fires `publish.yml`). The skill's `Next steps (human)` block enumerates the **7 platform artifacts** the human releaser should expect on the draft GitHub Release: `PI-Dashboard-darwin-arm64-<ver>.dmg` (Apple Silicon), `PI-Dashboard-darwin-x64-<ver>.dmg` (Intel), Linux `.deb` × 2 (x64+arm64), Linux `.AppImage` (x64 only — appimagetool has no arm64 build), Windows NSIS+ZIP+portable (x64), Windows ZIP+portable (arm64, no NSIS cross-compile). Missing artifacts in the draft = a CI failure; do NOT click Publish. (change: add-darwin-x64-build updated the count from 6 → 7 and split the macOS DMG into two arches.) |
281
+ | `.pi/skills/spec-coherence-check/SKILL.md` | Skill: sweep proposals for staleness, conflicts, obsolescence against codebase |
282
+ | `.pi/skills/spec-coherence-check/references/proposal-queue-schema.md` | JSON schema for `.pi/proposal-queue.json` |
283
+ | `.pi/skills/code-review/SKILL.md` | Skill: comprehensive code review with severity labels, four-phase process, language-specific guides |
284
+ | `.pi/skills/code-review/references/` | On-demand language guides (React, TypeScript, Vue, Rust, Go, Java, Python, C/C++, CSS, Qt) + architecture/performance/security reviews |
285
+ | `.pi/skills/nano-banana-imagegen/SKILL.md` | Skill: AI image generation/editing via Google Gemini (nano-banana CLI) |
286
+ | `.pi/skills/nano-banana-imagegen/references/` | Prompting guide, example prompts (headers, icons, illustrations, photography) |
287
+ | `.pi/skills/browser-visual-debug/SKILL.md` | Skill: visual debugging with a real browser (screenshots, interaction, responsive testing) via pi-agent-browser |
288
+ | `.pi/skills/browser-visual-debug/references/` | Dashboard recipes, responsive testing presets, agent-browser commands cheatsheet |
289
+ | `.pi/skills/browser-visual-debug/scripts/detect-dashboard.sh` | Auto-detect dashboard URL, mode, and Vite dev server status |
290
+ | `packages/electron/src/main.ts` | Electron main process: single-instance, wizard, server launch, loading page, tray. **Power-user-mode managed install** (change: fix-electron-windows-installer-and-server-bootstrap, Defect 1): the firstRun `pi.found && bridge.found` auto-skip path no longer skips `installStandalone()`. Auto-skip removes the wizard UI; the managed install (tsx + pi@0.70.0 + openspec from the offline cacache) runs anyway so the bundled server's runtime can resolve its TS loader from `~/.pi-dashboard/`. Decision logic extracted into the pure `decideStartupAction(state)` helper in `lib/power-user-install.ts` and pinned by `wizard-power-user-managed-install.test.ts`. **External-link hardening (change: harden-external-link-handling, #13)**: `createMainWindow` registers `webContents.setWindowOpenHandler` + `will-navigate` BEFORE `loadURL` so `target="_blank"` / `window.open` and bare-`<a>` external navigation both route through `shell.openExternal` (system browser) instead of replacing the dashboard. The `will-navigate` callback is **OAuth-aware** via `decideWillNavigate(serverUrl, webContents.getURL(), url)` (change: fix-oauth-blocked-by-external-link-guard) so the trap-guard fires only when leaving the dashboard — while the user is mid-login on an OAuth provider page (Google, GitHub, generic OIDC), provider-internal multi-step navigation is allowed and the eventual callback redirect lands cleanly. `setWindowOpenHandler` is unchanged. |
291
+ | `packages/electron/src/lib/link-handling.ts` | Pure helpers used by the Electron shell. **`isSameOriginUrl(href, serverOrigin)`** — classifies URLs as same-origin vs external (handles absolute, relative, fragment-only, and malformed inputs; see change: harden-external-link-handling, #13). **`decideWillNavigate(serverOrigin, currentUrl, targetUrl) → "allow" | "open-external" | "cancel"`** — OAuth-aware decision wrapper for the `will-navigate` callback: while the BrowserWindow shows a non-dashboard origin (mid-OAuth) it returns `"allow"` so provider login flows proceed; on the dashboard it composes `isSameOriginUrl` to decide between `"allow"` (same-origin) and `"open-external"` (external trap-guard); fail-closes to `"cancel"` on unparseable `serverOrigin` (see change: fix-oauth-blocked-by-external-link-guard). No Electron imports; 23 unit tests in `packages/electron/src/__tests__/link-handling.test.ts`. |
292
+ | `packages/client/src/components/MarkdownContent.tsx` | ReactMarkdown-based renderer for chat bodies, thinking blocks, flow agent detail, package READMEs, markdown previews. **External-link hardening (change: harden-external-link-handling, #13)**: exports pure `isExternalHref(href)` and overrides the `a` component so external URLs render with `target="_blank" rel="noopener noreferrer"` while fragment-only and same-origin hrefs stay in-document. |
293
+ | `packages/client/src/__tests__/no-bare-external-anchor.test.ts` | Repo-level lint: scans client `.tsx` files for `<a href="http(s)://...">` tags missing `target="_blank"`. Per-line opt-out via `// ban:bare-anchor-ok`. See change: harden-external-link-handling. |
294
+ | `packages/electron/src/lib/server-lifecycle.ts` | Health check → tsx binary spawn (inlined config/health, no shared pkg imports). **60-second startup deadline + cause-aware error wording** (change: fix-electron-windows-installer-and-server-bootstrap, Defect 4): exports `SERVER_READY_DEADLINE_MS = 60_000` (was inline `15_000`) and a pure helper `buildServerStartupError(...)` that renders "Server child process exited prematurely (...)" when `ready.error` mentions an exit, vs. "Server did not respond within 60 seconds (...)" when the deadline elapsed. The pre-fix message conflated both cases and was misleading when the child died in <1s. Both `launchViaCli` and `launchServer` use the same constant + helper. |
295
+ | `packages/server/src/extension-register.ts` | Auto-registers bundled bridge extension in pi's global settings on startup |
296
+ | `packages/electron/src/lib/doctor.ts` | Doctor diagnostic: checks all binaries, versions, server status, offers setup |
297
+ | `packages/electron/src/lib/app-menu.ts` | App menu with About dialog and Doctor on all platforms |
298
+ | `packages/electron/src/lib/tray.ts` | System tray with platform-specific icons (template on macOS, ico/png on Win/Linux) |
299
+ | `packages/electron/src/lib/dependency-installer.ts` | Async npm install of pi, openspec, tsx into ~/.pi-dashboard/ using bundled Node |
300
+ | `packages/electron/src/lib/dependency-detector.ts` | Detects pi, openspec, Node.js on system PATH and managed install. **AppImage self-recursion guard** (change: fix-electron-appimage-cli-self-detection): `detectPiDashboardCli` rejects any candidate that matches `isAppImageSelfHit(path)` (in addition to the existing `_npx` filter) so power-user mode falls through to the standalone tsx + `cli.ts` path when the only `pi-dashboard` on PATH is the AppImage's own launcher (`packagerConfig.executableName: "pi-dashboard"` collides by design). `detectPi` and `detectSystemNode` apply the same guard symmetrically on the registry-resolved path — belt-and-braces beyond the `whereStrategy` filter. **Windows extension filter** (change: fix-electron-windows-installer-and-server-bootstrap, Defect 3): exports a pure helper `pickSpawnableShim(rawWhereOutput, platform)` that, on `win32`, prefers candidates ending in `.cmd`/`.exe`/`.bat`/`.ps1` over an extensionless POSIX shim from npm-global. `spawn()` without `shell:true` cannot invoke an extensionless shim on Windows, so the pre-fix `lines[0]` pick produced `ENOENT`. POSIX behaviour (single-line `which`) unchanged. Locked by `dependency-detector-windows-extensions.test.ts`. |
301
+ | `packages/electron/src/lib/bundled-node.ts` | Resolves bundled Node.js/npm paths in Electron resources |
302
+ | `packages/electron/src/lib/wizard-window.ts` | First-run setup wizard window with preload bridge |
303
+ | `packages/electron/forge.config.ts` | Electron Forge config: DMG, DEB, AppImage, NSIS makers, icon, extraResources. **NSIS naming overrides** (change: fix-electron-windows-installer-and-server-bootstrap): the NSIS maker's `getAppBuilderConfig` callback explicitly pins `productName`, `appId`, `nsis.artifactName`, `nsis.shortcutName`, `nsis.uninstallDisplayName` all to `pi-dashboard`. Without this override, electron-builder's NSIS install-dir fallback chain reads npm `name` slash-stripped and produces `@blackbelt-technologypi-dashboard-electron`. Locked by `forge-config-naming.test.ts`. |
304
+ | `packages/electron/scripts/build-installer.sh` | Build script: native + Docker cross-platform (--linux, --windows, --all). **`--mac-both`** (change: add-darwin-x64-build) builds BOTH macOS DMGs (arm64 + x64) on an Apple Silicon host in one invocation: orchestrates two sequential `build_native_one_arch` calls with sentinel-driven cache invalidation between them (`resources/.last-arch`); refuses on Intel hosts (Rosetta is one-way, x64 → arm64 cross-compile is not supported locally) and on non-darwin hosts. Cross-arch x64 builds on Apple Silicon hosts auto-detect Rosetta 2 via `arch -x86_64 /usr/bin/true` (fail-fast with `softwareupdate --install-rosetta --agree-to-license` hint if missing) and wrap `bundle-server.mjs` in `arch -x86_64` + sets `TARGET_ARCH=x64` env so node-pty's prebuilt x64 binary is downloaded. The `--mac-both` post-build smoke summary mounts each DMG and reports the inner Mach-O arch tag (`arm64` / `x86_64`) via `file` so silent arch-mismatch artifacts cannot ship undetected. The arch-aware cache wipe also fires on single-arch back-to-back invocations (`--arch arm64` then `--arch x64`) so manual cache cleanup is no longer required. Pure helpers: `maybe_wipe_arch_caches`, `verify_rosetta_or_fail`, `build_native_one_arch`. |
305
+ | `packages/electron/scripts/docker-make.sh` | Docker entrypoint: platform-aware native module handling, ZIP for Windows |
306
+ | `packages/electron/scripts/Dockerfile.build` | Docker image for cross-platform builds (node:22-bookworm-slim + build tools) |
307
+ | `packages/electron/scripts/bundle-server.mjs` | Bundles dashboard server source + deps into resources/server/ (`--source-only` for cross-builds). Node-native ESM script (replaces `bundle-server.sh`) so Windows electron CI runs without MSYS/bash. Uses `fs.cpSync` / `fs.chmodSync` / recursive `readdir` instead of `cp -R`/`find`/`chmod`/`du`. Verified bit-parity with the old shell script: identical 2251-file layout, identical structure. See change: eliminate-bash-on-windows-runners. **Architectural lock**: synthetic `package.json` deliberately does NOT declare `@mariozechner/pi-coding-agent` (or any managed-dir-resident dep). The bundled tree only contains workspace deps (`fastify`, `ws`, `node-pty`, etc.) directly imported by the bundled `cli.ts`. pi/openspec/tsx live in the managed dir (`~/.pi-dashboard/`) and are installed there by `installStandalone()` from the offline cacache pinned in `offline-packages.json`. An earlier `/opsx:apply` session against `fix-electron-windows-installer-and-server-bootstrap` proposed adding a `dependencies: { "@mariozechner/pi-coding-agent": "0.70.0" }` block here — reverted as architecturally wrong (would duplicate ~10MB and create version-drift risk vs. the offline cacache). Bundle stays at ~80MB; if it ever climbs to ~160MB, that's the regression marker. See change: fix-electron-windows-installer-and-server-bootstrap (D5 reconsidered). |
308
+ | `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`) |
309
+ | `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`. |
310
+ | `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()`. |
311
+ | `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. |
312
+ | `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`. |
313
+ | `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. |
314
+ | `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)"`. |
315
+ | `packages/electron/src/lib/wizard-badge.ts` | Pure `classifyProgressBadge(output)` helper (`bundled` / `system` / null) shared between wizard HTML inline JS and unit tests. |
316
+ | `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"]` only** — `"pi-flows"` was removed in commit b9b3d7e because the upstream repo (BlackBeltTechnology/pi-flows) declares no SPDX license (no `LICENSE` file, no `package.json#license`), and `bundle-recommended-extensions.sh`'s allowlist (MIT/Apache-2.0/BSD-2-Clause/BSD-3-Clause/ISC) correctly rejects it — which was blocking every electron build matrix variant. Re-add `"pi-flows"` once upstream declares a license. The npm publish path is unaffected (only electron build was broken). See `openspec/changes/archive/2026-04-21-bundle-first-party-extensions/design.md` for original design + license-blocker discussion. |
317
+ | `packages/electron/scripts/docker-make.sh` | Docker entrypoint: bundles server, installs native deps, runs Forge make |
318
+ | `packages/electron/scripts/Dockerfile.build` | Docker image for cross-platform builds (node:22-bookworm-slim + build tools) |
319
+ | `packages/electron/scripts/test-server-launch.sh` | Docker-based test for server launch on clean Linux |
320
+ | `packages/electron/scripts/test-electron-install.sh` | Full e2e Docker test: install, wizard, server launch, health check |
321
+ | `packages/electron/scripts/test-electron-install-inner.sh` | Inner test script run inside Docker container |
322
+ | `packages/electron/resources/icon.png` | Master 1024×1024 app icon (π on dark navy) |
323
+ | `.github/workflows/publish.yml` | CI: builds DMG × 2 (macOS arm64 + x64), DEB+AppImage (Linux), NSIS+ZIP+portable (Windows) on native runners; publishes npm + GitHub Release. **Build matrix covers 6 (platform, arch) tuples** — darwin/arm64 (`macos-14`), darwin/x64 (`macos-13`, change: add-darwin-x64-build), linux/x64 (`ubuntu-latest`), linux/arm64 (`ubuntu-24.04-arm`), win32/x64 (`windows-latest`), win32/arm64 (`windows-latest`). Missing rows are a regression — the spec requirement `electron-build-pipeline > CI build matrix` enumerates each scenario explicitly to prevent silent drift. **Two triggers**: (a) push of any `v*` tag (the original path — release-cut skill / hand tag), (b) `workflow_dispatch` from the GitHub Actions UI with a single required `version` input (e.g. `"0.4.1"`). The `prepare` job branches on `github.event_name`: tag-push extracts the version from `GITHUB_REF_NAME`; dispatch validates the input as semver, checks tag uniqueness on origin, bumps every workspace `package.json` via `npm version -ws`, syncs cross-ref specifiers via `scripts/sync-versions.js`, promotes `## [Unreleased]` to a dated `## [<version>]` section in `CHANGELOG.md`, commits + tags + pushes the branch. The publish, electron, and github-release jobs all `needs: prepare` and check out `ref: ${{ needs.prepare.outputs.tag }}` so both trigger paths publish the same tree. **Idempotent ordered npm publish (commit b9fcea9)**: the publish step replaced the bulk `npm publish --workspaces --include-workspace-root` call with a per-package loop that (a) **skips** already-published versions via `npm view <pkg>@<ver>` (so a re-run after partial-publish failure resumes cleanly instead of aborting on "cannot publish over previously published"), (b) publishes the **4 stable sub-packages first** (`pi-dashboard-shared` → `extension` → `server` → `web`), then the brand-new `dashboard-plugin-runtime`, then the **root metapackage last** so the registry already serves matching sub-package versions before the root tarball lands and resolves dependents like `@blackbelt-technology/pi-dashboard-extension@^X.Y.Z`. v0.4.0 and v0.4.1 shipped broken because the bulk call aborted on the first error and only the root tarball landed — `npm install @blackbelt-technology/pi-agent-dashboard@0.4.1` returned ETARGET on the sub-deps. Single-failure isolation: any non-skip failure marks the step failed via a `FAIL=1` accumulator but lets the loop finish so logs show every package's outcome. Also (commit b9fcea9): `packages/server/package.json#dependencies` now declares `@blackbelt-technology/dashboard-plugin-runtime: ^<ver>` — previously imported via workspace symlinks but missing from the published tarball, so a clean `npm install` of just the server crashed with `MODULE_NOT_FOUND` on first start. Also (commit 2728c31): every workspace `package.json` (`shared`, `extension`, `server`, `client`, `dashboard-plugin-runtime`, plus `electron`) now declares a `repository` field — required for npm provenance attestation validation when publishing with `--provenance` from GitHub Actions OIDC. **Electron-publish dependency-graph contract** (change: publish-fix-macos): the `electron` matrix job declares `needs: [prepare, publish]` and `strategy.fail-fast: false`. The `needs: publish` gate closes the ETARGET race that broke release run #34 — `bundle-server.mjs` runs `npm install --omit=dev` against the live npm registry and resolves `@blackbelt-technology/dashboard-plugin-runtime@^<ver>` (added in commit b9fcea9 to fix `MODULE_NOT_FOUND` on clean server installs), so it must run AFTER publish has uploaded the just-bumped sub-packages. `fail-fast: false` keeps a single-OS failure from cancelling the other four matrix variants — release engineers see the full diagnostic per OS instead of one error and four cancellations. Locked by `packages/shared/src/__tests__/publish-workflow-contract.test.ts`. **No-bash-on-Windows invariant** (change: eliminate-bash-on-windows-runners): no step in `publish.yml` or `ci.yml` combines `shell: bash` with a runtime configuration that can run on a `windows-latest` runner. Cross-OS build orchestration lives in `.mjs` scripts invoked by `node`; POSIX-only steps use `shell: bash` gated by `if: matrix.platform != 'win32'`; Windows-only steps use `shell: pwsh`. The bundle scripts (`bundle-server.mjs`, `bundle-offline-packages.mjs`, `bundle-recommended-extensions.mjs`) are Node-native, eliminating the bash↔Node bridge that produced the `MODULE_NOT_FOUND` regression on Windows runners. Locked by `packages/shared/src/__tests__/no-bash-on-windows.test.ts`. |
324
+ | `packages/shared/src/__tests__/publish-workflow-contract.test.ts` | Repo-level lint: parses `.github/workflows/publish.yml`, asserts the electron job's `needs:` array contains both `prepare` and `publish` AND `strategy.fail-fast` is the literal `false`. Failure messages cite change `publish-fix-macos` so the contributor knows where to look. Mirrors `no-direct-process-kill.test.ts` and `no-raw-node-import.test.ts`. See change: publish-fix-macos. |
325
+ | `packages/shared/src/__tests__/no-bash-on-windows.test.ts` | Repo-level lint: parses every workflow YAML, computes per-step Windows reachability from `electron` matrix × each step's `if:` filter (small grammar: `matrix.platform == 'X'`, `matrix.platform != 'X'`, `&&`, `||`, `!(...)` , parens), and fails when any `shell: bash` step is reachable on a Windows runner. Failure messages cite change `eliminate-bash-on-windows-runners` plus the offending `file:line` + step name. Unrecognised `if:` expressions fail closed (force the contributor to write a recognisable form or extend the evaluator). See change: eliminate-bash-on-windows-runners. |
326
+
327
+ ## Build & Restart Workflow
328
+
329
+ The dashboard has three components that need rebuilding depending on what changed:
330
+
331
+ ### After bridge extension changes (`src/extension/`)
332
+ Reload all connected pi sessions to pick up the new bridge code:
333
+ ```bash
334
+ npm run reload # Reload all pi sessions
335
+ npm run reload:check # Type-check first, then reload
336
+ ```
337
+
338
+ ### After server changes (`src/server/`, `src/shared/`)
339
+ Restart the dashboard server. The server runs TypeScript directly via jiti (pi's TypeScript loader), so no separate build step is needed — just restart:
340
+ ```bash
341
+ # Graceful restart via API (preserves current dev/prod mode)
342
+ curl -X POST http://localhost:8000/api/restart
343
+
344
+ # Or via CLI
345
+ pi-dashboard restart # production mode
346
+ pi-dashboard restart --dev # dev mode
347
+
348
+ # Manual stop + start
349
+ pi-dashboard stop && pi-dashboard start
350
+ pi-dashboard stop && pi-dashboard start --dev
351
+ ```
352
+
353
+ ### After client changes (`src/client/`)
354
+ - **Dev mode**: Vite hot-reloads automatically, no action needed. Start with `npm run dev`.
355
+ - **Production mode**: Rebuild the client and restart the server:
356
+ ```bash
357
+ npm run build
358
+ curl -X POST http://localhost:8000/api/restart
359
+ ```
360
+
361
+ ### After OpenSpec apply finishes (full rebuild)
362
+ When an openspec-apply-change skill completes implementation, do a full rebuild and restart:
363
+ ```bash
364
+ npm run build
365
+ curl -X POST http://localhost:8000/api/restart
366
+ npm run reload
367
+ ```
368
+
369
+ ### Check current mode
370
+ ```bash
371
+ curl -s http://localhost:8000/api/health | jq .mode
372
+ # Returns "dev" or "production"
373
+ ```
374
+
375
+ ### Dev mode with production fallback
376
+ In `--dev` mode, the server proxies to Vite for HMR. If Vite is not running, it **automatically falls back** to serving the production build from `dist/client/`. This means `pi-dashboard start --dev` always works — no 502 errors.
377
+
378
+ ### Fault-tolerant restart
379
+ - `POST /api/restart` waits for the old server to exit, starts a new one, and verifies health
380
+ - `POST /api/restart` with body `{"dev": true}` or `{"dev": false}` switches modes
381
+ - `pi-dashboard stop` kills stale processes holding the ports (via `lsof`), not just the PID file
382
+ - **Single restart path** (change: fix-restart-bridge-auto-start-race): `/api/restart` is the single source of truth. `pi-dashboard restart` (CLI) probes `isDashboardRunning(port)` and **delegates to `/api/restart`** when the dashboard is up; only when no dashboard is running does it fall back to local `cmdStop` + `cmdStart`. The `restart-helper.ts` orchestrator runs detached, kills the previous PID explicitly (SIGTERM → SIGKILL), then spawns the replacement. Before exit, the server broadcasts `server_restarting { reason, quiesceMs }` to every connected pi bridge so bridges suppress their auto-start spawn step for the quiesce window (5 s for restart, 60 s for shutdown) and don't race the orchestrator. Discovery + reconnection still run during the window so bridges pick up the new server as soon as it advertises.
383
+
384
+ ## OpenSpec Conventions
385
+
386
+ When creating OpenSpec change artifacts, always place them at `openspec/changes/<name>/` — never nest under subdirectories like `active/` or `archive/`. Prefer using `openspec change new <name>` CLI to scaffold the directory structure correctly.
387
+
388
+ ## Diagram Style
389
+
390
+ When creating diagrams, use Mermaid syntax (```mermaid blocks) instead of ASCII box drawings. This applies to explore mode, design documents, and all other artifacts.
391
+
392
+ ## Code Instructions
393
+
394
+ 1. First think through the problem, read the codebase for relevant files.
395
+ 2. Before you make any major changes, check in with me and I will verify the plan.
396
+ 3. Please every step of the way just give me a high level explanation of what changes you made.
397
+ 4. Make every task and code change you do as simple as possible. We want to avoid making any massive or complex changes. Every change should impact as little code as possible. Everything is about simplicity.
398
+ 5. Maintain a documentation file that describes how the architecture of the app works inside and out.
399
+ 6. Never speculate about code you have not opened. If the user references a specific file, you MUST read the file before answering. Make sure to investigate and read relevant files BEFORE answering questions about the codebase. Never make any claims about code before investigating unless you are certain of the correct answer - give grounded and hallucination-free answers.
400
+ 7. For implementation use TDD (Test-Driven Development): write or update tests first to define the expected behaviour, verify they fail, then write the minimal implementation to make them pass.
401
+ 8. Use DRY (Don't Repeat Yourself): extract reusable logic into separate classes, utilities, or components. If the same pattern appears in multiple places, refactor it into a shared helper.
402
+
403
+ ## Document changes
404
+
405
+ When an implementation is ready, update AGENTS.md, README.md, and docs/architecture.md. AGENTS.md contains instructions for AI agents, key files, and commands needed to build and operate. README.md contains end-user and developer documentation with CI badges, prerequisites, configuration, and project structure. docs/architecture.md contains detailed data flows, persistence model, reconnection logic, and configuration reference.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Robert Csakany
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.