@vantran-se/openchamber-web 1.23.1 → 1.24.4

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 (313) 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-BOpqXpFU.js → CodeMirrorCommentWidgets-B4a0JkUz.js} +1 -1
  5. package/dist/assets/DiffView-B2CD9wC8.js +4 -0
  6. package/dist/assets/FilesView-C4vMFclc.js +13 -0
  7. package/dist/assets/GitView-Byu2DY7s.js +6 -0
  8. package/dist/assets/GuestAttachDialog-B808E0-d.js +1 -0
  9. package/dist/assets/{JsonTreeView-nd_E8eVM.js → JsonTreeView-VW_7czo3.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-EzGNCGrd.js +1 -0
  70. package/dist/assets/{MarkdownImageGallery-CVYds3sQ.js → MarkdownImageGallery-B2YUDFaB.js} +1 -1
  71. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +12 -0
  72. package/dist/assets/{OnboardingScreen-Dd2IbPTn.js → OnboardingScreen-CQHBSTEi.js} +1 -1
  73. package/dist/assets/{PatchDiff-nUu5i4bG.js → PatchDiff-DCjGhlYV.js} +1 -1
  74. package/dist/assets/PierreDiffViewer-CEDZNkU2.js +210 -0
  75. package/dist/assets/PlanView-D3x-pTkZ.js +5 -0
  76. package/dist/assets/PluginPane-DEcgDQ9h.js +3 -0
  77. package/dist/assets/SettingsView-CSinuH-1.js +29 -0
  78. package/dist/assets/{SettingsWindow-BccxY81J.js → SettingsWindow-B5CHoJFZ.js} +1 -1
  79. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +1 -0
  80. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +15 -0
  81. package/dist/assets/{ToolPartDiffPreview-BFVgk0UK.js → ToolPartDiffPreview-CflzmK-x.js} +2 -2
  82. package/dist/assets/{VirtualizedFileDiff-z3_H74UR.js → VirtualizedFileDiff-NU0FHFhT.js} +3 -3
  83. package/dist/assets/{WalkthroughView-BqM9GFbJ.js → WalkthroughView-BZyjhqHv.js} +2 -2
  84. package/dist/assets/appThemeRegistry-DLicw4dH.js +1 -0
  85. package/dist/assets/commitAndPush-BDftrjr6.js +1 -0
  86. package/dist/assets/{computeVirtualFileMetrics-Bdazp9Qo.js → computeVirtualFileMetrics-DlWlEM3x.js} +1 -1
  87. package/dist/assets/de-Bro6_zyl.js +10 -0
  88. package/dist/assets/es-Bb9XQZvd.js +16 -0
  89. package/dist/assets/fr-Dgfzn-IL.js +14 -0
  90. package/dist/assets/{index-eo1-D_7z.js → index-CtQNmqUL.js} +1 -1
  91. package/dist/assets/index-DqaMWNrC.css +1 -0
  92. package/dist/assets/index-Dqc6USl3.js +40 -0
  93. package/dist/assets/{isDiffPlainText-BvKH712-.js → isDiffPlainText-CO4e5J_S.js} +1 -1
  94. package/dist/assets/{isFilePlainText-BdVVPukf.js → isFilePlainText-ue6kCou7.js} +1 -1
  95. package/dist/assets/ja-BYsakGFh.js +16 -0
  96. package/dist/assets/ko-BSaQz9aN.js +16 -0
  97. package/dist/assets/main-B62_pjL5.js +2 -0
  98. package/dist/assets/main-BEdcOI2P.js +896 -0
  99. package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
  100. package/dist/assets/markdownCore-CK48LG0z.js +5 -0
  101. package/dist/assets/miniChat-LT8Rqofr.js +2 -0
  102. package/dist/assets/mobile-CqarEynh.js +2 -0
  103. package/dist/assets/{patchFileDiff-B7LjppSS.js → patchFileDiff-D7QVlovD.js} +3 -3
  104. package/dist/assets/pl-OIviPdoj.js +16 -0
  105. package/dist/assets/pt-BR-Cltg-E6s.js +16 -0
  106. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +2 -0
  107. package/dist/assets/renderMobileApp-BC1MHg6g.js +25 -0
  108. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +25 -0
  109. package/dist/assets/{scheduledTasksApi-hceovDei.js → scheduledTasksApi-B2SWMGEU.js} +1 -1
  110. package/dist/assets/shikiHighlight-BPu3JiQs.js +1 -0
  111. package/dist/assets/textMateThemeFromAppTheme-DWqRPo9A.js +1 -0
  112. package/dist/assets/tr-CQI-KPre.js +7 -0
  113. package/dist/assets/uk-CqW7R4DA.js +16 -0
  114. package/dist/assets/useAppFontEffects-ne84JBVL.js +7333 -0
  115. package/dist/assets/useInlineCommentController-CgS0fnw_.js +2 -0
  116. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +2 -0
  117. package/dist/assets/{vendor-base-ui-BY_5qla_.js → vendor-base-ui-CsXYnR5c.js} +1 -1
  118. package/dist/assets/{vendor-codemirror-language-data-DWA1bm1F.js → vendor-codemirror-language-data-Bx2EHc1O.js} +2 -2
  119. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
  120. package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
  121. package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
  122. package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
  123. package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
  124. package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
  125. package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
  126. package/dist/assets/{vendor-zod-B6551dwG.js → vendor-zod-CgAdXWZO.js} +4 -4
  127. package/dist/assets/{web-components-B_Z64So5.js → web-components-DxX8bZ1o.js} +2 -2
  128. package/dist/assets/zh-CN-CVZG_o_J.js +16 -0
  129. package/dist/assets/zh-TW-D5ZxzCZp.js +16 -0
  130. package/dist/index.html +6 -4
  131. package/dist/mini-chat.html +6 -4
  132. package/dist/mobile.html +6 -4
  133. package/package.json +12 -9
  134. package/server/built-in-extensions/registry.json +4 -0
  135. package/server/index.d.ts +2 -0
  136. package/server/index.js +44 -10
  137. package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
  138. package/server/lib/agent-tool/runtime.js +62 -15
  139. package/server/lib/agent-tool/runtime.test.js +88 -2
  140. package/server/lib/changelog/update-notes.js +2 -1
  141. package/server/lib/changelog/update-notes.test.js +19 -0
  142. package/server/lib/event-stream/DOCUMENTATION.md +23 -0
  143. package/server/lib/event-stream/delta-coalescer.js +0 -0
  144. package/server/lib/event-stream/delta-coalescer.test.js +292 -0
  145. package/server/lib/event-stream/global-hub.js +53 -23
  146. package/server/lib/event-stream/global-hub.test.js +160 -0
  147. package/server/lib/event-stream/global-ws-bridge.js +1 -0
  148. package/server/lib/event-stream/index.js +4 -0
  149. package/server/lib/fs/DOCUMENTATION.md +10 -1
  150. package/server/lib/fs/git-process.test.js +77 -0
  151. package/server/lib/fs/routes.js +58 -5
  152. package/server/lib/fs/routes.test.js +116 -0
  153. package/server/lib/fs/search.js +2 -1
  154. package/server/lib/git/DOCUMENTATION.md +20 -5
  155. package/server/lib/git/routes.js +16 -3
  156. package/server/lib/git/serial-refresh.js +93 -0
  157. package/server/lib/git/serial-refresh.test.js +112 -0
  158. package/server/lib/git/service.js +429 -70
  159. package/server/lib/git/service.test.js +418 -0
  160. package/server/lib/github/pr-status.js +5 -5
  161. package/server/lib/github/pr-status.test.js +1 -1
  162. package/server/lib/github/routes.js +4 -4
  163. package/server/lib/guests/DOCUMENTATION.md +68 -0
  164. package/server/lib/guests/auth-store.js +184 -0
  165. package/server/lib/guests/auth-store.test.js +112 -0
  166. package/server/lib/guests/background.test.js +83 -0
  167. package/server/lib/guests/builtins.js +34 -0
  168. package/server/lib/guests/builtins.test.js +174 -0
  169. package/server/lib/guests/catalog.js +477 -0
  170. package/server/lib/guests/catalog.test.js +390 -0
  171. package/server/lib/guests/clone.js +297 -0
  172. package/server/lib/guests/extract-zip.js +136 -0
  173. package/server/lib/guests/extract-zip.test.js +109 -0
  174. package/server/lib/guests/files.js +308 -0
  175. package/server/lib/guests/files.test.js +262 -0
  176. package/server/lib/guests/grant-scope.js +81 -0
  177. package/server/lib/guests/grant-scope.test.js +75 -0
  178. package/server/lib/guests/host-session.js +73 -0
  179. package/server/lib/guests/host-session.test.js +30 -0
  180. package/server/lib/guests/html-styles.js +6 -0
  181. package/server/lib/guests/html-styles.test.js +45 -0
  182. package/server/lib/guests/html-tokens.js +73 -0
  183. package/server/lib/guests/html-tokens.test.js +53 -0
  184. package/server/lib/guests/install.js +406 -0
  185. package/server/lib/guests/install.test.js +338 -0
  186. package/server/lib/guests/oauth.js +501 -0
  187. package/server/lib/guests/oauth.test.js +481 -0
  188. package/server/lib/guests/persist.js +336 -0
  189. package/server/lib/guests/persist.test.js +288 -0
  190. package/server/lib/guests/request.js +112 -0
  191. package/server/lib/guests/request.test.js +206 -0
  192. package/server/lib/guests/routes.js +838 -0
  193. package/server/lib/guests/service.js +538 -0
  194. package/server/lib/guests/service.test.js +277 -0
  195. package/server/lib/guests/sockets.js +129 -0
  196. package/server/lib/guests/sockets.test.js +110 -0
  197. package/server/lib/guests/ssh-install.test.js +188 -0
  198. package/server/lib/guests/storage.js +68 -0
  199. package/server/lib/guests/storage.test.js +59 -0
  200. package/server/lib/guests/updates.js +294 -0
  201. package/server/lib/guests/updates.test.js +334 -0
  202. package/server/lib/guests/upload.js +50 -0
  203. package/server/lib/guests/upload.test.js +131 -0
  204. package/server/lib/linear/DOCUMENTATION.md +1 -1
  205. package/server/lib/message-queue/DOCUMENTATION.md +10 -1
  206. package/server/lib/message-queue/runtime.js +21 -1
  207. package/server/lib/message-queue/runtime.test.js +52 -5
  208. package/server/lib/notifications/routes.js +5 -0
  209. package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
  210. package/server/lib/openchamber-sessions/routes.js +62 -29
  211. package/server/lib/openchamber-sessions/routes.test.js +96 -0
  212. package/server/lib/opencode/DOCUMENTATION.md +55 -1
  213. package/server/lib/opencode/bootstrap-runtime.js +1 -0
  214. package/server/lib/opencode/core-routes.js +39 -1
  215. package/server/lib/opencode/feature-routes-runtime.js +15 -0
  216. package/server/lib/opencode/lifecycle-process.test.js +84 -0
  217. package/server/lib/opencode/lifecycle.js +54 -67
  218. package/server/lib/opencode/openchamber-routes.js +72 -14
  219. package/server/lib/opencode/openchamber-routes.test.js +71 -3
  220. package/server/lib/opencode/session-runtime.js +68 -0
  221. package/server/lib/opencode/session-runtime.test.js +43 -0
  222. package/server/lib/opencode/settings-helpers.js +8 -0
  223. package/server/lib/opencode/settings-helpers.test.js +13 -1
  224. package/server/lib/opencode/settings-normalization-runtime.js +2 -0
  225. package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
  226. package/server/lib/opencode/settings-registry.json +15 -0
  227. package/server/lib/opencode/shutdown-runtime.js +3 -0
  228. package/server/lib/opencode/shutdown-runtime.test.js +24 -0
  229. package/server/lib/opencode/theme-archive.js +53 -0
  230. package/server/lib/opencode/theme-catalog.js +171 -0
  231. package/server/lib/opencode/theme-catalog.test.js +125 -0
  232. package/server/lib/opencode/theme-import.test.js +144 -0
  233. package/server/lib/opencode/theme-runtime.js +100 -28
  234. package/server/lib/opencode/theme-runtime.test.js +15 -0
  235. package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
  236. package/server/lib/permission-auto-accept/runtime.js +13 -0
  237. package/server/lib/permission-auto-accept/runtime.test.js +37 -1
  238. package/server/lib/projects/project-config.js +2 -2
  239. package/server/lib/quota/DOCUMENTATION.md +0 -1
  240. package/server/lib/quota/providers/index.js +0 -7
  241. package/server/lib/realtime-proxy-terminal.test.js +150 -0
  242. package/server/lib/realtime-proxy.js +2 -0
  243. package/server/lib/routing/DOCUMENTATION.md +83 -0
  244. package/server/lib/routing/defaults.js +98 -0
  245. package/server/lib/routing/feature-flag.js +11 -0
  246. package/server/lib/routing/history.js +34 -0
  247. package/server/lib/routing/jev.js +98 -0
  248. package/server/lib/routing/routes.http.test.js +100 -0
  249. package/server/lib/routing/routes.js +81 -0
  250. package/server/lib/routing/runtime.js +217 -0
  251. package/server/lib/routing/runtime.test.js +188 -0
  252. package/server/lib/routing/store.js +249 -0
  253. package/server/lib/routing/store.test.js +96 -0
  254. package/server/lib/scheduled-tasks/runtime.js +2 -2
  255. package/server/lib/scheduled-tasks/runtime.test.js +14 -0
  256. package/server/lib/security/request-security.js +9 -5
  257. package/server/lib/security/request-security.test.js +37 -0
  258. package/server/lib/small-model/DOCUMENTATION.md +14 -2
  259. package/server/lib/small-model/call.js +37 -6
  260. package/server/lib/small-model/call.test.js +182 -0
  261. package/server/lib/terminal/DOCUMENTATION.md +23 -2
  262. package/server/lib/terminal/runtime.js +27 -5
  263. package/server/lib/terminal/runtime.test.js +67 -0
  264. package/server/lib/terminal/shutdown.js +106 -0
  265. package/server/lib/terminal/shutdown.test.js +84 -0
  266. package/server/lib/tts/service.test.js +78 -0
  267. package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
  268. package/server/lib/ui-auth/session-cookie.js +51 -0
  269. package/server/lib/ui-auth/session-cookie.test.js +46 -0
  270. package/server/lib/ui-auth/ui-auth.js +56 -17
  271. package/server/lib/ui-auth/ui-auth.test.js +96 -0
  272. package/dist/assets/DiffView-CyLmb-wF.js +0 -4
  273. package/dist/assets/DirtyBranchSwitchDialog-pu5kcoir.js +0 -1
  274. package/dist/assets/FilesView-WtJB0y_l.js +0 -13
  275. package/dist/assets/GitView-DkNrV0BK.js +0 -6
  276. package/dist/assets/LinearIssuesView-B0H0Otii.js +0 -1
  277. package/dist/assets/MarkdownRendererImpl-DSqVN2d8.js +0 -12
  278. package/dist/assets/PierreDiffViewer-Bsq9z-sw.js +0 -161
  279. package/dist/assets/PlanView-CAFziKH5.js +0 -5
  280. package/dist/assets/SettingsView-BRittavZ.js +0 -28
  281. package/dist/assets/ToolOutputDialog-B0uYaJio.js +0 -15
  282. package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
  283. package/dist/assets/de-c7CYLCAW.js +0 -10
  284. package/dist/assets/es-BKAVPQmQ.js +0 -16
  285. package/dist/assets/fr-Bj1glqfV.js +0 -14
  286. package/dist/assets/index-BYFyOu_D.css +0 -1
  287. package/dist/assets/index-BfwRClDe.js +0 -40
  288. package/dist/assets/ja-5jwSIbK0.js +0 -16
  289. package/dist/assets/ko-BaDbg-Eb.js +0 -16
  290. package/dist/assets/main-CGG2_uHq.js +0 -2
  291. package/dist/assets/main-CcrSaL4J.js +0 -930
  292. package/dist/assets/markdownCore-DsmQr3bt.js +0 -5
  293. package/dist/assets/miniChat-DEaAhP4b.js +0 -2
  294. package/dist/assets/mobile-BC3rsvv0.js +0 -2
  295. package/dist/assets/pl-BFKVBDFu.js +0 -16
  296. package/dist/assets/pt-BR-qKfHvkDL.js +0 -16
  297. package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
  298. package/dist/assets/renderElectronMiniChatApp-CR9d1Mr0.js +0 -2
  299. package/dist/assets/renderMobileApp-Da5dwobZ.js +0 -25
  300. package/dist/assets/runtimeEndpointReset-CLYVRw2H.js +0 -41
  301. package/dist/assets/shikiHighlight-D1Z9G58W.js +0 -1
  302. package/dist/assets/tr-DYXPoEDG.js +0 -7
  303. package/dist/assets/uk-CwAoloR8.js +0 -16
  304. package/dist/assets/useAppFontEffects-DeStGcUT.js +0 -7282
  305. package/dist/assets/useGitComparison-CpxioduU.js +0 -1
  306. package/dist/assets/useInlineCommentController-gmUnJtrb.js +0 -2
  307. package/dist/assets/useRootScrollLock--a3cPz8s.js +0 -1
  308. package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
  309. package/dist/assets/zh-CN-D9MG0lqF.js +0 -16
  310. package/dist/assets/zh-TW-1HNK7shh.js +0 -16
  311. package/server/lib/quota/providers/crof.js +0 -96
  312. package/server/lib/quota/providers/crof.test.js +0 -98
  313. /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { createRoutingStore, parseEffectiveConfig, resolveEffectiveConfig, toStoredConfig } from './store.js';
6
+ import { BUILTIN_CATEGORIES } from './defaults.js';
7
+
8
+ const tempDir = () => fs.mkdtemp(path.join(os.tmpdir(), 'routing-store-'));
9
+
10
+ const sampleConfig = (patch = {}) => ({
11
+ enabled: true,
12
+ fallback: { model: { providerID: 'anthropic', modelID: 'claude-sonnet-5' }, variant: 'medium' },
13
+ minConfidence: 0.6,
14
+ safetyNet: { enabled: true, threshold: 0.6 },
15
+ categories: resolveEffectiveConfig(null).categories,
16
+ ...patch,
17
+ });
18
+
19
+ describe('routing store', () => {
20
+ it('resolves the defaults when nothing is stored', async () => {
21
+ const store = createRoutingStore({ dataDir: await tempDir() });
22
+ const config = await store.readConfig();
23
+ expect(config.enabled).toBe(false);
24
+ expect(config.fallback).toBeNull();
25
+ expect(config.categories.map((c) => c.id)).toEqual(BUILTIN_CATEGORIES.map((c) => c.id));
26
+ expect(config.categories.every((c) => c.builtin && c.enabled && c.model === null)).toBe(true);
27
+ });
28
+
29
+ it('stores only deviations from the built-ins and round-trips them', async () => {
30
+ const dir = await tempDir();
31
+ const store = createRoutingStore({ dataDir: dir });
32
+ const config = sampleConfig();
33
+ config.categories[0] = { ...config.categories[0], enabled: false };
34
+ config.categories[3] = { ...config.categories[3], model: { providerID: 'openai', modelID: 'gpt-6-astra' }, variant: 'high', agent: 'plan' };
35
+ config.categories.push({ id: 'my-refactors', builtin: false, enabled: true, name: 'My refactors', description: 'Refactors across modules.', model: null, variant: null, agent: null });
36
+ await store.writeConfig(config);
37
+
38
+ const stored = JSON.parse(await fs.readFile(path.join(dir, 'routing.json'), 'utf8'));
39
+ expect(stored.categories).toEqual({
40
+ trivial: { builtin: true, disabled: true },
41
+ hard: { builtin: true, model: { providerID: 'openai', modelID: 'gpt-6-astra' }, variant: 'high', agent: 'plan' },
42
+ 'my-refactors': { builtin: false, name: 'My refactors', description: 'Refactors across modules.' },
43
+ });
44
+ expect(await store.readConfig()).toEqual(config);
45
+ });
46
+
47
+ it('records a removed built-in as deleted and keeps the others', async () => {
48
+ const config = sampleConfig();
49
+ config.categories = config.categories.filter((c) => c.id !== 'research');
50
+ const stored = toStoredConfig(config);
51
+ expect(stored.categories.research).toEqual({ builtin: true, deleted: true });
52
+ const resolved = resolveEffectiveConfig(stored);
53
+ expect(resolved.categories.map((c) => c.id)).toEqual(['trivial', 'implement', 'hard']);
54
+ });
55
+
56
+ it('applies later built-in text to an untouched category but keeps an edited one', () => {
57
+ const edited = resolveEffectiveConfig({ version: 1, categories: { hard: { builtin: true, description: 'My own hard.' } } });
58
+ expect(edited.categories.find((c) => c.id === 'hard').description).toBe('My own hard.');
59
+ expect(edited.categories.find((c) => c.id === 'trivial').description).toBe(BUILTIN_CATEGORIES[0].description);
60
+ });
61
+
62
+ it('rejects a malformed file instead of treating it as empty', async () => {
63
+ const dir = await tempDir();
64
+ await fs.writeFile(path.join(dir, 'routing.json'), '{"version":1,"categories":{"x":{"builtin":false}}}');
65
+ const store = createRoutingStore({ dataDir: dir });
66
+ await expect(store.readConfig()).rejects.toThrow(/Invalid routing.json/);
67
+ });
68
+
69
+ it('rejects an invalid PUT body with a 400 status', () => {
70
+ expect(() => parseEffectiveConfig({ enabled: true })).toThrow(expect.objectContaining({ status: 400 }));
71
+ const duplicate = sampleConfig();
72
+ duplicate.categories = [...duplicate.categories, { ...duplicate.categories[0], builtin: false }];
73
+ expect(() => parseEffectiveConfig(duplicate)).toThrow(/Duplicate category id/);
74
+ });
75
+
76
+ it('refuses the Auto sentinel as a fallback or category model', () => {
77
+ const asFallback = sampleConfig({ fallback: { model: { providerID: 'openchamber', modelID: 'auto' }, variant: null } });
78
+ expect(() => parseEffectiveConfig(asFallback)).toThrow(/Auto model cannot be a routing target/);
79
+ const asCategory = sampleConfig();
80
+ asCategory.categories = asCategory.categories.map((c, i) => (i === 0 ? { ...c, model: { providerID: 'openchamber', modelID: 'auto' } } : c));
81
+ expect(() => parseEffectiveConfig(asCategory)).toThrow(/Auto model cannot be a routing target/);
82
+ });
83
+
84
+ it('keeps the token in its own 0600 file and clears it', async () => {
85
+ const dir = await tempDir();
86
+ const store = createRoutingStore({ dataDir: dir });
87
+ expect(await store.readToken()).toBeNull();
88
+ await store.writeToken('ts-secret');
89
+ expect(await store.readToken()).toBe('ts-secret');
90
+ const stat = await fs.stat(path.join(dir, 'routing-auth.json'));
91
+ if (process.platform !== 'win32') expect(stat.mode & 0o777).toBe(0o600);
92
+ expect(JSON.stringify(JSON.parse(await fs.readFile(path.join(dir, 'routing.json'), 'utf8').catch(() => '{}')))).not.toContain('ts-secret');
93
+ await store.clearToken();
94
+ expect(await store.readToken()).toBeNull();
95
+ });
96
+ });
@@ -1,6 +1,6 @@
1
1
  import { createOpencodeClient } from '@opencode-ai/sdk/v2';
2
2
  import { DateTime } from 'luxon';
3
- import parser from 'cron-parser';
3
+ import { CronExpressionParser } from 'cron-parser';
4
4
  import { expandSnippets } from '../opencode/snippets.js';
5
5
  import { buildGoalIntroText, createSessionGoal } from '../session-goal/create.js';
6
6
  import { discoverLoops } from './loops.js';
@@ -216,7 +216,7 @@ export const computeNextRunAt = (task, nowMs = Date.now()) => {
216
216
 
217
217
  if (schedule.kind === 'cron') {
218
218
  try {
219
- const iterator = parser.parseExpression(schedule.cron, {
219
+ const iterator = CronExpressionParser.parse(schedule.cron, {
220
220
  tz: zone,
221
221
  currentDate: new Date(nowMs),
222
222
  });
@@ -12,6 +12,20 @@ import {
12
12
  import { createProjectConfigRuntime } from '../projects/project-config.js';
13
13
 
14
14
  describe('scheduled-tasks runtime helpers', () => {
15
+ it.each([
16
+ ['*/15 * * * *', 'UTC', '2026-09-18T08:07:00Z', '2026-09-18T08:15:00Z'],
17
+ ['30 */5 * * * *', 'UTC', '2026-09-18T08:07:00Z', '2026-09-18T08:10:30Z'],
18
+ ['0 9 * * MON-FRI', 'Europe/Kyiv', '2026-09-18T07:00:00Z', '2026-09-21T06:00:00Z'],
19
+ ['0 9 * * *', 'Europe/Kyiv', '2026-03-28T08:00:00Z', '2026-03-29T06:00:00Z'],
20
+ ['0 9 * * *', 'Europe/Kyiv', '2026-10-24T08:00:00Z', '2026-10-25T07:00:00Z'],
21
+ ['0 0 L * *', 'UTC', '2026-02-01T00:00:00Z', '2026-02-28T00:00:00Z'],
22
+ ])('computes cron %s in %s from %s', (cron, timezone, now, expected) => {
23
+ expect(computeNextRunAt({
24
+ enabled: true,
25
+ schedule: { kind: 'cron', cron, timezone },
26
+ }, Date.parse(now))).toBe(Date.parse(expected));
27
+ });
28
+
15
29
  it('computes next daily run in timezone', () => {
16
30
  const nowUtc = Date.UTC(2025, 0, 1, 8, 0, 0);
17
31
  const next = computeNextRunAt({
@@ -1,3 +1,5 @@
1
+ import { sessionCookieNameForRequest } from '../ui-auth/session-cookie.js';
2
+
1
3
  export const createRequestSecurityRuntime = (deps) => {
2
4
  const { readSettingsFromDiskMigrated } = deps;
3
5
  // Origins of packaged (non-browser) clients whose WebView origin never
@@ -17,12 +19,14 @@ export const createRequestSecurityRuntime = (deps) => {
17
19
  if (!cookieHeader || typeof cookieHeader !== 'string') {
18
20
  return null;
19
21
  }
20
- const segments = cookieHeader.split(';');
21
- for (const segment of segments) {
22
+ // Match the exact slot for the host:port this request arrived on. A browser
23
+ // shares cookies across ports on LAN and loopback hosts. This extracts
24
+ // notification/session identity, not a CSRF token, using the same name
25
+ // as ui-auth's session issuance and validation.
26
+ const expected = sessionCookieNameForRequest(req);
27
+ for (const segment of cookieHeader.split(';')) {
22
28
  const [rawName, ...rest] = segment.split('=');
23
- const name = rawName?.trim();
24
- if (!name) continue;
25
- if (name !== 'oc_ui_session') continue;
29
+ if (rawName?.trim() !== expected) continue;
26
30
  const value = rest.join('=').trim();
27
31
  try {
28
32
  return decodeURIComponent(value || '');
@@ -80,4 +80,41 @@ describe('request security runtime', () => {
80
80
  headers: { ...request.headers, origin: 'https://evil.example.com' },
81
81
  })).toBe(false);
82
82
  });
83
+
84
+ test('reads the slot for the request port when a browser shares jars across LAN ports', () => {
85
+ const runtime = createRuntime();
86
+ // Two instances on one LAN IP: the browser sends BOTH session cookies to
87
+ // either port (browsers key cookie jars on host, not port — issue #2377).
88
+ const req = {
89
+ headers: {
90
+ host: '192.168.0.1:3001',
91
+ cookie: 'oc_ui_session_3000=token-a; oc_ui_session_3001=token-b',
92
+ },
93
+ };
94
+ expect(runtime.getUiSessionTokenFromRequest(req)).toBe('token-b');
95
+ });
96
+
97
+ test('never reads another port cookie when this port has none of its own', () => {
98
+ const runtime = createRuntime();
99
+ // Reaching :3001 with only the :3000 cookie in the jar must NOT borrow the
100
+ // other instance's session — it stays unauthenticated for this port.
101
+ const req = {
102
+ headers: { host: '192.168.0.1:3001', cookie: 'oc_ui_session_3000=token-a' },
103
+ };
104
+ expect(runtime.getUiSessionTokenFromRequest(req)).toBeNull();
105
+ });
106
+
107
+ test('reads the bare cookie for a host without an explicit port', () => {
108
+ const runtime = createRuntime();
109
+ const req = {
110
+ headers: { host: '192.168.0.1', cookie: 'oc_ui_session=token-bare' },
111
+ };
112
+ expect(runtime.getUiSessionTokenFromRequest(req)).toBe('token-bare');
113
+ });
114
+
115
+ test('returns null when no session cookie is present', () => {
116
+ const runtime = createRuntime();
117
+ const req = { headers: { host: '192.168.0.1:3000', cookie: 'theme=dark; oc_url_token=x' } };
118
+ expect(runtime.getUiSessionTokenFromRequest(req)).toBeNull();
119
+ });
83
120
  });
@@ -118,14 +118,26 @@ other runtime API.
118
118
  provider's base URL, resolved from (1) `provider.<id>.options.baseURL`
119
119
  in the OpenCode config, (2) the hardcoded `https://api.openai.com/v1`
120
120
  endpoint, (3) the selected model's endpoint OpenCode resolved at runtime,
121
- (4) the provider-level runtime endpoint, or (5) the provider's `api` field
122
- from the models.dev catalog. The credential follows
121
+ (4) the provider-level runtime endpoint, (5) the provider's `api` field
122
+ from the models.dev catalog, or (6) `SDK_DEFAULT_BASE_URLS` — providers
123
+ such as Groq, xAI, and Mistral keep their endpoint inside a dedicated AI
124
+ SDK package, so neither the catalog nor OpenCode reports one. The
125
+ credential follows
123
126
  the same shape: config `options.apiKey`, then the runtime credential, then
124
127
  the auth.json entry. `provider.<id>.options.headers` is sent with the
125
128
  request and overrides the bearer default, so gateways that authenticate on
126
129
  their own header work here exactly as they do in a chat turn. Configured API
127
130
  keys and header values honor OpenCode's `{env:NAME}` and `{file:path}`
128
131
  substitutions; file contents and resolved credentials remain server-side.
132
+ - `thinking: { type: 'disabled' }` is sent in two cases, mirroring OpenCode's
133
+ `transform.ts`. The GLM switch belongs to the endpoint, not the model
134
+ family: it goes to a provider whose id contains `zai`/`zhipu` or whose
135
+ base URL is on `api.z.ai`/`bigmodel.cn`, because the same GLM model served
136
+ by another provider (OpenCode Go) rejects the unknown field with a 400.
137
+ The MiniMax M3 switch goes only behind the `@ai-sdk/openai-compatible` and
138
+ `@ai-sdk/anthropic` adapters (runtime model, then catalog model, then
139
+ catalog provider `npm`); a provider that reports no adapter counts as
140
+ OpenAI-compatible.
129
141
  - The runtime credential is refused for providers listed in
130
142
  `OWN_CREDENTIAL_HANDLING`. Their branches need the stored entry rather than
131
143
  a bearer token: the clearest case is the ChatGPT-plan `openai` login, whose
@@ -20,6 +20,25 @@ const DEFAULT_MAX_OUTPUT_TOKENS = 4_000;
20
20
 
21
21
  const USER_AGENT = 'opencode/1.0 openchamber';
22
22
 
23
+ // Providers whose endpoint lives inside their dedicated AI SDK package, so the
24
+ // models.dev catalog carries no `api` URL and OpenCode reports none at runtime.
25
+ // Each of these serves OpenAI-compatible `/chat/completions` at the URL below.
26
+ const SDK_DEFAULT_BASE_URLS = new Map([
27
+ ['groq', 'https://api.groq.com/openai/v1'],
28
+ ['xai', 'https://api.x.ai/v1'],
29
+ ['mistral', 'https://api.mistral.ai/v1'],
30
+ ['cerebras', 'https://api.cerebras.ai/v1'],
31
+ ['togetherai', 'https://api.together.xyz/v1'],
32
+ ['deepinfra', 'https://api.deepinfra.com/v1/openai'],
33
+ ['perplexity', 'https://api.perplexity.ai'],
34
+ ['cohere', 'https://api.cohere.ai/compatibility/v1'],
35
+ ]);
36
+
37
+ // Where `thinking: { type: 'disabled' }` is a real switch rather than an
38
+ // unknown field (mirrors the gates in OpenCode's provider/transform.ts).
39
+ const ZAI_ENDPOINT_HOSTS = ['api.z.ai', 'bigmodel.cn'];
40
+ const MINIMAX_THINKING_ADAPTERS = new Set(['@ai-sdk/openai-compatible', '@ai-sdk/anthropic']);
41
+
23
42
  const mergeHeadersCaseInsensitive = (base, overrides) => {
24
43
  const merged = { ...base };
25
44
  for (const [name, value] of Object.entries(overrides || {})) {
@@ -765,7 +784,8 @@ export async function callSmallModel({ auth, catalog, workingDirectory, sessionI
765
784
  // selected model's endpoint OpenCode resolved at runtime, then to the
766
785
  // provider-level runtime endpoint. For a plugin provider, the runtime listing
767
786
  // is the only place those endpoints exist, and several are local proxies the
768
- // plugin itself runs. The openai provider also respects
787
+ // plugin itself runs. Last comes SDK_DEFAULT_BASE_URLS, for providers whose
788
+ // endpoint only their SDK package knows. The openai provider also respects
769
789
  // provider.openai.options.baseURL — OpenCode itself uses the same config for
770
790
  // all providers including openai.
771
791
  const provider = getCatalogProvider(catalog, providerID);
@@ -779,7 +799,7 @@ export async function callSmallModel({ auth, catalog, workingDirectory, sessionI
779
799
  ?? runtimeProvider?.baseURL
780
800
  ?? (typeof provider?.api === 'string' && provider.api
781
801
  ? provider.api
782
- : null);
802
+ : SDK_DEFAULT_BASE_URLS.get(providerID) ?? null);
783
803
  if (!baseURL) {
784
804
  throw new Error(`Provider "${providerID}" has no known API base URL`);
785
805
  }
@@ -790,11 +810,22 @@ export async function callSmallModel({ auth, catalog, workingDirectory, sessionI
790
810
  // parameter: unknown body fields 400 on some providers, so this stays an
791
811
  // explicit allowlist. Models without a switch (DeepSeek, Qwen, Kimi, …)
792
812
  // just get the generous output budget.
793
- const lowerModel = modelID.toLowerCase();
794
- const supportsThinkingToggle = providerID.includes('zai')
813
+ // GLM's switch belongs to the Z.ai/Zhipu endpoints, not to the model: the
814
+ // same GLM served by another provider (OpenCode Go) rejects the field. The
815
+ // host check keeps the switch for a custom-named provider pointed at them.
816
+ const servedByZai = providerID.includes('zai')
795
817
  || providerID.includes('zhipu')
796
- || lowerModel.includes('glm')
797
- || lowerModel.includes('minimax-m3');
818
+ || ZAI_ENDPOINT_HOSTS.some((host) => baseURL.includes(host));
819
+ // MiniMax M3's switch is understood only behind the adapters OpenCode sends
820
+ // it through. An adapter nobody reports (a custom provider) is OpenAI-
821
+ // compatible by OpenCode's own default.
822
+ const adapter = runtimeModel?.api?.npm
823
+ ?? provider?.models?.[modelID]?.provider?.npm
824
+ ?? provider?.npm
825
+ ?? null;
826
+ const minimaxSwitch = modelID.toLowerCase().includes('minimax-m3')
827
+ && (adapter === null || MINIMAX_THINKING_ADAPTERS.has(adapter));
828
+ const supportsThinkingToggle = servedByZai || minimaxSwitch;
798
829
  const extraBody = supportsThinkingToggle ? { thinking: { type: 'disabled' } } : undefined;
799
830
 
800
831
  return callOpenaiCompatible({
@@ -772,6 +772,188 @@ describe('callSmallModel — Google thinking configuration', () => {
772
772
  });
773
773
  });
774
774
 
775
+ describe('callSmallModel — OpenAI-compatible thinking toggle', () => {
776
+ let fetchMock;
777
+ let originalFetch;
778
+
779
+ beforeEach(() => {
780
+ fetchMock = vi.fn();
781
+ originalFetch = globalThis.fetch;
782
+ globalThis.fetch = fetchMock;
783
+ readConfig.mockReset();
784
+ readConfig.mockReturnValue({});
785
+ readConfigLayers.mockReset();
786
+ readConfigLayers.mockReturnValue({ mergedConfig: {} });
787
+ });
788
+
789
+ afterEach(() => {
790
+ globalThis.fetch = originalFetch;
791
+ });
792
+
793
+ const requestBody = async ({ providerID, modelID }) => {
794
+ fetchMock.mockResolvedValue(ok('done'));
795
+ await callSmallModel({
796
+ auth: { [providerID]: { type: 'api', key: 'sk-test' } },
797
+ catalog: { [providerID]: { id: providerID, api: 'https://provider.test/v1', models: {} } },
798
+ workingDirectory: '/proj',
799
+ providerID,
800
+ modelID,
801
+ prompt: 'summarize',
802
+ });
803
+ return JSON.parse(lastCall(fetchMock).init.body);
804
+ };
805
+
806
+ it('does not send the thinking field for a GLM model served by OpenCode Go', async () => {
807
+ const body = await requestBody({ providerID: 'opencode-go', modelID: 'glm-5.3-flash' });
808
+ expect(body.thinking).toBeUndefined();
809
+ });
810
+
811
+ it('disables thinking on the Z.ai and Zhipu endpoints', async () => {
812
+ const zai = await requestBody({ providerID: 'zai-coding-plan', modelID: 'glm-5.3-flash' });
813
+ expect(zai.thinking).toEqual({ type: 'disabled' });
814
+ const zhipu = await requestBody({ providerID: 'zhipuai', modelID: 'glm-5.3-flash' });
815
+ expect(zhipu.thinking).toEqual({ type: 'disabled' });
816
+ });
817
+
818
+ it('keeps the GLM switch for a custom-named provider pointed at a Z.ai endpoint', async () => {
819
+ fetchMock.mockResolvedValue(ok('done'));
820
+ await callSmallModel({
821
+ auth: { 'my-glm': { type: 'api', key: 'sk-test' } },
822
+ catalog: { 'my-glm': { id: 'my-glm', api: 'https://api.z.ai/api/coding/paas/v4', models: {} } },
823
+ workingDirectory: '/proj',
824
+ providerID: 'my-glm',
825
+ modelID: 'glm-5.3-flash',
826
+ prompt: 'summarize',
827
+ });
828
+ expect(JSON.parse(lastCall(fetchMock).init.body).thinking).toEqual({ type: 'disabled' });
829
+ });
830
+
831
+ const minimaxBody = async (providerEntry) => {
832
+ fetchMock.mockResolvedValue(ok('done'));
833
+ await callSmallModel({
834
+ auth: { host: { type: 'api', key: 'sk-test' } },
835
+ catalog: { host: { id: 'host', api: 'https://provider.test/v1', ...providerEntry } },
836
+ workingDirectory: '/proj',
837
+ providerID: 'host',
838
+ modelID: 'minimax-m3',
839
+ prompt: 'summarize',
840
+ });
841
+ return JSON.parse(lastCall(fetchMock).init.body);
842
+ };
843
+
844
+ it('disables MiniMax M3 thinking behind an OpenAI-compatible adapter', async () => {
845
+ const body = await minimaxBody({ npm: '@ai-sdk/openai-compatible', models: {} });
846
+ expect(body.thinking).toEqual({ type: 'disabled' });
847
+ });
848
+
849
+ it('honors a model-level adapter over the provider one for MiniMax M3', async () => {
850
+ const body = await minimaxBody({
851
+ npm: '@ai-sdk/deepinfra',
852
+ models: { 'minimax-m3': { provider: { npm: '@ai-sdk/anthropic' } } },
853
+ });
854
+ expect(body.thinking).toEqual({ type: 'disabled' });
855
+ });
856
+
857
+ it('does not send the MiniMax M3 switch through an adapter that has none', async () => {
858
+ const body = await minimaxBody({ npm: '@ai-sdk/deepinfra', models: {} });
859
+ expect(body.thinking).toBeUndefined();
860
+ });
861
+
862
+ it('treats a provider with no reported adapter as OpenAI-compatible for MiniMax M3', async () => {
863
+ const body = await minimaxBody({ models: {} });
864
+ expect(body.thinking).toEqual({ type: 'disabled' });
865
+ });
866
+ });
867
+
868
+ describe('callSmallModel — providers with an SDK-owned endpoint', () => {
869
+ let fetchMock;
870
+ let originalFetch;
871
+
872
+ beforeEach(() => {
873
+ fetchMock = vi.fn();
874
+ originalFetch = globalThis.fetch;
875
+ globalThis.fetch = fetchMock;
876
+ readConfig.mockReset();
877
+ readConfig.mockReturnValue({});
878
+ readConfigLayers.mockReset();
879
+ readConfigLayers.mockReturnValue({ mergedConfig: {} });
880
+ });
881
+
882
+ afterEach(() => {
883
+ globalThis.fetch = originalFetch;
884
+ });
885
+
886
+ it.each([
887
+ ['groq', 'https://api.groq.com/openai/v1'],
888
+ ['xai', 'https://api.x.ai/v1'],
889
+ ['mistral', 'https://api.mistral.ai/v1'],
890
+ ['cerebras', 'https://api.cerebras.ai/v1'],
891
+ ['togetherai', 'https://api.together.xyz/v1'],
892
+ ['deepinfra', 'https://api.deepinfra.com/v1/openai'],
893
+ ['perplexity', 'https://api.perplexity.ai'],
894
+ ['cohere', 'https://api.cohere.ai/compatibility/v1'],
895
+ ])('calls %s at its default endpoint when the catalog has no api URL', async (providerID, baseURL) => {
896
+ fetchMock.mockResolvedValue(ok('done'));
897
+
898
+ const text = await callSmallModel({
899
+ auth: { [providerID]: { type: 'api', key: 'key-test' } },
900
+ catalog: { [providerID]: { id: providerID, api: null, models: {} } },
901
+ workingDirectory: '/proj',
902
+ providerID,
903
+ modelID: 'some-model',
904
+ prompt: 'summarize',
905
+ });
906
+
907
+ expect(text).toBe('done');
908
+ expect(lastCall(fetchMock).url).toBe(`${baseURL}/chat/completions`);
909
+ });
910
+
911
+ it('prefers a catalog api URL over the default endpoint', async () => {
912
+ fetchMock.mockResolvedValue(ok('done'));
913
+
914
+ await callSmallModel({
915
+ auth: { groq: { type: 'api', key: 'key-test' } },
916
+ catalog: { groq: { id: 'groq', api: 'https://catalog.test/v1', models: {} } },
917
+ workingDirectory: '/proj',
918
+ providerID: 'groq',
919
+ modelID: 'some-model',
920
+ prompt: 'summarize',
921
+ });
922
+
923
+ expect(lastCall(fetchMock).url).toBe('https://catalog.test/v1/chat/completions');
924
+ });
925
+
926
+ it('prefers a configured baseURL over the default endpoint', async () => {
927
+ readConfig.mockReturnValue({
928
+ provider: { groq: { options: { baseURL: 'https://proxy.test/v1' } } },
929
+ });
930
+ fetchMock.mockResolvedValue(ok('done'));
931
+
932
+ await callSmallModel({
933
+ auth: { groq: { type: 'api', key: 'key-test' } },
934
+ catalog: { groq: { id: 'groq', api: null, models: {} } },
935
+ workingDirectory: '/proj',
936
+ providerID: 'groq',
937
+ modelID: 'some-model',
938
+ prompt: 'summarize',
939
+ });
940
+
941
+ expect(lastCall(fetchMock).url).toBe('https://proxy.test/v1/chat/completions');
942
+ });
943
+
944
+ it('still fails clearly for a provider with no endpoint anywhere', async () => {
945
+ await expect(callSmallModel({
946
+ auth: { mystery: { type: 'api', key: 'sk-test' } },
947
+ catalog: {},
948
+ workingDirectory: '/proj',
949
+ providerID: 'mystery',
950
+ modelID: 'some-model',
951
+ prompt: 'summarize',
952
+ })).rejects.toThrow('has no known API base URL');
953
+ expect(fetchMock).not.toHaveBeenCalled();
954
+ });
955
+ });
956
+
775
957
  describe('callSmallModel — GitHub Copilot endpoint routing', () => {
776
958
  let fetchMock;
777
959
  let originalFetch;
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Ownership
4
4
 
5
- `runtime.js` owns terminal identity, PTY processes, launch mode, session purpose, ordered output, bounded scrollback, WebSocket attachments, and lifecycle routes. `shells.js` discovers executable shell families, resolves the persisted shell ID, and builds the per-shell argv for interactive versus command launches. Clients own tab arrangement. Interactive terminals use stable IDs; project actions keep a stable UI tab and allocate a fresh terminal ID for each command execution. Electron uses this same runtime in-process; VS Code returns an explicit unsupported error.
5
+ `runtime.js` owns terminal identity, PTY processes, launch mode, session purpose, ordered output, bounded scrollback, WebSocket attachments, and lifecycle routes. `shutdown.js` owns bounded runtime-wide process cleanup. `shells.js` discovers executable shell families, resolves the persisted shell ID, and builds the per-shell argv for interactive versus command launches. Clients own tab arrangement. Interactive terminals use stable IDs; project actions keep a stable UI tab and allocate a fresh terminal ID for each command execution. Electron uses this same runtime in-process; VS Code returns an explicit unsupported error.
6
6
 
7
7
  ## Protocol
8
8
 
@@ -37,8 +37,11 @@ HTTP remains the authenticated command plane for create, resize, appearance upda
37
37
  - Deduplicated create responses may describe another client's execution. Cancellation cleanup closes only the terminal ID allocated for the cancelled request; it never closes an adopted peer execution.
38
38
  - Create and restart validate the working directory with a real `stat` and answer HTTP 400 `Invalid working directory` when it is not a directory. When the path does not exist at all (`ENOENT`/`ENOTDIR`, a worktree deleted outside OpenChamber) the body also carries `code: "TERMINAL_CWD_MISSING"`. The client shows the failure without moving the session. The runtime never substitutes a parent directory on its own.
39
39
  - Restarts are serialized per terminal. Each restart spawns and wires the replacement before terminating the old process, retaining the terminal ID. Command-mode sessions reject restart with HTTP 400 instead of silently turning into interactive shells with stale action metadata.
40
+ - A restart rechecks session ownership before and after PTY creation. Close, force-kill, idle removal, or shutdown can invalidate it while spawn is pending. A late replacement is terminated instead of being attached to a retired session.
40
41
  - A delete that arrives while create is still pending leaves a cancellation tombstone. When the PTY arrives, the runtime terminates it immediately, never inserts the session into the live map, and returns a create error while the delete still succeeds.
41
- - Close uses SIGTERM with bounded SIGKILL escalation. Force-kill, idle cleanup, and runtime shutdown terminate process groups immediately where supported. Removal explicitly sends a fatal scoped closure and evicts client projections even when a PTY backend fails to emit `onExit`; attached terminals are not considered idle.
42
+ - Close uses SIGTERM with bounded SIGKILL escalation. Force-kill and idle cleanup terminate process groups immediately where supported. Removal explicitly sends a fatal scoped closure and evicts client projections even when a PTY backend fails to emit `onExit`; attached terminals are not considered idle.
43
+ - Shutdown is single-flight, rejects new creates, cancels pending creates, and waits for pending creates and restarts before clearing live sessions. A PTY that arrives during shutdown follows the same cancellation cleanup as a pending create that was explicitly closed.
44
+ - On POSIX, shutdown disconnects input and gives terminal workloads a shared 20-second SIGTERM grace. Process-table snapshots retain descendant ownership across shell exit and separate job-control groups; PID start times guard against reused PIDs. Linux reads `/proc` directly so minimal containers do not need `ps`. Shells remain open until the workloads exit, then receive SIGHUP. An exec'ed server receives SIGTERM and the full grace even though it occupies the shell PID. Remaining processes receive SIGKILL at the deadline. A process-table failure warns and forces known owned processes closed. Windows retains ConPTY-owned teardown because it has no POSIX signal protocol.
42
45
 
43
46
  ## Security And Relay
44
47
 
@@ -50,5 +53,23 @@ Run:
50
53
 
51
54
  ```sh
52
55
  bun test packages/web/server/lib/terminal/runtime.test.js packages/web/server/lib/terminal/terminal-ws-protocol.test.js
56
+ bun run --cwd packages/web test server/lib/terminal/shutdown.test.js
53
57
  bun test packages/web/server/lib/ui-auth/ui-auth.test.js packages/web/server/lib/relay/cross-compat.test.js
54
58
  ```
59
+
60
+ The #3389 macOS ARM64 reproduction used real Electron 43.7.0 PTYs and HTTP
61
+ servers with signal handlers that took 15 seconds to finish cleanup. On the
62
+ baseline, File > Restart and Quit sent SIGKILL to the exec'ed server within
63
+ 3–4 ms; a foreground child in another process group received SIGHUP and outlived
64
+ Electron. After the fix, both workloads completed after SIGTERM before Electron
65
+ exited, in bundled and HMR runs. A noncooperative workload was killed after the
66
+ shared 20-second deadline.
67
+
68
+ Packaged test builds 1.24.90 and 1.24.91 exercised the real macOS Squirrel install
69
+ with a loopback feed, isolated app identity/profile, and a shared test signing
70
+ requirement. Both the normal update and closing the window during cleanup
71
+ installed and relaunched N+1 after the terminal workloads finished. The installed
72
+ ASAR matched N+1, and OpenCode remained bundled after its own restart. A rejected
73
+ signature also reached the installer error handler after the 15-second cleanup.
74
+ Linux ARM64 process regressions pass without `ps` installed. Native Windows
75
+ ConPTY and Linux Electron installer behavior were not exercised for this fix.
@@ -11,6 +11,7 @@ import { sanitizeTerminalHistoryChunk } from './history.js';
11
11
  import { consumeTerminalThemeQueries, terminalThemeModeReport } from './theme-response.js';
12
12
  import { buildTerminalShellLaunch, createTerminalShellResolver, normalizeTerminalShell } from './shells.js';
13
13
  import { stripAppImageArgv0Leak, resolvePosixPtyLaunch } from '../inherited-env.js';
14
+ import { shutdownTerminalProcesses } from './shutdown.js';
14
15
 
15
16
  const MAX_SESSIONS = 20;
16
17
  const MAX_HISTORY_BYTES = 512 * 1024;
@@ -91,7 +92,7 @@ const trimHistory = (history) => {
91
92
  export function createTerminalRuntime({
92
93
  app, server, fs, path, uiAuthController, buildAugmentedPath, searchPathFor, isExecutable,
93
94
  isRequestOriginAllowed, rejectWebSocketUpgrade, TERMINAL_INPUT_WS_HEARTBEAT_INTERVAL_MS,
94
- loadPtyProvider, terminalTerminationGraceMs = TERMINATION_GRACE_MS,
95
+ loadPtyProvider, terminalTerminationGraceMs = TERMINATION_GRACE_MS, shutdownProcesses = shutdownTerminalProcesses,
95
96
  }) {
96
97
  const sessions = new Map();
97
98
  const pendingSessionCreates = new Map();
@@ -100,6 +101,7 @@ export function createTerminalRuntime({
100
101
  const pendingTerminations = new Set();
101
102
  const runtime = typeof globalThis.Bun === 'undefined' ? 'node' : 'bun';
102
103
  let ptyProviderPromise = null;
104
+ let shutdownPromise = null;
103
105
  let wsServer = new WebSocketServer({ noServer: true, maxPayload: TERMINAL_WS_MAX_PAYLOAD_BYTES });
104
106
  const shellResolver = createTerminalShellResolver({ fs, path, searchPathFor, isExecutable, buildAugmentedPath });
105
107
 
@@ -135,7 +137,7 @@ export function createTerminalRuntime({
135
137
  const launch = resolvePosixPtyLaunch(shellLaunch.executable, shellLaunch.args);
136
138
  const options = { name: 'xterm-256color', cwd, cols, rows, env };
137
139
  if (process.platform === 'win32') options.useConpty = true;
138
- return { process: await provider.spawn(launch.executable, launch.args, options), backend: provider.backend, shell: resolvedShell.id, loginShell };
140
+ return { process: await provider.spawn(launch.executable, launch.args, options), backend: provider.backend, shell: resolvedShell.id, shellExecutable: executable, loginShell };
139
141
  } catch (error) { lastError = error; }
140
142
  }
141
143
  throw lastError ?? new Error('No executable shell found');
@@ -273,6 +275,7 @@ export function createTerminalRuntime({
273
275
  if (clear) { session.history = ''; session.pendingHistoryControlSequence = ''; session.pendingThemeControlSequence = ''; session.themeModeEnabled = false; }
274
276
  session.cwd = cwd; session.cols = cols; session.rows = rows; session.process = spawned.process;
275
277
  session.backend = spawned.backend; session.shell = spawned.shell; session.loginShell = spawned.loginShell; session.status = 'running'; session.exitCode = null; session.signal = null;
278
+ session.shellExecutable = spawned.shellExecutable;
276
279
  session.mode = mode; session.command = mode === COMMAND_TERMINAL_MODE ? command : null;
277
280
  session.purpose = purpose;
278
281
  session.themeMode = themeMode === 'light' ? 'light' : 'dark'; session.terminalBackground = terminalBackground; session.terminalForeground = terminalForeground;
@@ -282,6 +285,7 @@ export function createTerminalRuntime({
282
285
  };
283
286
 
284
287
  const createSession = async ({ sessionId, cwd, cols = 80, rows = 24, themeMode, terminalBackground, terminalForeground, shell = 'auto', loginShell = false, mode, command, purpose }) => {
288
+ if (shutdownPromise) throw new Error('Terminal runtime is shutting down');
285
289
  if (!validateSize(cols, 1000) || !validateSize(rows, 500)) throw new Error('Invalid terminal dimensions');
286
290
  if (typeof loginShell !== 'boolean') throw new Error('Invalid terminal login mode');
287
291
  const normalizedShell = normalizeTerminalShell(shell);
@@ -507,11 +511,17 @@ export function createTerminalRuntime({
507
511
  const previousRestart = pendingSessionRestarts.get(session.id) ?? Promise.resolve();
508
512
  const restart = previousRestart.catch(() => {}).then(async () => {
509
513
  await validateCwd(cwd);
514
+ if (shutdownPromise || sessions.get(session.id) !== session) throw new Error('Terminal session was closed during restart');
510
515
  if (!validateSize(cols, 1000) || !validateSize(rows, 500)) throw new Error('Invalid terminal dimensions');
511
516
  if (typeof loginShell !== 'boolean') throw new Error('Invalid terminal login mode');
512
517
  const oldProcess = session.process;
513
518
  const spawned = await spawnPty({ cwd, cols, rows, themeMode, shell, loginShell });
519
+ if (shutdownPromise || sessions.get(session.id) !== session) {
520
+ await terminateProcess(spawned.process, true);
521
+ throw new Error('Terminal session was closed during restart');
522
+ }
514
523
  session.process = spawned.process; session.backend = spawned.backend; session.shell = spawned.shell; session.loginShell = spawned.loginShell; session.cwd = cwd; session.cols = cols; session.rows = rows;
524
+ session.shellExecutable = spawned.shellExecutable;
515
525
  session.history = ''; session.pendingHistoryControlSequence = ''; session.pendingThemeControlSequence = ''; session.themeModeEnabled = false; session.status = 'running'; session.exitCode = null; session.signal = null; session.eventQueue.length = 0;
516
526
  session.themeMode = themeMode === 'light' ? 'light' : 'dark'; session.terminalBackground = terminalBackground; session.terminalForeground = terminalForeground;
517
527
  wire(session, spawned.process); void terminateProcess(oldProcess); publish(session, { t: 'restarted', history: '' });
@@ -560,11 +570,19 @@ export function createTerminalRuntime({
560
570
  }
561
571
  }, 5 * 60 * 1000);
562
572
 
563
- const shutdown = async () => {
573
+ const stop = async () => {
564
574
  server.off('upgrade', upgradeHandler); clearInterval(idleSweep);
565
- await Promise.allSettled([...pendingSessionRestarts.values()]);
566
- for (const session of sessions.values()) void terminateProcess(session.process, true);
575
+ for (const client of wsServer?.clients ?? []) client.terminate();
576
+ for (const pending of pendingSessionCreates.values()) pending.cancelled = true;
577
+ await Promise.allSettled([
578
+ ...[...pendingSessionCreates.values()].map((pending) => pending.promise),
579
+ ...pendingSessionRestarts.values(),
580
+ ]);
581
+ const terminals = [...sessions.values()]
582
+ .filter(session => session.status === 'running' && session.process)
583
+ .map(session => ({ process: session.process, shellExecutable: session.shellExecutable }));
567
584
  sessions.clear();
585
+ await shutdownProcesses(terminals);
568
586
  await Promise.allSettled([...pendingTerminations]);
569
587
  if (!wsServer) return;
570
588
  for (const client of wsServer.clients) client.terminate();
@@ -574,5 +592,9 @@ export function createTerminalRuntime({
574
592
  ]);
575
593
  wsServer = null;
576
594
  };
595
+ const shutdown = () => {
596
+ if (!shutdownPromise) shutdownPromise = stop();
597
+ return shutdownPromise;
598
+ };
577
599
  return { shutdown };
578
600
  }