@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
@@ -0,0 +1,125 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { createHash } from 'node:crypto';
3
+ import { deflateRawSync } from 'node:zlib';
4
+ import { createThemeCatalog, registerThemeCatalogRoutes } from './theme-catalog.js';
5
+ import { registerCommonRequestMiddleware } from './core-routes.js';
6
+ import express from 'express';
7
+ import request from 'supertest';
8
+ import { openThemeArchive } from './theme-archive.js';
9
+
10
+ function archive(files) {
11
+ const locals = [];
12
+ const directory = [];
13
+ let offset = 0;
14
+ for (const [filename, content] of Object.entries(files)) {
15
+ const name = Buffer.from(filename);
16
+ const data = Buffer.from(content);
17
+ const compressed = deflateRawSync(data);
18
+ const header = Buffer.alloc(30);
19
+ header.writeUInt32LE(0x04034b50);
20
+ header.writeUInt16LE(8, 8);
21
+ header.writeUInt32LE(compressed.length, 18);
22
+ header.writeUInt32LE(data.length, 22);
23
+ header.writeUInt16LE(name.length, 26);
24
+ locals.push(header, name, compressed);
25
+ const central = Buffer.alloc(46);
26
+ central.writeUInt32LE(0x02014b50);
27
+ central.writeUInt16LE(8, 10);
28
+ central.writeUInt32LE(compressed.length, 20);
29
+ central.writeUInt32LE(data.length, 24);
30
+ central.writeUInt16LE(name.length, 28);
31
+ central.writeUInt32LE(offset, 42);
32
+ directory.push(central, name);
33
+ offset += header.length + name.length + compressed.length;
34
+ }
35
+ const end = Buffer.alloc(22);
36
+ end.writeUInt32LE(0x06054b50);
37
+ end.writeUInt16LE(directory.length / 2, 8);
38
+ end.writeUInt16LE(directory.length / 2, 10);
39
+ end.writeUInt32LE(Buffer.concat(directory).length, 12);
40
+ end.writeUInt32LE(offset, 16);
41
+ return Buffer.concat([...locals, ...directory, end]);
42
+ }
43
+
44
+ const identity = { namespace: 'Author', name: 'fixture', version: '1.0.0' };
45
+ const manifest = { publisher: 'Author', name: 'fixture', version: '1.0.0', contributes: { themes: [
46
+ { label: 'Readable Night', path: './themes/night.json', uiTheme: 'vs-dark' },
47
+ { label: 'Broken sibling', path: '../../outside.json', uiTheme: 'vs' },
48
+ ] } };
49
+ const packageFiles = {
50
+ 'extension/package.json': JSON.stringify(manifest),
51
+ 'extension/base.json': '{"colors":{"editor.background":"#111111","editor.foreground":"#eeeeee"},"tokenColors":[{"scope":"string","settings":{"foreground":"#aaaaaa"}}]}',
52
+ 'extension/themes/night.json': '{ // JSONC\n"include":"../base.json", "colors":{"editor.foreground":"#ffffff"},"tokenColors":"./tokens.json",}',
53
+ 'extension/themes/tokens.json': '[{"scope":"keyword","settings":{"foreground":"#aabbcc"}}]',
54
+ };
55
+
56
+ function transport(bytes, overrides = {}) {
57
+ const responses = new Map([
58
+ ['https://open-vsx.org/api/Author/fixture/1.0.0', JSON.stringify({ ...identity, files: { download: 'https://open-vsx.org/package.vsix', sha256: 'https://open-vsx.org/package.sha256' } })],
59
+ ['https://open-vsx.org/package.vsix', bytes],
60
+ ['https://open-vsx.org/package.sha256', createHash('sha256').update(bytes).digest('hex')],
61
+ ...Object.entries(overrides),
62
+ ]);
63
+ return async (url) => {
64
+ const body = responses.get(url.href);
65
+ if (body === undefined) throw new Error(`Unexpected request ${url}`);
66
+ return new Response(body);
67
+ };
68
+ }
69
+
70
+ describe('Open VSX theme catalog', () => {
71
+ test('catalog requests retain their bodies through the production middleware', async () => {
72
+ const app = express();
73
+ registerCommonRequestMiddleware(app, { express });
74
+ const catalog = createThemeCatalog({ fetchImpl: transport(archive(packageFiles)) });
75
+ registerThemeCatalogRoutes(app, {
76
+ search: async (input) => { expect(input).toEqual({ query: 'nord' }); return []; },
77
+ readPackage: catalog.readPackage,
78
+ });
79
+ await request(app).post('/api/config/themes/catalog/search').send({ query: 'nord' }).expect(200);
80
+ const response = await request(app).post('/api/config/themes/catalog/package').send(identity).expect(200);
81
+ expect(response.body.items[0].name).toBe('Readable Night');
82
+ });
83
+ test('resolves JSONC includes and external token files, preserving healthy siblings and manifest labels', async () => {
84
+ const catalog = createThemeCatalog({ fetchImpl: transport(archive(packageFiles)) });
85
+ const result = await catalog.readPackage(identity);
86
+ expect(result[0].error).toBe(false);
87
+ const source = JSON.parse(result[0].text);
88
+ expect(source.name).toBe('Readable Night');
89
+ expect(source.type).toBe('dark');
90
+ expect(source.colors).toEqual({ 'editor.background': '#111111', 'editor.foreground': '#ffffff' });
91
+ expect(source.tokenColors.map((rule) => rule.scope)).toEqual(['string', 'keyword']);
92
+ expect(source.include).toBeUndefined();
93
+ expect(result[1].error).toBe(true);
94
+ });
95
+
96
+ test('rejects checksum and package identity mismatches', async () => {
97
+ const bytes = archive(packageFiles);
98
+ await expect(createThemeCatalog({ fetchImpl: transport(bytes, { 'https://open-vsx.org/package.sha256': '0'.repeat(64) }) }).readPackage(identity)).rejects.toThrow('checksum');
99
+ const wrong = archive({ ...packageFiles, 'extension/package.json': JSON.stringify({ ...manifest, publisher: 'Impostor' }) });
100
+ await expect(createThemeCatalog({ fetchImpl: transport(wrong) }).readPackage(identity)).rejects.toThrow('identity');
101
+ });
102
+
103
+ test('limits decompression and rejects traversal and cyclic includes', async () => {
104
+ expect(() => openThemeArchive(archive({ '../outside.json': '{}' }))).toThrow('Unsafe');
105
+ const read = openThemeArchive(archive({ 'extension/huge.json': ' '.repeat(512 * 1024 + 1) }));
106
+ await expect(read('extension/huge.json')).rejects.toThrow('large');
107
+ const cyclic = archive({ ...packageFiles, 'extension/base.json': '{"include":"./themes/night.json"}' });
108
+ expect((await createThemeCatalog({ fetchImpl: transport(cyclic) }).readPackage(identity))[0].error).toBe(true);
109
+ });
110
+
111
+ test('searches only the theme category and rejects redirects outside the catalog', async () => {
112
+ const catalog = createThemeCatalog({ fetchImpl: async (url, options) => {
113
+ expect(url.searchParams.get('category')).toBe('Themes');
114
+ expect(url.searchParams.get('query')).toBe('Dune');
115
+ expect(options.redirect).toBe('manual');
116
+ return Response.json({ extensions: [{ ...identity, displayName: 'Dune', files: {} }] });
117
+ } });
118
+ expect((await catalog.search({ query: 'Dune' }))[0].label).toBe('Dune');
119
+ let calls = 0;
120
+ const hostile = createThemeCatalog({ fetchImpl: async () => { calls++; return new Response(null, { status: 302, headers: { location: 'http://127.0.0.1/private' } }); } });
121
+ await expect(hostile.search({ query: 'Dune' })).rejects.toThrow('host');
122
+ expect(calls).toBe(1);
123
+ await expect(catalog.readPackage({ ...identity, name: '../secret' })).rejects.toThrow();
124
+ });
125
+ });
@@ -0,0 +1,144 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import express from 'express';
6
+ import request from 'supertest';
7
+ import { createThemeRuntime } from './theme-runtime.js';
8
+ import { registerSettingsUtilityRoutes, registerCommonRequestMiddleware } from './core-routes.js';
9
+
10
+ const directories = [];
11
+ afterEach(async () => {
12
+ await Promise.all(directories.splice(0).map((directory) => fs.rm(directory, { recursive: true, force: true })));
13
+ });
14
+
15
+ const definition = {
16
+ metadata: { id: '../../escape', name: 'Imported fixture', variant: 'dark' },
17
+ colors: {
18
+ primary: { base: '#ffcc66' },
19
+ surface: { background: '#111111', foreground: '#eeeeee', muted: '#181818', mutedForeground: '#999999', elevated: '#222222' },
20
+ interactive: { border: '#333333' },
21
+ status: { error: '#ff6666', warning: '#ffcc66', success: '#66cc66', info: '#66aaff' },
22
+ syntax: { base: { comment: '#888888', keyword: '#aa88ff', string: '#88cc66', number: '#ffcc66', function: '#66aaff', variable: '#eeeeee', type: '#66dddd', operator: '#ff6677' } },
23
+ },
24
+ };
25
+
26
+ async function setup(overrides = {}) {
27
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-theme-import-'));
28
+ directories.push(root);
29
+ const themesDir = path.join(root, 'themes');
30
+ const runtime = createThemeRuntime({ fsPromises: { ...fs, ...overrides }, path, themesDir, maxThemeJsonBytes: 512 * 1024, logger: console });
31
+ const app = express();
32
+ registerCommonRequestMiddleware(app, { express });
33
+ registerSettingsUtilityRoutes(app, runtime);
34
+ return { root, themesDir, runtime, app };
35
+ }
36
+
37
+ describe('theme import persistence', () => {
38
+ it('deletes only imported files, leaves siblings intact and allows an identical reimport', async () => {
39
+ const { app, runtime, themesDir } = await setup();
40
+ const theme = await runtime.saveImportedTheme(definition);
41
+ await fs.writeFile(path.join(themesDir, 'custom.json'), JSON.stringify(definition));
42
+ await request(app).delete('/api/config/themes/openchamber-dark').expect(200);
43
+ await request(app).delete(`/api/config/themes/${theme.metadata.id}`).expect(200);
44
+ expect(await fs.readdir(themesDir)).toEqual(['custom.json']);
45
+ await request(app).delete(`/api/config/themes/${theme.metadata.id}`).expect(200);
46
+ expect(await runtime.saveImportedTheme(definition)).toEqual(theme);
47
+ });
48
+
49
+ it('does not follow a symlink during deletion or report deletion failure as success', async () => {
50
+ const { app, root, themesDir } = await setup();
51
+ await fs.mkdir(themesDir);
52
+ const outside = path.join(root, 'outside.json');
53
+ await fs.writeFile(outside, 'keep');
54
+ const id = `imported-vscode-${'a'.repeat(24)}`;
55
+ await fs.symlink(outside, path.join(themesDir, `${id}.json`));
56
+ await request(app).delete(`/api/config/themes/${id}`).expect(200);
57
+ expect(await fs.readFile(outside, 'utf8')).toBe('keep');
58
+ const denied = await setup({ unlink: async () => { throw Object.assign(new Error('denied'), { code: 'EACCES' }); } });
59
+ await fs.mkdir(denied.themesDir);
60
+ await fs.writeFile(path.join(denied.themesDir, `${id}.json`), JSON.stringify({ ...definition, metadata: { ...definition.metadata, id } }));
61
+ await request(denied.app).delete(`/api/config/themes/${id}`).expect(500);
62
+ expect(await fs.readdir(denied.themesDir)).toEqual([`${id}.json`]);
63
+ });
64
+ it('deletes a hand-added theme by its metadata ID, not by an assumed filename', async () => {
65
+ const { app, themesDir } = await setup();
66
+ await fs.mkdir(themesDir);
67
+ const custom = { ...definition, metadata: { ...definition.metadata, id: 't3-code-dark', name: 'T3 Code' } };
68
+ await fs.writeFile(path.join(themesDir, 'my-custom-palette.json'), JSON.stringify(custom));
69
+ const loaded = await request(app).get('/api/config/themes').expect(200);
70
+ expect(loaded.body.themes[0].metadata.id).toBe('t3-code-dark');
71
+ await request(app).delete('/api/config/themes/t3-code-dark').expect(200);
72
+ expect(await fs.readdir(themesDir)).toEqual([]);
73
+ });
74
+
75
+ it('refuses ambiguous duplicate IDs and does not turn an ID into a filesystem path', async () => {
76
+ const { app, themesDir, root } = await setup();
77
+ await fs.mkdir(themesDir);
78
+ const custom = { ...definition, metadata: { ...definition.metadata, id: 'duplicate' } };
79
+ for (const name of ['one.json', 'two.json']) await fs.writeFile(path.join(themesDir, name), JSON.stringify(custom));
80
+ await request(app).delete('/api/config/themes/duplicate').expect(409);
81
+ expect((await fs.readdir(themesDir)).sort()).toEqual(['one.json', 'two.json']);
82
+ await fs.writeFile(path.join(root, 'outside.json'), JSON.stringify(custom));
83
+ await request(app).delete('/api/config/themes/..%2Foutside').expect(200);
84
+ expect(await fs.readFile(path.join(root, 'outside.json'), 'utf8')).toBe(JSON.stringify(custom));
85
+ });
86
+
87
+ it('allows deleting a readable theme beside an unreadable sibling, but does not treat failed reads as absence', async () => {
88
+ const { runtime, themesDir } = await setup({ readFile: async (file, ...args) => {
89
+ if (path.basename(file) === 'unreadable.json') throw Object.assign(new Error('denied'), { code: 'EACCES' });
90
+ return fs.readFile(file, ...args);
91
+ } });
92
+ const theme = await runtime.saveImportedTheme(definition);
93
+ await fs.writeFile(path.join(themesDir, 'unreadable.json'), JSON.stringify(definition));
94
+ await runtime.deleteImportedTheme(theme.metadata.id);
95
+ expect(await fs.readdir(themesDir)).toEqual(['unreadable.json']);
96
+ await expect(runtime.deleteImportedTheme('possibly-unreadable')).rejects.toMatchObject({ code: 'EACCES' });
97
+ });
98
+ it('saves through the API under a server-owned ID and returns it through reload', async () => {
99
+ const { app, themesDir, root } = await setup();
100
+ const response = await request(app).post('/api/config/themes').send({ theme: definition }).expect(201);
101
+ expect(response.body.theme.metadata.id).toMatch(/^imported-vscode-[a-f0-9]{24}$/);
102
+ expect(await fs.readdir(root)).toEqual(['themes']);
103
+ expect(await fs.readdir(themesDir)).toEqual([`${response.body.theme.metadata.id}.json`]);
104
+ const loaded = await request(app).get('/api/config/themes').expect(200);
105
+ expect(loaded.body.themes).toEqual([response.body.theme]);
106
+ });
107
+
108
+ it('deduplicates simultaneous imports and never overwrites a subsequently edited theme', async () => {
109
+ const { runtime, themesDir } = await setup();
110
+ const [first, second] = await Promise.all([runtime.saveImportedTheme(definition), runtime.saveImportedTheme(definition)]);
111
+ expect(first).toEqual(second);
112
+ expect((await fs.readdir(themesDir)).length).toBe(1);
113
+ const filename = path.join(themesDir, `${first.metadata.id}.json`);
114
+ await fs.writeFile(filename, 'manually edited');
115
+ await expect(runtime.saveImportedTheme(definition)).rejects.toMatchObject({ code: 'conflict', status: 409 });
116
+ expect(await fs.readFile(filename, 'utf8')).toBe('manually edited');
117
+ expect(await fs.readdir(themesDir)).toEqual([`${first.metadata.id}.json`]);
118
+ });
119
+
120
+ it('rejects malformed definitions and unsafe token names without creating files', async () => {
121
+ const { app, root } = await setup();
122
+ await request(app).post('/api/config/themes').send({ theme: { ...definition, colors: {} } }).expect(400);
123
+ await request(app).post('/api/config/themes').send({ theme: { ...definition, colors: { ...definition.colors, syntax: { ...definition.colors.syntax, tokens: { 'x;body': '#000000' } } } } }).expect(400);
124
+ expect(await fs.readdir(root)).toEqual([]);
125
+ });
126
+
127
+ it('cleans temporary files after a publication failure', async () => {
128
+ const { runtime, themesDir } = await setup({ link: async () => { throw Object.assign(new Error('denied'), { code: 'EACCES' }); } });
129
+ await expect(runtime.saveImportedTheme(definition)).rejects.toMatchObject({ code: 'EACCES' });
130
+ expect(await fs.readdir(themesDir)).toEqual([]);
131
+ });
132
+
133
+ it('does not report a directory read failure as an empty theme library', async () => {
134
+ const { runtime } = await setup({ readdir: async () => { throw Object.assign(new Error('denied'), { code: 'EACCES' }); } });
135
+ await expect(runtime.readCustomThemesFromDisk()).rejects.toMatchObject({ code: 'EACCES' });
136
+ });
137
+
138
+ it('rejects oversized imports without touching the themes directory', async () => {
139
+ const { runtime, root } = await setup();
140
+ const tokens = Object.fromEntries(Array.from({ length: 30000 }, (_, index) => [`token${index}`, '#123456']));
141
+ await expect(runtime.saveImportedTheme({ ...definition, colors: { ...definition.colors, syntax: { ...definition.colors.syntax, tokens } } })).rejects.toMatchObject({ code: 'size', status: 413 });
142
+ expect(await fs.readdir(root)).toEqual([]);
143
+ });
144
+ });
@@ -1,3 +1,29 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { z } from 'zod';
3
+
4
+ const color = z.string().regex(/^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i);
5
+ const role = z.string().regex(/^[a-zA-Z][a-zA-Z0-9]*$/).max(64);
6
+ const importedThemeSchema = z.object({
7
+ metadata: z.object({
8
+ name: z.string().trim().min(1).max(160),
9
+ author: z.string().max(160).optional(),
10
+ variant: z.enum(['light', 'dark']),
11
+ description: z.string().max(1024).default(''),
12
+ version: z.string().max(40).default('1.0.0'),
13
+ tags: z.array(z.string().max(40)).max(16).default([]),
14
+ }),
15
+ colors: z.record(role, z.record(role, z.union([color, z.record(role, color)]))),
16
+ });
17
+ const fileErrorSchema = z.object({ code: z.string() });
18
+
19
+ export class ThemeImportStorageError extends Error {
20
+ constructor(code, status) {
21
+ super(code);
22
+ this.code = code;
23
+ this.status = status;
24
+ }
25
+ }
26
+
1
27
  export const createThemeRuntime = (dependencies) => {
2
28
  const {
3
29
  fsPromises,
@@ -34,46 +60,24 @@ export const createThemeRuntime = (dependencies) => {
34
60
  const status = colors.status;
35
61
  const syntax = colors.syntax;
36
62
  const syntaxBase = syntax && typeof syntax === 'object' ? syntax.base : null;
37
- const syntaxHighlights = syntax && typeof syntax === 'object' ? syntax.highlights : null;
38
63
 
39
- if (!primary || !surface || !interactive || !status || !syntaxBase || !syntaxHighlights) {
64
+ if (!primary || !surface || !interactive || !status || !syntaxBase) {
40
65
  return null;
41
66
  }
42
67
 
43
- // Minimal fields required by CSSVariableGenerator and diff/syntax rendering.
68
+ // Authored inputs only. The UI resolves optional roles before rendering.
44
69
  const required = [
45
70
  primary.base,
46
- primary.foreground,
47
71
  surface.background,
48
72
  surface.foreground,
49
73
  surface.muted,
50
74
  surface.mutedForeground,
51
75
  surface.elevated,
52
- surface.elevatedForeground,
53
- surface.subtle,
54
76
  interactive.border,
55
- interactive.selection,
56
- interactive.selectionForeground,
57
- interactive.focusRing,
58
- interactive.hover,
59
77
  status.error,
60
- status.errorForeground,
61
- status.errorBackground,
62
- status.errorBorder,
63
78
  status.warning,
64
- status.warningForeground,
65
- status.warningBackground,
66
- status.warningBorder,
67
79
  status.success,
68
- status.successForeground,
69
- status.successBackground,
70
- status.successBorder,
71
80
  status.info,
72
- status.infoForeground,
73
- status.infoBackground,
74
- status.infoBorder,
75
- syntaxBase.background,
76
- syntaxBase.foreground,
77
81
  syntaxBase.keyword,
78
82
  syntaxBase.string,
79
83
  syntaxBase.number,
@@ -82,9 +86,6 @@ export const createThemeRuntime = (dependencies) => {
82
86
  syntaxBase.type,
83
87
  syntaxBase.comment,
84
88
  syntaxBase.operator,
85
- syntaxHighlights.diffAdded,
86
- syntaxHighlights.diffRemoved,
87
- syntaxHighlights.lineNumber,
88
89
  ];
89
90
 
90
91
  if (!required.every(isValidThemeColor)) {
@@ -156,12 +157,83 @@ export const createThemeRuntime = (dependencies) => {
156
157
  return [];
157
158
  }
158
159
  logger.warn('[themes] Failed to list custom themes dir:', error);
159
- return [];
160
+ throw error;
161
+ }
162
+ };
163
+
164
+ const saveImportedTheme = async (raw) => {
165
+ const parsed = importedThemeSchema.safeParse(raw);
166
+ if (!parsed.success) throw new ThemeImportStorageError('invalid', 400);
167
+ const normalized = normalizeThemeJson({ ...parsed.data, metadata: { ...parsed.data.metadata, id: 'import' } });
168
+ if (!normalized) throw new ThemeImportStorageError('invalid', 400);
169
+ // The client supplies colors, never a filename or an overwrite target.
170
+ // Identical imports have the same ID, including retries after a lost reply.
171
+ const digest = createHash('sha256').update(JSON.stringify(parsed.data)).digest('hex').slice(0, 24);
172
+ const theme = { ...normalized, metadata: { ...normalized.metadata, id: `imported-vscode-${digest}` } };
173
+ const contents = `${JSON.stringify(theme, null, 2)}\n`;
174
+ if (Buffer.byteLength(contents, 'utf8') > maxThemeJsonBytes) throw new ThemeImportStorageError('size', 413);
175
+ await fsPromises.mkdir(themesDir, { recursive: true });
176
+ const target = path.join(themesDir, `${theme.metadata.id}.json`);
177
+ const temporary = path.join(themesDir, `${randomUUID()}.tmp`);
178
+ try {
179
+ await fsPromises.writeFile(temporary, contents, { flag: 'wx', mode: 0o600 });
180
+ try {
181
+ // Link publishes the complete file atomically and cannot overwrite an
182
+ // existing theme, even if another import finishes at the same time.
183
+ await fsPromises.link(temporary, target);
184
+ } catch (error) {
185
+ const parsedError = fileErrorSchema.safeParse(error);
186
+ if (!parsedError.success || parsedError.data.code !== 'EEXIST') throw error;
187
+ if (await fsPromises.readFile(target, 'utf8') !== contents) throw new ThemeImportStorageError('conflict', 409);
188
+ }
189
+ return theme;
190
+ } finally {
191
+ await fsPromises.unlink(temporary).catch((error) => {
192
+ const parsedError = fileErrorSchema.safeParse(error);
193
+ if (!parsedError.success || parsedError.data.code !== 'ENOENT') logger.warn('[themes] Failed to clean import temporary file');
194
+ });
160
195
  }
161
196
  };
162
197
 
163
198
  return {
164
199
  normalizeThemeJson,
165
200
  readCustomThemesFromDisk,
201
+ saveImportedTheme,
202
+ async deleteImportedTheme(id) {
203
+ if (!z.string().trim().min(1).max(256).safeParse(id).success) throw new ThemeImportStorageError('invalid', 400);
204
+ const matches = [];
205
+ let readFailure;
206
+ let entries;
207
+ try { entries = await fsPromises.readdir(themesDir, { withFileTypes: true }); }
208
+ catch (error) {
209
+ if (fileErrorSchema.safeParse(error).data?.code === 'ENOENT') return;
210
+ throw error;
211
+ }
212
+ for (const entry of entries) {
213
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
214
+ const target = path.join(themesDir, entry.name);
215
+ let text;
216
+ try {
217
+ const stat = await fsPromises.lstat(target);
218
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > maxThemeJsonBytes) continue;
219
+ text = await fsPromises.readFile(target, 'utf8');
220
+ } catch (error) {
221
+ if (fileErrorSchema.safeParse(error).data?.code !== 'ENOENT') readFailure = error;
222
+ continue;
223
+ }
224
+ let theme;
225
+ try { theme = normalizeThemeJson(JSON.parse(text)); }
226
+ catch { continue; }
227
+ if (theme?.metadata.id === id) matches.push(target);
228
+ }
229
+ // IDs identify themes, not filenames. Refuse ambiguous matches rather
230
+ // than deleting an arbitrary sibling or constructing a path from the ID.
231
+ if (matches.length > 1) throw new ThemeImportStorageError('conflict', 409);
232
+ if (!matches.length) {
233
+ if (readFailure) throw readFailure;
234
+ return;
235
+ }
236
+ await fsPromises.unlink(matches[0]);
237
+ },
166
238
  };
167
239
  };
@@ -90,6 +90,21 @@ const createTestRuntime = ({ entries, files, stats }) => createThemeRuntime({
90
90
 
91
91
  describe('theme runtime', () => {
92
92
  describe('readCustomThemesFromDisk', () => {
93
+ it('loads compact authored roles without requiring derived colors', async () => {
94
+ const theme = validTheme('compact');
95
+ theme.colors.primary = { base: '#ffffff' };
96
+ theme.colors.interactive = { border: '#444444' };
97
+ theme.colors.status = { error: '#ff0000', warning: '#ffaa00', success: '#00ff00', info: '#0000ff' };
98
+ delete theme.colors.syntax.highlights;
99
+ delete theme.colors.syntax.base.background;
100
+ delete theme.colors.syntax.base.foreground;
101
+ const runtime = createTestRuntime({
102
+ entries: [fileEntry('compact.json'), fileEntry('bad.json')],
103
+ files: { '/themes/compact.json': JSON.stringify(theme), '/themes/bad.json': '{broken' },
104
+ stats: { '/themes/compact.json': { isFile: () => true, size: 1024 }, '/themes/bad.json': { isFile: () => true, size: 10 } },
105
+ });
106
+ expect((await runtime.readCustomThemesFromDisk()).map((item) => item.metadata.id)).toEqual(['compact']);
107
+ });
93
108
  it('loads valid theme files', async () => {
94
109
  const runtime = createTestRuntime({
95
110
  entries: [fileEntry('direct.json')],
@@ -16,6 +16,14 @@ Policy inheritance uses the nearest explicit session value. A child `false` ther
16
16
 
17
17
  Unknown lineage and failed policy loads fail closed. A failed pending-permission fetch is distinct from an empty successful response and never clears policy state.
18
18
 
19
+ ## Safety net
20
+
21
+ `evaluatePermission` (the routing runtime, `../routing/DOCUMENTATION.md`) is
22
+ consulted after the policy check and before the reply. `hold` counts the
23
+ request as handled without replying, so it stays on screen; `accept` replies as
24
+ before. A `permission.replied` event is passed to `onPermissionReplied` so the
25
+ routing runtime forgets its cached decision.
26
+
19
27
  ## Routes
20
28
 
21
29
  - `GET /api/permission-auto-accept`
@@ -25,6 +25,10 @@ export function createPermissionAutoAcceptRuntime({
25
25
  readSettingsFromDiskMigrated,
26
26
  persistSettings,
27
27
  broadcastGlobalUiEvent,
28
+ // The routing safety net: asked once per request before the automatic reply.
29
+ // `hold` leaves the request for the user; absent means every request is replied to.
30
+ evaluatePermission = null,
31
+ onPermissionReplied = null,
28
32
  fetchImpl = fetch,
29
33
  retryDelaysMs = RETRY_DELAYS_MS,
30
34
  requestTimeoutMs = REQUEST_TIMEOUT_MS,
@@ -148,6 +152,10 @@ export function createPermissionAutoAcceptRuntime({
148
152
  if (!permission?.id || !permission?.sessionID) return false;
149
153
  await load();
150
154
  if (!(await isSessionAutoAccepting(permission.sessionID, directory))) return false;
155
+ if (evaluatePermission) {
156
+ const verdict = await evaluatePermission(permission, directory);
157
+ if (verdict?.action === 'hold') return true;
158
+ }
151
159
  await request(`/permission/${encodeURIComponent(permission.id)}/reply`, {
152
160
  directory,
153
161
  method: 'POST',
@@ -218,6 +226,11 @@ export function createPermissionAutoAcceptRuntime({
218
226
  }
219
227
  if (payload?.type === 'permission.asked') {
220
228
  void processPermission(payload.properties, directory);
229
+ return;
230
+ }
231
+ if (payload?.type === 'permission.replied') {
232
+ const permissionId = payload.properties?.requestID;
233
+ if (typeof permissionId === 'string') onPermissionReplied?.(permissionId);
221
234
  }
222
235
  };
223
236
 
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import { createPermissionAutoAcceptRuntime } from './runtime.js';
3
3
 
4
- const createRuntime = ({ stored, fetchImpl, retryDelaysMs = [0] } = {}) => {
4
+ const createRuntime = ({ stored, fetchImpl, retryDelaysMs = [0], evaluatePermission, onPermissionReplied } = {}) => {
5
5
  let settings = stored ?? { permissionAutoAccept: { sessions: {} } };
6
6
  let eventHandler;
7
7
  let statusHandler;
@@ -16,6 +16,8 @@ const createRuntime = ({ stored, fetchImpl, retryDelaysMs = [0] } = {}) => {
16
16
  persistSettings: async (changes) => { settings = { ...settings, ...changes }; },
17
17
  fetchImpl: fetchImpl ?? vi.fn(async () => new Response('[]')),
18
18
  retryDelaysMs,
19
+ evaluatePermission,
20
+ onPermissionReplied,
19
21
  });
20
22
  runtime.start();
21
23
  return {
@@ -143,4 +145,38 @@ describe('permission auto-accept runtime', () => {
143
145
  expect(fetchImpl.mock.calls.some(([url]) => new URL(url).searchParams.get('directory') === '/project')).toBe(true);
144
146
  expect(await runtime.load()).toEqual({ sessions: { root: true }, revision: 1 });
145
147
  });
148
+
149
+ it('leaves a request held by the safety net unanswered and forgets it once replied', async () => {
150
+ const fetchImpl = vi.fn(async () => new Response('[]'));
151
+ const verdicts = { held: { action: 'hold', score: 0.9, kind: 'git_history' }, safe: { action: 'accept', score: 0.1 } };
152
+ const evaluatePermission = vi.fn(async (permission) => verdicts[permission.id]);
153
+ const onPermissionReplied = vi.fn();
154
+ const { runtime, emit } = createRuntime({
155
+ stored: { permissionAutoAccept: { sessions: { root: true } } },
156
+ fetchImpl,
157
+ evaluatePermission,
158
+ onPermissionReplied,
159
+ });
160
+ await runtime.load();
161
+
162
+ emit({ type: 'permission.asked', properties: { id: 'held', sessionID: 'root', permission: 'bash', metadata: {} } });
163
+ emit({ type: 'permission.asked', properties: { id: 'safe', sessionID: 'root', permission: 'bash', metadata: {} } });
164
+ await flush();
165
+
166
+ const replies = fetchImpl.mock.calls.map(([url]) => String(url)).filter((url) => url.includes('/reply'));
167
+ expect(replies).toEqual(['http://opencode.test/permission/safe/reply?directory=%2Fproject']);
168
+ expect(evaluatePermission).toHaveBeenCalledTimes(2);
169
+
170
+ emit({ type: 'permission.replied', properties: { sessionID: 'root', requestID: 'held', reply: 'once' } });
171
+ expect(onPermissionReplied).toHaveBeenCalledWith('held');
172
+ });
173
+
174
+ it('does not consult the safety net for sessions that are not auto-accepting', async () => {
175
+ const evaluatePermission = vi.fn(async () => ({ action: 'hold' }));
176
+ const { runtime, emit } = createRuntime({ evaluatePermission });
177
+ await runtime.load();
178
+ emit({ type: 'permission.asked', properties: { id: 'p', sessionID: 'manual', permission: 'bash', metadata: {} } });
179
+ await flush();
180
+ expect(evaluatePermission).not.toHaveBeenCalled();
181
+ });
146
182
  });
@@ -12,8 +12,18 @@ The managed Chats root (`~/.config/openchamber/chats`) is also one context owner
12
12
  | `<projectsDir>/<projectId>.json` | `packages/web/server/lib/projects` (`project-setup.js` for the client-owned keys behind `/api/projects/:projectId/config`; `project-config.js` for `version` / `scheduledTasks`), one write lock for both | worktree setup, draft starters, project actions, scheduled tasks |
13
13
  | `<projectsDir>/<projectId>/context.json` | **this module, exclusively** | notes, todos, plan manifest |
14
14
  | `<projectsDir>/<projectId>/plans/*.md` | **this module, exclusively** | plan bodies |
15
+ | `<projectsDir>/<projectId>/memory.json` | `packages/web/server/lib/agent-memory` | what the agent chose to remember about the project |
15
16
  | `<repo>/<plansDir>/*.md` | this module (read, edit, delete, move) when the team config names a `plansDir`; the folder is the team's, any tool may write there | shared plan bodies |
16
17
 
18
+ `<projectId>` in these paths is the bounded stem `projectConfigFileStemOf`
19
+ (`packages/web/server/lib/projects/project-id.js`) gives the id: the id itself
20
+ up to 200 characters, `path_sha256_<digest>` beyond that, so a deeply nested
21
+ checkout gets a folder the filesystem accepts. The folder, the config file,
22
+ and the memory file all share that one stem; nothing here composes a path from
23
+ the raw id. The legacy-migration read below looks at the bounded config file
24
+ for the same reason: a read of `<raw id>.json` would fail with ENAMETOOLONG
25
+ and turn an empty project into an error.
26
+
17
27
  The split is the point. Both files were previously one, written by the client
18
28
  with a whole-file read-modify-write. Adding a server writer to that file would
19
29
  have made unrelated features (project actions, draft starters) clobber notes
@@ -147,9 +157,9 @@ and I/O failures are `500`.
147
157
  ## Legacy migration
148
158
 
149
159
  `projectNotes`, `projectTodos`, and `projectPlanFiles` originally lived in
150
- `<projectId>.json`. On the first read with no `context.json`, those three keys
151
- are moved out and deleted from the client-owned file; every other key is
152
- preserved untouched.
160
+ `<projectId>.json` (the bounded name, see Ownership). On the first read with
161
+ no `context.json`, those three keys are moved out and deleted from the
162
+ client-owned file; every other key is preserved untouched.
153
163
 
154
164
  Plan links carried absolute paths. Migration converts each to a base name. A
155
165
  file already in the plans directory is used in place; one referenced from
@@ -12,6 +12,8 @@
12
12
  * directory never invalidates a reference.
13
13
  */
14
14
 
15
+ import { projectConfigFileStemOf } from '../projects/project-id.js';
16
+
15
17
  const PROJECT_CONTEXT_VERSION = 2;
16
18
  const PROJECT_NOTE_BODY_MAX_LENGTH = 3000;
17
19
  const PROJECT_NOTE_MAX_ITEMS = 200;
@@ -221,10 +223,14 @@ export const createProjectContextRuntime = (deps) => {
221
223
  return value;
222
224
  };
223
225
 
224
- const storageDirFor = (projectId) => path.join(projectsDirPath, sanitizeProjectId(projectId));
226
+ // The folder and the client-owned config file share one bounded stem
227
+ // (`projectConfigFileStemOf`): an id too long for a file name would
228
+ // otherwise fail every read with ENAMETOOLONG instead of reading as empty.
229
+ const storageStemFor = (projectId) => projectConfigFileStemOf(sanitizeProjectId(projectId));
230
+ const storageDirFor = (projectId) => path.join(projectsDirPath, storageStemFor(projectId));
225
231
  const contextPathFor = (projectId) => path.join(storageDirFor(projectId), 'context.json');
226
232
  const plansDirFor = (projectId) => path.join(storageDirFor(projectId), 'plans');
227
- const legacyConfigPathFor = (projectId) => path.join(projectsDirPath, `${sanitizeProjectId(projectId)}.json`);
233
+ const legacyConfigPathFor = (projectId) => path.join(projectsDirPath, `${storageStemFor(projectId)}.json`);
228
234
 
229
235
  const readJson = async (filePath) => {
230
236
  let raw;