@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
@@ -1,6 +1,13 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
 
3
+ import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
3
4
  import { registerSessionTurnPageRoutes } from './routes.js';
5
+ import {
6
+ projectSessionMessage as projectSessionMessageRecord,
7
+ projectSessionMessageRecords,
8
+ } from './session-message-projection.js';
9
+
10
+ vi.mock('../opencode/v2-client.js', () => ({ makeOpenCodeV2Client: vi.fn() }));
4
11
 
5
12
  const registry = () => {
6
13
  const routes = new Map();
@@ -30,6 +37,85 @@ const response = () => ({
30
37
 
31
38
  const ROUTE = '/api/openchamber/sessions/:sessionID/messages';
32
39
 
40
+ describe('projectSessionMessageRecord (host SessionMessage → {info, parts})', () => {
41
+ it('drops idle control and projects two-turn wire without duplicate text', () => {
42
+ const records = projectSessionMessageRecords([
43
+ {
44
+ id: 'msg_u1',
45
+ type: 'user',
46
+ time: { created: 1 },
47
+ text: 'UI-E2E-FINAL-BUILD',
48
+ sessionID: 'ses_1',
49
+ },
50
+ {
51
+ id: 'msg_a1',
52
+ type: 'assistant',
53
+ time: { created: 2, completed: 3 },
54
+ sessionID: 'ses_1',
55
+ agent: 'build',
56
+ model: { id: 'glm-5.3-flash', providerID: 'zai-coding-plan', variant: 'default' },
57
+ content: [
58
+ { type: 'reasoning', text: 'think' },
59
+ { type: 'text', text: 'FINAL_BUILD_OK' },
60
+ ],
61
+ finish: 'stop',
62
+ rawFinish: 'stop',
63
+ },
64
+ {
65
+ id: 'msg_u2',
66
+ type: 'user',
67
+ time: { created: 4 },
68
+ text: 'UI-E2E-FINAL-FOLLOWUP',
69
+ sessionID: 'ses_1',
70
+ },
71
+ {
72
+ id: 'msg_a2',
73
+ type: 'assistant',
74
+ time: { created: 5, completed: 6 },
75
+ sessionID: 'ses_1',
76
+ agent: 'build',
77
+ model: { id: 'glm-5.3-flash', providerID: 'zai-coding-plan', variant: 'default' },
78
+ text: 'FINAL_FOLLOWUP_OK',
79
+ content: [
80
+ { type: 'reasoning', text: 'think2' },
81
+ { type: 'text', text: 'FINAL_FOLLOWUP_OK' },
82
+ ],
83
+ finish: 'stop',
84
+ },
85
+ {
86
+ id: 'msg_idle',
87
+ type: 'idle',
88
+ outcome: 'succeeded',
89
+ time: { created: 7 },
90
+ },
91
+ ]);
92
+
93
+ expect(records.map((row) => [row.info.role, row.info.id])).toEqual([
94
+ ['user', 'msg_u1'],
95
+ ['assistant', 'msg_a1'],
96
+ ['user', 'msg_u2'],
97
+ ['assistant', 'msg_a2'],
98
+ ]);
99
+ expect(projectSessionMessageRecord({
100
+ id: 'msg_idle2',
101
+ type: 'idle',
102
+ outcome: 'failed',
103
+ time: { created: 1 },
104
+ })).toBeNull();
105
+
106
+ const follow = records[3];
107
+ expect(follow.parts.filter((part) => part.type === 'text')).toHaveLength(1);
108
+ expect(follow.parts.find((part) => part.type === 'text').text).toBe('FINAL_FOLLOWUP_OK');
109
+ expect(follow.info.model).toEqual({
110
+ providerID: 'zai-coding-plan',
111
+ modelID: 'glm-5.3-flash',
112
+ variant: 'default',
113
+ });
114
+ expect(follow.info.type).toBeUndefined();
115
+ expect(follow.info.rawFinish).toBeUndefined();
116
+ });
117
+ });
118
+
33
119
  describe('registerSessionTurnPageRoutes', () => {
34
120
  it('registers GET /api/openchamber/sessions/:sessionID/messages', () => {
35
121
  const { app, route } = registry();
@@ -163,6 +249,59 @@ describe('registerSessionTurnPageRoutes', () => {
163
249
  expect(kept.body.records[0].parts.some((part) => part.type === 'reasoning')).toBe(true);
164
250
  });
165
251
 
252
+ it('normalizes native SessionMessage content before slim+reasoning so content cannot leak', async () => {
253
+ const loadPage = vi.fn(async () => ({
254
+ ok: true,
255
+ records: [{
256
+ id: 'msg_a1',
257
+ sessionID: 'ses_1',
258
+ type: 'assistant',
259
+ time: { created: 1, completed: 2 },
260
+ agent: 'build',
261
+ model: { providerID: 'p', id: 'm' },
262
+ tokens: { input: 1, output: 2, reasoning: 9 },
263
+ content: [
264
+ { type: 'reasoning', text: 'SECRET_REASONING_BODY' },
265
+ { type: 'text', text: 'visible-answer' },
266
+ {
267
+ type: 'tool',
268
+ name: 'bash',
269
+ id: 'call_1',
270
+ state: { status: 'completed', title: 'ran', output: 'HUGE_TOOL_OUTPUT' },
271
+ },
272
+ ],
273
+ }],
274
+ turnCount: 0,
275
+ cursor: null,
276
+ complete: true,
277
+ }));
278
+ const { app, route } = registry();
279
+ registerSessionTurnPageRoutes(app, { sessionTurnPageService: { loadPage } });
280
+
281
+ const stripped = response();
282
+ await route('GET', ROUTE)({
283
+ params: { sessionID: 'ses_1' },
284
+ query: { includeReasoning: 'false' },
285
+ headers: {},
286
+ }, stripped);
287
+
288
+ expect(stripped.statusCode).toBe(200);
289
+ const record = stripped.body.records[0];
290
+ expect(record.info.id).toBe('msg_a1');
291
+ expect(record.info.role).toBe('assistant');
292
+ expect(record.info.tokens.reasoning).toBe(9);
293
+ expect(record.info.content).toBeUndefined();
294
+ expect(record.parts.every((part) => part.type !== 'reasoning')).toBe(true);
295
+ expect(record.parts.some((part) => part.type === 'text' && part.text === 'visible-answer')).toBe(true);
296
+ const tool = record.parts.find((part) => part.type === 'tool');
297
+ expect(tool.slim).toBe(true);
298
+ expect(tool.state.output).toBeUndefined();
299
+ expect(stripped.body.partsProjection).toBe('slim-v1');
300
+ const serialized = JSON.stringify(stripped.body);
301
+ expect(serialized).not.toContain('SECRET_REASONING_BODY');
302
+ expect(serialized).not.toContain('HUGE_TOOL_OUTPUT');
303
+ });
304
+
166
305
  it('maps upstream service errors to an upstream HTTP status', async () => {
167
306
  const loadPage = vi.fn(async () => ({
168
307
  ok: false,
@@ -637,6 +776,285 @@ describe('registerSessionTurnPageRoutes', () => {
637
776
  resolveLoad?.();
638
777
  await pending;
639
778
  });
779
+
780
+ it('default fetch uses message.list SessionMessageInfo[] + cursor and projects {info, parts}', async () => {
781
+ const list = vi.fn(async () => ({
782
+ data: [
783
+ { id: 'msg_a1', type: 'assistant', time: { created: 2, completed: 3 }, content: [{ type: 'text', text: 'ok' }] },
784
+ { id: 'msg_u1', type: 'user', time: { created: 1 }, text: 'hi' },
785
+ ],
786
+ cursor: { previous: null, next: null },
787
+ }));
788
+ makeOpenCodeV2Client.mockReturnValue({
789
+ message: { list },
790
+ session: { messages: undefined, status: undefined, abort: undefined },
791
+ });
792
+ const { app, route } = registry();
793
+ registerSessionTurnPageRoutes(app, {
794
+ buildOpenCodeUrl: () => 'http://open.code/',
795
+ getOpenCodeAuthHeaders: () => ({ Authorization: 'secret' }),
796
+ });
797
+ const res = response();
798
+ await route('GET', ROUTE)({
799
+ params: { sessionID: 'ses_1' },
800
+ query: { turns: '1' },
801
+ headers: {},
802
+ }, res);
803
+
804
+ expect(list).toHaveBeenCalledWith({
805
+ sessionID: 'ses_1',
806
+ limit: 100,
807
+ order: 'desc',
808
+ }, expect.objectContaining({ signal: expect.any(Object) }));
809
+ expect(res.statusCode).toBe(200);
810
+ expect(res.body.complete).toBe(true);
811
+ expect(res.body.records).toEqual([
812
+ expect.objectContaining({
813
+ info: expect.objectContaining({ id: 'msg_u1', role: 'user' }),
814
+ parts: [expect.objectContaining({ type: 'text', text: 'hi' })],
815
+ }),
816
+ expect.objectContaining({
817
+ info: expect.objectContaining({ id: 'msg_a1', role: 'assistant' }),
818
+ parts: [expect.objectContaining({ type: 'text', text: 'ok' })],
819
+ }),
820
+ ]);
821
+ });
822
+
823
+ it('default fetch continues desc pages with cursor.next only, never order + cursor', async () => {
824
+ const list = vi.fn()
825
+ .mockResolvedValueOnce({
826
+ data: [{ id: 'msg_a2', type: 'assistant', time: { created: 4, completed: 5 }, content: [{ type: 'text', text: 'later' }] }],
827
+ cursor: { previous: 'raw_prev', next: 'raw_next' },
828
+ })
829
+ .mockResolvedValueOnce({
830
+ data: [
831
+ { id: 'msg_a1', type: 'assistant', time: { created: 2, completed: 3 }, content: [{ type: 'text', text: 'ok' }] },
832
+ { id: 'msg_u1', type: 'user', time: { created: 1 }, text: 'hi' },
833
+ ],
834
+ cursor: { previous: 'raw_prev_2', next: null },
835
+ });
836
+ makeOpenCodeV2Client.mockReturnValue({
837
+ message: { list },
838
+ session: { messages: undefined, status: undefined, abort: undefined },
839
+ });
840
+ const { app, route } = registry();
841
+ registerSessionTurnPageRoutes(app, {
842
+ buildOpenCodeUrl: () => 'http://open.code/',
843
+ getOpenCodeAuthHeaders: () => ({}),
844
+ });
845
+ const res = response();
846
+ await route('GET', ROUTE)({
847
+ params: { sessionID: 'ses_1' },
848
+ query: { turns: '1' },
849
+ headers: {},
850
+ }, res);
851
+
852
+ expect(list).toHaveBeenCalledTimes(2);
853
+ expect(list.mock.calls[0][0]).toEqual({ sessionID: 'ses_1', limit: 100, order: 'desc' });
854
+ expect(list.mock.calls[1][0]).toEqual({ sessionID: 'ses_1', limit: 100, cursor: 'raw_next' });
855
+ expect(res.statusCode).toBe(200);
856
+ expect(res.body.complete).toBe(true);
857
+ expect(res.body.records.map((record) => record.info.id)).toEqual(['msg_u1', 'msg_a1', 'msg_a2']);
858
+ });
859
+
860
+ it('drops v2 idle/control rows and does not double-expand text+content (FINAL-GATE cold reload)', async () => {
861
+ // Real wire shape from isolated backend hard-reload sessions:
862
+ // user + assistant(content) + idle outcome=succeeded, and assistants that
863
+ // occasionally carry both top-level text and content[].text.
864
+ const list = vi.fn(async () => ({
865
+ data: [
866
+ {
867
+ id: 'msg_idle_ok',
868
+ time: { created: 4 },
869
+ type: 'idle',
870
+ outcome: 'succeeded',
871
+ },
872
+ {
873
+ id: 'msg_a_follow',
874
+ time: { created: 3, completed: 4 },
875
+ type: 'assistant',
876
+ agent: 'build',
877
+ model: { id: 'glm-5.3-flash', providerID: 'zai-coding-plan', variant: 'default' },
878
+ // Both carriers present — must project exactly one text part.
879
+ text: 'FINAL_FOLLOWUP_OK',
880
+ content: [
881
+ { type: 'reasoning', text: 'think' },
882
+ { type: 'text', text: 'FINAL_FOLLOWUP_OK' },
883
+ ],
884
+ finish: 'stop',
885
+ rawFinish: 'stop',
886
+ cost: 0,
887
+ tokens: { input: 1, output: 1, reasoning: 1, cache: { read: 0, write: 0 } },
888
+ },
889
+ {
890
+ id: 'msg_u_follow',
891
+ time: { created: 2 },
892
+ type: 'user',
893
+ text: 'UI-E2E-FINAL-FOLLOWUP',
894
+ },
895
+ {
896
+ id: 'msg_a1',
897
+ time: { created: 1, completed: 2 },
898
+ type: 'assistant',
899
+ agent: 'build',
900
+ model: { id: 'glm-5.3-flash', providerID: 'zai-coding-plan', variant: 'default' },
901
+ content: [
902
+ { type: 'reasoning', text: 'first think' },
903
+ { type: 'text', text: 'FINAL_BUILD_OK' },
904
+ ],
905
+ finish: 'stop',
906
+ },
907
+ {
908
+ id: 'msg_u1',
909
+ time: { created: 0 },
910
+ type: 'user',
911
+ text: 'UI-E2E-FINAL-BUILD',
912
+ },
913
+ ],
914
+ cursor: { previous: null, next: null },
915
+ }));
916
+ makeOpenCodeV2Client.mockReturnValue({
917
+ message: { list },
918
+ session: { messages: undefined, status: undefined, abort: undefined },
919
+ });
920
+ const { app, route } = registry();
921
+ registerSessionTurnPageRoutes(app, {
922
+ buildOpenCodeUrl: () => 'http://open.code/',
923
+ getOpenCodeAuthHeaders: () => ({}),
924
+ });
925
+ const res = response();
926
+ await route('GET', ROUTE)({
927
+ params: { sessionID: 'ses_final_build' },
928
+ query: { turns: '2' },
929
+ headers: {},
930
+ }, res);
931
+
932
+ expect(res.statusCode).toBe(200);
933
+ expect(res.body.records.map((row) => row.info.role)).toEqual([
934
+ 'user',
935
+ 'assistant',
936
+ 'user',
937
+ 'assistant',
938
+ ]);
939
+ expect(res.body.records.map((row) => row.info.id)).toEqual([
940
+ 'msg_u1',
941
+ 'msg_a1',
942
+ 'msg_u_follow',
943
+ 'msg_a_follow',
944
+ ]);
945
+ // No idle / control leakage into ChatMessage props.
946
+ expect(res.body.records.some((row) => row.info.role === 'idle' || row.info.type === 'idle')).toBe(false);
947
+ expect(JSON.stringify(res.body)).not.toContain('"type":"idle"');
948
+
949
+ const firstAssistant = res.body.records[1];
950
+ expect(firstAssistant.info.agent).toBe('build');
951
+ expect(firstAssistant.info.modelID).toBe('glm-5.3-flash');
952
+ expect(firstAssistant.info.model).toEqual({
953
+ providerID: 'zai-coding-plan',
954
+ modelID: 'glm-5.3-flash',
955
+ variant: 'default',
956
+ });
957
+ expect(firstAssistant.info.type).toBeUndefined();
958
+ expect(firstAssistant.info.rawFinish).toBeUndefined();
959
+ const firstTexts = firstAssistant.parts.filter((part) => part.type === 'text');
960
+ expect(firstTexts).toHaveLength(1);
961
+ expect(firstTexts[0].text).toBe('FINAL_BUILD_OK');
962
+
963
+ const followAssistant = res.body.records[3];
964
+ const followTexts = followAssistant.parts.filter((part) => part.type === 'text');
965
+ expect(followTexts).toHaveLength(1);
966
+ expect(followTexts[0].text).toBe('FINAL_FOLLOWUP_OK');
967
+ expect(followAssistant.parts.filter((part) => part.type === 'reasoning')).toHaveLength(1);
968
+ });
969
+
970
+ it('keeps provider.auth 401 assistant error and drops idle failed control', async () => {
971
+ const list = vi.fn(async () => ({
972
+ data: [
973
+ { id: 'msg_idle_fail', time: { created: 3 }, type: 'idle', outcome: 'failed' },
974
+ {
975
+ id: 'msg_a_401',
976
+ time: { created: 2, completed: 3 },
977
+ type: 'assistant',
978
+ agent: 'build',
979
+ model: { id: 'deepseek-flash', providerID: 'deepseek', variant: 'default' },
980
+ content: null,
981
+ finish: 'error',
982
+ error: {
983
+ type: 'provider.auth',
984
+ message: 'Authentication Fails, Your api key: **** is invalid',
985
+ status: 401,
986
+ },
987
+ },
988
+ { id: 'msg_u_401', time: { created: 1 }, type: 'user', text: 'UI-E2E-FINAL-DS' },
989
+ ],
990
+ cursor: { previous: null, next: null },
991
+ }));
992
+ makeOpenCodeV2Client.mockReturnValue({
993
+ message: { list },
994
+ session: { messages: undefined, status: undefined, abort: undefined },
995
+ });
996
+ const { app, route } = registry();
997
+ registerSessionTurnPageRoutes(app, {
998
+ buildOpenCodeUrl: () => 'http://open.code/',
999
+ getOpenCodeAuthHeaders: () => ({}),
1000
+ });
1001
+ const res = response();
1002
+ await route('GET', ROUTE)({
1003
+ params: { sessionID: 'ses_ds' },
1004
+ query: { turns: '1' },
1005
+ headers: {},
1006
+ }, res);
1007
+
1008
+ expect(res.statusCode).toBe(200);
1009
+ expect(res.body.records).toHaveLength(2);
1010
+ expect(res.body.records.map((row) => row.info.role)).toEqual(['user', 'assistant']);
1011
+ const assistant = res.body.records[1];
1012
+ expect(assistant.info.finish).toBe('error');
1013
+ expect(assistant.info.error).toEqual({
1014
+ type: 'provider.auth',
1015
+ message: 'Authentication Fails, Your api key: **** is invalid',
1016
+ status: 401,
1017
+ });
1018
+ expect(assistant.parts).toEqual([]);
1019
+ });
1020
+
1021
+ it('default fetch throws on message.list failure and maps to upstream', async () => {
1022
+ makeOpenCodeV2Client.mockReturnValue({
1023
+ message: { list: vi.fn(async () => { throw Object.assign(new Error('upstream'), { status: 500 }); }) },
1024
+ });
1025
+ const { app, route } = registry();
1026
+ registerSessionTurnPageRoutes(app, {
1027
+ buildOpenCodeUrl: () => 'http://open.code/',
1028
+ getOpenCodeAuthHeaders: () => ({}),
1029
+ });
1030
+ const res = response();
1031
+ await route('GET', ROUTE)({
1032
+ params: { sessionID: 'ses_1' },
1033
+ query: { turns: '1' },
1034
+ headers: {},
1035
+ }, res);
1036
+ expect(res.statusCode).toBeGreaterThanOrEqual(502);
1037
+ expect(res.body).toMatchObject({ error: expect.stringMatching(/upstream/i) });
1038
+ });
1039
+
1040
+ it('default fetch rejects HeyAPI data/error shapes without a SessionMessageInfo array', async () => {
1041
+ makeOpenCodeV2Client.mockReturnValue({
1042
+ message: { list: vi.fn(async () => ({ error: { status: 500 }, data: undefined })) },
1043
+ });
1044
+ const { app, route } = registry();
1045
+ registerSessionTurnPageRoutes(app, {
1046
+ buildOpenCodeUrl: () => 'http://open.code/',
1047
+ getOpenCodeAuthHeaders: () => ({}),
1048
+ });
1049
+ const res = response();
1050
+ await route('GET', ROUTE)({
1051
+ params: { sessionID: 'ses_1' },
1052
+ query: { turns: '1' },
1053
+ headers: {},
1054
+ }, res);
1055
+ expect(res.statusCode).toBeGreaterThanOrEqual(502);
1056
+ expect(res.body.records).toBeUndefined();
1057
+ });
640
1058
  });
641
1059
 
642
1060
  const EXACT_MESSAGE_ROUTE = '/api/session/:sessionID/message/:messageID';
@@ -752,6 +1170,154 @@ describe('registerSessionTurnPageRoutes — exact message GET', () => {
752
1170
  expect(res.statusCode).toBe(404);
753
1171
  expect(res.body.code).toBe('not_found');
754
1172
  });
1173
+
1174
+ it('maps AbortError to 499 and does not fold it into upstream 502', async () => {
1175
+ const fetchExactMessage = vi.fn(async () => {
1176
+ const error = new Error('aborted');
1177
+ error.name = 'AbortError';
1178
+ throw error;
1179
+ });
1180
+ const { app, route } = registry();
1181
+ registerSessionTurnPageRoutes(app, {
1182
+ sessionTurnPageService: { loadPage: vi.fn() },
1183
+ fetchExactMessage,
1184
+ });
1185
+ const res = response();
1186
+ await route('GET', EXACT_MESSAGE_ROUTE)({
1187
+ params: { sessionID: 'ses_1', messageID: 'msg_1' },
1188
+ query: {},
1189
+ headers: {},
1190
+ }, res);
1191
+ expect(res.statusCode).toBe(499);
1192
+ expect(res.statusCode).not.toBe(502);
1193
+ });
1194
+ });
1195
+
1196
+ describe('registerSessionTurnPageRoutes — real OpenCode.make + fake HTTP', () => {
1197
+ const jsonResponse = (status, body) => ({
1198
+ ok: status >= 200 && status < 300,
1199
+ status,
1200
+ headers: {
1201
+ get: (name) => (String(name).toLowerCase() === 'content-type' ? 'application/json' : null),
1202
+ },
1203
+ json: async () => body,
1204
+ text: async () => JSON.stringify(body ?? null),
1205
+ arrayBuffer: async () => new TextEncoder().encode(JSON.stringify(body ?? null)).buffer,
1206
+ });
1207
+
1208
+ it('session.message MessageNotFoundError _tag maps to HTTP 404 via real client', async () => {
1209
+ const fetchImpl = vi.fn(async (url) => {
1210
+ const path = String(url);
1211
+ if (path.includes('/message/msg_missing')) {
1212
+ return jsonResponse(404, {
1213
+ _tag: 'MessageNotFoundError',
1214
+ sessionID: 'ses_1',
1215
+ messageID: 'msg_missing',
1216
+ message: 'Message not found',
1217
+ });
1218
+ }
1219
+ return jsonResponse(500, { _tag: 'UnknownError', message: 'unexpected' });
1220
+ });
1221
+ const { makeOpenCodeV2Client: realMake } = await vi.importActual('../opencode/v2-client.js');
1222
+ makeOpenCodeV2Client.mockImplementation(({ baseUrl, authHeaders }) =>
1223
+ realMake({ baseUrl, authHeaders, fetchImpl }));
1224
+
1225
+ const { app, route } = registry();
1226
+ registerSessionTurnPageRoutes(app, {
1227
+ sessionTurnPageService: { loadPage: vi.fn() },
1228
+ buildOpenCodeUrl: () => 'http://open.code/',
1229
+ getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic test' }),
1230
+ });
1231
+ const res = response();
1232
+ await route('GET', EXACT_MESSAGE_ROUTE)({
1233
+ params: { sessionID: 'ses_1', messageID: 'msg_missing' },
1234
+ query: {},
1235
+ headers: {},
1236
+ }, res);
1237
+
1238
+ expect(res.statusCode).toBe(404);
1239
+ expect(res.body.code).toBe('not_found');
1240
+ expect(fetchImpl).toHaveBeenCalled();
1241
+ });
1242
+
1243
+ it('exact message serializes native assistant with reasoning strip and L1 slim', async () => {
1244
+ const nativeAssistant = {
1245
+ id: 'msg_a1',
1246
+ type: 'assistant',
1247
+ time: { created: 10, completed: 20 },
1248
+ agent: 'build',
1249
+ model: { providerID: 'openai', id: 'gpt' },
1250
+ tokens: { input: 3, output: 4, reasoning: 7 },
1251
+ content: [
1252
+ { type: 'reasoning', text: 'PRIVATE_CHAIN' },
1253
+ { type: 'text', text: 'hello-world' },
1254
+ ],
1255
+ // Host may still see legacy summary on some mirrors; L1 must slim it.
1256
+ summary: {
1257
+ diffs: [{
1258
+ file: 'a.ts',
1259
+ status: 'modified',
1260
+ additions: 1,
1261
+ deletions: 0,
1262
+ patch: 'PATCH_BODY_SHOULD_NOT_LEAVE',
1263
+ }],
1264
+ },
1265
+ };
1266
+ const fetchImpl = vi.fn(async () => jsonResponse(200, { data: nativeAssistant }));
1267
+ const { makeOpenCodeV2Client: realMake } = await vi.importActual('../opencode/v2-client.js');
1268
+ makeOpenCodeV2Client.mockImplementation(({ baseUrl, authHeaders }) =>
1269
+ realMake({ baseUrl, authHeaders, fetchImpl }));
1270
+
1271
+ const { app, route } = registry();
1272
+ registerSessionTurnPageRoutes(app, {
1273
+ sessionTurnPageService: { loadPage: vi.fn() },
1274
+ buildOpenCodeUrl: () => 'http://open.code/',
1275
+ getOpenCodeAuthHeaders: () => ({}),
1276
+ });
1277
+
1278
+ const stripped = response();
1279
+ await route('GET', EXACT_MESSAGE_ROUTE)({
1280
+ params: { sessionID: 'ses_1', messageID: 'msg_a1' },
1281
+ query: { includeReasoning: 'false' },
1282
+ headers: {},
1283
+ }, stripped);
1284
+
1285
+ expect(stripped.statusCode).toBe(200);
1286
+ expect(stripped.body.info.id).toBe('msg_a1');
1287
+ expect(stripped.body.info.role).toBe('assistant');
1288
+ expect(stripped.body.info.content).toBeUndefined();
1289
+ expect(stripped.body.info.tokens.reasoning).toBe(7);
1290
+ expect(stripped.body.parts.every((part) => part.type !== 'reasoning')).toBe(true);
1291
+ expect(stripped.body.parts.some((part) => part.type === 'text' && part.text === 'hello-world')).toBe(true);
1292
+ expect(stripped.body.info.summary?.diffs?.[0]?.patch).toBeUndefined();
1293
+ expect(JSON.stringify(stripped.body)).not.toContain('PRIVATE_CHAIN');
1294
+ expect(JSON.stringify(stripped.body)).not.toContain('PATCH_BODY_SHOULD_NOT_LEAVE');
1295
+ });
1296
+
1297
+ it('preserves abort from real client transport instead of 502', async () => {
1298
+ const fetchImpl = vi.fn(async () => {
1299
+ const error = new Error('The operation was aborted');
1300
+ error.name = 'AbortError';
1301
+ throw error;
1302
+ });
1303
+ const { makeOpenCodeV2Client: realMake } = await vi.importActual('../opencode/v2-client.js');
1304
+ makeOpenCodeV2Client.mockImplementation(({ baseUrl, authHeaders }) =>
1305
+ realMake({ baseUrl, authHeaders, fetchImpl }));
1306
+
1307
+ const { app, route } = registry();
1308
+ registerSessionTurnPageRoutes(app, {
1309
+ sessionTurnPageService: { loadPage: vi.fn() },
1310
+ buildOpenCodeUrl: () => 'http://open.code/',
1311
+ getOpenCodeAuthHeaders: () => ({}),
1312
+ });
1313
+ const res = response();
1314
+ await route('GET', EXACT_MESSAGE_ROUTE)({
1315
+ params: { sessionID: 'ses_1', messageID: 'msg_1' },
1316
+ query: {},
1317
+ headers: {},
1318
+ }, res);
1319
+ expect(res.statusCode).toBe(499);
1320
+ });
755
1321
  });
756
1322
 
757
1323
  describe('registerSessionTurnPageRoutes — changes API', () => {
@@ -39,7 +39,7 @@ const hasPartType = (parts, type) =>
39
39
  Array.isArray(parts) && parts.some((part) => part && typeof part === 'object' && part.type === type);
40
40
 
41
41
  const isHostedSessionDivider = (record) => {
42
- const id = record?.info?.id;
42
+ const id = record?.info?.id ?? record?.id;
43
43
  return typeof id === 'string' && id.startsWith(ASSISTANT_SESSION_DIVIDER_PREFIX);
44
44
  };
45
45
 
@@ -53,6 +53,10 @@ export const isUserAuthoredTurnBoundary = (record) => {
53
53
  if (!record || typeof record !== 'object') return false;
54
54
  if (isHostedSessionDivider(record)) return false;
55
55
 
56
+ if (typeof record.type === 'string' && record.info == null) {
57
+ return record.type === 'user';
58
+ }
59
+
56
60
  const info = record.info ?? {};
57
61
  const role = typeof info.clientRole === 'string' ? info.clientRole : info.role;
58
62
  if (role !== 'user') return false;
@@ -94,7 +98,7 @@ export const selectTurnRecords = (timeline, turnLimit) => {
94
98
  };
95
99
 
96
100
  const recordId = (record) => {
97
- const id = record?.info?.id;
101
+ const id = record?.info?.id ?? record?.id;
98
102
  return typeof id === 'string' && id.length > 0 ? id : null;
99
103
  };
100
104
 
@@ -1241,3 +1241,37 @@ describe('createSessionTurnPageService', () => {
1241
1241
  });
1242
1242
  });
1243
1243
  });
1244
+
1245
+ describe('SessionMessageInfo turn pages', () => {
1246
+ const v2User = (id, extra = {}) => ({ id, type: 'user', time: { created: 1 }, text: 'hi', ...extra });
1247
+ const v2Assistant = (id, extra = {}) => ({
1248
+ id,
1249
+ type: 'assistant',
1250
+ time: { created: 2, completed: 3 },
1251
+ content: [{ type: 'text', text: 'ok' }],
1252
+ ...extra,
1253
+ });
1254
+
1255
+ it('counts SessionMessageInfo type=user as an authored boundary', () => {
1256
+ expect(isUserAuthoredTurnBoundary(v2User('msg_u1'))).toBe(true);
1257
+ expect(isUserAuthoredTurnBoundary(v2Assistant('msg_a1'))).toBe(false);
1258
+ expect(isUserAuthoredTurnBoundary({ id: 'msg_syn', type: 'synthetic', time: { created: 1 }, text: 'loop' })).toBe(false);
1259
+ expect(isUserAuthoredTurnBoundary({
1260
+ id: `oc_asst_session_divider:ses_old`,
1261
+ type: 'system',
1262
+ time: { created: 0 },
1263
+ })).toBe(false);
1264
+ });
1265
+
1266
+ it('aggregates SessionMessageInfo pages by id and type', async () => {
1267
+ const fetchPage = vi.fn(async () => ({
1268
+ records: [v2User('msg_u1'), v2Assistant('msg_a1')],
1269
+ nextCursor: null,
1270
+ complete: true,
1271
+ }));
1272
+ const service = createSessionTurnPageService({ fetchPage });
1273
+ const result = await service.loadPage({ sessionID: 'ses_1', turns: 1 });
1274
+ expect(result).toMatchObject({ ok: true, turnCount: 1, complete: true });
1275
+ expect(result.records.map((entry) => entry.id)).toEqual(['msg_u1', 'msg_a1']);
1276
+ });
1277
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Type surface for session-message-projection.js (consumed by the VS Code
3
+ * turn-page bridge). Runtime implementation stays in the .js module.
4
+ */
5
+
6
+ export type ProjectedSessionMessage = {
7
+ info: { id: string; role: string; [key: string]: unknown };
8
+ parts: unknown[];
9
+ };
10
+
11
+ export function projectSessionMessage(entry: unknown): ProjectedSessionMessage | null;
12
+
13
+ export function projectSessionMessageRecords(records: unknown): ProjectedSessionMessage[];