@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,203 @@
1
+ import { createServer } from 'node:http';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { createSessionAssistRuntime } from './runtime.js';
4
+
5
+ const resources = [];
6
+ const message = (id, role, text, extra = {}) => ({
7
+ info: { id, role, parentID: 'user', finish: 'stop', time: { completed: 1 }, providerID: 'test-provider', modelID: 'test-model', ...extra },
8
+ parts: [{ type: 'text', text }],
9
+ });
10
+ const output = (recap = 'Зміни готові', suggestion = '') => ({ text: JSON.stringify({ recap, suggestion }), providerID: 'test-provider', modelID: 'test-model' });
11
+ const pause = () => new Promise((resolve) => setTimeout(resolve, 15));
12
+
13
+ async function fixture(generate = async () => output()) {
14
+ const state = {
15
+ messages: [message('user', 'user', 'Виправ помилку'), message('answer', 'assistant', 'Виправлено')],
16
+ session: { id: 'session', directory: '/project', time: {}, metadata: { external: 'keep', openchamber: { note: 'keep' } } },
17
+ targets: { recap: true, suggestion: true },
18
+ gets: 0, failFresh: false, patches: [], requests: [], calls: [],
19
+ };
20
+ const server = createServer(async (request, response) => {
21
+ const url = new URL(request.url, 'http://localhost');
22
+ state.requests.push({ method: request.method, directory: url.searchParams.get('directory'), limit: url.searchParams.get('limit'), auth: request.headers['x-test-auth'] });
23
+ response.setHeader('content-type', 'application/json');
24
+ if (request.method === 'PATCH') {
25
+ const chunks = [];
26
+ for await (const chunk of request) chunks.push(chunk);
27
+ state.patches.push(JSON.parse(Buffer.concat(chunks).toString()));
28
+ response.end(JSON.stringify(state.session));
29
+ } else if (url.pathname.endsWith('/message')) {
30
+ response.end(JSON.stringify(url.searchParams.get('limit') === '1' ? state.messages.slice(-1) : state.messages));
31
+ } else {
32
+ state.gets++;
33
+ response.statusCode = state.failFresh && state.gets > 1 ? 500 : 200;
34
+ response.end(JSON.stringify(state.session));
35
+ }
36
+ });
37
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
38
+ const base = `http://127.0.0.1:${server.address().port}`;
39
+ state.base = base;
40
+ const runtime = createSessionAssistRuntime({
41
+ buildOpenCodeUrl: (route) => state.base + route,
42
+ getOpenCodeAuthHeaders: () => ({ 'x-test-auth': 'fixture' }),
43
+ getTargets: () => state.targets,
44
+ quietMs: 1,
45
+ getSmallModelService: async () => ({
46
+ describeSmallModel: async () => ({ inputCharBudget: 64_000 }),
47
+ generateSmallModelText: async (args) => { state.calls.push(args); return generate(args, state); },
48
+ }),
49
+ });
50
+ resources.push({ runtime, server });
51
+ const status = (type) => runtime.processPayload({ type: 'session.status', properties: { sessionID: 'session', status: { type } } }, '/project');
52
+ return { state, runtime, status };
53
+ }
54
+
55
+ afterEach(async () => {
56
+ for (const { runtime, server } of resources.splice(0)) {
57
+ runtime.stop();
58
+ server.closeAllConnections();
59
+ await new Promise((resolve) => server.close(resolve));
60
+ }
61
+ vi.restoreAllMocks();
62
+ });
63
+
64
+ describe('session assist runtime', () => {
65
+ it('uses bounded authenticated SDK reads and preserves metadata with an empty suggestion', async () => {
66
+ const { state, status } = await fixture(async (_args, current) => {
67
+ current.session.metadata.openchamber.concurrent = 'new';
68
+ return output();
69
+ });
70
+ status('idle');
71
+ await vi.waitFor(() => expect(state.patches).toHaveLength(1));
72
+ expect(state.requests.every((r) => r.directory === '/project' && r.auth === 'fixture')).toBe(true);
73
+ expect(state.requests.filter((r) => r.limit).map((r) => r.limit)).toEqual(['50', '1']);
74
+ expect(state.calls[0]).toMatchObject({ restrictToPreferredProvider: true, onOverflow: 'error', preferredProviderID: 'test-provider', preferredModelID: 'test-model', sessionID: 'session' });
75
+ expect(state.patches[0].metadata).toMatchObject({ external: 'keep', openchamber: {
76
+ note: 'keep', concurrent: 'new', assist: { recap: 'Зміни готові', suggestion: '', forMessageID: 'answer' },
77
+ } });
78
+ });
79
+
80
+ it('does no work with both settings off and skips child, archived, or reverted sessions', async () => {
81
+ const { state, status } = await fixture();
82
+ state.targets = { recap: false, suggestion: false };
83
+ status('idle');
84
+ await pause();
85
+ expect(state.requests).toHaveLength(0);
86
+ state.targets.recap = true;
87
+ state.session.parentID = 'parent';
88
+ status('idle');
89
+ await vi.waitFor(() => expect(state.gets).toBe(1));
90
+ delete state.session.parentID;
91
+ state.session.revert = { messageID: 'user' };
92
+ status('idle');
93
+ await vi.waitFor(() => expect(state.gets).toBe(2));
94
+ delete state.session.revert;
95
+ state.session.time.archived = 1;
96
+ status('idle');
97
+ await vi.waitFor(() => expect(state.gets).toBe(3));
98
+ expect(state.calls).toHaveLength(0);
99
+ });
100
+
101
+ it('keeps recent context for recap-only and performs no write for an empty suggestion-only result', async () => {
102
+ const { state, status } = await fixture();
103
+ state.targets.suggestion = false;
104
+ state.messages.unshift(message('previous-user', 'user', 'Попередня задача'), message('previous-answer', 'assistant', 'Зміст зробленого', { parentID: 'previous-user' }));
105
+ status('idle');
106
+ await vi.waitFor(() => expect(state.patches).toHaveLength(1));
107
+ expect(state.calls[0].prompt).toContain('Зміст зробленого');
108
+ expect(state.calls[0].system).not.toContain('suggestion');
109
+ state.targets = { recap: false, suggestion: true };
110
+ status('idle');
111
+ await vi.waitFor(() => expect(state.calls).toHaveLength(2));
112
+ await pause();
113
+ expect(state.patches).toHaveLength(1);
114
+ });
115
+
116
+ it('does not write a stale result when the tail moves during generation', async () => {
117
+ const { state, status } = await fixture(async (_args, current) => {
118
+ current.messages.push(message('new-user', 'user', 'Нова задача'));
119
+ return output();
120
+ });
121
+ status('idle');
122
+ await vi.waitFor(() => expect(state.requests.some((r) => r.limit === '1')).toBe(true));
123
+ await pause();
124
+ expect(state.patches).toHaveLength(0);
125
+ });
126
+
127
+ it('does not fall back to stale metadata when the fresh read fails', async () => {
128
+ const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
129
+ const { state, status } = await fixture();
130
+ state.failFresh = true;
131
+ status('idle');
132
+ await vi.waitFor(() => expect(warning).toHaveBeenCalled());
133
+ expect(state.gets).toBe(2);
134
+ expect(state.patches).toHaveLength(0);
135
+ });
136
+
137
+ it('cancels old work and retains an expired newer idle timer until it can run', async () => {
138
+ const releases = [];
139
+ const { state, status } = await fixture(() => new Promise((resolve) => releases.push(resolve)));
140
+ status('idle');
141
+ await vi.waitFor(() => expect(releases).toHaveLength(1));
142
+ status('busy');
143
+ expect(state.calls[0].signal.aborted).toBe(true);
144
+ state.messages.push(message('next-user', 'user', 'Далі'), message('next-answer', 'assistant', 'Готово', { parentID: 'next-user' }));
145
+ status('idle');
146
+ await pause();
147
+ releases[0](output('Старий результат'));
148
+ await vi.waitFor(() => expect(releases).toHaveLength(2));
149
+ expect(state.patches).toHaveLength(0);
150
+ releases[1](output('Новий результат'));
151
+ await vi.waitFor(() => expect(state.patches).toHaveLength(1));
152
+ expect(state.patches[0].metadata.openchamber.assist).toMatchObject({ recap: 'Новий результат', forMessageID: 'next-answer' });
153
+ });
154
+
155
+ it('aborts on stop and honors settings switched off during generation', async () => {
156
+ let release;
157
+ const { state, runtime, status } = await fixture(() => new Promise((resolve) => { release = resolve; }));
158
+ status('idle');
159
+ await vi.waitFor(() => expect(state.calls).toHaveLength(1));
160
+ runtime.stop();
161
+ expect(state.calls[0].signal.aborted).toBe(true);
162
+ release(output());
163
+ await pause();
164
+ expect(state.patches).toHaveLength(0);
165
+ const second = await fixture(async (_args, current) => {
166
+ current.targets = { recap: false, suggestion: false };
167
+ return output();
168
+ });
169
+ second.status('idle');
170
+ await vi.waitFor(() => expect(second.state.gets).toBe(2));
171
+ await pause();
172
+ expect(second.state.patches).toHaveLength(0);
173
+ });
174
+
175
+ it('ignores historical user updates but cancels a new request during generation', async () => {
176
+ let release;
177
+ const { state, runtime, status } = await fixture(() => new Promise((resolve) => { release = resolve; }));
178
+ status('idle');
179
+ await vi.waitFor(() => expect(state.calls).toHaveLength(1));
180
+ const userUpdate = (created) => runtime.processPayload({ type: 'message.updated', properties: {
181
+ info: { id: 'user', sessionID: 'session', role: 'user', time: { created } },
182
+ } });
183
+ userUpdate(1);
184
+ expect(state.calls[0].signal.aborted).toBe(false);
185
+ userUpdate(Date.now());
186
+ expect(state.calls[0].signal.aborted).toBe(true);
187
+ release(output());
188
+ await pause();
189
+ expect(state.patches).toHaveLength(0);
190
+ });
191
+
192
+ it('rejects endpoint changes before writing instead of carrying a session into a new runtime', async () => {
193
+ const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
194
+ const { state, status } = await fixture(async (_args, current) => {
195
+ current.base = 'http://unreachable.invalid';
196
+ return output();
197
+ });
198
+ status('idle');
199
+ await vi.waitFor(() => expect(warning).toHaveBeenCalled());
200
+ expect(state.patches).toHaveLength(0);
201
+ expect(state.gets).toBe(1);
202
+ });
203
+ });
@@ -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
  }