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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) 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-B_JQoreg.js} +1 -1
  7. package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
  8. package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
  10. package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
  13. package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
  15. package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
  16. package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
  17. package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
  20. package/dist/assets/SettingsView-BRxELjow.js +25 -0
  21. package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
  22. package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
  24. package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
  25. package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
  26. package/dist/assets/collapsible-DXbDynN4.js +3 -0
  27. package/dist/assets/context-menu-CqolsLCm.js +1 -0
  28. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
  29. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
  30. package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
  31. package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
  32. package/dist/assets/es-CN3E_zXj.js +15 -0
  33. package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
  34. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
  35. package/dist/assets/fr-9zZvQZD2.js +13 -0
  36. package/dist/assets/gitApi-CM42qUed.js +10 -0
  37. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
  38. package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
  39. package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
  40. package/dist/assets/ja-Dnjk_82N.js +15 -0
  41. package/dist/assets/ko-BxCEE99C.js +15 -0
  42. package/dist/assets/main-BILgBI3B.js +169 -0
  43. package/dist/assets/main-BeveiW6F.js +2 -0
  44. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  45. package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
  46. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  47. package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
  48. package/dist/assets/miniChat-BgrzM3u7.js +2 -0
  49. package/dist/assets/mobile-DtYoQE8C.js +2 -0
  50. package/dist/assets/multirun-jy8dEI96.js +4 -0
  51. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
  52. package/dist/assets/pl-CNNsWnPM.js +15 -0
  53. package/dist/assets/platform-BQghHRfp.js +6 -0
  54. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  55. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
  56. package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
  57. package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
  58. package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
  59. package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
  60. package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
  61. package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
  62. package/dist/assets/src-CLN7Z-zx.css +2 -0
  63. package/dist/assets/store-DLCiZaA6.js +15 -0
  64. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
  65. package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
  66. package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
  67. package/dist/assets/uk-rwQrxeYM.js +15 -0
  68. package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
  69. package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
  70. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  71. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
  72. package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
  73. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
  74. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
  75. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  76. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +2 -0
  77. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  78. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  79. package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.js} +2 -2
  80. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  81. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  82. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  83. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  84. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  85. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  86. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  87. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  88. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  89. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  90. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  91. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  92. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  93. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  94. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  95. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  96. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  97. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  98. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  99. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  100. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  101. package/dist/index.html +13 -13
  102. package/dist/mini-chat.html +13 -13
  103. package/dist/mobile.html +13 -13
  104. package/package.json +2 -2
  105. package/server/index.js +392 -20
  106. package/server/lib/assistants/DOCUMENTATION.md +26 -21
  107. package/server/lib/assistants/assign.js +2 -1
  108. package/server/lib/assistants/contact-store.js +37 -19
  109. package/server/lib/assistants/contracts.js +11 -1
  110. package/server/lib/assistants/projection-history.test.js +454 -0
  111. package/server/lib/assistants/routes.js +11 -5
  112. package/server/lib/assistants/service.js +850 -294
  113. package/server/lib/assistants/service.test.js +588 -274
  114. package/server/lib/conversations/DOCUMENTATION.md +5 -5
  115. package/server/lib/conversations/service.js +87 -57
  116. package/server/lib/conversations/service.test.js +132 -100
  117. package/server/lib/conversations/validation.js +11 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +51 -5
  120. package/server/lib/event-stream/runtime.js +57 -20
  121. package/server/lib/event-stream/runtime.test.js +112 -0
  122. package/server/lib/llm/DOCUMENTATION.md +95 -84
  123. package/server/lib/llm/catalog.js +111 -45
  124. package/server/lib/llm/catalog.test.js +121 -59
  125. package/server/lib/llm/completions.js +41 -12
  126. package/server/lib/llm/completions.test.js +45 -4
  127. package/server/lib/llm/generate.js +516 -453
  128. package/server/lib/llm/generate.test.js +705 -484
  129. package/server/lib/llm/routes.js +8 -4
  130. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  131. package/server/lib/message-queue/opencode-adapter.js +191 -104
  132. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  133. package/server/lib/message-queue/runtime.js +15 -2
  134. package/server/lib/message-queue/runtime.test.js +6 -6
  135. package/server/lib/message-queue/worker.js +7 -1
  136. package/server/lib/notifications/runtime.js +5 -0
  137. package/server/lib/opencode/DOCUMENTATION.md +103 -16
  138. package/server/lib/opencode/agent-document.d.ts +28 -0
  139. package/server/lib/opencode/agent-document.js +280 -0
  140. package/server/lib/opencode/agent-document.test.js +137 -0
  141. package/server/lib/opencode/agents.js +96 -0
  142. package/server/lib/opencode/auth-state-runtime.js +5 -0
  143. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  144. package/server/lib/opencode/config-entity-routes.js +111 -102
  145. package/server/lib/opencode/config-entity-routes.test.js +123 -10
  146. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  147. package/server/lib/opencode/ensure-cli.js +238 -0
  148. package/server/lib/opencode/ensure-cli.test.js +119 -0
  149. package/server/lib/opencode/env-runtime.js +208 -111
  150. package/server/lib/opencode/env-runtime.test.js +211 -14
  151. package/server/lib/opencode/feature-routes-runtime.js +80 -5
  152. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  153. package/server/lib/opencode/http-server-connections.js +98 -0
  154. package/server/lib/opencode/http-server-connections.test.js +321 -0
  155. package/server/lib/opencode/lifecycle.js +539 -95
  156. package/server/lib/opencode/lifecycle.test.js +698 -6
  157. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  158. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  159. package/server/lib/opencode/network-runtime.js +28 -16
  160. package/server/lib/opencode/network-runtime.test.js +68 -4
  161. package/server/lib/opencode/openchamber-routes.js +7 -1
  162. package/server/lib/opencode/opencode2-pin.js +139 -0
  163. package/server/lib/opencode/opencode2-pin.test.js +88 -0
  164. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
  165. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  166. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  167. package/server/lib/opencode/plugin-compatibility.js +15 -0
  168. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  169. package/server/lib/opencode/plugin-routes.js +3 -18
  170. package/server/lib/opencode/plugin-routes.test.js +9 -9
  171. package/server/lib/opencode/plugins.js +4 -3
  172. package/server/lib/opencode/plugins.test.js +12 -0
  173. package/server/lib/opencode/provider-catalog.js +58 -12
  174. package/server/lib/opencode/provider-catalog.test.js +48 -0
  175. package/server/lib/opencode/proxy.js +368 -28
  176. package/server/lib/opencode/proxy.test.js +338 -0
  177. package/server/lib/opencode/routes.js +303 -76
  178. package/server/lib/opencode/routes.test.js +315 -50
  179. package/server/lib/opencode/runtime-contract.js +401 -0
  180. package/server/lib/opencode/runtime-contract.test.js +214 -0
  181. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  182. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  183. package/server/lib/opencode/server-utils-runtime.js +35 -0
  184. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  185. package/server/lib/opencode/shared-service.js +147 -0
  186. package/server/lib/opencode/shared-service.test.js +93 -0
  187. package/server/lib/opencode/shared.js +3 -3
  188. package/server/lib/opencode/shutdown-runtime.js +15 -6
  189. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  190. package/server/lib/opencode/skill-routes.js +26 -40
  191. package/server/lib/opencode/skill-routes.test.js +71 -7
  192. package/server/lib/opencode/skills.test.js +15 -0
  193. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  194. package/server/lib/opencode/static-routes-runtime.js +3 -1
  195. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  196. package/server/lib/opencode/v1-migration-gate.js +157 -0
  197. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  198. package/server/lib/opencode/v2-client.js +60 -0
  199. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  200. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  201. package/server/lib/opencode/watcher.js +2 -1
  202. package/server/lib/opencode/watcher.test.js +1 -1
  203. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  204. package/server/lib/permission-auto-accept/runtime.js +14 -7
  205. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  206. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  207. package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
  208. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  209. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  210. package/server/lib/scheduled-tasks/routes.js +27 -0
  211. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  212. package/server/lib/scheduled-tasks/runtime.js +381 -310
  213. package/server/lib/scheduled-tasks/runtime.test.js +600 -309
  214. package/server/lib/session-goal/DOCUMENTATION.md +173 -22
  215. package/server/lib/session-goal/capability.js +57 -0
  216. package/server/lib/session-goal/capability.test.js +33 -0
  217. package/server/lib/session-goal/routes.js +240 -2
  218. package/server/lib/session-goal/routes.test.js +211 -0
  219. package/server/lib/session-goal/runtime.js +1617 -113
  220. package/server/lib/session-goal/runtime.test.js +1500 -70
  221. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  222. package/server/lib/session-index/event-ingest.js +89 -16
  223. package/server/lib/session-index/event-ingest.test.js +73 -0
  224. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  225. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  226. package/server/lib/session-index/routes.js +63 -10
  227. package/server/lib/session-index/routes.test.js +8 -8
  228. package/server/lib/session-index/service.js +7 -1
  229. package/server/lib/session-index/sync-runtime.js +171 -41
  230. package/server/lib/session-index/sync-runtime.test.js +164 -9
  231. package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
  232. package/server/lib/session-metadata/routes.js +193 -0
  233. package/server/lib/session-metadata/routes.test.js +156 -0
  234. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  235. package/server/lib/session-metadata/session-archive.js +654 -0
  236. package/server/lib/session-metadata/session-archive.test.js +359 -0
  237. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  238. package/server/lib/session-metadata/session-metadata-store.js +375 -0
  239. package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
  240. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  241. package/server/lib/session-metadata/session-projection.js +255 -0
  242. package/server/lib/session-metadata/session-projection.test.js +64 -0
  243. package/server/lib/session-metadata/system-session.js +55 -0
  244. package/server/lib/session-metadata/system-session.test.js +55 -0
  245. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  246. package/server/lib/session-title/native-runtime.test.js +167 -0
  247. package/server/lib/session-title/runtime.js +67 -42
  248. package/server/lib/session-title/session-access.js +46 -0
  249. package/server/lib/session-title/session-access.test.js +91 -0
  250. package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
  251. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  252. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  253. package/server/lib/session-turn-pages/routes.js +123 -75
  254. package/server/lib/session-turn-pages/routes.test.js +566 -0
  255. package/server/lib/session-turn-pages/service.js +6 -2
  256. package/server/lib/session-turn-pages/service.test.js +34 -0
  257. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  258. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  259. package/server/lib/small-model/DOCUMENTATION.md +101 -146
  260. package/server/lib/small-model/catalog.js +84 -54
  261. package/server/lib/small-model/catalog.test.js +111 -9
  262. package/server/lib/small-model/custom-api.js +68 -0
  263. package/server/lib/small-model/custom-api.test.js +64 -0
  264. package/server/lib/small-model/index.js +125 -241
  265. package/server/lib/small-model/resolve.js +35 -120
  266. package/server/lib/small-model/resolve.test.js +20 -110
  267. package/server/lib/small-model/routes.js +1 -3
  268. package/server/lib/small-model/summary-settings.test.js +157 -253
  269. package/server/opencode-proxy.test.js +63 -12
  270. package/dist/assets/AssistantView-BUveAgc4.js +0 -3
  271. package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
  272. package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
  273. package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
  274. package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
  275. package/dist/assets/SettingsView-PkXIavr7.js +0 -25
  276. package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
  277. package/dist/assets/collapsible-D5Phaykh.js +0 -1
  278. package/dist/assets/context-menu-CubSDW2g.js +0 -1
  279. package/dist/assets/es-DA3MqOo0.js +0 -15
  280. package/dist/assets/fr-Dbz0CdIA.js +0 -13
  281. package/dist/assets/gitApi-DXyC4MiV.js +0 -7
  282. package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
  283. package/dist/assets/ja-DumanVQA.js +0 -15
  284. package/dist/assets/ko-C0IkiUjQ.js +0 -15
  285. package/dist/assets/main-BPS5ylib.js +0 -2
  286. package/dist/assets/main-Cpd9VWYs.js +0 -169
  287. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  288. package/dist/assets/miniChat-B8_KLWiY.js +0 -2
  289. package/dist/assets/mobile-CtktKrXn.js +0 -2
  290. package/dist/assets/multirun-CYfP9Cfm.js +0 -4
  291. package/dist/assets/pl-B9No6MQp.js +0 -15
  292. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  293. package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
  294. package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
  295. package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
  296. package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
  297. package/dist/assets/src-rS3zCcsC.css +0 -2
  298. package/dist/assets/store-5B_LDnLM.js +0 -15
  299. package/dist/assets/uk-C_1KkqyP.js +0 -15
  300. package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
  301. package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
  302. package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
  303. package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
  304. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  305. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  306. package/dist/assets/zh-CN-uWILgBdt.js +0 -15
  307. package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
  308. package/server/lib/small-model/call.js +0 -427
  309. package/server/lib/small-model/call.test.js +0 -382
  310. package/server/lib/small-model/opencode-session.js +0 -339
  311. package/server/lib/small-model/opencode-session.test.js +0 -177
@@ -6,6 +6,7 @@ import {
6
6
  MESSAGE_STREAM_GLOBAL_WS_PATH,
7
7
  MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS,
8
8
  sendMessageStreamWsEvent,
9
+ sendMessageStreamWsFrame,
9
10
  } from './protocol.js';
10
11
  import { createGlobalMessageStreamHub } from './global-hub.js';
11
12
  import { createGlobalMessageStreamWsBridge } from './global-ws-bridge.js';
@@ -62,6 +63,9 @@ export function createMessageStreamWsRuntime({
62
63
  buildOpenCodeUrl,
63
64
  getOpenCodeAuthHeaders,
64
65
  processForwardedEventPayload,
66
+ projectOutboundSessionPayload = null,
67
+ /** Await Host metadata ready before directory WS attaches upstream. */
68
+ ensureSessionMetadataReady = null,
65
69
  wsClients,
66
70
  triggerHealthCheck,
67
71
  heartbeatIntervalMs = MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS,
@@ -104,29 +108,62 @@ export function createMessageStreamWsRuntime({
104
108
  const includeReasoning = shouldIncludeReasoning(requestUrl.searchParams.get('includeReasoning'));
105
109
  const reasoningFilter = includeReasoning ? null : createReasoningOutboundFilter();
106
110
 
107
- if (isGlobalStream) {
108
- globalBridge.accept(socket, {
111
+ // Host metadata readiness gates *both* global and directory streams before
112
+ // accept/hub.start — otherwise globalBridge.accept → hub.start bypasses the
113
+ // gate and lifecycle frames can fan out while the store is still unknown.
114
+ void (async () => {
115
+ if (typeof ensureSessionMetadataReady === 'function') {
116
+ try {
117
+ const ready = await ensureSessionMetadataReady();
118
+ if (!ready) {
119
+ sendMessageStreamWsFrame(socket, {
120
+ type: 'error',
121
+ message: 'session metadata is unavailable',
122
+ code: 'session_metadata_unavailable',
123
+ retryable: true,
124
+ });
125
+ socket.close(1013, 'session metadata unavailable');
126
+ return;
127
+ }
128
+ } catch {
129
+ sendMessageStreamWsFrame(socket, {
130
+ type: 'error',
131
+ message: 'session metadata is unavailable',
132
+ code: 'session_metadata_unavailable',
133
+ retryable: true,
134
+ });
135
+ socket.close(1013, 'session metadata unavailable');
136
+ return;
137
+ }
138
+ }
139
+
140
+ if (socket.readyState !== 1) return;
141
+
142
+ if (isGlobalStream) {
143
+ globalBridge.accept(socket, {
144
+ requestedLastEventId,
145
+ reasoningFilter,
146
+ });
147
+ return;
148
+ }
149
+
150
+ acceptDirectoryMessageStreamWsConnection({
151
+ socket,
109
152
  requestedLastEventId,
153
+ requestedDirectory,
110
154
  reasoningFilter,
155
+ buildOpenCodeUrl,
156
+ getOpenCodeAuthHeaders,
157
+ processForwardedEventPayload,
158
+ projectOutboundSessionPayload,
159
+ wsClients,
160
+ triggerHealthCheck,
161
+ heartbeatIntervalMs,
162
+ upstreamStallTimeoutMs,
163
+ upstreamReconnectDelayMs,
164
+ fetchImpl,
111
165
  });
112
- return;
113
- }
114
-
115
- acceptDirectoryMessageStreamWsConnection({
116
- socket,
117
- requestedLastEventId,
118
- requestedDirectory,
119
- reasoningFilter,
120
- buildOpenCodeUrl,
121
- getOpenCodeAuthHeaders,
122
- processForwardedEventPayload,
123
- wsClients,
124
- triggerHealthCheck,
125
- heartbeatIntervalMs,
126
- upstreamStallTimeoutMs,
127
- upstreamReconnectDelayMs,
128
- fetchImpl,
129
- });
166
+ })();
130
167
  });
131
168
 
132
169
  const upgradeHandler = (req, socket, head) => {
@@ -126,6 +126,118 @@ describe('event stream broadcaster', () => {
126
126
  });
127
127
  });
128
128
 
129
+ describe('message stream websocket runtime Host metadata readiness', () => {
130
+ it('does not accept/start global hub while metadata load is pending', async () => {
131
+ const server = new EventEmitter();
132
+ const wsClients = new Set();
133
+ let fetchCalls = 0;
134
+ let releaseReady;
135
+ const readyGate = new Promise((resolve) => { releaseReady = resolve; });
136
+
137
+ const runtime = createMessageStreamWsRuntime({
138
+ server,
139
+ uiAuthController: null,
140
+ isRequestOriginAllowed: async () => true,
141
+ rejectWebSocketUpgrade() {},
142
+ buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
143
+ getOpenCodeAuthHeaders: () => ({}),
144
+ processForwardedEventPayload() {},
145
+ ensureSessionMetadataReady: async () => {
146
+ await readyGate;
147
+ return true;
148
+ },
149
+ wsClients,
150
+ upstreamReconnectDelayMs: 0,
151
+ fetchImpl: async (_url, options) => {
152
+ fetchCalls += 1;
153
+ return createSseResponse({
154
+ signal: options.signal,
155
+ holdOpen: true,
156
+ blocks: [
157
+ 'id: evt-1\ndata: {"type":"session.updated","properties":{"info":{"id":"ses_1","time":{}}}}\n\n',
158
+ ],
159
+ });
160
+ },
161
+ });
162
+
163
+ const socket = new FakeSocket();
164
+ runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
165
+ await new Promise((resolve) => setTimeout(resolve, 15));
166
+
167
+ // Still pending → no hub.start → no upstream fetch, no accept frames yet.
168
+ expect(fetchCalls).toBe(0);
169
+ expect(socket.closeCalls).toEqual([]);
170
+ expect(socket.sent.some((f) => f.type === 'event')).toBe(false);
171
+
172
+ releaseReady(true);
173
+ await new Promise((resolve) => setTimeout(resolve, 30));
174
+
175
+ expect(fetchCalls).toBe(1);
176
+ expect(socket.sent.some((f) => f.type === 'event'
177
+ && f.payload?.type === 'session.updated')).toBe(true);
178
+
179
+ socket.close();
180
+ await runtime.close();
181
+ });
182
+
183
+ it('rejects global connection when metadata ready fails', async () => {
184
+ const server = new EventEmitter();
185
+ const runtime = createMessageStreamWsRuntime({
186
+ server,
187
+ uiAuthController: null,
188
+ isRequestOriginAllowed: async () => true,
189
+ rejectWebSocketUpgrade() {},
190
+ buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
191
+ getOpenCodeAuthHeaders: () => ({}),
192
+ processForwardedEventPayload() {},
193
+ ensureSessionMetadataReady: async () => false,
194
+ wsClients: new Set(),
195
+ upstreamReconnectDelayMs: 0,
196
+ fetchImpl: async () => {
197
+ throw new Error('upstream must not start');
198
+ },
199
+ });
200
+
201
+ const socket = new FakeSocket();
202
+ runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
203
+ await new Promise((resolve) => setTimeout(resolve, 15));
204
+
205
+ expect(socket.sent.some((f) => f.type === 'error'
206
+ && f.code === 'session_metadata_unavailable'
207
+ && f.retryable === true)).toBe(true);
208
+ expect(socket.closeCalls.some((c) => c.code === 1013)).toBe(true);
209
+
210
+ await runtime.close();
211
+ });
212
+
213
+ it('gates directory stream the same way as global', async () => {
214
+ const server = new EventEmitter();
215
+ const runtime = createMessageStreamWsRuntime({
216
+ server,
217
+ uiAuthController: null,
218
+ isRequestOriginAllowed: async () => true,
219
+ rejectWebSocketUpgrade() {},
220
+ buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
221
+ getOpenCodeAuthHeaders: () => ({}),
222
+ processForwardedEventPayload() {},
223
+ ensureSessionMetadataReady: async () => false,
224
+ wsClients: new Set(),
225
+ upstreamReconnectDelayMs: 0,
226
+ fetchImpl: async () => {
227
+ throw new Error('upstream must not start');
228
+ },
229
+ });
230
+
231
+ const socket = new FakeSocket();
232
+ runtime.wsServer.emit('connection', socket, { url: '/api/event/ws?directory=/repo' });
233
+ await new Promise((resolve) => setTimeout(resolve, 15));
234
+
235
+ expect(socket.sent.some((f) => f.code === 'session_metadata_unavailable')).toBe(true);
236
+ expect(socket.closeCalls.some((c) => c.code === 1013)).toBe(true);
237
+ await runtime.close();
238
+ });
239
+ });
240
+
129
241
  describe('message stream websocket runtime', () => {
130
242
  it('filters reasoning events per connection when includeReasoning=false', async () => {
131
243
  const server = new EventEmitter();
@@ -5,103 +5,104 @@ only model path the Assistant harness (`pi-agent-core` `streamFn`) talks to.
5
5
 
6
6
  ## Contract
7
7
 
8
- - `GET /api/openchamber/llm/models` — connected `{providerID, modelID}` catalog
9
- from OpenCode `GET /provider` (`connected`) plus `GET /config/providers`.
10
- Plugin adapter formats stay inside OpenCode; this route does not parse them.
8
+ - `GET /api/openchamber/llm/models` — available `{providerID, modelID}` catalog
9
+ from official `@opencode/client` `provider.list` + `model.list`
10
+ (`{ location, data }`). `ModelInfo.id` is the external modelID used in
11
+ generate/session refs (`ModelInfo.modelID` is a separate internal field).
12
+ Vision prefers `capabilities.input.image === true` (SDK object shape), then
13
+ v2 list `capabilities.input` containing `"image"`, then legacy
14
+ modalities/input/attachment. Catalog loads use a bounded AbortSignal
15
+ (default 8s) so a pending provider call cannot stall a contact lane.
11
16
  - `POST /api/openchamber/llm/chat/completions` — `{ model, messages, stream?,
12
17
  providerID?, modelID?, variant? }` → OpenAI `chat.completion` JSON.
13
- This **public** gateway is **non-streaming**. Bundled OpenCode 1.18.4 generate
14
- (`POST /generate` when present, otherwise throwaway `session.promptAsync`
15
- plus idle wait) returns a full assistant turn. `stream: true` is rejected with
18
+ This **public** gateway is **non-streaming**. `stream: true` is rejected with
16
19
  `validation_error` (HTTP 400). Do not emit fake SSE after the fact on this
17
- route (never typewrite a completed string into tokens over HTTP).
18
- The contact UI may stream later via a **server-internal** path only (below).
20
+ route. The contact UI may stream later via a **server-internal** path only.
19
21
 
20
22
  `model` may be `providerID/modelID` or a bare `modelID` paired with `providerID`.
21
23
 
22
24
  `variant` is an optional OpenCode model variant string. Null, omitted, or empty
23
25
  values preserve the provider default. Other types fail validation before a model
24
- call. A selected variant is forwarded unchanged to sessionless generate and to
25
- the throwaway `session.promptAsync` body. OpenCode owns provider-specific variant
26
+ call. A selected variant is forwarded unchanged on the throwaway session's
27
+ `model` ref (both paths). OpenCode owns provider-specific variant
26
28
  semantics. Variant selection changes inference configuration only; completion
27
29
  output continues to include text parts and keeps reasoning parts private.
28
30
 
29
- ## Internals (verified on bundled `@opencode-ai/sdk` 1.18.4)
30
-
31
- The 1.18.4 client exposes `GET /provider`, `GET /config/providers`, and
32
- `session.prompt` / `session.promptAsync`. It does **not** expose
33
- `POST /api/generate` or any other sessionless generate method.
34
-
35
- 1. Probe `POST /generate` (and SDK `generate` if present). Use it only when
36
- the response is JSON (`Content-Type` or a JSON object body). SPA / OpenCode
37
- HTML `200 <!doctype` is not generate — fall through to the throwaway path.
38
- Bundled 1.18.4 has no sessionless generate.
39
- 2. Otherwise create a throwaway archived OpenCode session under a hidden
40
- `openchamber-llm` agent whose frontmatter uses OpenCode 1.18
41
- `permission: { "*": deny }` (not legacy `action`/`resource`/`effect` lists —
42
- those load without a terminal `*` deny). `session.create` also sets
43
- `permission: [{ permission: '*', pattern: '*', action: 'deny' }]` (SDK
44
- PermissionRuleset; verified to round-trip on the session object). Deny every
45
- catalog tool via `client.tool.ids()` → `{ [id]: false }` on `promptAsync`;
46
- a `tool.ids` SDK/transport error fails generate (never silent empty deny).
47
- Send messages as `system` + user text via `session.promptAsync` (v2
48
- `session.prompt` only forwards `{ id, prompt, delivery, resume }` and drops
49
- `model`/`parts` — that produced empty assistant text and a 502). Contact
50
- file parts reuse the existing OpenCode `{ type: 'file', mime, url, filename? }`
51
- delivery shape (data URLs in the contact SQLite store — not a second
52
- attachment store) and are forwarded on `promptAsync` only when the connected
53
- catalog marks that model as image-capable (`modalities.input`, `input`, or
54
- `attachment`). Non-vision models (for example deepseek-v4-flash) keep the
55
- `[image: …]` description and any text-file bytes, and skip image data URLs so
56
- generate cannot stall on unsupported vision parts. Non-image text files are
57
- also inlined into the flattened prompt. Wait for idle via `session.status` +
58
- `session.messages`; a deterministic `session.messages` SDK error (for example
59
- HTTP 400) fails generate immediately. The generator deadline is a **stall**
60
- timer (`GENERATE_TIMEOUT_MS` = 90s), not a wall-clock cap. Busy / retry
61
- session status, an incomplete assistant message, and throwaway
62
- `message.part.delta` tokens reset the stall timer — a live model must not
63
- look like "no response".
64
- Then delete the session and surface
65
- delete SDK errors in diagnostics without erasing a successful text result.
66
- Throwaway sessions use `metadata.openchamber.llm.purpose = 'chat-completions'`
67
- (and archived immediately) so sidebar visibility, session-index, session-title,
68
- and notification/push fanout treat them as system-owned — same contract as
69
- non-empty `smallModel.purpose`. This is a text generator only — never the
70
- contact transcript and never a coding SessionPrompt loop. Upstream
71
- `info.error.message` is forwarded on 502.
72
- `ensureLlmTempDirectory` keeps one process-wide throwaway root (`openchamber-llm-…`).
73
- Concurrent first callers share a single `mkdtemp` inflight; every caller still
74
- writes/refreshes `.opencode/agent/<name>.md` after settle so a mid-flight ensure
75
- cannot skip a newer agent body. Concurrent warm refreshes serialize through one
76
- write chain (last committed body wins; no interleaved partial writes).
77
- `stopLlmTempDirectory` clears the singleton for shutdown/tests.
31
+ ## Internals (official `@opencode/client`)
32
+
33
+ Credentials stay in OpenCode. This module does not read `auth.json`, does not
34
+ call Anthropic/OpenAI/plugin SDKs, and does not use the `openai` npm package.
35
+
36
+ ### Text path (no vision image bytes)
37
+
38
+ `session.generate` inside a throwaway session owned by the hidden deny-all
39
+ `openchamber-text` agent (same temp root, same deny-all verification and
40
+ `session.create` / `session.remove` lifecycle as the attachment path below):
41
+
42
+ 1. Ensure `.opencode/agent/openchamber-text.md` and verify its final
43
+ permission rule is deny-all before creating the session.
44
+ 2. `session.create({ location, agent: 'openchamber-text', model, metadata })`.
45
+ 3. `session.generate({ sessionID, prompt })` → `{ text }`. One model call with
46
+ the session's provider context, no tool loop, no messages written. System
47
+ text is prepended into `prompt`.
48
+ 4. Always `session.remove` in `finally` (interrupt first on abort/timeout).
49
+
50
+ `generate.text` (sessionless) is not used: session-scoped providers such as
51
+ OpenCode Go reject calls without a session (`x-opencode-session`).
52
+ `onTextDelta` is skipped honestly (no fake typewriter).
53
+
54
+ OpenCode loads a fresh location's config lazily, so `agent.get` can report the
55
+ agent as missing on the first read. Deny-all verification polls within a
56
+ bounded window (5s) before failing.
57
+
58
+ ### Attachment / vision path
59
+
60
+ When the catalog marks the model vision-capable and the request includes image
61
+ data-URL file parts:
62
+
63
+ 1. Ensure the throwaway temp directory agent markdown (`openchamber-llm`,
64
+ permissions array ending in `action:* resource:* effect:deny`).
65
+ 2. `agent.get({ agentID: 'openchamber-llm', location })` and require the **final**
66
+ permissions rule to be deny-all. Failure → `llm_attachment_generation_unavailable`
67
+ **before** any `session.prompt`. Title is never treated as permission isolation.
68
+ 3. `session.create({ location, agent, model: { id, providerID, variant? },
69
+ metadata: { openchamber: { llm: { purpose: 'chat-completions' } } } })`
70
+ (bare session id). Host `persistSessionMetadata` then commits the same
71
+ isolation patch; `onSystemSessionPersisted` drops the throwaway session
72
+ from the sidebar index. Create-time `metadata` is a hint only.
73
+ 4. Optional `session.instructions.entry.put` for the system prompt.
74
+ 5. Subscribe for `session.text.delta` (`data.sessionID` / `assistantMessageID` /
75
+ `ordinal` / `delta`) before prompt; unsubscribe in `finally`.
76
+ 6. `session.prompt({ sessionID, text, files: [{ uri, name? }], delivery })`.
77
+ 7. `session.wait` then `message.list({ order: 'desc' })`; verify newest assistant
78
+ `finish` / `error`; extract text content.
79
+ 8. On timeout/abort: `session.interrupt({ continue: false })` and always
80
+ `session.remove` in `finally`.
81
+
82
+ Non-vision models keep the `[image: …]` description strategy and never forward
83
+ image bytes. Data URLs are strictly validated; a single attachment is capped at
84
+ 20 MiB (plus existing inline text-file limits). An external/temp workspace that
85
+ OpenCode cannot see fails explicitly (`llm_attachment_generation_unavailable`).
86
+
87
+ `ensureLlmTempDirectory` keeps one process-wide throwaway root (`openchamber-llm-…`).
88
+ Concurrent first callers share a single `mkdtemp` inflight; every caller still
89
+ writes/refreshes `.opencode/agent/<name>.md` after settle so a mid-flight ensure
90
+ cannot skip a newer agent body. Concurrent warm refreshes serialize through one
91
+ write chain (last committed body wins; no interleaved partial writes).
92
+ `stopLlmTempDirectory` clears the singleton for shutdown/tests.
93
+
94
+ There is no `/generate` probe, no `tool.ids` deny map, and no `promptAsync` path.
95
+ There is no `config.providers` catalog path.
78
96
 
79
97
  ### Internal token callback (in-process only)
80
98
 
81
99
  `generateOpenCodeText` / `createChatCompletion` accept optional
82
- `onTextDelta(text: string)` and `globalEventHub` (the shared
83
- `globalMessageStreamHub` / `subscribeEvent` surface).
84
-
85
- - **Throwaway session path:** after archive, before `promptAsync`, subscribe to
86
- the hub. Forward real OpenCode `message.part.delta` events whose
87
- `properties.sessionID` matches this throwaway session, `field` is `text`
88
- (or omitted), and `messageID` matches the first assistant message locked for
89
- that generate. Still wait for idle and return the **full** text (strip/parse
90
- needs the complete string). Unsubscribe in `finally` so failed generates
91
- cannot leak listeners.
92
- - **Sessionless `/generate` JSON path:** cannot emit live deltas — skip
93
- `onTextDelta` honestly (no fake typewriter of the completed string).
94
- - **HTTP `POST .../chat/completions`:** does not pass these options and still
95
- rejects `stream: true`. Token deltas never leave the server as SSE on that
96
- route.
100
+ `onTextDelta(text: string)` and `globalEventHub`.
97
101
 
98
- Bundled OpenCode 1.18.4 emits `message.part.delta` on the global event stream
99
- for normal session prompts (same events the UI transcript already consumes).
100
- Throwaway LLM sessions use the same `session.promptAsync` path, so they are
101
- expected to emit the same delta events when the model streams tokens.
102
-
103
- Credentials stay in OpenCode. This module does not read `auth.json`, does not
104
- call Anthropic/OpenAI/plugin SDKs, and does not use the `openai` npm package.
102
+ - **Attachment session path:** real `session.text.delta` tokens for this session.
103
+ - **`session.generate` text path:** cannot emit live deltas — skip `onTextDelta`.
104
+ - **HTTP `POST .../chat/completions`:** does not pass these options and still
105
+ rejects `stream: true`.
105
106
 
106
107
  No connected provider → `no_provider` (HTTP 400). Upstream failure is never
107
108
  an empty success. The 502 body includes `{ error, message }`.
@@ -117,6 +118,16 @@ application tool catalog and actual results.
117
118
  The Assistant contact harness owns system prompt, OpenChamber transcript,
118
119
  bubble splitting, and OpenChamber API tools (`assign_session`). Those tools
119
120
  deliver through contact **cards**, not this completions payload. The gateway
120
- stays a text generator: OpenCode coding tools stay denied.
121
-
122
- Internal completion/generate calls accept an optional AbortSignal from the contact continuation. It is combined with the stall generator deadline and passed to upstream requests and settle polling. Cancellation still runs throwaway-session cleanup; it must not turn a late model response into a new tool operation.
121
+ stays a text generator: OpenCode coding tools stay denied on both paths via the
122
+ verified agent permissions ruleset.
123
+
124
+ The gateway is also the single OpenCode-backed path for server utility calls
125
+ (`small-model`: commit messages, session titles, goal audits, scheduled-task
126
+ distill, TTS summaries, PR descriptions). No server module calls provider APIs
127
+ directly with OpenCode credentials.
128
+
129
+ Internal completion/generate calls accept an optional AbortSignal from the
130
+ contact continuation. It is combined with the generator deadline and passed to
131
+ upstream requests. Cancellation still runs throwaway-session cleanup
132
+ (`interrupt` + `remove`); it must not turn a late model response into a new
133
+ tool operation.
@@ -7,8 +7,10 @@ const listIncludesImage = (value) => Array.isArray(value)
7
7
 
8
8
  /**
9
9
  * Vision capability projection.
10
- * Prefer authoritative OpenCode SDK Model.capabilities (`input.image` / `attachment`),
11
- * then fall back to legacy modalities/input/attachment shapes used by older fixtures.
10
+ * Prefer authoritative OpenCode Model.capabilities (`input.image` boolean /
11
+ * `attachment`, or v2 list `input` containing `"image"`), then fall back to
12
+ * legacy modalities/input/attachment shapes used by older fixtures.
13
+ * Do not treat model titles or unrelated flags as vision isolation.
12
14
  */
13
15
  export function modelAcceptsImages(model) {
14
16
  if (!isRecord(model)) return false;
@@ -16,6 +18,7 @@ export function modelAcceptsImages(model) {
16
18
  if (isRecord(model.capabilities.input) && typeof model.capabilities.input.image === 'boolean') {
17
19
  return model.capabilities.input.image === true;
18
20
  }
21
+ if (listIncludesImage(model.capabilities.input)) return true;
19
22
  if (model.capabilities.attachment === true) return true;
20
23
  }
21
24
  if (isRecord(model.modalities) && listIncludesImage(model.modalities.input)) return true;
@@ -24,49 +27,76 @@ export function modelAcceptsImages(model) {
24
27
  }
25
28
 
26
29
  /**
27
- * Merge OpenCode `GET /provider` (connected ids) with `GET /config/providers`
28
- * (model catalog). Do not interpret plugin-specific provider configs.
30
+ * Project official v2 provider.list + model.list into the LLM gateway catalog.
31
+ *
32
+ * ModelInfo.id is the external modelID used in generate/session model refs.
33
+ * ModelInfo.modelID is a separate internal field and must not be preferred.
29
34
  *
30
35
  * @param {{
31
- * connected?: unknown,
32
36
  * providers?: unknown,
37
+ * models?: unknown,
33
38
  * }} source
34
39
  */
35
40
  export function projectConnectedModels(source) {
36
- const connected = new Set(
37
- Array.isArray(source?.connected)
38
- ? source.connected.map(stringID).filter(Boolean)
39
- : [],
40
- );
41
- const models = [];
42
- const providers = [];
43
- const list = Array.isArray(source?.providers) ? source.providers : [];
44
- for (const provider of list) {
41
+ const providersIn = Array.isArray(source?.providers) ? source.providers : [];
42
+ const modelsIn = Array.isArray(source?.models) ? source.models : [];
43
+
44
+ const providerMeta = new Map();
45
+ for (const provider of providersIn) {
45
46
  if (!isRecord(provider)) continue;
46
47
  const providerID = stringID(provider.id);
47
- if (!providerID || !connected.has(providerID)) continue;
48
- const providerModels = [];
49
- const rawModels = isRecord(provider.models) ? Object.values(provider.models) : [];
50
- for (const model of rawModels) {
51
- if (!isRecord(model)) continue;
52
- const modelID = stringID(model.id);
53
- if (!modelID) continue;
54
- const entry = {
55
- providerID,
56
- modelID,
57
- name: stringID(model.name) || modelID,
58
- acceptsImages: modelAcceptsImages(model),
59
- };
60
- models.push(entry);
61
- providerModels.push({ id: modelID, name: entry.name });
62
- }
63
- providers.push({
48
+ if (!providerID) continue;
49
+ providerMeta.set(providerID, {
64
50
  id: providerID,
65
51
  name: stringID(provider.name) || providerID,
52
+ });
53
+ }
54
+
55
+ const models = [];
56
+ const modelsByProvider = new Map();
57
+ for (const model of modelsIn) {
58
+ if (!isRecord(model)) continue;
59
+ const providerID = stringID(model.providerID);
60
+ // External model id for session model refs.
61
+ const modelID = stringID(model.id);
62
+ if (!providerID || !modelID) continue;
63
+ if (providerMeta.size > 0 && !providerMeta.has(providerID)) continue;
64
+ const name = stringID(model.name) || modelID;
65
+ const entry = {
66
+ providerID,
67
+ modelID,
68
+ name,
69
+ acceptsImages: modelAcceptsImages(model),
70
+ };
71
+ models.push(entry);
72
+ if (!modelsByProvider.has(providerID)) modelsByProvider.set(providerID, []);
73
+ modelsByProvider.get(providerID).push({ id: modelID, name });
74
+ }
75
+
76
+ const connected = [];
77
+ const providers = [];
78
+ const providerIDs = providerMeta.size > 0
79
+ ? [...providerMeta.keys()]
80
+ : [...modelsByProvider.keys()];
81
+ for (const providerID of providerIDs) {
82
+ const meta = providerMeta.get(providerID) || { id: providerID, name: providerID };
83
+ const providerModels = modelsByProvider.get(providerID) || [];
84
+ if (providerModels.length === 0 && providerMeta.size > 0) {
85
+ // Provider listed with zero models is still "connected" but contributes no entries.
86
+ connected.push(providerID);
87
+ providers.push({ id: meta.id, name: meta.name, models: [] });
88
+ continue;
89
+ }
90
+ if (providerModels.length === 0) continue;
91
+ connected.push(providerID);
92
+ providers.push({
93
+ id: meta.id,
94
+ name: meta.name,
66
95
  models: providerModels,
67
96
  });
68
97
  }
69
- return { connected: [...connected], providers, models };
98
+
99
+ return { connected, providers, models };
70
100
  }
71
101
 
72
102
  export function parseModelRef(model, providerID, modelID) {
@@ -92,7 +122,7 @@ export function isConnectedModel(catalog, providerID, modelID) {
92
122
  return catalog.models.some((entry) => entry.providerID === providerID && entry.modelID === modelID);
93
123
  }
94
124
 
95
- /** Bound provider/config catalog loads so a pending SDK call cannot stall a contact lane. */
125
+ /** Bound provider/model catalog loads so a pending SDK call cannot stall a contact lane. */
96
126
  export const CONNECTED_CATALOG_TIMEOUT_MS = 8_000;
97
127
 
98
128
  const isAbortError = (error) => (
@@ -101,18 +131,41 @@ const isAbortError = (error) => (
101
131
  || (typeof error?.message === 'string' && /aborted|timed out/i.test(error.message))
102
132
  );
103
133
 
134
+ const resolveCatalogLocation = (options) => {
135
+ if (!options || typeof options !== 'object') return undefined;
136
+ if (isRecord(options.location)) return options.location;
137
+ if (options.directory || options.workspace) {
138
+ return {
139
+ ...(options.directory ? { directory: options.directory } : {}),
140
+ ...(options.workspace ? { workspace: options.workspace } : {}),
141
+ };
142
+ }
143
+ return undefined;
144
+ };
145
+
104
146
  /**
105
- * Load the connected catalog from an OpenCode SDK client.
147
+ * Load the connected catalog from an official `@opencode/client`.
148
+ * Uses model.list + provider.list only (no config.providers).
106
149
  * Failure is distinct from a successful empty catalog.
107
- * Uses a bounded AbortSignal (default 8s) on provider.list + config.providers.
150
+ *
151
+ * Real client returns `{ location, data }` and throws declared JSON errors
152
+ * (no `{ error }` envelope). Uses a bounded AbortSignal (default 8s).
108
153
  *
109
154
  * @param {object} client
110
- * @param {{ signal?: AbortSignal, timeoutMs?: number }} [options]
155
+ * @param {{
156
+ * location?: { directory?: string, workspace?: string },
157
+ * directory?: string,
158
+ * workspace?: string,
159
+ * signal?: AbortSignal,
160
+ * timeoutMs?: number,
161
+ * }} [options]
111
162
  */
112
163
  export async function loadConnectedCatalog(client, options = {}) {
113
164
  const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
114
165
  ? Math.trunc(options.timeoutMs)
115
166
  : CONNECTED_CATALOG_TIMEOUT_MS;
167
+ const location = resolveCatalogLocation(options);
168
+ const request = location ? { location } : undefined;
116
169
  const parentSignal = options.signal;
117
170
  const controller = new AbortController();
118
171
  const onParentAbort = () => {
@@ -139,21 +192,34 @@ export async function loadConnectedCatalog(client, options = {}) {
139
192
 
140
193
  const requestOptions = { signal: controller.signal };
141
194
  try {
142
- const listed = await client.provider.list({}, requestOptions);
143
- if (listed?.error || !isRecord(listed?.data)) {
144
- const error = new Error('OpenCode provider list is unavailable');
145
- error.code = 'upstream_error';
146
- throw error;
195
+ let providersResult;
196
+ let modelsResult;
197
+ try {
198
+ [providersResult, modelsResult] = await Promise.all([
199
+ client.provider.list(request, requestOptions),
200
+ client.model.list(request, requestOptions),
201
+ ]);
202
+ } catch (error) {
203
+ if (controller.signal.aborted || isAbortError(error)) {
204
+ throw error;
205
+ }
206
+ const upstream = new Error(
207
+ typeof error?.message === 'string' && error.message.trim()
208
+ ? error.message
209
+ : 'OpenCode provider catalog is unavailable',
210
+ );
211
+ upstream.code = 'upstream_error';
212
+ upstream.cause = error;
213
+ throw upstream;
147
214
  }
148
- const configured = await client.config.providers({}, requestOptions);
149
- if (configured?.error || !isRecord(configured?.data)) {
215
+ if (!Array.isArray(providersResult?.data) || !Array.isArray(modelsResult?.data)) {
150
216
  const error = new Error('OpenCode provider catalog is unavailable');
151
217
  error.code = 'upstream_error';
152
218
  throw error;
153
219
  }
154
220
  return projectConnectedModels({
155
- connected: listed.data.connected,
156
- providers: configured.data.providers,
221
+ providers: providersResult.data,
222
+ models: modelsResult.data,
157
223
  });
158
224
  } catch (error) {
159
225
  if (controller.signal.aborted || isAbortError(error)) {