@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
@@ -174,4 +174,200 @@ describe("preferences-store", () => {
174
174
  expect(data.hiddenSessions).toBeUndefined();
175
175
  store.dispose();
176
176
  });
177
+
178
+ // ── folder-workspaces ──────────────────────────────────
179
+
180
+ describe("workspaces", () => {
181
+ it("defaults to empty workspaces[] when field absent", () => {
182
+ fs.writeFileSync(filePath, JSON.stringify({
183
+ pinnedDirectories: [A_PATH], sessionOrder: {},
184
+ }));
185
+ const store = createPreferencesStore(filePath);
186
+ expect(store.getWorkspaces()).toEqual([]);
187
+ store.dispose();
188
+ });
189
+
190
+ it("loads workspaces from disk preserving order, ids, name, collapsed, folders", () => {
191
+ fs.writeFileSync(filePath, JSON.stringify({
192
+ pinnedDirectories: [], sessionOrder: {},
193
+ workspaces: [
194
+ { id: "ws_1", name: "a", collapsed: false, folders: [A_PATH] },
195
+ { id: "ws_2", name: "b", collapsed: true, folders: [] },
196
+ ],
197
+ }));
198
+ const store = createPreferencesStore(filePath);
199
+ const got = store.getWorkspaces();
200
+ expect(got).toHaveLength(2);
201
+ expect(got[0]).toMatchObject({ id: "ws_1", name: "a", collapsed: false, folders: [A_PATH] });
202
+ expect(got[1]).toMatchObject({ id: "ws_2", name: "b", collapsed: true, folders: [] });
203
+ store.dispose();
204
+ });
205
+
206
+ it("createWorkspace generates ws_<uuid> id and trims name; rejects empty", () => {
207
+ const store = createPreferencesStore(filePath);
208
+ expect(store.createWorkspace("")).toBeNull();
209
+ expect(store.createWorkspace(" ")).toBeNull();
210
+ const w = store.createWorkspace(" client-work ");
211
+ expect(w).not.toBeNull();
212
+ expect(w!.id).toMatch(/^ws_[0-9a-f-]{36}$/);
213
+ expect(w!.name).toBe("client-work");
214
+ expect(w!.collapsed).toBe(false);
215
+ expect(w!.folders).toEqual([]);
216
+ store.dispose();
217
+ });
218
+
219
+ it("createWorkspace rejects names longer than 80 chars", () => {
220
+ const store = createPreferencesStore(filePath);
221
+ expect(store.createWorkspace("x".repeat(81))).toBeNull();
222
+ expect(store.createWorkspace("x".repeat(80))).not.toBeNull();
223
+ store.dispose();
224
+ });
225
+
226
+ it("allows duplicate workspace names", () => {
227
+ const store = createPreferencesStore(filePath);
228
+ const a = store.createWorkspace("scratch");
229
+ const b = store.createWorkspace("scratch");
230
+ expect(a).not.toBeNull();
231
+ expect(b).not.toBeNull();
232
+ expect(a!.id).not.toBe(b!.id);
233
+ expect(store.getWorkspaces()).toHaveLength(2);
234
+ store.dispose();
235
+ });
236
+
237
+ it("renameWorkspace returns false on unknown id and on empty name", () => {
238
+ const store = createPreferencesStore(filePath);
239
+ const w = store.createWorkspace("a")!;
240
+ expect(store.renameWorkspace("missing", "x")).toBe(false);
241
+ expect(store.renameWorkspace(w.id, "")).toBe(false);
242
+ expect(store.renameWorkspace(w.id, "a")).toBe(false); // same value, no-op
243
+ expect(store.renameWorkspace(w.id, "b")).toBe(true);
244
+ expect(store.getWorkspaces()[0].name).toBe("b");
245
+ store.dispose();
246
+ });
247
+
248
+ it("deleteWorkspace removes record and leaves pinnedDirectories alone", () => {
249
+ const store = createPreferencesStore(filePath);
250
+ store.pinDirectory(A_PATH);
251
+ const w = store.createWorkspace("w")!;
252
+ store.addFolderToWorkspace(w.id, A_PATH);
253
+ expect(store.deleteWorkspace("missing")).toBe(false);
254
+ expect(store.deleteWorkspace(w.id)).toBe(true);
255
+ expect(store.getWorkspaces()).toEqual([]);
256
+ expect(store.getPinnedDirectories()).toEqual([A_PATH]);
257
+ store.dispose();
258
+ });
259
+
260
+ it("setWorkspaceCollapsed toggles flag; no-op on same value or unknown id", () => {
261
+ const store = createPreferencesStore(filePath);
262
+ const w = store.createWorkspace("w")!;
263
+ expect(store.setWorkspaceCollapsed("missing", true)).toBe(false);
264
+ expect(store.setWorkspaceCollapsed(w.id, false)).toBe(false); // already false
265
+ expect(store.setWorkspaceCollapsed(w.id, true)).toBe(true);
266
+ expect(store.getWorkspaces()[0].collapsed).toBe(true);
267
+ store.dispose();
268
+ });
269
+
270
+ it("addFolderToWorkspace appends and is idempotent on duplicate", () => {
271
+ const store = createPreferencesStore(filePath);
272
+ const w = store.createWorkspace("w")!;
273
+ expect(store.addFolderToWorkspace(w.id, A_PATH)).toBe(true);
274
+ expect(store.addFolderToWorkspace(w.id, A_PATH)).toBe(false); // idempotent
275
+ expect(store.getWorkspaces()[0].folders).toEqual([A_PATH]);
276
+ store.dispose();
277
+ });
278
+
279
+ it("single-membership: adding folder to workspace B detaches it from workspace A", () => {
280
+ const store = createPreferencesStore(filePath);
281
+ const a = store.createWorkspace("a")!;
282
+ const b = store.createWorkspace("b")!;
283
+ store.addFolderToWorkspace(a.id, A_PATH);
284
+ store.addFolderToWorkspace(b.id, A_PATH);
285
+ const ws = store.getWorkspaces();
286
+ expect(ws.find((w) => w.id === a.id)!.folders).toEqual([]);
287
+ expect(ws.find((w) => w.id === b.id)!.folders).toEqual([A_PATH]);
288
+ store.dispose();
289
+ });
290
+
291
+ it("adding folder does NOT touch pinnedDirectories", () => {
292
+ const store = createPreferencesStore(filePath);
293
+ store.pinDirectory(A_PATH);
294
+ const w = store.createWorkspace("w")!;
295
+ store.addFolderToWorkspace(w.id, A_PATH);
296
+ expect(store.getPinnedDirectories()).toEqual([A_PATH]);
297
+ expect(store.getWorkspaces()[0].folders).toEqual([A_PATH]);
298
+ store.dispose();
299
+ });
300
+
301
+ it("removeFolderFromWorkspace does NOT touch pinnedDirectories", () => {
302
+ const store = createPreferencesStore(filePath);
303
+ store.pinDirectory(A_PATH);
304
+ const w = store.createWorkspace("w")!;
305
+ store.addFolderToWorkspace(w.id, A_PATH);
306
+ expect(store.removeFolderFromWorkspace(w.id, A_PATH)).toBe(true);
307
+ expect(store.removeFolderFromWorkspace(w.id, A_PATH)).toBe(false); // not member
308
+ expect(store.getPinnedDirectories()).toEqual([A_PATH]);
309
+ expect(store.getWorkspaces()[0].folders).toEqual([]);
310
+ store.dispose();
311
+ });
312
+
313
+ it("reorderWorkspaceFolders rejects mismatched set", () => {
314
+ const store = createPreferencesStore(filePath);
315
+ const w = store.createWorkspace("w")!;
316
+ store.addFolderToWorkspace(w.id, A_PATH);
317
+ store.addFolderToWorkspace(w.id, B_PATH);
318
+ expect(store.reorderWorkspaceFolders(w.id, [A_PATH])).toBe(false); // missing B
319
+ expect(store.reorderWorkspaceFolders(w.id, [A_PATH, B_PATH, X_PATH])).toBe(false); // extra
320
+ expect(store.reorderWorkspaceFolders(w.id, [B_PATH, A_PATH])).toBe(true);
321
+ expect(store.getWorkspaces()[0].folders).toEqual([B_PATH, A_PATH]);
322
+ store.dispose();
323
+ });
324
+
325
+ it("reorderWorkspaces rejects mismatched id set", () => {
326
+ const store = createPreferencesStore(filePath);
327
+ const a = store.createWorkspace("a")!;
328
+ const b = store.createWorkspace("b")!;
329
+ expect(store.reorderWorkspaces([a.id])).toBe(false);
330
+ expect(store.reorderWorkspaces([a.id, b.id, "ghost"])).toBe(false);
331
+ expect(store.reorderWorkspaces([b.id, a.id])).toBe(true);
332
+ expect(store.getWorkspaces().map((w) => w.id)).toEqual([b.id, a.id]);
333
+ store.dispose();
334
+ });
335
+
336
+ it("workspaces persist round-trip through file with debounced write", () => {
337
+ const store = createPreferencesStore(filePath);
338
+ const w = store.createWorkspace("persisted")!;
339
+ store.addFolderToWorkspace(w.id, A_PATH);
340
+ store.setWorkspaceCollapsed(w.id, true);
341
+ store.flush();
342
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
343
+ expect(data.workspaces).toHaveLength(1);
344
+ expect(data.workspaces[0]).toMatchObject({
345
+ id: w.id, name: "persisted", collapsed: true, folders: [A_PATH],
346
+ });
347
+ store.dispose();
348
+ });
349
+
350
+ it("mutation triggers debounced save", () => {
351
+ const store = createPreferencesStore(filePath);
352
+ store.createWorkspace("x");
353
+ expect(fs.existsSync(filePath)).toBe(false);
354
+ vi.advanceTimersByTime(1000);
355
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
356
+ expect(data.workspaces).toHaveLength(1);
357
+ store.dispose();
358
+ });
359
+
360
+ it("getWorkspaces returns defensive clones — callers cannot mutate internal state", () => {
361
+ const store = createPreferencesStore(filePath);
362
+ const w = store.createWorkspace("w")!;
363
+ store.addFolderToWorkspace(w.id, A_PATH);
364
+ const snap = store.getWorkspaces();
365
+ snap[0].folders.push("/poisoned");
366
+ snap[0].name = "poisoned";
367
+ const fresh = store.getWorkspaces();
368
+ expect(fresh[0].folders).toEqual([A_PATH]);
369
+ expect(fresh[0].name).toBe("w");
370
+ store.dispose();
371
+ });
372
+ });
177
373
  });
@@ -28,6 +28,15 @@ function makePrefs(): PreferencesStore {
28
28
  pinDirectory: () => {},
29
29
  unpinDirectory: () => {},
30
30
  reorderPinnedDirs: () => {},
31
+ getWorkspaces: () => [],
32
+ createWorkspace: () => null,
33
+ renameWorkspace: () => false,
34
+ deleteWorkspace: () => false,
35
+ setWorkspaceCollapsed: () => false,
36
+ addFolderToWorkspace: () => false,
37
+ removeFolderFromWorkspace: () => false,
38
+ reorderWorkspaceFolders: () => false,
39
+ reorderWorkspaces: () => false,
31
40
  flush: () => {},
32
41
  dispose: () => {},
33
42
  };
@@ -49,9 +49,9 @@ describe("parseSourceKey", () => {
49
49
  });
50
50
 
51
51
  it("parses scoped npm: sources", () => {
52
- expect(parseSourceKey("npm:@tintinweb/pi-subagents")).toEqual({
52
+ expect(parseSourceKey("npm:@scope/example-pkg")).toEqual({
53
53
  kind: "npm",
54
- name: "@tintinweb/pi-subagents",
54
+ name: "@scope/example-pkg",
55
55
  });
56
56
  });
57
57
 
@@ -0,0 +1,203 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import http from "node:http";
3
+ import {
4
+ parseModuleNotFoundError,
5
+ isModuleNotFoundError,
6
+ detectInstallLayout,
7
+ suggestedReinstallCommand,
8
+ buildRecoveryHtml,
9
+ startRecoveryServer,
10
+ } from "../recovery-server.js";
11
+
12
+ describe("parseModuleNotFoundError", () => {
13
+ it("extracts a bare-module name from ERR_MODULE_NOT_FOUND", () => {
14
+ const e = Object.assign(new Error("Cannot find module 'fastify'"), {
15
+ code: "ERR_MODULE_NOT_FOUND",
16
+ });
17
+ expect(parseModuleNotFoundError(e)).toBe("fastify");
18
+ });
19
+
20
+ it("extracts an absolute path from ERR_MODULE_NOT_FOUND", () => {
21
+ const e = Object.assign(
22
+ new Error("Cannot find module '/abs/path/foo.cjs' imported from /bar"),
23
+ { code: "ERR_MODULE_NOT_FOUND" },
24
+ );
25
+ expect(parseModuleNotFoundError(e)).toBe("/abs/path/foo.cjs");
26
+ });
27
+
28
+ it("handles 'Cannot find package' phrasing", () => {
29
+ const e = Object.assign(new Error("Cannot find package 'toad-cache'"), {
30
+ code: "ERR_MODULE_NOT_FOUND",
31
+ });
32
+ expect(parseModuleNotFoundError(e)).toBe("toad-cache");
33
+ });
34
+
35
+ it("handles legacy MODULE_NOT_FOUND", () => {
36
+ const e = Object.assign(new Error("Cannot find module 'foo'"), {
37
+ code: "MODULE_NOT_FOUND",
38
+ });
39
+ expect(parseModuleNotFoundError(e)).toBe("foo");
40
+ });
41
+
42
+ it("returns null for non-module errors", () => {
43
+ expect(parseModuleNotFoundError(new Error("nope"))).toBeNull();
44
+ expect(parseModuleNotFoundError(null)).toBeNull();
45
+ expect(parseModuleNotFoundError(undefined)).toBeNull();
46
+ });
47
+ });
48
+
49
+ describe("isModuleNotFoundError", () => {
50
+ it("recognizes ERR_MODULE_NOT_FOUND", () => {
51
+ const e = Object.assign(new Error("Cannot find module 'x'"), { code: "ERR_MODULE_NOT_FOUND" });
52
+ expect(isModuleNotFoundError(e)).toBe(true);
53
+ });
54
+
55
+ it("recognizes phrase-only matches (no code)", () => {
56
+ expect(isModuleNotFoundError(new Error("Cannot find module 'x'"))).toBe(true);
57
+ expect(isModuleNotFoundError(new Error("Cannot find package 'x'"))).toBe(true);
58
+ });
59
+
60
+ it("rejects unrelated errors", () => {
61
+ expect(isModuleNotFoundError(new Error("EADDRINUSE"))).toBe(false);
62
+ expect(isModuleNotFoundError(null)).toBe(false);
63
+ });
64
+ });
65
+
66
+ describe("detectInstallLayout", () => {
67
+ it("detects npm-global layout", () => {
68
+ expect(
69
+ detectInstallLayout("/usr/local/lib/node_modules/@blackbelt-technology/pi-agent-dashboard/packages/server/src/cli.ts"),
70
+ ).toBe("npm-global");
71
+ });
72
+
73
+ it("detects monorepo layout", () => {
74
+ expect(detectInstallLayout("/Users/x/repo/packages/server/src/cli.ts")).toBe("monorepo");
75
+ });
76
+
77
+ it("returns unknown for unrecognized paths", () => {
78
+ expect(detectInstallLayout("/tmp/foo.js")).toBe("unknown");
79
+ });
80
+ });
81
+
82
+ describe("suggestedReinstallCommand", () => {
83
+ it("returns npm -g for npm-global", () => {
84
+ expect(suggestedReinstallCommand("npm-global")).toMatch(/npm install -g/);
85
+ });
86
+ it("returns repo-root install for monorepo", () => {
87
+ expect(suggestedReinstallCommand("monorepo")).toMatch(/repo root/);
88
+ });
89
+ });
90
+
91
+ describe("buildRecoveryHtml", () => {
92
+ it("includes the missing-module identifier and error stack", () => {
93
+ const html = buildRecoveryHtml({
94
+ port: 8000,
95
+ error: Object.assign(new Error("Cannot find module 'fastify'"), { stack: "STACK_TRACE_HERE" }),
96
+ missingModule: "fastify",
97
+ suggestedFix: "npm install -g foo",
98
+ });
99
+ expect(html).toContain("fastify");
100
+ expect(html).toContain("STACK_TRACE_HERE");
101
+ expect(html).toContain("npm install -g foo");
102
+ expect(html).toContain("Recovery Mode");
103
+ });
104
+
105
+ it("escapes HTML in error messages to prevent XSS", () => {
106
+ const html = buildRecoveryHtml({
107
+ port: 8000,
108
+ error: new Error("<script>alert('x')</script>"),
109
+ missingModule: "<img onerror=1>",
110
+ });
111
+ expect(html).not.toContain("<script>alert");
112
+ expect(html).toContain("&lt;script&gt;");
113
+ expect(html).toContain("&lt;img");
114
+ });
115
+
116
+ it("handles missing optional fields gracefully", () => {
117
+ const html = buildRecoveryHtml({
118
+ port: 8000,
119
+ error: new Error("oops"),
120
+ });
121
+ expect(html).toContain("(unknown)");
122
+ });
123
+ });
124
+
125
+ // Pick an ephemeral port (0 → OS assigns) and verify the live HTTP server.
126
+ async function withRecoveryServer<T>(
127
+ fn: (port: number) => Promise<T>,
128
+ ): Promise<T> {
129
+ // Probe an open port via a throwaway server.
130
+ const probe = http.createServer();
131
+ await new Promise<void>((r) => probe.listen(0, () => r()));
132
+ const port = (probe.address() as { port: number }).port;
133
+ await new Promise<void>((r) => probe.close(() => r()));
134
+
135
+ // Capture & swallow noisy console.error during the test
136
+ const origErr = console.error;
137
+ console.error = () => {};
138
+
139
+ // Start in the background — startRecoveryServer resolves once `listen`
140
+ // succeeds (server keeps running on its own).
141
+ await startRecoveryServer({
142
+ port,
143
+ error: new Error("Cannot find module 'fastify'"),
144
+ missingModule: "fastify",
145
+ });
146
+
147
+ try {
148
+ return await fn(port);
149
+ } finally {
150
+ console.error = origErr;
151
+ // No clean shutdown API — the test will leak the server until vitest
152
+ // tears the worker down. Acceptable for unit tests.
153
+ }
154
+ }
155
+
156
+ async function fetchText(url: string): Promise<{ status: number; body: string; contentType: string }> {
157
+ return new Promise((resolve, reject) => {
158
+ http
159
+ .get(url, (res) => {
160
+ const chunks: Buffer[] = [];
161
+ res.on("data", (c) => chunks.push(c));
162
+ res.on("end", () =>
163
+ resolve({
164
+ status: res.statusCode ?? 0,
165
+ body: Buffer.concat(chunks).toString("utf8"),
166
+ contentType: res.headers["content-type"] ?? "",
167
+ }),
168
+ );
169
+ })
170
+ .on("error", reject);
171
+ });
172
+ }
173
+
174
+ describe("startRecoveryServer (integration)", () => {
175
+ it("serves the recovery HTML at /", async () => {
176
+ await withRecoveryServer(async (port) => {
177
+ const res = await fetchText(`http://127.0.0.1:${port}/`);
178
+ expect(res.status).toBe(200);
179
+ expect(res.contentType).toMatch(/text\/html/);
180
+ expect(res.body).toContain("Recovery Mode");
181
+ expect(res.body).toContain("fastify");
182
+ });
183
+ });
184
+
185
+ it("returns recovery-mode JSON at /api/health", async () => {
186
+ await withRecoveryServer(async (port) => {
187
+ const res = await fetchText(`http://127.0.0.1:${port}/api/health`);
188
+ expect(res.status).toBe(200);
189
+ const parsed = JSON.parse(res.body);
190
+ expect(parsed.ok).toBe(false);
191
+ expect(parsed.mode).toBe("recovery");
192
+ expect(parsed.missingModule).toBe("fastify");
193
+ });
194
+ });
195
+
196
+ it("falls through to recovery HTML for unknown routes", async () => {
197
+ await withRecoveryServer(async (port) => {
198
+ const res = await fetchText(`http://127.0.0.1:${port}/some/unknown/path`);
199
+ expect(res.status).toBe(200);
200
+ expect(res.body).toContain("Recovery Mode");
201
+ });
202
+ });
203
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Tests for pi-native queue control handlers. See change: add-followup-edit-and-steer-cancel.
3
+ */
4
+ import { describe, it, expect, vi } from "vitest";
5
+ import {
6
+ handleClearSteeringQueue,
7
+ handleClearFollowupSlot,
8
+ handleEditFollowupSlot,
9
+ handlePromoteFollowupEntry,
10
+ handleRemoveFollowupEntry,
11
+ handleEditFollowupEntry,
12
+ } from "../browser-handlers/session-action-handler.js";
13
+
14
+ function makeCtx(sessionExists: boolean) {
15
+ const sendToSession = vi.fn();
16
+ return {
17
+ sendToSession,
18
+ ctx: {
19
+ sessionManager: {
20
+ get: vi.fn().mockReturnValue(sessionExists ? { id: "s1", cwd: "/p" } : undefined),
21
+ },
22
+ piGateway: { sendToSession },
23
+ } as never,
24
+ };
25
+ }
26
+
27
+ describe("handleClearSteeringQueue", () => {
28
+ it("forwards clear_steering_queue to the bridge when session exists", () => {
29
+ const { sendToSession, ctx } = makeCtx(true);
30
+ handleClearSteeringQueue({ type: "clear_steering_queue", sessionId: "s1" }, ctx);
31
+ expect(sendToSession).toHaveBeenCalledWith("s1", { type: "clear_steering_queue", sessionId: "s1" });
32
+ });
33
+
34
+ it("drops silently when session is unknown", () => {
35
+ const { sendToSession, ctx } = makeCtx(false);
36
+ handleClearSteeringQueue({ type: "clear_steering_queue", sessionId: "missing" }, ctx);
37
+ expect(sendToSession).not.toHaveBeenCalled();
38
+ });
39
+ });
40
+
41
+ describe("handleClearFollowupSlot", () => {
42
+ it("forwards clear_followup_slot to the bridge when session exists", () => {
43
+ const { sendToSession, ctx } = makeCtx(true);
44
+ handleClearFollowupSlot({ type: "clear_followup_slot", sessionId: "s1" }, ctx);
45
+ expect(sendToSession).toHaveBeenCalledWith("s1", { type: "clear_followup_slot", sessionId: "s1" });
46
+ });
47
+
48
+ it("drops silently when session is unknown", () => {
49
+ const { sendToSession, ctx } = makeCtx(false);
50
+ handleClearFollowupSlot({ type: "clear_followup_slot", sessionId: "missing" }, ctx);
51
+ expect(sendToSession).not.toHaveBeenCalled();
52
+ });
53
+ });
54
+
55
+ describe("handleEditFollowupSlot", () => {
56
+ it("forwards edit_followup_slot with text + images to the bridge", () => {
57
+ const { sendToSession, ctx } = makeCtx(true);
58
+ handleEditFollowupSlot({ type: "edit_followup_slot", sessionId: "s1", text: "revised", images: undefined }, ctx);
59
+ expect(sendToSession).toHaveBeenCalledWith("s1", {
60
+ type: "edit_followup_slot",
61
+ sessionId: "s1",
62
+ text: "revised",
63
+ images: undefined,
64
+ });
65
+ });
66
+
67
+ it("preserves images array when provided", () => {
68
+ const { sendToSession, ctx } = makeCtx(true);
69
+ const images = [{ type: "image" as const, data: "AAA", mimeType: "image/png" }];
70
+ handleEditFollowupSlot({ type: "edit_followup_slot", sessionId: "s1", text: "with image", images }, ctx);
71
+ expect(sendToSession).toHaveBeenCalledWith("s1", {
72
+ type: "edit_followup_slot",
73
+ sessionId: "s1",
74
+ text: "with image",
75
+ images,
76
+ });
77
+ });
78
+
79
+ it("drops silently when session is unknown", () => {
80
+ const { sendToSession, ctx } = makeCtx(false);
81
+ handleEditFollowupSlot({ type: "edit_followup_slot", sessionId: "missing", text: "hi" }, ctx);
82
+ expect(sendToSession).not.toHaveBeenCalled();
83
+ });
84
+ });
85
+
86
+ describe("handlePromoteFollowupEntry (v2)", () => {
87
+ it("forwards promote_followup_entry with index", () => {
88
+ const { sendToSession, ctx } = makeCtx(true);
89
+ handlePromoteFollowupEntry({ type: "promote_followup_entry", sessionId: "s1", index: 2 }, ctx);
90
+ expect(sendToSession).toHaveBeenCalledWith("s1", {
91
+ type: "promote_followup_entry",
92
+ sessionId: "s1",
93
+ index: 2,
94
+ });
95
+ });
96
+
97
+ it("drops silently when session is unknown", () => {
98
+ const { sendToSession, ctx } = makeCtx(false);
99
+ handlePromoteFollowupEntry({ type: "promote_followup_entry", sessionId: "missing", index: 0 }, ctx);
100
+ expect(sendToSession).not.toHaveBeenCalled();
101
+ });
102
+ });
103
+
104
+ describe("handleRemoveFollowupEntry (v2)", () => {
105
+ it("forwards remove_followup_entry with index", () => {
106
+ const { sendToSession, ctx } = makeCtx(true);
107
+ handleRemoveFollowupEntry({ type: "remove_followup_entry", sessionId: "s1", index: 1 }, ctx);
108
+ expect(sendToSession).toHaveBeenCalledWith("s1", {
109
+ type: "remove_followup_entry",
110
+ sessionId: "s1",
111
+ index: 1,
112
+ });
113
+ });
114
+
115
+ it("drops silently when session is unknown", () => {
116
+ const { sendToSession, ctx } = makeCtx(false);
117
+ handleRemoveFollowupEntry({ type: "remove_followup_entry", sessionId: "missing", index: 0 }, ctx);
118
+ expect(sendToSession).not.toHaveBeenCalled();
119
+ });
120
+ });
121
+
122
+ describe("handleEditFollowupEntry (v2)", () => {
123
+ it("forwards edit_followup_entry with index + text", () => {
124
+ const { sendToSession, ctx } = makeCtx(true);
125
+ handleEditFollowupEntry({ type: "edit_followup_entry", sessionId: "s1", index: 1, text: "updated" }, ctx);
126
+ expect(sendToSession).toHaveBeenCalledWith("s1", {
127
+ type: "edit_followup_entry",
128
+ sessionId: "s1",
129
+ index: 1,
130
+ text: "updated",
131
+ images: undefined,
132
+ });
133
+ });
134
+
135
+ it("preserves images when provided", () => {
136
+ const { sendToSession, ctx } = makeCtx(true);
137
+ const images = [{ type: "image" as const, data: "AAA", mimeType: "image/png" }];
138
+ handleEditFollowupEntry({ type: "edit_followup_entry", sessionId: "s1", index: 0, text: "img", images }, ctx);
139
+ expect(sendToSession).toHaveBeenCalledWith("s1", {
140
+ type: "edit_followup_entry",
141
+ sessionId: "s1",
142
+ index: 0,
143
+ text: "img",
144
+ images,
145
+ });
146
+ });
147
+
148
+ it("drops silently when session is unknown", () => {
149
+ const { sendToSession, ctx } = makeCtx(false);
150
+ handleEditFollowupEntry({ type: "edit_followup_entry", sessionId: "missing", index: 0, text: "x" }, ctx);
151
+ expect(sendToSession).not.toHaveBeenCalled();
152
+ });
153
+ });
@@ -464,4 +464,47 @@ describe("handleSendPrompt — interception wiring", () => {
464
464
  expect(spawnPiSession).not.toHaveBeenCalled();
465
465
  expect(ctx.piGateway.sendToSession).toHaveBeenCalled();
466
466
  });
467
+
468
+ it("forwards delivery field to bridge unchanged", async () => {
469
+ const { ctx } = makeCtx({
470
+ pidBySession: { S1: undefined },
471
+ sessions: {
472
+ S1: { id: "S1", cwd: "/p", sessionFile: "/p/s.jsonl", status: "active" },
473
+ },
474
+ });
475
+
476
+ await handleSendPrompt(
477
+ {
478
+ type: "send_prompt",
479
+ sessionId: "S1",
480
+ text: "steer this",
481
+ delivery: "steer",
482
+ } as any,
483
+ ctx,
484
+ );
485
+
486
+ expect(ctx.piGateway.sendToSession).toHaveBeenCalledWith(
487
+ "S1",
488
+ expect.objectContaining({ delivery: "steer" }),
489
+ );
490
+ });
491
+
492
+ it("forwards undefined delivery as undefined (JSON.stringify strips on wire)", async () => {
493
+ const { ctx } = makeCtx({
494
+ pidBySession: { S1: undefined },
495
+ sessions: {
496
+ S1: { id: "S1", cwd: "/p", sessionFile: "/p/s.jsonl", status: "active" },
497
+ },
498
+ });
499
+
500
+ await handleSendPrompt(
501
+ { type: "send_prompt", sessionId: "S1", text: "no delivery" } as any,
502
+ ctx,
503
+ );
504
+
505
+ expect(ctx.piGateway.sendToSession).toHaveBeenCalledWith(
506
+ "S1",
507
+ expect.objectContaining({ delivery: undefined }),
508
+ );
509
+ });
467
510
  });
@@ -12,6 +12,15 @@ function createMockPreferencesStore(initialOrder: Record<string, string[]> = {})
12
12
  pinDirectory: vi.fn(),
13
13
  unpinDirectory: vi.fn(),
14
14
  reorderPinnedDirs: vi.fn(),
15
+ getWorkspaces: vi.fn(() => []),
16
+ createWorkspace: vi.fn(() => null),
17
+ renameWorkspace: vi.fn(() => false),
18
+ deleteWorkspace: vi.fn(() => false),
19
+ setWorkspaceCollapsed: vi.fn(() => false),
20
+ addFolderToWorkspace: vi.fn(() => false),
21
+ removeFolderFromWorkspace: vi.fn(() => false),
22
+ reorderWorkspaceFolders: vi.fn(() => false),
23
+ reorderWorkspaces: vi.fn(() => false),
15
24
  flush: vi.fn(),
16
25
  dispose: vi.fn(),
17
26
  };
@@ -34,6 +34,15 @@ function makePrefs(): PreferencesStore {
34
34
  pinDirectory: () => {},
35
35
  unpinDirectory: () => {},
36
36
  reorderPinnedDirs: () => {},
37
+ getWorkspaces: () => [],
38
+ createWorkspace: () => null,
39
+ renameWorkspace: () => false,
40
+ deleteWorkspace: () => false,
41
+ setWorkspaceCollapsed: () => false,
42
+ addFolderToWorkspace: () => false,
43
+ removeFolderFromWorkspace: () => false,
44
+ reorderWorkspaceFolders: () => false,
45
+ reorderWorkspaces: () => false,
37
46
  flush: () => {},
38
47
  dispose: () => {},
39
48
  };
@@ -16,6 +16,15 @@ function createMockPreferencesStore(): PreferencesStore {
16
16
  pinDirectory: vi.fn(),
17
17
  unpinDirectory: vi.fn(),
18
18
  reorderPinnedDirs: vi.fn(),
19
+ getWorkspaces: vi.fn(() => []),
20
+ createWorkspace: vi.fn(() => null),
21
+ renameWorkspace: vi.fn(() => false),
22
+ deleteWorkspace: vi.fn(() => false),
23
+ setWorkspaceCollapsed: vi.fn(() => false),
24
+ addFolderToWorkspace: vi.fn(() => false),
25
+ removeFolderFromWorkspace: vi.fn(() => false),
26
+ reorderWorkspaceFolders: vi.fn(() => false),
27
+ reorderWorkspaces: vi.fn(() => false),
19
28
  flush: vi.fn(),
20
29
  dispose: vi.fn(),
21
30
  };