@vantran-se/openchamber-web 1.23.0 → 1.24.3

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 (362) hide show
  1. package/bin/cli.test.js +29 -6
  2. package/bin/lib/cli-http.js +5 -1
  3. package/bin/lib/commands-status.js +5 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-DFg2wnpo.js → CodeMirrorCommentWidgets-BjJHrzmU.js} +1 -1
  5. package/dist/assets/DiffView-COVQhWGd.js +4 -0
  6. package/dist/assets/FilesView-B1V5JxFX.js +13 -0
  7. package/dist/assets/GitView-CDOJy5Ay.js +6 -0
  8. package/dist/assets/GuestAttachDialog-D3S0Rjic.js +1 -0
  9. package/dist/assets/{JsonTreeView-BKdsjg5B.js → JsonTreeView-D0C9nU7K.js} +1 -1
  10. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  12. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  30. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  33. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  39. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  42. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  54. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  57. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  60. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  62. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  65. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  68. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  69. package/dist/assets/LinearIssuesView-D7FKg1XJ.js +1 -0
  70. package/dist/assets/{MarkdownImageGallery-_5gEVr0X.js → MarkdownImageGallery-BrFjAVEf.js} +1 -1
  71. package/dist/assets/MarkdownRendererImpl-DWepTafh.js +12 -0
  72. package/dist/assets/{OnboardingScreen-DdP4_nuc.js → OnboardingScreen-Dyxxiqdt.js} +1 -1
  73. package/dist/assets/{PatchDiff-am3SAss2.js → PatchDiff-DnEhjXcC.js} +1 -1
  74. package/dist/assets/PierreDiffViewer-DyxZVVVR.js +210 -0
  75. package/dist/assets/{PlanView-BoJxdRZe.js → PlanView-LW2z_EAP.js} +4 -4
  76. package/dist/assets/PluginPane-B4tKno2h.js +3 -0
  77. package/dist/assets/SettingsView-DEaMulN9.js +29 -0
  78. package/dist/assets/{SettingsWindow-DVTdDSol.js → SettingsWindow-FLDJXOgQ.js} +1 -1
  79. package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +1 -0
  80. package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +15 -0
  81. package/dist/assets/{ToolPartDiffPreview-C7WLk5i9.js → ToolPartDiffPreview-CkR4msKP.js} +2 -2
  82. package/dist/assets/{VirtualizedFileDiff-DcdgkH-x.js → VirtualizedFileDiff-0pyr_mHY.js} +4 -4
  83. package/dist/assets/WalkthroughView-yNj4nOP5.js +4 -0
  84. package/dist/assets/appThemeRegistry-BuUKhbYQ.js +1 -0
  85. package/dist/assets/commitAndPush-DvJKa3Ks.js +1 -0
  86. package/dist/assets/{computeVirtualFileMetrics-C8byMnRh.js → computeVirtualFileMetrics-DxTBMuu-.js} +2 -2
  87. package/dist/assets/de-DgxgFyGG.js +10 -0
  88. package/dist/assets/es-BFsZj5F_.js +16 -0
  89. package/dist/assets/fr-jdZ5KKQi.js +14 -0
  90. package/dist/assets/{index-BB7ryz8y.js → index-CKHjPKbq.js} +1 -1
  91. package/dist/assets/index-Cbci4i1W.js +40 -0
  92. package/dist/assets/index-DqaMWNrC.css +1 -0
  93. package/dist/assets/{isDiffPlainText-45ujMPGb.js → isDiffPlainText-D7FkBMP6.js} +1 -1
  94. package/dist/assets/{isFilePlainText-Cc4KFSYi.js → isFilePlainText-CL2vsrIZ.js} +1 -1
  95. package/dist/assets/ja-C5kKrhO4.js +16 -0
  96. package/dist/assets/ko-DXDTT_0O.js +16 -0
  97. package/dist/assets/main-CZALtVek.js +896 -0
  98. package/dist/assets/main-CgXMBNvG.js +2 -0
  99. package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
  100. package/dist/assets/markdownCore-Dh7WEsdM.js +5 -0
  101. package/dist/assets/miniChat-CwTmDixi.js +2 -0
  102. package/dist/assets/mobile-CLf2vCtO.js +2 -0
  103. package/dist/assets/patchFileDiff-Bsba8KFp.js +13 -0
  104. package/dist/assets/pl-D-7x4Y8Q.js +16 -0
  105. package/dist/assets/pt-BR-CSBZrHa8.js +16 -0
  106. package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +2 -0
  107. package/dist/assets/renderMobileApp-V4hYd32H.js +25 -0
  108. package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +25 -0
  109. package/dist/assets/scheduledTasksApi-DzjbLd8P.js +1 -0
  110. package/dist/assets/shikiHighlight-8_3csmr6.js +1 -0
  111. package/dist/assets/surface-B6n7wJtt.js +1 -0
  112. package/dist/assets/textMateThemeFromAppTheme-CHo2fCgW.js +1 -0
  113. package/dist/assets/tr-Dr-oJ22L.js +7 -0
  114. package/dist/assets/uk-9Nx83UcI.js +16 -0
  115. package/dist/assets/useAppFontEffects-khZXpFL_.js +7333 -0
  116. package/dist/assets/useInlineCommentController-DkQ_gqpn.js +2 -0
  117. package/dist/assets/useRootScrollLock-DdvHf9Pt.js +2 -0
  118. package/dist/assets/{vendor-base-ui-DdV8vhHn.js → vendor-base-ui-CsXYnR5c.js} +1 -1
  119. package/dist/assets/{vendor-codemirror-language-data-DzlSIu_W.js → vendor-codemirror-language-data-CdzUmUyZ.js} +2 -2
  120. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
  121. package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
  122. package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
  123. package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
  124. package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
  125. package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
  126. package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
  127. package/dist/assets/vendor-zod-CgAdXWZO.js +39 -0
  128. package/dist/assets/{web-components-CdGfR2XJ.js → web-components-B8IcVQAA.js} +2 -2
  129. package/dist/assets/zh-CN-Bxy_Hj3r.js +16 -0
  130. package/dist/assets/zh-TW-0BB4gpHM.js +16 -0
  131. package/dist/index.html +6 -4
  132. package/dist/mini-chat.html +6 -4
  133. package/dist/mobile.html +6 -4
  134. package/package.json +12 -9
  135. package/server/built-in-extensions/registry.json +4 -0
  136. package/server/index.d.ts +2 -0
  137. package/server/index.js +47 -23
  138. package/server/lib/agent-memory/runtime.js +7 -2
  139. package/server/lib/agent-memory/runtime.test.js +22 -0
  140. package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
  141. package/server/lib/agent-tool/runtime.js +62 -15
  142. package/server/lib/agent-tool/runtime.test.js +88 -2
  143. package/server/lib/changelog/update-notes.js +2 -1
  144. package/server/lib/changelog/update-notes.test.js +19 -0
  145. package/server/lib/event-stream/DOCUMENTATION.md +26 -0
  146. package/server/lib/event-stream/delta-coalescer.js +0 -0
  147. package/server/lib/event-stream/delta-coalescer.test.js +292 -0
  148. package/server/lib/event-stream/global-hub.js +68 -13
  149. package/server/lib/event-stream/global-hub.test.js +182 -0
  150. package/server/lib/event-stream/global-ws-bridge.js +16 -18
  151. package/server/lib/event-stream/index.js +4 -0
  152. package/server/lib/event-stream/protocol.js +21 -7
  153. package/server/lib/event-stream/runtime.js +9 -6
  154. package/server/lib/event-stream/runtime.test.js +60 -0
  155. package/server/lib/fs/DOCUMENTATION.md +10 -1
  156. package/server/lib/fs/git-process.test.js +77 -0
  157. package/server/lib/fs/routes.js +58 -5
  158. package/server/lib/fs/routes.test.js +116 -0
  159. package/server/lib/fs/search.js +2 -1
  160. package/server/lib/git/DOCUMENTATION.md +30 -6
  161. package/server/lib/git/routes.js +37 -11
  162. package/server/lib/git/routes.test.js +78 -0
  163. package/server/lib/git/serial-refresh.js +93 -0
  164. package/server/lib/git/serial-refresh.test.js +112 -0
  165. package/server/lib/git/service.js +588 -74
  166. package/server/lib/git/service.test.js +492 -0
  167. package/server/lib/github/DOCUMENTATION.md +5 -0
  168. package/server/lib/github/pr-status.js +5 -5
  169. package/server/lib/github/pr-status.test.js +1 -1
  170. package/server/lib/github/routes.js +6 -6
  171. package/server/lib/guests/DOCUMENTATION.md +68 -0
  172. package/server/lib/guests/auth-store.js +184 -0
  173. package/server/lib/guests/auth-store.test.js +112 -0
  174. package/server/lib/guests/background.test.js +83 -0
  175. package/server/lib/guests/builtins.js +34 -0
  176. package/server/lib/guests/builtins.test.js +174 -0
  177. package/server/lib/guests/catalog.js +477 -0
  178. package/server/lib/guests/catalog.test.js +390 -0
  179. package/server/lib/guests/clone.js +297 -0
  180. package/server/lib/guests/extract-zip.js +136 -0
  181. package/server/lib/guests/extract-zip.test.js +109 -0
  182. package/server/lib/guests/files.js +308 -0
  183. package/server/lib/guests/files.test.js +262 -0
  184. package/server/lib/guests/grant-scope.js +81 -0
  185. package/server/lib/guests/grant-scope.test.js +75 -0
  186. package/server/lib/guests/host-session.js +73 -0
  187. package/server/lib/guests/host-session.test.js +30 -0
  188. package/server/lib/guests/html-styles.js +6 -0
  189. package/server/lib/guests/html-styles.test.js +45 -0
  190. package/server/lib/guests/html-tokens.js +73 -0
  191. package/server/lib/guests/html-tokens.test.js +53 -0
  192. package/server/lib/guests/install.js +406 -0
  193. package/server/lib/guests/install.test.js +338 -0
  194. package/server/lib/guests/oauth.js +501 -0
  195. package/server/lib/guests/oauth.test.js +481 -0
  196. package/server/lib/guests/persist.js +336 -0
  197. package/server/lib/guests/persist.test.js +288 -0
  198. package/server/lib/guests/request.js +112 -0
  199. package/server/lib/guests/request.test.js +206 -0
  200. package/server/lib/guests/routes.js +838 -0
  201. package/server/lib/guests/service.js +538 -0
  202. package/server/lib/guests/service.test.js +277 -0
  203. package/server/lib/guests/sockets.js +129 -0
  204. package/server/lib/guests/sockets.test.js +110 -0
  205. package/server/lib/guests/ssh-install.test.js +188 -0
  206. package/server/lib/guests/storage.js +68 -0
  207. package/server/lib/guests/storage.test.js +59 -0
  208. package/server/lib/guests/updates.js +294 -0
  209. package/server/lib/guests/updates.test.js +334 -0
  210. package/server/lib/guests/upload.js +50 -0
  211. package/server/lib/guests/upload.test.js +131 -0
  212. package/server/lib/linear/DOCUMENTATION.md +1 -1
  213. package/server/lib/message-queue/DOCUMENTATION.md +17 -1
  214. package/server/lib/message-queue/runtime.js +34 -1
  215. package/server/lib/message-queue/runtime.test.js +83 -6
  216. package/server/lib/notifications/DOCUMENTATION.md +1 -1
  217. package/server/lib/notifications/emitter-runtime.js +4 -3
  218. package/server/lib/notifications/routes.js +5 -0
  219. package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
  220. package/server/lib/openchamber-sessions/routes.js +62 -29
  221. package/server/lib/openchamber-sessions/routes.test.js +96 -0
  222. package/server/lib/opencode/DOCUMENTATION.md +73 -5
  223. package/server/lib/opencode/bootstrap-runtime.js +1 -0
  224. package/server/lib/opencode/core-routes.js +39 -1
  225. package/server/lib/opencode/env-runtime.js +5 -1
  226. package/server/lib/opencode/env-runtime.test.js +72 -0
  227. package/server/lib/opencode/feature-routes-runtime.js +30 -1
  228. package/server/lib/opencode/lifecycle-process.test.js +84 -0
  229. package/server/lib/opencode/lifecycle.js +58 -70
  230. package/server/lib/opencode/lifecycle.test.js +29 -0
  231. package/server/lib/opencode/managed-plugin-config.js +1 -1
  232. package/server/lib/opencode/openchamber-routes.js +72 -14
  233. package/server/lib/opencode/openchamber-routes.test.js +71 -3
  234. package/server/lib/opencode/proxy.js +33 -0
  235. package/server/lib/opencode/session-runtime.js +68 -0
  236. package/server/lib/opencode/session-runtime.test.js +43 -0
  237. package/server/lib/opencode/settings-helpers.js +11 -0
  238. package/server/lib/opencode/settings-helpers.test.js +24 -2
  239. package/server/lib/opencode/settings-normalization-runtime.js +2 -0
  240. package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
  241. package/server/lib/opencode/settings-registry.json +18 -0
  242. package/server/lib/opencode/settings-runtime.js +36 -9
  243. package/server/lib/opencode/settings-runtime.test.js +158 -2
  244. package/server/lib/opencode/shutdown-runtime.js +3 -0
  245. package/server/lib/opencode/shutdown-runtime.test.js +24 -0
  246. package/server/lib/opencode/theme-archive.js +53 -0
  247. package/server/lib/opencode/theme-catalog.js +171 -0
  248. package/server/lib/opencode/theme-catalog.test.js +125 -0
  249. package/server/lib/opencode/theme-import.test.js +144 -0
  250. package/server/lib/opencode/theme-runtime.js +100 -28
  251. package/server/lib/opencode/theme-runtime.test.js +15 -0
  252. package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
  253. package/server/lib/permission-auto-accept/runtime.js +13 -0
  254. package/server/lib/permission-auto-accept/runtime.test.js +37 -1
  255. package/server/lib/project-context/DOCUMENTATION.md +13 -3
  256. package/server/lib/project-context/runtime.js +8 -2
  257. package/server/lib/project-context/runtime.test.js +54 -0
  258. package/server/lib/projects/DOCUMENTATION.md +2 -1
  259. package/server/lib/projects/project-config.js +40 -11
  260. package/server/lib/projects/project-config.test.js +97 -0
  261. package/server/lib/projects/project-id.js +26 -0
  262. package/server/lib/quota/DOCUMENTATION.md +0 -1
  263. package/server/lib/quota/providers/index.js +0 -7
  264. package/server/lib/realtime-proxy-terminal.test.js +150 -0
  265. package/server/lib/realtime-proxy.js +2 -0
  266. package/server/lib/relay/DOCUMENTATION.md +20 -0
  267. package/server/lib/relay/host-client.js +7 -0
  268. package/server/lib/relay/host-client.test.js +14 -0
  269. package/server/lib/routing/DOCUMENTATION.md +83 -0
  270. package/server/lib/routing/defaults.js +98 -0
  271. package/server/lib/routing/feature-flag.js +11 -0
  272. package/server/lib/routing/history.js +34 -0
  273. package/server/lib/routing/jev.js +98 -0
  274. package/server/lib/routing/routes.http.test.js +100 -0
  275. package/server/lib/routing/routes.js +81 -0
  276. package/server/lib/routing/runtime.js +217 -0
  277. package/server/lib/routing/runtime.test.js +188 -0
  278. package/server/lib/routing/store.js +249 -0
  279. package/server/lib/routing/store.test.js +96 -0
  280. package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
  281. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
  282. package/server/lib/scheduled-tasks/runtime.js +14 -4
  283. package/server/lib/scheduled-tasks/runtime.test.js +67 -0
  284. package/server/lib/security/request-security.js +9 -5
  285. package/server/lib/security/request-security.test.js +37 -0
  286. package/server/lib/session-assist/DOCUMENTATION.md +131 -66
  287. package/server/lib/session-assist/context.js +127 -0
  288. package/server/lib/session-assist/context.test.js +132 -0
  289. package/server/lib/session-assist/prompt.js +54 -0
  290. package/server/lib/session-assist/runtime.js +111 -257
  291. package/server/lib/session-assist/runtime.test.js +203 -0
  292. package/server/lib/small-model/DOCUMENTATION.md +14 -2
  293. package/server/lib/small-model/call.js +37 -6
  294. package/server/lib/small-model/call.test.js +182 -0
  295. package/server/lib/terminal/DOCUMENTATION.md +23 -2
  296. package/server/lib/terminal/runtime.js +27 -5
  297. package/server/lib/terminal/runtime.test.js +67 -0
  298. package/server/lib/terminal/shutdown.js +106 -0
  299. package/server/lib/terminal/shutdown.test.js +84 -0
  300. package/server/lib/tts/service.test.js +78 -0
  301. package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
  302. package/server/lib/ui-auth/session-cookie.js +51 -0
  303. package/server/lib/ui-auth/session-cookie.test.js +46 -0
  304. package/server/lib/ui-auth/ui-auth.js +56 -17
  305. package/server/lib/ui-auth/ui-auth.test.js +96 -0
  306. package/server/lib/walkthrough/DOCUMENTATION.md +15 -5
  307. package/server/lib/walkthrough/pr-source.test.js +21 -0
  308. package/server/lib/walkthrough/pull-request.js +7 -4
  309. package/server/lib/walkthrough/pull-request.test.js +16 -0
  310. package/server/lib/walkthrough/routes.js +19 -0
  311. package/server/lib/walkthrough/routes.test.js +26 -0
  312. package/server/lib/walkthrough/sources.js +13 -2
  313. package/server/opencode-worktree-readiness.test.js +130 -0
  314. package/dist/assets/DiffView-3Dn4QHbu.js +0 -4
  315. package/dist/assets/DirtyBranchSwitchDialog-CwOerqVi.js +0 -1
  316. package/dist/assets/FilesView-BdTOpew9.js +0 -17
  317. package/dist/assets/GitView-Dqf9vHJh.js +0 -6
  318. package/dist/assets/LinearIssuesView-CmbeL7I6.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-wVZlx4dd.js +0 -12
  320. package/dist/assets/PierreDiffViewer-DCsydlKs.js +0 -161
  321. package/dist/assets/SettingsView-CcM8pF5O.js +0 -28
  322. package/dist/assets/ToolOutputDialog-BgfTbDZ4.js +0 -15
  323. package/dist/assets/WalkthroughView-BEVUDxuw.js +0 -4
  324. package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
  325. package/dist/assets/de-C8jV7J9y.js +0 -10
  326. package/dist/assets/es-BI6J1M5R.js +0 -16
  327. package/dist/assets/fr-D6oDFt7E.js +0 -14
  328. package/dist/assets/index-BqhiCmYS.js +0 -39
  329. package/dist/assets/index-FqSOBz0J.css +0 -1
  330. package/dist/assets/ja-C_4i5EQu.js +0 -16
  331. package/dist/assets/ko-BFbOTpbq.js +0 -16
  332. package/dist/assets/main-C9ETLKFc.js +0 -2
  333. package/dist/assets/main-CJmb2dF8.js +0 -930
  334. package/dist/assets/markdownCore-DVuZpnli.js +0 -5
  335. package/dist/assets/miniChat-B4Ytoq2n.js +0 -2
  336. package/dist/assets/mobile-BUT6-lo1.js +0 -2
  337. package/dist/assets/patchFileDiff-8j1apICU.js +0 -13
  338. package/dist/assets/pl-COFvT56l.js +0 -16
  339. package/dist/assets/pt-BR-DtVOO2Lj.js +0 -16
  340. package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
  341. package/dist/assets/renderElectronMiniChatApp-B6vUG-vF.js +0 -2
  342. package/dist/assets/renderMobileApp-BNgHraq6.js +0 -25
  343. package/dist/assets/runtimeEndpointReset-B8LYpWvr.js +0 -17
  344. package/dist/assets/scheduledTasksApi-GC4epIYA.js +0 -1
  345. package/dist/assets/shikiHighlight-BJISeZsQ.js +0 -1
  346. package/dist/assets/surface-BvRzS58U.js +0 -1
  347. package/dist/assets/tr-B8Q0Go2l.js +0 -7
  348. package/dist/assets/uk-Dk4a-Fn-.js +0 -16
  349. package/dist/assets/useAppFontEffects-DpFNefUA.js +0 -7284
  350. package/dist/assets/useGitComparison-Dfxaqd0s.js +0 -1
  351. package/dist/assets/useInlineCommentController-CscsE4t-.js +0 -2
  352. package/dist/assets/useRootScrollLock-BaCvuRlT.js +0 -1
  353. package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
  354. package/dist/assets/vendor-zod-BPAANqvH.js +0 -39
  355. package/dist/assets/zh-CN-B_BXxG1Q.js +0 -16
  356. package/dist/assets/zh-TW-DLrl1SPZ.js +0 -16
  357. package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
  358. package/server/lib/mcp-reconnect/runtime.js +0 -122
  359. package/server/lib/mcp-reconnect/runtime.test.js +0 -140
  360. package/server/lib/quota/providers/crof.js +0 -96
  361. package/server/lib/quota/providers/crof.test.js +0 -98
  362. /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
@@ -1,4 +1,4 @@
1
- import { createProjectIdFromPath } from '../projects/project-id.js';
1
+ import { createProjectIdFromPath, projectConfigFileStemOf } from '../projects/project-id.js';
2
2
  import {
3
3
  buildPreferencesFields,
4
4
  flattenPreferences,
@@ -288,10 +288,10 @@ export const createSettingsRuntime = (deps) => {
288
288
  return;
289
289
  }
290
290
 
291
- const oldConfigPath = path.join(PROJECTS_ROOT_DIR, `${oldId}.json`);
292
- const newConfigPath = path.join(PROJECTS_ROOT_DIR, `${newId}.json`);
293
- const oldStorageDir = path.join(PROJECTS_ROOT_DIR, oldId);
294
- const newStorageDir = path.join(PROJECTS_ROOT_DIR, newId);
291
+ const oldConfigPath = path.join(PROJECTS_ROOT_DIR, `${projectConfigFileStemOf(oldId)}.json`);
292
+ const newConfigPath = path.join(PROJECTS_ROOT_DIR, `${projectConfigFileStemOf(newId)}.json`);
293
+ const oldStorageDir = path.join(PROJECTS_ROOT_DIR, projectConfigFileStemOf(oldId));
294
+ const newStorageDir = path.join(PROJECTS_ROOT_DIR, projectConfigFileStemOf(newId));
295
295
 
296
296
  const [oldConfig, newConfig] = await Promise.all([
297
297
  readJsonFile(oldConfigPath),
@@ -308,6 +308,28 @@ export const createSettingsRuntime = (deps) => {
308
308
  await fsPromises.rm(oldConfigPath, { force: true });
309
309
  };
310
310
 
311
+ /**
312
+ * A build before the bounded folder name created `<projectId>/` for an id
313
+ * the filesystem still accepted (201 to 255 characters; longer ids never
314
+ * got a folder). Only the bounded folder is read now, so that folder's
315
+ * notes, plans, and memory are moved over once. A raw name the filesystem
316
+ * cannot hold (ENAMETOOLONG) means no such folder ever existed.
317
+ */
318
+ const migrateRawIdStorageFolder = async (projectId) => {
319
+ const stem = projectConfigFileStemOf(projectId);
320
+ if (stem === projectId) return;
321
+ const rawStorageDir = path.join(PROJECTS_ROOT_DIR, projectId);
322
+ try {
323
+ await fsPromises.stat(rawStorageDir);
324
+ } catch (error) {
325
+ if (error?.code === 'ENOENT' || error?.code === 'ENAMETOOLONG') return;
326
+ throw error;
327
+ }
328
+ const boundedStorageDir = path.join(PROJECTS_ROOT_DIR, stem);
329
+ await mergeProjectContextFiles(rawStorageDir, boundedStorageDir);
330
+ await moveDirectoryContents(rawStorageDir, boundedStorageDir);
331
+ };
332
+
311
333
  const migrateSettingsToDeterministicProjectIds = async (current) => {
312
334
  const settings = current && typeof current === 'object' ? current : {};
313
335
  const projects = sanitizeProjects(settings.projects) || [];
@@ -328,6 +350,7 @@ export const createSettingsRuntime = (deps) => {
328
350
  await migrateProjectScopedStorage({ oldId: project.id, newId: nextId, projectPath: project.path });
329
351
  await migrateProjectIconFiles({ oldId: project.id, newId: nextId });
330
352
  }
353
+ await migrateRawIdStorageFolder(nextId);
331
354
  nextProjects.push({ ...project, id: nextId });
332
355
  }
333
356
 
@@ -468,19 +491,23 @@ export const createSettingsRuntime = (deps) => {
468
491
  for (const [projectId, orphansForProject] of matches.entries()) {
469
492
  const project = canonicalProjects.find((p) => p.id === projectId);
470
493
  if (!project) continue;
471
- const targetPath = path.join(PROJECTS_ROOT_DIR, `${project.id}.json`);
494
+ const targetStem = projectConfigFileStemOf(project.id);
495
+ const targetPath = path.join(PROJECTS_ROOT_DIR, `${targetStem}.json`);
496
+ const targetStorageDir = path.join(PROJECTS_ROOT_DIR, targetStem);
472
497
 
473
498
  for (const orphan of orphansForProject) {
474
499
  const targetExisting = (await readJsonFile(targetPath)) || {};
500
+ // An orphan is named by the file found on disk, so its folder is the raw name.
501
+ const orphanStorageDir = path.join(PROJECTS_ROOT_DIR, orphan.orphanId);
475
502
  const merged = mergeProjectConfigData({
476
503
  oldConfig: orphan.content,
477
504
  newConfig: targetExisting,
478
- oldStorageDir: path.join(PROJECTS_ROOT_DIR, orphan.orphanId),
479
- newStorageDir: path.join(PROJECTS_ROOT_DIR, project.id),
505
+ oldStorageDir: orphanStorageDir,
506
+ newStorageDir: targetStorageDir,
480
507
  projectPath: project.path,
481
508
  });
482
509
  await writeJsonFile(targetPath, merged);
483
- await moveDirectoryContents(path.join(PROJECTS_ROOT_DIR, orphan.orphanId), path.join(PROJECTS_ROOT_DIR, project.id));
510
+ await moveDirectoryContents(orphanStorageDir, targetStorageDir);
484
511
  await fsPromises.rm(orphan.filePath, { force: true });
485
512
  orphansConsumed.add(orphan.orphanId);
486
513
  console.log(`[projects] Recovered orphan ${orphan.orphanId} -> ${project.id} (${project.path})`);
@@ -1,9 +1,9 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { describe, expect, it, vi } from 'vitest';
2
2
  import crypto from 'crypto';
3
3
  import fsPromises from 'fs/promises';
4
4
  import os from 'os';
5
5
  import path from 'path';
6
- import { createProjectIdFromPath } from '../projects/project-id.js';
6
+ import { createProjectIdFromPath, projectConfigFileStemOf } from '../projects/project-id.js';
7
7
  import { createSettingsRuntime } from './settings-runtime.js';
8
8
 
9
9
  const createRuntime = async ({ mergePersistedSettings = (_current, changes) => changes } = {}) => {
@@ -39,6 +39,20 @@ const createRuntime = async ({ mergePersistedSettings = (_current, changes) => c
39
39
  };
40
40
 
41
41
  describe('settings runtime', () => {
42
+ it('round-trips both archived-only retention states through instance settings', async () => {
43
+ const { runtime, settingsFilePath, cleanup } = await createRuntime();
44
+ try {
45
+ for (const sessionRetentionOnlyArchived of [true, false]) {
46
+ const settings = { sessionRetentionOnlyArchived, sessionRetentionAction: 'delete', autoDeleteAfterDays: 30 };
47
+ await runtime.persistSettings(settings);
48
+ expect(await runtime.readSettingsFromDisk()).toEqual(settings);
49
+ expect(JSON.parse(await fsPromises.readFile(settingsFilePath, 'utf8'))).toEqual(settings);
50
+ }
51
+ } finally {
52
+ await cleanup();
53
+ }
54
+ });
55
+
42
56
  it('uses OpenChamber themes when a new install has no theme preferences', async () => {
43
57
  const { runtime, cleanup } = await createRuntime();
44
58
  try {
@@ -145,6 +159,52 @@ describe('settings runtime', () => {
145
159
  }
146
160
  });
147
161
 
162
+ it('migrates a legacy id into the bounded file of a project whose path is too long for a file name', async () => {
163
+ const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
164
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
165
+ try {
166
+ const projectPath = path.join(tempRoot, 'a'.repeat(160));
167
+ const oldProjectId = 'legacy-project-id';
168
+ const newProjectId = createProjectIdFromPath(projectPath);
169
+ expect(newProjectId.length).toBeGreaterThan(200);
170
+ const projectsRoot = path.join(path.dirname(settingsFilePath), 'projects');
171
+ const boundedPath = path.join(projectsRoot, `${projectConfigFileStemOf(newProjectId)}.json`);
172
+ expect(path.basename(boundedPath).startsWith('path_sha256_')).toBe(true);
173
+
174
+ await fsPromises.mkdir(projectPath, { recursive: true });
175
+ await fsPromises.mkdir(projectsRoot, { recursive: true });
176
+ await fsPromises.writeFile(
177
+ settingsFilePath,
178
+ JSON.stringify({
179
+ projects: [{ id: oldProjectId, path: projectPath, addedAt: 1, lastOpenedAt: 1 }],
180
+ activeProjectId: oldProjectId,
181
+ }, null, 2),
182
+ 'utf8',
183
+ );
184
+ await fsPromises.writeFile(
185
+ path.join(projectsRoot, `${oldProjectId}.json`),
186
+ JSON.stringify({ 'setup-worktree': ['bun install'] }, null, 2),
187
+ 'utf8',
188
+ );
189
+
190
+ const settings = await runtime.readSettingsFromDiskMigrated();
191
+
192
+ expect(settings.projects[0].id).toBe(newProjectId);
193
+ const migratedConfig = JSON.parse(await fsPromises.readFile(boundedPath, 'utf8'));
194
+ expect(migratedConfig['setup-worktree']).toEqual(['bun install']);
195
+ await expect(fsPromises.readFile(path.join(projectsRoot, `${oldProjectId}.json`), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' });
196
+
197
+ // A second pass sees the bounded file beside the settings; it is the
198
+ // project's own file, not a stray from the random-id era.
199
+ await runtime.readSettingsFromDiskMigrated();
200
+ expect(JSON.parse(await fsPromises.readFile(boundedPath, 'utf8'))['setup-worktree']).toEqual(['bun install']);
201
+ expect(warn.mock.calls.some((call) => String(call[0]).includes('orphan'))).toBe(false);
202
+ } finally {
203
+ warn.mockRestore();
204
+ await cleanup();
205
+ }
206
+ });
207
+
148
208
  it.skipIf(process.platform !== 'win32')('falls back when Windows blocks atomic settings replacement', async () => {
149
209
  const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-settings-runtime-'));
150
210
  const settingsFilePath = path.join(tempRoot, 'settings.json');
@@ -184,6 +244,102 @@ describe('settings runtime', () => {
184
244
  }
185
245
  });
186
246
 
247
+ it('moves project storage into the bounded folder when the canonical id is too long for a folder name', async () => {
248
+ const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
249
+ try {
250
+ const projectPath = path.join(tempRoot, `${'segment-'.repeat(18)}`, 'demo-repo');
251
+ const oldProjectId = 'legacy-project-id';
252
+ const newProjectId = createProjectIdFromPath(projectPath);
253
+ const stem = projectConfigFileStemOf(newProjectId);
254
+ expect(newProjectId.length).toBeGreaterThan(240);
255
+ expect(stem).not.toBe(newProjectId);
256
+ const projectsRoot = path.join(path.dirname(settingsFilePath), 'projects');
257
+ const oldStorageDir = path.join(projectsRoot, oldProjectId);
258
+ const newStorageDir = path.join(projectsRoot, stem);
259
+
260
+ await fsPromises.mkdir(projectPath, { recursive: true });
261
+ await fsPromises.mkdir(path.join(oldStorageDir, 'plans'), { recursive: true });
262
+ await fsPromises.writeFile(path.join(oldStorageDir, 'context.json'), JSON.stringify({ version: 2, notes: [], todos: [{ id: 't1', text: 'keep', completed: false, createdAt: 1 }], plans: [] }), 'utf8');
263
+ await fsPromises.writeFile(path.join(oldStorageDir, 'plans', 'inside.md'), '# Inside\n', 'utf8');
264
+ await fsPromises.writeFile(path.join(oldStorageDir, 'memory.json'), JSON.stringify({ version: 1, entries: [] }), 'utf8');
265
+ await fsPromises.writeFile(
266
+ settingsFilePath,
267
+ JSON.stringify({
268
+ projects: [{ id: oldProjectId, path: projectPath, addedAt: 1, lastOpenedAt: 1 }],
269
+ activeProjectId: oldProjectId,
270
+ }, null, 2),
271
+ 'utf8',
272
+ );
273
+ await fsPromises.writeFile(
274
+ path.join(projectsRoot, `${oldProjectId}.json`),
275
+ JSON.stringify({ projectPlanFiles: [{ id: 'inside', path: path.join(oldStorageDir, 'plans', 'inside.md') }] }, null, 2),
276
+ 'utf8',
277
+ );
278
+
279
+ const settings = await runtime.readSettingsFromDiskMigrated();
280
+
281
+ expect(settings.projects.map((project) => project.id)).toEqual([newProjectId]);
282
+ const migratedConfig = JSON.parse(await fsPromises.readFile(path.join(projectsRoot, `${stem}.json`), 'utf8'));
283
+ expect(migratedConfig.projectPlanFiles).toEqual([{ id: 'inside', path: path.join(newStorageDir, 'plans', 'inside.md') }]);
284
+ expect(JSON.parse(await fsPromises.readFile(path.join(newStorageDir, 'context.json'), 'utf8')).todos).toHaveLength(1);
285
+ await fsPromises.access(path.join(newStorageDir, 'plans', 'inside.md'));
286
+ await fsPromises.access(path.join(newStorageDir, 'memory.json'));
287
+ expect((await fsPromises.readdir(projectsRoot)).sort()).toEqual([stem, `${stem}.json`]);
288
+ } finally {
289
+ await cleanup();
290
+ }
291
+ });
292
+
293
+ it('moves a folder an older build created under a raw id that still fit a folder name into the bounded folder', async () => {
294
+ const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
295
+ try {
296
+ // Long enough for the bounded name, short enough that an older build
297
+ // could still have created `<id>/` on this filesystem. The temp root
298
+ // differs per OS (a few characters on Linux runners, dozens on macOS),
299
+ // so pad to a fixed path length instead of a fixed segment.
300
+ const projectPathLength = 170;
301
+ const segment = 'x'.repeat(Math.max(1, projectPathLength - path.join(tempRoot, 'demo-repo').length - 1));
302
+ const projectPath = path.join(tempRoot, segment, 'demo-repo');
303
+ const projectId = createProjectIdFromPath(projectPath);
304
+ const stem = projectConfigFileStemOf(projectId);
305
+ expect(projectId.length).toBeGreaterThan(200);
306
+ expect(projectId.length).toBeLessThanOrEqual(255);
307
+ expect(stem).not.toBe(projectId);
308
+ const projectsRoot = path.join(path.dirname(settingsFilePath), 'projects');
309
+ const rawStorageDir = path.join(projectsRoot, projectId);
310
+ const boundedStorageDir = path.join(projectsRoot, stem);
311
+
312
+ await fsPromises.mkdir(projectPath, { recursive: true });
313
+ await fsPromises.mkdir(path.join(rawStorageDir, 'plans'), { recursive: true });
314
+ await fsPromises.writeFile(path.join(rawStorageDir, 'context.json'), JSON.stringify({ version: 2, notes: [{ id: 'n1', body: 'old note', createdAt: 1, updatedAt: 1 }], todos: [], plans: [] }), 'utf8');
315
+ await fsPromises.writeFile(path.join(rawStorageDir, 'plans', 'old.md'), '# Old\n', 'utf8');
316
+ await fsPromises.writeFile(path.join(rawStorageDir, 'memory.json'), JSON.stringify({ version: 1, entries: [{ id: 'm1', title: 'Old memory', body: 'x', type: 'fact', createdAt: 1, updatedAt: 1 }] }), 'utf8');
317
+ // The bounded folder already holds newer context: both sides survive.
318
+ await fsPromises.mkdir(boundedStorageDir, { recursive: true });
319
+ await fsPromises.writeFile(path.join(boundedStorageDir, 'context.json'), JSON.stringify({ version: 2, notes: [{ id: 'n2', body: 'new note', createdAt: 2, updatedAt: 2 }], todos: [], plans: [] }), 'utf8');
320
+ await fsPromises.writeFile(
321
+ settingsFilePath,
322
+ JSON.stringify({ projects: [{ id: projectId, path: projectPath, addedAt: 1, lastOpenedAt: 1 }], activeProjectId: projectId }, null, 2),
323
+ 'utf8',
324
+ );
325
+
326
+ const settings = await runtime.readSettingsFromDiskMigrated();
327
+
328
+ expect(settings.projects.map((project) => project.id)).toEqual([projectId]);
329
+ const context = JSON.parse(await fsPromises.readFile(path.join(boundedStorageDir, 'context.json'), 'utf8'));
330
+ expect(context.notes.map((note) => note.id).sort()).toEqual(['n1', 'n2']);
331
+ await fsPromises.access(path.join(boundedStorageDir, 'plans', 'old.md'));
332
+ expect(JSON.parse(await fsPromises.readFile(path.join(boundedStorageDir, 'memory.json'), 'utf8')).entries).toHaveLength(1);
333
+ await expect(fsPromises.access(rawStorageDir)).rejects.toMatchObject({ code: 'ENOENT' });
334
+
335
+ // A second startup finds nothing left to move.
336
+ await runtime.readSettingsFromDiskMigrated();
337
+ expect(JSON.parse(await fsPromises.readFile(path.join(boundedStorageDir, 'context.json'), 'utf8')).notes).toHaveLength(2);
338
+ } finally {
339
+ await cleanup();
340
+ }
341
+ });
342
+
187
343
  it('cleans up orphaned settings.json.tmp files during startup migration', async () => {
188
344
  const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
189
345
  try {
@@ -108,6 +108,9 @@ export const createGracefulShutdownRuntime = (dependencies) => {
108
108
  console.log('HTTP server closed');
109
109
  resolve();
110
110
  });
111
+ // The backend has stopped. Active SSE/HTTP clients must not keep
112
+ // Desktop waiting for the outer shutdown deadline.
113
+ server.closeAllConnections?.();
111
114
  }),
112
115
  new Promise((resolve) => {
113
116
  closeTimeout = setTimeout(() => {
@@ -1,4 +1,5 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import http from 'node:http';
2
3
 
3
4
  import { createGracefulShutdownRuntime } from './shutdown-runtime.js';
4
5
 
@@ -55,4 +56,27 @@ describe('graceful shutdown runtime', () => {
55
56
  expect(warnSpy).not.toHaveBeenCalledWith('Server close timeout reached, forcing shutdown');
56
57
  expect(vi.getTimerCount()).toBe(0);
57
58
  });
59
+
60
+ it('closes an active HTTP stream instead of waiting for the shutdown deadline', async () => {
61
+ const server = http.createServer((_request, response) => {
62
+ response.writeHead(200, { 'content-type': 'text/event-stream' });
63
+ response.write('data: fixture\n\n');
64
+ });
65
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
66
+ const request = http.get(`http://127.0.0.1:${server.address().port}`);
67
+ request.on('error', () => {});
68
+ const response = await new Promise((resolve) => request.once('response', resolve));
69
+ response.resume();
70
+ const closed = new Promise((resolve) => response.once('close', resolve));
71
+ const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
72
+ try {
73
+ await createRuntime(server).gracefulShutdown({ exitProcess: false });
74
+ expect(warning).not.toHaveBeenCalledWith('Server close timeout reached, forcing shutdown');
75
+ await closed;
76
+ } finally {
77
+ request.destroy();
78
+ server.closeAllConnections();
79
+ await new Promise((resolve) => server.close(resolve));
80
+ }
81
+ });
58
82
  });
@@ -0,0 +1,53 @@
1
+ import { inflateRaw } from 'node:zlib';
2
+ import { promisify } from 'node:util';
3
+
4
+ const inflate = promisify(inflateRaw);
5
+ const MAX_FILE_BYTES = 512 * 1024;
6
+
7
+ // Read only selected JSON entries in memory. Nothing from a VSIX is executed or
8
+ // extracted onto the host filesystem.
9
+ export function openThemeArchive(bytes) {
10
+ let end = bytes.length - 22;
11
+ const lower = Math.max(0, end - 65535);
12
+ while (end >= lower && (bytes.readUInt32LE(end) !== 0x06054b50 || end + 22 + bytes.readUInt16LE(end + 20) !== bytes.length)) end--;
13
+ if (end < lower) throw new Error('Invalid VSIX directory');
14
+ const count = bytes.readUInt16LE(end + 10);
15
+ const directorySize = bytes.readUInt32LE(end + 12);
16
+ let cursor = bytes.readUInt32LE(end + 16);
17
+ if (count > 5000 || bytes.readUInt16LE(end + 4) || bytes.readUInt16LE(end + 6)
18
+ || bytes.readUInt16LE(end + 8) !== count || cursor + directorySize !== end) throw new Error('Unsupported VSIX directory');
19
+ const entries = new Map();
20
+ let totalSize = 0;
21
+ for (let index = 0; index < count; index++) {
22
+ if (cursor + 46 > end || bytes.readUInt32LE(cursor) !== 0x02014b50) throw new Error('Invalid VSIX entry');
23
+ const flags = bytes.readUInt16LE(cursor + 8);
24
+ const method = bytes.readUInt16LE(cursor + 10);
25
+ const compressed = bytes.readUInt32LE(cursor + 20);
26
+ const size = bytes.readUInt32LE(cursor + 24);
27
+ const nameLength = bytes.readUInt16LE(cursor + 28);
28
+ const next = cursor + 46 + nameLength + bytes.readUInt16LE(cursor + 30) + bytes.readUInt16LE(cursor + 32);
29
+ const offset = bytes.readUInt32LE(cursor + 42);
30
+ if (next > end || nameLength > 1024 || flags & 1 || ![0, 8].includes(method)) throw new Error('Unsupported VSIX entry');
31
+ const name = bytes.toString('utf8', cursor + 46, cursor + 46 + nameLength);
32
+ const parts = name.replace(/\/$/, '').split('/');
33
+ if (parts.some((part) => !part || part === '.' || part === '..') || /[\\\0:]/.test(name) || entries.has(name)) throw new Error('Unsafe VSIX path');
34
+ totalSize += size;
35
+ if (totalSize > 100 * 1024 * 1024) throw new Error('VSIX contents too large');
36
+ entries.set(name, { offset, compressed, size, method });
37
+ cursor = next;
38
+ }
39
+ if (cursor !== end) throw new Error('Invalid VSIX directory size');
40
+ return async (name) => {
41
+ const entry = entries.get(name);
42
+ if (!entry || entry.size > MAX_FILE_BYTES) throw new Error('Theme file missing or too large');
43
+ const { offset, compressed, size, method } = entry;
44
+ if (offset + 30 > bytes.length || bytes.readUInt32LE(offset) !== 0x04034b50 || bytes.readUInt16LE(offset + 8) !== method) throw new Error('Invalid VSIX file');
45
+ const nameLength = bytes.readUInt16LE(offset + 26);
46
+ const start = offset + 30 + nameLength + bytes.readUInt16LE(offset + 28);
47
+ if (start + compressed > bytes.length || bytes.toString('utf8', offset + 30, offset + 30 + nameLength) !== name) throw new Error('Invalid VSIX file bounds');
48
+ const data = bytes.subarray(start, start + compressed);
49
+ const result = method === 0 ? data : await inflate(data, { maxOutputLength: MAX_FILE_BYTES });
50
+ if (result.length !== size) throw new Error('Invalid VSIX file size');
51
+ return result.toString('utf8');
52
+ };
53
+ }
@@ -0,0 +1,171 @@
1
+ import path from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
+ import { parse } from 'jsonc-parser';
4
+ import { z } from 'zod';
5
+ import { openThemeArchive } from './theme-archive.js';
6
+
7
+ const identifier = z.string().min(1).max(160).regex(/^[\w-]+$/);
8
+ const version = z.string().min(1).max(80).regex(/^[\w.+-]+$/);
9
+ const settings = z.object({ foreground: z.string().optional() });
10
+ const tokenRules = z.array(z.object({ scope: z.union([z.string(), z.array(z.string())]).optional(), settings }));
11
+ const sourceSchema = z.object({
12
+ name: z.string().optional(), type: z.string().optional(), include: z.string().optional(),
13
+ colors: z.record(z.string(), z.string().nullable()).default({}),
14
+ tokenColors: z.union([z.string(), tokenRules]).optional(),
15
+ semanticHighlighting: z.boolean().optional(),
16
+ semanticTokenColors: z.record(z.string(), z.union([z.string(), settings])).optional(),
17
+ });
18
+ const extensionSchema = z.object({
19
+ namespace: identifier, name: identifier, version,
20
+ displayName: z.string().optional(), files: z.object({ icon: z.string().optional() }),
21
+ });
22
+ const detailSchema = extensionSchema.extend({ files: z.object({ download: z.string(), sha256: z.string() }) });
23
+ const manifestSchema = z.object({
24
+ publisher: identifier, name: identifier, version,
25
+ contributes: z.object({ themes: z.array(z.object({
26
+ label: z.string().min(1).max(160), path: z.string().min(1).max(1024),
27
+ uiTheme: z.enum(['vs', 'vs-dark', 'hc-black', 'hc-light']).optional(),
28
+ })).min(1).max(40) }),
29
+ });
30
+ const querySchema = z.object({ query: z.string().trim().min(1).max(160) });
31
+ const packageSchema = z.object({ namespace: identifier, name: identifier, version });
32
+ const hosts = new Set(['open-vsx.org', 'openvsx.eclipsecontent.org']);
33
+
34
+ function trustedUrl(value) {
35
+ const url = new URL(value);
36
+ if (url.protocol !== 'https:' || !hosts.has(url.hostname) || url.port || url.username || url.password) throw new Error('Unexpected catalog host');
37
+ return url;
38
+ }
39
+
40
+ async function download(url, maxBytes, signal, fetchImpl) {
41
+ for (let redirects = 0; redirects < 4; redirects++) {
42
+ const response = await fetchImpl(trustedUrl(url), { signal, redirect: 'manual' });
43
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
44
+ await response.body?.cancel();
45
+ url = new URL(response.headers.get('location') ?? '', url).href;
46
+ continue;
47
+ }
48
+ if (!response.ok || !response.body || Number(response.headers.get('content-length')) > maxBytes) {
49
+ await response.body?.cancel();
50
+ throw new Error('Catalog request failed');
51
+ }
52
+ const reader = response.body.getReader();
53
+ const chunks = [];
54
+ let length = 0;
55
+ try {
56
+ while (true) {
57
+ const { done, value } = await reader.read();
58
+ if (done) break;
59
+ length += value.byteLength;
60
+ if (length > maxBytes) throw new Error('Catalog response too large');
61
+ chunks.push(value);
62
+ }
63
+ } finally {
64
+ await reader.cancel();
65
+ reader.releaseLock();
66
+ }
67
+ return Buffer.concat(chunks, length);
68
+ }
69
+ throw new Error('Too many catalog redirects');
70
+ }
71
+
72
+ function parseJsonc(text, schema) {
73
+ const errors = [];
74
+ const value = parse(text, errors, { allowTrailingComma: true });
75
+ if (errors.length) throw new Error('Invalid theme JSON');
76
+ return schema.parse(value);
77
+ }
78
+
79
+ function themePath(from, reference) {
80
+ if (/[\\\0:]/.test(reference) || reference.startsWith('/')) throw new Error('Invalid theme reference');
81
+ const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(from), reference));
82
+ if (!resolved.startsWith('extension/')) throw new Error('Theme reference escapes package');
83
+ return resolved;
84
+ }
85
+
86
+ export function createThemeCatalog({ fetchImpl = fetch } = {}) {
87
+ const search = async (input, signal = AbortSignal.timeout(15000)) => {
88
+ const { query } = querySchema.parse(input);
89
+ const url = new URL('https://open-vsx.org/api/-/search');
90
+ url.search = new URLSearchParams({ query, category: 'Themes', size: '24', sortBy: 'relevance' }).toString();
91
+ const bytes = await download(url.href, 1024 * 1024, signal, fetchImpl);
92
+ const result = z.object({ extensions: z.array(extensionSchema).max(24) }).parse(JSON.parse(bytes.toString('utf8')));
93
+ return result.extensions.map((extension) => ({
94
+ namespace: extension.namespace, name: extension.name, version: extension.version,
95
+ label: extension.displayName || extension.name,
96
+ icon: extension.files.icon && URL.canParse(extension.files.icon) && hosts.has(new URL(extension.files.icon).hostname)
97
+ ? trustedUrl(extension.files.icon).href : null,
98
+ }));
99
+ };
100
+
101
+ const readPackage = async (input, signal = AbortSignal.timeout(45000)) => {
102
+ const requested = packageSchema.parse(input);
103
+ const url = `https://open-vsx.org/api/${requested.namespace}/${requested.name}/${requested.version}`;
104
+ const detail = detailSchema.parse(JSON.parse((await download(url, 512 * 1024, signal, fetchImpl)).toString('utf8')));
105
+ const bytes = await download(detail.files.download, 20 * 1024 * 1024, signal, fetchImpl);
106
+ const checksum = (await download(detail.files.sha256, 256, signal, fetchImpl)).toString('utf8').trim().split(/\s+/)[0];
107
+ if (createHash('sha256').update(bytes).digest('hex') !== checksum?.toLowerCase()) throw new Error('VSIX checksum mismatch');
108
+ const read = openThemeArchive(bytes);
109
+ const manifest = parseJsonc(await read('extension/package.json'), manifestSchema);
110
+ if (manifest.publisher.toLowerCase() !== requested.namespace.toLowerCase() || manifest.name !== requested.name || manifest.version !== requested.version) throw new Error('VSIX identity mismatch');
111
+ let reads = 0;
112
+ const load = async (file, ancestors = []) => {
113
+ signal.throwIfAborted();
114
+ if (ancestors.includes(file) || ancestors.length >= 8 || ++reads > 320) throw new Error('Too many theme references');
115
+ const source = parseJsonc(await read(file), sourceSchema);
116
+ const parent = source.include ? await load(themePath(file, source.include), [...ancestors, file]) : null;
117
+ const reference = z.string().safeParse(source.tokenColors);
118
+ const rules = reference.success
119
+ ? parseJsonc(await read(themePath(file, reference.data)), tokenRules) : tokenRules.parse(source.tokenColors ?? []);
120
+ const { include: _include, ...own } = source;
121
+ return {
122
+ ...parent, ...own,
123
+ colors: { ...parent?.colors, ...source.colors },
124
+ tokenColors: [...(parent?.tokenColors ?? []), ...(rules ?? [])],
125
+ semanticTokenColors: { ...parent?.semanticTokenColors, ...source.semanticTokenColors },
126
+ };
127
+ };
128
+ const themes = [];
129
+ let totalBytes = 0;
130
+ for (const contribution of manifest.contributes.themes) {
131
+ try {
132
+ const file = themePath('extension/package.json', contribution.path);
133
+ const source = await load(file);
134
+ source.name = contribution.label;
135
+ if (contribution.uiTheme) source.type = contribution.uiTheme === 'vs' ? 'light' : contribution.uiTheme === 'vs-dark' ? 'dark' : contribution.uiTheme;
136
+ const text = JSON.stringify(source);
137
+ totalBytes += Buffer.byteLength(text);
138
+ if (Buffer.byteLength(text) > 512 * 1024 || totalBytes > 10 * 1024 * 1024) throw new Error('Resolved themes too large');
139
+ themes.push({ path: contribution.path, name: contribution.label, text, error: false });
140
+ } catch {
141
+ signal.throwIfAborted();
142
+ themes.push({ path: contribution.path, name: contribution.label, text: '', error: true });
143
+ }
144
+ }
145
+ return themes;
146
+ };
147
+ return { search, readPackage };
148
+ }
149
+
150
+ export function registerThemeCatalogRoutes(app, catalog = createThemeCatalog()) {
151
+ for (const [route, operation, schema] of [
152
+ ['/api/config/themes/catalog/search', catalog.search, querySchema],
153
+ ['/api/config/themes/catalog/package', catalog.readPackage, packageSchema],
154
+ ]) {
155
+ app.post(route, async (req, res) => {
156
+ const input = schema.safeParse(req.body);
157
+ if (!input.success) return res.status(400).json({ error: 'invalid' });
158
+ const controller = new AbortController();
159
+ const cancel = () => controller.abort();
160
+ res.on('close', cancel);
161
+ try {
162
+ const items = await operation(input.data, AbortSignal.any([controller.signal, AbortSignal.timeout(45000)]));
163
+ res.json({ items });
164
+ } catch {
165
+ if (!res.destroyed) res.status(502).json({ error: 'catalog' });
166
+ } finally {
167
+ res.off('close', cancel);
168
+ }
169
+ });
170
+ }
171
+ }