@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
@@ -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
+
@@ -11,6 +11,7 @@ import {
11
11
  import { createRealpathCache } from '../path-realpath-cache.js';
12
12
  import { DEFAULT_UPSTREAM_STALL_TIMEOUT_MS } from '../event-stream/upstream-reader.js';
13
13
  import { recordStartupPerformance } from './startup-performance.js';
14
+ import { getWorktreeBootstrapStatus } from '../git/service.js';
14
15
 
15
16
  const DEFAULT_SSE_HEARTBEAT_INTERVAL_MS = 20_000;
16
17
 
@@ -286,6 +287,8 @@ export const registerOpenCodeProxy = (app, deps) => {
286
287
  SSE_HEARTBEAT_INTERVAL_MS = DEFAULT_SSE_HEARTBEAT_INTERVAL_MS,
287
288
  SSE_UPSTREAM_STALL_TIMEOUT_MS = DEFAULT_UPSTREAM_STALL_TIMEOUT_MS,
288
289
  getSseUpstreamStallTimeoutMs = () => SSE_UPSTREAM_STALL_TIMEOUT_MS,
290
+ readWorktreeBootstrapStatus = getWorktreeBootstrapStatus,
291
+ WORKTREE_READY_TIMEOUT_MS = 5 * 60 * 1000,
289
292
  } = deps;
290
293
 
291
294
  if (app.get('opencodeProxyConfigured')) {
@@ -775,6 +778,36 @@ export const registerOpenCodeProxy = (app, deps) => {
775
778
  }
776
779
  });
777
780
 
781
+ // Any directory-scoped read can initialize OpenCode's cached project/config,
782
+ // before session.create runs. Hold all upstream requests until Git population
783
+ // finishes, independently of the user's optional setup-script wait.
784
+ app.use('/api', async (req, res, next) => {
785
+ normalizeForwardedDirectoryHeaders(req.headers);
786
+ const url = new URL(req.url, 'http://localhost');
787
+ const directory = url.searchParams.get('directory') || req.get('x-opencode-directory');
788
+ if (!directory) return next();
789
+
790
+ const deadline = Date.now() + WORKTREE_READY_TIMEOUT_MS;
791
+ try {
792
+ while (!res.destroyed && !res.writableEnded && !req.aborted) {
793
+ const status = await readWorktreeBootstrapStatus(directory);
794
+ if (res.destroyed || res.writableEnded || req.aborted) return;
795
+ if (status.status === 'failed') {
796
+ return res.status(503).json({ error: status.error || 'Worktree bootstrap failed' });
797
+ }
798
+ if (status.status === 'ready' || status.phase === 'git-ready' || status.phase === 'setup-ready') {
799
+ return next();
800
+ }
801
+ if (Date.now() >= deadline) {
802
+ return res.status(503).json({ error: 'Timed out waiting for worktree checkout' });
803
+ }
804
+ await sleep(75);
805
+ }
806
+ } catch (error) {
807
+ next(error);
808
+ }
809
+ });
810
+
778
811
  // Windows: session merge for cross-directory session listing
779
812
  if (process.platform === 'win32') {
780
813
  app.get('/api/session', async (req, res, next) => {
@@ -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({
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
 
3
3
  import { isAgentMemoryFeatureAvailable } from '../agent-memory/feature-flag.js';
4
+ import { isRoutingFeatureAvailable } from '../routing/feature-flag.js';
4
5
 
5
6
  // Generated from packages/ui/src/lib/settings/registry.ts by
6
7
  // `bun run settings-registry:generate`; `registry.test.ts` fails when stale.
@@ -233,6 +234,11 @@ export const createSettingsHelpers = (dependencies) => {
233
234
  if (typeof candidate.workStatusHiddenSectionsExplicit === 'boolean') {
234
235
  result.workStatusHiddenSectionsExplicit = candidate.workStatusHiddenSectionsExplicit;
235
236
  }
237
+ if (Array.isArray(candidate.workStatusSectionOrder)) {
238
+ result.workStatusSectionOrder = [
239
+ ...new Set(candidate.workStatusSectionOrder.filter((entry) => typeof entry === 'string' && entry.length > 0)),
240
+ ];
241
+ }
236
242
  if (typeof candidate.desktopLanAccessEnabled === 'boolean') {
237
243
  result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled;
238
244
  }
@@ -473,6 +479,9 @@ export const createSettingsHelpers = (dependencies) => {
473
479
  if (candidate.sessionRetentionAction === 'archive' || candidate.sessionRetentionAction === 'delete') {
474
480
  result.sessionRetentionAction = candidate.sessionRetentionAction;
475
481
  }
482
+ if (typeof candidate.sessionRetentionOnlyArchived === 'boolean') {
483
+ result.sessionRetentionOnlyArchived = candidate.sessionRetentionOnlyArchived;
484
+ }
476
485
  if (candidate.tunnelBootstrapTtlMs === null) {
477
486
  result.tunnelBootstrapTtlMs = null;
478
487
  } else if (typeof candidate.tunnelBootstrapTtlMs === 'number' && Number.isFinite(candidate.tunnelBootstrapTtlMs)) {
@@ -1005,6 +1014,8 @@ export const createSettingsHelpers = (dependencies) => {
1005
1014
  // Tells the client whether agent memory exists in this build at all, so
1006
1015
  // its settings row and panel tab can be absent rather than merely off.
1007
1016
  agentMemoryFeatureAvailable: isAgentMemoryFeatureAvailable(),
1017
+ // Same idea for Jev routing: absent from the picker and Settings unless the build has it.
1018
+ routingFeatureAvailable: isRoutingFeatureAvailable(),
1008
1019
  ...(pwaAppName ? { pwaAppName } : {}),
1009
1020
  pwaOrientation,
1010
1021
  mobileKeyboardMode,
@@ -69,6 +69,18 @@ const createTestHelpersWithRealSanitizers = () => {
69
69
  };
70
70
 
71
71
  describe('settings helpers', () => {
72
+ it('round-trips section order and preserves it across unrelated writes', () => {
73
+ const helpers = createTestHelpers();
74
+ const changes = helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: ['mcp', 'session', 'mcp', null, ''] });
75
+ expect(changes.workStatusSectionOrder).toEqual(['mcp', 'session']);
76
+ const saved = helpers.mergePersistedSettings({}, changes);
77
+ const reloaded = helpers.formatSettingsResponse(JSON.parse(JSON.stringify(saved)));
78
+ expect(reloaded.workStatusSectionOrder).toEqual(['mcp', 'session']);
79
+ const next = helpers.mergePersistedSettings(reloaded, helpers.sanitizeSettingsUpdate({ workStatusPanelEnabled: false }));
80
+ expect(helpers.formatSettingsResponse(next).workStatusSectionOrder).toEqual(['mcp', 'session']);
81
+ expect(helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: 'bad' }).workStatusSectionOrder).toBeUndefined();
82
+ expect(helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: [] }).workStatusSectionOrder).toEqual([]);
83
+ });
72
84
  it('round-trips telemetry opt-in with the hidden list and preserves it across unrelated writes', () => {
73
85
  const helpers = createTestHelpers();
74
86
  const legacy = helpers.sanitizeSettingsUpdate({ workStatusHiddenSections: [] });
@@ -690,6 +702,14 @@ describe('settings helpers', () => {
690
702
  });
691
703
 
692
704
  describe('session retention settings persistence', () => {
705
+ it('round-trips archived-only retention and rejects non-boolean values', () => {
706
+ const helpers = createTestHelpersWithRealSanitizers();
707
+ for (const sessionRetentionOnlyArchived of [true, false]) {
708
+ expect(helpers.sanitizeSettingsUpdate({ sessionRetentionOnlyArchived })).toEqual({ sessionRetentionOnlyArchived });
709
+ }
710
+ expect(helpers.sanitizeSettingsUpdate({ sessionRetentionOnlyArchived: 'true' })).toEqual({});
711
+ expect(helpers.sanitizeSettingsUpdate({ sessionRetentionOnlyArchived: null })).toEqual({});
712
+ });
693
713
  it('round-trips sessionRetentionAction archive and delete through the sanitizer', () => {
694
714
  const helpers = createTestHelpersWithRealSanitizers();
695
715
 
@@ -714,6 +734,7 @@ describe('settings helpers', () => {
714
734
  autoDeleteEnabled: true,
715
735
  autoDeleteAfterDays: 60,
716
736
  sessionRetentionAction: 'delete',
737
+ sessionRetentionOnlyArchived: true,
717
738
  };
718
739
 
719
740
  const sanitized = helpers.sanitizeSettingsUpdate(payload);
@@ -721,6 +742,7 @@ describe('settings helpers', () => {
721
742
  expect(sanitized.autoDeleteEnabled).toBe(true);
722
743
  expect(sanitized.autoDeleteAfterDays).toBe(60);
723
744
  expect(sanitized.sessionRetentionAction).toBe('delete');
745
+ expect(sanitized.sessionRetentionOnlyArchived).toBe(true);
724
746
  });
725
747
  });
726
748
  });
@@ -746,13 +768,13 @@ describe('settings registry gate', () => {
746
768
  desktopUiPassword: 'secret', githubClientId: 'client', githubScopes: 'repo', skillCatalogs: [{ id: 'c', label: 'C', source: 'https://x' }],
747
769
  defaultGitIdentityId: 'global', permissionAutoAccept: { sessions: { s: true }, revision: 1 },
748
770
  agentControlToolEnabled: true, agentWebToolEnabled: true, agentMemoryToolEnabled: true, openCodeUpdateToastDismissedVersion: '1.0.0',
749
- autoDeleteEnabled: true, autoDeleteAfterDays: 30, sessionRetentionAction: 'archive', terminalShell: 'zsh', terminalLoginShells: ['zsh'],
771
+ autoDeleteEnabled: true, autoDeleteAfterDays: 30, sessionRetentionOnlyArchived: false, sessionRetentionAction: 'archive', terminalShell: 'zsh', terminalLoginShells: ['zsh'],
750
772
  openInAppId: 'vscode', dictationEnabled: true, sttProvider: 'local', sttServerUrl: 'http://localhost:8001/v1', sttModel: 'm', sttLocalModel: 'm', sttLanguage: 'en',
751
773
  tunnelProvider: 'cloudflare', tunnelMode: 'quick', tunnelBootstrapTtlMs: 600000, tunnelSessionTtlMs: 86400000, managedLocalTunnelConfigPath: '/tmp/x',
752
774
  managedRemoteTunnelHostname: 'x.example', managedRemoteTunnelToken: 'token', managedRemoteTunnelPresets: [{ id: 'a', name: 'A', hostname: 'a.example' }],
753
775
  managedRemoteTunnelSelectedPresetId: 'a', managedRemoteTunnelPresetTokens: { a: 'token' },
754
776
  sidebarProjectDisplayMode: 'all', sidebarSessionGroupingMode: 'flat', sidebarProjectSortOrder: 'manual', sidebarShowRecentSection: true,
755
- workStatusPanelEnabled: true, workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true,
777
+ workStatusPanelEnabled: true, workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true, workStatusSectionOrder: ['mcp', 'session'],
756
778
  showReasoningTraces: true, streamingAutoFollowEnabled: true, collapsibleThinkingBlocks: true, showTextJustificationActivity: true,
757
779
  chatRenderMode: 'live', activityRenderMode: 'summary', mermaidRenderingMode: 'svg', userMessageRenderingMode: 'markdown', collapsibleUserMessages: true,
758
780
  stickyUserHeader: true, promptNavigatorEnabled: true, wideChatLayoutEnabled: true, showSplitAssistantMessageActions: true, showToolFileIcons: true,
@@ -154,6 +154,7 @@ export const createSettingsNormalizationRuntime = (dependencies) => {
154
154
  : null;
155
155
  const iconBackground = normalizeIconBackground(candidate.iconBackground);
156
156
  const color = typeof candidate.color === 'string' ? candidate.color.trim() : '';
157
+ const defaultAgent = typeof candidate.defaultAgent === 'string' ? candidate.defaultAgent.trim() : '';
157
158
  const defaultModel = typeof candidate.defaultModel === 'string' ? candidate.defaultModel.trim() : '';
158
159
  const defaultVariant = typeof candidate.defaultVariant === 'string' ? candidate.defaultVariant.trim() : '';
159
160
  const addedAt = Number.isFinite(candidate.addedAt) ? Number(candidate.addedAt) : null;
@@ -175,6 +176,7 @@ export const createSettingsNormalizationRuntime = (dependencies) => {
175
176
  ...(icon ? { icon } : {}),
176
177
  ...(iconBackground ? { iconBackground } : {}),
177
178
  ...(color ? { color } : {}),
179
+ ...(defaultAgent ? { defaultAgent } : {}),
178
180
  ...(defaultModel && defaultModel.includes('/') ? { defaultModel } : {}),
179
181
  // A variant is meaningless without the model it belongs to.
180
182
  ...(defaultModel && defaultModel.includes('/') && defaultVariant ? { defaultVariant } : {}),
@@ -122,6 +122,19 @@ describe('settings normalization runtime - symlink resolution', () => {
122
122
  expect(result[1].defaultVariant).toBe(undefined);
123
123
  });
124
124
 
125
+ it('keeps a project default agent', () => {
126
+ const runtime = createTestRuntime({
127
+ realpathSync: (p) => p,
128
+ path: { resolve: (p) => p, sep: '/', dirname: (p) => p.split('/').slice(0, -1).join('/') || '/' },
129
+ });
130
+
131
+ const result = runtime.sanitizeProjects([
132
+ { id: 'proj1', path: '/a', defaultAgent: 'plan' },
133
+ ]);
134
+
135
+ expect(result[0].defaultAgent).toBe('plan');
136
+ });
137
+
125
138
  it('deduplicates projects that resolve to the same realpath', () => {
126
139
  const runtime = createTestRuntime({
127
140
  realpathSync: (p) => p.startsWith('/symlink') ? '/real/project' : p,
@@ -127,6 +127,10 @@
127
127
  "scope": "instance",
128
128
  "computed": true
129
129
  },
130
+ "routingFeatureAvailable": {
131
+ "scope": "instance",
132
+ "computed": true
133
+ },
130
134
  "openCodeUpdateToastDismissedVersion": {
131
135
  "scope": "instance"
132
136
  },
@@ -136,6 +140,9 @@
136
140
  "autoDeleteAfterDays": {
137
141
  "scope": "instance"
138
142
  },
143
+ "sessionRetentionOnlyArchived": {
144
+ "scope": "instance"
145
+ },
139
146
  "sessionRetentionAction": {
140
147
  "scope": "instance"
141
148
  },
@@ -214,6 +221,9 @@
214
221
  "sidebarShowRecentSection": {
215
222
  "scope": "profile"
216
223
  },
224
+ "workStatusSectionOrder": {
225
+ "scope": "profile"
226
+ },
217
227
  "workStatusPanelEnabled": {
218
228
  "scope": "profile"
219
229
  },
@@ -602,6 +612,10 @@
602
612
  "scope": "device",
603
613
  "local": true
604
614
  },
615
+ "contextEditorVisible": {
616
+ "scope": "device",
617
+ "local": true
618
+ },
605
619
  "contextEditorTreeWidth": {
606
620
  "scope": "device",
607
621
  "local": true
@@ -614,6 +628,10 @@
614
628
  "scope": "device",
615
629
  "local": true
616
630
  },
631
+ "messageQueueExpanded": {
632
+ "scope": "device",
633
+ "local": true
634
+ },
617
635
  "workStatusScrollTop": {
618
636
  "scope": "device",
619
637
  "local": true