@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.30

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 (292) hide show
  1. package/README.md +14 -0
  2. package/dist/assets/{AgentAvatar-B_JQoreg.js → AgentAvatar-O2x_1cFU.js} +1 -1
  3. package/dist/assets/AssistantView-pBywAsqy.js +3 -0
  4. package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-B984aE-R.js} +1 -1
  5. package/dist/assets/{FadeInOnReveal-VYQTSAkr.js → FadeInOnReveal-CMBd5BTd.js} +1 -1
  6. package/dist/assets/Icon-BN87CyS4.js +1 -0
  7. package/dist/assets/MarkdownRenderer-DHiNU8n3.js +2 -0
  8. package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-xJHMGHju.js} +3 -3
  9. package/dist/assets/MarkstreamRendererImpl-BZflns6x.js +3 -0
  10. package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DUvxsvrB.js} +1 -1
  11. package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-DWo-J3Vc.js} +3 -3
  12. package/dist/assets/MobileSurface-B-Y6U5xm.js +2 -0
  13. package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-rYiVSNKK.js} +1 -1
  14. package/dist/assets/{OnboardingScreen-BjHmlwlw.js → OnboardingScreen-BLjZIBOs.js} +1 -1
  15. package/dist/assets/{OverlayScrollbar-DLtWO_Gp.js → OverlayScrollbar-CoZ4wtau.js} +1 -1
  16. package/dist/assets/SettingsView-pjD0LrHE.js +25 -0
  17. package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CFOQRwjA.js} +1 -1
  18. package/dist/assets/TerminalView-Dw5jaYkc.js +8 -0
  19. package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-B4v-ona2.js} +1 -1
  20. package/dist/assets/appThemeRegistry-BqDCGocy.js +1 -0
  21. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-sG8YQsVs.js} +1 -1
  22. package/dist/assets/{button-DKuz3L8l.js → button-Ck8ipzBm.js} +1 -1
  23. package/dist/assets/collapsible-nU99prkD.js +1 -0
  24. package/dist/assets/context-menu-DMzZN1oi.js +1 -0
  25. package/dist/assets/{desktopHosts-CoDkOnyS.js → desktopHosts-DuftEj8B.js} +1 -1
  26. package/dist/assets/desktopRelayRestore-g378BdZf.js +1 -0
  27. package/dist/assets/{diagram-D7T8HQgj.js → diagram-CvO4P8ds.js} +1 -1
  28. package/dist/assets/dialog-ByGUkS3c.js +1 -0
  29. package/dist/assets/dropdown-menu.styles-ayLWk2gf.js +1 -0
  30. package/dist/assets/es-DCjl24dV.js +15 -0
  31. package/dist/assets/fileMentionTouchSelection-Cgkxwzqb.js +2 -0
  32. package/dist/assets/{fileQueries-JrpcsOCs.js → fileQueries-DuF89nFO.js} +1 -1
  33. package/dist/assets/fr-DfJ5j0t5.js +13 -0
  34. package/dist/assets/{gitApi-CM42qUed.js → gitApi-Dwqkc2nZ.js} +3 -3
  35. package/dist/assets/{imageSource-CIl0FL10.js → imageSource-BpUsiwFd.js} +1 -1
  36. package/dist/assets/{input-CzQy8UhV.js → input-CXhFmHcq.js} +1 -1
  37. package/dist/assets/insertionBoundaries-BhWfAOPY.js +1 -0
  38. package/dist/assets/ja-B_9_w2Xf.js +15 -0
  39. package/dist/assets/ko-DlGbo3kV.js +15 -0
  40. package/dist/assets/main-CGMinhgQ.js +169 -0
  41. package/dist/assets/main-pMpcLJha.js +2 -0
  42. package/dist/assets/{markdownHeightCache-Cj1I5NDw.js → markdownHeightCache-g_UsWzbA.js} +1 -1
  43. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  44. package/dist/assets/{mcpQueries-D5mQymgM.js → mcpQueries-DfrW3VHF.js} +1 -1
  45. package/dist/assets/miniChat-YOt30ECc.js +2 -0
  46. package/dist/assets/mobile-BK1bBCYD.js +2 -0
  47. package/dist/assets/multirun-Dua24A69.js +3 -0
  48. package/dist/assets/persistence-Dnj1QOdX.js +3 -0
  49. package/dist/assets/pl-DjUsS8Ei.js +15 -0
  50. package/dist/assets/platform-X3Y3GyM5.js +6 -0
  51. package/dist/assets/projectMeta-DXdoPmbz.js +1 -0
  52. package/dist/assets/pt-BR-B8sV7qo1.js +15 -0
  53. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-D2XooXKZ.js} +1 -1
  54. package/dist/assets/renderElectronMiniChatApp-BOuEz_oT.js +2 -0
  55. package/dist/assets/renderMobileApp-C8Alpv-B.js +28 -0
  56. package/dist/assets/runtime-fetch-CiqQd__Y.js +1 -0
  57. package/dist/assets/runtime-switch-BJM2k_uH.js +1 -0
  58. package/dist/assets/runtimeConfig-DX_NP2Zm.js +2 -0
  59. package/dist/assets/runtimeEndpointReset-BSQU3e5U.js +148 -0
  60. package/dist/assets/runtimeEndpointReset-DsuP5FPT.css +1 -0
  61. package/dist/assets/sessionChangeSummary-8AIBkExf.js +2 -0
  62. package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-Bdwd1Fit.js} +1 -1
  63. package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-T-dk_L69.js} +1 -1
  64. package/dist/assets/src-OzbnJsph.css +2 -0
  65. package/dist/assets/store-Cuktref-.js +15 -0
  66. package/dist/assets/{terminalApi-BxcdiduX.js → terminalApi-C_4joKUG.js} +1 -1
  67. package/dist/assets/textarea-biAqiqnJ.js +1 -0
  68. package/dist/assets/{tooltip-Cx5gTbdY.js → tooltip-BKL7J5hx.js} +1 -1
  69. package/dist/assets/uk-Bh7HXyLp.js +15 -0
  70. package/dist/assets/{url-CDHnjlsm.js → url-DNRMq_0R.js} +1 -1
  71. package/dist/assets/{useAppFontEffects-CNeo0aZs.js → useAppFontEffects-bafOEpgK.js} +57 -52
  72. package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
  73. package/dist/assets/useDesktopWindowControlsLayout-CkCVOkxa.js +1 -0
  74. package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-B7htpJfU.js} +1 -1
  75. package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-CMrbTA0r.js} +1 -1
  76. package/dist/assets/useMobileNavigationStore-D2tnhKYV.js +415 -0
  77. package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
  78. package/dist/assets/useSessionAutoCleanup-BC5OOH3M.js +2 -0
  79. package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
  80. package/dist/assets/useUIStore-Dfx6TKvi.js +1 -0
  81. package/dist/assets/{useWorkerHighlightedLines-CJZbJDW3.js → useWorkerHighlightedLines-BNQUAMEB.js} +1 -1
  82. package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
  83. package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
  84. package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
  85. package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
  86. package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
  87. package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
  88. package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
  89. package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
  90. package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
  91. package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
  92. package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
  93. package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
  94. package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
  95. package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
  96. package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
  97. package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
  98. package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
  99. package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-DzRqcymP.js} +2 -2
  100. package/dist/assets/zh-CN-Uzia0gk4.js +15 -0
  101. package/dist/assets/zh-TW-CiI7DLdB.js +15 -0
  102. package/dist/index.html +15 -15
  103. package/dist/mini-chat.html +15 -15
  104. package/dist/mobile.html +15 -15
  105. package/package.json +3 -2
  106. package/server/index.js +46 -27
  107. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  108. package/server/lib/browser-provider/contract.js +312 -0
  109. package/server/lib/browser-provider/host.js +421 -0
  110. package/server/lib/browser-provider/host.test.js +304 -0
  111. package/server/lib/browser-provider/routes.js +122 -0
  112. package/server/lib/browser-provider/routes.test.js +120 -0
  113. package/server/lib/browser-provider/selection-store.js +43 -0
  114. package/server/lib/browser-provider/selection-store.test.js +35 -0
  115. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  116. package/server/lib/event-stream/reasoning-projection.js +16 -1
  117. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  118. package/server/lib/git/DOCUMENTATION.md +1 -0
  119. package/server/lib/git/routes.js +7 -4
  120. package/server/lib/git/routes.test.js +34 -0
  121. package/server/lib/git/service.js +3 -1
  122. package/server/lib/llm/DOCUMENTATION.md +6 -4
  123. package/server/lib/llm/generate.js +8 -9
  124. package/server/lib/llm/generate.test.js +33 -6
  125. package/server/lib/message-queue/service.js +1 -1
  126. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  127. package/server/lib/notifications/runtime.js +128 -31
  128. package/server/lib/notifications/runtime.test.js +103 -0
  129. package/server/lib/notifications/template-runtime.js +130 -38
  130. package/server/lib/notifications/template-runtime.test.js +96 -0
  131. package/server/lib/opencode/DOCUMENTATION.md +36 -19
  132. package/server/lib/opencode/agents.js +13 -22
  133. package/server/lib/opencode/auth-state-runtime.js +3 -3
  134. package/server/lib/opencode/auth-state-runtime.test.js +59 -0
  135. package/server/lib/opencode/auth.d.ts +13 -0
  136. package/server/lib/opencode/auth.js +74 -64
  137. package/server/lib/opencode/auth.test.js +57 -0
  138. package/server/lib/opencode/commands.js +1 -1
  139. package/server/lib/opencode/config-entities-layering.test.js +72 -0
  140. package/server/lib/opencode/config-entity-routes.js +16 -124
  141. package/server/lib/opencode/config-entity-routes.test.js +48 -11
  142. package/server/lib/opencode/config-files.d.ts +17 -0
  143. package/server/lib/opencode/config-files.js +187 -0
  144. package/server/lib/opencode/config-files.test.js +201 -0
  145. package/server/lib/opencode/core-routes.js +0 -1
  146. package/server/lib/opencode/core-routes.test.js +0 -15
  147. package/server/lib/opencode/credential-boundary.d.ts +1 -0
  148. package/server/lib/opencode/credential-boundary.js +11 -0
  149. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  150. package/server/lib/opencode/ensure-cli.js +4 -1
  151. package/server/lib/opencode/env-config.js +4 -1
  152. package/server/lib/opencode/env-config.test.js +28 -0
  153. package/server/lib/opencode/env-runtime.test.js +18 -1
  154. package/server/lib/opencode/feature-routes-runtime.js +19 -7
  155. package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
  156. package/server/lib/opencode/hmr-state-runtime.js +4 -4
  157. package/server/lib/opencode/lifecycle.js +1 -49
  158. package/server/lib/opencode/lifecycle.test.js +30 -4
  159. package/server/lib/opencode/location-release.d.ts +8 -0
  160. package/server/lib/opencode/location-release.js +32 -0
  161. package/server/lib/opencode/location-release.test.js +43 -0
  162. package/server/lib/opencode/mcp.js +1 -1
  163. package/server/lib/opencode/opencode2-pin.js +2 -1
  164. package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
  165. package/server/lib/opencode/plugin-routes.test.js +4 -2
  166. package/server/lib/opencode/plugins.js +12 -60
  167. package/server/lib/opencode/plugins.test.js +5 -3
  168. package/server/lib/opencode/providers.js +6 -3
  169. package/server/lib/opencode/proxy.js +13 -1
  170. package/server/lib/opencode/proxy.test.js +31 -3
  171. package/server/lib/opencode/remote-upgrade.integration.test.js +66 -0
  172. package/server/lib/opencode/routes.js +40 -8
  173. package/server/lib/opencode/routes.test.js +51 -10
  174. package/server/lib/opencode/runtime-contract.js +3 -2
  175. package/server/lib/opencode/runtime-contract.test.js +12 -12
  176. package/server/lib/opencode/shared.js +6 -175
  177. package/server/lib/opencode/update-discovery.js +39 -0
  178. package/server/lib/opencode/update-discovery.test.js +39 -0
  179. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  180. package/server/lib/opencode/upgrade-screen.js +292 -0
  181. package/server/lib/opencode/upgrade-screen.test.js +196 -0
  182. package/server/lib/opencode/version-alignment.test.js +40 -0
  183. package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
  184. package/server/lib/question-auto-delegate/core.d.ts +5 -1
  185. package/server/lib/question-auto-delegate/core.js +21 -13
  186. package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
  187. package/server/lib/question-auto-delegate/form-io.js +39 -0
  188. package/server/lib/question-auto-delegate/runtime.js +4 -24
  189. package/server/lib/question-auto-delegate/runtime.test.js +138 -0
  190. package/server/lib/quota/DOCUMENTATION.md +9 -0
  191. package/server/lib/quota/provider-credentials.test.js +29 -0
  192. package/server/lib/quota/providers/claude.js +4 -4
  193. package/server/lib/quota/providers/codex.js +4 -4
  194. package/server/lib/quota/providers/codex.test.js +1 -1
  195. package/server/lib/quota/providers/copilot.js +5 -5
  196. package/server/lib/quota/providers/google/auth.js +3 -3
  197. package/server/lib/quota/providers/google/index.js +2 -2
  198. package/server/lib/quota/providers/index.js +3 -7
  199. package/server/lib/quota/providers/index.test.js +2 -2
  200. package/server/lib/quota/providers/kimi.js +4 -4
  201. package/server/lib/quota/providers/minimax-shared.js +4 -4
  202. package/server/lib/quota/providers/nanogpt.js +4 -4
  203. package/server/lib/quota/providers/openai.js +4 -4
  204. package/server/lib/quota/providers/openrouter.js +4 -4
  205. package/server/lib/quota/providers/wafer.js +4 -4
  206. package/server/lib/quota/providers/zai.js +4 -4
  207. package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
  208. package/server/lib/quota/routes.js +1 -1
  209. package/server/lib/relay/DOCUMENTATION.md +3 -1
  210. package/server/lib/relay/service.js +1 -0
  211. package/server/lib/relay/service.test.js +13 -0
  212. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -1
  213. package/server/lib/scheduled-tasks/runtime.js +12 -4
  214. package/server/lib/scheduled-tasks/runtime.test.js +81 -6
  215. package/server/lib/session-goal/DOCUMENTATION.md +11 -0
  216. package/server/lib/session-goal/runtime.js +4 -4
  217. package/server/lib/session-goal/runtime.test.js +32 -3
  218. package/server/lib/session-index/DOCUMENTATION.md +7 -0
  219. package/server/lib/session-index/event-ingest.js +13 -0
  220. package/server/lib/session-index/event-ingest.test.js +7 -0
  221. package/server/lib/session-index/service.js +45 -0
  222. package/server/lib/session-index/service.test.js +47 -0
  223. package/server/lib/session-metadata/DOCUMENTATION.md +33 -17
  224. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  225. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  226. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  227. package/server/lib/session-metadata/routes.js +5 -1
  228. package/server/lib/session-metadata/session-archive.js +1 -1
  229. package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
  230. package/server/lib/session-metadata/session-metadata-store.js +63 -16
  231. package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
  232. package/server/lib/session-metadata/session-projection.d.ts +1 -0
  233. package/server/lib/session-metadata/session-projection.js +49 -2
  234. package/server/lib/session-metadata/session-projection.test.js +35 -0
  235. package/server/lib/session-title/DOCUMENTATION.md +52 -14
  236. package/server/lib/session-title/native-runtime.test.js +59 -5
  237. package/server/lib/session-title/runtime.js +65 -19
  238. package/server/lib/session-title/session-access.js +108 -3
  239. package/server/lib/session-title/session-access.test.js +125 -7
  240. package/server/lib/tts/DOCUMENTATION.md +7 -5
  241. package/server/lib/tts/credential-source.test.js +23 -0
  242. package/server/lib/tts/routes.js +2 -2
  243. package/server/lib/tts/service.js +16 -37
  244. package/dist/assets/AssistantView-ClQAfGOT.js +0 -3
  245. package/dist/assets/Icon-BiEQEcih.js +0 -1
  246. package/dist/assets/MarkdownRenderer-CJHSsWin.js +0 -2
  247. package/dist/assets/MarkstreamRendererImpl-CSnbQMu3.js +0 -3
  248. package/dist/assets/MobileSurface-D2jUkX9l.js +0 -2
  249. package/dist/assets/SettingsView-BRxELjow.js +0 -25
  250. package/dist/assets/TerminalView-BU3x8-fr.js +0 -8
  251. package/dist/assets/appThemeRegistry-4MR0F7Jz.js +0 -1
  252. package/dist/assets/collapsible-DXbDynN4.js +0 -3
  253. package/dist/assets/context-menu-CqolsLCm.js +0 -1
  254. package/dist/assets/desktopRelayRestore-DDlVKyOE.js +0 -1
  255. package/dist/assets/dialog-DQYl3W_o.js +0 -1
  256. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +0 -1
  257. package/dist/assets/es-CN3E_zXj.js +0 -15
  258. package/dist/assets/fileMentionTouchSelection-BZ6ovBYH.js +0 -4
  259. package/dist/assets/fr-9zZvQZD2.js +0 -13
  260. package/dist/assets/insertionBoundaries-CtkZ9720.js +0 -1
  261. package/dist/assets/ja-Dnjk_82N.js +0 -15
  262. package/dist/assets/ko-BxCEE99C.js +0 -15
  263. package/dist/assets/main-BILgBI3B.js +0 -169
  264. package/dist/assets/main-BeveiW6F.js +0 -2
  265. package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
  266. package/dist/assets/miniChat-BgrzM3u7.js +0 -2
  267. package/dist/assets/mobile-DtYoQE8C.js +0 -2
  268. package/dist/assets/multirun-jy8dEI96.js +0 -4
  269. package/dist/assets/persistence-C9LQpiOS.js +0 -3
  270. package/dist/assets/pl-CNNsWnPM.js +0 -15
  271. package/dist/assets/platform-BQghHRfp.js +0 -6
  272. package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
  273. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +0 -2
  274. package/dist/assets/renderMobileApp-u3c4GtFC.js +0 -28
  275. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  276. package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
  277. package/dist/assets/runtimeConfig-BIGBRGQI.js +0 -2
  278. package/dist/assets/runtimeEndpointReset-D2savkuw.js +0 -147
  279. package/dist/assets/sessionChangeSummary-eIbpSfrO.js +0 -2
  280. package/dist/assets/src-CLN7Z-zx.css +0 -2
  281. package/dist/assets/store-DLCiZaA6.js +0 -15
  282. package/dist/assets/textarea-CakKvqFw.js +0 -1
  283. package/dist/assets/uk-rwQrxeYM.js +0 -15
  284. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +0 -1
  285. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +0 -414
  286. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +0 -2
  287. package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
  288. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
  289. package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
  290. package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
  291. package/server/lib/opencode/snippets.js +0 -242
  292. package/server/lib/opencode/snippets.test.js +0 -97
@@ -0,0 +1,201 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { readConfigFile, readConfigLayers, getConfigForPath, getJsonEntrySource, getJsonWriteTarget, writeConfig, writeConfigText } from './config-files.js';
6
+
7
+ let root;
8
+ let globalDir;
9
+ const put = (file, text) => {
10
+ fs.mkdirSync(path.dirname(file), { recursive: true });
11
+ fs.writeFileSync(file, text);
12
+ return file;
13
+ };
14
+ beforeEach(() => {
15
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-config-files-'));
16
+ globalDir = path.join(root, 'xdg', 'opencode');
17
+ vi.stubEnv('XDG_CONFIG_HOME', path.dirname(globalDir));
18
+ vi.stubEnv('OPENCODE_CONFIG', '');
19
+ vi.stubEnv('OPENCODE_CONFIG_DIR', '');
20
+ });
21
+ afterEach(() => {
22
+ vi.restoreAllMocks();
23
+ vi.unstubAllEnvs();
24
+ fs.rmSync(root, { recursive: true, force: true });
25
+ });
26
+
27
+ describe('physical configuration layers', () => {
28
+ it('resolves an explicit global directory at call time', () => {
29
+ const customDirectory = path.join(root, 'custom-directory');
30
+ put(path.join(customDirectory, 'opencode.jsonc'), '{"model":"custom-directory"}');
31
+ vi.stubEnv('OPENCODE_CONFIG_DIR', customDirectory);
32
+ expect(readConfigLayers().mergedConfig).toEqual({ model: 'custom-directory' });
33
+ expect(getJsonWriteTarget(readConfigLayers(), 'user').path).toBe(path.join(customDirectory, 'opencode.jsonc'));
34
+ });
35
+ it('merges both global documents and keeps each write target unmerged', () => {
36
+ const json = put(path.join(globalDir, 'opencode.json'), '{"model":"base","agent":{"base":{"model":"a"},"shared":{"model":"base","description":"kept"}}}');
37
+ const jsonc = put(path.join(globalDir, 'opencode.jsonc'), '{ // override\n "model":"override","agent":{"shared":{"model":"override"}},}');
38
+ put(path.join(globalDir, 'config.json'), '{"model":"legacy-not-loaded-by-v2"}');
39
+ const layers = readConfigLayers();
40
+ expect(layers.mergedConfig).toEqual({ model: 'override', agent: { base: { model: 'a' }, shared: { model: 'override', description: 'kept' } } });
41
+ expect(getJsonEntrySource(layers, 'agent', 'base').path).toBe(json);
42
+ expect(getJsonEntrySource(layers, 'agent', 'shared').path).toBe(jsonc);
43
+ expect(getConfigForPath(layers, jsonc)).toEqual({ model: 'override', agent: { shared: { model: 'override' } } });
44
+ expect(getJsonWriteTarget(layers, 'user').path).toBe(jsonc);
45
+ });
46
+
47
+ it('loads explicit config above global and below discovered project documents', () => {
48
+ put(path.join(globalDir, 'opencode.json'), '{"model":"global","global":true}');
49
+ const custom = put(path.join(root, 'custom.jsonc'), '{"model":"explicit","explicit":true}');
50
+ vi.stubEnv('OPENCODE_CONFIG', custom);
51
+ const workspace = path.join(root, 'workspace');
52
+ const nested = path.join(workspace, 'nested');
53
+ put(path.join(workspace, 'opencode.json'), '{"model":"parent","parent":true}');
54
+ put(path.join(nested, 'opencode.jsonc'), '{"model":"child"}');
55
+ put(path.join(workspace, '.opencode', 'opencode.json'), '{"model":"supplementary"}');
56
+ const layers = readConfigLayers(nested);
57
+ expect(layers.mergedConfig).toEqual({ model: 'supplementary', global: true, explicit: true, parent: true });
58
+ expect(getJsonWriteTarget(layers, 'user').path).toBe(custom);
59
+ expect(getJsonWriteTarget(layers, 'project').path).toBe(path.join(workspace, '.opencode', 'opencode.json'));
60
+ vi.stubEnv('OPENCODE_CONFIG', '');
61
+ expect(readConfigLayers().mergedConfig).toEqual({ model: 'global', global: true });
62
+ });
63
+
64
+ it.each(['opencode.json', 'opencode.jsonc'])('reads a single %s and treats missing, blank and comments-only documents explicitly as empty', (name) => {
65
+ expect(readConfigLayers().mergedConfig).toEqual({});
66
+ const file = put(path.join(globalDir, name), ' \n');
67
+ expect(readConfigLayers().mergedConfig).toEqual({});
68
+ put(file, '// empty config\n/* documented */');
69
+ expect(readConfigLayers().mergedConfig).toEqual({});
70
+ put(file, '{"model":"one"}');
71
+ expect(readConfigLayers().mergedConfig).toEqual({ model: 'one' });
72
+ expect(getJsonWriteTarget(readConfigLayers(), 'user').path).toBe(file);
73
+ });
74
+
75
+ it.each(['{"model":', '{"model":"x", broken}', '[]', 'null', 'plain text'])('rejects malformed/non-object content %s without changing it', (text) => {
76
+ const file = put(path.join(globalDir, 'opencode.jsonc'), text);
77
+ expect(() => readConfigLayers()).toThrow('Invalid JSONC');
78
+ expect(() => writeConfig({ model: 'new' }, file)).toThrow('Invalid JSONC');
79
+ expect(() => writeConfigText('{}', file)).toThrow('Invalid JSONC');
80
+ expect(fs.readFileSync(file, 'utf8')).toBe(text);
81
+ expect(fs.existsSync(`${file}.openchamber.backup`)).toBe(false);
82
+ });
83
+
84
+ it('propagates read failures instead of returning empty success', () => {
85
+ const original = fs.readFileSync;
86
+ vi.spyOn(fs, 'readFileSync').mockImplementation((file, ...args) => {
87
+ if (String(file) === path.join(globalDir, 'opencode.json')) throw Object.assign(new Error('denied'), { code: 'EACCES' });
88
+ return original(file, ...args);
89
+ });
90
+ expect(() => readConfigLayers()).toThrow('denied');
91
+ });
92
+ });
93
+
94
+ describe('structural JSONC writes', () => {
95
+ it.each(['absolute', 'relative'])('preserves a %s dotfiles symlink while updating its target, permissions and backup', (kind) => {
96
+ const original = '{ // dotfiles comment\n "model":"old"\n}';
97
+ const target = put(path.join(root, 'dotfiles', 'config.jsonc'), original);
98
+ fs.chmodSync(target, 0o664);
99
+ fs.mkdirSync(globalDir, { recursive: true });
100
+ const file = path.join(globalDir, 'opencode.jsonc');
101
+ const link = kind === 'relative' ? path.relative(globalDir, target) : target;
102
+ fs.symlinkSync(link, file);
103
+
104
+ writeConfig({ model: 'new' }, file);
105
+ expect(fs.lstatSync(file).isSymbolicLink()).toBe(true);
106
+ expect(fs.readlinkSync(file)).toBe(link);
107
+ expect(readConfigFile(target)).toEqual({ model: 'new' });
108
+ expect(fs.readFileSync(target, 'utf8')).toContain('// dotfiles comment');
109
+ expect(fs.statSync(target).mode & 0o777).toBe(0o664);
110
+ expect(fs.readFileSync(`${file}.openchamber.backup`, 'utf8')).toBe(original);
111
+ expect(fs.existsSync(`${target}.openchamber.backup`)).toBe(false);
112
+
113
+ const structured = fs.readFileSync(target, 'utf8');
114
+ writeConfigText('{"model":"raw"}', file);
115
+ expect(fs.readlinkSync(file)).toBe(link);
116
+ expect(readConfigFile(target)).toEqual({ model: 'raw' });
117
+ expect(fs.statSync(target).mode & 0o777).toBe(0o664);
118
+ expect(fs.readFileSync(`${file}.openchamber.backup`, 'utf8')).toBe(structured);
119
+ });
120
+
121
+ it.each(['writeFileSync', 'chmodSync', 'copyFileSync', 'renameSync'])('preserves a symlink and its original target when %s fails', (method) => {
122
+ const original = '{ // retained\n "model":"old"\n}';
123
+ const target = put(path.join(root, 'dotfiles', 'config.jsonc'), original);
124
+ fs.chmodSync(target, 0o640);
125
+ fs.mkdirSync(globalDir, { recursive: true });
126
+ const file = path.join(globalDir, 'opencode.jsonc');
127
+ const link = path.relative(globalDir, target);
128
+ fs.symlinkSync(link, file);
129
+ vi.spyOn(fs, method).mockImplementation(() => { throw new Error('simulated IO failure'); });
130
+ expect(() => writeConfig({ model: 'new' }, file)).toThrow('simulated IO failure');
131
+ expect(fs.readlinkSync(file)).toBe(link);
132
+ expect(fs.readFileSync(target, 'utf8')).toBe(original);
133
+ expect(fs.statSync(target).mode & 0o777).toBe(0o640);
134
+ for (const dir of [globalDir, path.dirname(target)]) {
135
+ expect(fs.readdirSync(dir).some((name) => name.endsWith('.tmp'))).toBe(false);
136
+ }
137
+ });
138
+
139
+ it.each(['structured', 'raw'])('rejects a dangling symlink during a %s write without replacing it', (kind) => {
140
+ fs.mkdirSync(globalDir, { recursive: true });
141
+ const file = path.join(globalDir, 'opencode.jsonc');
142
+ const link = '../../dotfiles/missing.jsonc';
143
+ fs.symlinkSync(link, file);
144
+ const write = () => kind === 'structured' ? writeConfig({ model: 'new' }, file) : writeConfigText('{}', file);
145
+ expect(write).toThrow(expect.objectContaining({ code: 'ENOENT' }));
146
+ expect(fs.readlinkSync(file)).toBe(link);
147
+ expect(fs.existsSync(path.resolve(globalDir, link))).toBe(false);
148
+ expect(fs.readdirSync(globalDir)).toEqual(['opencode.jsonc']);
149
+ });
150
+
151
+ it('edits nested values, adds fields and removes fields without losing surrounding comments or unrelated bytes', () => {
152
+ const original = '\uFEFF{\r\n\t// model comment\r\n\t"model": "old",\r\n\t"agent": {"review": {/* owned */ "model":"old", "remove":true, /* trailing */}},\r\n\t// unrelated field\r\n\t"unrelated" : [ 1, 2 ], // leave alone\r\n}\r\n';
153
+ const file = put(path.join(globalDir, 'opencode.jsonc'), original);
154
+ const config = readConfigFile(file);
155
+ config.model = 'new';
156
+ config.agent.review.model = 'new';
157
+ delete config.agent.review.remove;
158
+ config.agent.review.description = 'added';
159
+ writeConfig(config, file);
160
+ const saved = fs.readFileSync(file, 'utf8');
161
+ expect(readConfigFile(file)).toEqual(config);
162
+ for (const comment of ['// model comment', '/* owned */', '/* trailing */', '// unrelated field', '// leave alone']) expect(saved).toContain(comment);
163
+ expect(saved).toContain('\t"unrelated" : [ 1, 2 ], // leave alone\r\n');
164
+ expect(saved.startsWith('\uFEFF')).toBe(true);
165
+ expect(fs.readFileSync(`${file}.openchamber.backup`, 'utf8')).toBe(original);
166
+ writeConfig(config, file);
167
+ expect(fs.readFileSync(file, 'utf8')).toBe(saved);
168
+ expect(fs.readFileSync(`${file}.openchamber.backup`, 'utf8')).toBe(original);
169
+ });
170
+
171
+ it.each([
172
+ '{/* before */ "only":1, /* after , */}',
173
+ '{"keep":2, /* before */ "only":1 /* after */}',
174
+ '{/* before */ "only":1, /* after */ "keep":2}',
175
+ ])('removes a property and its comma from %s', (text) => {
176
+ const file = put(path.join(globalDir, 'opencode.jsonc'), text);
177
+ const desired = readConfigFile(file);
178
+ delete desired.only;
179
+ writeConfig(desired, file);
180
+ expect(readConfigFile(file)).toEqual(desired);
181
+ expect(fs.readFileSync(file, 'utf8')).toContain('/* before */');
182
+ expect(fs.readFileSync(file, 'utf8')).toContain('/* after');
183
+ });
184
+
185
+ it.each(['', '// notes without newline', '{"old":1}'])('supports new/empty/comment-only/JSON files (%s)', (text) => {
186
+ const file = path.join(globalDir, 'opencode.json');
187
+ if (text) put(file, text);
188
+ writeConfig({ model: 'new', nested: { enabled: true } }, file);
189
+ expect(readConfigFile(file)).toEqual({ model: 'new', nested: { enabled: true } });
190
+ if (text.startsWith('//')) expect(fs.readFileSync(file, 'utf8')).toContain(text);
191
+ });
192
+
193
+ it.each(['writeFileSync', 'copyFileSync', 'renameSync'])('keeps original content and cleans temporary files when %s fails', (method) => {
194
+ const original = '{ // retained\n "model":"old"\n}';
195
+ const file = put(path.join(globalDir, 'opencode.jsonc'), original);
196
+ vi.spyOn(fs, method).mockImplementation(() => { throw new Error('simulated IO failure'); });
197
+ expect(() => writeConfig({ model: 'new' }, file)).toThrow('simulated IO failure');
198
+ expect(fs.readFileSync(file, 'utf8')).toBe(original);
199
+ expect(fs.readdirSync(globalDir).some((name) => name.endsWith('.tmp'))).toBe(false);
200
+ });
201
+ });
@@ -1179,7 +1179,6 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
1179
1179
  req.path.startsWith('/api/config/commands') ||
1180
1180
  req.path.startsWith('/api/config/global') ||
1181
1181
  req.path.startsWith('/api/config/mcp') ||
1182
- req.path.startsWith('/api/config/snippets') ||
1183
1182
  req.path.startsWith('/api/config/settings') ||
1184
1183
  req.path.startsWith('/api/config/skills') ||
1185
1184
  req.path.startsWith('/api/config/plugins') ||
@@ -310,21 +310,6 @@ describe('core-routes', () => {
310
310
  expect(dependencies.gracefulShutdown).not.toHaveBeenCalled();
311
311
  });
312
312
 
313
- it('should parse JSON bodies for snippet config routes', async () => {
314
- const app = express();
315
- registerCommonRequestMiddleware(app, { express });
316
- app.post('/api/config/snippets/example', (req, res) => {
317
- res.json({ body: req.body });
318
- });
319
-
320
- const response = await request(app)
321
- .post('/api/config/snippets/example')
322
- .send({ content: 'Snippet body' })
323
- .expect(200);
324
-
325
- expect(response.body).toEqual({ body: { content: 'Snippet body' } });
326
- });
327
-
328
313
  // Global raw config saves send { content: "<jsonc string>" }. Without this
329
314
  // prefix in the JSON middleware allowlist, PUT arrives with an empty body and
330
315
  // the handler rejects with "Configuration content must be a string".
@@ -0,0 +1 @@
1
+ export function isSensitiveCredentialResponse(method: string, requestPath: string): boolean;
@@ -0,0 +1,11 @@
1
+ /** Credential list/create replies contain raw keys in v2. Keep them host-only. */
2
+ export function isSensitiveCredentialResponse(method, requestPath) {
3
+ let pathname;
4
+ try {
5
+ pathname = decodeURIComponent(new URL(requestPath, 'http://openchamber.invalid').pathname).replace(/\/+$/, '').toLowerCase();
6
+ } catch {
7
+ return true;
8
+ }
9
+ const root = pathname === '/credential' || pathname === '/api/credential';
10
+ return root && ['GET', 'HEAD', 'POST'].includes(String(method).toUpperCase());
11
+ }
@@ -0,0 +1,8 @@
1
+ export function readOpenCode2BinaryVersion(binaryPath: string): string;
2
+
3
+ export function installPinnedOpenCode2Cli(options?: {
4
+ version?: string;
5
+ platform?: string;
6
+ arch?: string;
7
+ dataDir?: string;
8
+ }): Promise<string>;
@@ -233,6 +233,9 @@ export async function ensurePinnedOpenCode2Cli(input = {}) {
233
233
  }
234
234
 
235
235
  const installedPath = await install({ ...input, version: pin });
236
- const version = readVersion(installedPath) || pin;
236
+ const version = readVersion(installedPath);
237
+ if (version !== pin) {
238
+ throw new Error(`Installed OpenCode version mismatch: expected ${pin}, got ${version || 'unknown'}`);
239
+ }
237
240
  return { path: installedPath, version, source: 'installed', installed: true };
238
241
  }
@@ -33,7 +33,10 @@ export const resolveOpenCodeEnvConfig = (options = {}) => {
33
33
  warnInvalidHost(`must use http or https scheme (got ${JSON.stringify(url.protocol)})`);
34
34
  return null;
35
35
  }
36
- const port = parseInt(url.port, 10);
36
+ // WHATWG URL removes explicit default ports; inspect the authority before normalization.
37
+ const authority = raw.match(/^https?:\/\/([^/?#]+)/i)?.[1] ?? '';
38
+ const explicitPort = authority.match(/:(\d+)$/)?.[1];
39
+ const port = parseInt(url.port || explicitPort || '', 10);
37
40
  if (!Number.isFinite(port) || port <= 0) {
38
41
  warnInvalidHost('must include an explicit port (example: http://hostname:4096)');
39
42
  return null;
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { resolveOpenCodeEnvConfig } from './env-config.js';
3
+
4
+ describe('explicit OpenCode host ports', () => {
5
+ it.each([
6
+ ['http://example.test:80', 'http://example.test', 80],
7
+ ['https://example.test:443/', 'https://example.test', 443],
8
+ ['http://[::1]:80', 'http://[::1]', 80],
9
+ ['https://[::1]:443', 'https://[::1]', 443],
10
+ ['http://[::1]:4096', 'http://[::1]:4096', 4096],
11
+ ['https://example.test:80', 'https://example.test:80', 80],
12
+ ])('preserves the effective port of %s', (host, origin, port) => {
13
+ const config = resolveOpenCodeEnvConfig({ env: { OPENCODE_HOST: host, OPENCODE_PORT: '1234' } });
14
+ expect(config.configuredOpenCodeHost).toEqual({ origin, port });
15
+ expect(config.effectivePort).toBe(port);
16
+ expect(new URL('/api/info', config.configuredOpenCodeHost.origin).href).toBe(`${origin}/api/info`);
17
+ });
18
+
19
+ it.each(['http://example.test', 'https://example.test', 'http://[::1]', 'http://example.test:',
20
+ 'ftp://example.test:80', 'http://example.test:80/path', 'http://example.test:80?q=1',
21
+ 'http://example.test:80#hash', 'http://example.test:0', 'http://example.test:65536', 'invalid'])('retains rejection of %s', (host) => {
22
+ const logger = { warn: vi.fn() };
23
+ const config = resolveOpenCodeEnvConfig({ env: { OPENCODE_HOST: host, OPENCODE_PORT: '1234' }, logger });
24
+ expect(config.configuredOpenCodeHost).toBeNull();
25
+ expect(config.effectivePort).toBe(1234);
26
+ expect(logger.warn).toHaveBeenCalledOnce();
27
+ });
28
+ });
@@ -3,6 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { afterEach, describe, expect, it } from 'vitest';
5
5
  import { createOpenCodeEnvRuntime } from './env-runtime.js';
6
+ import { PINNED_OPENCODE2_VERSION } from './opencode2-pin.js';
6
7
 
7
8
  const originalOpencodeBinary = process.env.OPENCODE_BINARY;
8
9
  const originalComSpec = process.env.ComSpec;
@@ -243,6 +244,22 @@ describe('OpenCode env runtime', () => {
243
244
  expect(state.resolvedOpencodeBinarySource).toBe('path');
244
245
  });
245
246
 
247
+ it('uses a newly installed global CLI on a fresh startup with an existing owned cache', async () => {
248
+ const dataDir = createTempDir('openchamber-restart-cache-');
249
+ const cached = path.join(dataDir, 'opencode-cli', '2.0.12', 'opencode');
250
+ const pathDir = createTempDir('openchamber-restart-global-');
251
+ const globalBinary = path.join(pathDir, 'opencode');
252
+ writeVersionBinary(cached, '2.0.12');
253
+ writeVersionBinary(globalBinary, '2.0.15');
254
+ process.env.OPENCHAMBER_DATA_DIR = dataDir;
255
+ process.env.PATH = pathDir;
256
+ delete process.env.OPENCODE_BINARY;
257
+ const { runtime, state } = createRuntime({});
258
+
259
+ await expect(runtime.ensurePinnedOpenCode2CliEnv()).resolves.toBe(globalBinary);
260
+ expect(state.resolvedOpencodeBinarySource).toBe('path');
261
+ });
262
+
246
263
  it('keeps explicit OpenCode binary ahead of bundled CLI', () => {
247
264
  const bundledDir = createTempDir('openchamber-bundled-opencode-');
248
265
  const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
@@ -289,7 +306,7 @@ describe('OpenCode env runtime', () => {
289
306
 
290
307
  it('discovers a previously installed pin from the OpenChamber data dir', () => {
291
308
  const dataDir = createTempDir('openchamber-installed-cli-');
292
- const installedDir = path.join(dataDir, 'opencode-cli', '2.0.12');
309
+ const installedDir = path.join(dataDir, 'opencode-cli', PINNED_OPENCODE2_VERSION);
293
310
  const installedBinary = path.join(installedDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
294
311
  fs.mkdirSync(installedDir, { recursive: true });
295
312
  fs.writeFileSync(installedBinary, '#!/bin/sh\nexit 0\n');
@@ -5,6 +5,8 @@ import { registerSessionGoalRoutes } from '../session-goal/routes.js';
5
5
  import { registerSessionMetadataRoutes } from '../session-metadata/routes.js';
6
6
  import { registerGitHubRoutes } from '../github/routes.js';
7
7
  import { registerGitRoutes } from '../git/routes.js';
8
+ import { makeOpenCodeV2Client } from './v2-client.js';
9
+ import { releaseDeletedWorktreeLocation } from './location-release.js';
8
10
  import { registerMagicPromptRoutes } from '../magic-prompts/routes.js';
9
11
  import { registerSessionFoldersRoutes } from '../session-folders/routes.js';
10
12
  import { registerPermissionAutoAcceptRoutes } from '../permission-auto-accept/runtime.js';
@@ -19,6 +21,9 @@ import { registerSessionTurnPageRoutes } from '../session-turn-pages/routes.js';
19
21
  import { registerAssistantRoutes } from '../assistants/routes.js';
20
22
  import { registerLlmRoutes } from '../llm/routes.js';
21
23
  import { registerMessageQueueRoutes } from '../message-queue/routes.js';
24
+ import { getBrowserProviderHost } from '../browser-provider/host.js';
25
+ import { registerBrowserProviderRoutes } from '../browser-provider/routes.js';
26
+ import { createBrowserProviderSelectionStore } from '../browser-provider/selection-store.js';
22
27
  import { registerSkillRoutes } from './skill-routes.js';
23
28
  import { registerPluginRoutes } from './plugin-routes.js';
24
29
  import { getNpmInfo, clearCache as clearNpmCache } from './npm-registry.js';
@@ -28,7 +33,6 @@ import { getProviderSources, removeProviderConfig } from './providers.js';
28
33
  import { getAgentSources, getAgentConfig, listDisabledAgentOverrides, createAgent, updateAgent, deleteAgent } from './agents.js';
29
34
  import { getCommandSources, createCommand, updateCommand, deleteCommand } from './commands.js';
30
35
  import { listMcpConfigs, getMcpConfig, createMcpConfig, updateMcpConfig, deleteMcpConfig } from './mcp.js';
31
- import { listSnippets, getSnippet, createSnippet, updateSnippet, deleteSnippet, expandSnippets } from './snippets.js';
32
36
  import {
33
37
  listPluginEntries,
34
38
  getPluginEntry,
@@ -356,12 +360,6 @@ export const createFeatureRoutesRuntime = (dependencies) => {
356
360
  createMcpConfig,
357
361
  updateMcpConfig,
358
362
  deleteMcpConfig,
359
- listSnippets,
360
- getSnippet,
361
- createSnippet,
362
- updateSnippet,
363
- deleteSnippet,
364
- expandSnippets,
365
363
  });
366
364
 
367
365
  registerPluginRoutes(app, {
@@ -489,6 +487,13 @@ export const createFeatureRoutesRuntime = (dependencies) => {
489
487
  registerGitRoutes(app, {
490
488
  messageQueueService,
491
489
  broadcastWorktreeTopologyChanged,
490
+ releaseWorktreeLocation: (directory) => releaseDeletedWorktreeLocation({
491
+ directory,
492
+ getClient: () => makeOpenCodeV2Client({
493
+ baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
494
+ authHeaders: getOpenCodeAuthHeaders(),
495
+ }),
496
+ }),
492
497
  });
493
498
  registerMagicPromptRoutes(app, {
494
499
  fsPromises,
@@ -513,6 +518,13 @@ export const createFeatureRoutesRuntime = (dependencies) => {
513
518
  openchamberUserConfigRoot,
514
519
  openchamberDataDir,
515
520
  });
521
+ const browserProviderHost = getBrowserProviderHost();
522
+ if (typeof openchamberDataDir === 'string' && openchamberDataDir) {
523
+ browserProviderHost.attachSelectionStore(createBrowserProviderSelectionStore(
524
+ path.join(openchamberDataDir, 'browser-provider-selection.json'),
525
+ ));
526
+ }
527
+ registerBrowserProviderRoutes(app, { host: browserProviderHost });
516
528
  };
517
529
 
518
530
  return {
@@ -54,6 +54,13 @@ describe('feature routes runtime composition', () => {
54
54
  expect(source).toMatch(/registerScheduledTaskRoutes\(app, \{[\s\S]*runHistoryStore,/);
55
55
  });
56
56
 
57
+ it('registers browser provider routes on the shared host before proxy composition', async () => {
58
+ const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
59
+ expect(source).toContain("import { registerBrowserProviderRoutes } from '../browser-provider/routes.js';");
60
+ expect(source).toContain('registerBrowserProviderRoutes(app, { host: browserProviderHost });');
61
+ expect(source).toContain('createBrowserProviderSelectionStore(');
62
+ });
63
+
57
64
  it('registers message queue routes with the injected service before proxy composition', async () => {
58
65
  const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
59
66
  expect(source).toContain("import { registerMessageQueueRoutes } from '../message-queue/routes.js';");
@@ -35,9 +35,9 @@ export const createHmrStateRuntime = (dependencies) => {
35
35
  if (typeof hmrState.userProvidedOpenCodePassword !== 'undefined') {
36
36
  return;
37
37
  }
38
- const initialPassword = typeof processLike.env.OPENCODE_SERVER_PASSWORD === 'string'
39
- ? processLike.env.OPENCODE_SERVER_PASSWORD.trim()
40
- : '';
38
+ const initialPassword = processLike.env.OPENCODE_PASSWORD?.trim()
39
+ || processLike.env.OPENCODE_SERVER_PASSWORD?.trim()
40
+ || '';
41
41
  hmrState.userProvidedOpenCodePassword = initialPassword || null;
42
42
  };
43
43
 
@@ -51,7 +51,7 @@ export const createHmrStateRuntime = (dependencies) => {
51
51
  openCodeAuthPassword:
52
52
  typeof hmrState.openCodeAuthPassword === 'string' && hmrState.openCodeAuthPassword.length > 0
53
53
  ? hmrState.openCodeAuthPassword
54
- : userProvidedOpenCodePassword,
54
+ : hmrState.openCodeAuthSource === 'shared-service' ? null : userProvidedOpenCodePassword,
55
55
  openCodeAuthSource:
56
56
  typeof hmrState.openCodeAuthSource === 'string' && hmrState.openCodeAuthSource.length > 0
57
57
  ? hmrState.openCodeAuthSource
@@ -29,8 +29,6 @@ const OPENCODE_CHILD_SIGTERM_GRACE_MS = 2500;
29
29
  const OPENCODE_CHILD_SIGKILL_WAIT_MS = 1000;
30
30
  // Last-used directory plus recently opened projects — deeper tails are unlikely
31
31
  // to be the user's first click and just add background work.
32
- const WARMUP_DIRECTORY_LIMIT = 4;
33
- const WARMUP_REQUEST_TIMEOUT_MS = 30000;
34
32
 
35
33
  export const createOpenCodeLifecycleRuntime = (deps) => {
36
34
  const {
@@ -57,7 +55,6 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
57
55
  getManagedOpenCodeShellEnvSnapshot,
58
56
  managedCapabilitiesRuntime = null,
59
57
  getActiveSessionCount = () => 0,
60
- getWarmupDirectories = async () => [],
61
58
  // Official background service (shared-service.js). null keeps the private
62
59
  // managed `opencode serve` path.
63
60
  sharedService = null,
@@ -796,6 +793,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
796
793
  ...shellEnv,
797
794
  ...process.env,
798
795
  PATH: envPath,
796
+ OPENCODE_PASSWORD: openCodePassword,
799
797
  OPENCODE_SERVER_PASSWORD: openCodePassword,
800
798
  };
801
799
  const managedEnv = managedCapabilitiesRuntime
@@ -1253,11 +1251,6 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
1253
1251
  await waitForOpenCodePort();
1254
1252
  try {
1255
1253
  await waitForOpenCodeReady();
1256
- // Warm only after readiness admits; a failed gate must not kick off
1257
- // directory-scoped traffic against an unready upstream.
1258
- if (state.isOpenCodeReady) {
1259
- void warmOpenCodeDirectories();
1260
- }
1261
1254
  } catch (error) {
1262
1255
  console.error(`OpenCode readiness check failed: ${error.message}`);
1263
1256
  }
@@ -1268,47 +1261,6 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
1268
1261
  }
1269
1262
  };
1270
1263
 
1271
- // OpenCode initializes each project directory lazily on its first
1272
- // directory-scoped request, and that initialization takes seconds on large
1273
- // session stores. Without warming, the user's first session open pays it
1274
- // interactively. Warm the most recently used directories right after
1275
- // readiness so the work overlaps UI startup instead. Sequential and
1276
- // best-effort: a failed or slow directory never blocks the others for long,
1277
- // and a restart invalidates the pass via the port/readiness guard.
1278
- const warmOpenCodeDirectories = async () => {
1279
- let directories = [];
1280
- try {
1281
- directories = await getWarmupDirectories();
1282
- } catch {
1283
- return;
1284
- }
1285
- if (!Array.isArray(directories) || directories.length === 0) return;
1286
-
1287
- const warmedPort = state.openCodePort;
1288
- for (const directory of directories.slice(0, WARMUP_DIRECTORY_LIMIT)) {
1289
- if (typeof directory !== 'string' || !directory) continue;
1290
- if (!state.isOpenCodeReady || state.openCodePort !== warmedPort) return;
1291
- let timeout = null;
1292
- try {
1293
- const controller = new AbortController();
1294
- timeout = setTimeout(() => controller.abort(), WARMUP_REQUEST_TIMEOUT_MS);
1295
- // Warming a directory is the point, not the answer: any directory-scoped
1296
- // read makes OpenCode initialise it. `/api/session` is the cheapest one
1297
- // that takes a directory.
1298
- const url = `${buildOpenCodeUrl('/api/session', '')}?directory=${encodeURIComponent(directory)}&limit=1`;
1299
- await fetch(url, {
1300
- method: 'GET',
1301
- headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
1302
- signal: controller.signal,
1303
- });
1304
- } catch {
1305
- // Best-effort — the directory stays lazy and the UI's own request warms it.
1306
- } finally {
1307
- if (timeout) clearTimeout(timeout);
1308
- }
1309
- }
1310
- };
1311
-
1312
1264
  const bootstrapOpenCodeAtStartup = () => {
1313
1265
  if (!bootstrapOpenCodePromise) {
1314
1266
  bootstrapOpenCodePromise = runBootstrapOpenCodeAtStartup().finally(() => {
@@ -10,6 +10,8 @@ vi.mock('node:child_process', () => ({
10
10
 
11
11
  const { createOpenCodeLifecycleRuntime } = await import('./lifecycle.js');
12
12
  const { createOpenCodeNetworkRuntime } = await import('./network-runtime.js');
13
+ const { resolveOpenCodeEnvConfig } = await import('./env-config.js');
14
+ const { RUNTIME_CONTRACT_MIN_VERIFIED } = await import('./runtime-contract.js');
13
15
 
14
16
  const originalOpencodeBinary = process.env.OPENCODE_BINARY;
15
17
  const originalPath = process.env.PATH;
@@ -106,6 +108,7 @@ const createRuntime = (overrides = {}, stateRef = null) => {
106
108
  getManagedOpenCodeShellEnvSnapshot: vi.fn(() => ({
107
109
  PATH: '/home/user/.bun/bin:/usr/local/bin:/usr/bin',
108
110
  SHELL_ONLY: 'yes',
111
+ OPENCODE_PASSWORD: 'shell-v2-password',
109
112
  OPENCODE_SERVER_PASSWORD: 'shell-password',
110
113
  })),
111
114
  ...overrides,
@@ -160,6 +163,28 @@ const startListeningChild = () => {
160
163
  };
161
164
 
162
165
  describe('OpenCode lifecycle', () => {
166
+ it.each(['http://example.test:80', 'https://example.test:443', 'http://[::1]:80'])('attaches explicit default-port host %s without spawning', async (host) => {
167
+ const config = resolveOpenCodeEnvConfig({ env: { OPENCODE_HOST: host, OPENCODE_PORT: '9999' } });
168
+ const stateRef = {};
169
+ const runtime = createRuntime({
170
+ env: {
171
+ ENV_CONFIGURED_OPENCODE_PORT: config.configuredOpenCodePort,
172
+ ENV_CONFIGURED_OPENCODE_HOST: config.configuredOpenCodeHost,
173
+ ENV_EFFECTIVE_PORT: config.effectivePort,
174
+ ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
175
+ ENV_SKIP_OPENCODE_START: true,
176
+ },
177
+ buildOpenCodeUrl: (route) => new URL(route, config.configuredOpenCodeHost.origin).href,
178
+ }, stateRef);
179
+ const fetchMock = stubOpenCodeFetch({ health: { ...DEFAULT_V2_HEALTH, version: '2.0.23' } });
180
+ await runtime.bootstrapOpenCodeAtStartup();
181
+ await runtime.waitForOpenCodeReady(1000, 1);
182
+ expect(stateRef.current.isExternalOpenCode).toBe(true);
183
+ expect(stateRef.current.openCodePort).toBe(config.effectivePort);
184
+ expect(fetchMock.mock.calls.some(([url]) => String(url) === `${config.configuredOpenCodeHost.origin}/api/info`)).toBe(true);
185
+ expect(spawnMock).not.toHaveBeenCalled();
186
+ });
187
+
163
188
  it('launches managed OpenCode with the managed PATH', async () => {
164
189
  delete process.env.OPENCODE_BINARY;
165
190
  const child = createMockChild();
@@ -179,6 +204,7 @@ describe('OpenCode lifecycle', () => {
179
204
  expect(options.env.PATH).toBe('/home/user/.bun/bin:/usr/local/bin:/usr/bin');
180
205
  expect(options.env.SHELL_ONLY).toBe('yes');
181
206
  expect(options.env.OPENCODE_SERVER_PASSWORD).toBe('password');
207
+ expect(options.env.OPENCODE_PASSWORD).toBe('password');
182
208
 
183
209
  await server.close();
184
210
  });
@@ -847,7 +873,7 @@ describe('OpenCode lifecycle', () => {
847
873
  stateRef.current.runtimeContract = {
848
874
  executionAllowed: true,
849
875
  phase: 'ready',
850
- serveVersion: '2.0.12',
876
+ serveVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
851
877
  instanceGeneration: 1,
852
878
  };
853
879
 
@@ -861,7 +887,7 @@ describe('OpenCode lifecycle', () => {
861
887
  runtime.refreshRuntimeContractFromProbe(
862
888
  {
863
889
  ok: true,
864
- version: '2.0.12',
890
+ version: RUNTIME_CONTRACT_MIN_VERIFIED,
865
891
  authenticated: true,
866
892
  healthOk: true,
867
893
  },
@@ -875,7 +901,7 @@ describe('OpenCode lifecycle', () => {
875
901
  runtime.refreshRuntimeContractFromProbe(
876
902
  {
877
903
  ok: true,
878
- version: '2.0.12',
904
+ version: RUNTIME_CONTRACT_MIN_VERIFIED,
879
905
  authenticated: true,
880
906
  healthOk: true,
881
907
  },
@@ -884,7 +910,7 @@ describe('OpenCode lifecycle', () => {
884
910
  );
885
911
  expect(stateRef.current.runtimeContract.executionAllowed).toBe(true);
886
912
  expect(stateRef.current.runtimeContract.instanceGeneration).toBe(2);
887
- expect(stateRef.current.runtimeContract.serveVersion).toBe('2.0.12');
913
+ expect(stateRef.current.runtimeContract.serveVersion).toBe(RUNTIME_CONTRACT_MIN_VERIFIED);
888
914
  });
889
915
 
890
916
  it('does not mark external skip-start ready before version and migration admit', async () => {
@@ -0,0 +1,8 @@
1
+ import type { OpenCodeClient } from '@opencode/client';
2
+
3
+ export type LocationReleaseResult = { state: 'released' | 'timeout' | 'failed' | 'unavailable' };
4
+ export function releaseDeletedWorktreeLocation(input: {
5
+ getClient: () => OpenCodeClient | null;
6
+ directory: string;
7
+ timeoutMs?: number;
8
+ }): Promise<LocationReleaseResult>;