shadow-claw 1.23.4 → 1.24.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 (267) hide show
  1. package/README.md +42 -5
  2. package/bin/cli.mjs +166 -0
  3. package/bin/cli.test.mjs +60 -0
  4. package/bin/commands/backup.mjs +70 -0
  5. package/bin/commands/clients.mjs +41 -0
  6. package/bin/commands/peer-id.mjs +76 -0
  7. package/bin/commands/peer-id.test.mjs +104 -0
  8. package/bin/commands/send.mjs +51 -0
  9. package/bin/commands/tasks.mjs +59 -0
  10. package/bin/commands/webrtc-listen.mjs +254 -0
  11. package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
  12. package/bin/rolldown.config.test.mjs +401 -8
  13. package/bin/utils/control-client.mjs +243 -0
  14. package/bin/utils/control-client.test.mjs +83 -0
  15. package/bin/utils/ulid.mjs +45 -0
  16. package/bin/utils/webrtc-control-client.mjs +699 -0
  17. package/bin/utils/webrtc-control-client.test.mjs +318 -0
  18. package/dist/electron/main.cjs +1 -1
  19. package/dist/public/AGENTS.md +12 -3
  20. package/dist/public/README.md +42 -5
  21. package/dist/public/agent.worker.js +50 -50
  22. package/dist/public/backup-controller-CvzDOq4T.js +1 -0
  23. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
  24. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
  25. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
  26. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  27. package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
  28. package/dist/public/config-CrHchneq.js +1 -0
  29. package/dist/public/{configurePeerJs-B4Ym9VMj.js → configurePeerJs-UfQ6HUlv.js} +1 -1
  30. package/dist/public/{connections-BEn6JYdu.js → connections-DvbgdkaB.js} +1 -1
  31. package/dist/public/{custom-element-security-DdYlUozq.js → custom-element-security-NCqQ5lmC.js} +1 -1
  32. package/dist/public/docs/README.md +3 -1
  33. package/dist/public/docs/architecture/overview.md +12 -12
  34. package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
  35. package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +2 -2
  36. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  37. package/dist/public/docs/example/article/index.html +1 -1
  38. package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
  39. package/dist/public/docs/guides/server-development-configuration.md +24 -0
  40. package/dist/public/docs/publishing/index.html +1 -1
  41. package/dist/public/docs/skill-creator/index.html +1 -1
  42. package/dist/public/docs/subsystems/backup.md +56 -0
  43. package/dist/public/docs/subsystems/cli.md +124 -0
  44. package/dist/public/docs/subsystems/control-plane.md +169 -0
  45. package/dist/public/docs/subsystems/notifications.md +54 -2
  46. package/dist/public/docs/subsystems/reactive-ui.md +1 -0
  47. package/dist/public/downloadGroupFile-9niHBj9n.js +1 -0
  48. package/dist/public/e2e/README.md +5 -1
  49. package/dist/public/{e2e-bridge-BQ4KzUt5.js → e2e-bridge-D28tOggc.js} +1 -1
  50. package/dist/public/file-viewer-BVg3B_JH.js +1 -0
  51. package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
  52. package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
  53. package/dist/public/{git-DEUvQTUA.js → git-7Vq_SJSp.js} +2 -2
  54. package/dist/public/{iframe-sanitizer-0NS4XLOd.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
  55. package/dist/public/index.html +1 -1
  56. package/dist/public/index.js +1 -1
  57. package/dist/public/{initChatSplitResize-llGkSJXg.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
  58. package/dist/public/initControlPlane-DKNht5ph.js +1 -0
  59. package/dist/public/llms.txt +1 -1
  60. package/dist/public/main/index.html +1 -1
  61. package/dist/public/main/memory/index.html +1 -1
  62. package/dist/public/{markdown-CyYR4App.js → markdown-SiFq0PTy.js} +1 -1
  63. package/dist/public/{mcp-reconnect-CSx4aMwy.js → mcp-reconnect-9emsqGRe.js} +1 -1
  64. package/dist/public/{memoryStorage-Dwg8NhAZ.js → memoryStorage-C0KvLNUp.js} +1 -1
  65. package/dist/public/orchestrator-8w9-exfN.js +15 -0
  66. package/dist/public/{orchestrator-DRfEudXk.js → orchestrator-tHWrqi_F.js} +676 -676
  67. package/dist/public/{peerjs-CurBuKvk.js → peerjs-D8hck8Ih.js} +1 -1
  68. package/dist/public/push-client-D0lkwrK0.js +1 -0
  69. package/dist/public/service-worker/fetch-proxy.js +1 -1
  70. package/dist/public/service-worker/push-handler.js +1 -1
  71. package/dist/public/service-worker.js +1 -1
  72. package/dist/public/{shadow-claw-5IHaDDMJ.js → shadow-claw-Cs8wbk3d.js} +2 -2
  73. package/dist/public/{shadow-claw-accounts-CZvz4iQz.js → shadow-claw-accounts-B4sBTQEj.js} +1 -1
  74. package/dist/public/{shadow-claw-channels-BKXFyRS3.js → shadow-claw-channels-mY-JMvib.js} +1 -1
  75. package/dist/public/{shadow-claw-chat-CCLuhWdI.js → shadow-claw-chat-DGPKpmU5.js} +6 -6
  76. package/dist/public/shadow-claw-control-plane-B1F0jnPk.js +199 -0
  77. package/dist/public/{shadow-claw-conversations-DV-87UUh.js → shadow-claw-conversations-0Q-VC_7N.js} +1 -1
  78. package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
  79. package/dist/public/{shadow-claw-file-viewer-Dk2dbAUd.js → shadow-claw-file-viewer-AZfG4D-u.js} +1 -1
  80. package/dist/public/shadow-claw-files-Bx4v0Ggp.js +840 -0
  81. package/dist/public/{shadow-claw-git-DRA7_qck.js → shadow-claw-git-DD7iQRxq.js} +2 -2
  82. package/dist/public/{shadow-claw-integrations-1Xu5snuV.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
  83. package/dist/public/{shadow-claw-llm-6Aci1BDP.js → shadow-claw-llm-CfeDBZKB.js} +2 -2
  84. package/dist/public/{shadow-claw-mcp-remote-BUjAeVXC.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
  85. package/dist/public/{shadow-claw-networking-FQe4nCp3.js → shadow-claw-networking-e0EarDVs.js} +1 -1
  86. package/dist/public/{shadow-claw-notifications-DV2wibL3.js → shadow-claw-notifications-BkxuDEQI.js} +3 -3
  87. package/dist/public/{shadow-claw-page-header-B63S4HTU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
  88. package/dist/public/{shadow-claw-pages-DE3jWj5K.js → shadow-claw-pages-BWSE91I1.js} +1 -1
  89. package/dist/public/{shadow-claw-settings-S6DAKfYx.js → shadow-claw-settings-D6lPBn6m.js} +10 -3
  90. package/dist/public/{shadow-claw-storage-DaacA3_K.js → shadow-claw-storage-Crs6LY15.js} +2 -2
  91. package/dist/public/{shadow-claw-task-server-IjKX042p.js → shadow-claw-task-server-CKm_Ennl.js} +25 -4
  92. package/dist/public/{shadow-claw-tasks-Cy0JYmGU.js → shadow-claw-tasks-Czx6Ucf5.js} +1 -1
  93. package/dist/public/{shadow-claw-terminal-znAt7HyP.js → shadow-claw-terminal-Dmtr9KY1.js} +1 -1
  94. package/dist/public/{shadow-claw-tools-Cutv9Si5.js → shadow-claw-tools-t3Qq4TDO.js} +1 -1
  95. package/dist/public/{shadow-claw-webvm-BH8mPDEu.js → shadow-claw-webvm-CIuSywD_.js} +1 -1
  96. package/dist/public/{syncWebMcpRegistration-BqJxaMGR.js → syncWebMcpRegistration-DzC80wVG.js} +1 -1
  97. package/dist/server.js +286 -189
  98. package/package.json +2 -1
  99. package/src/__mocks__/jszip.cjs +15 -5
  100. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
  101. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
  102. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
  103. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
  104. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
  105. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
  106. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
  107. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
  108. package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
  109. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
  110. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
  111. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
  112. package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.test.ts +3 -0
  113. package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.ts +43 -9
  114. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
  115. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +76 -0
  116. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
  117. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  118. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
  119. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
  120. package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
  121. package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
  122. package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
  123. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
  124. package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
  125. package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
  126. package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
  127. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
  128. package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
  129. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +273 -0
  130. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +408 -1
  131. package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
  132. package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
  133. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
  134. package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
  135. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
  136. package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
  137. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
  138. package/src/config/config.ts +10 -0
  139. package/src/core/backup-controller.test.ts +101 -0
  140. package/src/core/backup-controller.ts +166 -0
  141. package/src/core/control-plane-client.test.ts +415 -0
  142. package/src/core/control-plane-client.ts +441 -0
  143. package/src/core/orchestrator/orchestrator.ts +4 -0
  144. package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
  145. package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
  146. package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
  147. package/src/core/orchestrator/utils/operations/task.ts +46 -15
  148. package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
  149. package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
  150. package/src/core/utils/initControlPlane.test.ts +276 -0
  151. package/src/core/utils/initControlPlane.ts +437 -0
  152. package/src/core/utils/initializeApp.ts +18 -0
  153. package/src/db/getSession.test.ts +13 -1
  154. package/src/server/app.test.ts +12 -0
  155. package/src/server/app.ts +20 -8
  156. package/src/server/client-registry.test.ts +250 -0
  157. package/src/server/client-registry.ts +477 -0
  158. package/src/server/config.ts +15 -0
  159. package/src/server/control-plane-types.ts +120 -0
  160. package/src/server/control-plane.test.ts +508 -0
  161. package/src/server/control-plane.ts +607 -0
  162. package/src/server/middleware/pna.test.ts +15 -3
  163. package/src/server/middleware/pna.ts +6 -2
  164. package/src/server/routes/backup.test.ts +277 -0
  165. package/src/server/routes/backup.ts +291 -0
  166. package/src/server/routes/integrations-email.test.ts +532 -0
  167. package/src/server/routes/llamafile.test.ts +172 -48
  168. package/src/server/routes/mesh-llm.test.ts +192 -0
  169. package/src/server/routes/ollama.test.ts +49 -0
  170. package/src/server/server-peer.test.ts +86 -0
  171. package/src/server/server-peer.ts +330 -0
  172. package/src/server/server.ts +37 -0
  173. package/src/server/services/llamafile-manager.test.ts +230 -6
  174. package/src/server/services/transformers-runtime.test.ts +186 -18
  175. package/src/server/webrtc.test.ts +21 -0
  176. package/src/server/webrtc.ts +72 -0
  177. package/src/service-worker/fetch-proxy-rules.test.ts +41 -0
  178. package/src/service-worker/fetch-proxy-rules.ts +15 -8
  179. package/src/service-worker/fetch-proxy.test.ts +64 -1
  180. package/src/service-worker/fetch-proxy.ts +18 -2
  181. package/src/service-worker/push-handler.ts +39 -0
  182. package/src/shell/vm.test.ts +744 -12
  183. package/src/storage/ensureMainGroupIndex.test.ts +153 -0
  184. package/src/storage/getGroupFile.test.ts +66 -0
  185. package/src/storage/memoryStorage.test.ts +82 -2
  186. package/src/storage/writeGroupFileBytes.test.ts +143 -0
  187. package/src/stores/chat-ui.test.ts +20 -0
  188. package/src/stores/orchestrator.test.ts +134 -0
  189. package/src/stores/toast.test.ts +50 -0
  190. package/src/stores/tools.test.ts +147 -10
  191. package/src/subsystems/channels/imessage.test.ts +59 -0
  192. package/src/subsystems/channels/peerjs.test.ts +500 -62
  193. package/src/subsystems/channels/peerjs.ts +71 -4
  194. package/src/subsystems/channels/telegram.test.ts +177 -0
  195. package/src/subsystems/email/catalog.test.ts +1 -0
  196. package/src/subsystems/email/connections.test.ts +39 -1
  197. package/src/subsystems/git/credentials.test.ts +160 -825
  198. package/src/subsystems/git/git.test.ts +463 -0
  199. package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
  200. package/src/subsystems/notifications/push-client.test.ts +5 -1
  201. package/src/subsystems/notifications/push-client.ts +50 -12
  202. package/src/subsystems/notifications/push-routes.test.ts +33 -0
  203. package/src/subsystems/notifications/push-routes.ts +23 -0
  204. package/src/subsystems/notifications/push-store.ts +1 -1
  205. package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
  206. package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
  207. package/src/subsystems/providers/model-registry.test.ts +115 -0
  208. package/src/subsystems/providers/providers.test.ts +94 -1
  209. package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
  210. package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
  211. package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
  212. package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
  213. package/src/subsystems/skills/activateSkill.test.ts +101 -0
  214. package/src/subsystems/skills/discoverSkills.test.ts +251 -0
  215. package/src/subsystems/tools/cron.test.ts +64 -0
  216. package/src/subsystems/tools/declarative.test.ts +255 -89
  217. package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
  218. package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
  219. package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
  220. package/src/testing/jest-setup.ts +328 -40
  221. package/src/testing/mock-v86.mjs +49 -0
  222. package/src/ui/a2ui/types.ts +4 -3
  223. package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
  224. package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
  225. package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
  226. package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
  227. package/src/worker/tools/email/email.test.ts +193 -8
  228. package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
  229. package/src/worker/tools/git/git.test.ts +214 -3
  230. package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
  231. package/src/worker/tools/rooms/leave-room.test.ts +40 -0
  232. package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
  233. package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
  234. package/src/worker/tools/ui/ask-user.test.ts +50 -0
  235. package/src/worker/tools/ui/clear-chat.test.ts +19 -0
  236. package/src/worker/tools/ui/get-current-time.test.ts +19 -0
  237. package/src/worker/tools/ui/javascript.test.ts +116 -0
  238. package/src/worker/tools/ui/list-components.test.ts +10 -0
  239. package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
  240. package/src/worker/tools/ui/manage-tools.test.ts +34 -0
  241. package/src/worker/tools/ui/render-component.test.ts +346 -0
  242. package/src/worker/tools/ui/send-notification.test.ts +40 -0
  243. package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
  244. package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
  245. package/src/worker/tools/workspace/attach-file.test.ts +84 -0
  246. package/src/worker/tools/workspace/copy-file.test.ts +69 -0
  247. package/src/worker/tools/workspace/create-directory.test.ts +40 -0
  248. package/src/worker/tools/workspace/delete-file.test.ts +73 -0
  249. package/src/worker/tools/workspace/diff-files.test.ts +94 -0
  250. package/src/worker/tools/workspace/list-files.test.ts +36 -0
  251. package/src/worker/tools/workspace/move-file.test.ts +66 -0
  252. package/src/worker/tools/workspace/open-file.test.ts +57 -0
  253. package/src/worker/tools/workspace/patch-file.test.ts +81 -0
  254. package/src/worker/tools/workspace/read-file.test.ts +116 -0
  255. package/src/worker/tools/workspace/send-file.test.ts +82 -0
  256. package/src/worker/tools/workspace/update-memory.test.ts +29 -0
  257. package/src/worker/tools/workspace/write-file.test.ts +28 -0
  258. package/src/worker/utils/rate-limit.test.ts +199 -0
  259. package/src/worker/utils/tool-state.test.ts +29 -0
  260. package/src/worker/utils/transformers-js-utils.test.ts +59 -0
  261. package/dist/public/config-BYAuxx3j.js +0 -1
  262. package/dist/public/downloadGroupFile-Drdvg1Ev.js +0 -1
  263. package/dist/public/file-viewer-CcZYw-PS.js +0 -1
  264. package/dist/public/orchestrator-o9Fj67BN.js +0 -15
  265. package/dist/public/push-client-Dii-vPEh.js +0 -1
  266. package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
  267. package/dist/public/shadow-claw-files-BpfpJIuP.js +0 -840
@@ -198,4 +198,190 @@ describe("shadow-claw-peerjs", () => {
198
198
  "http://localhost/?peer=test",
199
199
  );
200
200
  });
201
+
202
+ it("should show room QR code", async () => {
203
+ const el = new ShadowClawPeerJs() as any;
204
+ document.body.appendChild(el);
205
+ await el.connectedCallback();
206
+
207
+ const room = { roomId: "room-1", name: "Room One" };
208
+ await el.showRoomQr(room);
209
+
210
+ const group = el.shadowRoot?.querySelector(
211
+ '[data-info="room-qr-group"]',
212
+ ) as HTMLElement;
213
+ const label = el.shadowRoot?.querySelector('[data-info="room-qr-label"]');
214
+ expect(group.hidden).toBe(false);
215
+ expect(label?.textContent).toContain("Room One");
216
+
217
+ document.body.removeChild(el);
218
+ });
219
+
220
+ it("should add and remove trusted peer rows", async () => {
221
+ const el = new ShadowClawPeerJs() as any;
222
+ document.body.appendChild(el);
223
+ await el.connectedCallback();
224
+
225
+ const initialRows =
226
+ el.shadowRoot?.querySelectorAll(".peerjs-trusted-peer-row").length || 0;
227
+ el._appendTrustedPeerRow("peer-xyz", "Peer XYZ");
228
+
229
+ const newRows = el.shadowRoot?.querySelectorAll(".peerjs-trusted-peer-row");
230
+ expect(newRows.length).toBe(initialRows + 1);
231
+
232
+ // Remove row
233
+ const removeBtn = newRows[newRows.length - 1].querySelector(
234
+ ".save-btn--danger",
235
+ ) as HTMLButtonElement;
236
+ removeBtn?.click();
237
+
238
+ expect(
239
+ el.shadowRoot?.querySelectorAll(".peerjs-trusted-peer-row").length,
240
+ ).toBe(initialRows);
241
+
242
+ document.body.removeChild(el);
243
+ });
244
+
245
+ it("should copy room URL and generate trusted peer options with aliases", async () => {
246
+ const el = new ShadowClawPeerJs() as any;
247
+ document.body.appendChild(el);
248
+ await el.connectedCallback();
249
+
250
+ const mockClipboard = {
251
+ writeText: jest.fn<any>().mockResolvedValue(undefined),
252
+ };
253
+ Object.assign(navigator, { clipboard: mockClipboard });
254
+
255
+ const options = el._trustedPeerOptions();
256
+ expect(options.length).toBeGreaterThan(0);
257
+
258
+ document.body.removeChild(el);
259
+ });
260
+
261
+ it("handles copyPeerId and copyPeerUrl", async () => {
262
+ const el = new ShadowClawPeerJs() as any;
263
+ document.body.appendChild(el);
264
+ await el.connectedCallback();
265
+
266
+ const mockClipboard = {
267
+ writeText: jest.fn<any>().mockResolvedValue(undefined),
268
+ };
269
+ Object.assign(navigator, { clipboard: mockClipboard });
270
+
271
+ // 1. Copy Peer ID
272
+ const peerIdInput = el.shadowRoot?.querySelector(
273
+ '[data-setting="peerjs-my-peer-id-input"]',
274
+ ) as HTMLInputElement;
275
+ if (peerIdInput) peerIdInput.value = "my-test-peer";
276
+ await el.copyPeerId();
277
+ expect(mockClipboard.writeText).toHaveBeenCalledWith("my-test-peer");
278
+
279
+ // 2. Copy Peer URL
280
+ el._currentPeerUrl = "http://localhost/?peer=my-test-peer";
281
+ await el.copyPeerUrl();
282
+ expect(mockClipboard.writeText).toHaveBeenCalledWith(
283
+ "http://localhost/?peer=my-test-peer",
284
+ );
285
+
286
+ document.body.removeChild(el);
287
+ });
288
+
289
+ it("handles savePeerJsConfig", async () => {
290
+ const el = new ShadowClawPeerJs() as any;
291
+ document.body.appendChild(el);
292
+ await el.connectedCallback();
293
+
294
+ const hostInput = el.shadowRoot?.querySelector(
295
+ '[data-setting="peerjs-server-host-input"]',
296
+ ) as HTMLInputElement;
297
+ const portInput = el.shadowRoot?.querySelector(
298
+ '[data-setting="peerjs-server-port-input"]',
299
+ ) as HTMLInputElement;
300
+
301
+ if (hostInput) hostInput.value = "peer.example.com";
302
+ if (portInput) portInput.value = "9001";
303
+
304
+ await el.savePeerJsConfig();
305
+ expect(mockConfigurePeerJs).toHaveBeenCalledWith(
306
+ expect.anything(),
307
+ expect.anything(),
308
+ "test-peer-id",
309
+ expect.any(Array),
310
+ "peer.example.com",
311
+ 9001,
312
+ "/myapp",
313
+ false,
314
+ );
315
+
316
+ document.body.removeChild(el);
317
+ });
318
+
319
+ it("handles copyRoomUrl, showRoomQr, updateConnectionStatus, and updateQrCode", async () => {
320
+ const el = new ShadowClawPeerJs() as any;
321
+ document.body.appendChild(el);
322
+ await el.connectedCallback();
323
+
324
+ const mockClipboard = {
325
+ writeText: jest.fn<any>().mockResolvedValue(undefined),
326
+ };
327
+ Object.assign(navigator, { clipboard: mockClipboard });
328
+
329
+ // 1. copyRoomUrl
330
+ el._currentRoomUrl = "http://localhost/?room=test-room";
331
+ await el.copyRoomUrl();
332
+ expect(mockClipboard.writeText).toHaveBeenCalledWith(
333
+ "http://localhost/?room=test-room",
334
+ );
335
+
336
+ // 2. showRoomQr
337
+ const room = {
338
+ roomId: "room-1",
339
+ name: "Test Room",
340
+ members: [],
341
+ createdAt: 0,
342
+ updatedAt: 0,
343
+ };
344
+ await el.showRoomQr(room);
345
+ expect(el._currentRoomUrl).toContain("room-1");
346
+
347
+ // 4. renderRooms
348
+ const { listRooms } =
349
+ await import("../../../core/orchestrator/utils/operations/room.js");
350
+ const testRooms = [
351
+ {
352
+ roomId: "room-host-1",
353
+ name: "My Host Room",
354
+ hostPeerId: "test-peer-id",
355
+ members: [
356
+ { peerId: "test-peer-id", alias: "Host Me", kind: "human" },
357
+ { peerId: "peer-remote-1", alias: "Agent Remote", kind: "agent" },
358
+ ],
359
+ createdAt: Date.now(),
360
+ updatedAt: Date.now(),
361
+ },
362
+ {
363
+ roomId: "room-guest-2",
364
+ name: "Guest Room",
365
+ hostPeerId: "peer-other",
366
+ members: [{ peerId: "peer-other", alias: "Other", kind: "human" }],
367
+ createdAt: Date.now(),
368
+ updatedAt: Date.now(),
369
+ },
370
+ ];
371
+ (listRooms as jest.Mock<any>).mockReturnValue(testRooms);
372
+
373
+ let roomsContainer = el.shadowRoot?.querySelector(
374
+ '[data-info="rooms-list"]',
375
+ );
376
+ if (!roomsContainer) {
377
+ roomsContainer = document.createElement("div");
378
+ roomsContainer.setAttribute("data-info", "rooms-list");
379
+ el.shadowRoot?.appendChild(roomsContainer);
380
+ }
381
+
382
+ el.renderRooms();
383
+ expect(roomsContainer.querySelectorAll(".peerjs-room-card").length).toBe(2);
384
+
385
+ document.body.removeChild(el);
386
+ });
201
387
  });
@@ -51,6 +51,28 @@ jest.unstable_mockModule("../../../storage/getStorageEstimate.js", () => ({
51
51
  .mockResolvedValue({ usage: 1024, quota: 10240 }),
52
52
  }));
53
53
 
54
+ jest.unstable_mockModule("../../../storage/storage.js", () => ({
55
+ resetStorageDirectory: jest.fn<any>().mockResolvedValue(undefined),
56
+ getStorageRoot: jest.fn<any>().mockResolvedValue({}),
57
+ setStorageRoot: jest.fn(),
58
+ invalidateStorageRoot: jest.fn(),
59
+ isStaleHandleError: jest.fn(() => false),
60
+ setStorageDirectory: jest.fn<any>().mockResolvedValue(undefined),
61
+ getStorageStatus: jest
62
+ .fn<any>()
63
+ .mockResolvedValue({ type: "opfs", permission: "granted", name: "OPFS" }),
64
+ getOpfsRootDirName: jest.fn(() => "shadow-claw-opfs"),
65
+ isDirectoryHandle: jest.fn(() => false),
66
+ }));
67
+
68
+ jest.unstable_mockModule("../../../stores/orchestrator.js", () => ({
69
+ orchestratorStore: {
70
+ storageStatus: { type: "opfs", permission: "granted" },
71
+ resetSiteConfigSeed: jest.fn<any>().mockResolvedValue(undefined),
72
+ loadFiles: jest.fn<any>().mockResolvedValue(undefined),
73
+ },
74
+ }));
75
+
54
76
  const { CONFIG_KEYS } = await import("../../../config/config.js");
55
77
  const { getConfig } = await import("../../../db/getConfig.js");
56
78
  const { setConfig } = await import("../../../db/setConfig.js");
@@ -115,4 +137,58 @@ describe("shadow-claw-storage", () => {
115
137
 
116
138
  document.body.removeChild(el);
117
139
  });
140
+
141
+ it("formats bytes accurately", () => {
142
+ const el = new ShadowClawStorage();
143
+ expect(el.formatBytes(0)).toBe("0 B");
144
+ expect(el.formatBytes(1024)).toBe("1 KB");
145
+ expect(el.formatBytes(1048576)).toBe("1 MB");
146
+ expect(el.formatBytes(1073741824)).toBe("1 GB");
147
+ });
148
+
149
+ it("handles requestPersistentStorage", async () => {
150
+ const el = new ShadowClawStorage() as any;
151
+ el.db = {};
152
+ el.updateStorageInfo = jest.fn();
153
+
154
+ const { requestPersistentStorage } =
155
+ await import("../../../storage/requestPersistentStorage.js");
156
+ (requestPersistentStorage as jest.Mock<any>).mockResolvedValueOnce(true);
157
+
158
+ await el.handleRequestPersistent();
159
+ expect(showSuccess).toHaveBeenCalledWith(
160
+ "Persistent storage granted",
161
+ 3500,
162
+ );
163
+
164
+ const { showWarning } = await import("../../../ui/toast.js");
165
+ (requestPersistentStorage as jest.Mock<any>).mockResolvedValueOnce(false);
166
+ await el.handleRequestPersistent();
167
+ expect(showWarning).toHaveBeenCalled();
168
+ });
169
+
170
+ it("handles handleResetSiteConfig and handleResetStorageDir", async () => {
171
+ const el = new ShadowClawStorage() as any;
172
+ el.db = {};
173
+ el.updateStorageInfo = jest.fn();
174
+
175
+ // With confirmation false
176
+ el.requestConfirmation = (jest.fn() as any).mockResolvedValue(false);
177
+ await el.handleResetSiteConfig();
178
+ await el.handleResetStorageDir();
179
+
180
+ // With confirmation true
181
+ el.requestConfirmation = (jest.fn() as any).mockResolvedValue(true);
182
+ await el.handleResetSiteConfig();
183
+ expect(showSuccess).toHaveBeenCalledWith(
184
+ "Site config reset. Reload to apply its defaults again.",
185
+ 5000,
186
+ );
187
+
188
+ await el.handleResetStorageDir();
189
+ expect(showSuccess).toHaveBeenCalledWith(
190
+ "Reverted to browser-internal storage",
191
+ 3500,
192
+ );
193
+ });
118
194
  });
@@ -33,7 +33,8 @@
33
33
  margin-bottom: 0.375rem;
34
34
  }
35
35
 
36
- .form-input {
36
+ .form-input,
37
+ .form-select {
37
38
  background-color: var(--shadow-claw-bg-primary);
38
39
  border: 0.0625rem solid var(--shadow-claw-border-color);
39
40
  border-radius: var(--shadow-claw-radius-s, 0.625rem);
@@ -48,12 +49,32 @@
48
49
  width: 100%;
49
50
  }
50
51
 
51
- .form-input:focus {
52
+ .form-input:focus,
53
+ .form-select:focus {
52
54
  border-color: var(--shadow-claw-accent-primary);
53
55
  box-shadow: 0 0 0 0.125rem rgba(0, 0, 0, 0.06);
54
56
  outline: none;
55
57
  }
56
58
 
59
+ .form-toggle {
60
+ align-items: center;
61
+ display: flex;
62
+ gap: 0.75rem;
63
+ }
64
+
65
+ .form-toggle input[type="checkbox"] {
66
+ accent-color: var(--shadow-claw-accent-primary);
67
+ cursor: pointer;
68
+ height: 1.125rem;
69
+ width: 1.125rem;
70
+ }
71
+
72
+ .form-toggle .form-label {
73
+ cursor: pointer;
74
+ display: inline;
75
+ margin-bottom: 0;
76
+ }
77
+
57
78
  .form-helper {
58
79
  color: var(--shadow-claw-text-tertiary);
59
80
  font-size: 0.75rem;
@@ -44,7 +44,7 @@
44
44
  site, and task scheduling runs on a separate backend.
45
45
  </div>
46
46
  <button class="save-btn" data-action="save-task-server-url">
47
- 💾 Save Task Server URL
47
+ 💾 Save URL
48
48
  </button>
49
49
  </div>
50
50
  </div>
@@ -25,6 +25,7 @@ jest.unstable_mockModule("../../../stores/orchestrator.js", () => ({
25
25
  orchestratorStore: {
26
26
  orchestrator: {
27
27
  taskServerUrl: "/test-schedule",
28
+ taskServerEnabled: true,
28
29
  },
29
30
  ready: true,
30
31
  },
@@ -56,6 +57,10 @@ describe("shadow-claw-task-server", () => {
56
57
  '[data-setting="task-server-url-input"]',
57
58
  ) as HTMLInputElement;
58
59
  expect(input.value).toBe("/test-schedule");
60
+ const enabledToggle = el.shadowRoot?.querySelector(
61
+ '[data-setting="task-server-enabled-toggle"]',
62
+ ) as HTMLInputElement;
63
+ expect(enabledToggle.checked).toBe(true);
59
64
  });
60
65
 
61
66
  it("should save task server url", async () => {
@@ -77,4 +82,18 @@ describe("shadow-claw-task-server", () => {
77
82
  );
78
83
  expect(showSuccess).toHaveBeenCalled();
79
84
  });
85
+
86
+ it("should save task server enabled toggle", async () => {
87
+ const el = new ShadowClawTaskServer();
88
+ document.body.appendChild(el);
89
+ await el.connectedCallback();
90
+
91
+ await el.saveTaskServerEnabled(false);
92
+
93
+ expect(mockSetTaskServerEnabled).toHaveBeenCalledWith(
94
+ orchestratorStore.orchestrator,
95
+ expect.anything(),
96
+ false,
97
+ );
98
+ });
80
99
  });
@@ -17,7 +17,7 @@ import shadowClawTaskServerTemplate from "./shadow-claw-task-server.html" with {
17
17
  const elementName = "shadow-claw-task-server";
18
18
 
19
19
  /**
20
- * Settings sub-component for Task Server URL configuration.
20
+ * Settings sub-component for Task Server URL and server-side task scheduling configuration.
21
21
  */
22
22
  export class ShadowClawTaskServer extends ShadowClawElement {
23
23
  static styles = shadowClawTaskServerStyles;
@@ -100,6 +100,7 @@ export class ShadowClawTaskServer extends ShadowClawElement {
100
100
  enabledToggle.checked = this.orchestrator.taskServerEnabled;
101
101
  }
102
102
  }
103
+
103
104
  async saveTaskServerEnabled(enabled: boolean) {
104
105
  if (!this.orchestrator || !this.db) {
105
106
  return;
@@ -1,27 +1,88 @@
1
1
  import { jest } from "@jest/globals";
2
- import { initChatSplitResize } from "./initChatSplitResize.js";
2
+ import {
3
+ DEFAULT_CHAT_SPLIT_HEIGHT_PX,
4
+ MIN_CHAT_SPLIT_HEIGHT_PX,
5
+ } from "../constants.js";
6
+
7
+ const mockGetConfig = jest.fn() as any;
8
+ const mockSetConfig = jest.fn() as any;
9
+
10
+ jest.unstable_mockModule("../../../db/getConfig.js", () => ({
11
+ getConfig: mockGetConfig,
12
+ }));
13
+
14
+ jest.unstable_mockModule("../../../db/setConfig.js", () => ({
15
+ setConfig: mockSetConfig,
16
+ }));
17
+
18
+ const { initChatSplitResize } = await import("./initChatSplitResize.js");
3
19
 
4
20
  describe("initChatSplitResize", () => {
5
21
  let shadowRoot: ShadowRoot;
6
22
  let mainContent: HTMLElement;
23
+ let chatPage: HTMLElement;
7
24
  let handle: HTMLElement;
8
25
  let shadowClaw: any;
26
+ let cleanups: Array<() => void>;
9
27
 
10
28
  beforeEach(() => {
29
+ jest.clearAllMocks();
30
+ cleanups = [];
31
+
32
+ // Mock desktop window dimensions
33
+ Object.defineProperty(window, "innerWidth", {
34
+ writable: true,
35
+ configurable: true,
36
+ value: 1200,
37
+ });
38
+ Object.defineProperty(window, "innerHeight", {
39
+ writable: true,
40
+ configurable: true,
41
+ value: 900,
42
+ });
43
+
11
44
  const host = document.createElement("div");
12
45
  shadowRoot = host.attachShadow({ mode: "open" });
13
46
 
14
47
  mainContent = document.createElement("div");
15
48
  mainContent.classList.add("main-content");
49
+ mainContent.getBoundingClientRect = () => ({
50
+ height: 900,
51
+ width: 1200,
52
+ top: 0,
53
+ left: 0,
54
+ bottom: 900,
55
+ right: 1200,
56
+ x: 0,
57
+ y: 0,
58
+ toJSON: () => {},
59
+ });
60
+
61
+ chatPage = document.createElement("div");
62
+ chatPage.classList.add("chat-page");
63
+ chatPage.getBoundingClientRect = () => ({
64
+ height: 300,
65
+ width: 400,
66
+ top: 0,
67
+ left: 0,
68
+ bottom: 300,
69
+ right: 400,
70
+ x: 0,
71
+ y: 0,
72
+ toJSON: () => {},
73
+ });
16
74
 
17
75
  handle = document.createElement("div");
18
76
  handle.classList.add("chat-split-resize-handle");
19
77
 
20
78
  mainContent.appendChild(handle);
79
+ mainContent.appendChild(chatPage);
21
80
  shadowRoot.appendChild(mainContent);
22
81
 
23
82
  shadowClaw = {
24
- addCleanup: jest.fn(),
83
+ addCleanup: jest.fn((fn: () => void) => {
84
+ cleanups.push(fn);
85
+ }),
25
86
  };
26
87
 
27
88
  if (!Element.prototype.setPointerCapture) {
@@ -29,31 +90,137 @@ describe("initChatSplitResize", () => {
29
90
  }
30
91
  });
31
92
 
32
- it("should initialize attributes and event listeners", async () => {
33
- await initChatSplitResize(shadowRoot, shadowClaw, handle, undefined);
93
+ afterEach(() => {
94
+ for (const fn of cleanups) {
95
+ fn();
96
+ }
97
+ });
98
+
99
+ it("handles null shadow or null handle safely", async () => {
100
+ await initChatSplitResize(null, shadowClaw, handle, undefined);
101
+ expect(shadowClaw.addCleanup).not.toHaveBeenCalled();
34
102
 
35
- expect(handle.getAttribute("tabindex")).toBe("0");
36
- expect(handle.getAttribute("role")).toBe("separator");
37
- expect(handle.getAttribute("aria-orientation")).toBe("horizontal");
38
- expect(handle.getAttribute("aria-label")).toBe("Resize chat panel height");
39
- expect(shadowClaw.addCleanup).toHaveBeenCalled();
103
+ await initChatSplitResize(shadowRoot, shadowClaw, null as any, undefined);
104
+ expect(shadowClaw.addCleanup).not.toHaveBeenCalled();
40
105
  });
41
106
 
42
- it("should update height on ArrowUp / ArrowDown keydown", async () => {
43
- await initChatSplitResize(shadowRoot, shadowClaw, handle, undefined);
107
+ it("loads saved height from db or defaults", async () => {
108
+ const mockDb: any = {};
109
+ mockGetConfig.mockResolvedValueOnce(450);
110
+
111
+ await initChatSplitResize(shadowRoot, shadowClaw, handle, mockDb);
112
+ expect(mainContent.style.getPropertyValue("--chat-split-height")).toBe(
113
+ "450px",
114
+ );
115
+
116
+ // String saved value
117
+ mockGetConfig.mockResolvedValueOnce("350");
118
+ await initChatSplitResize(shadowRoot, shadowClaw, handle, mockDb);
119
+ expect(mainContent.style.getPropertyValue("--chat-split-height")).toBe(
120
+ "350px",
121
+ );
122
+
123
+ // Fallback on error
124
+ mockGetConfig.mockRejectedValueOnce(new Error("DB error"));
125
+ await initChatSplitResize(shadowRoot, shadowClaw, handle, mockDb);
126
+ expect(mainContent.style.getPropertyValue("--chat-split-height")).toBe(
127
+ `${DEFAULT_CHAT_SPLIT_HEIGHT_PX}px`,
128
+ );
129
+ });
130
+
131
+ it("handles pointer drag resize workflow", async () => {
132
+ const mockDb: any = {};
133
+ await initChatSplitResize(shadowRoot, shadowClaw, handle, mockDb);
134
+
135
+ // Pointer down to start drag
136
+ const pointerDownEvent = new Event("pointerdown", { bubbles: true }) as any;
137
+ pointerDownEvent.pointerId = 1;
138
+ pointerDownEvent.pointerType = "mouse";
139
+ pointerDownEvent.button = 0;
140
+ pointerDownEvent.clientY = 500;
141
+ pointerDownEvent.preventDefault = jest.fn();
142
+
143
+ handle.dispatchEvent(pointerDownEvent);
144
+ expect(handle.classList.contains("active")).toBe(true);
145
+
146
+ // Pointer move dragging upward (delta = 500 - 400 = 100 increase)
147
+ const pointerMoveEvent = new Event("pointermove", { bubbles: true }) as any;
148
+ pointerMoveEvent.pointerId = 1;
149
+ pointerMoveEvent.clientY = 400;
150
+ document.dispatchEvent(pointerMoveEvent);
151
+
152
+ // Pointer up to finish drag
153
+ const pointerUpEvent = new Event("pointerup", { bubbles: true }) as any;
154
+ pointerUpEvent.pointerId = 1;
155
+ handle.dispatchEvent(pointerUpEvent);
44
156
 
157
+ expect(handle.classList.contains("active")).toBe(false);
158
+ });
159
+
160
+ it("handles dblclick to reset to default height", async () => {
161
+ const mockDb: any = {};
162
+ await initChatSplitResize(shadowRoot, shadowClaw, handle, mockDb);
163
+
164
+ mainContent.style.setProperty("--chat-split-height", "600px");
165
+ handle.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }));
166
+
167
+ expect(mainContent.style.getPropertyValue("--chat-split-height")).toBe(
168
+ `${DEFAULT_CHAT_SPLIT_HEIGHT_PX}px`,
169
+ );
170
+ });
171
+
172
+ it("handles keyboard navigation with Home, End, Shift+Arrow", async () => {
173
+ const mockDb: any = {};
174
+ await initChatSplitResize(shadowRoot, shadowClaw, handle, mockDb);
175
+
176
+ // Home key sets to min height
45
177
  handle.dispatchEvent(
46
- new KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }),
178
+ new KeyboardEvent("keydown", { key: "Home", bubbles: true }),
179
+ );
180
+ expect(mainContent.style.getPropertyValue("--chat-split-height")).toBe(
181
+ `${MIN_CHAT_SPLIT_HEIGHT_PX}px`,
182
+ );
183
+
184
+ // End key sets to max clamped height
185
+ handle.dispatchEvent(
186
+ new KeyboardEvent("keydown", { key: "End", bubbles: true }),
47
187
  );
48
188
  expect(
49
189
  mainContent.style.getPropertyValue("--chat-split-height"),
50
190
  ).toBeTruthy();
51
191
 
192
+ // Shift + ArrowUp increases by 32
52
193
  handle.dispatchEvent(
53
- new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
194
+ new KeyboardEvent("keydown", {
195
+ key: "ArrowUp",
196
+ shiftKey: true,
197
+ bubbles: true,
198
+ }),
54
199
  );
55
200
  expect(
56
201
  mainContent.style.getPropertyValue("--chat-split-height"),
57
202
  ).toBeTruthy();
58
203
  });
204
+
205
+ it("ignores resize events on mobile screen width (< 896px)", async () => {
206
+ Object.defineProperty(window, "innerWidth", {
207
+ writable: true,
208
+ configurable: true,
209
+ value: 600,
210
+ });
211
+
212
+ await initChatSplitResize(shadowRoot, shadowClaw, handle, undefined);
213
+
214
+ const pointerDownEvent = new Event("pointerdown", { bubbles: true }) as any;
215
+ pointerDownEvent.pointerId = 1;
216
+ pointerDownEvent.clientY = 500;
217
+ pointerDownEvent.button = 0;
218
+
219
+ handle.dispatchEvent(pointerDownEvent);
220
+ expect(handle.classList.contains("active")).toBe(false);
221
+
222
+ handle.dispatchEvent(
223
+ new KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }),
224
+ );
225
+ });
59
226
  });
@@ -10,6 +10,8 @@ describe("setupEffects", () => {
10
10
  let mockShowSuccess: any;
11
11
  let mockShowPage: any;
12
12
  let mockUpdateActivityLogToggleVisibility: any;
13
+ let mockEnsureComponentLoaded: any;
14
+ let mockFileViewerStore: any;
13
15
  let setupEffects: any;
14
16
 
15
17
  let effectCallbacks: Array<() => void>;
@@ -25,6 +27,8 @@ describe("setupEffects", () => {
25
27
  mockShowSuccess = jest.fn();
26
28
  mockShowPage = jest.fn();
27
29
  mockUpdateActivityLogToggleVisibility = jest.fn();
30
+ mockEnsureComponentLoaded = jest.fn().mockResolvedValue(undefined as never);
31
+ mockFileViewerStore = { file: null };
28
32
 
29
33
  jest.unstable_mockModule("../../../core/effect.js", () => ({
30
34
  effect: mockEffect,
@@ -42,7 +46,19 @@ describe("setupEffects", () => {
42
46
  updateActivityLogToggleVisibility: mockUpdateActivityLogToggleVisibility,
43
47
  }));
44
48
 
49
+ jest.unstable_mockModule("./loadComponent.js", () => ({
50
+ ensureComponentLoaded: mockEnsureComponentLoaded,
51
+ }));
52
+
53
+ jest.unstable_mockModule("../../../stores/file-viewer.js", () => ({
54
+ fileViewerStore: mockFileViewerStore,
55
+ }));
56
+
45
57
  shadowRoot = document.createElement("div").attachShadow({ mode: "open" });
58
+ const main = document.createElement("div");
59
+ main.classList.add("main-content");
60
+ shadowRoot.appendChild(main);
61
+
46
62
  shadowClaw = { previousOrchestratorState: "idle", currentPage: "chat" };
47
63
  db = {};
48
64
  oStore = { state: "idle", activePage: "chat", activityLog: [] };
@@ -65,6 +81,29 @@ describe("setupEffects", () => {
65
81
  expect(mockEffect).toHaveBeenCalledTimes(4);
66
82
  });
67
83
 
84
+ it("fileViewerStore effect should load file-viewer and pdf-viewer and stamp DOM", async () => {
85
+ setupEffects(shadowRoot, shadowClaw, db, oStore);
86
+ const cb = effectCallbacks[0];
87
+
88
+ // When file is null, do nothing
89
+ mockFileViewerStore.file = null;
90
+ cb();
91
+ expect(mockEnsureComponentLoaded).not.toHaveBeenCalled();
92
+
93
+ // When file is a text file
94
+ mockFileViewerStore.file = { name: "doc.txt", kind: "text" };
95
+ cb();
96
+ expect(mockEnsureComponentLoaded).toHaveBeenCalledWith("file-viewer");
97
+
98
+ await Promise.resolve();
99
+ expect(shadowRoot.querySelector("shadow-claw-file-viewer")).not.toBeNull();
100
+
101
+ // When file is a PDF
102
+ mockFileViewerStore.file = { name: "manual.pdf", kind: "pdf" };
103
+ cb();
104
+ expect(mockEnsureComponentLoaded).toHaveBeenCalledWith("pdf-viewer");
105
+ });
106
+
68
107
  it("orchestrator state effect should showSuccess when transitioning to idle from thinking/responding", () => {
69
108
  setupEffects(shadowRoot, shadowClaw, db, oStore);
70
109
  const cb = effectCallbacks[1];