shadow-claw 1.23.3 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/README.md +42 -5
  2. package/bin/assert-version-bump.mjs +60 -38
  3. package/bin/assert-version-bump.test.mjs +74 -11
  4. package/bin/cli.mjs +166 -0
  5. package/bin/cli.test.mjs +60 -0
  6. package/bin/commands/backup.mjs +70 -0
  7. package/bin/commands/clients.mjs +41 -0
  8. package/bin/commands/peer-id.mjs +76 -0
  9. package/bin/commands/peer-id.test.mjs +104 -0
  10. package/bin/commands/send.mjs +51 -0
  11. package/bin/commands/tasks.mjs +59 -0
  12. package/bin/commands/webrtc-listen.mjs +254 -0
  13. package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
  14. package/bin/rolldown.config.test.mjs +401 -8
  15. package/bin/utils/control-client.mjs +243 -0
  16. package/bin/utils/control-client.test.mjs +83 -0
  17. package/bin/utils/ulid.mjs +45 -0
  18. package/bin/utils/webrtc-control-client.mjs +699 -0
  19. package/bin/utils/webrtc-control-client.test.mjs +318 -0
  20. package/dist/electron/main.cjs +1 -1
  21. package/dist/public/AGENTS.md +5 -3
  22. package/dist/public/README.md +42 -5
  23. package/dist/public/agent.worker.js +6 -6
  24. package/dist/public/{app-routes-B0ePT49v.js → app-routes-Bs-riIvr.js} +1 -1
  25. package/dist/public/backup-controller-C_s9in2t.js +1 -0
  26. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
  27. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
  28. package/dist/public/components/settings/shadow-claw-storage/shadow-claw-storage.html +1 -1
  29. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
  30. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  31. package/dist/public/components/shadow-claw-files/shadow-claw-files.css +1 -1
  32. package/dist/public/components/shadow-claw-files/shadow-claw-files.html +1 -1
  33. package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
  34. package/dist/public/config-CrHchneq.js +1 -0
  35. package/dist/public/{configurePeerJs-B7kNU8AQ.js → configurePeerJs-CxpZeDJJ.js} +1 -1
  36. package/dist/public/{connections-xd_66YVU.js → connections-DvbgdkaB.js} +1 -1
  37. package/dist/public/{custom-element-security-CZ92anB8.js → custom-element-security-NCqQ5lmC.js} +1 -1
  38. package/dist/public/docs/README.md +3 -1
  39. package/dist/public/docs/architecture/overview.md +12 -12
  40. package/dist/public/docs/architecture/storage.md +5 -2
  41. package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
  42. package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +6 -3
  43. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  44. package/dist/public/docs/example/article/index.html +2 -2
  45. package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
  46. package/dist/public/docs/guides/server-development-configuration.md +24 -0
  47. package/dist/public/docs/publishing/index.html +19 -76
  48. package/dist/public/docs/skill-creator/index.html +2 -2
  49. package/dist/public/docs/subsystems/backup.md +56 -0
  50. package/dist/public/docs/subsystems/cli.md +124 -0
  51. package/dist/public/docs/subsystems/control-plane.md +158 -0
  52. package/dist/public/docs/subsystems/notifications.md +54 -2
  53. package/dist/public/docs/subsystems/reactive-ui.md +1 -0
  54. package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
  55. package/dist/public/e2e/README.md +5 -1
  56. package/dist/public/{e2e-bridge-DkS5xFGX.js → e2e-bridge-IGWsRl4i.js} +1 -1
  57. package/dist/public/file-viewer-D0eflgt6.js +1 -0
  58. package/dist/public/files/main/~/docs/publishing-to-github-pages.md +17 -82
  59. package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
  60. package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
  61. package/dist/public/{git-BkjrsRXX.js → git-7Vq_SJSp.js} +2 -2
  62. package/dist/public/{iframe-sanitizer-L6Jyu__Z.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
  63. package/dist/public/index.html +2 -2
  64. package/dist/public/index.js +1 -1
  65. package/dist/public/{initChatSplitResize-BslPXAXS.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
  66. package/dist/public/initControlPlane-C9jpnWev.js +1 -0
  67. package/dist/public/llms.txt +1 -1
  68. package/dist/public/main/index.html +2 -2
  69. package/dist/public/main/memory/index.html +2 -2
  70. package/dist/public/{markdown-6MBrIo-S.js → markdown-BnpKxSxl.js} +1 -1
  71. package/dist/public/mcp-reconnect-9emsqGRe.js +1 -0
  72. package/dist/public/{memoryStorage-CjGseKI1.js → memoryStorage-C0KvLNUp.js} +1 -1
  73. package/dist/public/{orchestrator-B9KmIj8Q.js → orchestrator-3h9xxHUm.js} +676 -676
  74. package/dist/public/orchestrator-nJj59RqD.js +15 -0
  75. package/dist/public/pages/main/~/docs/publishing-to-github-pages.md +17 -82
  76. package/dist/public/{peerjs-Dv8JNm7v.js → peerjs-BP4laMXj.js} +1 -1
  77. package/dist/public/{push-client-Dqq06wbB.js → push-client-CrjEXK3U.js} +1 -1
  78. package/dist/public/service-worker/fetch-proxy.js +1 -1
  79. package/dist/public/service-worker/push-handler.js +1 -1
  80. package/dist/public/service-worker.js +1 -1
  81. package/dist/public/{shadow-claw-Bd1rfuVC.js → shadow-claw-CbwETBWf.js} +2 -2
  82. package/dist/public/{shadow-claw-accounts-DL2l3oM0.js → shadow-claw-accounts-B4sBTQEj.js} +3 -3
  83. package/dist/public/{shadow-claw-channels-BGXDsQ38.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
  84. package/dist/public/{shadow-claw-chat-NXg6zz_l.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
  85. package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
  86. package/dist/public/{shadow-claw-conversations-C8eqGpP9.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
  87. package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
  88. package/dist/public/shadow-claw-file-viewer-Blf6yGVD.js +577 -0
  89. package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
  90. package/dist/public/{shadow-claw-git-C0ntz7aX.js → shadow-claw-git-v8S0JjuR.js} +3 -3
  91. package/dist/public/{shadow-claw-integrations-DCXV2r69.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
  92. package/dist/public/{shadow-claw-llm-CudgFQsS.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
  93. package/dist/public/{shadow-claw-mcp-remote-B8RkpX28.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
  94. package/dist/public/{shadow-claw-networking-BLiRsrsK.js → shadow-claw-networking-qHbP95z9.js} +1 -1
  95. package/dist/public/{shadow-claw-notifications-CQhQcjtL.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
  96. package/dist/public/{shadow-claw-page-header-NeRD0tKU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
  97. package/dist/public/{shadow-claw-pages-DCHnFh6r.js → shadow-claw-pages-DFZHhzFZ.js} +3 -3
  98. package/dist/public/{shadow-claw-settings-VAfjKFQg.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
  99. package/dist/public/shadow-claw-storage-BFynbP0j.js +219 -0
  100. package/dist/public/{shadow-claw-task-server-Dk33N_1G.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
  101. package/dist/public/{shadow-claw-tasks-xA1VfmWN.js → shadow-claw-tasks-Caxc9atL.js} +4 -4
  102. package/dist/public/{shadow-claw-terminal-Du2r0ciq.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
  103. package/dist/public/{shadow-claw-tools-CZj4rq7l.js → shadow-claw-tools-BfJx3A9W.js} +2 -2
  104. package/dist/public/{shadow-claw-webvm-JYUkWICG.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
  105. package/dist/public/static-main/~/docs/publishing-to-github-pages.md +17 -82
  106. package/dist/public/static-main-manifest.json +1 -1
  107. package/dist/public/{syncWebMcpRegistration-DfpvASnh.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
  108. package/dist/server.js +286 -189
  109. package/package.json +2 -1
  110. package/src/__mocks__/jszip.cjs +15 -5
  111. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
  112. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
  113. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
  114. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
  115. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
  116. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
  117. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
  118. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
  119. package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
  120. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
  121. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
  122. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
  123. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
  124. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.html +17 -0
  125. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +120 -0
  126. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.ts +56 -0
  127. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
  128. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  129. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
  130. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
  131. package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
  132. package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
  133. package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
  134. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
  135. package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
  136. package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
  137. package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
  138. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
  139. package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
  140. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +378 -0
  141. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +11 -9
  142. package/src/components/shadow-claw-files/shadow-claw-files.css +41 -17
  143. package/src/components/shadow-claw-files/shadow-claw-files.html +35 -0
  144. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +627 -1
  145. package/src/components/shadow-claw-files/shadow-claw-files.ts +170 -6
  146. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.test.ts +31 -0
  147. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.ts +2 -1
  148. package/src/components/shadow-claw-pages/utils/rewriteWorkspacePreviewHtml.test.ts +15 -0
  149. package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
  150. package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
  151. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
  152. package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
  153. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
  154. package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
  155. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
  156. package/src/config/config.test.ts +9 -0
  157. package/src/config/config.ts +15 -0
  158. package/src/core/app-routes.test.ts +56 -0
  159. package/src/core/app-routes.ts +8 -1
  160. package/src/core/backup-controller.test.ts +101 -0
  161. package/src/core/backup-controller.ts +137 -0
  162. package/src/core/control-plane-client.test.ts +408 -0
  163. package/src/core/control-plane-client.ts +425 -0
  164. package/src/core/orchestrator/orchestrator.ts +4 -0
  165. package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
  166. package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
  167. package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
  168. package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
  169. package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
  170. package/src/core/utils/initControlPlane.test.ts +276 -0
  171. package/src/core/utils/initControlPlane.ts +437 -0
  172. package/src/core/utils/initializeApp.ts +18 -0
  173. package/src/db/getSession.test.ts +13 -1
  174. package/src/db/openDatabase.test.ts +10 -1
  175. package/src/db/openDatabase.ts +3 -1
  176. package/src/server/app.test.ts +12 -0
  177. package/src/server/app.ts +18 -8
  178. package/src/server/client-registry.test.ts +250 -0
  179. package/src/server/client-registry.ts +477 -0
  180. package/src/server/config.ts +15 -0
  181. package/src/server/control-plane-types.ts +120 -0
  182. package/src/server/control-plane.test.ts +472 -0
  183. package/src/server/control-plane.ts +581 -0
  184. package/src/server/routes/backup.test.ts +263 -0
  185. package/src/server/routes/backup.ts +262 -0
  186. package/src/server/routes/integrations-email.test.ts +532 -0
  187. package/src/server/routes/llamafile.test.ts +172 -48
  188. package/src/server/routes/mesh-llm.test.ts +192 -0
  189. package/src/server/routes/ollama.test.ts +49 -0
  190. package/src/server/server-peer.test.ts +86 -0
  191. package/src/server/server-peer.ts +330 -0
  192. package/src/server/server.ts +35 -0
  193. package/src/server/services/llamafile-manager.test.ts +230 -6
  194. package/src/server/services/transformers-runtime.test.ts +186 -18
  195. package/src/server/webrtc.test.ts +21 -0
  196. package/src/server/webrtc.ts +72 -0
  197. package/src/service-worker/fetch-proxy.test.ts +64 -1
  198. package/src/service-worker/push-handler.ts +39 -0
  199. package/src/shell/vm.test.ts +744 -12
  200. package/src/storage/ensureMainGroupIndex.test.ts +153 -0
  201. package/src/storage/getGroupFile.test.ts +66 -0
  202. package/src/storage/memoryStorage.test.ts +82 -2
  203. package/src/storage/migrateLegacyOpfs.test.ts +327 -0
  204. package/src/storage/migrateLegacyOpfs.ts +117 -0
  205. package/src/storage/writeGroupFileBytes.test.ts +143 -0
  206. package/src/stores/chat-ui.test.ts +20 -0
  207. package/src/stores/orchestrator.test.ts +134 -0
  208. package/src/stores/toast.test.ts +50 -0
  209. package/src/stores/tools.test.ts +147 -10
  210. package/src/subsystems/channels/imessage.test.ts +59 -0
  211. package/src/subsystems/channels/peerjs.test.ts +500 -62
  212. package/src/subsystems/channels/peerjs.ts +71 -4
  213. package/src/subsystems/channels/telegram.test.ts +177 -0
  214. package/src/subsystems/email/catalog.test.ts +1 -0
  215. package/src/subsystems/email/connections.test.ts +39 -1
  216. package/src/subsystems/git/credentials.test.ts +160 -825
  217. package/src/subsystems/git/git.test.ts +463 -0
  218. package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
  219. package/src/subsystems/notifications/push-routes.test.ts +33 -0
  220. package/src/subsystems/notifications/push-routes.ts +23 -0
  221. package/src/subsystems/notifications/push-store.ts +1 -1
  222. package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
  223. package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
  224. package/src/subsystems/providers/model-registry.test.ts +115 -0
  225. package/src/subsystems/providers/providers.test.ts +94 -1
  226. package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
  227. package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
  228. package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
  229. package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
  230. package/src/subsystems/skills/activateSkill.test.ts +101 -0
  231. package/src/subsystems/skills/discoverSkills.test.ts +251 -0
  232. package/src/subsystems/tools/cron.test.ts +64 -0
  233. package/src/subsystems/tools/declarative.test.ts +255 -89
  234. package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
  235. package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
  236. package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
  237. package/src/testing/jest-setup.ts +328 -40
  238. package/src/testing/mock-v86.mjs +49 -0
  239. package/src/ui/a2ui/types.ts +4 -3
  240. package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
  241. package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
  242. package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
  243. package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
  244. package/src/worker/tools/email/email.test.ts +193 -8
  245. package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
  246. package/src/worker/tools/git/git.test.ts +214 -3
  247. package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
  248. package/src/worker/tools/rooms/leave-room.test.ts +40 -0
  249. package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
  250. package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
  251. package/src/worker/tools/ui/ask-user.test.ts +50 -0
  252. package/src/worker/tools/ui/clear-chat.test.ts +19 -0
  253. package/src/worker/tools/ui/get-current-time.test.ts +19 -0
  254. package/src/worker/tools/ui/javascript.test.ts +116 -0
  255. package/src/worker/tools/ui/list-components.test.ts +10 -0
  256. package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
  257. package/src/worker/tools/ui/manage-tools.test.ts +34 -0
  258. package/src/worker/tools/ui/render-component.test.ts +346 -0
  259. package/src/worker/tools/ui/send-notification.test.ts +40 -0
  260. package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
  261. package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
  262. package/src/worker/tools/workspace/attach-file.test.ts +84 -0
  263. package/src/worker/tools/workspace/copy-file.test.ts +69 -0
  264. package/src/worker/tools/workspace/create-directory.test.ts +40 -0
  265. package/src/worker/tools/workspace/delete-file.test.ts +73 -0
  266. package/src/worker/tools/workspace/diff-files.test.ts +94 -0
  267. package/src/worker/tools/workspace/list-files.test.ts +36 -0
  268. package/src/worker/tools/workspace/move-file.test.ts +66 -0
  269. package/src/worker/tools/workspace/open-file.test.ts +57 -0
  270. package/src/worker/tools/workspace/patch-file.test.ts +81 -0
  271. package/src/worker/tools/workspace/read-file.test.ts +116 -0
  272. package/src/worker/tools/workspace/send-file.test.ts +82 -0
  273. package/src/worker/tools/workspace/update-memory.test.ts +29 -0
  274. package/src/worker/tools/workspace/write-file.test.ts +28 -0
  275. package/src/worker/utils/rate-limit.test.ts +199 -0
  276. package/src/worker/utils/tool-state.test.ts +29 -0
  277. package/src/worker/utils/transformers-js-utils.test.ts +59 -0
  278. package/dist/public/config-AL77QAx4.js +0 -1
  279. package/dist/public/downloadGroupFile-BABv_1mJ.js +0 -1
  280. package/dist/public/file-viewer-CCSABMsE.js +0 -1
  281. package/dist/public/mcp-reconnect-DoicazQd.js +0 -1
  282. package/dist/public/orchestrator-4su7iyWj.js +0 -15
  283. package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
  284. package/dist/public/shadow-claw-file-viewer-9Qkwc30T.js +0 -577
  285. package/dist/public/shadow-claw-files-aDyq6Hv7.js +0 -781
  286. package/dist/public/shadow-claw-storage-Cf31np81.js +0 -202
@@ -17,6 +17,10 @@ jest.unstable_mockModule("../../../db/getConfig.js", () => ({
17
17
  getConfig: jest.fn<any>().mockResolvedValue(undefined),
18
18
  }));
19
19
 
20
+ jest.unstable_mockModule("../../../db/setConfig.js", () => ({
21
+ setConfig: jest.fn<any>().mockResolvedValue(undefined),
22
+ }));
23
+
20
24
  jest.unstable_mockModule("../../../storage/isPersistent.js", () => ({
21
25
  isPersistent: jest.fn<any>().mockResolvedValue(false),
22
26
  }));
@@ -47,6 +51,32 @@ jest.unstable_mockModule("../../../storage/getStorageEstimate.js", () => ({
47
51
  .mockResolvedValue({ usage: 1024, quota: 10240 }),
48
52
  }));
49
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
+
76
+ const { CONFIG_KEYS } = await import("../../../config/config.js");
77
+ const { getConfig } = await import("../../../db/getConfig.js");
78
+ const { setConfig } = await import("../../../db/setConfig.js");
79
+ const { showSuccess } = await import("../../../ui/toast.js");
50
80
  const { ShadowClawStorage } = await import("./shadow-claw-storage.js");
51
81
 
52
82
  describe("shadow-claw-storage", () => {
@@ -71,4 +101,94 @@ describe("shadow-claw-storage", () => {
71
101
 
72
102
  document.body.removeChild(el);
73
103
  });
104
+
105
+ it("renders and syncs upload append ULID toggle", async () => {
106
+ (getConfig as any).mockImplementation((_db: any, key: string) => {
107
+ if (key === CONFIG_KEYS.FILES_UPLOAD_APPEND_ULID) {
108
+ return Promise.resolve("true");
109
+ }
110
+ return Promise.resolve(undefined);
111
+ });
112
+
113
+ const el = new ShadowClawStorage();
114
+ document.body.appendChild(el);
115
+ await new Promise((r) => setTimeout(r, 100));
116
+ await el.updateStorageInfo();
117
+
118
+ const toggle = el.shadowRoot?.querySelector(
119
+ '[data-setting="files-upload-append-ulid-toggle"]',
120
+ ) as HTMLInputElement | null;
121
+ expect(toggle).not.toBeNull();
122
+ expect(toggle?.checked).toBe(true);
123
+
124
+ toggle!.checked = false;
125
+ toggle!.dispatchEvent(new Event("change"));
126
+ await new Promise((r) => setTimeout(r, 50));
127
+
128
+ expect(setConfig).toHaveBeenCalledWith(
129
+ expect.anything(),
130
+ CONFIG_KEYS.FILES_UPLOAD_APPEND_ULID,
131
+ "false",
132
+ );
133
+ expect(showSuccess).toHaveBeenCalledWith(
134
+ "Unique ID prefix on file upload disabled",
135
+ 2500,
136
+ );
137
+
138
+ document.body.removeChild(el);
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
+ });
74
194
  });
@@ -13,6 +13,7 @@ import { resetStorageDirectory } from "../../../storage/storage.js";
13
13
 
14
14
  import { orchestratorStore } from "../../../stores/orchestrator.js";
15
15
  import { showError, showSuccess, showWarning } from "../../../ui/toast.js";
16
+ import { isTruthyConfigValue } from "../../../common/utils/config-value.mjs";
16
17
 
17
18
  import type { ShadowClawDatabase } from "../../../db/types.js";
18
19
 
@@ -86,6 +87,15 @@ export class ShadowClawStorage extends ShadowClawElement {
86
87
  orchestratorStore.grantStorageAccess(this.db);
87
88
  }
88
89
  });
90
+
91
+ root
92
+ .querySelector('[data-setting="files-upload-append-ulid-toggle"]')
93
+ ?.addEventListener("change", (e: Event) => {
94
+ const target = e.target as HTMLInputElement;
95
+ if (target) {
96
+ void this.onUploadAppendUlidToggle(target.checked);
97
+ }
98
+ });
89
99
  }
90
100
 
91
101
  /**
@@ -240,6 +250,37 @@ export class ShadowClawStorage extends ShadowClawElement {
240
250
  }
241
251
  }
242
252
 
253
+ /**
254
+ * Handle toggling unique ID prefix for uploaded files.
255
+ */
256
+ async onUploadAppendUlidToggle(enabled: boolean) {
257
+ if (!this.db) {
258
+ return;
259
+ }
260
+
261
+ try {
262
+ const { setConfig } = await import("../../../db/setConfig.js");
263
+ await setConfig(
264
+ this.db,
265
+ CONFIG_KEYS.FILES_UPLOAD_APPEND_ULID,
266
+ enabled ? "true" : "false",
267
+ );
268
+
269
+ showSuccess(
270
+ enabled
271
+ ? "Unique ID prefix on file upload enabled"
272
+ : "Unique ID prefix on file upload disabled",
273
+ 2500,
274
+ );
275
+ } catch (err) {
276
+ const errorMsg = err instanceof Error ? err.message : String(err);
277
+ showError(
278
+ "Error saving unique ID file upload setting: " + errorMsg,
279
+ 6000,
280
+ );
281
+ }
282
+ }
283
+
243
284
  /**
244
285
  * Handle resetting storage directory.
245
286
  */
@@ -383,6 +424,21 @@ export class ShadowClawStorage extends ShadowClawElement {
383
424
  if (requestPersistentBtn) {
384
425
  requestPersistentBtn.disabled = persistent;
385
426
  }
427
+
428
+ // Sync upload append ULID toggle
429
+ const uploadAppendUlidVal = await getConfig(
430
+ this.db,
431
+ CONFIG_KEYS.FILES_UPLOAD_APPEND_ULID,
432
+ );
433
+ const uploadAppendUlidToggle = root.querySelector(
434
+ '[data-setting="files-upload-append-ulid-toggle"]',
435
+ ) as HTMLInputElement | null;
436
+ if (uploadAppendUlidToggle) {
437
+ uploadAppendUlidToggle.checked = isTruthyConfigValue(
438
+ uploadAppendUlidVal,
439
+ false,
440
+ );
441
+ }
386
442
  } catch (err) {
387
443
  console.warn("Failed to update storage info:", err);
388
444
  }
@@ -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];