@vantran-se/openchamber-web 1.23.0 → 1.24.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/bin/cli.test.js +29 -6
  2. package/bin/lib/cli-http.js +5 -1
  3. package/bin/lib/commands-status.js +5 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-DFg2wnpo.js → CodeMirrorCommentWidgets-BjJHrzmU.js} +1 -1
  5. package/dist/assets/DiffView-COVQhWGd.js +4 -0
  6. package/dist/assets/FilesView-B1V5JxFX.js +13 -0
  7. package/dist/assets/GitView-CDOJy5Ay.js +6 -0
  8. package/dist/assets/GuestAttachDialog-D3S0Rjic.js +1 -0
  9. package/dist/assets/{JsonTreeView-BKdsjg5B.js → JsonTreeView-D0C9nU7K.js} +1 -1
  10. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  12. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  30. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  33. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  39. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  42. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  54. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  57. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  60. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  62. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  65. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  68. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  69. package/dist/assets/LinearIssuesView-D7FKg1XJ.js +1 -0
  70. package/dist/assets/{MarkdownImageGallery-_5gEVr0X.js → MarkdownImageGallery-BrFjAVEf.js} +1 -1
  71. package/dist/assets/MarkdownRendererImpl-DWepTafh.js +12 -0
  72. package/dist/assets/{OnboardingScreen-DdP4_nuc.js → OnboardingScreen-Dyxxiqdt.js} +1 -1
  73. package/dist/assets/{PatchDiff-am3SAss2.js → PatchDiff-DnEhjXcC.js} +1 -1
  74. package/dist/assets/PierreDiffViewer-DyxZVVVR.js +210 -0
  75. package/dist/assets/{PlanView-BoJxdRZe.js → PlanView-LW2z_EAP.js} +4 -4
  76. package/dist/assets/PluginPane-B4tKno2h.js +3 -0
  77. package/dist/assets/SettingsView-DEaMulN9.js +29 -0
  78. package/dist/assets/{SettingsWindow-DVTdDSol.js → SettingsWindow-FLDJXOgQ.js} +1 -1
  79. package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +1 -0
  80. package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +15 -0
  81. package/dist/assets/{ToolPartDiffPreview-C7WLk5i9.js → ToolPartDiffPreview-CkR4msKP.js} +2 -2
  82. package/dist/assets/{VirtualizedFileDiff-DcdgkH-x.js → VirtualizedFileDiff-0pyr_mHY.js} +4 -4
  83. package/dist/assets/WalkthroughView-yNj4nOP5.js +4 -0
  84. package/dist/assets/appThemeRegistry-BuUKhbYQ.js +1 -0
  85. package/dist/assets/commitAndPush-DvJKa3Ks.js +1 -0
  86. package/dist/assets/{computeVirtualFileMetrics-C8byMnRh.js → computeVirtualFileMetrics-DxTBMuu-.js} +2 -2
  87. package/dist/assets/de-DgxgFyGG.js +10 -0
  88. package/dist/assets/es-BFsZj5F_.js +16 -0
  89. package/dist/assets/fr-jdZ5KKQi.js +14 -0
  90. package/dist/assets/{index-BB7ryz8y.js → index-CKHjPKbq.js} +1 -1
  91. package/dist/assets/index-Cbci4i1W.js +40 -0
  92. package/dist/assets/index-DqaMWNrC.css +1 -0
  93. package/dist/assets/{isDiffPlainText-45ujMPGb.js → isDiffPlainText-D7FkBMP6.js} +1 -1
  94. package/dist/assets/{isFilePlainText-Cc4KFSYi.js → isFilePlainText-CL2vsrIZ.js} +1 -1
  95. package/dist/assets/ja-C5kKrhO4.js +16 -0
  96. package/dist/assets/ko-DXDTT_0O.js +16 -0
  97. package/dist/assets/main-CZALtVek.js +896 -0
  98. package/dist/assets/main-CgXMBNvG.js +2 -0
  99. package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
  100. package/dist/assets/markdownCore-Dh7WEsdM.js +5 -0
  101. package/dist/assets/miniChat-CwTmDixi.js +2 -0
  102. package/dist/assets/mobile-CLf2vCtO.js +2 -0
  103. package/dist/assets/patchFileDiff-Bsba8KFp.js +13 -0
  104. package/dist/assets/pl-D-7x4Y8Q.js +16 -0
  105. package/dist/assets/pt-BR-CSBZrHa8.js +16 -0
  106. package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +2 -0
  107. package/dist/assets/renderMobileApp-V4hYd32H.js +25 -0
  108. package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +25 -0
  109. package/dist/assets/scheduledTasksApi-DzjbLd8P.js +1 -0
  110. package/dist/assets/shikiHighlight-8_3csmr6.js +1 -0
  111. package/dist/assets/surface-B6n7wJtt.js +1 -0
  112. package/dist/assets/textMateThemeFromAppTheme-CHo2fCgW.js +1 -0
  113. package/dist/assets/tr-Dr-oJ22L.js +7 -0
  114. package/dist/assets/uk-9Nx83UcI.js +16 -0
  115. package/dist/assets/useAppFontEffects-khZXpFL_.js +7333 -0
  116. package/dist/assets/useInlineCommentController-DkQ_gqpn.js +2 -0
  117. package/dist/assets/useRootScrollLock-DdvHf9Pt.js +2 -0
  118. package/dist/assets/{vendor-base-ui-DdV8vhHn.js → vendor-base-ui-CsXYnR5c.js} +1 -1
  119. package/dist/assets/{vendor-codemirror-language-data-DzlSIu_W.js → vendor-codemirror-language-data-CdzUmUyZ.js} +2 -2
  120. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
  121. package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
  122. package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
  123. package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
  124. package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
  125. package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
  126. package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
  127. package/dist/assets/vendor-zod-CgAdXWZO.js +39 -0
  128. package/dist/assets/{web-components-CdGfR2XJ.js → web-components-B8IcVQAA.js} +2 -2
  129. package/dist/assets/zh-CN-Bxy_Hj3r.js +16 -0
  130. package/dist/assets/zh-TW-0BB4gpHM.js +16 -0
  131. package/dist/index.html +6 -4
  132. package/dist/mini-chat.html +6 -4
  133. package/dist/mobile.html +6 -4
  134. package/package.json +12 -9
  135. package/server/built-in-extensions/registry.json +4 -0
  136. package/server/index.d.ts +2 -0
  137. package/server/index.js +47 -23
  138. package/server/lib/agent-memory/runtime.js +7 -2
  139. package/server/lib/agent-memory/runtime.test.js +22 -0
  140. package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
  141. package/server/lib/agent-tool/runtime.js +62 -15
  142. package/server/lib/agent-tool/runtime.test.js +88 -2
  143. package/server/lib/changelog/update-notes.js +2 -1
  144. package/server/lib/changelog/update-notes.test.js +19 -0
  145. package/server/lib/event-stream/DOCUMENTATION.md +26 -0
  146. package/server/lib/event-stream/delta-coalescer.js +0 -0
  147. package/server/lib/event-stream/delta-coalescer.test.js +292 -0
  148. package/server/lib/event-stream/global-hub.js +68 -13
  149. package/server/lib/event-stream/global-hub.test.js +182 -0
  150. package/server/lib/event-stream/global-ws-bridge.js +16 -18
  151. package/server/lib/event-stream/index.js +4 -0
  152. package/server/lib/event-stream/protocol.js +21 -7
  153. package/server/lib/event-stream/runtime.js +9 -6
  154. package/server/lib/event-stream/runtime.test.js +60 -0
  155. package/server/lib/fs/DOCUMENTATION.md +10 -1
  156. package/server/lib/fs/git-process.test.js +77 -0
  157. package/server/lib/fs/routes.js +58 -5
  158. package/server/lib/fs/routes.test.js +116 -0
  159. package/server/lib/fs/search.js +2 -1
  160. package/server/lib/git/DOCUMENTATION.md +30 -6
  161. package/server/lib/git/routes.js +37 -11
  162. package/server/lib/git/routes.test.js +78 -0
  163. package/server/lib/git/serial-refresh.js +93 -0
  164. package/server/lib/git/serial-refresh.test.js +112 -0
  165. package/server/lib/git/service.js +588 -74
  166. package/server/lib/git/service.test.js +492 -0
  167. package/server/lib/github/DOCUMENTATION.md +5 -0
  168. package/server/lib/github/pr-status.js +5 -5
  169. package/server/lib/github/pr-status.test.js +1 -1
  170. package/server/lib/github/routes.js +6 -6
  171. package/server/lib/guests/DOCUMENTATION.md +68 -0
  172. package/server/lib/guests/auth-store.js +184 -0
  173. package/server/lib/guests/auth-store.test.js +112 -0
  174. package/server/lib/guests/background.test.js +83 -0
  175. package/server/lib/guests/builtins.js +34 -0
  176. package/server/lib/guests/builtins.test.js +174 -0
  177. package/server/lib/guests/catalog.js +477 -0
  178. package/server/lib/guests/catalog.test.js +390 -0
  179. package/server/lib/guests/clone.js +297 -0
  180. package/server/lib/guests/extract-zip.js +136 -0
  181. package/server/lib/guests/extract-zip.test.js +109 -0
  182. package/server/lib/guests/files.js +308 -0
  183. package/server/lib/guests/files.test.js +262 -0
  184. package/server/lib/guests/grant-scope.js +81 -0
  185. package/server/lib/guests/grant-scope.test.js +75 -0
  186. package/server/lib/guests/host-session.js +73 -0
  187. package/server/lib/guests/host-session.test.js +30 -0
  188. package/server/lib/guests/html-styles.js +6 -0
  189. package/server/lib/guests/html-styles.test.js +45 -0
  190. package/server/lib/guests/html-tokens.js +73 -0
  191. package/server/lib/guests/html-tokens.test.js +53 -0
  192. package/server/lib/guests/install.js +406 -0
  193. package/server/lib/guests/install.test.js +338 -0
  194. package/server/lib/guests/oauth.js +501 -0
  195. package/server/lib/guests/oauth.test.js +481 -0
  196. package/server/lib/guests/persist.js +336 -0
  197. package/server/lib/guests/persist.test.js +288 -0
  198. package/server/lib/guests/request.js +112 -0
  199. package/server/lib/guests/request.test.js +206 -0
  200. package/server/lib/guests/routes.js +838 -0
  201. package/server/lib/guests/service.js +538 -0
  202. package/server/lib/guests/service.test.js +277 -0
  203. package/server/lib/guests/sockets.js +129 -0
  204. package/server/lib/guests/sockets.test.js +110 -0
  205. package/server/lib/guests/ssh-install.test.js +188 -0
  206. package/server/lib/guests/storage.js +68 -0
  207. package/server/lib/guests/storage.test.js +59 -0
  208. package/server/lib/guests/updates.js +294 -0
  209. package/server/lib/guests/updates.test.js +334 -0
  210. package/server/lib/guests/upload.js +50 -0
  211. package/server/lib/guests/upload.test.js +131 -0
  212. package/server/lib/linear/DOCUMENTATION.md +1 -1
  213. package/server/lib/message-queue/DOCUMENTATION.md +17 -1
  214. package/server/lib/message-queue/runtime.js +34 -1
  215. package/server/lib/message-queue/runtime.test.js +83 -6
  216. package/server/lib/notifications/DOCUMENTATION.md +1 -1
  217. package/server/lib/notifications/emitter-runtime.js +4 -3
  218. package/server/lib/notifications/routes.js +5 -0
  219. package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
  220. package/server/lib/openchamber-sessions/routes.js +62 -29
  221. package/server/lib/openchamber-sessions/routes.test.js +96 -0
  222. package/server/lib/opencode/DOCUMENTATION.md +73 -5
  223. package/server/lib/opencode/bootstrap-runtime.js +1 -0
  224. package/server/lib/opencode/core-routes.js +39 -1
  225. package/server/lib/opencode/env-runtime.js +5 -1
  226. package/server/lib/opencode/env-runtime.test.js +72 -0
  227. package/server/lib/opencode/feature-routes-runtime.js +30 -1
  228. package/server/lib/opencode/lifecycle-process.test.js +84 -0
  229. package/server/lib/opencode/lifecycle.js +58 -70
  230. package/server/lib/opencode/lifecycle.test.js +29 -0
  231. package/server/lib/opencode/managed-plugin-config.js +1 -1
  232. package/server/lib/opencode/openchamber-routes.js +72 -14
  233. package/server/lib/opencode/openchamber-routes.test.js +71 -3
  234. package/server/lib/opencode/proxy.js +33 -0
  235. package/server/lib/opencode/session-runtime.js +68 -0
  236. package/server/lib/opencode/session-runtime.test.js +43 -0
  237. package/server/lib/opencode/settings-helpers.js +11 -0
  238. package/server/lib/opencode/settings-helpers.test.js +24 -2
  239. package/server/lib/opencode/settings-normalization-runtime.js +2 -0
  240. package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
  241. package/server/lib/opencode/settings-registry.json +18 -0
  242. package/server/lib/opencode/settings-runtime.js +36 -9
  243. package/server/lib/opencode/settings-runtime.test.js +158 -2
  244. package/server/lib/opencode/shutdown-runtime.js +3 -0
  245. package/server/lib/opencode/shutdown-runtime.test.js +24 -0
  246. package/server/lib/opencode/theme-archive.js +53 -0
  247. package/server/lib/opencode/theme-catalog.js +171 -0
  248. package/server/lib/opencode/theme-catalog.test.js +125 -0
  249. package/server/lib/opencode/theme-import.test.js +144 -0
  250. package/server/lib/opencode/theme-runtime.js +100 -28
  251. package/server/lib/opencode/theme-runtime.test.js +15 -0
  252. package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
  253. package/server/lib/permission-auto-accept/runtime.js +13 -0
  254. package/server/lib/permission-auto-accept/runtime.test.js +37 -1
  255. package/server/lib/project-context/DOCUMENTATION.md +13 -3
  256. package/server/lib/project-context/runtime.js +8 -2
  257. package/server/lib/project-context/runtime.test.js +54 -0
  258. package/server/lib/projects/DOCUMENTATION.md +2 -1
  259. package/server/lib/projects/project-config.js +40 -11
  260. package/server/lib/projects/project-config.test.js +97 -0
  261. package/server/lib/projects/project-id.js +26 -0
  262. package/server/lib/quota/DOCUMENTATION.md +0 -1
  263. package/server/lib/quota/providers/index.js +0 -7
  264. package/server/lib/realtime-proxy-terminal.test.js +150 -0
  265. package/server/lib/realtime-proxy.js +2 -0
  266. package/server/lib/relay/DOCUMENTATION.md +20 -0
  267. package/server/lib/relay/host-client.js +7 -0
  268. package/server/lib/relay/host-client.test.js +14 -0
  269. package/server/lib/routing/DOCUMENTATION.md +83 -0
  270. package/server/lib/routing/defaults.js +98 -0
  271. package/server/lib/routing/feature-flag.js +11 -0
  272. package/server/lib/routing/history.js +34 -0
  273. package/server/lib/routing/jev.js +98 -0
  274. package/server/lib/routing/routes.http.test.js +100 -0
  275. package/server/lib/routing/routes.js +81 -0
  276. package/server/lib/routing/runtime.js +217 -0
  277. package/server/lib/routing/runtime.test.js +188 -0
  278. package/server/lib/routing/store.js +249 -0
  279. package/server/lib/routing/store.test.js +96 -0
  280. package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
  281. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
  282. package/server/lib/scheduled-tasks/runtime.js +14 -4
  283. package/server/lib/scheduled-tasks/runtime.test.js +67 -0
  284. package/server/lib/security/request-security.js +9 -5
  285. package/server/lib/security/request-security.test.js +37 -0
  286. package/server/lib/session-assist/DOCUMENTATION.md +131 -66
  287. package/server/lib/session-assist/context.js +127 -0
  288. package/server/lib/session-assist/context.test.js +132 -0
  289. package/server/lib/session-assist/prompt.js +54 -0
  290. package/server/lib/session-assist/runtime.js +111 -257
  291. package/server/lib/session-assist/runtime.test.js +203 -0
  292. package/server/lib/small-model/DOCUMENTATION.md +14 -2
  293. package/server/lib/small-model/call.js +37 -6
  294. package/server/lib/small-model/call.test.js +182 -0
  295. package/server/lib/terminal/DOCUMENTATION.md +23 -2
  296. package/server/lib/terminal/runtime.js +27 -5
  297. package/server/lib/terminal/runtime.test.js +67 -0
  298. package/server/lib/terminal/shutdown.js +106 -0
  299. package/server/lib/terminal/shutdown.test.js +84 -0
  300. package/server/lib/tts/service.test.js +78 -0
  301. package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
  302. package/server/lib/ui-auth/session-cookie.js +51 -0
  303. package/server/lib/ui-auth/session-cookie.test.js +46 -0
  304. package/server/lib/ui-auth/ui-auth.js +56 -17
  305. package/server/lib/ui-auth/ui-auth.test.js +96 -0
  306. package/server/lib/walkthrough/DOCUMENTATION.md +15 -5
  307. package/server/lib/walkthrough/pr-source.test.js +21 -0
  308. package/server/lib/walkthrough/pull-request.js +7 -4
  309. package/server/lib/walkthrough/pull-request.test.js +16 -0
  310. package/server/lib/walkthrough/routes.js +19 -0
  311. package/server/lib/walkthrough/routes.test.js +26 -0
  312. package/server/lib/walkthrough/sources.js +13 -2
  313. package/server/opencode-worktree-readiness.test.js +130 -0
  314. package/dist/assets/DiffView-3Dn4QHbu.js +0 -4
  315. package/dist/assets/DirtyBranchSwitchDialog-CwOerqVi.js +0 -1
  316. package/dist/assets/FilesView-BdTOpew9.js +0 -17
  317. package/dist/assets/GitView-Dqf9vHJh.js +0 -6
  318. package/dist/assets/LinearIssuesView-CmbeL7I6.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-wVZlx4dd.js +0 -12
  320. package/dist/assets/PierreDiffViewer-DCsydlKs.js +0 -161
  321. package/dist/assets/SettingsView-CcM8pF5O.js +0 -28
  322. package/dist/assets/ToolOutputDialog-BgfTbDZ4.js +0 -15
  323. package/dist/assets/WalkthroughView-BEVUDxuw.js +0 -4
  324. package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
  325. package/dist/assets/de-C8jV7J9y.js +0 -10
  326. package/dist/assets/es-BI6J1M5R.js +0 -16
  327. package/dist/assets/fr-D6oDFt7E.js +0 -14
  328. package/dist/assets/index-BqhiCmYS.js +0 -39
  329. package/dist/assets/index-FqSOBz0J.css +0 -1
  330. package/dist/assets/ja-C_4i5EQu.js +0 -16
  331. package/dist/assets/ko-BFbOTpbq.js +0 -16
  332. package/dist/assets/main-C9ETLKFc.js +0 -2
  333. package/dist/assets/main-CJmb2dF8.js +0 -930
  334. package/dist/assets/markdownCore-DVuZpnli.js +0 -5
  335. package/dist/assets/miniChat-B4Ytoq2n.js +0 -2
  336. package/dist/assets/mobile-BUT6-lo1.js +0 -2
  337. package/dist/assets/patchFileDiff-8j1apICU.js +0 -13
  338. package/dist/assets/pl-COFvT56l.js +0 -16
  339. package/dist/assets/pt-BR-DtVOO2Lj.js +0 -16
  340. package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
  341. package/dist/assets/renderElectronMiniChatApp-B6vUG-vF.js +0 -2
  342. package/dist/assets/renderMobileApp-BNgHraq6.js +0 -25
  343. package/dist/assets/runtimeEndpointReset-B8LYpWvr.js +0 -17
  344. package/dist/assets/scheduledTasksApi-GC4epIYA.js +0 -1
  345. package/dist/assets/shikiHighlight-BJISeZsQ.js +0 -1
  346. package/dist/assets/surface-BvRzS58U.js +0 -1
  347. package/dist/assets/tr-B8Q0Go2l.js +0 -7
  348. package/dist/assets/uk-Dk4a-Fn-.js +0 -16
  349. package/dist/assets/useAppFontEffects-DpFNefUA.js +0 -7284
  350. package/dist/assets/useGitComparison-Dfxaqd0s.js +0 -1
  351. package/dist/assets/useInlineCommentController-CscsE4t-.js +0 -2
  352. package/dist/assets/useRootScrollLock-BaCvuRlT.js +0 -1
  353. package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
  354. package/dist/assets/vendor-zod-BPAANqvH.js +0 -39
  355. package/dist/assets/zh-CN-B_BXxG1Q.js +0 -16
  356. package/dist/assets/zh-TW-DLrl1SPZ.js +0 -16
  357. package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
  358. package/server/lib/mcp-reconnect/runtime.js +0 -122
  359. package/server/lib/mcp-reconnect/runtime.test.js +0 -140
  360. package/server/lib/quota/providers/crof.js +0 -96
  361. package/server/lib/quota/providers/crof.test.js +0 -98
  362. /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
@@ -1,4 +1,6 @@
1
1
  import { buildExternalManualRestartResponse } from './config-mutation-response.js';
2
+ import { ThemeImportStorageError } from './theme-runtime.js';
3
+ import { registerThemeCatalogRoutes } from './theme-catalog.js';
2
4
 
3
5
  const parseLoopbackUrl = (rawUrl) => {
4
6
  if (typeof rawUrl !== 'string') {
@@ -589,7 +591,15 @@ export const registerAuthAndAccessRoutes = (app, dependencies) => {
589
591
  res.status(statusCode).json({ error: 'Invalid or expired pairing session' });
590
592
  };
591
593
 
594
+ const isGuestOauthCallback = (req) => (
595
+ req.method === 'GET'
596
+ && /^\/guests\/[a-z][a-z0-9-]*\/oauth\/callback$/.test(req.path || '')
597
+ );
598
+
592
599
  const requireApiAuth = async (req, res, next) => {
600
+ if (isGuestOauthCallback(req)) {
601
+ return next();
602
+ }
593
603
  const requestScope = tunnelAuthController.classifyRequestScope(req);
594
604
  if (requestScope === 'tunnel' || requestScope === 'unknown-public') {
595
605
  return tunnelAuthController.requireTunnelSession(req, res, next);
@@ -995,6 +1005,8 @@ export const registerAuthAndAccessRoutes = (app, dependencies) => {
995
1005
  export const registerSettingsUtilityRoutes = (app, dependencies) => {
996
1006
  const {
997
1007
  readCustomThemesFromDisk,
1008
+ saveImportedTheme,
1009
+ deleteImportedTheme,
998
1010
  refreshOpenCodeAfterConfigChange,
999
1011
  clientReloadDelayMs,
1000
1012
  } = dependencies;
@@ -1009,6 +1021,30 @@ export const registerSettingsUtilityRoutes = (app, dependencies) => {
1009
1021
  }
1010
1022
  });
1011
1023
 
1024
+ app.post('/api/config/themes', async (req, res) => {
1025
+ try {
1026
+ const theme = await saveImportedTheme(req.body?.theme);
1027
+ res.status(201).json({ theme });
1028
+ } catch (error) {
1029
+ if (error instanceof ThemeImportStorageError) {
1030
+ res.status(error.status).json({ error: error.code });
1031
+ return;
1032
+ }
1033
+ console.error('[themes] Failed to save imported theme');
1034
+ res.status(500).json({ error: 'save' });
1035
+ }
1036
+ });
1037
+
1038
+ registerThemeCatalogRoutes(app);
1039
+ app.delete('/api/config/themes/:id', async (req, res) => {
1040
+ try {
1041
+ await deleteImportedTheme(req.params.id);
1042
+ res.json({ success: true });
1043
+ } catch (error) {
1044
+ res.status(error instanceof ThemeImportStorageError ? error.status : 500).json({ error: 'delete' });
1045
+ }
1046
+ });
1047
+
1012
1048
  app.post('/api/config/reload', async (_req, res) => {
1013
1049
  try {
1014
1050
  console.log('[Server] Manual configuration reload requested');
@@ -1041,7 +1077,9 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
1041
1077
  const { express, verboseRequestLogs = false } = dependencies;
1042
1078
 
1043
1079
  app.use((req, res, next) => {
1044
- if (req.path.startsWith('/api/behavior')) {
1080
+ if (req.path === '/api/config/themes' || req.path.startsWith('/api/config/themes/')) {
1081
+ express.json({ limit: '1mb' })(req, res, next);
1082
+ } else if (req.path.startsWith('/api/behavior')) {
1045
1083
  const contentLength = parseInt(req.headers['content-length'] || '0', 10);
1046
1084
  if (contentLength > 1024 * 1024) {
1047
1085
  return res.status(413).json({ error: 'Content exceeds maximum size of 1048576 bytes' });
@@ -1110,7 +1110,11 @@ export const createOpenCodeEnvRuntime = (deps) => {
1110
1110
  return resolved;
1111
1111
  }
1112
1112
 
1113
- process.env.OPENCODE_BINARY = resolved;
1113
+ // AppImage updates inherit process.env. Keep automatic desktop bundle
1114
+ // selection local so the next app does not treat the old mount as an override.
1115
+ if (process.env.OPENCHAMBER_RUNTIME !== 'desktop' || state.resolvedOpencodeBinarySource !== 'bundled') {
1116
+ process.env.OPENCODE_BINARY = resolved;
1117
+ }
1114
1118
  prependToPath(path.dirname(resolved));
1115
1119
  ensureOpencodeShimRuntime(resolved);
1116
1120
  state.resolvedOpencodeBinary = resolved;
@@ -14,6 +14,7 @@ const originalResourcesPath = process.resourcesPath;
14
14
  const originalWslBinary = process.env.WSL_BINARY;
15
15
  const originalOpenChamberWslBinary = process.env.OPENCHAMBER_WSL_BINARY;
16
16
  const originalPlatform = process.platform;
17
+ const originalRuntime = process.env.OPENCHAMBER_RUNTIME;
17
18
  const tempDirs = [];
18
19
  const itIf = (condition) => condition ? it : it.skip;
19
20
 
@@ -30,6 +31,8 @@ const setPlatform = (platform) => {
30
31
  };
31
32
 
32
33
  afterEach(() => {
34
+ if (originalRuntime === undefined) delete process.env.OPENCHAMBER_RUNTIME;
35
+ else process.env.OPENCHAMBER_RUNTIME = originalRuntime;
33
36
  Object.defineProperty(process, 'platform', {
34
37
  value: originalPlatform,
35
38
  });
@@ -117,7 +120,76 @@ const createRuntime = (settings, options = {}) => {
117
120
  return { runtime, state };
118
121
  };
119
122
 
123
+ const createBundledCli = () => {
124
+ const resourcesPath = createTempDir('openchamber-resources-');
125
+ const directory = path.join(resourcesPath, 'opencode-cli');
126
+ fs.mkdirSync(directory);
127
+ const binary = path.join(directory, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
128
+ fs.writeFileSync(binary, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
129
+ Object.defineProperty(process, 'resourcesPath', { configurable: true, value: resourcesPath });
130
+ return binary;
131
+ };
132
+
120
133
  describe('OpenCode env runtime', () => {
134
+ it.each(['linux', 'darwin', 'win32'])('keeps automatic desktop bundled resolution local across relaunch on %s', (platform) => {
135
+ setPlatform(platform);
136
+ process.env.OPENCHAMBER_RUNTIME = 'desktop';
137
+ delete process.env.OPENCODE_BINARY;
138
+ delete process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
139
+
140
+ const oldBinary = createBundledCli();
141
+ const oldRuntime = createRuntime({});
142
+ expect(oldRuntime.runtime.ensureOpencodeCliEnv()).toBe(oldBinary);
143
+ expect(oldRuntime.runtime.ensureOpencodeCliEnv()).toBe(oldBinary);
144
+ expect(oldRuntime.state.resolvedOpencodeBinarySource).toBe('bundled');
145
+ expect(process.env.OPENCODE_BINARY).toBeUndefined();
146
+ expect(process.env.PATH.split(path.delimiter)[0]).toBe(path.dirname(oldBinary));
147
+
148
+ // The old binary remains executable and its directory is still in PATH.
149
+ const newBinary = createBundledCli();
150
+ const newRuntime = createRuntime({});
151
+ expect(newRuntime.runtime.ensureOpencodeCliEnv()).toBe(newBinary);
152
+ expect(newRuntime.state.resolvedOpencodeBinarySource).toBe('bundled');
153
+ expect(newRuntime.runtime.isBundledOpenCodeCliPath(newBinary)).toBe(true);
154
+ expect(process.env.OPENCODE_BINARY).toBeUndefined();
155
+ });
156
+
157
+ it.each(['web', 'ssh-remote'])('preserves automatic binary export outside desktop in %s', (runtimeName) => {
158
+ process.env.OPENCHAMBER_RUNTIME = runtimeName;
159
+ delete process.env.OPENCODE_BINARY;
160
+ delete process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
161
+ const binary = createBundledCli();
162
+ const { runtime } = createRuntime({});
163
+ expect(runtime.ensureOpencodeCliEnv()).toBe(binary);
164
+ expect(process.env.OPENCODE_BINARY).toBe(binary);
165
+ });
166
+
167
+ it.each(['env', 'settings'])('preserves explicit desktop %s selection even when it points at a bundled CLI', async (source) => {
168
+ process.env.OPENCHAMBER_RUNTIME = 'desktop';
169
+ delete process.env.OPENCODE_BINARY;
170
+ delete process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
171
+ const binary = createBundledCli();
172
+ if (source === 'env') process.env.OPENCODE_BINARY = binary;
173
+ const { runtime, state } = createRuntime(source === 'settings' ? { opencodeBinary: binary } : {});
174
+ await runtime.applyOpencodeBinaryFromSettings({ strict: true });
175
+ expect(runtime.ensureOpencodeCliEnv()).toBe(binary);
176
+ expect(state.resolvedOpencodeBinarySource).toBe(source);
177
+ expect(process.env.OPENCODE_BINARY).toBe(binary);
178
+ });
179
+
180
+ it('preserves automatic PATH binary export for desktop without a bundled CLI', () => {
181
+ process.env.OPENCHAMBER_RUNTIME = 'desktop';
182
+ delete process.env.OPENCODE_BINARY;
183
+ delete process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
184
+ const binary = createBundledCli();
185
+ Object.defineProperty(process, 'resourcesPath', { configurable: true, value: undefined });
186
+ process.env.PATH = path.dirname(binary);
187
+ const { runtime, state } = createRuntime({});
188
+ expect(runtime.ensureOpencodeCliEnv()).toBe(binary);
189
+ expect(state.resolvedOpencodeBinarySource).toBe('path');
190
+ expect(process.env.OPENCODE_BINARY).toBe(binary);
191
+ });
192
+
121
193
  it('searches an explicit PATH without mutating the process environment', () => {
122
194
  const defaultDir = createTempDir('openchamber-default-path-');
123
195
  const explicitDir = createTempDir('openchamber-explicit-path-');
@@ -5,6 +5,9 @@ import { registerWalkthroughRoutes } from '../walkthrough/routes.js';
5
5
  import { registerSessionGoalRoutes } from '../session-goal/routes.js';
6
6
  import { registerGitHubRoutes } from '../github/routes.js';
7
7
  import { registerLinearRoutes } from '../linear/routes.js';
8
+ import { registerGuestRoutes } from '../guests/routes.js';
9
+ import { registerBuiltInGuests } from '../guests/catalog.js';
10
+ import { extensionsPersistPath } from '../guests/persist.js';
8
11
  import { registerGitRoutes } from '../git/routes.js';
9
12
  import { registerDevServerRoutes } from '../dev-servers/routes.js';
10
13
  import { registerMagicPromptRoutes } from '../magic-prompts/routes.js';
@@ -15,6 +18,7 @@ import { registerAgentMemoryRoutes } from '../agent-memory/routes.js';
15
18
  import { registerSessionKnowledgeRoutes } from '../session-knowledge/routes.js';
16
19
  import { registerPermissionAutoAcceptRoutes } from '../permission-auto-accept/runtime.js';
17
20
  import { registerMessageQueueRoutes } from '../message-queue/runtime.js';
21
+ import { registerRoutingPromptRewrite, registerRoutingRoutes } from '../routing/routes.js';
18
22
  import { registerConfigEntityRoutes } from './config-entity-routes.js';
19
23
  import { registerSettingsUtilityRoutes } from './core-routes.js';
20
24
  import { registerProjectIconRoutes } from './project-icon-routes.js';
@@ -105,6 +109,8 @@ export const createFeatureRoutesRuntime = (dependencies) => {
105
109
  resolveOptionalProjectDirectory,
106
110
  validateDirectoryPath,
107
111
  readCustomThemesFromDisk,
112
+ saveImportedTheme,
113
+ deleteImportedTheme,
108
114
  refreshOpenCodeAfterConfigChange,
109
115
  getOpenCodeResolutionSnapshot,
110
116
  getOpenCodeUpgradeCapability,
@@ -136,16 +142,23 @@ export const createFeatureRoutesRuntime = (dependencies) => {
136
142
  emitSessionCreatedEvent,
137
143
  permissionAutoAcceptRuntime,
138
144
  messageQueueRuntime,
145
+ routingRuntime,
146
+ openchamberVersion,
139
147
  } = routeDependencies;
140
148
 
141
149
  registerSettingsUtilityRoutes(app, {
142
150
  readCustomThemesFromDisk,
151
+ saveImportedTheme,
152
+ deleteImportedTheme,
143
153
  refreshOpenCodeAfterConfigChange,
144
154
  clientReloadDelayMs,
145
155
  });
146
156
 
147
157
  registerPermissionAutoAcceptRoutes(app, permissionAutoAcceptRuntime);
148
158
  registerMessageQueueRoutes(app, messageQueueRuntime);
159
+ registerRoutingRoutes(app, routingRuntime);
160
+ // Before the generic OpenCode proxy: turns `openchamber/auto` into a real model.
161
+ registerRoutingPromptRewrite(app, routingRuntime);
149
162
 
150
163
  registerOpenCodeRoutes(app, {
151
164
  crypto,
@@ -307,7 +320,23 @@ export const createFeatureRoutesRuntime = (dependencies) => {
307
320
  registerSessionGoalRoutes(app);
308
321
  registerGitHubRoutes(app);
309
322
  registerLinearRoutes(app);
310
- registerGitRoutes(app);
323
+ await registerBuiltInGuests({ persistPath: extensionsPersistPath(openchamberDataDir), root: routeDependencies.builtInExtensionsDir });
324
+ registerGuestRoutes(app, { openchamberDataDir, openchamberVersion, resolveGitBinaryForSpawn, resolveOptionalProjectDirectory, getSmallModelService });
325
+ registerGitRoutes(app, {
326
+ emitWorktreeChanged: ({ directories, at }) => {
327
+ const clients = getOpenChamberEventClients();
328
+ for (const client of clients) {
329
+ try {
330
+ writeSseEvent(client, {
331
+ type: 'openchamber:worktree-changed',
332
+ properties: { directories, at },
333
+ });
334
+ } catch {
335
+ clients.delete(client);
336
+ }
337
+ }
338
+ },
339
+ });
311
340
  registerDevServerRoutes(app, { scanner: devServerScanner, getOwnPorts });
312
341
  registerMagicPromptRoutes(app, {
313
342
  fsPromises,
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { createOpenCodeLifecycleRuntime } from './lifecycle.js';
6
+
7
+ const alive = (pid) => {
8
+ try { process.kill(pid, 0); return true; } catch { return false; }
9
+ };
10
+
11
+ const createRuntime = (waitForReady, state) => createOpenCodeLifecycleRuntime({
12
+ state,
13
+ env: { ENV_CONFIGURED_OPENCODE_PORT: 45678, ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1' },
14
+ syncToHmrState() {}, syncFromHmrState() {},
15
+ ensureOpencodeCliEnv: () => process.execPath,
16
+ applyOpencodeBinaryFromSettings: async () => {},
17
+ ensureLocalOpenCodeServerPassword: async () => 'fixture-only',
18
+ resolveManagedOpenCodeLaunchSpec: (binary) => ({ binary, args: [] }),
19
+ normalizeApiPrefix: (value) => value,
20
+ setOpenCodePort() {}, setDetectedOpenCodeApiPrefix() {},
21
+ waitForReady,
22
+ managedStartupTimeoutMs: 1500,
23
+ });
24
+
25
+ describe('managed process lifecycle with real children', () => {
26
+ for (const failure of ['invalid-readiness', 'health-error', 'startup-timeout', 'shutdown-during-startup', 'none']) {
27
+ it(`reaps the server and its child after ${failure}`, async () => {
28
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-process-'));
29
+ const previousRegistry = process.env.OPENCHAMBER_MANAGED_PROCESS_REGISTRY;
30
+ process.env.OPENCHAMBER_MANAGED_PROCESS_REGISTRY = path.join(root, 'registry');
31
+ const marker = path.join(root, 'pids');
32
+ const childScript = `process.on('SIGTERM', () => {}); require('node:fs').appendFileSync(${JSON.stringify(marker)}, process.pid + '\\n'); process.stdout.write('ready\\n'); setInterval(() => {}, 1000);`;
33
+ let readinessMessage = 'opencode server listening on http://127.0.0.1:45678\n';
34
+ if (failure === 'invalid-readiness') readinessMessage = 'opencode server listening without a URL\n';
35
+ if (failure === 'startup-timeout' || failure === 'shutdown-during-startup') readinessMessage = '';
36
+ // Node is an isolated stand-in for the native OpenCode binary. Lifecycle
37
+ // still launches its real `serve --hostname ... --port ...` command.
38
+ await fs.writeFile(path.join(root, 'serve'), `
39
+ const fs = require('node:fs');
40
+ fs.appendFileSync(${JSON.stringify(marker)}, process.pid + '\\n');
41
+ const child = require('node:child_process').spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: ['ignore', 'pipe', 'ignore'] });
42
+ child.stdout.once('data', () => {
43
+ process.stdout.write(${JSON.stringify(readinessMessage)});
44
+ });
45
+ setInterval(() => {}, 1000);
46
+ `);
47
+ try {
48
+ const state = { openCodeWorkingDirectory: root, useWslForOpencode: false };
49
+ const runtime = createRuntime(async () => {
50
+ if (failure === 'health-error') throw new Error('fixture health failure');
51
+ return true;
52
+ }, state);
53
+ if (failure === 'none') {
54
+ const server = await runtime.startOpenCode();
55
+ await Promise.all([server.close(), server.close()]);
56
+ } else if (failure === 'shutdown-during-startup') {
57
+ const starting = runtime.startOpenCode();
58
+ const rejected = expect(starting).rejects.toThrow('exited before serving');
59
+ await expect.poll(async () => (await fs.readFile(marker, 'utf8').catch(() => '')).trim().split('\n').filter(Boolean).length).toBe(2);
60
+ state.isShuttingDown = true;
61
+ await state.openCodeProcess.close();
62
+ await rejected;
63
+ } else if (failure === 'startup-timeout') {
64
+ await expect(runtime.startOpenCode()).rejects.toThrow('Timeout waiting for OpenCode');
65
+ } else {
66
+ await expect(runtime.startOpenCode()).rejects.toThrow(failure === 'health-error' ? 'fixture health failure' : 'Failed to parse server url');
67
+ }
68
+ const pids = (await fs.readFile(marker, 'utf8')).trim().split('\n').map(Number);
69
+ expect(pids).toHaveLength(failure === 'none' || failure === 'shutdown-during-startup' ? 2 : 4);
70
+ await expect.poll(() => pids.filter(alive), { timeout: 3000 }).toEqual([]);
71
+ expect(await fs.readdir(path.join(root, 'registry')).catch(() => [])).toEqual([]);
72
+ } finally {
73
+ const pids = (await fs.readFile(marker, 'utf8').catch(() => '')).trim().split('\n').map(Number).filter((pid) => pid > 0);
74
+ for (const pid of pids.reverse()) {
75
+ if (!alive(pid)) continue;
76
+ try { process.kill(pid, 'SIGKILL'); } catch { /* already exited */ }
77
+ }
78
+ if (previousRegistry === undefined) delete process.env.OPENCHAMBER_MANAGED_PROCESS_REGISTRY;
79
+ else process.env.OPENCHAMBER_MANAGED_PROCESS_REGISTRY = previousRegistry;
80
+ await fs.rm(root, { recursive: true, force: true });
81
+ }
82
+ }, 15_000);
83
+ }
84
+ });
@@ -109,6 +109,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
109
109
  reapManagedOrphanedProcesses = reapOrphanedProcesses,
110
110
  getWarmupDirectories = async () => [],
111
111
  onOpenCodeRestarted = null,
112
+ managedStartupTimeoutMs = 30_000,
112
113
  now = Date.now,
113
114
  } = deps;
114
115
 
@@ -287,7 +288,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
287
288
  }
288
289
 
289
290
  const pid = child.pid;
290
- if (!pid || hasChildProcessExited(child)) {
291
+ if (!pid || (process.platform === 'win32' && hasChildProcessExited(child))) {
291
292
  await waitForChildProcessClose(child, 250);
292
293
  return;
293
294
  }
@@ -301,34 +302,14 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
301
302
  }
302
303
 
303
304
  try {
304
- child.kill(signal);
305
+ if (!hasChildProcessExited(child)) child.kill(signal);
305
306
  } catch {
306
307
  }
307
308
  };
308
309
 
309
310
  if (process.platform === 'win32') {
310
- try {
311
- child.kill();
312
- } catch {
313
- }
314
-
315
- if (await waitForChildProcessClose(child, 800)) {
316
- return;
317
- }
318
-
319
- try {
320
- spawnSync('taskkill', ['/pid', String(pid), '/t'], {
321
- stdio: 'ignore',
322
- timeout: 3000,
323
- windowsHide: true,
324
- });
325
- } catch {
326
- }
327
-
328
- if (await waitForChildProcessClose(child, 1500)) {
329
- return;
330
- }
331
-
311
+ // Windows child.kill() terminates only the parent. Kill the owned tree
312
+ // while its parent still exists, otherwise /T cannot find its children.
332
313
  try {
333
314
  spawnSync('taskkill', ['/pid', String(pid), '/f', '/t'], {
334
315
  stdio: 'ignore',
@@ -343,11 +324,9 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
343
324
  }
344
325
 
345
326
  signalProcessTree('SIGTERM');
346
-
347
- if (await waitForChildProcessClose(child, 2500)) {
348
- return;
349
- }
350
-
327
+ await waitForChildProcessClose(child, 2500);
328
+ // Parent exit does not prove group exit. Tools can ignore SIGTERM and keep
329
+ // running after their server has exited and closed its own stdio.
351
330
  signalProcessTree('SIGKILL');
352
331
 
353
332
  await waitForChildProcessClose(child, 1000);
@@ -377,8 +356,8 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
377
356
  return parts.length > 0 ? parts.join('\n\n') : 'No stdout/stderr captured';
378
357
  };
379
358
 
380
- const createManagedOpenCodeServerProcess = async ({ hostname, port, timeout, cwd, env: processEnv, shellEnvKeysCount = 0 }) => {
381
- let binary = (process.env.OPENCODE_BINARY || 'opencode').trim() || 'opencode';
359
+ const createManagedOpenCodeServerProcess = async ({ resolvedBinary, hostname, port, timeout, cwd, env: processEnv, shellEnvKeysCount = 0 }) => {
360
+ let binary = (resolvedBinary || process.env.OPENCODE_BINARY || 'opencode').trim() || 'opencode';
382
361
  const sourceBinary = binary;
383
362
  let args = ['serve', '--hostname', hostname, '--port', String(port)];
384
363
  let launchWrapperType = null;
@@ -452,7 +431,29 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
452
431
  child.on('exit', recordManagedProcessExit);
453
432
  child.on('close', recordManagedProcessExit);
454
433
 
455
- const url = await new Promise((resolve, reject) => {
434
+ // Ownership starts at spawn, including processes that never become ready.
435
+ const registration = registerManagedProcess({
436
+ pid: child.pid,
437
+ ownerPid: process.pid,
438
+ port,
439
+ binary,
440
+ runtime: process.env.OPENCHAMBER_RUNTIME || 'web',
441
+ });
442
+
443
+ let closePromise = null;
444
+ const serverProcess = {
445
+ url: null,
446
+ pid: child.pid || null,
447
+ get exitCode() { return observedExitCode ?? child.exitCode; },
448
+ get signalCode() { return observedSignalCode ?? child.signalCode; },
449
+ get stderrTail() { return getManagedProcessSnapshot().stderrTail; },
450
+ close() {
451
+ if (!closePromise) closePromise = registration.then(() => closeManagedOpenCodeChild(child));
452
+ return closePromise;
453
+ },
454
+ };
455
+
456
+ const readiness = new Promise((resolve, reject) => {
456
457
  let stdout = '';
457
458
  let stderr = '';
458
459
  let done = false;
@@ -507,37 +508,21 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
507
508
  child.stderr?.on('data', onStderr);
508
509
  child.on('exit', onExit);
509
510
  child.on('error', onError);
511
+ }).catch(async (error) => {
512
+ await serverProcess.close();
513
+ if (state.openCodeProcess === serverProcess) {
514
+ state.openCodeProcess = null;
515
+ syncToHmrState();
516
+ }
517
+ throw error;
510
518
  });
511
519
 
512
- // Record this child so a future run can reap it if we crash before teardown.
513
- // The web-server lifecycle runs in-process inside multiple hosts, so tag the
514
- // actual host (Electron sets OPENCHAMBER_RUNTIME='desktop'; the standalone
515
- // web CLI leaves it unset → 'web'; SSH remote → 'ssh-remote') rather than a
516
- // hardcoded label, matching the server's existing runtimeName convention.
517
- await registerManagedProcess({
518
- pid: child.pid,
519
- ownerPid: process.pid,
520
- port,
521
- binary,
522
- runtime: process.env.OPENCHAMBER_RUNTIME || 'web',
523
- });
524
-
525
- return {
526
- url,
527
- pid: child.pid || null,
528
- get exitCode() {
529
- return observedExitCode ?? child.exitCode;
530
- },
531
- get signalCode() {
532
- return observedSignalCode ?? child.signalCode;
533
- },
534
- get stderrTail() {
535
- return getManagedProcessSnapshot().stderrTail;
536
- },
537
- async close() {
538
- await closeManagedOpenCodeChild(child);
539
- },
540
- };
520
+ // Shutdown must be able to close an in-flight startup, not only a ready server.
521
+ state.openCodeProcess = serverProcess;
522
+ syncToHmrState();
523
+ serverProcess.url = await readiness;
524
+ await registration;
525
+ return serverProcess;
541
526
  };
542
527
 
543
528
  const resolveManagedOpenCodePort = async (requestedPort, hostname = '127.0.0.1') => {
@@ -694,7 +679,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
694
679
  );
695
680
 
696
681
  await applyOpencodeBinaryFromSettings({ strict: true });
697
- ensureOpencodeCliEnv();
682
+ const resolvedBinary = ensureOpencodeCliEnv();
698
683
  recordStartupPerformance('opencode.binary.ready', {
699
684
  attempt,
700
685
  durationMs: performance.now() - phaseStartedAt,
@@ -719,11 +704,14 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
719
704
  });
720
705
  phaseStartedAt = performance.now();
721
706
 
707
+ let serverInstance;
722
708
  try {
723
- const serverInstance = await createManagedOpenCodeServerProcess({
709
+ if (state.isShuttingDown) throw new Error('OpenCode startup cancelled during shutdown');
710
+ serverInstance = await createManagedOpenCodeServerProcess({
711
+ resolvedBinary,
724
712
  hostname: env.ENV_CONFIGURED_OPENCODE_HOSTNAME,
725
713
  port: spawnPort,
726
- timeout: 30000,
714
+ timeout: managedStartupTimeoutMs,
727
715
  cwd: state.openCodeWorkingDirectory,
728
716
  shellEnvKeysCount: Object.keys(shellEnv).length,
729
717
  env: stripAppImageArgv0Leak(applyProviderEnvAliases({
@@ -749,7 +737,9 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
749
737
  const port = parseInt(url.port, 10);
750
738
  const prefix = normalizeApiPrefix(url.pathname);
751
739
 
752
- if (await waitForReady(serverInstance.url, 10000)) {
740
+ const ready = await waitForReady(serverInstance.url, 10000);
741
+ if (state.isShuttingDown) throw new Error('OpenCode startup cancelled during shutdown');
742
+ if (ready) {
753
743
  setOpenCodePort(port);
754
744
  setDetectedOpenCodeApiPrefix(prefix);
755
745
 
@@ -767,12 +757,10 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
767
757
  return serverInstance;
768
758
  }
769
759
 
770
- try {
771
- await serverInstance.close();
772
- } catch {
773
- }
774
760
  throw new Error('Server started but health check failed (timeout)');
775
761
  } catch (error) {
762
+ await serverInstance?.close();
763
+ if (serverInstance && state.openCodeProcess === serverInstance) state.openCodeProcess = null;
776
764
  const message = error instanceof Error ? error.message : String(error);
777
765
  state.lastOpenCodeError = message;
778
766
  state.openCodePort = null;
@@ -794,7 +782,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
794
782
  return await startOpenCodeOnce(attempt);
795
783
  } catch (error) {
796
784
  lastError = error;
797
- if (error?.code === 'OPENCODE_BINARY_INVALID') {
785
+ if (state.isShuttingDown || error?.code === 'OPENCODE_BINARY_INVALID') {
798
786
  break;
799
787
  }
800
788
  if (attempt >= START_OPEN_CODE_MAX_ATTEMPTS) {
@@ -126,6 +126,35 @@ const createRuntime = (overrides = {}, stateOverrides = {}, envOverrides = {}) =
126
126
  };
127
127
 
128
128
  describe('OpenCode lifecycle', () => {
129
+ it('uses the resolved binary directly on startup and managed restart without an env override', async () => {
130
+ delete process.env.OPENCODE_BINARY;
131
+ const binaries = ['/bundle one/opencode-cli/opencode', '/bundle two/opencode-cli/opencode'];
132
+ const ensureOpencodeCliEnv = vi.fn()
133
+ .mockReturnValueOnce(binaries[0])
134
+ .mockReturnValueOnce(binaries[1]);
135
+ spawnMock.mockImplementation(() => {
136
+ const child = createMockChild();
137
+ queueMicrotask(() => child.stdout.emit('data', 'opencode server listening on http://127.0.0.1:45678\n'));
138
+ return child;
139
+ });
140
+ globalThis.fetch = vi.fn(async () => ({ ok: false }));
141
+ const runtime = createRuntime({ ensureOpencodeCliEnv });
142
+ const firstServer = await runtime.startOpenCode();
143
+ runtime.testState.openCodeProcess = firstServer;
144
+ try {
145
+ await runtime.restartOpenCode();
146
+ expect(firstServer.signalCode).toBe('SIGTERM');
147
+ expect(spawnMock.mock.calls.map(([binary]) => binary)).toEqual(binaries);
148
+ expect(runtime.testState.lastOpenCodeLaunchDiagnostics.sourceBinary).toBe(binaries[1]);
149
+ expect(process.env.OPENCODE_BINARY).toBeUndefined();
150
+ for (const [, , options] of spawnMock.mock.calls) {
151
+ expect(options.env).not.toHaveProperty('OPENCODE_BINARY');
152
+ }
153
+ } finally {
154
+ await runtime.testState.openCodeProcess.close();
155
+ }
156
+ });
157
+
129
158
  it('records an authoritative ready terminal event for external startup', async () => {
130
159
  globalThis.fetch = vi.fn(async () => ({
131
160
  ok: true,
@@ -8,7 +8,7 @@ const isJsonObject = (value) => value !== null && value !== undefined && Object.
8
8
  *
9
9
  * Existing entries are preserved; an earlier entry for the same URL is dropped
10
10
  * so a restart never registers the same plugin twice. Every managed plugin
11
- * (agent tools, system prompt optimizer, MCP reconnect) goes through this one
11
+ * (agent tools, system prompt optimizer) goes through this one
12
12
  * merge so they compose in any order.
13
13
  *
14
14
  * @param {string | undefined} rawConfig the current `OPENCODE_CONFIG_CONTENT`, JSONC or unset