@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.10

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 (328) hide show
  1. package/README.md +13 -7
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +179 -0
  6. package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-CzTtVMnI.js} +1 -1
  7. package/dist/assets/AssistantView-Cg1qQvVe.js +3 -0
  8. package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-hOi72Wdx.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-DCqrQkkz.js} +1 -1
  10. package/dist/assets/Icon-B8fSTLdP.js +1 -0
  11. package/dist/assets/MarkdownRenderer-Cw74oVxV.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-YM7iRmzK.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-BDy_oGJQ.js +3 -0
  14. package/dist/assets/MobileDetailNavigation-iX_hb_Xt.js +1 -0
  15. package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-CAMUupwR.js} +2 -2
  16. package/dist/assets/MobileSurface-CHbdPcLH.js +2 -0
  17. package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-Ci1ODFFX.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-O4cGdz5p.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-CRfGtcmG.js +2 -0
  20. package/dist/assets/SettingsView-Dg8BtTV1.js +25 -0
  21. package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-B8d8qIQ7.js} +1 -1
  22. package/dist/assets/TerminalView-CMiw3s_c.js +8 -0
  23. package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-ByaQYRTp.js} +2 -2
  24. package/dist/assets/appThemeRegistry-BGGHio8T.js +1 -0
  25. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  26. package/dist/assets/{button-CI7Q-Zw_.js → button-DIIrsMjW.js} +1 -1
  27. package/dist/assets/{collapsible-D5Phaykh.js → collapsible-CnvUeYst.js} +1 -1
  28. package/dist/assets/{context-menu-CubSDW2g.js → context-menu-BGBUpZEQ.js} +1 -1
  29. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-JGFqhlaM.js} +1 -1
  30. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-CHEwrI93.js} +1 -1
  31. package/dist/assets/{diagram-DdLO8HZ9.js → diagram-rvn7_qBM.js} +1 -1
  32. package/dist/assets/{dialog-B6pnIYmp.js → dialog-CQ63sc9E.js} +1 -1
  33. package/dist/assets/es-Dsmc0A_3.js +15 -0
  34. package/dist/assets/fileMentionTouchSelection-BzzcWJCi.js +3 -0
  35. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-DN6gbLhM.js} +1 -1
  36. package/dist/assets/fr-C_ZlZDI2.js +13 -0
  37. package/dist/assets/gitApi-BF8vuoZc.js +10 -0
  38. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-BMztRu0c.js} +1 -1
  39. package/dist/assets/{input-D-__fwRi.js → input-BVu-iykl.js} +1 -1
  40. package/dist/assets/insertionBoundaries-EcAfGT2D.js +1 -0
  41. package/dist/assets/ja-BqqTaWTi.js +15 -0
  42. package/dist/assets/ko-DP2XPM37.js +15 -0
  43. package/dist/assets/main-B7t9lavz.js +2 -0
  44. package/dist/assets/main-D2BzpnUt.css +1 -0
  45. package/dist/assets/main-DWP-prNB.js +169 -0
  46. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  47. package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-D9-B1wdY.js} +1 -1
  48. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  49. package/dist/assets/mcpQueries-CfXrAkuU.js +1 -0
  50. package/dist/assets/miniChat-DgOrEQbN.js +2 -0
  51. package/dist/assets/mobile-Co_ZOHkZ.js +2 -0
  52. package/dist/assets/multirun-DelyUO6k.js +4 -0
  53. package/dist/assets/persistence-D0y7226-.js +3 -0
  54. package/dist/assets/pl-BCf5uLlK.js +15 -0
  55. package/dist/assets/platform-CurqO28D.js +6 -0
  56. package/dist/assets/{projectMeta-S1hJ7oBp.js → projectMeta-CPElzJ0n.js} +2 -2
  57. package/dist/assets/pt-BR-W09Q2pvn.js +15 -0
  58. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  59. package/dist/assets/renderElectronMiniChatApp-pOE93w86.js +2 -0
  60. package/dist/assets/renderMobileApp-F7ZSLcw8.js +28 -0
  61. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  62. package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-DSS0KxG3.js} +1 -1
  63. package/dist/assets/runtimeEndpointReset-x2Wgx57R.js +149 -0
  64. package/dist/assets/sessionChangeSummary-BUGSYcHp.js +2 -0
  65. package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-Bss8M20i.js} +1 -1
  66. package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-CE7W7lJ9.js} +1 -1
  67. package/dist/assets/src-lS1keDVV.css +2 -0
  68. package/dist/assets/store-jaP79mqI.js +15 -0
  69. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-mDR2jXTo.js} +1 -1
  70. package/dist/assets/textarea-XD2oX_Ee.js +1 -0
  71. package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-DYeQ_Hvy.js} +1 -1
  72. package/dist/assets/uk-BgFzz8m2.js +15 -0
  73. package/dist/assets/{url-_z7evzdK.js → url-CGbJbiJq.js} +1 -1
  74. package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-BAWHZ-t0.js} +64 -45
  75. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
  76. package/dist/assets/useDesktopWindowControlsLayout-hN_nLb7T.js +1 -0
  77. package/dist/assets/useEffectiveDirectory-Do9DQ453.js +1 -0
  78. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-Bjx8zkGM.js} +1 -1
  79. package/dist/assets/useMobileNavigationStore-BsY_gi_K.js +414 -0
  80. package/dist/assets/useSessionAutoCleanup-C4g5KmDf.js +2 -0
  81. package/dist/assets/useUIStore-syVmbuDA.js +1 -0
  82. package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-B5QXxBok.js} +2 -2
  83. package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
  84. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
  85. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  86. package/dist/assets/zh-CN-BhqSLgUY.js +15 -0
  87. package/dist/assets/zh-TW-BsaBgY-p.js +15 -0
  88. package/dist/index.html +12 -12
  89. package/dist/mini-chat.html +12 -12
  90. package/dist/mobile.html +12 -12
  91. package/package.json +3 -2
  92. package/server/index.js +423 -20
  93. package/server/lib/assistants/DOCUMENTATION.md +26 -21
  94. package/server/lib/assistants/assign.js +2 -1
  95. package/server/lib/assistants/contact-store.js +37 -19
  96. package/server/lib/assistants/contracts.js +11 -1
  97. package/server/lib/assistants/projection-history.test.js +454 -0
  98. package/server/lib/assistants/routes.js +11 -5
  99. package/server/lib/assistants/service.js +850 -294
  100. package/server/lib/assistants/service.test.js +588 -274
  101. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  102. package/server/lib/browser-provider/contract.js +312 -0
  103. package/server/lib/browser-provider/host.js +421 -0
  104. package/server/lib/browser-provider/host.test.js +304 -0
  105. package/server/lib/browser-provider/routes.js +122 -0
  106. package/server/lib/browser-provider/routes.test.js +120 -0
  107. package/server/lib/browser-provider/selection-store.js +43 -0
  108. package/server/lib/browser-provider/selection-store.test.js +35 -0
  109. package/server/lib/conversations/DOCUMENTATION.md +5 -5
  110. package/server/lib/conversations/service.js +87 -57
  111. package/server/lib/conversations/service.test.js +132 -100
  112. package/server/lib/conversations/validation.js +11 -1
  113. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  114. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  115. package/server/lib/event-stream/global-hub.js +51 -5
  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/event-stream/runtime.js +57 -20
  119. package/server/lib/event-stream/runtime.test.js +112 -0
  120. package/server/lib/llm/DOCUMENTATION.md +95 -84
  121. package/server/lib/llm/catalog.js +111 -45
  122. package/server/lib/llm/catalog.test.js +121 -59
  123. package/server/lib/llm/completions.js +41 -12
  124. package/server/lib/llm/completions.test.js +45 -4
  125. package/server/lib/llm/generate.js +516 -453
  126. package/server/lib/llm/generate.test.js +705 -484
  127. package/server/lib/llm/routes.js +8 -4
  128. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  129. package/server/lib/message-queue/opencode-adapter.js +191 -104
  130. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  131. package/server/lib/message-queue/runtime.js +15 -2
  132. package/server/lib/message-queue/runtime.test.js +6 -6
  133. package/server/lib/message-queue/worker.js +7 -1
  134. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  135. package/server/lib/notifications/runtime.js +133 -31
  136. package/server/lib/notifications/runtime.test.js +103 -0
  137. package/server/lib/notifications/template-runtime.js +130 -38
  138. package/server/lib/notifications/template-runtime.test.js +96 -0
  139. package/server/lib/opencode/DOCUMENTATION.md +111 -16
  140. package/server/lib/opencode/agent-document.d.ts +28 -0
  141. package/server/lib/opencode/agent-document.js +280 -0
  142. package/server/lib/opencode/agent-document.test.js +137 -0
  143. package/server/lib/opencode/agents.js +96 -0
  144. package/server/lib/opencode/auth-state-runtime.js +5 -0
  145. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  146. package/server/lib/opencode/config-entity-routes.js +117 -103
  147. package/server/lib/opencode/config-entity-routes.test.js +148 -10
  148. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  149. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  150. package/server/lib/opencode/ensure-cli.js +238 -0
  151. package/server/lib/opencode/ensure-cli.test.js +119 -0
  152. package/server/lib/opencode/env-runtime.js +208 -111
  153. package/server/lib/opencode/env-runtime.test.js +211 -14
  154. package/server/lib/opencode/feature-routes-runtime.js +90 -5
  155. package/server/lib/opencode/feature-routes-runtime.test.js +25 -0
  156. package/server/lib/opencode/http-server-connections.js +98 -0
  157. package/server/lib/opencode/http-server-connections.test.js +321 -0
  158. package/server/lib/opencode/lifecycle.js +539 -95
  159. package/server/lib/opencode/lifecycle.test.js +698 -6
  160. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  161. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  162. package/server/lib/opencode/network-runtime.js +28 -16
  163. package/server/lib/opencode/network-runtime.test.js +68 -4
  164. package/server/lib/opencode/openchamber-routes.js +7 -1
  165. package/server/lib/opencode/opencode2-pin.js +139 -0
  166. package/server/lib/opencode/opencode2-pin.test.js +88 -0
  167. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
  168. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  169. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  170. package/server/lib/opencode/plugin-compatibility.js +15 -0
  171. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  172. package/server/lib/opencode/plugin-routes.js +3 -18
  173. package/server/lib/opencode/plugin-routes.test.js +9 -9
  174. package/server/lib/opencode/plugins.js +4 -3
  175. package/server/lib/opencode/plugins.test.js +12 -0
  176. package/server/lib/opencode/provider-catalog.js +58 -12
  177. package/server/lib/opencode/provider-catalog.test.js +48 -0
  178. package/server/lib/opencode/proxy.js +373 -28
  179. package/server/lib/opencode/proxy.test.js +353 -0
  180. package/server/lib/opencode/routes.js +333 -76
  181. package/server/lib/opencode/routes.test.js +315 -50
  182. package/server/lib/opencode/runtime-contract.js +401 -0
  183. package/server/lib/opencode/runtime-contract.test.js +214 -0
  184. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  185. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  186. package/server/lib/opencode/server-utils-runtime.js +35 -0
  187. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  188. package/server/lib/opencode/shared-service.js +147 -0
  189. package/server/lib/opencode/shared-service.test.js +93 -0
  190. package/server/lib/opencode/shared.js +3 -3
  191. package/server/lib/opencode/shutdown-runtime.js +15 -6
  192. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  193. package/server/lib/opencode/skill-routes.js +26 -40
  194. package/server/lib/opencode/skill-routes.test.js +71 -7
  195. package/server/lib/opencode/skills.test.js +15 -0
  196. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  197. package/server/lib/opencode/static-routes-runtime.js +3 -1
  198. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  199. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  200. package/server/lib/opencode/upgrade-screen.js +299 -0
  201. package/server/lib/opencode/upgrade-screen.test.js +196 -0
  202. package/server/lib/opencode/v1-migration-gate.js +157 -0
  203. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  204. package/server/lib/opencode/v2-client.js +60 -0
  205. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  206. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  207. package/server/lib/opencode/watcher.js +2 -1
  208. package/server/lib/opencode/watcher.test.js +1 -1
  209. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  210. package/server/lib/permission-auto-accept/runtime.js +14 -7
  211. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  212. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  213. package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
  214. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  215. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  216. package/server/lib/scheduled-tasks/routes.js +27 -0
  217. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  218. package/server/lib/scheduled-tasks/runtime.js +381 -310
  219. package/server/lib/scheduled-tasks/runtime.test.js +600 -309
  220. package/server/lib/session-goal/DOCUMENTATION.md +173 -22
  221. package/server/lib/session-goal/capability.js +57 -0
  222. package/server/lib/session-goal/capability.test.js +33 -0
  223. package/server/lib/session-goal/routes.js +240 -2
  224. package/server/lib/session-goal/routes.test.js +211 -0
  225. package/server/lib/session-goal/runtime.js +1617 -113
  226. package/server/lib/session-goal/runtime.test.js +1500 -70
  227. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  228. package/server/lib/session-index/event-ingest.js +89 -16
  229. package/server/lib/session-index/event-ingest.test.js +73 -0
  230. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  231. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  232. package/server/lib/session-index/routes.js +63 -10
  233. package/server/lib/session-index/routes.test.js +8 -8
  234. package/server/lib/session-index/service.js +7 -1
  235. package/server/lib/session-index/sync-runtime.js +171 -41
  236. package/server/lib/session-index/sync-runtime.test.js +164 -9
  237. package/server/lib/session-metadata/DOCUMENTATION.md +96 -0
  238. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  239. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  240. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  241. package/server/lib/session-metadata/routes.js +197 -0
  242. package/server/lib/session-metadata/routes.test.js +156 -0
  243. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  244. package/server/lib/session-metadata/session-archive.js +654 -0
  245. package/server/lib/session-metadata/session-archive.test.js +359 -0
  246. package/server/lib/session-metadata/session-metadata-store.d.ts +38 -0
  247. package/server/lib/session-metadata/session-metadata-store.js +422 -0
  248. package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
  249. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  250. package/server/lib/session-metadata/session-projection.js +279 -0
  251. package/server/lib/session-metadata/session-projection.test.js +64 -0
  252. package/server/lib/session-metadata/system-session.js +55 -0
  253. package/server/lib/session-metadata/system-session.test.js +55 -0
  254. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  255. package/server/lib/session-title/native-runtime.test.js +167 -0
  256. package/server/lib/session-title/runtime.js +69 -44
  257. package/server/lib/session-title/session-access.js +46 -0
  258. package/server/lib/session-title/session-access.test.js +91 -0
  259. package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
  260. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  261. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  262. package/server/lib/session-turn-pages/routes.js +123 -75
  263. package/server/lib/session-turn-pages/routes.test.js +566 -0
  264. package/server/lib/session-turn-pages/service.js +6 -2
  265. package/server/lib/session-turn-pages/service.test.js +34 -0
  266. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  267. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  268. package/server/lib/small-model/DOCUMENTATION.md +101 -146
  269. package/server/lib/small-model/catalog.js +84 -54
  270. package/server/lib/small-model/catalog.test.js +111 -9
  271. package/server/lib/small-model/custom-api.js +68 -0
  272. package/server/lib/small-model/custom-api.test.js +64 -0
  273. package/server/lib/small-model/index.js +125 -241
  274. package/server/lib/small-model/resolve.js +35 -120
  275. package/server/lib/small-model/resolve.test.js +20 -110
  276. package/server/lib/small-model/routes.js +1 -3
  277. package/server/lib/small-model/summary-settings.test.js +157 -253
  278. package/server/opencode-proxy.test.js +63 -12
  279. package/dist/assets/AssistantView-BUveAgc4.js +0 -3
  280. package/dist/assets/Icon-BgdfPhNs.js +0 -1
  281. package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
  282. package/dist/assets/MarkstreamRendererImpl-D2dsW5Lg.js +0 -3
  283. package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
  284. package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
  285. package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
  286. package/dist/assets/SettingsView-PkXIavr7.js +0 -25
  287. package/dist/assets/TerminalView-CDdzF4yi.js +0 -8
  288. package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
  289. package/dist/assets/es-DA3MqOo0.js +0 -15
  290. package/dist/assets/fileMentionTouchSelection-DNeiqns-.js +0 -4
  291. package/dist/assets/fr-Dbz0CdIA.js +0 -13
  292. package/dist/assets/gitApi-DXyC4MiV.js +0 -7
  293. package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
  294. package/dist/assets/ja-DumanVQA.js +0 -15
  295. package/dist/assets/ko-C0IkiUjQ.js +0 -15
  296. package/dist/assets/main-BPS5ylib.js +0 -2
  297. package/dist/assets/main-Cpd9VWYs.js +0 -169
  298. package/dist/assets/main-qSeeJRW1.css +0 -1
  299. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  300. package/dist/assets/markdownTheme-DOfGg-A7.js +0 -1
  301. package/dist/assets/miniChat-B8_KLWiY.js +0 -2
  302. package/dist/assets/mobile-CtktKrXn.js +0 -2
  303. package/dist/assets/multirun-CYfP9Cfm.js +0 -4
  304. package/dist/assets/persistence-Ce3AnpEr.js +0 -3
  305. package/dist/assets/pl-B9No6MQp.js +0 -15
  306. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  307. package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
  308. package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
  309. package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
  310. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  311. package/dist/assets/runtimeEndpointReset-ChOxt-J7.js +0 -147
  312. package/dist/assets/sessionChangeSummary-NnxcqCTI.js +0 -2
  313. package/dist/assets/src-rS3zCcsC.css +0 -2
  314. package/dist/assets/store-5B_LDnLM.js +0 -15
  315. package/dist/assets/textarea-bHYxZtfH.js +0 -1
  316. package/dist/assets/uk-C_1KkqyP.js +0 -15
  317. package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
  318. package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
  319. package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
  320. package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
  321. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  322. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  323. package/dist/assets/zh-CN-uWILgBdt.js +0 -15
  324. package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
  325. package/server/lib/small-model/call.js +0 -427
  326. package/server/lib/small-model/call.test.js +0 -382
  327. package/server/lib/small-model/opencode-session.js +0 -339
  328. package/server/lib/small-model/opencode-session.test.js +0 -177
@@ -2,9 +2,62 @@ import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { parse as parseJsonc } from 'jsonc-parser';
5
- import { createOpencodeClient } from '@opencode-ai/sdk/v2';
5
+ import { OpenCode } from '@opencode/client';
6
6
  import { projectProviderCatalog } from './provider-catalog.js';
7
7
 
8
+ // Compose official v2 provider.list + model.list (+ optional default) into the
9
+ // catalog shape consumed by projectProviderCatalog. Missing arrays are failure.
10
+ const composeV2ProviderCatalogSource = (providers, models, defaultModel) => {
11
+ if (!Array.isArray(providers) || !Array.isArray(models)) return null;
12
+
13
+ const modelsByProvider = new Map();
14
+ for (const model of models) {
15
+ if (!model || typeof model !== 'object') continue;
16
+ const providerID = typeof model.providerID === 'string' ? model.providerID : '';
17
+ // ModelInfo.id is the external model id used in generate/session refs.
18
+ // ModelInfo.modelID is a separate internal field and must not be preferred.
19
+ const modelID = typeof model.id === 'string' && model.id
20
+ ? model.id
21
+ : (typeof model.modelID === 'string' ? model.modelID : '');
22
+ if (!providerID || !modelID) continue;
23
+ if (!modelsByProvider.has(providerID)) modelsByProvider.set(providerID, Object.create(null));
24
+ const entry = { id: modelID };
25
+ if (typeof model.name === 'string' && model.name) entry.name = model.name;
26
+ if (typeof model.family === 'string' && model.family) entry.family = model.family;
27
+ if (model.limit && typeof model.limit === 'object') entry.limit = model.limit;
28
+ if (model.capabilities) entry.capabilities = model.capabilities;
29
+ // v2 cost is a tier array; v2 variants are `{ id }[]`. Projection owns the safe shape.
30
+ if (model.cost && typeof model.cost === 'object') entry.cost = model.cost;
31
+ if (model.variants && typeof model.variants === 'object') entry.variants = model.variants;
32
+ if (typeof model.time?.released === 'number' && Number.isFinite(model.time.released)) {
33
+ entry.release_date = new Date(model.time.released).toISOString().slice(0, 10);
34
+ }
35
+ modelsByProvider.get(providerID)[modelID] = entry;
36
+ }
37
+
38
+ const list = [];
39
+ for (const provider of providers) {
40
+ if (!provider || typeof provider.id !== 'string' || !provider.id) continue;
41
+ list.push({
42
+ id: provider.id,
43
+ name: typeof provider.name === 'string' && provider.name ? provider.name : provider.id,
44
+ models: modelsByProvider.get(provider.id) || Object.create(null),
45
+ });
46
+ }
47
+
48
+ const defaults = Object.create(null);
49
+ const defaultModelID = typeof defaultModel?.id === 'string' && defaultModel.id
50
+ ? defaultModel.id
51
+ : (typeof defaultModel?.modelID === 'string' ? defaultModel.modelID : '');
52
+ if (defaultModel && typeof defaultModel.providerID === 'string' && defaultModelID) {
53
+ defaults[defaultModel.providerID] = defaultModelID;
54
+ }
55
+ return { providers: list, default: defaults };
56
+ };
57
+
58
+ const createOpenCodeClient = ({ baseUrl, headers, fetch: fetchImpl }) =>
59
+ OpenCode.make({ baseUrl, headers, fetch: fetchImpl });
60
+
8
61
  const MAX_GLOBAL_CONFIG_SIZE = 2 * 1024 * 1024;
9
62
  const GLOBAL_CONFIG_FILES = {
10
63
  opencode: ['opencode.json', 'opencode.jsonc'],
@@ -81,10 +134,10 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
81
134
  const {
82
135
  resolveProjectDirectory,
83
136
  resolveOptionalProjectDirectory,
84
- refreshOpenCodeAfterConfigChange,
85
- clientReloadDelayMs,
137
+ waitForOpenCodeReady,
86
138
  getAgentSources,
87
139
  getAgentConfig,
140
+ listDisabledAgentOverrides,
88
141
  createAgent,
89
142
  updateAgent,
90
143
  deleteAgent,
@@ -115,25 +168,43 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
115
168
  if (!directory) {
116
169
  return res.status(400).json({ error });
117
170
  }
118
- const client = createOpencodeClient({
171
+ // Gate on managed-OpenCode readiness like the generic proxy: without this
172
+ // the SDK hits an upstream that is still starting (or a stale orphan from
173
+ // a nodemon restart) and gets HTML/401 back, surfacing as a spurious 502.
174
+ if (typeof waitForOpenCodeReady === 'function') {
175
+ await waitForOpenCodeReady(20_000, 200);
176
+ }
177
+ const client = createOpenCodeClient({
119
178
  baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
120
- directory,
121
179
  headers: getOpenCodeAuthHeaders(),
122
- fetch: (request) => fetch(request, { signal: AbortSignal.timeout(8_000) }),
180
+ // Forward the full init: the SDK carries Authorization in init.headers.
181
+ // Dropping init (the old `(request) => fetch(request, { signal })` shape)
182
+ // silently stripped auth and produced spurious upstream 401s.
183
+ fetch: (request, init) => fetch(request, { ...init, signal: AbortSignal.timeout(8_000) }),
123
184
  });
124
- const response = await client.config.providers({ directory });
125
- if (response?.error || response?.data === undefined) {
185
+ const location = { directory };
186
+ const [providersResult, modelsResult, defaultResult] = await Promise.all([
187
+ client.provider.list({ location }),
188
+ client.model.list({ location }),
189
+ client.model.default({ location }),
190
+ ]);
191
+ const source = composeV2ProviderCatalogSource(
192
+ providersResult?.data,
193
+ modelsResult?.data,
194
+ defaultResult?.data,
195
+ );
196
+ if (!source) {
126
197
  console.error('Provider catalog upstream response failed');
127
198
  return res.status(502).json({ error: 'Provider catalog is unavailable' });
128
199
  }
129
- const catalog = projectProviderCatalog(response?.data);
200
+ const catalog = projectProviderCatalog(source);
130
201
  if (!catalog.ok) {
131
202
  console.error('Provider catalog upstream response is malformed');
132
203
  return res.status(502).json({ error: 'Provider catalog is unavailable' });
133
204
  }
134
205
  return res.json(catalog.value);
135
- } catch {
136
- console.error('Provider catalog request failed');
206
+ } catch (catalogError) {
207
+ console.error('Provider catalog request failed', catalogError?.reason ? `(${catalogError.reason})` : catalogError?.message ?? catalogError);
137
208
  return res.status(502).json({ error: 'Provider catalog is unavailable' });
138
209
  }
139
210
  });
@@ -191,7 +262,8 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
191
262
  target: req.params.target,
192
263
  fileName: target.fileName,
193
264
  content,
194
- requiresManualRestart: true,
265
+ requiresManualRestart: req.params.target !== 'opencode',
266
+ application: req.params.target === 'opencode' ? 'watch' : 'manual',
195
267
  });
196
268
  } catch (error) {
197
269
  console.error('Failed to write global configuration:', error);
@@ -199,29 +271,9 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
199
271
  }
200
272
  });
201
273
 
202
- // Build the response for a config mutation based on whether OpenCode actually
203
- // reloaded the change. When connected to an external OpenCode server that
204
- // OpenChamber cannot restart, the change is persisted to disk but the running
205
- // server will not serve it until the user restarts that server. We must not
206
- // report a clean "reloading" success in that case, otherwise the UI silently
207
- // reverts the edit to the stale value on the next refresh.
208
- const buildConfigMutationResponse = (refreshResult, { liveMessage, manualRestartMessage }) => {
209
- if (refreshResult && refreshResult.external) {
210
- return {
211
- success: true,
212
- requiresReload: false,
213
- requiresManualRestart: true,
214
- message: manualRestartMessage,
215
- };
216
- }
217
-
218
- return {
219
- success: true,
220
- requiresReload: true,
221
- message: liveMessage,
222
- reloadDelayMs: clientReloadDelayMs,
223
- };
224
- };
274
+ // A receipt confirms persistence, not runtime activation. V2 watches local
275
+ // configuration; domain events confirm changes without rebuilding locations.
276
+ const configSaved = () => ({ success: true, requiresReload: false, application: 'watch' });
225
277
 
226
278
  const readMutationBody = (req) => (
227
279
  req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : {}
@@ -230,24 +282,7 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
230
282
  const completeMcpMutation = async (res, action, name, applyChange) => {
231
283
  applyChange();
232
284
 
233
- try {
234
- await refreshOpenCodeAfterConfigChange(`mcp ${action}`);
235
- return res.json({
236
- success: true,
237
- requiresReload: true,
238
- message: `MCP server "${name}" ${action}d. Reloading interface…`,
239
- reloadDelayMs: clientReloadDelayMs,
240
- });
241
- } catch (error) {
242
- console.error(`[API:MCP ${action}] Reload failed after config write:`, error);
243
- return res.json({
244
- success: true,
245
- requiresReload: false,
246
- reloadFailed: true,
247
- message: `MCP server "${name}" ${action}d, but OpenCode reload failed.`,
248
- warning: error.message || 'OpenCode reload failed after the MCP configuration changed',
249
- });
250
- }
285
+ return res.json({ ...configSaved(), message: `MCP server "${name}" ${action}d.` });
251
286
  };
252
287
 
253
288
  app.post('/api/config/agents/metadata', async (req, res) => {
@@ -277,7 +312,10 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
277
312
  sources,
278
313
  };
279
314
  }
280
- return res.json({ agents });
315
+ const disabled = typeof listDisabledAgentOverrides === 'function'
316
+ ? listDisabledAgentOverrides(directory)
317
+ : [];
318
+ return res.json(disabled.length > 0 ? { agents, disabled } : { agents });
281
319
  } catch (error) {
282
320
  console.error('Failed to get agent metadata batch:', error);
283
321
  return res.status(500).json({ error: 'Failed to get agent configuration metadata' });
@@ -337,15 +375,11 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
337
375
  console.log('[Server] Creating agent');
338
376
 
339
377
  createAgent(agentName, config, directory, scope);
340
- const refreshResult = await refreshOpenCodeAfterConfigChange('agent creation', {
341
- agentName
342
- });
343
-
344
- res.json(buildConfigMutationResponse(refreshResult, {
345
- liveMessage: `Agent ${agentName} created successfully. Reloading interface…`,
346
- manualRestartMessage: `Agent ${agentName} saved. Restart your connected OpenCode server to apply the change.`,
347
- }));
378
+ res.json(configSaved());
348
379
  } catch (error) {
380
+ if (error?.code === 'drop-confirmation') {
381
+ return res.status(409).json({ code: 'drop-confirmation', dropped: error.dropped, error: error.message });
382
+ }
349
383
  console.error('Failed to create agent');
350
384
  res.status(500).json({ error: error.message || 'Failed to create agent' });
351
385
  }
@@ -363,15 +397,14 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
363
397
  console.log('[Server] Updating agent');
364
398
 
365
399
  updateAgent(agentName, updates, directory);
366
- const refreshResult = await refreshOpenCodeAfterConfigChange('agent update');
367
400
 
368
401
  console.log('[Server] Agent updated successfully');
369
402
 
370
- res.json(buildConfigMutationResponse(refreshResult, {
371
- liveMessage: `Agent ${agentName} updated successfully. Reloading interface…`,
372
- manualRestartMessage: `Agent ${agentName} saved. Restart your connected OpenCode server to apply the change.`,
373
- }));
403
+ res.json(configSaved());
374
404
  } catch (error) {
405
+ if (error?.code === 'drop-confirmation') {
406
+ return res.status(409).json({ code: 'drop-confirmation', dropped: error.dropped, error: error.message });
407
+ }
375
408
  console.error('[Server] Failed to update agent');
376
409
  res.status(500).json({ error: error.message || 'Failed to update agent' });
377
410
  }
@@ -387,12 +420,7 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
387
420
 
388
421
  const scope = req.body?.scope;
389
422
  deleteAgent(agentName, directory, scope);
390
- const refreshResult = await refreshOpenCodeAfterConfigChange('agent deletion');
391
-
392
- res.json(buildConfigMutationResponse(refreshResult, {
393
- liveMessage: `Agent ${agentName} deleted successfully. Reloading interface…`,
394
- manualRestartMessage: `Agent ${agentName} deleted. Restart your connected OpenCode server to apply the change.`,
395
- }));
423
+ res.json(configSaved());
396
424
  } catch (error) {
397
425
  console.error('Failed to delete agent');
398
426
  res.status(500).json({ error: error.message || 'Failed to delete agent' });
@@ -500,14 +528,22 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
500
528
  if (!getOpenCodePort()) {
501
529
  return res.json({ commands: [] });
502
530
  }
503
- const client = createOpencodeClient({
531
+ const client = createOpenCodeClient({
504
532
  baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
505
- directory,
506
533
  headers: getOpenCodeAuthHeaders(),
507
- fetch: (request) => fetch(request, { signal: AbortSignal.timeout(8_000) }),
534
+ fetch: (request, init) => fetch(request, {
535
+ ...init,
536
+ signal: init?.signal
537
+ ? AbortSignal.any([init.signal, AbortSignal.timeout(8_000)])
538
+ : AbortSignal.timeout(8_000),
539
+ }),
508
540
  });
509
- const response = await client.command.list({ directory });
510
- const commands = Array.isArray(response?.data) ? response.data : [];
541
+ const response = await client.command.list({ location: { directory } });
542
+ if (!Array.isArray(response?.data)) {
543
+ console.error('Command catalog upstream response failed');
544
+ return res.status(502).json({ error: 'Command catalog is unavailable' });
545
+ }
546
+ const commands = response.data;
511
547
  return res.json({
512
548
  commands: commands
513
549
  .filter((command) => command?.source !== 'skill')
@@ -598,16 +634,7 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
598
634
  console.log('[Server] Creating command');
599
635
 
600
636
  createCommand(commandName, config, directory, scope);
601
- await refreshOpenCodeAfterConfigChange('command creation', {
602
- commandName
603
- });
604
-
605
- res.json({
606
- success: true,
607
- requiresReload: true,
608
- message: `Command ${commandName} created successfully. Reloading interface…`,
609
- reloadDelayMs: clientReloadDelayMs,
610
- });
637
+ res.json(configSaved());
611
638
  } catch (error) {
612
639
  console.error('Failed to create command');
613
640
  res.status(500).json({ error: error.message || 'Failed to create command' });
@@ -626,16 +653,10 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
626
653
  console.log('[Server] Updating command');
627
654
 
628
655
  updateCommand(commandName, updates, directory);
629
- await refreshOpenCodeAfterConfigChange('command update');
630
656
 
631
657
  console.log('[Server] Command updated successfully');
632
658
 
633
- res.json({
634
- success: true,
635
- requiresReload: true,
636
- message: `Command ${commandName} updated successfully. Reloading interface…`,
637
- reloadDelayMs: clientReloadDelayMs,
638
- });
659
+ res.json(configSaved());
639
660
  } catch (error) {
640
661
  console.error('[Server] Failed to update command');
641
662
  res.status(500).json({ error: error.message || 'Failed to update command' });
@@ -651,14 +672,7 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
651
672
  }
652
673
 
653
674
  deleteCommand(commandName, directory);
654
- await refreshOpenCodeAfterConfigChange('command deletion');
655
-
656
- res.json({
657
- success: true,
658
- requiresReload: true,
659
- message: `Command ${commandName} deleted successfully. Reloading interface…`,
660
- reloadDelayMs: clientReloadDelayMs,
661
- });
675
+ res.json(configSaved());
662
676
  } catch (error) {
663
677
  console.error('Failed to delete command');
664
678
  res.status(500).json({ error: error.message || 'Failed to delete command' });
@@ -5,10 +5,11 @@ import fs from 'node:fs/promises';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
7
 
8
- vi.mock('@opencode-ai/sdk/v2', () => ({ createOpencodeClient: vi.fn() }));
8
+ vi.mock('@opencode/client', () => ({ OpenCode: { make: vi.fn() } }));
9
9
 
10
- const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
10
+ const { OpenCode } = await import('@opencode/client');
11
11
  const { registerConfigEntityRoutes } = await import('./config-entity-routes.js');
12
+ const createOpencodeClient = OpenCode.make;
12
13
 
13
14
  beforeEach(() => {
14
15
  createOpencodeClient.mockReset();
@@ -49,7 +50,50 @@ const createDependencies = (getCommandSources, configDirectory, getAgentSources
49
50
  getOpenCodePort: () => 4096,
50
51
  });
51
52
 
53
+ describe('V2 configuration persistence', () => {
54
+ it.each(['agents', 'commands', 'mcp'])('%s CRUD never restarts or reloads OpenCode', async (entity) => {
55
+ const dependencies = createDependencies(vi.fn());
56
+ dependencies.refreshOpenCodeAfterConfigChange.mockRejectedValue(new Error('must not manage the service'));
57
+ const app = express();
58
+ app.use(express.json());
59
+ registerConfigEntityRoutes(app, dependencies);
60
+ for (const method of ['post', 'patch', 'delete']) {
61
+ const response = await request(app)[method](`/api/config/${entity}/example?directory=%2Frepo`)
62
+ .send({ name: 'example', scope: 'project', type: 'local', command: ['example'] });
63
+ expect(response.status).toBe(200);
64
+ expect(response.body).toMatchObject({ success: true, requiresReload: false, application: 'watch' });
65
+ expect(response.body.requiresManualRestart).not.toBe(true);
66
+ }
67
+ expect(dependencies.refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
68
+ });
69
+ });
70
+
52
71
  describe('config entity command metadata route', () => {
72
+ it('preserves real SDK authentication and request options when fetching the catalog', async () => {
73
+ const { OpenCode: realOpenCode } = await vi.importActual('@opencode/client');
74
+ createOpencodeClient.mockImplementation(realOpenCode.make);
75
+ const upstream = vi.spyOn(globalThis, 'fetch').mockImplementation(async (_input, init) => {
76
+ if (new Headers(init?.headers).get('Authorization') !== 'Basic example') {
77
+ return new Response('Unauthorized', { status: 401 });
78
+ }
79
+ return Response.json({ data: [{ name: 'review', description: 'Review changes' }] });
80
+ });
81
+ try {
82
+ const app = express();
83
+ app.use(express.json());
84
+ registerConfigEntityRoutes(app, createDependencies(() => ({ md: { exists: false }, json: { exists: false } })));
85
+ const response = await request(app).post('/api/config/commands/metadata').send({ catalog: true });
86
+ expect(response.status).toBe(200);
87
+ expect(response.body.commands).toEqual([expect.objectContaining({ name: 'review' })]);
88
+ const [url, init] = upstream.mock.calls[0];
89
+ expect(new URL(url).pathname).toBe('/api/command');
90
+ expect(init.method).toBe('GET');
91
+ expect(init.signal).toBeInstanceOf(AbortSignal);
92
+ } finally {
93
+ upstream.mockRestore();
94
+ }
95
+ });
96
+
53
97
  it('returns metadata for many commands through one request', async () => {
54
98
  const app = express();
55
99
  app.use(express.json());
@@ -100,9 +144,9 @@ describe('config entity command metadata route', () => {
100
144
 
101
145
  expect(createOpencodeClient).toHaveBeenCalledWith(expect.objectContaining({
102
146
  baseUrl: 'http://opencode-upstream:4096',
103
- directory: '/repo',
104
147
  headers: { Authorization: 'Basic example' },
105
148
  }));
149
+ expect(createOpencodeClient.mock.calls[0][0].directory).toBeUndefined();
106
150
  expect(response.body.commands).toEqual([
107
151
  expect.objectContaining({ name: 'markdown', reference: markdownPath, description: `${'😀'.repeat(160)}…`, scope: 'project', isBuiltIn: false }),
108
152
  expect.objectContaining({ name: 'json', reference: 'json', description: 'JSON command', scope: 'project', isBuiltIn: false }),
@@ -149,14 +193,35 @@ describe('config entity command metadata route', () => {
149
193
  { name: 'long-path', reference: 'long-path' },
150
194
  ]);
151
195
  });
196
+
197
+ it('returns 502 when the command catalog is not an array instead of an empty success', async () => {
198
+ const app = express();
199
+ app.use(express.json());
200
+ createOpencodeClient.mockReturnValue({
201
+ command: { list: vi.fn(async () => ({ data: { commands: [] } })) },
202
+ });
203
+ registerConfigEntityRoutes(app, createDependencies(vi.fn()));
204
+
205
+ const response = await request(app)
206
+ .post('/api/config/commands/metadata?directory=%2Frepo')
207
+ .send({ catalog: true })
208
+ .expect(502);
209
+ expect(response.body).toEqual({ error: 'Command catalog is unavailable' });
210
+ });
152
211
  });
153
212
 
154
213
  describe('provider catalog route', () => {
155
214
  it('uses the resolved directory, OpenCode auth, SDK providers call, and explicit catalog route', async () => {
156
215
  const app = express();
157
216
  const resolveProjectDirectory = vi.fn(async () => ({ directory: '/project/from-header' }));
158
- const providers = vi.fn(async () => ({ data: { providers: [], default: {} } }));
159
- createOpencodeClient.mockReturnValue({ config: { providers } });
217
+ const location = { directory: '/project/from-header' };
218
+ const providers = vi.fn(async () => ({ data: [] }));
219
+ const models = vi.fn(async () => ({ data: [] }));
220
+ const modelDefault = vi.fn(async () => ({ data: null }));
221
+ createOpencodeClient.mockReturnValue({
222
+ provider: { list: providers },
223
+ model: { list: models, default: modelDefault },
224
+ });
160
225
  registerConfigEntityRoutes(app, {
161
226
  ...createDependencies(vi.fn()),
162
227
  resolveProjectDirectory,
@@ -171,22 +236,94 @@ describe('provider catalog route', () => {
171
236
  expect(resolveProjectDirectory).toHaveBeenCalledWith(expect.objectContaining({ query: { directory: '/repo' } }));
172
237
  expect(createOpencodeClient).toHaveBeenCalledWith(expect.objectContaining({
173
238
  baseUrl: 'http://opencode-upstream:4096',
174
- directory: '/project/from-header',
175
239
  headers: { Authorization: 'Basic example' },
176
240
  fetch: expect.any(Function),
177
241
  }));
178
- expect(providers).toHaveBeenCalledWith({ directory: '/project/from-header' });
242
+ expect(createOpencodeClient.mock.calls[0][0].directory).toBeUndefined();
243
+ expect(providers).toHaveBeenCalledWith({ location });
244
+ expect(models).toHaveBeenCalledWith({ location });
245
+ expect(modelDefault).toHaveBeenCalledWith({ location });
179
246
  expect(response.body).toEqual({ schemaVersion: 1, providers: [], default: {}, partial: false });
180
247
  });
181
248
 
249
+ it('projects ModelInfo.id as the catalog model id and default', async () => {
250
+ const app = express();
251
+ createOpencodeClient.mockReturnValue({
252
+ provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
253
+ model: {
254
+ list: vi.fn(async () => ({
255
+ data: [{
256
+ id: 'gpt-5.4-mini',
257
+ modelID: 'internal-pack',
258
+ providerID: 'openai',
259
+ name: 'GPT-5.4 Mini',
260
+ }],
261
+ })),
262
+ default: vi.fn(async () => ({
263
+ data: { id: 'gpt-5.4-mini', modelID: 'internal-pack', providerID: 'openai', name: 'GPT-5.4 Mini' },
264
+ })),
265
+ },
266
+ });
267
+ registerConfigEntityRoutes(app, createDependencies(vi.fn()));
268
+
269
+ const response = await request(app).get('/api/config/catalog/providers').expect(200);
270
+ expect(response.body.providers).toEqual([{
271
+ id: 'openai',
272
+ name: 'OpenAI',
273
+ models: { 'gpt-5.4-mini': { id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini' } },
274
+ }]);
275
+ expect(response.body.default).toEqual({ openai: 'gpt-5.4-mini' });
276
+ expect(JSON.stringify(response.body)).not.toContain('internal-pack');
277
+ });
278
+
279
+ it('projects v2 thinking variants into the provider catalog and strips request payloads', async () => {
280
+ const app = express();
281
+ createOpencodeClient.mockReturnValue({
282
+ provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
283
+ model: {
284
+ list: vi.fn(async () => ({
285
+ data: [{
286
+ id: 'gpt-5.4',
287
+ modelID: 'internal-pack',
288
+ providerID: 'openai',
289
+ name: 'GPT 5.4',
290
+ capabilities: { tools: true, input: ['text', 'image'], output: ['text'] },
291
+ variants: [
292
+ { id: 'low', settings: { reasoningEffort: 'low', apiKey: 'variant-settings-sentinel' } },
293
+ { id: 'high', headers: { Authorization: 'variant-headers-sentinel' }, body: { store: false } },
294
+ ],
295
+ }],
296
+ })),
297
+ default: vi.fn(async () => ({ data: null })),
298
+ },
299
+ });
300
+ registerConfigEntityRoutes(app, createDependencies(vi.fn()));
301
+
302
+ const response = await request(app).get('/api/config/catalog/providers').expect(200);
303
+ expect(response.body.providers[0].models['gpt-5.4'].variants).toEqual({ low: {}, high: {} });
304
+ expect(response.body.providers[0].models['gpt-5.4'].capabilities).toEqual({
305
+ toolcall: true,
306
+ input: { text: true, image: true },
307
+ output: { text: true },
308
+ });
309
+ expect(JSON.stringify(response.body)).not.toContain('sentinel');
310
+ expect(JSON.stringify(response.body)).not.toContain('internal-pack');
311
+ });
312
+
182
313
  it('returns 502 for SDK failure and malformed catalog responses', async () => {
183
314
  const app = express();
184
- createOpencodeClient.mockReturnValue({ config: { providers: vi.fn(async () => ({ data: { providers: {} } })) } });
315
+ createOpencodeClient.mockReturnValue({
316
+ provider: { list: vi.fn(async () => ({ data: {} })) },
317
+ model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
318
+ });
185
319
  registerConfigEntityRoutes(app, createDependencies(vi.fn()));
186
320
  await request(app).get('/api/config/catalog/providers').expect(502);
187
321
 
188
322
  const failingApp = express();
189
- createOpencodeClient.mockReturnValue({ config: { providers: vi.fn(async () => { throw new Error('upstream unavailable'); }) } });
323
+ createOpencodeClient.mockReturnValue({
324
+ provider: { list: vi.fn(async () => { throw new Error('upstream unavailable'); }) },
325
+ model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
326
+ });
190
327
  registerConfigEntityRoutes(failingApp, createDependencies(vi.fn()));
191
328
  await request(failingApp).get('/api/config/catalog/providers').expect(502);
192
329
  });
@@ -194,7 +331,8 @@ describe('provider catalog route', () => {
194
331
  it('returns 502 for SDK error envelopes that include data', async () => {
195
332
  const app = express();
196
333
  createOpencodeClient.mockReturnValue({
197
- config: { providers: vi.fn(async () => ({ error: { message: 'upstream sentinel' }, data: { providers: [], default: {} } })) },
334
+ provider: { list: vi.fn(async () => { throw new Error('upstream sentinel'); }) },
335
+ model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
198
336
  });
199
337
  registerConfigEntityRoutes(app, createDependencies(vi.fn()));
200
338
 
@@ -0,0 +1,105 @@
1
+ import { test, expect } from 'vitest';
2
+ import { OpenCode } from '@opencode/client';
3
+ import { spawn } from 'node:child_process';
4
+ import { once } from 'node:events';
5
+ import { randomBytes } from 'node:crypto';
6
+ import fs from 'node:fs/promises';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+
10
+ // Opt-in, isolated real V2 server. No user config, credentials, model calls,
11
+ // shared service registration, or npm plugin installs are used.
12
+ test.skipIf(!process.env.OPENCHAMBER_TEST_OPENCODE_BINARY)('V2 applies credentials, global config and plugin changes in the same process', async () => {
13
+ const temporary = path.join(os.tmpdir(), 'opencode');
14
+ await fs.mkdir(temporary, { recursive: true });
15
+ const root = await fs.mkdtemp(path.join(temporary, 'config-hot-reload-'));
16
+ const config = path.join(root, 'config', 'opencode');
17
+ const directory = path.join(root, 'project');
18
+ await fs.mkdir(config, { recursive: true });
19
+ await fs.mkdir(directory, { recursive: true });
20
+ const password = randomBytes(24).toString('hex');
21
+ const child = spawn(process.env.OPENCHAMBER_TEST_OPENCODE_BINARY, ['serve', '--hostname', '127.0.0.1', '--port', '0'], {
22
+ cwd: directory,
23
+ env: {
24
+ PATH: process.env.PATH,
25
+ HOME: root,
26
+ TMPDIR: temporary,
27
+ XDG_CONFIG_HOME: path.join(root, 'config'),
28
+ XDG_DATA_HOME: path.join(root, 'data'),
29
+ XDG_STATE_HOME: path.join(root, 'state'),
30
+ XDG_CACHE_HOME: path.join(root, 'cache'),
31
+ OPENCODE_CONFIG_DIR: config,
32
+ OPENCODE_SERVER_PASSWORD: password,
33
+ OPENCODE_DISABLE_MODELS_FETCH: '1',
34
+ },
35
+ stdio: ['ignore', 'pipe', 'pipe'],
36
+ });
37
+ let output = '';
38
+ let spawnError;
39
+ child.on('error', (error) => { spawnError = error; });
40
+ child.stdout.on('data', (data) => { output = (output + data).slice(-8192); });
41
+ child.stderr.on('data', () => undefined);
42
+ const closed = once(child, 'close').catch(() => undefined);
43
+ const until = async (read, accepts, label) => {
44
+ const deadline = Date.now() + 20_000;
45
+ while (Date.now() < deadline) {
46
+ if (spawnError) throw spawnError;
47
+ if (child.exitCode !== null) throw new Error(`Isolated OpenCode exited during ${label}`);
48
+ const value = await read();
49
+ if (accepts(value)) return value;
50
+ await new Promise((resolve) => setTimeout(resolve, 100));
51
+ }
52
+ throw new Error(`Timed out waiting for ${label}`);
53
+ };
54
+ const write = async (info) => {
55
+ const file = path.join(config, 'opencode.json');
56
+ await fs.writeFile(`${file}.tmp`, JSON.stringify(info));
57
+ await fs.rename(`${file}.tmp`, file);
58
+ };
59
+ try {
60
+ const origin = await until(
61
+ async () => output.match(/(?:server listening on|opencode server listening on) (http:\/\/[^\s]+)/)?.[1],
62
+ Boolean, 'listener',
63
+ );
64
+ const client = OpenCode.make({
65
+ baseUrl: origin,
66
+ headers: { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` },
67
+ fetch: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(15_000) }),
68
+ });
69
+ const location = { directory };
70
+ await client.command.list({ location });
71
+ const pid = child.pid;
72
+ const commands = { 'hot-reload-probe': { template: 'Do not execute anything.', description: 'one' } };
73
+ await write({ commands });
74
+ await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe'), 'global command create');
75
+ await write({ commands: { 'hot-reload-probe': { ...commands['hot-reload-probe'], description: 'two' } } });
76
+ await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe' && entry.description === 'two'), 'command update');
77
+
78
+ await write({ commands, plugins: ['*', '-opencode.config.command'] });
79
+ await until(() => client.command.list({ location }), (result) => !result.data.some((entry) => entry.name === 'hot-reload-probe'), 'plugin disable');
80
+ await write({ commands });
81
+ await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe'), 'plugin re-enable');
82
+
83
+ await client.integration.connect.key({ integrationID: 'anthropic', key: 'isolated-test-placeholder', location });
84
+ const integration = await until(() => client.integration.get({ integrationID: 'anthropic', location }),
85
+ (result) => result.data.connections.some((connection) => connection.type === 'credential'), 'credential connection');
86
+ for (const connection of integration.data.connections) {
87
+ if (connection.type === 'credential') await client.credential.remove({ credentialID: connection.id });
88
+ }
89
+ await until(() => client.integration.get({ integrationID: 'anthropic', location }),
90
+ (result) => result.data.connections.length === 0, 'credential removal');
91
+ await write({ commands, mcp: { servers: { 'hot-reload-mcp': { type: 'local', command: ['unused-test-command'], disabled: true } } } });
92
+ await until(() => client.mcp.list({ location }), (result) => result.data.some((server) => server.name === 'hot-reload-mcp'), 'MCP config add');
93
+ await write({});
94
+ await until(() => client.mcp.list({ location }), (result) => !result.data.some((server) => server.name === 'hot-reload-mcp'), 'MCP config removal');
95
+ await until(() => client.command.list({ location }), (result) => !result.data.some((entry) => entry.name === 'hot-reload-probe'), 'command removal');
96
+ expect(child.pid).toBe(pid);
97
+ expect(child.exitCode).toBeNull();
98
+ } finally {
99
+ child.kill('SIGTERM');
100
+ const kill = setTimeout(() => child.kill('SIGKILL'), 5000);
101
+ await closed;
102
+ clearTimeout(kill);
103
+ await fs.rm(root, { recursive: true, force: true });
104
+ }
105
+ }, 120_000);