@blackbelt-technology/pi-agent-dashboard 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/AGENTS.md +19 -30
  2. package/README.md +69 -1
  3. package/docs/architecture.md +89 -165
  4. package/package.json +11 -7
  5. package/packages/extension/package.json +2 -2
  6. package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
  7. package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
  8. package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
  9. package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
  10. package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
  11. package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
  12. package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
  13. package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
  14. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
  15. package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
  16. package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
  17. package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
  19. package/packages/extension/src/bridge-default-model-gate.ts +32 -0
  20. package/packages/extension/src/bridge.ts +299 -20
  21. package/packages/extension/src/command-handler.ts +53 -7
  22. package/packages/extension/src/dashboard-default-adapter.ts +5 -0
  23. package/packages/extension/src/prompt-bus.ts +15 -0
  24. package/packages/extension/src/slash-dispatch.ts +30 -15
  25. package/packages/extension/src/source-detector.ts +13 -5
  26. package/packages/extension/src/usage-limit-orderer.ts +18 -1
  27. package/packages/server/bin/pi-dashboard.mjs +62 -14
  28. package/packages/server/package.json +9 -5
  29. package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
  30. package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
  31. package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
  32. package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
  33. package/packages/server/src/__tests__/cli-version.test.ts +151 -0
  34. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
  35. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
  36. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
  37. package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
  38. package/packages/server/src/__tests__/directory-service.test.ts +9 -0
  39. package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
  40. package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
  41. package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
  42. package/packages/server/src/__tests__/health-shape.test.ts +35 -12
  43. package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
  44. package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
  45. package/packages/server/src/__tests__/package-routes.test.ts +6 -2
  46. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
  47. package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
  48. package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
  49. package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
  50. package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
  51. package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
  52. package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
  53. package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
  54. package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
  55. package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
  56. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
  57. package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
  58. package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
  59. package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
  60. package/packages/server/src/browser-gateway.ts +83 -5
  61. package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
  62. package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
  63. package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
  64. package/packages/server/src/changelog-parser.ts +1 -1
  65. package/packages/server/src/cli.ts +68 -250
  66. package/packages/server/src/event-status-extraction.ts +14 -62
  67. package/packages/server/src/event-wiring.ts +23 -10
  68. package/packages/server/src/memory-session-manager.ts +4 -0
  69. package/packages/server/src/pi-core-checker.ts +1 -1
  70. package/packages/server/src/pi-dev-version-check.ts +1 -1
  71. package/packages/server/src/pi-version-skew.ts +24 -46
  72. package/packages/server/src/plugin-intent-cache.ts +67 -0
  73. package/packages/server/src/preferences-store.ts +199 -13
  74. package/packages/server/src/recovery-server.ts +366 -0
  75. package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
  76. package/packages/server/src/routes/doctor-routes.ts +26 -21
  77. package/packages/server/src/routes/manifest-route.ts +162 -0
  78. package/packages/server/src/routes/openspec-routes.ts +4 -25
  79. package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
  80. package/packages/server/src/routes/pi-core-routes.ts +3 -23
  81. package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
  82. package/packages/server/src/routes/recommended-routes.ts +21 -0
  83. package/packages/server/src/routes/system-routes.ts +73 -11
  84. package/packages/server/src/server.ts +105 -307
  85. package/packages/server/src/session-api.ts +5 -63
  86. package/packages/shared/package.json +1 -1
  87. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
  88. package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
  89. package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
  90. package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
  91. package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
  92. package/packages/shared/src/__tests__/config.test.ts +40 -0
  93. package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
  94. package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
  95. package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
  96. package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
  97. package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
  98. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
  99. package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
  100. package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
  101. package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
  102. package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
  103. package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
  104. package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
  105. package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
  106. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
  107. package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
  108. package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
  109. package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
  110. package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
  111. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
  112. package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
  113. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
  114. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
  115. package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
  116. package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
  117. package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
  118. package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
  119. package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
  120. package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
  121. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
  122. package/packages/shared/src/bridge-register.ts +35 -2
  123. package/packages/shared/src/browser-protocol.ts +176 -2
  124. package/packages/shared/src/config.ts +12 -0
  125. package/packages/shared/src/dashboard-paths.ts +69 -0
  126. package/packages/shared/src/dashboard-plugin/index.ts +2 -0
  127. package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
  128. package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
  129. package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
  130. package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
  131. package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
  132. package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
  133. package/packages/shared/src/dashboard-starter.ts +22 -0
  134. package/packages/shared/src/doctor-core.ts +49 -27
  135. package/packages/shared/src/launch-source-types.ts +9 -9
  136. package/packages/shared/src/legacy-managed-dir.ts +97 -0
  137. package/packages/shared/src/mdns-discovery.ts +4 -1
  138. package/packages/shared/src/pi-package-resolver.ts +388 -0
  139. package/packages/shared/src/platform/binary-lookup.ts +27 -3
  140. package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
  141. package/packages/shared/src/platform/exec.ts +22 -0
  142. package/packages/shared/src/platform/node-spawn.ts +42 -41
  143. package/packages/shared/src/plugin-bridge-register.ts +275 -18
  144. package/packages/shared/src/protocol.ts +94 -2
  145. package/packages/shared/src/recommended-extensions.ts +34 -10
  146. package/packages/shared/src/server-identity.ts +74 -5
  147. package/packages/shared/src/server-launcher.ts +20 -0
  148. package/packages/shared/src/source-matching.ts +1 -1
  149. package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
  150. package/packages/shared/src/tool-registry/definitions.ts +91 -7
  151. package/packages/shared/src/types.ts +12 -8
  152. package/scripts/maybe-patch-package.cjs +44 -0
  153. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
  154. package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
  155. package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
  156. package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
  157. package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
  158. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
  159. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
  160. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
  161. package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
  162. package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
  163. package/packages/server/src/bootstrap-install-from-list.ts +0 -232
  164. package/packages/server/src/bootstrap-queue.ts +0 -130
  165. package/packages/server/src/bootstrap-state.ts +0 -159
  166. package/packages/server/src/legacy-pi-cleanup.ts +0 -151
  167. package/packages/server/src/routes/bootstrap-routes.ts +0 -125
  168. package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
  169. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
  170. package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
  171. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
  172. package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
  173. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
  174. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
  175. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
  176. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
  177. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
  178. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
  179. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
  180. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
  181. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
  182. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
  183. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
  184. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
  185. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
  186. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
  187. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
  188. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
  189. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
  190. package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
  191. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
  192. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
  193. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
  194. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
  195. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
  196. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
  197. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
  198. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
  199. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
  200. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
  201. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
  202. package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
  203. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
  204. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
  205. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
  206. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
  207. package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
  208. package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
  209. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
  210. package/packages/shared/src/bootstrap-install.ts +0 -406
  211. package/packages/shared/src/installable-list.ts +0 -152
  212. package/packages/shared/src/launch-source-flag.ts +0 -14
@@ -99,59 +99,59 @@ export function toFileUrl(pathOrUrl: string): string {
99
99
  return pathToFileURL(absolute).href;
100
100
  }
101
101
 
102
+ /**
103
+ * Detect whether a loader (file:// URL or raw path) is jiti.
104
+ *
105
+ * Mirrors `isTsxLoader`. jiti's hook ships under `jiti/lib/`. Used so
106
+ * `shouldUrlWrapEntry` can refuse to URL-wrap the entry when jiti is
107
+ * the loader on Windows — see the JITI VERSION CONTRACT below.
108
+ */
109
+ export function isJitiLoader(loader: string | null | undefined): boolean {
110
+ if (!loader) return false;
111
+ const normalized = loader.replace(/\\/g, "/");
112
+ return /\/jiti\//i.test(normalized);
113
+ }
114
+
102
115
  /**
103
116
  * Decide whether the entry-script position needs `file://` URL wrapping.
104
117
  *
105
118
  * Rule:
106
119
  * - tsx loader: always raw path (tsx rejects file:// entries on every OS)
107
- * - non-tsx (jiti / Node default) on POSIX: raw path
108
- * (POSIX has no drive-letter / URL-scheme collision; jiti's resolver
109
- * actively MISBEHAVES when handed `file://` URL entries it
110
- * normalises away the triple-slash and then treats `file:/...` as
111
- * a relative specifier, producing `<cwd>/file:/...` ENOENT errors.)
112
- * - non-tsx on Windows: file:// URL
120
+ * - jiti loader: always raw path (jiti misnormalises file:// URL
121
+ * entries on Windows see the JITI VERSION CONTRACT below)
122
+ * - any loader on POSIX: raw path (no drive-letter / URL-scheme collision)
123
+ * - other / default Node resolver on Windows: file:// URL
113
124
  * (Node parses drive letters like `B:` / `A:` as URL schemes in argv
114
125
  * before loaders run, throwing ERR_UNSUPPORTED_ESM_URL_SCHEME.
115
- * Wrapping with `file://` sidesteps the parse.)
126
+ * Wrapping with `file://` sidesteps the parse. The drive-letter
127
+ * heuristic in Node catches `C:\…` / `D:\…` etc., which is what
128
+ * jiti relies on for the standalone install layout.)
116
129
  *
117
130
  * Keeps a `platform` parameter for testability so unit tests on a POSIX
118
131
  * host can exercise the Windows branch without mutating `process.platform`.
119
132
  *
120
133
  * !! JITI VERSION CONTRACT !!
121
- * The Windows-non-tsx arm relies on jiti's `file:///` triple-slash URL
122
- * handling. Verified-good baselines (must be one of these in the
123
- * offline cacache):
124
- * `@earendil-works/pi-coding-agent@0.74.x` (jiti `^2.7.0`) — current
125
- * `@mariozechner/pi-coding-agent@0.70.x` (jiti 2.x) — legacy
126
- *
127
- * Both ship a jiti that correctly normalises `file:///` entries on
128
- * Windows. The contract was originally carved around 0.70.x in change
129
- * `fix-windows-entry-script-url` and re-anchored at 0.74.x in change
130
- * `migrate-pi-fork-to-earendil` (E.7).
131
- *
132
- * Known-broken (do NOT pin): `pi-coding-agent@0.71.x` shipping
133
- * `jiti@2.6.5`. That jiti version misnormalises triple-slash to
134
- * single-slash and prepends cwd as if the entry were a relative
135
- * specifier, producing `<cwd>/file:/...` ENOENT errors. Keep the
136
- * 0.71.x / 2.6.5 mention here so contributors recognise the
137
- * regression pattern if it recurs in a future jiti.
138
- *
139
- * The Electron Windows codepath defends against version drift by
140
- * resolving jiti from the managed dir's pinned `pi-coding-agent`
141
- * (currently `@earendil-works/pi-coding-agent@0.74.0`, pinned in
142
- * `packages/electron/offline-packages.json` and extracted into
143
- * `~/.pi-dashboard/` by `installStandalone()` on first launch — see
144
- * Defect 1 of change `fix-electron-windows-installer-and-server-bootstrap`).
145
- * Since the managed-dir tree is pinned, the contract holds regardless
146
- * of what jiti is on the user's PATH.
147
- *
148
- * If a future change bumps the offline-cacache `pi-coding-agent` pin to
149
- * a version OUTSIDE the verified baselines, RE-VERIFY this contract on
150
- * Windows manually (run a packaged Electron app on Win10 + Win11) and
151
- * either:
152
- * 1. Update the contract (fix the file:// URL handling expectation), OR
153
- * 2. Add a per-jiti-version branch here, OR
154
- * 3. Switch the bundled loader to tsx (which has its own contract).
134
+ * jiti at every version verified on Windows so far, including the
135
+ * current pin `jiti@^2.7.0` shipped under
136
+ * `@earendil-works/pi-coding-agent@0.74.x` — MISHANDLES `file:///`
137
+ * triple-slash URL entries on Windows. Symptom: the entry is rewritten
138
+ * to single-slash `file:/C:/…` and then re-resolved relative to cwd,
139
+ * yielding `Cannot find module 'file:///<cwd>/file:/C:/…/cli.ts'`.
140
+ *
141
+ * This was verified live in a Windows 11 standalone install
142
+ * (Node 22.18.0 + jiti 2.7.0) and is the reason the Windows branch of
143
+ * this function now returns `false` for jiti loaders. Node's own
144
+ * drive-letter heuristic accepts raw `C:\…` argv entries, so the URL
145
+ * wrap is unnecessary for the common standalone-install layout where
146
+ * pi + the dashboard live under `C:\Users\<u>\.pi-dashboard\…`.
147
+ *
148
+ * Earlier baselines (`@mariozechner/pi-coding-agent@0.70.x`,
149
+ * `jiti@2.6.5` in `pi-coding-agent@0.71.x`) exhibited the same or
150
+ * worse breakage; we no longer attempt to special-case any single
151
+ * jiti version. If a future jiti release fixes file:/// handling and
152
+ * we want to URL-wrap again (e.g. to cover `B:` / `A:` drives outside
153
+ * Node's heuristic), narrow the rule here and add a per-version
154
+ * branch — re-verify on real Windows before changing.
155
155
  *
156
156
  * Locked by `node-spawn-jiti-contract.test.ts`.
157
157
  */
@@ -160,6 +160,7 @@ export function shouldUrlWrapEntry(
160
160
  platform: NodeJS.Platform = process.platform,
161
161
  ): boolean {
162
162
  if (isTsxLoader(loader)) return false;
163
+ if (isJitiLoader(loader)) return false;
163
164
  return platform === "win32";
164
165
  }
165
166
 
@@ -2,26 +2,47 @@
2
2
  * Plugin bridge entry management in pi's settings.json.
3
3
  *
4
4
  * Manages `dashboard-<plugin-id>` keys in a dedicated
5
- * `dashboardPluginBridges` object inside settings.json.
5
+ * `dashboardPluginBridges` object inside settings.json AND mirrors each
6
+ * managed bridge path into the top-level `packages[]` array so
7
+ * pi-coding-agent (which only reads `packages[]`) actually loads the
8
+ * bridge as an extension.
9
+ *
10
+ * See change: fix-pi-flows-end-to-end (Group 1).
11
+ *
12
+ * Ownership tracking:
13
+ * - `dashboardPluginBridges["dashboard-<id>"] = "<absPath>"` — managed key
14
+ * - `_dashboardManagedPackages["<absPath>"] = "<id>"` — ownership map
15
+ * - `packages[]` gains a plain string entry `"<absPath>"` — readable by pi
6
16
  *
7
17
  * Rules:
8
- * - Only touches entries under the `dashboardPluginBridges` key.
9
- * - NEVER modifies user-owned `packages[]` entries.
18
+ * - Only touches entries under managed keys / paths.
19
+ * - NEVER modifies user-owned `packages[]` entries (those without an entry
20
+ * in `_dashboardManagedPackages`).
10
21
  * - Uses atomic write (tmp + rename) for all updates.
11
22
  * - Detects path conflicts (existing entry with mismatched path).
23
+ *
24
+ * Escape hatch: setting `PI_DASHBOARD_DISABLE_PLUGIN_BRIDGE_PACKAGES_WRITE=1`
25
+ * skips the `packages[]` write (rollback parity with pre-change behavior).
12
26
  */
13
27
  import fs from "node:fs";
14
28
  import path from "node:path";
15
29
  import os from "node:os";
30
+ import type { BridgeLoadSource } from "./dashboard-plugin/plugin-status.js";
16
31
 
17
32
  export interface PluginBridgeRegisterOptions {
18
33
  homedir?: string;
34
+ /** Override env-driven escape hatch for tests. */
35
+ skipPackagesWrite?: boolean;
19
36
  }
20
37
 
21
38
  export type PluginBridgeConflict =
22
39
  | { type: "ok" }
23
40
  | { type: "conflict"; existingPath: string; newPath: string };
24
41
 
42
+ const MANAGED_PREFIX = "dashboard-";
43
+ const OWNERSHIP_KEY = "_dashboardManagedPackages";
44
+ const ENV_SKIP = "PI_DASHBOARD_DISABLE_PLUGIN_BRIDGE_PACKAGES_WRITE";
45
+
25
46
  function getSettingsPath(homedir?: string): string {
26
47
  const home = homedir ?? process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
27
48
  return path.join(home, ".pi", "agent", "settings.json");
@@ -55,11 +76,95 @@ function getManagedBridges(
55
76
  return {};
56
77
  }
57
78
 
58
- const MANAGED_PREFIX = "dashboard-";
79
+ function getOwnershipMap(
80
+ settings: Record<string, unknown>,
81
+ ): Record<string, string> {
82
+ const val = settings[OWNERSHIP_KEY];
83
+ if (val && typeof val === "object" && !Array.isArray(val)) {
84
+ return val as Record<string, string>;
85
+ }
86
+ return {};
87
+ }
88
+
89
+ function getPackages(settings: Record<string, unknown>): unknown[] {
90
+ const val = settings.packages;
91
+ return Array.isArray(val) ? val : [];
92
+ }
93
+
94
+ function packageEntryPath(entry: unknown): string | null {
95
+ if (typeof entry === "string") return entry;
96
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
97
+ const src = (entry as Record<string, unknown>).source;
98
+ if (typeof src === "string") return src;
99
+ }
100
+ return null;
101
+ }
102
+
103
+ function shouldSkipPackagesWrite(opts: PluginBridgeRegisterOptions): boolean {
104
+ if (opts.skipPackagesWrite === true) return true;
105
+ if (opts.skipPackagesWrite === false) return false;
106
+ return process.env[ENV_SKIP] === "1";
107
+ }
108
+
109
+ // ─────────────────────────────────────────────────────────────────────────
110
+ // Pure helpers (Task 1.1)
111
+ // ─────────────────────────────────────────────────────────────────────────
112
+
113
+ /**
114
+ * Ensure `packages[]` contains `bridgePath` and record ownership.
115
+ * No-op when entry already present. Mutates `packages` and `ownership` in place.
116
+ * Returns `true` if a new entry was added.
117
+ */
118
+ export function ensurePackageEntry(
119
+ packages: unknown[],
120
+ ownership: Record<string, string>,
121
+ bridgePath: string,
122
+ ownerMarker: string,
123
+ ): boolean {
124
+ const already = packages.some((e) => packageEntryPath(e) === bridgePath);
125
+ ownership[bridgePath] = ownerMarker; // record ownership regardless
126
+ if (already) return false;
127
+ packages.push(bridgePath);
128
+ return true;
129
+ }
130
+
131
+ /**
132
+ * Remove a managed `packages[]` entry whose ownership matches `ownerMarker`.
133
+ * Leaves user-added entries (no matching ownership record) untouched.
134
+ * Mutates `packages` and `ownership` in place. Returns `true` if removed.
135
+ */
136
+ export function removePackageEntry(
137
+ packages: unknown[],
138
+ ownership: Record<string, string>,
139
+ ownerMarker: string,
140
+ ): boolean {
141
+ const owned = Object.entries(ownership)
142
+ .filter(([, owner]) => owner === ownerMarker)
143
+ .map(([p]) => p);
144
+ if (owned.length === 0) return false;
145
+ let removed = false;
146
+ for (const p of owned) {
147
+ const idx = packages.findIndex((e) => packageEntryPath(e) === p);
148
+ if (idx >= 0) {
149
+ packages.splice(idx, 1);
150
+ removed = true;
151
+ }
152
+ delete ownership[p];
153
+ }
154
+ return removed;
155
+ }
156
+
157
+ // ─────────────────────────────────────────────────────────────────────────
158
+ // Public API (Tasks 1.2, 1.3, 1.4)
159
+ // ─────────────────────────────────────────────────────────────────────────
59
160
 
60
161
  /**
61
162
  * Register a plugin's bridge entry in pi's settings.json.
62
163
  *
164
+ * Writes to BOTH:
165
+ * - `dashboardPluginBridges["dashboard-<pluginId>"]` (forward-compat)
166
+ * - `packages[]` + ownership record (so pi-coding-agent loads the bridge)
167
+ *
63
168
  * Returns { type: "conflict", existingPath, newPath } if a
64
169
  * `dashboard-<pluginId>` key already exists but points to a different path.
65
170
  * In that case the settings.json is NOT modified.
@@ -74,25 +179,64 @@ export function registerPluginBridge(
74
179
  const settingsPath = getSettingsPath(opts.homedir);
75
180
  const settings = readSettings(settingsPath);
76
181
  const managed = getManagedBridges(settings);
182
+ const ownership = getOwnershipMap(settings);
183
+ const packages = getPackages(settings);
77
184
  const key = MANAGED_PREFIX + pluginId;
185
+ const ownerMarker = key;
78
186
 
79
187
  const existing = managed[key];
80
- if (existing) {
81
- if (existing === bridgePath) return { type: "ok" }; // already registered
82
- return { type: "conflict", existingPath: existing, newPath: bridgePath };
188
+ if (existing && existing !== bridgePath) {
189
+ // Self-heal: if the existing path no longer resolves on disk (typical
190
+ // after a dev-monorepo deployed-bundle path change), silently replace.
191
+ // This avoids cosmetic "Bridge path conflict" errors on every restart
192
+ // when the user switches between dev and production launch sources.
193
+ // See change: add-plugin-activation-ui (deployment-fix follow-up).
194
+ let existingStillOnDisk = true;
195
+ try {
196
+ existingStillOnDisk = fs.existsSync(existing);
197
+ } catch {
198
+ existingStillOnDisk = false;
199
+ }
200
+ if (existingStillOnDisk) {
201
+ return { type: "conflict", existingPath: existing, newPath: bridgePath };
202
+ }
203
+ // Strip the stale entry and matching ownership/packages mirrors so the
204
+ // subsequent register block below installs the new path cleanly.
205
+ delete managed[key];
206
+ if (ownership[existing] === ownerMarker) delete ownership[existing];
207
+ const idx = packages.indexOf(existing);
208
+ if (idx >= 0) packages.splice(idx, 1);
209
+ }
210
+
211
+ let mutated = false;
212
+ if (!managed[key]) {
213
+ managed[key] = bridgePath;
214
+ settings.dashboardPluginBridges = managed;
215
+ mutated = true;
83
216
  }
84
217
 
85
- managed[key] = bridgePath;
86
- settings.dashboardPluginBridges = managed;
87
- writeSettings(settingsPath, settings);
88
- console.info(`[plugin-bridge] Registered bridge for plugin "${pluginId}": ${bridgePath}`);
218
+ if (!shouldSkipPackagesWrite(opts)) {
219
+ const ownershipBefore = ownership[bridgePath];
220
+ const added = ensurePackageEntry(packages, ownership, bridgePath, ownerMarker);
221
+ if (added || ownershipBefore !== ownerMarker) {
222
+ mutated = true;
223
+ settings.packages = packages;
224
+ settings[OWNERSHIP_KEY] = ownership;
225
+ }
226
+ }
227
+
228
+ if (mutated) {
229
+ writeSettings(settingsPath, settings);
230
+ console.info(`[plugin-bridge] Registered bridge for plugin "${pluginId}": ${bridgePath}`);
231
+ }
89
232
  return { type: "ok" };
90
233
  }
91
234
 
92
235
  /**
93
236
  * Remove a plugin's bridge entry from pi's settings.json.
94
- * No-op if the entry does not exist.
95
- * NEVER touches entries without the `dashboard-` prefix.
237
+ * Removes BOTH the `dashboardPluginBridges` key AND the matching ownership-marked
238
+ * `packages[]` entry. No-op if neither exists.
239
+ * NEVER touches entries without matching ownership.
96
240
  */
97
241
  export function deregisterPluginBridge(
98
242
  pluginId: string,
@@ -101,14 +245,29 @@ export function deregisterPluginBridge(
101
245
  const settingsPath = getSettingsPath(opts.homedir);
102
246
  const settings = readSettings(settingsPath);
103
247
  const managed = getManagedBridges(settings);
248
+ const ownership = getOwnershipMap(settings);
249
+ const packages = getPackages(settings);
104
250
  const key = MANAGED_PREFIX + pluginId;
251
+ const ownerMarker = key;
252
+
253
+ let mutated = false;
105
254
 
106
- if (!(key in managed)) return; // nothing to remove
255
+ if (key in managed) {
256
+ delete managed[key];
257
+ settings.dashboardPluginBridges = managed;
258
+ mutated = true;
259
+ }
260
+
261
+ if (removePackageEntry(packages, ownership, ownerMarker)) {
262
+ settings.packages = packages;
263
+ settings[OWNERSHIP_KEY] = ownership;
264
+ mutated = true;
265
+ }
107
266
 
108
- delete managed[key];
109
- settings.dashboardPluginBridges = managed;
110
- writeSettings(settingsPath, settings);
111
- console.info(`[plugin-bridge] Deregistered bridge for plugin "${pluginId}"`);
267
+ if (mutated) {
268
+ writeSettings(settingsPath, settings);
269
+ console.info(`[plugin-bridge] Deregistered bridge for plugin "${pluginId}"`);
270
+ }
112
271
  }
113
272
 
114
273
  /**
@@ -127,6 +286,57 @@ export function registerAllPluginBridges(
127
286
  return results;
128
287
  }
129
288
 
289
+ /**
290
+ * One-shot reconciliation (Task 1.4): for each entry in
291
+ * `dashboardPluginBridges`, ensure a matching `packages[]` entry exists with
292
+ * the same ownership marker. Heals installs that pre-date the dual-write.
293
+ *
294
+ * Returns a list of `{ pluginId, bridgePath, action }` summaries — `"added"`
295
+ * when a new packages[] entry was inserted, `"already"` when no change needed.
296
+ */
297
+ export function reconcilePluginBridgePackages(
298
+ opts: PluginBridgeRegisterOptions = {},
299
+ ): Array<{ pluginId: string; bridgePath: string; action: "added" | "already" }> {
300
+ if (shouldSkipPackagesWrite(opts)) return [];
301
+ const settingsPath = getSettingsPath(opts.homedir);
302
+ const settings = readSettings(settingsPath);
303
+ const managed = getManagedBridges(settings);
304
+ const ownership = getOwnershipMap(settings);
305
+ const packages = getPackages(settings);
306
+
307
+ const summary: Array<{ pluginId: string; bridgePath: string; action: "added" | "already" }> = [];
308
+ let mutated = false;
309
+
310
+ for (const [key, bridgePath] of Object.entries(managed)) {
311
+ if (!key.startsWith(MANAGED_PREFIX)) continue;
312
+ const pluginId = key.slice(MANAGED_PREFIX.length);
313
+ const ownershipBefore = ownership[bridgePath];
314
+ const added = ensurePackageEntry(packages, ownership, bridgePath, key);
315
+ summary.push({ pluginId, bridgePath, action: added ? "added" : "already" });
316
+ if (added) mutated = true;
317
+ // `ensurePackageEntry` already wrote `ownership[bridgePath] = key`
318
+ // unconditionally. Compare the previous value (`ownershipBefore`) to
319
+ // detect whether persistence is needed for the ownership marker. This
320
+ // fixes a bug where the marker was set in memory but the file write
321
+ // was skipped because the *current* ownership equalled `key`.
322
+ if (ownershipBefore !== key) mutated = true;
323
+ }
324
+
325
+ if (mutated) {
326
+ settings.packages = packages;
327
+ settings[OWNERSHIP_KEY] = ownership;
328
+ writeSettings(settingsPath, settings);
329
+ for (const { pluginId, bridgePath, action } of summary) {
330
+ if (action === "added") {
331
+ console.info(
332
+ `[plugin-bridge] Reconciled packages[] entry for plugin "${pluginId}": ${bridgePath}`,
333
+ );
334
+ }
335
+ }
336
+ }
337
+ return summary;
338
+ }
339
+
130
340
  /**
131
341
  * List all currently managed plugin bridge entries.
132
342
  */
@@ -137,3 +347,50 @@ export function listManagedBridges(
137
347
  const settings = readSettings(settingsPath);
138
348
  return getManagedBridges(settings);
139
349
  }
350
+
351
+ /**
352
+ * Inspect ownership map (for diagnostics / health).
353
+ */
354
+ export function listManagedPackageOwnership(
355
+ opts: PluginBridgeRegisterOptions = {},
356
+ ): Record<string, string> {
357
+ const settingsPath = getSettingsPath(opts.homedir);
358
+ const settings = readSettings(settingsPath);
359
+ return getOwnershipMap(settings);
360
+ }
361
+
362
+ /**
363
+ * Classify where a bridge path is registered in pi's settings.
364
+ *
365
+ * Precedence (intentional — packages[] is what pi actually loads, so it wins
366
+ * even when both keys point at the same path):
367
+ * 1. matching entry in `packages[]` → `"packages[]"`
368
+ * 2. matching value in `dashboardPluginBridges` → `"dashboardPluginBridges"`
369
+ * 3. no match → `"none"`
370
+ *
371
+ * Used by `/api/health.plugins[].bridgeLoadedFrom`. See change:
372
+ * fix-pi-flows-end-to-end (Group 2, task 2.2).
373
+ */
374
+ export function classifyBridgeSource(
375
+ settings: unknown,
376
+ bridgePath: string,
377
+ ): BridgeLoadSource {
378
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
379
+ return "none";
380
+ }
381
+ const s = settings as Record<string, unknown>;
382
+
383
+ const packages = Array.isArray(s.packages) ? s.packages : [];
384
+ for (const entry of packages) {
385
+ if (packageEntryPath(entry) === bridgePath) return "packages[]";
386
+ }
387
+
388
+ const bridges = s.dashboardPluginBridges;
389
+ if (bridges && typeof bridges === "object" && !Array.isArray(bridges)) {
390
+ for (const value of Object.values(bridges as Record<string, unknown>)) {
391
+ if (value === bridgePath) return "dashboardPluginBridges";
392
+ }
393
+ }
394
+
395
+ return "none";
396
+ }
@@ -3,6 +3,19 @@
3
3
  */
4
4
  import type { DashboardEvent, CommandInfo, FlowInfo, SessionSource, ImageContent, FileEntry, TurnUsage, ContextUsage, ModelInfo, ProviderInfo, PiSessionInfo, OpenSpecPhase, RoleInfo, ExtensionUiModule, DecoratorDescriptor } from "./types.js";
5
5
 
6
+ /**
7
+ * Bridge -> server: mirror of pi's native steering + follow-up queues, forwarded
8
+ * from pi's `queue_update` event. Server caches the latest snapshot per session
9
+ * in `SessionUiState.pendingQueues` and broadcasts via `session_updated`.
10
+ * See change: add-followup-edit-and-steer-cancel.
11
+ */
12
+ export interface QueueUpdateToServerMessage {
13
+ type: "queue_update";
14
+ sessionId: string;
15
+ steering: string[];
16
+ followUp: string[];
17
+ }
18
+
6
19
  // ── Extension → Server ──────────────────────────────────────────────
7
20
 
8
21
  export interface SessionRegisterMessage {
@@ -372,7 +385,8 @@ export type ExtensionToServerMessage =
372
385
  | UiDataListMessage
373
386
  | ExtUiDecoratorMessage
374
387
  | AssetRegisterMessage
375
- | DispatchExtensionCommandMessage;
388
+ | DispatchExtensionCommandMessage
389
+ | QueueUpdateToServerMessage;
376
390
 
377
391
  // ── Server → Extension ──────────────────────────────────────────────
378
392
 
@@ -381,6 +395,8 @@ export interface SendPromptToExtensionMessage {
381
395
  sessionId: string;
382
396
  text: string;
383
397
  images?: ImageContent[];
398
+ /** Delivery mode: "steer" (after current turn) or "followUp" (after agent finishes). Defaults to "followUp" when absent. See change: add-steering-message. */
399
+ delivery?: "steer" | "followUp";
384
400
  }
385
401
 
386
402
  export interface AbortToExtensionMessage {
@@ -574,6 +590,76 @@ export interface ServerRestartingExtensionMessage {
574
590
  quiesceMs: number;
575
591
  }
576
592
 
593
+ /**
594
+ * Server -> bridge: clear pi's steering queue for the named session.
595
+ * Bridge calls `pi.clearSteeringQueue()`. Idempotent on empty queue.
596
+ * Pi emits a fresh `queue_update` reflecting the empty array.
597
+ * See change: add-followup-edit-and-steer-cancel.
598
+ */
599
+ export interface ClearSteeringQueueToExtensionMessage {
600
+ type: "clear_steering_queue";
601
+ sessionId: string;
602
+ }
603
+
604
+ /**
605
+ * Server -> bridge: clear pi's follow-up queue for the named session.
606
+ * Bridge calls `pi.clearFollowUpQueue()` and wipes its shadow `bridgeFollowUp[]`.
607
+ * Idempotent on empty queue.
608
+ * See change: add-followup-edit-and-steer-cancel.
609
+ */
610
+ export interface ClearFollowupSlotToExtensionMessage {
611
+ type: "clear_followup_slot";
612
+ sessionId: string;
613
+ }
614
+
615
+ /**
616
+ * Server -> bridge (v1, deprecated): atomic replace of pi's follow-up slot.
617
+ * v2 prefers `edit_followup_entry { index: 0 }`. Bridge accepts both.
618
+ * See change: add-followup-edit-and-steer-cancel.
619
+ */
620
+ export interface EditFollowupSlotToExtensionMessage {
621
+ type: "edit_followup_slot";
622
+ sessionId: string;
623
+ text: string;
624
+ images?: ImageContent[];
625
+ }
626
+
627
+ /**
628
+ * Server -> bridge (v2): move the follow-up entry at `index` to position 0
629
+ * (head of the queue). Bridge rewrites its shadow + replays via
630
+ * `clearFollowUpQueue` then `sendUserMessage` for each entry in the new
631
+ * order. See change: add-followup-edit-and-steer-cancel.
632
+ */
633
+ export interface PromoteFollowupEntryToExtensionMessage {
634
+ type: "promote_followup_entry";
635
+ sessionId: string;
636
+ index: number;
637
+ }
638
+
639
+ /**
640
+ * Server -> bridge (v2): remove the follow-up entry at `index`. Bridge
641
+ * rewrites its shadow + replays the surviving entries.
642
+ * See change: add-followup-edit-and-steer-cancel.
643
+ */
644
+ export interface RemoveFollowupEntryToExtensionMessage {
645
+ type: "remove_followup_entry";
646
+ sessionId: string;
647
+ index: number;
648
+ }
649
+
650
+ /**
651
+ * Server -> bridge (v2): replace the follow-up entry at `index` with new text.
652
+ * Bridge rewrites its shadow + replays.
653
+ * See change: add-followup-edit-and-steer-cancel.
654
+ */
655
+ export interface EditFollowupEntryToExtensionMessage {
656
+ type: "edit_followup_entry";
657
+ sessionId: string;
658
+ index: number;
659
+ text: string;
660
+ images?: ImageContent[];
661
+ }
662
+
577
663
  export type ServerToExtensionMessage =
578
664
  | SendPromptToExtensionMessage
579
665
  | AbortToExtensionMessage
@@ -602,4 +688,10 @@ export type ServerToExtensionMessage =
602
688
  | RequestRolesMessage
603
689
  | UiManagementMessage
604
690
  | KillProcessMessage
605
- | ServerRestartingExtensionMessage;
691
+ | ServerRestartingExtensionMessage
692
+ | ClearSteeringQueueToExtensionMessage
693
+ | ClearFollowupSlotToExtensionMessage
694
+ | EditFollowupSlotToExtensionMessage
695
+ | PromoteFollowupEntryToExtensionMessage
696
+ | RemoveFollowupEntryToExtensionMessage
697
+ | EditFollowupEntryToExtensionMessage;