@vantran-se/openchamber-web 1.23.1 → 1.24.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/bin/cli.test.js +29 -6
  2. package/bin/lib/cli-http.js +5 -1
  3. package/bin/lib/commands-status.js +5 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-BOpqXpFU.js → CodeMirrorCommentWidgets-B4a0JkUz.js} +1 -1
  5. package/dist/assets/DiffView-B2CD9wC8.js +4 -0
  6. package/dist/assets/FilesView-C4vMFclc.js +13 -0
  7. package/dist/assets/GitView-Byu2DY7s.js +6 -0
  8. package/dist/assets/GuestAttachDialog-B808E0-d.js +1 -0
  9. package/dist/assets/{JsonTreeView-nd_E8eVM.js → JsonTreeView-VW_7czo3.js} +1 -1
  10. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  12. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  30. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  33. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  39. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  42. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  54. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  57. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  60. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  62. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  65. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  68. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  69. package/dist/assets/LinearIssuesView-EzGNCGrd.js +1 -0
  70. package/dist/assets/{MarkdownImageGallery-CVYds3sQ.js → MarkdownImageGallery-B2YUDFaB.js} +1 -1
  71. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +12 -0
  72. package/dist/assets/{OnboardingScreen-Dd2IbPTn.js → OnboardingScreen-CQHBSTEi.js} +1 -1
  73. package/dist/assets/{PatchDiff-nUu5i4bG.js → PatchDiff-DCjGhlYV.js} +1 -1
  74. package/dist/assets/PierreDiffViewer-CEDZNkU2.js +210 -0
  75. package/dist/assets/PlanView-D3x-pTkZ.js +5 -0
  76. package/dist/assets/PluginPane-DEcgDQ9h.js +3 -0
  77. package/dist/assets/SettingsView-CSinuH-1.js +29 -0
  78. package/dist/assets/{SettingsWindow-BccxY81J.js → SettingsWindow-B5CHoJFZ.js} +1 -1
  79. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +1 -0
  80. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +15 -0
  81. package/dist/assets/{ToolPartDiffPreview-BFVgk0UK.js → ToolPartDiffPreview-CflzmK-x.js} +2 -2
  82. package/dist/assets/{VirtualizedFileDiff-z3_H74UR.js → VirtualizedFileDiff-NU0FHFhT.js} +3 -3
  83. package/dist/assets/{WalkthroughView-BqM9GFbJ.js → WalkthroughView-BZyjhqHv.js} +2 -2
  84. package/dist/assets/appThemeRegistry-DLicw4dH.js +1 -0
  85. package/dist/assets/commitAndPush-BDftrjr6.js +1 -0
  86. package/dist/assets/{computeVirtualFileMetrics-Bdazp9Qo.js → computeVirtualFileMetrics-DlWlEM3x.js} +1 -1
  87. package/dist/assets/de-Bro6_zyl.js +10 -0
  88. package/dist/assets/es-Bb9XQZvd.js +16 -0
  89. package/dist/assets/fr-Dgfzn-IL.js +14 -0
  90. package/dist/assets/{index-eo1-D_7z.js → index-CtQNmqUL.js} +1 -1
  91. package/dist/assets/index-DqaMWNrC.css +1 -0
  92. package/dist/assets/index-Dqc6USl3.js +40 -0
  93. package/dist/assets/{isDiffPlainText-BvKH712-.js → isDiffPlainText-CO4e5J_S.js} +1 -1
  94. package/dist/assets/{isFilePlainText-BdVVPukf.js → isFilePlainText-ue6kCou7.js} +1 -1
  95. package/dist/assets/ja-BYsakGFh.js +16 -0
  96. package/dist/assets/ko-BSaQz9aN.js +16 -0
  97. package/dist/assets/main-B62_pjL5.js +2 -0
  98. package/dist/assets/main-BEdcOI2P.js +896 -0
  99. package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
  100. package/dist/assets/markdownCore-CK48LG0z.js +5 -0
  101. package/dist/assets/miniChat-LT8Rqofr.js +2 -0
  102. package/dist/assets/mobile-CqarEynh.js +2 -0
  103. package/dist/assets/{patchFileDiff-B7LjppSS.js → patchFileDiff-D7QVlovD.js} +3 -3
  104. package/dist/assets/pl-OIviPdoj.js +16 -0
  105. package/dist/assets/pt-BR-Cltg-E6s.js +16 -0
  106. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +2 -0
  107. package/dist/assets/renderMobileApp-BC1MHg6g.js +25 -0
  108. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +25 -0
  109. package/dist/assets/{scheduledTasksApi-hceovDei.js → scheduledTasksApi-B2SWMGEU.js} +1 -1
  110. package/dist/assets/shikiHighlight-BPu3JiQs.js +1 -0
  111. package/dist/assets/textMateThemeFromAppTheme-DWqRPo9A.js +1 -0
  112. package/dist/assets/tr-CQI-KPre.js +7 -0
  113. package/dist/assets/uk-CqW7R4DA.js +16 -0
  114. package/dist/assets/useAppFontEffects-ne84JBVL.js +7333 -0
  115. package/dist/assets/useInlineCommentController-CgS0fnw_.js +2 -0
  116. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +2 -0
  117. package/dist/assets/{vendor-base-ui-BY_5qla_.js → vendor-base-ui-CsXYnR5c.js} +1 -1
  118. package/dist/assets/{vendor-codemirror-language-data-DWA1bm1F.js → vendor-codemirror-language-data-Bx2EHc1O.js} +2 -2
  119. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
  120. package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
  121. package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
  122. package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
  123. package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
  124. package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
  125. package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
  126. package/dist/assets/{vendor-zod-B6551dwG.js → vendor-zod-CgAdXWZO.js} +4 -4
  127. package/dist/assets/{web-components-B_Z64So5.js → web-components-DxX8bZ1o.js} +2 -2
  128. package/dist/assets/zh-CN-CVZG_o_J.js +16 -0
  129. package/dist/assets/zh-TW-D5ZxzCZp.js +16 -0
  130. package/dist/index.html +6 -4
  131. package/dist/mini-chat.html +6 -4
  132. package/dist/mobile.html +6 -4
  133. package/package.json +12 -9
  134. package/server/built-in-extensions/registry.json +4 -0
  135. package/server/index.d.ts +2 -0
  136. package/server/index.js +44 -10
  137. package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
  138. package/server/lib/agent-tool/runtime.js +62 -15
  139. package/server/lib/agent-tool/runtime.test.js +88 -2
  140. package/server/lib/changelog/update-notes.js +2 -1
  141. package/server/lib/changelog/update-notes.test.js +19 -0
  142. package/server/lib/event-stream/DOCUMENTATION.md +23 -0
  143. package/server/lib/event-stream/delta-coalescer.js +0 -0
  144. package/server/lib/event-stream/delta-coalescer.test.js +292 -0
  145. package/server/lib/event-stream/global-hub.js +53 -23
  146. package/server/lib/event-stream/global-hub.test.js +160 -0
  147. package/server/lib/event-stream/global-ws-bridge.js +1 -0
  148. package/server/lib/event-stream/index.js +4 -0
  149. package/server/lib/fs/DOCUMENTATION.md +10 -1
  150. package/server/lib/fs/git-process.test.js +77 -0
  151. package/server/lib/fs/routes.js +58 -5
  152. package/server/lib/fs/routes.test.js +116 -0
  153. package/server/lib/fs/search.js +2 -1
  154. package/server/lib/git/DOCUMENTATION.md +20 -5
  155. package/server/lib/git/routes.js +16 -3
  156. package/server/lib/git/serial-refresh.js +93 -0
  157. package/server/lib/git/serial-refresh.test.js +112 -0
  158. package/server/lib/git/service.js +429 -70
  159. package/server/lib/git/service.test.js +418 -0
  160. package/server/lib/github/pr-status.js +5 -5
  161. package/server/lib/github/pr-status.test.js +1 -1
  162. package/server/lib/github/routes.js +4 -4
  163. package/server/lib/guests/DOCUMENTATION.md +68 -0
  164. package/server/lib/guests/auth-store.js +184 -0
  165. package/server/lib/guests/auth-store.test.js +112 -0
  166. package/server/lib/guests/background.test.js +83 -0
  167. package/server/lib/guests/builtins.js +34 -0
  168. package/server/lib/guests/builtins.test.js +174 -0
  169. package/server/lib/guests/catalog.js +477 -0
  170. package/server/lib/guests/catalog.test.js +390 -0
  171. package/server/lib/guests/clone.js +297 -0
  172. package/server/lib/guests/extract-zip.js +136 -0
  173. package/server/lib/guests/extract-zip.test.js +109 -0
  174. package/server/lib/guests/files.js +308 -0
  175. package/server/lib/guests/files.test.js +262 -0
  176. package/server/lib/guests/grant-scope.js +81 -0
  177. package/server/lib/guests/grant-scope.test.js +75 -0
  178. package/server/lib/guests/host-session.js +73 -0
  179. package/server/lib/guests/host-session.test.js +30 -0
  180. package/server/lib/guests/html-styles.js +6 -0
  181. package/server/lib/guests/html-styles.test.js +45 -0
  182. package/server/lib/guests/html-tokens.js +73 -0
  183. package/server/lib/guests/html-tokens.test.js +53 -0
  184. package/server/lib/guests/install.js +406 -0
  185. package/server/lib/guests/install.test.js +338 -0
  186. package/server/lib/guests/oauth.js +501 -0
  187. package/server/lib/guests/oauth.test.js +481 -0
  188. package/server/lib/guests/persist.js +336 -0
  189. package/server/lib/guests/persist.test.js +288 -0
  190. package/server/lib/guests/request.js +112 -0
  191. package/server/lib/guests/request.test.js +206 -0
  192. package/server/lib/guests/routes.js +838 -0
  193. package/server/lib/guests/service.js +538 -0
  194. package/server/lib/guests/service.test.js +277 -0
  195. package/server/lib/guests/sockets.js +129 -0
  196. package/server/lib/guests/sockets.test.js +110 -0
  197. package/server/lib/guests/ssh-install.test.js +188 -0
  198. package/server/lib/guests/storage.js +68 -0
  199. package/server/lib/guests/storage.test.js +59 -0
  200. package/server/lib/guests/updates.js +294 -0
  201. package/server/lib/guests/updates.test.js +334 -0
  202. package/server/lib/guests/upload.js +50 -0
  203. package/server/lib/guests/upload.test.js +131 -0
  204. package/server/lib/linear/DOCUMENTATION.md +1 -1
  205. package/server/lib/message-queue/DOCUMENTATION.md +10 -1
  206. package/server/lib/message-queue/runtime.js +21 -1
  207. package/server/lib/message-queue/runtime.test.js +52 -5
  208. package/server/lib/notifications/routes.js +5 -0
  209. package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
  210. package/server/lib/openchamber-sessions/routes.js +62 -29
  211. package/server/lib/openchamber-sessions/routes.test.js +96 -0
  212. package/server/lib/opencode/DOCUMENTATION.md +55 -1
  213. package/server/lib/opencode/bootstrap-runtime.js +1 -0
  214. package/server/lib/opencode/core-routes.js +39 -1
  215. package/server/lib/opencode/feature-routes-runtime.js +15 -0
  216. package/server/lib/opencode/lifecycle-process.test.js +84 -0
  217. package/server/lib/opencode/lifecycle.js +54 -67
  218. package/server/lib/opencode/openchamber-routes.js +72 -14
  219. package/server/lib/opencode/openchamber-routes.test.js +71 -3
  220. package/server/lib/opencode/session-runtime.js +68 -0
  221. package/server/lib/opencode/session-runtime.test.js +43 -0
  222. package/server/lib/opencode/settings-helpers.js +8 -0
  223. package/server/lib/opencode/settings-helpers.test.js +13 -1
  224. package/server/lib/opencode/settings-normalization-runtime.js +2 -0
  225. package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
  226. package/server/lib/opencode/settings-registry.json +15 -0
  227. package/server/lib/opencode/shutdown-runtime.js +3 -0
  228. package/server/lib/opencode/shutdown-runtime.test.js +24 -0
  229. package/server/lib/opencode/theme-archive.js +53 -0
  230. package/server/lib/opencode/theme-catalog.js +171 -0
  231. package/server/lib/opencode/theme-catalog.test.js +125 -0
  232. package/server/lib/opencode/theme-import.test.js +144 -0
  233. package/server/lib/opencode/theme-runtime.js +100 -28
  234. package/server/lib/opencode/theme-runtime.test.js +15 -0
  235. package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
  236. package/server/lib/permission-auto-accept/runtime.js +13 -0
  237. package/server/lib/permission-auto-accept/runtime.test.js +37 -1
  238. package/server/lib/projects/project-config.js +2 -2
  239. package/server/lib/quota/DOCUMENTATION.md +0 -1
  240. package/server/lib/quota/providers/index.js +0 -7
  241. package/server/lib/realtime-proxy-terminal.test.js +150 -0
  242. package/server/lib/realtime-proxy.js +2 -0
  243. package/server/lib/routing/DOCUMENTATION.md +83 -0
  244. package/server/lib/routing/defaults.js +98 -0
  245. package/server/lib/routing/feature-flag.js +11 -0
  246. package/server/lib/routing/history.js +34 -0
  247. package/server/lib/routing/jev.js +98 -0
  248. package/server/lib/routing/routes.http.test.js +100 -0
  249. package/server/lib/routing/routes.js +81 -0
  250. package/server/lib/routing/runtime.js +217 -0
  251. package/server/lib/routing/runtime.test.js +188 -0
  252. package/server/lib/routing/store.js +249 -0
  253. package/server/lib/routing/store.test.js +96 -0
  254. package/server/lib/scheduled-tasks/runtime.js +2 -2
  255. package/server/lib/scheduled-tasks/runtime.test.js +14 -0
  256. package/server/lib/security/request-security.js +9 -5
  257. package/server/lib/security/request-security.test.js +37 -0
  258. package/server/lib/small-model/DOCUMENTATION.md +14 -2
  259. package/server/lib/small-model/call.js +37 -6
  260. package/server/lib/small-model/call.test.js +182 -0
  261. package/server/lib/terminal/DOCUMENTATION.md +23 -2
  262. package/server/lib/terminal/runtime.js +27 -5
  263. package/server/lib/terminal/runtime.test.js +67 -0
  264. package/server/lib/terminal/shutdown.js +106 -0
  265. package/server/lib/terminal/shutdown.test.js +84 -0
  266. package/server/lib/tts/service.test.js +78 -0
  267. package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
  268. package/server/lib/ui-auth/session-cookie.js +51 -0
  269. package/server/lib/ui-auth/session-cookie.test.js +46 -0
  270. package/server/lib/ui-auth/ui-auth.js +56 -17
  271. package/server/lib/ui-auth/ui-auth.test.js +96 -0
  272. package/dist/assets/DiffView-CyLmb-wF.js +0 -4
  273. package/dist/assets/DirtyBranchSwitchDialog-pu5kcoir.js +0 -1
  274. package/dist/assets/FilesView-WtJB0y_l.js +0 -13
  275. package/dist/assets/GitView-DkNrV0BK.js +0 -6
  276. package/dist/assets/LinearIssuesView-B0H0Otii.js +0 -1
  277. package/dist/assets/MarkdownRendererImpl-DSqVN2d8.js +0 -12
  278. package/dist/assets/PierreDiffViewer-Bsq9z-sw.js +0 -161
  279. package/dist/assets/PlanView-CAFziKH5.js +0 -5
  280. package/dist/assets/SettingsView-BRittavZ.js +0 -28
  281. package/dist/assets/ToolOutputDialog-B0uYaJio.js +0 -15
  282. package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
  283. package/dist/assets/de-c7CYLCAW.js +0 -10
  284. package/dist/assets/es-BKAVPQmQ.js +0 -16
  285. package/dist/assets/fr-Bj1glqfV.js +0 -14
  286. package/dist/assets/index-BYFyOu_D.css +0 -1
  287. package/dist/assets/index-BfwRClDe.js +0 -40
  288. package/dist/assets/ja-5jwSIbK0.js +0 -16
  289. package/dist/assets/ko-BaDbg-Eb.js +0 -16
  290. package/dist/assets/main-CGG2_uHq.js +0 -2
  291. package/dist/assets/main-CcrSaL4J.js +0 -930
  292. package/dist/assets/markdownCore-DsmQr3bt.js +0 -5
  293. package/dist/assets/miniChat-DEaAhP4b.js +0 -2
  294. package/dist/assets/mobile-BC3rsvv0.js +0 -2
  295. package/dist/assets/pl-BFKVBDFu.js +0 -16
  296. package/dist/assets/pt-BR-qKfHvkDL.js +0 -16
  297. package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
  298. package/dist/assets/renderElectronMiniChatApp-CR9d1Mr0.js +0 -2
  299. package/dist/assets/renderMobileApp-Da5dwobZ.js +0 -25
  300. package/dist/assets/runtimeEndpointReset-CLYVRw2H.js +0 -41
  301. package/dist/assets/shikiHighlight-D1Z9G58W.js +0 -1
  302. package/dist/assets/tr-DYXPoEDG.js +0 -7
  303. package/dist/assets/uk-CwAoloR8.js +0 -16
  304. package/dist/assets/useAppFontEffects-DeStGcUT.js +0 -7282
  305. package/dist/assets/useGitComparison-CpxioduU.js +0 -1
  306. package/dist/assets/useInlineCommentController-gmUnJtrb.js +0 -2
  307. package/dist/assets/useRootScrollLock--a3cPz8s.js +0 -1
  308. package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
  309. package/dist/assets/zh-CN-D9MG0lqF.js +0 -16
  310. package/dist/assets/zh-TW-1HNK7shh.js +0 -16
  311. package/server/lib/quota/providers/crof.js +0 -96
  312. package/server/lib/quota/providers/crof.test.js +0 -98
  313. /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
@@ -0,0 +1,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);
@@ -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') => {
@@ -719,12 +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({
724
711
  resolvedBinary,
725
712
  hostname: env.ENV_CONFIGURED_OPENCODE_HOSTNAME,
726
713
  port: spawnPort,
727
- timeout: 30000,
714
+ timeout: managedStartupTimeoutMs,
728
715
  cwd: state.openCodeWorkingDirectory,
729
716
  shellEnvKeysCount: Object.keys(shellEnv).length,
730
717
  env: stripAppImageArgv0Leak(applyProviderEnvAliases({
@@ -750,7 +737,9 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
750
737
  const port = parseInt(url.port, 10);
751
738
  const prefix = normalizeApiPrefix(url.pathname);
752
739
 
753
- 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) {
754
743
  setOpenCodePort(port);
755
744
  setDetectedOpenCodeApiPrefix(prefix);
756
745
 
@@ -768,12 +757,10 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
768
757
  return serverInstance;
769
758
  }
770
759
 
771
- try {
772
- await serverInstance.close();
773
- } catch {
774
- }
775
760
  throw new Error('Server started but health check failed (timeout)');
776
761
  } catch (error) {
762
+ await serverInstance?.close();
763
+ if (serverInstance && state.openCodeProcess === serverInstance) state.openCodeProcess = null;
777
764
  const message = error instanceof Error ? error.message : String(error);
778
765
  state.lastOpenCodeError = message;
779
766
  state.openCodePort = null;
@@ -795,7 +782,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
795
782
  return await startOpenCodeOnce(attempt);
796
783
  } catch (error) {
797
784
  lastError = error;
798
- if (error?.code === 'OPENCODE_BINARY_INVALID') {
785
+ if (state.isShuttingDown || error?.code === 'OPENCODE_BINARY_INVALID') {
799
786
  break;
800
787
  }
801
788
  if (attempt >= START_OPEN_CODE_MAX_ATTEMPTS) {
@@ -16,6 +16,43 @@ function quotePosixShell(value) {
16
16
  return `'${String(value).replace(/'/g, "'\\''")}'`;
17
17
  }
18
18
 
19
+ /**
20
+ * `echo` in batch treats `& | < > ( ) ^` as syntax and `%var%` as expansion,
21
+ * so a line of the log preamble is escaped before it becomes an `echo`.
22
+ */
23
+ const escapeBatchEchoLine = (line) => {
24
+ if (!line) return 'echo.';
25
+ return `echo ${line.replace(/%/g, '%%').replace(/[&|<>()^]/g, '^$&')}`;
26
+ };
27
+
28
+ /**
29
+ * The Windows update runs as a batch file, one command per line, so the
30
+ * `if ... else` block and the multi-line log preamble keep their structure.
31
+ */
32
+ const buildWindowsUpdateScript = ({ logPreamble, updateCmd, restartCmd }) => [
33
+ '@echo off',
34
+ ...logPreamble.split('\n').map(escapeBatchEchoLine),
35
+ // `timeout` refuses redirected stdin, which is what a detached child has;
36
+ // a ping to loopback waits about two seconds without a console.
37
+ 'ping -n 3 127.0.0.1 >nul',
38
+ // npm, pnpm and yarn are .cmd shims on Windows. Without `call`, a batch
39
+ // file hands control to them for good and the restart below never runs.
40
+ // Read from a file, cmd expands `%x%` and drops a lone `%`, so a `%` in a
41
+ // host or UI password would change the restart command. Doubling keeps it.
42
+ `call ${updateCmd.replace(/%/g, '%%')}`,
43
+ 'if %ERRORLEVEL% EQU 0 (',
44
+ ' echo Update successful, restarting OpenChamber...',
45
+ ` ${restartCmd ? restartCmd.replace(/%/g, '%%') : 'echo Service manager will restart OpenChamber.'}`,
46
+ ') else (',
47
+ ' echo Update failed',
48
+ ')',
49
+ // The restart command carries the server's own flags, `--ui-password`
50
+ // included, so the file does not outlive the run. Deleting the running
51
+ // batch file on its last line is safe: cmd has already read it.
52
+ 'del "%~f0"',
53
+ '',
54
+ ].join('\r\n');
55
+
19
56
  export const registerOpenChamberRoutes = (app, dependencies) => {
20
57
  const {
21
58
  fs,
@@ -300,6 +337,26 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
300
337
  `logPath=${updateLogPath}`,
301
338
  ].join('\n');
302
339
 
340
+ const shell = isWindows ? (process.env.ComSpec || 'cmd.exe') : 'sh';
341
+ const shellFlag = isWindows ? '/c' : '-c';
342
+ // cmd.exe /c takes one command line: a newline inside it ends the
343
+ // command, so a multi-line script passed as the argument ran nothing
344
+ // and exited 0, and the server shut itself down believing the update
345
+ // was under way (#3084). Batch runs from a file instead, written before
346
+ // the client is told to expect a restart.
347
+ const windowsScriptPath = path.join(openchamberDataDir, 'update-install.cmd');
348
+ if (isWindows) {
349
+ try {
350
+ fs.mkdirSync(path.dirname(windowsScriptPath), { recursive: true });
351
+ fs.writeFileSync(windowsScriptPath, buildWindowsUpdateScript({ logPreamble, updateCmd, restartCmd }), 'utf8');
352
+ } catch (scriptError) {
353
+ console.error('Failed to write the update script, update not started:', scriptError);
354
+ return res.status(500).json({
355
+ error: `Could not write the update script at ${windowsScriptPath}: ${scriptError instanceof Error ? scriptError.message : String(scriptError)}`,
356
+ });
357
+ }
358
+ }
359
+
303
360
  res.json({
304
361
  success: true,
305
362
  message: 'Update starting, server will restart shortly',
@@ -314,21 +371,8 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
314
371
  console.log(`Running: ${updateCmd}`);
315
372
  console.log(logPreamble);
316
373
 
317
- const shell = isWindows ? (process.env.ComSpec || 'cmd.exe') : 'sh';
318
- const shellFlag = isWindows ? '/c' : '-c';
319
374
  const script = isWindows
320
- ? `
321
- echo ${quoteCmd(logPreamble)}
322
- timeout /t 2 /nobreak >nul
323
- ${updateCmd}
324
- if %ERRORLEVEL% EQU 0 (
325
- echo Update successful, restarting OpenChamber...
326
- ${restartCmd || 'echo Service manager will restart OpenChamber.'}
327
- ) else (
328
- echo Update failed
329
- exit /b 1
330
- )
331
- `
375
+ ? windowsScriptPath
332
376
  : `
333
377
  printf '%s\n' ${quotePosix(logPreamble)}
334
378
  sleep 2
@@ -350,10 +394,24 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
350
394
  console.warn('Failed to open update log file, continuing without log capture:', logError);
351
395
  }
352
396
 
397
+ if (isWindows) {
398
+ // On Windows the detached child inherits this process's listening
399
+ // socket, and keeps the port for as long as the batch runs. The
400
+ // restart inside that batch then fails with "port already in use",
401
+ // and the update ends with no server. Closing the listener first
402
+ // leaves nothing to inherit; the process exits right after anyway.
403
+ try {
404
+ server.close();
405
+ } catch (closeError) {
406
+ console.warn('Failed to close the listener before the update script:', closeError);
407
+ }
408
+ }
409
+
353
410
  const child = spawnChild(shell, [shellFlag, script], {
354
411
  detached: true,
355
412
  stdio: logFd !== null ? ['ignore', logFd, logFd] : 'ignore',
356
413
  env: process.env,
414
+ windowsHide: true,
357
415
  });
358
416
  child.unref();
359
417
 
@@ -18,11 +18,15 @@ const childProcess = await import('child_process');
18
18
  const packageManager = await import('../package-manager.js');
19
19
  const { registerOpenChamberRoutes } = await import('./openchamber-routes.js');
20
20
 
21
- const createApp = ({ environment = {}, storedOptions = {}, desktopUpdater } = {}) => {
21
+ const createApp = ({ environment = {}, storedOptions = {}, desktopUpdater, platform = 'linux', execPath = '/usr/bin/node' } = {}) => {
22
22
  const app = express();
23
23
  const dependencies = {
24
24
  fs: {
25
25
  existsSync: vi.fn(() => false),
26
+ mkdirSync: vi.fn(),
27
+ writeFileSync: vi.fn(),
28
+ openSync: vi.fn(() => 7),
29
+ closeSync: vi.fn(),
26
30
  promises: {
27
31
  readFile: vi.fn(async () => JSON.stringify({
28
32
  launchMode: 'foreground',
@@ -34,11 +38,13 @@ const createApp = ({ environment = {}, storedOptions = {}, desktopUpdater } = {}
34
38
  path,
35
39
  process: {
36
40
  env: environment,
37
- platform: 'linux',
38
- execPath: '/usr/bin/node',
41
+ platform,
42
+ execPath,
43
+ exit: vi.fn(),
39
44
  },
40
45
  server: {
41
46
  address: () => ({ port: 7897 }),
47
+ close: vi.fn(),
42
48
  },
43
49
  __dirname: '/opt/openchamber/server',
44
50
  openchamberDataDir: '/tmp/openchamber',
@@ -268,3 +274,65 @@ describe('OpenChamber foreground update route', () => {
268
274
  });
269
275
  });
270
276
  });
277
+
278
+ describe('OpenChamber web update route on Windows', () => {
279
+ it('runs the install-and-restart script from a batch file instead of a cmd.exe /c argument', async () => {
280
+ const { app, dependencies } = createApp({
281
+ platform: 'win32',
282
+ execPath: 'C:\\Program Files\\nodejs\\node.exe',
283
+ environment: { ComSpec: 'C:\\Windows\\system32\\cmd.exe' },
284
+ storedOptions: { launchMode: 'daemon', port: 7897, uiPassword: 'pa%ss' },
285
+ });
286
+ childProcess.spawn.mockReturnValue({ unref: vi.fn() });
287
+ vi.spyOn(console, 'log').mockImplementation(() => {});
288
+
289
+ await request(app).post('/api/openchamber/update-install').expect(200);
290
+ await new Promise((resolve) => setTimeout(resolve, 1300));
291
+
292
+ const scriptPath = path.join('/tmp/openchamber', 'update-install.cmd');
293
+ expect(dependencies.fs.writeFileSync).toHaveBeenCalledWith(scriptPath, expect.any(String), 'utf8');
294
+ const script = dependencies.fs.writeFileSync.mock.calls[0][1];
295
+ const lines = script.split('\r\n');
296
+ expect(lines[0]).toBe('@echo off');
297
+ // Every preamble line is an echo; none is left to run as a command.
298
+ expect(lines.filter((line) => line.startsWith('currentVersion=') || line.startsWith('restartCommand='))).toEqual([]);
299
+ expect(lines).toContain('echo packageManager=npm');
300
+ expect(lines).toContain('echo restartCommand=^("C:\\Program Files\\nodejs\\node.exe" "/opt/openchamber/bin/cli.js" serve --port 7897 --ui-password "pa%%ss"^) ^|^| ^(openchamber serve --port 7897 --ui-password "pa%%ss"^)');
301
+ // A .cmd shim (npm, pnpm, yarn) must be `call`ed or the script ends there.
302
+ expect(lines).toContain('call npm install -g @openchamber/web@latest');
303
+ expect(lines).toContain('ping -n 3 127.0.0.1 >nul');
304
+ expect(lines.some((line) => line.startsWith('timeout '))).toBe(false);
305
+ expect(lines).toContain('if %ERRORLEVEL% EQU 0 (');
306
+ expect(lines.at(-2)).toBe('del "%~f0"');
307
+ // A `%` in the password survives batch expansion only when doubled.
308
+ expect(lines).toContain(' ("C:\\Program Files\\nodejs\\node.exe" "/opt/openchamber/bin/cli.js" serve --port 7897 --ui-password "pa%%ss") || (openchamber serve --port 7897 --ui-password "pa%%ss")');
309
+
310
+ expect(childProcess.spawn).toHaveBeenCalledWith(
311
+ 'C:\\Windows\\system32\\cmd.exe',
312
+ ['/c', scriptPath],
313
+ expect.objectContaining({ detached: true, windowsHide: true }),
314
+ );
315
+ // The listener is closed before the batch is spawned, so the detached
316
+ // child cannot inherit the socket and hold the port against the restart.
317
+ expect(dependencies.server.close).toHaveBeenCalledOnce();
318
+ expect(dependencies.server.close.mock.invocationCallOrder[0]).toBeLessThan(childProcess.spawn.mock.invocationCallOrder[0]);
319
+ expect(dependencies.process.exit).toHaveBeenCalledWith(0);
320
+ });
321
+
322
+ it('answers 500 and keeps the server up when the batch file cannot be written', async () => {
323
+ const { app, dependencies } = createApp({ platform: 'win32', storedOptions: { launchMode: 'daemon', port: 7897 } });
324
+ dependencies.fs.writeFileSync.mockImplementation(() => { throw new Error('EACCES'); });
325
+ vi.spyOn(console, 'log').mockImplementation(() => {});
326
+ const logError = vi.spyOn(console, 'error').mockImplementation(() => {});
327
+
328
+ const response = await request(app).post('/api/openchamber/update-install').expect(500);
329
+ await new Promise((resolve) => setTimeout(resolve, 1300));
330
+
331
+ expect(response.body.error).toContain('update-install.cmd');
332
+ expect(response.body.error).toContain('EACCES');
333
+ expect(childProcess.spawn).not.toHaveBeenCalled();
334
+ expect(dependencies.process.exit).not.toHaveBeenCalled();
335
+ expect(logError).toHaveBeenCalledOnce();
336
+ });
337
+ });
338
+
@@ -38,13 +38,76 @@ const extractSessionStatusUpdate = (payload) => {
38
38
  };
39
39
  };
40
40
 
41
+ const readRequestId = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
42
+
41
43
  export const createSessionRuntime = ({ writeSseEvent, getNotificationClients, broadcastEvent }) => {
42
44
  const sessionActivityPhases = new Map();
43
45
  const sessionActivityCooldowns = new Map();
44
46
  const sessionStates = new Map();
45
47
  const sessionAttentionStates = new Map();
48
+ // Pending permission and question requests per session, kept from the same
49
+ // upstream stream. Clients that do not initialize a directory cannot read
50
+ // its pending list from OpenCode (that read creates an instance), so this
51
+ // map is their seed. Entries live until the matching reply, the session's
52
+ // deletion, or an OpenCode restart, which drops every pending request.
53
+ const pendingRequestsBySession = new Map();
46
54
  let activeSessionCount = 0;
47
55
 
56
+ const getOrCreatePendingRequests = (sessionId) => {
57
+ let entry = pendingRequestsBySession.get(sessionId);
58
+ if (!entry) {
59
+ entry = { permissions: new Map(), questions: new Map() };
60
+ pendingRequestsBySession.set(sessionId, entry);
61
+ }
62
+ return entry;
63
+ };
64
+
65
+ const settlePendingRequest = (kind, sessionId, requestId) => {
66
+ const entry = pendingRequestsBySession.get(sessionId);
67
+ if (!entry) return;
68
+ // OpenCode may omit the request ID on a reply; the session then has no
69
+ // pending request of that kind we can still vouch for.
70
+ if (requestId) entry[kind].delete(requestId);
71
+ else entry[kind].clear();
72
+ if (entry.permissions.size === 0 && entry.questions.size === 0) pendingRequestsBySession.delete(sessionId);
73
+ };
74
+
75
+ const processBlockingRequestPayload = (payload) => {
76
+ if (!payload || typeof payload.type !== 'string') return;
77
+ const properties = payload.properties && typeof payload.properties === 'object' ? payload.properties : {};
78
+ if (payload.type === 'permission.asked' || payload.type === 'question.asked') {
79
+ const sessionId = readRequestId(properties.sessionID);
80
+ const requestId = readRequestId(properties.id);
81
+ if (!sessionId || !requestId) return;
82
+ getOrCreatePendingRequests(sessionId)[payload.type === 'permission.asked' ? 'permissions' : 'questions'].set(requestId, properties);
83
+ return;
84
+ }
85
+ if (payload.type === 'permission.replied') {
86
+ settlePendingRequest('permissions', readRequestId(properties.sessionID), readRequestId(properties.requestID));
87
+ return;
88
+ }
89
+ if (payload.type === 'question.replied' || payload.type === 'question.rejected') {
90
+ settlePendingRequest('questions', readRequestId(properties.sessionID), readRequestId(properties.requestID));
91
+ return;
92
+ }
93
+ if (payload.type === 'session.deleted') {
94
+ const info = properties.info && typeof properties.info === 'object' ? properties.info : {};
95
+ const sessionId = readRequestId(properties.sessionID) || readRequestId(info.id);
96
+ if (sessionId) pendingRequestsBySession.delete(sessionId);
97
+ }
98
+ };
99
+
100
+ const getPendingBlockingRequestsSnapshot = () => {
101
+ const result = {};
102
+ for (const [sessionId, entry] of pendingRequestsBySession) {
103
+ result[sessionId] = {
104
+ permissions: [...entry.permissions.values()],
105
+ questions: [...entry.questions.values()],
106
+ };
107
+ }
108
+ return result;
109
+ };
110
+
48
111
  const getOrCreateAttentionState = (sessionId) => {
49
112
  if (!sessionId || typeof sessionId !== 'string') return null;
50
113
 
@@ -307,6 +370,8 @@ export const createSessionRuntime = ({ writeSseEvent, getNotificationClients, br
307
370
  }
308
371
  }
309
372
 
373
+ // A restarted OpenCode forgot every pending request with the turns.
374
+ pendingRequestsBySession.clear();
310
375
  const eventId = `opencode-restart-${Date.now()}`;
311
376
  for (const sessionId of interruptedSessionIds) {
312
377
  updateSessionState(sessionId, 'idle', eventId, {
@@ -354,6 +419,7 @@ export const createSessionRuntime = ({ writeSseEvent, getNotificationClients, br
354
419
  const cleanupInterval = setInterval(cleanupOldSessionStates, SESSION_STATE_CLEANUP_INTERVAL_MS);
355
420
 
356
421
  const processOpenCodeSsePayload = (payload) => {
422
+ processBlockingRequestPayload(payload);
357
423
  const update = extractSessionStatusUpdate(payload);
358
424
  if (!update) return;
359
425
 
@@ -379,6 +445,7 @@ export const createSessionRuntime = ({ writeSseEvent, getNotificationClients, br
379
445
  sessionActivityPhases.clear();
380
446
  sessionStates.clear();
381
447
  sessionAttentionStates.clear();
448
+ pendingRequestsBySession.clear();
382
449
  activeSessionCount = 0;
383
450
  };
384
451
 
@@ -387,6 +454,7 @@ export const createSessionRuntime = ({ writeSseEvent, getNotificationClients, br
387
454
  getSessionActivitySnapshot,
388
455
  getActiveSessionCount,
389
456
  getSessionStateSnapshot,
457
+ getPendingBlockingRequestsSnapshot,
390
458
  getSessionAttentionSnapshot,
391
459
  getSessionState,
392
460
  getSessionAttentionState,
@@ -12,6 +12,49 @@ describe('session runtime', () => {
12
12
  runtimes.length = 0;
13
13
  });
14
14
 
15
+ it('keeps pending permission and question requests until they are answered', () => {
16
+ const runtime = createSessionRuntime({
17
+ writeSseEvent() {},
18
+ getNotificationClients: () => new Set(),
19
+ broadcastEvent: () => {},
20
+ });
21
+ runtimes.push(runtime);
22
+ const permission = { id: 'perm-1', sessionID: 'session-1', permission: 'bash', patterns: ['rm *'], metadata: {}, always: [] };
23
+ const question = { id: 'q-1', sessionID: 'session-2', questions: [{ header: 'Pick', question: 'Which?', options: [] }] };
24
+
25
+ runtime.processOpenCodeSsePayload({ type: 'permission.asked', properties: permission });
26
+ runtime.processOpenCodeSsePayload({ type: 'permission.asked', properties: permission });
27
+ runtime.processOpenCodeSsePayload({ type: 'question.asked', properties: question });
28
+ expect(runtime.getPendingBlockingRequestsSnapshot()).toEqual({
29
+ 'session-1': { permissions: [permission], questions: [] },
30
+ 'session-2': { permissions: [], questions: [question] },
31
+ });
32
+
33
+ runtime.processOpenCodeSsePayload({ type: 'permission.replied', properties: { sessionID: 'session-1', requestID: 'perm-1' } });
34
+ runtime.processOpenCodeSsePayload({ type: 'question.rejected', properties: { sessionID: 'session-2' } });
35
+ expect(runtime.getPendingBlockingRequestsSnapshot()).toEqual({});
36
+ });
37
+
38
+ it('drops pending requests when the session is deleted or OpenCode restarts', () => {
39
+ const runtime = createSessionRuntime({
40
+ writeSseEvent() {},
41
+ getNotificationClients: () => new Set(),
42
+ broadcastEvent: () => {},
43
+ });
44
+ runtimes.push(runtime);
45
+ const ask = (sessionID, id) => runtime.processOpenCodeSsePayload({
46
+ type: 'permission.asked', properties: { id, sessionID, permission: 'edit', patterns: [], metadata: {}, always: [] },
47
+ });
48
+
49
+ ask('session-1', 'perm-1');
50
+ ask('session-2', 'perm-2');
51
+ runtime.processOpenCodeSsePayload({ type: 'session.deleted', properties: { info: { id: 'session-1' } } });
52
+ expect(Object.keys(runtime.getPendingBlockingRequestsSnapshot())).toEqual(['session-2']);
53
+
54
+ runtime.interruptBusySessionsAfterRestart();
55
+ expect(runtime.getPendingBlockingRequestsSnapshot()).toEqual({});
56
+ });
57
+
15
58
  it('broadcasts attention clears through the shared broadcaster', () => {
16
59
  const events = [];
17
60
  const runtime = createSessionRuntime({