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
@@ -1,5 +1,8 @@
1
+ import { CONFIG_KEYS } from "../../config/config.js";
1
2
  import { effect } from "../../core/effect.js";
2
3
  import { getDb } from "../../db/db.js";
4
+ import { getConfig } from "../../db/getConfig.js";
5
+ import { isTruthyConfigValue } from "../../common/utils/config-value.mjs";
3
6
 
4
7
  import { setSanitizedHtml } from "../../security/trusted-types.js";
5
8
  import { copyGroupEntry } from "../../storage/copyGroupEntry.js";
@@ -1575,34 +1578,195 @@ export class ShadowClawFiles extends ShadowClawElement {
1575
1578
  return false;
1576
1579
  }
1577
1580
 
1581
+ async promptUploadConflict(
1582
+ fileName: string,
1583
+ existingNames: Set<string>,
1584
+ ): Promise<
1585
+ { action: "overwrite" } | { action: "rename"; name: string } | null
1586
+ > {
1587
+ const root = this.shadowRoot;
1588
+ if (!root) {
1589
+ return null;
1590
+ }
1591
+
1592
+ const dialog = root.querySelector(".files__upload-conflict-dialog");
1593
+ const messageEl = root.querySelector(".files__upload-conflict-message");
1594
+ const renameInput = root.querySelector(
1595
+ ".files__upload-conflict-rename-input",
1596
+ );
1597
+ const form = root.querySelector(".files__upload-conflict-form");
1598
+ const cancelBtn = root.querySelector(".files__upload-conflict-cancel");
1599
+ const overwriteBtn = root.querySelector(
1600
+ ".files__upload-conflict-overwrite",
1601
+ );
1602
+
1603
+ if (
1604
+ !(dialog instanceof HTMLDialogElement) ||
1605
+ !(form instanceof HTMLFormElement) ||
1606
+ !(renameInput instanceof HTMLInputElement)
1607
+ ) {
1608
+ return null;
1609
+ }
1610
+
1611
+ const targetDirName =
1612
+ orchestratorStore.currentPath === "."
1613
+ ? "Root"
1614
+ : orchestratorStore.currentPath;
1615
+
1616
+ if (messageEl instanceof HTMLElement) {
1617
+ messageEl.textContent = `"${fileName}" already exists in "${targetDirName}". Please rename or choose to overwrite.`;
1618
+ }
1619
+
1620
+ renameInput.value = fileName;
1621
+
1622
+ return new Promise((resolve) => {
1623
+ let resolved = false;
1624
+
1625
+ const cleanup = () => {
1626
+ form.removeEventListener("submit", handleSubmit);
1627
+ cancelBtn?.removeEventListener("click", handleCancel);
1628
+ overwriteBtn?.removeEventListener("click", handleOverwrite);
1629
+ dialog.removeEventListener("cancel", handleCancel);
1630
+ dialog.removeEventListener("close", handleClose);
1631
+ };
1632
+
1633
+ const finish = (
1634
+ result:
1635
+ | { action: "overwrite" }
1636
+ | { action: "rename"; name: string }
1637
+ | null,
1638
+ ) => {
1639
+ if (resolved) {
1640
+ return;
1641
+ }
1642
+ resolved = true;
1643
+ cleanup();
1644
+ if (typeof dialog.close === "function") {
1645
+ dialog.close();
1646
+ } else {
1647
+ dialog.removeAttribute("open");
1648
+ }
1649
+ resolve(result);
1650
+ };
1651
+
1652
+ const handleSubmit = (event: Event) => {
1653
+ event.preventDefault();
1654
+ const nextName = renameInput.value.trim();
1655
+ if (!nextName) {
1656
+ showWarning("Please enter a name", 3000);
1657
+ return;
1658
+ }
1659
+
1660
+ if (nextName.includes("/") || nextName.includes("\\")) {
1661
+ showWarning("Use only a name, not a path", 3500);
1662
+ return;
1663
+ }
1664
+
1665
+ if (existingNames.has(nextName) || existingNames.has(`${nextName}/`)) {
1666
+ showWarning(
1667
+ `"${nextName}" already exists. Please choose a different name.`,
1668
+ 4000,
1669
+ );
1670
+ renameInput.select();
1671
+ return;
1672
+ }
1673
+
1674
+ finish({ action: "rename", name: nextName });
1675
+ };
1676
+
1677
+ const handleOverwrite = (event: Event) => {
1678
+ event.preventDefault();
1679
+ finish({ action: "overwrite" });
1680
+ };
1681
+
1682
+ const handleCancel = (event: Event) => {
1683
+ event.preventDefault();
1684
+ finish(null);
1685
+ };
1686
+
1687
+ const handleClose = () => {
1688
+ finish(null);
1689
+ };
1690
+
1691
+ form.addEventListener("submit", handleSubmit);
1692
+ cancelBtn?.addEventListener("click", handleCancel);
1693
+ overwriteBtn?.addEventListener("click", handleOverwrite);
1694
+ dialog.addEventListener("cancel", handleCancel);
1695
+ dialog.addEventListener("close", handleClose);
1696
+
1697
+ if (typeof dialog.showModal === "function") {
1698
+ dialog.showModal();
1699
+ } else {
1700
+ dialog.setAttribute("open", "");
1701
+ }
1702
+ renameInput.select();
1703
+ });
1704
+ }
1705
+
1578
1706
  async uploadFileList(db: ShadowClawDatabase, files: FileList | File[]) {
1579
1707
  const fileList: File[] = Array.from(files) as File[];
1580
1708
  if (fileList.length === 0) {
1581
1709
  return;
1582
1710
  }
1583
1711
 
1712
+ const appendUlid = await getConfig(db, CONFIG_KEYS.FILES_UPLOAD_APPEND_ULID)
1713
+ .then((val) => isTruthyConfigValue(val, false))
1714
+ .catch(() => false);
1715
+
1584
1716
  const groupId = orchestratorStore.activeGroupId;
1585
1717
  const currentPath = orchestratorStore.currentPath;
1586
1718
  const count = fileList.length;
1587
1719
 
1588
1720
  filesUiStore.startUpload(count);
1589
1721
 
1722
+ const existingNames = new Set(orchestratorStore.files);
1723
+ let uploadedCount = 0;
1724
+
1590
1725
  try {
1591
1726
  for (let i = 0; i < count; i++) {
1592
1727
  const file = fileList[i];
1593
- const filename =
1594
- currentPath === "."
1595
- ? `${ulid()}-${file.name}`
1596
- : `${currentPath}/${ulid()}-${file.name}`;
1728
+ let filename = appendUlid ? `${ulid()}-${file.name}` : file.name;
1729
+
1730
+ const hasConflict =
1731
+ existingNames.has(filename) || existingNames.has(`${filename}/`);
1732
+
1733
+ if (hasConflict) {
1734
+ const decision = await this.promptUploadConflict(
1735
+ filename,
1736
+ existingNames,
1737
+ );
1738
+ if (!decision) {
1739
+ filesUiStore.setUploadCompleted(i + 1);
1740
+ continue;
1741
+ }
1597
1742
 
1598
- await uploadGroupFile(db, groupId, filename, file);
1743
+ if (decision.action === "rename") {
1744
+ filename = decision.name;
1745
+ } else if (decision.action === "overwrite") {
1746
+ const targetPath =
1747
+ currentPath === "." ? filename : `${currentPath}/${filename}`;
1748
+ await this.removePageAssignmentIfNeeded(db, targetPath);
1749
+ }
1750
+ }
1751
+
1752
+ const fullPath =
1753
+ currentPath === "." ? filename : `${currentPath}/${filename}`;
1754
+
1755
+ await uploadGroupFile(db, groupId, fullPath, file);
1756
+ existingNames.add(filename);
1757
+ uploadedCount++;
1599
1758
  filesUiStore.setUploadCompleted(i + 1);
1600
1759
  }
1601
1760
 
1602
1761
  // Reload files
1603
1762
  await orchestratorStore.loadFiles(db);
1604
1763
 
1605
- showSuccess(`Uploaded ${count} file${count === 1 ? "" : "s"}`, 3000);
1764
+ if (uploadedCount > 0) {
1765
+ showSuccess(
1766
+ `Uploaded ${uploadedCount} file${uploadedCount === 1 ? "" : "s"}`,
1767
+ 3000,
1768
+ );
1769
+ }
1606
1770
  } catch (err) {
1607
1771
  const message = err instanceof Error ? err.message : String(err);
1608
1772
 
@@ -36,6 +36,37 @@ describe("resolveWorkspaceLinkPath", () => {
36
36
  expect(result).toBe("docs/guide.md");
37
37
  });
38
38
 
39
+ it("resolves relative image link in file directory", () => {
40
+ const result = resolveWorkspaceLinkPath(
41
+ "01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg",
42
+ "index.md",
43
+ groupId,
44
+ origin,
45
+ );
46
+ expect(result).toBe("01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg");
47
+ });
48
+
49
+ it("resolves relative links when subpath base tag is active", () => {
50
+ document.querySelectorAll("base").forEach((el) => el.remove());
51
+ const baseEl = document.createElement("base");
52
+ baseEl.setAttribute("href", "/shadow-claw/");
53
+ document.head.appendChild(baseEl);
54
+ (globalThis as any).__applyBasePathCacheReset?.();
55
+
56
+ try {
57
+ const result = resolveWorkspaceLinkPath(
58
+ "01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg",
59
+ "index.md",
60
+ groupId,
61
+ origin,
62
+ );
63
+ expect(result).toBe("01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg");
64
+ } finally {
65
+ baseEl.remove();
66
+ (globalThis as any).__applyBasePathCacheReset?.();
67
+ }
68
+ });
69
+
39
70
  it("returns null if link points to external origin", () => {
40
71
  const result = resolveWorkspaceLinkPath(
41
72
  "https://example.com/page",
@@ -1,4 +1,5 @@
1
1
  import {
2
+ applyBasePath,
2
3
  getFileRouteDirPath,
3
4
  getWorkspaceRouteRequestPath,
4
5
  resolveHrefAgainstRoute,
@@ -43,7 +44,7 @@ export function resolveWorkspaceLinkPath(
43
44
  }
44
45
  }
45
46
 
46
- const routeDir = getFileRouteDirPath(groupId, filePath);
47
+ const routeDir = applyBasePath(getFileRouteDirPath(groupId, filePath));
47
48
  const resolved = resolveHrefAgainstRoute(
48
49
  trimmed,
49
50
  routeDir,
@@ -42,4 +42,19 @@ describe("rewriteWorkspacePreviewHtml", () => {
42
42
  );
43
43
  expect(rewritten).toContain('href="https://example.com/docs"');
44
44
  });
45
+
46
+ it("rewrites relative markdown links to subpath-prefixed workspace routes", () => {
47
+ const html =
48
+ '<p><a href="01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg">01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg</a></p>';
49
+ const rewritten = rewriteWorkspacePreviewHtml(
50
+ html,
51
+ "index.md",
52
+ "/shadow-claw/files/br:main/",
53
+ groupId,
54
+ origin,
55
+ );
56
+ expect(rewritten).toContain(
57
+ 'href="/shadow-claw/files/br:main/01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg"',
58
+ );
59
+ });
45
60
  });
@@ -0,0 +1,65 @@
1
+ import { jest } from "@jest/globals";
2
+ import { installGetOrInsertComputedPolyfill } from "./installGetOrInsertComputedPolyfill.js";
3
+
4
+ describe("installGetOrInsertComputedPolyfill", () => {
5
+ let originalMapMethod: any;
6
+ let originalWeakMapMethod: any;
7
+
8
+ beforeEach(() => {
9
+ originalMapMethod = (Map.prototype as any).getOrInsertComputed;
10
+ originalWeakMapMethod = (WeakMap.prototype as any).getOrInsertComputed;
11
+ delete (Map.prototype as any).getOrInsertComputed;
12
+ delete (WeakMap.prototype as any).getOrInsertComputed;
13
+ });
14
+
15
+ afterEach(() => {
16
+ if (originalMapMethod) {
17
+ (Map.prototype as any).getOrInsertComputed = originalMapMethod;
18
+ } else {
19
+ delete (Map.prototype as any).getOrInsertComputed;
20
+ }
21
+
22
+ if (originalWeakMapMethod) {
23
+ (WeakMap.prototype as any).getOrInsertComputed = originalWeakMapMethod;
24
+ } else {
25
+ delete (WeakMap.prototype as any).getOrInsertComputed;
26
+ }
27
+ });
28
+
29
+ it("installs polyfill on Map and WeakMap prototypes and computes missing values", () => {
30
+ installGetOrInsertComputedPolyfill();
31
+
32
+ const map = new Map<string, number>();
33
+ const compute = jest.fn(() => 42);
34
+
35
+ const val1 = (map as any).getOrInsertComputed("foo", compute);
36
+ expect(val1).toBe(42);
37
+ expect(compute).toHaveBeenCalledTimes(1);
38
+
39
+ // Second call should return cached value without computing
40
+ const val2 = (map as any).getOrInsertComputed("foo", compute);
41
+ expect(val2).toBe(42);
42
+ expect(compute).toHaveBeenCalledTimes(1);
43
+
44
+ const weakMap = new WeakMap<object, string>();
45
+ const keyObj = {};
46
+ const weakCompute = jest.fn(() => "computed");
47
+
48
+ const wVal1 = (weakMap as any).getOrInsertComputed(keyObj, weakCompute);
49
+ expect(wVal1).toBe("computed");
50
+ expect(weakCompute).toHaveBeenCalledTimes(1);
51
+
52
+ const wVal2 = (weakMap as any).getOrInsertComputed(keyObj, weakCompute);
53
+ expect(wVal2).toBe("computed");
54
+ expect(weakCompute).toHaveBeenCalledTimes(1);
55
+ });
56
+
57
+ it("does not overwrite existing getOrInsertComputed", () => {
58
+ const existing = jest.fn();
59
+ (Map.prototype as any).getOrInsertComputed = existing;
60
+
61
+ installGetOrInsertComputedPolyfill();
62
+
63
+ expect((Map.prototype as any).getOrInsertComputed).toBe(existing);
64
+ });
65
+ });
@@ -496,6 +496,13 @@
496
496
  </div>
497
497
  </details>
498
498
 
499
+ <details class="settings-collapsible">
500
+ <summary>🎛️ Control Plane</summary>
501
+ <div class="settings-collapsible-content">
502
+ <shadow-claw-control-plane></shadow-claw-control-plane>
503
+ </div>
504
+ </details>
505
+
499
506
  <details class="settings-collapsible">
500
507
  <summary>📧 Email</summary>
501
508
  <div class="settings-collapsible-content">
@@ -136,6 +136,23 @@ jest.unstable_mockModule(
136
136
  },
137
137
  );
138
138
 
139
+ jest.unstable_mockModule(
140
+ "../settings/shadow-claw-control-plane/shadow-claw-control-plane.js",
141
+ () => {
142
+ class MockControlPlane extends HTMLElement {
143
+ render = jest.fn();
144
+ constructor() {
145
+ super();
146
+ this.attachShadow({ mode: "open" });
147
+ }
148
+ }
149
+
150
+ customElements.define("shadow-claw-control-plane", MockControlPlane);
151
+
152
+ return { ShadowClawControlPlane: MockControlPlane };
153
+ },
154
+ );
155
+
139
156
  jest.unstable_mockModule(
140
157
  "../settings/shadow-claw-notifications/shadow-claw-notifications.js",
141
158
  () => {
@@ -812,4 +829,208 @@ describe("shadow-claw-settings", () => {
812
829
 
813
830
  document.body.removeChild(el);
814
831
  });
832
+
833
+ it("handles onChatSplitViewToggle and dispatches event", async () => {
834
+ const el = new ShadowClawSettings() as any;
835
+ el.db = {};
836
+ const events: any[] = [];
837
+ el.addEventListener("chat-split-view-change", (e: any) =>
838
+ events.push(e.detail),
839
+ );
840
+
841
+ await el.onChatSplitViewToggle(true);
842
+
843
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
844
+ expect.anything(),
845
+ "chat_split_view_enabled",
846
+ "true",
847
+ );
848
+ expect(events).toEqual([{ enabled: true }]);
849
+ });
850
+
851
+ it("handles onPagesAutoRefreshInputChange and dispatches event", async () => {
852
+ const el = new ShadowClawSettings() as any;
853
+ el.db = {};
854
+ const events: any[] = [];
855
+ const listener = (e: any) => events.push(e.detail);
856
+ window.addEventListener("shadow-claw-pages-auto-refresh-change", listener);
857
+
858
+ try {
859
+ await el.onPagesAutoRefreshInputChange(30);
860
+
861
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
862
+ expect.anything(),
863
+ "pages_auto_refresh_interval",
864
+ "30",
865
+ );
866
+ expect(events).toEqual([{ interval: 30 }]);
867
+ } finally {
868
+ window.removeEventListener(
869
+ "shadow-claw-pages-auto-refresh-change",
870
+ listener,
871
+ );
872
+ }
873
+ });
874
+
875
+ it("handles sidebar navigation visibility toggles", async () => {
876
+ const el = new ShadowClawSettings() as any;
877
+ el.db = {};
878
+
879
+ await el.onSidebarHideChatToggle(true);
880
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
881
+ expect.anything(),
882
+ "sidebar_chat_hidden",
883
+ "true",
884
+ );
885
+
886
+ await el.onSidebarHideFilesToggle(true);
887
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
888
+ expect.anything(),
889
+ "sidebar_files_hidden",
890
+ "true",
891
+ );
892
+
893
+ await el.onSidebarHideTasksToggle(true);
894
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
895
+ expect.anything(),
896
+ "sidebar_tasks_hidden",
897
+ "true",
898
+ );
899
+ });
900
+
901
+ it("handles replaceConfigEntries with transaction", async () => {
902
+ const el = new ShadowClawSettings() as any;
903
+ const mockStore = {
904
+ clear: jest.fn(),
905
+ put: jest.fn(),
906
+ };
907
+ const mockTx = {
908
+ objectStore: jest.fn(() => mockStore),
909
+ oncomplete: null as any,
910
+ onerror: null as any,
911
+ };
912
+ el.db = {
913
+ transaction: jest.fn(() => {
914
+ setTimeout(() => mockTx.oncomplete?.(), 0);
915
+ return mockTx;
916
+ }),
917
+ };
918
+
919
+ await el.replaceConfigEntries([
920
+ { key: "k1", value: "v1" },
921
+ { key: "k2", value: "v2" },
922
+ ]);
923
+
924
+ expect(mockStore.clear).toHaveBeenCalled();
925
+ expect(mockStore.put).toHaveBeenCalledWith({ key: "k1", value: "v1" });
926
+ expect(mockStore.put).toHaveBeenCalledWith({ key: "k2", value: "v2" });
927
+ });
928
+
929
+ it("returns null when promptForPlaintextBackupHandle encounters AbortError", async () => {
930
+ const el = new ShadowClawSettings() as any;
931
+ const abortErr = new Error("User canceled");
932
+ abortErr.name = "AbortError";
933
+
934
+ (globalThis as any).showSaveFilePicker = (
935
+ jest.fn() as any
936
+ ).mockRejectedValue(abortErr);
937
+
938
+ const handle = await el.promptForPlaintextBackupHandle();
939
+ expect(handle).toBeNull();
940
+
941
+ delete (globalThis as any).showSaveFilePicker;
942
+ });
943
+
944
+ it("handles activity log, chat split view, prerender skeleton, and pages auto refresh settings", async () => {
945
+ const el = new ShadowClawSettings() as any;
946
+ el.db = {} as any;
947
+
948
+ await el.onActivityLogDiskLoggingToggle(true);
949
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
950
+ el.db,
951
+ "activity_log_disk_logging_enabled",
952
+ "true",
953
+ );
954
+
955
+ const splitEventSpy = jest.fn();
956
+ el.addEventListener("chat-split-view-change", splitEventSpy);
957
+ await el.onChatSplitViewToggle(true);
958
+ expect(splitEventSpy).toHaveBeenCalled();
959
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
960
+ el.db,
961
+ "chat_split_view_enabled",
962
+ "true",
963
+ );
964
+
965
+ await el.onOverridePrerenderSkeletonToggle(true);
966
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
967
+ el.db,
968
+ "override_prerender_skeleton",
969
+ "true",
970
+ );
971
+
972
+ await el.onPagesAutoRefreshInputChange(30);
973
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
974
+ el.db,
975
+ "pages_auto_refresh_interval",
976
+ "30",
977
+ );
978
+
979
+ // Sidebar visibility toggles
980
+ await el.onSidebarHideChatToggle(true);
981
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
982
+ el.db,
983
+ "sidebar_chat_hidden",
984
+ "true",
985
+ );
986
+
987
+ await el.onSidebarHideFilesToggle(true);
988
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
989
+ el.db,
990
+ "sidebar_files_hidden",
991
+ "true",
992
+ );
993
+
994
+ await el.onSidebarHidePagesToggle(true);
995
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
996
+ el.db,
997
+ "sidebar_pages_hidden",
998
+ "true",
999
+ );
1000
+
1001
+ await el.onSidebarHideTasksToggle(true);
1002
+ expect((globalThis as any)._mockSetConfig).toHaveBeenCalledWith(
1003
+ el.db,
1004
+ "sidebar_tasks_hidden",
1005
+ "true",
1006
+ );
1007
+
1008
+ // Backup / Restore / Clear methods
1009
+ (global.URL.createObjectURL as any) = jest
1010
+ .fn<any>()
1011
+ .mockReturnValue("blob:settings-backup");
1012
+ (global.URL.revokeObjectURL as any) = jest.fn();
1013
+ el.getAllConfigEntries = jest
1014
+ .fn<any>()
1015
+ .mockResolvedValue([{ key: "k", value: "v" }]);
1016
+ el.replaceConfigEntries = jest.fn<any>().mockResolvedValue(undefined);
1017
+ el.closeDialog = jest.fn();
1018
+
1019
+ // confirmBackup
1020
+ await el.confirmBackup();
1021
+ expect(el.closeDialog).toHaveBeenCalledWith(".settings__backup-dialog");
1022
+
1023
+ // confirmClear
1024
+ await el.confirmClear();
1025
+ expect(el.replaceConfigEntries).toHaveBeenCalledWith([]);
1026
+
1027
+ // confirmRestore
1028
+ el.pendingRestoreFile = {
1029
+ name: "backup.json",
1030
+ text: async () =>
1031
+ JSON.stringify({ configEntries: [{ key: "k2", value: "v2" }] }),
1032
+ } as any;
1033
+ await el.confirmRestore();
1034
+ expect(el.replaceConfigEntries).toHaveBeenCalled();
1035
+ });
815
1036
  });
@@ -38,6 +38,8 @@ const settingsTabLoaders: Record<string, (() => Promise<unknown>)[]> = {
38
38
  () => import("../settings/shadow-claw-accounts/shadow-claw-accounts.js"),
39
39
  () =>
40
40
  import("../settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.js"),
41
+ () =>
42
+ import("../settings/shadow-claw-control-plane/shadow-claw-control-plane.js"),
41
43
  () =>
42
44
  import("../settings/shadow-claw-integrations/shadow-claw-integrations.js"),
43
45
  () =>