@vantran-se/openchamber-web 1.24.4 → 2.0.1

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 (367) hide show
  1. package/README.md +2 -8
  2. package/bin/lib/commands-connect-url.js +3 -8
  3. package/bin/lib/commands-tunnel.js +3 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
  5. package/dist/assets/DiffView-CjxUovbM.js +4 -0
  6. package/dist/assets/FilesView-DFWD0RQR.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-BWa7e5vw.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-3sGwqexh.js} +1 -1
  11. package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
  14. package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-DS-IMU1j.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
  17. package/dist/assets/PlanView-BCk_VobP.js +5 -0
  18. package/dist/assets/PluginPane-BPP1dbck.js +3 -0
  19. package/dist/assets/SettingsView-kMmrpn43.js +29 -0
  20. package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
  22. package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-lap5btCF.js} +2 -2
  26. package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-CwGJfJG-.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
  29. package/dist/assets/de-CndEg67T.js +10 -0
  30. package/dist/assets/es-DglvPlaC.js +16 -0
  31. package/dist/assets/fr-DejPCpbS.js +14 -0
  32. package/dist/assets/index-C2r64QfD.js +1 -0
  33. package/dist/assets/index-CLcM8HKx.css +1 -0
  34. package/dist/assets/index-CvLgnuNI.js +41 -0
  35. package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D47a6hMP.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-B-mGgeKl.js} +1 -1
  37. package/dist/assets/ja-CVpQawCd.js +16 -0
  38. package/dist/assets/ko-Bw446h9j.js +16 -0
  39. package/dist/assets/main-Bz4WWXbr.js +2 -0
  40. package/dist/assets/main-oreqDsOV.js +896 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-D2e5sp9y.js} +4 -4
  42. package/dist/assets/miniChat-D1v2gsBI.js +2 -0
  43. package/dist/assets/mobile-DBj3lCf2.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-BpeUOV91.js} +3 -3
  45. package/dist/assets/pl-BYNlPdhm.js +16 -0
  46. package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
  48. package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
  50. package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
  53. package/dist/assets/tr-DzmbxLy-.js +7 -0
  54. package/dist/assets/uk-C7vQnwro.js +16 -0
  55. package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CuUuakrt.js} +2 -2
  57. package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
  59. package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
  60. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
  61. package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
  62. package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
  63. package/dist/assets/{web-components-DxX8bZ1o.js → web-components-B6tKQyJN.js} +2 -2
  64. package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
  65. package/dist/assets/zh-TW-DtII5bDb.js +16 -0
  66. package/dist/index.html +13 -7
  67. package/dist/mini-chat.html +5 -5
  68. package/dist/mobile.html +5 -5
  69. package/package.json +6 -5
  70. package/server/index.d.ts +1 -0
  71. package/server/index.js +246 -91
  72. package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
  73. package/server/lib/agent-tool/runtime.js +135 -54
  74. package/server/lib/agent-tool/runtime.test.js +155 -87
  75. package/server/lib/browser-control/DOCUMENTATION.md +37 -2
  76. package/server/lib/browser-control/provider.js +217 -0
  77. package/server/lib/browser-control/provider.test.js +251 -0
  78. package/server/lib/context-obligatory/runtime.js +61 -50
  79. package/server/lib/context-obligatory/runtime.test.js +62 -162
  80. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  81. package/server/lib/event-stream/delta-coalescer.js +0 -0
  82. package/server/lib/event-stream/delta-coalescer.test.js +68 -57
  83. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  84. package/server/lib/event-stream/global-hub.js +10 -1
  85. package/server/lib/event-stream/global-hub.test.js +27 -26
  86. package/server/lib/event-stream/index.js +6 -0
  87. package/server/lib/event-stream/protocol.js +26 -19
  88. package/server/lib/event-stream/protocol.test.js +18 -7
  89. package/server/lib/event-stream/rebind.test.js +5 -5
  90. package/server/lib/event-stream/runtime.test.js +17 -15
  91. package/server/lib/event-stream/translate-v2.js +313 -0
  92. package/server/lib/event-stream/translate-v2.test.js +209 -0
  93. package/server/lib/event-stream/upstream-reader.test.js +16 -16
  94. package/server/lib/fs/DOCUMENTATION.md +4 -2
  95. package/server/lib/fs/byte-range.js +35 -0
  96. package/server/lib/fs/byte-range.test.js +31 -0
  97. package/server/lib/fs/routes.js +47 -42
  98. package/server/lib/fs/routes.test.js +123 -18
  99. package/server/lib/git/service.js +12 -0
  100. package/server/lib/github/DOCUMENTATION.md +1 -1
  101. package/server/lib/github/pr-status.js +22 -2
  102. package/server/lib/github/pr-status.test.js +27 -14
  103. package/server/lib/guests/DOCUMENTATION.md +13 -0
  104. package/server/lib/guests/catalog.js +12 -0
  105. package/server/lib/guests/catalog.test.js +33 -0
  106. package/server/lib/guests/routes.js +10 -1
  107. package/server/lib/guests/service.js +192 -14
  108. package/server/lib/guests/service.test.js +201 -1
  109. package/server/lib/guests/surface.js +630 -0
  110. package/server/lib/guests/surface.test.js +486 -0
  111. package/server/lib/markdown-image-grants/routes.js +18 -5
  112. package/server/lib/markdown-image-grants/routes.test.js +9 -6
  113. package/server/lib/message-queue/DOCUMENTATION.md +33 -22
  114. package/server/lib/message-queue/runtime.js +155 -100
  115. package/server/lib/message-queue/runtime.test.js +168 -82
  116. package/server/lib/notifications/DOCUMENTATION.md +28 -0
  117. package/server/lib/notifications/control-stream.http.test.js +68 -0
  118. package/server/lib/notifications/emitter-runtime.js +15 -0
  119. package/server/lib/notifications/emitter-runtime.test.js +32 -0
  120. package/server/lib/notifications/push-runtime.js +5 -1
  121. package/server/lib/notifications/push-runtime.test.js +2 -2
  122. package/server/lib/notifications/routes.js +1 -1
  123. package/server/lib/notifications/runtime.js +115 -26
  124. package/server/lib/notifications/template-runtime.js +13 -15
  125. package/server/lib/notifications/template-runtime.test.js +16 -9
  126. package/server/lib/notifications/trigger-runtime.test.js +187 -0
  127. package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
  128. package/server/lib/openchamber-control/actions.js +3 -2
  129. package/server/lib/openchamber-control/error.js +1 -1
  130. package/server/lib/openchamber-control/file-open.js +59 -0
  131. package/server/lib/openchamber-control/file-open.test.js +57 -0
  132. package/server/lib/openchamber-control/routes.test.js +12 -0
  133. package/server/lib/openchamber-control/service.js +102 -49
  134. package/server/lib/openchamber-control/service.test.js +84 -44
  135. package/server/lib/openchamber-sessions/archive-store.js +224 -0
  136. package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
  137. package/server/lib/openchamber-sessions/opencode-client.js +22 -0
  138. package/server/lib/openchamber-sessions/routes.js +398 -328
  139. package/server/lib/openchamber-sessions/routes.test.js +777 -740
  140. package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
  141. package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
  142. package/server/lib/opencode/DOCUMENTATION.md +449 -29
  143. package/server/lib/opencode/agents.js +233 -327
  144. package/server/lib/opencode/auth.js +41 -44
  145. package/server/lib/opencode/auth.test.js +92 -0
  146. package/server/lib/opencode/cli-upgrade.d.ts +4 -0
  147. package/server/lib/opencode/cli-upgrade.js +18 -0
  148. package/server/lib/opencode/cli-upgrade.test.js +36 -0
  149. package/server/lib/opencode/commands.js +164 -145
  150. package/server/lib/opencode/compatibility.d.ts +16 -0
  151. package/server/lib/opencode/compatibility.js +88 -0
  152. package/server/lib/opencode/compatibility.test.js +62 -0
  153. package/server/lib/opencode/config-entity-routes.js +58 -29
  154. package/server/lib/opencode/config-entity-v2.test.js +290 -0
  155. package/server/lib/opencode/config-mutation-response.js +17 -7
  156. package/server/lib/opencode/config-mutation-response.test.js +4 -7
  157. package/server/lib/opencode/config-paths.test.js +2 -2
  158. package/server/lib/opencode/config-v2.d.ts +183 -0
  159. package/server/lib/opencode/config-v2.js +801 -0
  160. package/server/lib/opencode/config-v2.test.js +466 -0
  161. package/server/lib/opencode/credential-db.js +144 -0
  162. package/server/lib/opencode/credential-db.test.js +104 -0
  163. package/server/lib/opencode/env-runtime.js +48 -9
  164. package/server/lib/opencode/env-runtime.test.js +45 -0
  165. package/server/lib/opencode/feature-routes-runtime.js +15 -5
  166. package/server/lib/opencode/lifecycle-process.test.js +7 -4
  167. package/server/lib/opencode/lifecycle.js +85 -30
  168. package/server/lib/opencode/lifecycle.test.js +80 -8
  169. package/server/lib/opencode/login-shell-env.js +27 -0
  170. package/server/lib/opencode/managed-config-file.js +119 -0
  171. package/server/lib/opencode/managed-config-file.test.js +152 -0
  172. package/server/lib/opencode/managed-plugin-config.js +33 -13
  173. package/server/lib/opencode/mcp.js +43 -149
  174. package/server/lib/opencode/network-runtime.js +6 -7
  175. package/server/lib/opencode/openchamber-routes.test.js +2 -5
  176. package/server/lib/opencode/plugin-routes.js +3 -3
  177. package/server/lib/opencode/plugin-routes.test.js +6 -24
  178. package/server/lib/opencode/plugins.js +91 -40
  179. package/server/lib/opencode/plugins.test.js +28 -15
  180. package/server/lib/opencode/providers.js +63 -83
  181. package/server/lib/opencode/providers.test.js +128 -59
  182. package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
  183. package/server/lib/opencode/proxy.js +179 -129
  184. package/server/lib/opencode/pwa-manifest-routes.js +4 -2
  185. package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
  186. package/server/lib/opencode/response-envelope.js +14 -0
  187. package/server/lib/opencode/response-envelope.test.js +23 -0
  188. package/server/lib/opencode/routes-upgrade.test.js +73 -116
  189. package/server/lib/opencode/routes.js +121 -428
  190. package/server/lib/opencode/server-utils-runtime.js +10 -4
  191. package/server/lib/opencode/session-activity.js +78 -0
  192. package/server/lib/opencode/session-runtime.js +21 -10
  193. package/server/lib/opencode/session-runtime.test.js +9 -9
  194. package/server/lib/opencode/settings-helpers.js +9 -9
  195. package/server/lib/opencode/settings-helpers.test.js +7 -12
  196. package/server/lib/opencode/settings-registry.json +6 -8
  197. package/server/lib/opencode/settings-runtime.js +18 -1
  198. package/server/lib/opencode/settings-runtime.test.js +31 -2
  199. package/server/lib/opencode/shared.js +54 -22
  200. package/server/lib/opencode/shared.test.js +8 -4
  201. package/server/lib/opencode/shutdown-runtime.js +54 -7
  202. package/server/lib/opencode/shutdown-runtime.test.js +224 -30
  203. package/server/lib/opencode/skill-routes.js +23 -24
  204. package/server/lib/opencode/skills.js +7 -5
  205. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
  206. package/server/lib/opencode/upgrade-capability.test.js +1 -1
  207. package/server/lib/opencode/v1-migration-topup.js +288 -0
  208. package/server/lib/opencode/v1-migration-topup.test.js +266 -0
  209. package/server/lib/opencode/v2-install.d.ts +8 -0
  210. package/server/lib/opencode/v2-install.js +158 -0
  211. package/server/lib/opencode/v2-install.test.js +124 -0
  212. package/server/lib/opencode/watcher.js +10 -3
  213. package/server/lib/opencode/watcher.test.js +17 -21
  214. package/server/lib/opencode/websearch-config.js +66 -0
  215. package/server/lib/package-manager.js +55 -68
  216. package/server/lib/package-manager.test.js +241 -75
  217. package/server/lib/permission-auto-accept/runtime.js +38 -22
  218. package/server/lib/permission-auto-accept/runtime.test.js +39 -17
  219. package/server/lib/quota/DOCUMENTATION.md +4 -0
  220. package/server/lib/quota/providers/xai.js +4 -4
  221. package/server/lib/relay/tunnel-host.js +4 -1
  222. package/server/lib/relay/tunnel-host.test.js +7 -0
  223. package/server/lib/routing/DOCUMENTATION.md +52 -21
  224. package/server/lib/routing/defaults.js +23 -6
  225. package/server/lib/routing/jev.js +17 -7
  226. package/server/lib/routing/routes.http.test.js +68 -33
  227. package/server/lib/routing/routes.js +65 -22
  228. package/server/lib/routing/runtime.js +145 -48
  229. package/server/lib/routing/runtime.test.js +79 -55
  230. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
  231. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
  232. package/server/lib/scheduled-tasks/runtime.js +81 -74
  233. package/server/lib/scheduled-tasks/runtime.test.js +53 -1
  234. package/server/lib/session-assist/DOCUMENTATION.md +8 -3
  235. package/server/lib/session-assist/context.js +122 -46
  236. package/server/lib/session-assist/context.test.js +117 -49
  237. package/server/lib/session-assist/runtime.js +52 -22
  238. package/server/lib/session-assist/runtime.test.js +145 -173
  239. package/server/lib/session-goal/DOCUMENTATION.md +23 -15
  240. package/server/lib/session-goal/create.js +9 -12
  241. package/server/lib/session-goal/create.test.js +27 -28
  242. package/server/lib/session-goal/runtime.js +164 -77
  243. package/server/lib/session-goal/runtime.test.js +271 -519
  244. package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
  245. package/server/lib/session-knowledge/runtime.js +59 -41
  246. package/server/lib/session-knowledge/runtime.test.js +90 -20
  247. package/server/lib/small-model/DOCUMENTATION.md +154 -208
  248. package/server/lib/small-model/client.js +113 -0
  249. package/server/lib/small-model/index.js +296 -158
  250. package/server/lib/small-model/index.test.js +423 -256
  251. package/server/lib/small-model/routes.js +1 -1
  252. package/server/lib/spaces/DOCUMENTATION.md +807 -0
  253. package/server/lib/spaces/code-in-bait.js +185 -0
  254. package/server/lib/spaces/code-in.js +641 -0
  255. package/server/lib/spaces/code-in.test.js +1048 -0
  256. package/server/lib/spaces/errors.js +8 -0
  257. package/server/lib/spaces/exec-http.js +94 -0
  258. package/server/lib/spaces/gatekeeper-channel.js +202 -0
  259. package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
  260. package/server/lib/spaces/gatekeeper-program.cjs +914 -0
  261. package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
  262. package/server/lib/spaces/hardening.js +457 -0
  263. package/server/lib/spaces/hardening.test.js +368 -0
  264. package/server/lib/spaces/host-git.js +127 -0
  265. package/server/lib/spaces/host-git.test.js +137 -0
  266. package/server/lib/spaces/labels.js +234 -0
  267. package/server/lib/spaces/labels.test.js +219 -0
  268. package/server/lib/spaces/layout.js +184 -0
  269. package/server/lib/spaces/layout.test.js +30 -0
  270. package/server/lib/spaces/manager.js +82 -0
  271. package/server/lib/spaces/manager.test.js +114 -0
  272. package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
  273. package/server/lib/spaces/places/contract-suite.js +156 -0
  274. package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
  275. package/server/lib/spaces/places/contract.test.js +6 -0
  276. package/server/lib/spaces/places/docker-engine.js +126 -0
  277. package/server/lib/spaces/places/docker-live-support.js +202 -0
  278. package/server/lib/spaces/places/docker-tools.js +186 -0
  279. package/server/lib/spaces/places/docker.js +587 -0
  280. package/server/lib/spaces/places/docker.test.js +1610 -0
  281. package/server/lib/spaces/places/escape-suite.js +1018 -0
  282. package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
  283. package/server/lib/spaces/places/fake-docker.js +375 -0
  284. package/server/lib/spaces/places/memory-place.js +103 -0
  285. package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
  286. package/server/lib/spaces/places/registry.js +43 -0
  287. package/server/lib/spaces/places/registry.test.js +50 -0
  288. package/server/lib/spaces/places/server.docker.live.test.js +225 -0
  289. package/server/lib/spaces/run-command.js +261 -0
  290. package/server/lib/spaces/run-command.test.js +396 -0
  291. package/server/lib/spaces/space-server.js +190 -0
  292. package/server/lib/spaces/space-server.test.js +335 -0
  293. package/server/lib/spaces/tools-filler.js +82 -0
  294. package/server/lib/spaces/tools-filler.test.js +148 -0
  295. package/server/lib/spaces/tools-pack.js +49 -0
  296. package/server/lib/spaces/tools-pack.test.js +57 -0
  297. package/server/lib/spaces/tools.js +138 -0
  298. package/server/lib/spaces/tools.test.js +153 -0
  299. package/server/lib/tts/openai-sdk.js +11 -0
  300. package/server/lib/tts/service.js +7 -5
  301. package/server/lib/tts/stt.js +3 -2
  302. package/server/lib/tunnels/DOCUMENTATION.md +2 -0
  303. package/server/lib/tunnels/routes.js +7 -0
  304. package/server/lib/tunnels/routes.test.js +55 -0
  305. package/server/lib/ui-auth/ui-auth.js +38 -2
  306. package/server/lib/ui-auth/ui-auth.test.js +42 -0
  307. package/server/lib/ui-auth/ui-passkeys.js +12 -10
  308. package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
  309. package/server/lib/walkthrough/pull-request.js +61 -8
  310. package/server/lib/walkthrough/pull-request.test.js +54 -1
  311. package/server/lib/walkthrough/routes.js +20 -0
  312. package/server/lib/walkthrough/routes.test.js +19 -0
  313. package/server/opencode-proxy.test.js +190 -166
  314. package/server/opencode-worktree-readiness.test.js +5 -4
  315. package/dist/assets/DiffView-B2CD9wC8.js +0 -4
  316. package/dist/assets/FilesView-C4vMFclc.js +0 -13
  317. package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
  320. package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
  321. package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
  322. package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
  323. package/dist/assets/SettingsView-CSinuH-1.js +0 -29
  324. package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
  326. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
  328. package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
  329. package/dist/assets/de-Bro6_zyl.js +0 -10
  330. package/dist/assets/es-Bb9XQZvd.js +0 -16
  331. package/dist/assets/fr-Dgfzn-IL.js +0 -14
  332. package/dist/assets/index-CtQNmqUL.js +0 -1
  333. package/dist/assets/index-DqaMWNrC.css +0 -1
  334. package/dist/assets/index-Dqc6USl3.js +0 -40
  335. package/dist/assets/ja-BYsakGFh.js +0 -16
  336. package/dist/assets/ko-BSaQz9aN.js +0 -16
  337. package/dist/assets/main-B62_pjL5.js +0 -2
  338. package/dist/assets/main-BEdcOI2P.js +0 -896
  339. package/dist/assets/miniChat-LT8Rqofr.js +0 -2
  340. package/dist/assets/mobile-CqarEynh.js +0 -2
  341. package/dist/assets/pl-OIviPdoj.js +0 -16
  342. package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
  344. package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
  346. package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
  347. package/dist/assets/tr-CQI-KPre.js +0 -7
  348. package/dist/assets/uk-CqW7R4DA.js +0 -16
  349. package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
  350. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
  351. package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
  352. package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
  353. package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
  354. package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
  355. package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
  356. package/server/lib/routing/feature-flag.js +0 -11
  357. package/server/lib/small-model/call.js +0 -851
  358. package/server/lib/small-model/call.test.js +0 -1292
  359. package/server/lib/small-model/catalog.js +0 -13
  360. package/server/lib/small-model/resolve.js +0 -131
  361. package/server/lib/small-model/resolve.test.js +0 -197
  362. package/server/lib/small-model/runtime-providers.js +0 -164
  363. package/server/lib/small-model/runtime-providers.test.js +0 -118
  364. package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
  365. package/server/lib/system-prompt/runtime.js +0 -49
  366. package/server/lib/system-prompt/runtime.test.js +0 -76
  367. package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
@@ -0,0 +1,486 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import http from 'node:http';
3
+
4
+ import { SURFACE_FRAME_PATH, SURFACE_INPUT_PATH, SURFACE_CONTROL_PATH, SURFACE_RESIZE_PATH, SURFACE_CLIPBOARD_PATH } from '@openchamber/sdk';
5
+ import WebSocket from 'ws';
6
+
7
+ import { GuestServiceError } from './service.js';
8
+ import { createGuestSurfaceRuntime, isSurfaceGuest } from './surface.js';
9
+
10
+ const surfaceGuest = (overrides = {}) => ({
11
+ id: 'sim',
12
+ name: 'Simulator',
13
+ packageRoot: '/ext/sim',
14
+ enabled: true,
15
+ service: { entry: 'service/main.js', runtime: 'host', surface: true },
16
+ capabilityGrants: ['service'],
17
+ panel: { id: 'sim', name: 'Simulator', icon: 'window' },
18
+ ...overrides,
19
+ });
20
+
21
+ /**
22
+ * A service that never spawns: frames are pushed by the test, every other
23
+ * call is recorded. The frame request waits like a real service would.
24
+ */
25
+ const createFakeService = () => {
26
+ const calls = [];
27
+ const frames = [];
28
+ const waiters = [];
29
+ let failWith = null;
30
+ /** Test hook: a promise the input handler awaits before answering. */
31
+ let inputGate = null;
32
+ const pushFrame = (frame) => {
33
+ frames.push(frame);
34
+ for (const wake of waiters.splice(0)) wake();
35
+ };
36
+ const nextFrameAfter = (seq) => frames.find((frame) => frame.seq > seq) ?? null;
37
+ const completed = [];
38
+ const openServiceRequest = async ({ path, method, body, query, signal }) => {
39
+ calls.push({ path, method, body: body ? JSON.parse(body) : undefined, query });
40
+ if (failWith) throw failWith;
41
+ if (path === SURFACE_INPUT_PATH && inputGate) {
42
+ const gate = inputGate;
43
+ inputGate = null;
44
+ await gate;
45
+ }
46
+ if (path === SURFACE_INPUT_PATH) completed.push(JSON.parse(body).events[0]);
47
+ let response;
48
+ if (path === SURFACE_FRAME_PATH) {
49
+ const after = Number(query.after);
50
+ let frame = nextFrameAfter(after);
51
+ if (!frame) {
52
+ await new Promise((resolve) => {
53
+ const wake = () => resolve();
54
+ waiters.push(wake);
55
+ signal?.addEventListener('abort', () => resolve(), { once: true });
56
+ setTimeout(resolve, 300);
57
+ });
58
+ frame = nextFrameAfter(after);
59
+ }
60
+ if (signal?.aborted) throw new GuestServiceError('cancelled', 'CANCELLED');
61
+ response = frame
62
+ ? new Response(frame.bytes, {
63
+ status: 200,
64
+ headers: {
65
+ 'content-type': frame.mime ?? 'image/png',
66
+ 'x-surface-seq': String(frame.seq),
67
+ 'x-surface-width': String(frame.width ?? 320),
68
+ 'x-surface-height': String(frame.height ?? 200),
69
+ ...(frame.title ? { 'x-surface-title': frame.title } : {}),
70
+ ...(frame.agentActive ? { 'x-surface-agent-active': '1' } : {}),
71
+ },
72
+ })
73
+ : new Response(null, { status: 204 });
74
+ } else if (path === SURFACE_RESIZE_PATH) {
75
+ response = Response.json({ width: 640, height: 400 });
76
+ } else if (path === SURFACE_CLIPBOARD_PATH) {
77
+ response = Response.json({ text: 'copied inside' });
78
+ } else {
79
+ response = new Response(null, { status: 204 });
80
+ }
81
+ return { response, finished: () => undefined };
82
+ };
83
+ return {
84
+ calls,
85
+ completed,
86
+ pushFrame,
87
+ gateNextInput: (promise) => { inputGate = promise; },
88
+ fail: (error) => { failWith = error; for (const wake of waiters.splice(0)) wake(); },
89
+ openServiceRequest,
90
+ callsTo: (path) => calls.filter((call) => call.path === path),
91
+ };
92
+ };
93
+
94
+ const listen = (server) => new Promise((resolve) => {
95
+ server.listen(0, '127.0.0.1', () => resolve(server.address().port));
96
+ });
97
+
98
+ const openViewer = (port, guestId = 'sim') => new Promise((resolve, reject) => {
99
+ const ws = new WebSocket(`ws://127.0.0.1:${port}/api/guests/${guestId}/surface/ws`);
100
+ ws.binaryType = 'arraybuffer';
101
+ const inbox = [];
102
+ const waiting = [];
103
+ ws.on('message', (data, isBinary) => {
104
+ const item = isBinary ? { binary: Buffer.from(data) } : JSON.parse(data.toString());
105
+ const waiter = waiting.shift();
106
+ if (waiter) waiter(item);
107
+ else inbox.push(item);
108
+ });
109
+ const next = () => (inbox.length ? Promise.resolve(inbox.shift()) : new Promise((r) => waiting.push(r)));
110
+ const send = (message) => ws.send(JSON.stringify(message));
111
+ ws.on('open', () => resolve({ ws, next, send, closed: new Promise((r) => ws.on('close', r)) }));
112
+ ws.on('error', reject);
113
+ ws.on('unexpected-response', (_req, res) => reject(new Error(`upgrade ${res.statusCode}`)));
114
+ });
115
+
116
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 30));
117
+
118
+ const cleanups = [];
119
+ afterEach(async () => {
120
+ for (const cleanup of cleanups.splice(0)) await cleanup();
121
+ });
122
+
123
+ const createHarness = async ({ guest = surfaceGuest(), rejectUpgradeAuth = false } = {}) => {
124
+ const server = http.createServer((_req, res) => { res.statusCode = 404; res.end(); });
125
+ const service = createFakeService();
126
+ const holds = [];
127
+ const rejected = [];
128
+ const runtime = createGuestSurfaceRuntime({
129
+ server,
130
+ uiAuthController: rejectUpgradeAuth ? { enabled: true, ensureSessionToken: async () => null } : null,
131
+ isRequestOriginAllowed: async () => true,
132
+ rejectWebSocketUpgrade: (socket, status, message) => {
133
+ rejected.push({ status, message });
134
+ socket.end(`HTTP/1.1 ${status} ${message}\r\n\r\n`);
135
+ },
136
+ persistPath: '/data/extensions.json',
137
+ findGuest: async (id) => (guest && guest.id === id ? guest : null),
138
+ idleStopMs: 60_000,
139
+ openServiceRequest: service.openServiceRequest,
140
+ holdService: (guestId) => {
141
+ const entry = { guestId, released: false };
142
+ holds.push(entry);
143
+ return () => { entry.released = true; };
144
+ },
145
+ createViewerId: (() => { let n = 0; return () => `viewer-${++n}`; })(),
146
+ });
147
+ const port = await listen(server);
148
+ cleanups.push(async () => {
149
+ runtime.stop();
150
+ await new Promise((resolve) => server.close(resolve));
151
+ });
152
+ return { port, runtime, service, holds, rejected };
153
+ };
154
+
155
+ describe('isSurfaceGuest', () => {
156
+ test('needs enabled, approved, and a declared surface', () => {
157
+ expect(isSurfaceGuest(surfaceGuest())).toBe(true);
158
+ expect(isSurfaceGuest(surfaceGuest({ enabled: false }))).toBe(false);
159
+ expect(isSurfaceGuest(surfaceGuest({ capabilityGrants: [] }))).toBe(false);
160
+ expect(isSurfaceGuest(surfaceGuest({ service: { entry: 'service/main.js', runtime: 'host' } }))).toBe(false);
161
+ });
162
+ });
163
+
164
+ describe('guest surface runtime', () => {
165
+ test('refuses an upgrade for an unknown or surface-less extension', async () => {
166
+ const { port, rejected } = await createHarness({ guest: surfaceGuest({ service: { entry: 'service/main.js', runtime: 'host' } }) });
167
+ await expect(openViewer(port)).rejects.toThrow('upgrade 404');
168
+ expect(rejected[0]).toMatchObject({ status: 404 });
169
+ });
170
+
171
+ test('refuses an upgrade without a UI session', async () => {
172
+ const { port, rejected } = await createHarness({ rejectUpgradeAuth: true });
173
+ await expect(openViewer(port)).rejects.toThrow('upgrade 401');
174
+ expect(rejected[0]).toMatchObject({ status: 401 });
175
+ });
176
+
177
+ test('delivers frames one at a time per viewer and skips to the newest after an ack', async () => {
178
+ const { port, service, holds } = await createHarness();
179
+ const viewer = await openViewer(port);
180
+ expect(await viewer.next()).toEqual({ type: 'hello', viewerId: 'viewer-1' });
181
+ expect(await viewer.next()).toEqual({ type: 'control', controller: 'none', mine: false });
182
+ expect(holds).toEqual([{ guestId: 'sim', released: false }]);
183
+
184
+ service.pushFrame({ seq: 1, bytes: Buffer.from('frame-1'), title: 'Home' });
185
+ const meta = await viewer.next();
186
+ expect(meta).toEqual({ type: 'frame', seq: 1, width: 320, height: 200, mime: 'image/png', bytes: 7, agentActive: false, title: 'Home' });
187
+ expect((await viewer.next()).binary.toString()).toBe('frame-1');
188
+
189
+ // Two more frames arrive before the viewer acknowledges: it gets only the newest.
190
+ service.pushFrame({ seq: 2, bytes: Buffer.from('frame-2') });
191
+ await settle();
192
+ service.pushFrame({ seq: 3, bytes: Buffer.from('frame-3') });
193
+ await settle();
194
+ viewer.send({ type: 'ack', seq: 1 });
195
+ expect(await viewer.next()).toMatchObject({ type: 'frame', seq: 3 });
196
+ expect((await viewer.next()).binary.toString()).toBe('frame-3');
197
+
198
+ viewer.ws.close();
199
+ await viewer.closed;
200
+ await settle();
201
+ expect(holds[0].released).toBe(true);
202
+ expect(service.callsTo(SURFACE_FRAME_PATH)[0].query).toEqual({ after: '0', wait: '25000' });
203
+ });
204
+
205
+ test('the first input takes control, is forwarded, and tells the service; release hands it back', async () => {
206
+ const { port, service, runtime } = await createHarness();
207
+ const viewer = await openViewer(port);
208
+ await viewer.next();
209
+ await viewer.next();
210
+
211
+ const click = { type: 'pointer', action: 'down', x: 10, y: 20, button: 0, buttons: 1, modifiers: { alt: false, ctrl: false, meta: false, shift: false } };
212
+ viewer.send({ type: 'input', events: [click] });
213
+ expect(await viewer.next()).toEqual({ type: 'control', controller: 'user', mine: true });
214
+ await settle();
215
+ expect(runtime.userControls('sim')).toBe(true);
216
+ expect(service.callsTo(SURFACE_INPUT_PATH)[0].body).toEqual({ events: [click] });
217
+ expect(service.callsTo(SURFACE_CONTROL_PATH).map((call) => call.body)).toEqual([{ controller: 'user' }]);
218
+
219
+ viewer.send({ type: 'release' });
220
+ expect(await viewer.next()).toEqual({ type: 'control', controller: 'none', mine: false });
221
+ await settle();
222
+ expect(runtime.userControls('sim')).toBe(false);
223
+ expect(service.callsTo(SURFACE_CONTROL_PATH).map((call) => call.body)).toEqual([{ controller: 'user' }, { controller: 'none' }]);
224
+ });
225
+
226
+ test('a second viewer cannot take control from the first and is told who has it', async () => {
227
+ const { port, service } = await createHarness();
228
+ const first = await openViewer(port);
229
+ await first.next(); await first.next();
230
+ const second = await openViewer(port);
231
+ await second.next(); await second.next();
232
+
233
+ const key = { type: 'key', action: 'down', key: 'a', code: 'KeyA', modifiers: { alt: false, ctrl: false, meta: false, shift: false } };
234
+ first.send({ type: 'input', events: [key] });
235
+ expect(await first.next()).toEqual({ type: 'control', controller: 'user', mine: true });
236
+ expect(await second.next()).toEqual({ type: 'control', controller: 'user', mine: false });
237
+
238
+ second.send({ type: 'input', events: [key] });
239
+ expect(await second.next()).toEqual({ type: 'control', controller: 'user', mine: false });
240
+ await settle();
241
+ expect(service.callsTo(SURFACE_INPUT_PATH)).toHaveLength(1);
242
+
243
+ // The holder disconnecting releases control for everyone.
244
+ first.ws.close();
245
+ expect(await second.next()).toEqual({ type: 'control', controller: 'none', mine: false });
246
+ });
247
+
248
+ test('agent activity holds control until the hold expires, and the user always wins', async () => {
249
+ const { port, runtime, service } = await createHarness();
250
+ const viewer = await openViewer(port);
251
+ await viewer.next(); await viewer.next();
252
+
253
+ // Not attached to a session: nothing to note, nothing thrown.
254
+ runtime.noteAgentActivity('other');
255
+
256
+ runtime.noteAgentActivity('sim');
257
+ expect(await viewer.next()).toEqual({ type: 'control', controller: 'agent', mine: false });
258
+ expect(runtime.userControls('sim')).toBe(false);
259
+ // A second action while already agent-held does not re-broadcast.
260
+ runtime.noteAgentActivity('sim');
261
+
262
+ const text = { type: 'text', text: 'hello' };
263
+ viewer.send({ type: 'input', events: [text] });
264
+ expect(await viewer.next()).toEqual({ type: 'control', controller: 'user', mine: true });
265
+ await settle();
266
+ expect(service.callsTo(SURFACE_INPUT_PATH)[0].body).toEqual({ events: [text] });
267
+ });
268
+
269
+ test('the agent hold expires back to nobody', async () => {
270
+ const timers = [];
271
+ const server = http.createServer((_req, res) => { res.statusCode = 404; res.end(); });
272
+ const service = createFakeService();
273
+ const runtime = createGuestSurfaceRuntime({
274
+ server,
275
+ uiAuthController: null,
276
+ isRequestOriginAllowed: async () => true,
277
+ rejectWebSocketUpgrade: (socket) => socket.destroy(),
278
+ persistPath: '/data/extensions.json',
279
+ findGuest: async () => surfaceGuest(),
280
+ idleStopMs: 60_000,
281
+ openServiceRequest: service.openServiceRequest,
282
+ holdService: () => () => undefined,
283
+ setTimer: (fn) => { timers.push(fn); return { unref() {} }; },
284
+ clearTimer: () => undefined,
285
+ });
286
+ const port = await listen(server);
287
+ cleanups.push(async () => { runtime.stop(); await new Promise((resolve) => server.close(resolve)); });
288
+ const viewer = await openViewer(port);
289
+ await viewer.next(); await viewer.next();
290
+ runtime.noteAgentActivity('sim');
291
+ expect(await viewer.next()).toMatchObject({ type: 'control', controller: 'agent' });
292
+ expect(timers).toHaveLength(1);
293
+ timers[0]();
294
+ expect(await viewer.next()).toEqual({ type: 'control', controller: 'none', mine: false });
295
+ });
296
+
297
+ test('hovering over the picture does not take control; a click does', async () => {
298
+ const { port, service, runtime } = await createHarness();
299
+ const viewer = await openViewer(port);
300
+ await viewer.next(); await viewer.next();
301
+ const modifiers = { alt: false, ctrl: false, meta: false, shift: false };
302
+ viewer.send({ type: 'input', events: [{ type: 'pointer', action: 'move', x: 5, y: 5, button: -1, buttons: 0, modifiers }] });
303
+ await settle();
304
+ expect(runtime.userControls('sim')).toBe(false);
305
+ expect(service.callsTo(SURFACE_INPUT_PATH)).toHaveLength(0);
306
+
307
+ runtime.noteAgentActivity('sim');
308
+ expect(await viewer.next()).toMatchObject({ type: 'control', controller: 'agent' });
309
+ viewer.send({ type: 'input', events: [{ type: 'pointer', action: 'move', x: 6, y: 6, button: -1, buttons: 0, modifiers }] });
310
+ await settle();
311
+ expect(runtime.userControls('sim')).toBe(false);
312
+
313
+ viewer.send({ type: 'input', events: [{ type: 'pointer', action: 'down', x: 6, y: 6, button: 0, buttons: 1, modifiers }] });
314
+ expect(await viewer.next()).toEqual({ type: 'control', controller: 'user', mine: true });
315
+ // Now moves flow, as the holder's.
316
+ viewer.send({ type: 'input', events: [{ type: 'pointer', action: 'move', x: 7, y: 7, button: -1, buttons: 0, modifiers }] });
317
+ await settle();
318
+ expect(service.callsTo(SURFACE_INPUT_PATH)).toHaveLength(2);
319
+ });
320
+
321
+ test('input batches reach the service in socket order even when one is slow', async () => {
322
+ const { port, service } = await createHarness();
323
+ const viewer = await openViewer(port);
324
+ await viewer.next(); await viewer.next();
325
+ const modifiers = { alt: false, ctrl: false, meta: false, shift: false };
326
+ let open;
327
+ service.gateNextInput(new Promise((resolve) => { open = resolve; }));
328
+ viewer.send({ type: 'input', events: [{ type: 'key', action: 'down', key: 'a', code: 'KeyA', modifiers }] });
329
+ await viewer.next(); // control
330
+ viewer.send({ type: 'input', events: [{ type: 'key', action: 'down', key: 'b', code: 'KeyB', modifiers }] });
331
+ viewer.send({ type: 'clipboard-read', id: 'c1' });
332
+ await settle();
333
+ expect(service.completed).toHaveLength(0);
334
+ expect(service.callsTo(SURFACE_CLIPBOARD_PATH)).toHaveLength(0);
335
+ open();
336
+ expect(await viewer.next()).toEqual({ type: 'clipboard', id: 'c1', text: 'copied inside' });
337
+ expect(service.completed.map((event) => event.key)).toEqual(['a', 'b']);
338
+ });
339
+
340
+ test('a session ended while the user held control never talks to the service again', async () => {
341
+ const { port, service, runtime, holds } = await createHarness();
342
+ const viewer = await openViewer(port);
343
+ await viewer.next(); await viewer.next();
344
+ const modifiers = { alt: false, ctrl: false, meta: false, shift: false };
345
+ viewer.send({ type: 'input', events: [{ type: 'key', action: 'down', key: 'a', code: 'KeyA', modifiers }] });
346
+ expect(await viewer.next()).toMatchObject({ type: 'control', controller: 'user', mine: true });
347
+ await settle();
348
+ const callsBefore = service.calls.length;
349
+
350
+ // Approval withdrawn: the guest routes end the session; the socket closes after.
351
+ runtime.endForGuest('sim');
352
+ expect(await viewer.next()).toEqual({ type: 'ended', reason: 'extension-unavailable' });
353
+ await viewer.closed;
354
+ await settle();
355
+ expect(service.calls.length).toBe(callsBefore);
356
+ expect(holds[0].released).toBe(true);
357
+ });
358
+
359
+ test('the last viewer leaving while holding control tells the service it let go', async () => {
360
+ const { port, service } = await createHarness();
361
+ const viewer = await openViewer(port);
362
+ await viewer.next(); await viewer.next();
363
+ const modifiers = { alt: false, ctrl: false, meta: false, shift: false };
364
+ viewer.send({ type: 'input', events: [{ type: 'key', action: 'down', key: 'a', code: 'KeyA', modifiers }] });
365
+ await viewer.next();
366
+ await settle();
367
+ viewer.ws.close();
368
+ await viewer.closed;
369
+ await settle();
370
+ expect(service.callsTo(SURFACE_CONTROL_PATH).map((call) => call.body)).toEqual([{ controller: 'user' }, { controller: 'none' }]);
371
+ });
372
+
373
+ test('a queued input from before a hand-back does not take control again', async () => {
374
+ const { port, service, runtime } = await createHarness();
375
+ const viewer = await openViewer(port);
376
+ await viewer.next(); await viewer.next();
377
+ const modifiers = { alt: false, ctrl: false, meta: false, shift: false };
378
+ let open;
379
+ service.gateNextInput(new Promise((resolve) => { open = resolve; }));
380
+ viewer.send({ type: 'input', events: [{ type: 'key', action: 'down', key: 'a', code: 'KeyA', modifiers }] });
381
+ expect(await viewer.next()).toMatchObject({ type: 'control', controller: 'user', mine: true });
382
+ viewer.send({ type: 'input', events: [{ type: 'key', action: 'down', key: 'b', code: 'KeyB', modifiers }] });
383
+ viewer.send({ type: 'release' });
384
+ expect(await viewer.next()).toEqual({ type: 'control', controller: 'none', mine: false });
385
+ expect(runtime.userControls('sim')).toBe(false);
386
+ open();
387
+ await settle();
388
+ await settle();
389
+ expect(runtime.userControls('sim')).toBe(false);
390
+ // B was typed before the release and still belonged to the holder: it is
391
+ // not re-taken, and it is not delivered under nobody's control either.
392
+ expect(service.completed.map((event) => event.key)).toEqual(['a']);
393
+ });
394
+
395
+ test('a queued input from a viewer that left never makes it the controller', async () => {
396
+ const { port, service, runtime } = await createHarness();
397
+ const first = await openViewer(port);
398
+ await first.next(); await first.next();
399
+ const second = await openViewer(port);
400
+ await second.next(); await second.next();
401
+ const modifiers = { alt: false, ctrl: false, meta: false, shift: false };
402
+ let open;
403
+ service.gateNextInput(new Promise((resolve) => { open = resolve; }));
404
+ first.send({ type: 'input', events: [{ type: 'key', action: 'down', key: 'a', code: 'KeyA', modifiers }] });
405
+ await first.next();
406
+ expect(await second.next()).toEqual({ type: 'control', controller: 'user', mine: false });
407
+ first.send({ type: 'input', events: [{ type: 'key', action: 'down', key: 'b', code: 'KeyB', modifiers }] });
408
+ first.ws.close();
409
+ expect(await second.next()).toEqual({ type: 'control', controller: 'none', mine: false });
410
+ open();
411
+ await settle();
412
+ await settle();
413
+ expect(runtime.userControls('sim')).toBe(false);
414
+ second.send({ type: 'input', events: [{ type: 'key', action: 'down', key: 'c', code: 'KeyC', modifiers }] });
415
+ expect(await second.next()).toEqual({ type: 'control', controller: 'user', mine: true });
416
+ await settle();
417
+ expect(service.completed.map((event) => event.key)).toEqual(['a', 'c']);
418
+ });
419
+
420
+ test('a deactivation while the closed panel\'s farewell is still queued cancels it', async () => {
421
+ const { port, service, runtime, holds } = await createHarness();
422
+ const viewer = await openViewer(port);
423
+ await viewer.next(); await viewer.next();
424
+ const modifiers = { alt: false, ctrl: false, meta: false, shift: false };
425
+ let open;
426
+ service.gateNextInput(new Promise((resolve) => { open = resolve; }));
427
+ viewer.send({ type: 'input', events: [{ type: 'key', action: 'down', key: 'a', code: 'KeyA', modifiers }] });
428
+ await viewer.next();
429
+ await settle();
430
+ viewer.ws.close();
431
+ await viewer.closed;
432
+ await settle();
433
+ // The session is still registered: its final "none" waits behind the slow input.
434
+ expect(runtime.sessionOf('sim')).not.toBeNull();
435
+ const callsBefore = service.calls.length;
436
+ runtime.endForGuest('sim');
437
+ open();
438
+ await settle();
439
+ await settle();
440
+ expect(service.calls.length).toBe(callsBefore);
441
+ expect(runtime.sessionOf('sim')).toBeNull();
442
+ expect(holds[0].released).toBe(true);
443
+ });
444
+
445
+ test('resize and clipboard reads round-trip through the service; clipboard needs control', async () => {
446
+ const { port, service } = await createHarness();
447
+ const viewer = await openViewer(port);
448
+ await viewer.next(); await viewer.next();
449
+ viewer.send({ type: 'resize', width: 800, height: 600 });
450
+ expect(await viewer.next()).toEqual({ type: 'resized', width: 640, height: 400 });
451
+ expect(service.callsTo(SURFACE_RESIZE_PATH)[0].body).toEqual({ width: 800, height: 600 });
452
+ viewer.send({ type: 'clipboard-read', id: 'c0' });
453
+ expect(await viewer.next()).toMatchObject({ type: 'error', code: 'NOT_CONTROLLING' });
454
+ expect(service.callsTo(SURFACE_CLIPBOARD_PATH)).toHaveLength(0);
455
+ const modifiers = { alt: false, ctrl: false, meta: false, shift: false };
456
+ viewer.send({ type: 'input', events: [{ type: 'key', action: 'down', key: 'c', code: 'KeyC', modifiers: { ...modifiers, meta: true } }] });
457
+ await viewer.next();
458
+ viewer.send({ type: 'clipboard-read', id: 'c1' });
459
+ expect(await viewer.next()).toEqual({ type: 'clipboard', id: 'c1', text: 'copied inside' });
460
+ });
461
+
462
+ test('a malformed viewer message is answered with an error, not a drop', async () => {
463
+ const { port } = await createHarness();
464
+ const viewer = await openViewer(port);
465
+ await viewer.next(); await viewer.next();
466
+ viewer.ws.send('{"type":"input","events":[{"type":"pointer"}]}');
467
+ expect(await viewer.next()).toMatchObject({ type: 'error', code: 'BAD_MESSAGE' });
468
+ });
469
+
470
+ test('a service that stops ends the session for every viewer; deactivation does too', async () => {
471
+ const { port, service, runtime } = await createHarness();
472
+ const viewer = await openViewer(port);
473
+ await viewer.next(); await viewer.next();
474
+ service.fail(new GuestServiceError('gone', 'SERVICE_FAILED'));
475
+ expect(await viewer.next()).toEqual({ type: 'ended', reason: 'service-stopped' });
476
+ await viewer.closed;
477
+
478
+ service.fail(null);
479
+ const again = await openViewer(port);
480
+ await again.next(); await again.next();
481
+ runtime.endForGuest('sim');
482
+ expect(await again.next()).toEqual({ type: 'ended', reason: 'extension-unavailable' });
483
+ await again.closed;
484
+ expect(runtime.sessionOf('sim')).toBeNull();
485
+ });
486
+ });
@@ -1,6 +1,7 @@
1
1
  import express from 'express';
2
2
  import { constants as fsConstants } from 'node:fs';
3
3
  import { mintOutsideFileGrant } from '../fs/routes.js';
4
+ import { unwrapOpenCodeResponse } from '../opencode/response-envelope.js';
4
5
 
5
6
  const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
6
7
  const MAX_IMAGE_SOURCES = 12;
@@ -128,9 +129,20 @@ const parseDefinitionDestination = (value) => {
128
129
  return match ? unescapeMarkdownDestination(match[0]) : '';
129
130
  };
130
131
 
132
+ /**
133
+ * v2 messages are flat records: an assistant one carries `content[]` and a user
134
+ * one a single `text`. Both are read here, because an image link can come from
135
+ * either side of the conversation.
136
+ */
137
+ const messageTextParts = (message) => {
138
+ if (Array.isArray(message?.content)) return message.content;
139
+ if (typeof message?.text === 'string') return [{ type: 'text', text: message.text }];
140
+ return [];
141
+ };
142
+
131
143
  const collectMarkdownLinesOutsideCode = (message) => {
132
144
  const lines = [];
133
- for (const part of Array.isArray(message?.parts) ? message.parts : []) {
145
+ for (const part of messageTextParts(message)) {
134
146
  if (part?.type !== 'text' || typeof part.text !== 'string') continue;
135
147
  let fence = null;
136
148
  for (const line of part.text.split('\n')) {
@@ -193,7 +205,7 @@ const markdownImageSources = (message) => {
193
205
 
194
206
  const fetchMessage = async ({ sessionId, messageId, directory, buildOpenCodeUrl, getOpenCodeAuthHeaders }) => {
195
207
  const url = new URL(buildOpenCodeUrl(
196
- `/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(messageId)}`,
208
+ `/api/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(messageId)}`,
197
209
  '',
198
210
  ));
199
211
  url.searchParams.set('directory', directory);
@@ -209,8 +221,8 @@ const fetchMessage = async ({ sessionId, messageId, directory, buildOpenCodeUrl,
209
221
  });
210
222
  if (response.status === 404) return null;
211
223
  if (!response.ok) throw new Error(`OpenCode returned ${response.status}`);
212
- const message = await response.json().catch(() => null);
213
- return message?.info && Array.isArray(message.parts) ? message : null;
224
+ const message = unwrapOpenCodeResponse(await response.json().catch(() => null));
225
+ return message && typeof message === 'object' && typeof message.id === 'string' ? message : null;
214
226
  };
215
227
 
216
228
  const inspectImage = async ({ source, directory, approvedTempRoot, fsPromises, path }) => {
@@ -289,7 +301,8 @@ export const registerMarkdownImageGrantRoutes = (app, dependencies) => {
289
301
  buildOpenCodeUrl,
290
302
  getOpenCodeAuthHeaders,
291
303
  });
292
- if (!message || message.info?.id !== messageId || message.info?.role !== 'assistant') {
304
+ // v2 messages are flat records with a `type` discriminator.
305
+ if (!message || message.id !== messageId || message.type !== 'assistant') {
293
306
  return res.status(404).json({ error: 'Assistant message not found' });
294
307
  }
295
308
  // Assistant text is authoritative: a remote client cannot mint grants for unreferenced paths.
@@ -32,8 +32,9 @@ const createFixture = async ({ sources, markdown } = {}) => {
32
32
  const requestedSources = sources ?? [new URL(`file://${defaultPath}`).toString()];
33
33
  const text = markdown ?? requestedSources.map((source) => `![image](${source})`).join('\n');
34
34
  const fetchMock = vi.fn(async () => new Response(JSON.stringify({
35
- info: { id: 'msg_1', role: 'assistant' },
36
- parts: [{ type: 'text', text }],
35
+ id: 'msg_1',
36
+ type: 'assistant',
37
+ content: [{ type: 'text', text }],
37
38
  }), { status: 200, headers: { 'content-type': 'application/json' } }));
38
39
  vi.stubGlobal('fetch', fetchMock);
39
40
 
@@ -95,8 +96,9 @@ describe('session image assets', () => {
95
96
  await fs.writeFile(temporaryPath, PNG);
96
97
  const temporarySource = new URL(`file://${temporaryPath}`).toString();
97
98
  fixture.fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
98
- info: { id: 'msg_1', role: 'assistant' },
99
- parts: [{ type: 'text', text: `![workspace](workspace.png)\n![temporary](${temporarySource})` }],
99
+ id: 'msg_1',
100
+ type: 'assistant',
101
+ content: [{ type: 'text', text: `![workspace](workspace.png)\n![temporary](${temporarySource})` }],
100
102
  }), { status: 200, headers: { 'content-type': 'application/json' } }));
101
103
 
102
104
  const response = await prepare(fixture.app, fixture.directory, ['workspace.png', temporarySource]);
@@ -212,8 +214,9 @@ describe('session image assets', () => {
212
214
  await fs.writeFile(outsidePath, PNG);
213
215
  const source = new URL(`file://${outsidePath}`).toString();
214
216
  fixture.fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
215
- info: { id: 'msg_1', role: 'assistant' },
216
- parts: [{ type: 'text', text: `![outside](${source})` }],
217
+ id: 'msg_1',
218
+ type: 'assistant',
219
+ content: [{ type: 'text', text: `![outside](${source})` }],
217
220
  }), { status: 200, headers: { 'content-type': 'application/json' } }));
218
221
 
219
222
  const response = await prepare(fixture.app, fixture.directory, [source]);
@@ -24,9 +24,11 @@ JSON bodies enabled in `opencode/core-routes.js`; stopped by
24
24
 
25
25
  ## Auto routing
26
26
 
27
- `resolvePromptBody` (the routing runtime) runs on the assembled body right
28
- before the prompt or command send, turning a queued `openchamber/auto` model
29
- into a real one. The queue captures the sentinel like any other send config.
27
+ `resolveAutoSelection` (the routing runtime) runs right before the send when
28
+ the queued send config names `openchamber/auto`, and answers with the real
29
+ model and agent. OpenCode 2.x holds both on the session, so the queue applies
30
+ them with the `POST /api/session/:id/model` and `/agent` calls it already
31
+ makes. The queue captures the sentinel like any other send config.
30
32
 
31
33
  ## Item
32
34
 
@@ -100,30 +102,39 @@ persisted "sending" flag would strand a message forever.
100
102
  old behavior: a stop is not immediately followed by the next prompt) or
101
103
  while the head item is in retry backoff.
102
104
  4. Idleness is re-verified against OpenCode before sending, because
103
- `prompt_async` into a running turn steers into it instead of starting the
104
- next one: `GET /session/status` must not list the session as busy/retry,
105
- and the trailing message must not be an unfinished assistant reply (the
106
- status map only lists busy sessions, so a missed busy event leaves no
107
- entry while a turn still streams). An unfinished reply created before this
105
+ a prompt into a running turn steers into it instead of starting the
106
+ next one: `GET /api/session/active` must not list the session, and the
107
+ trailing message must not be an unfinished assistant reply (that route
108
+ lists only running sessions, so a missed busy event leaves no entry while
109
+ a turn still streams). An unfinished reply created before this
108
110
  runtime started does not block: its run died with the previous server and
109
111
  will never complete, so a restored queue would wait on it forever. A reply
110
112
  with no `created` time still blocks. A failed fetch is unknown, never idle:
111
113
  the tick re-arms with backoff.
112
- 5. The head is marked in flight (broadcast), then sent:
114
+ The turn must also be over for the session's subagents: a parent idles
115
+ while a background subagent works and runs again when OpenCode hands the
116
+ result back. While `../opencode/session-activity.js` finds a running child
117
+ the head waits (the rerun's idle event re-arms it, a 5 s recheck covers a
118
+ missed one); a failed check is unknown and backs off like the rest.
119
+ 5. The head is marked in flight (broadcast), then sent. The captured model and
120
+ agent are switched onto the session first (`POST /session/:id/model`,
121
+ `/agent`), because v2 holds both on the session rather than in the body; a
122
+ captured `openchamber/auto` is resolved into a real pair by the routing
123
+ runtime beforehand. Then the captured context goes ahead as synthetic
124
+ messages (`POST /session/:id/synthetic` with `resume: false`, an attached
125
+ item's metadata riding along), followed by pending project knowledge
126
+ (`sessionKnowledgeRuntime.resolvePendingForSession`, recorded as delivered
127
+ only after the send is accepted), and then the message itself:
113
128
  - text starting with `/` that names a command in OpenCode's `/command`
114
- list (skills included) and carries no captured context goes to
115
- `POST /session/:id/command` with the captured model, agent, variant, and
116
- file parts. That route accepts file parts only, so a command queued
117
- **with** context takes the prompt route instead, the same rule the
118
- composer applies: the command's template is expanded with its arguments
119
- (`$ARGUMENTS`, `$1..$N`, or appended), a skill keeps its `/name args` text
120
- and gets an explicit "the user invoked this skill" synthetic part after
121
- the context;
122
- - otherwise `POST /session/:id/prompt_async` with the parts in the same
123
- order a UI send uses: text, files, the captured context, the skill
124
- invocation when there is one, pending project knowledge
125
- (`sessionKnowledgeRuntime.resolvePendingForSession`, synthetic, recorded
126
- as delivered only after the prompt is accepted), then the agent mention.
129
+ list (skills included) goes to `POST /session/:id/command` (body fields
130
+ `name` and `text` since OpenCode 2.0.8) with its file attachments. The
131
+ command route takes files only, which is why the context went ahead of
132
+ it; sending "/name args" as a prompt instead would skip the template
133
+ OpenCode expands only on that route. The command lookup runs before
134
+ anything is admitted, so a failed lookup fails the send without leaving
135
+ context behind for the retry to duplicate;
136
+ - otherwise `POST /session/:id/prompt` with the user's text, files and
137
+ agent mention.
127
138
  Success removes the item, persists, broadcasts, and marks the user
128
139
  message sent for notifications. Failure keeps the item, backs off
129
140
  2 s → 60 s (doubling per consecutive failure of that item), and re-arms.