@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,630 @@
1
+ /**
2
+ * Shared surface host: shows viewers what an extension's service is drawing
3
+ * and carries their input back, one controller at a time.
4
+ *
5
+ * One session per extension while at least one viewer is attached. A pump
6
+ * pulls frames from the service (`GET /surface/frame?after=<seq>`, held by
7
+ * the service until something changed) and hands each viewer the newest
8
+ * frame it has not seen, one at a time: a viewer gets the next frame only
9
+ * after acknowledging the previous one, so a slow connection skips frames
10
+ * instead of queueing them. The service is held running for the life of the
11
+ * session and returns to its idle window when the last viewer leaves.
12
+ *
13
+ * Control is owned here, not by the service or the panel. The user takes
14
+ * it by acting (any input from a viewer while nobody, or the agent, holds
15
+ * it); the agent counts as holding it for `SURFACE_AGENT_HOLD_MS` after its
16
+ * last action; a user hands it back with an explicit release. Every change
17
+ * is broadcast to viewers (with `mine` computed per connection) and posted
18
+ * to the service so its own automation can pause. A second viewer sees who
19
+ * holds control and cannot take it away; a viewer that disconnects while
20
+ * holding it releases it.
21
+ */
22
+
23
+ import { WebSocketServer } from 'ws';
24
+
25
+ import {
26
+ SURFACE_AGENT_ACTIVE_HEADER,
27
+ SURFACE_AGENT_HOLD_MS,
28
+ SURFACE_CLIPBOARD_PATH,
29
+ SURFACE_CONTROL_PATH,
30
+ SURFACE_FRAME_MAX_BYTES,
31
+ SURFACE_FRAME_MIMES,
32
+ SURFACE_FRAME_PATH,
33
+ SURFACE_FRAME_WAIT_MS,
34
+ SURFACE_HEIGHT_HEADER,
35
+ SURFACE_INPUT_PATH,
36
+ SURFACE_RESIZE_PATH,
37
+ SURFACE_SEQ_HEADER,
38
+ SURFACE_TITLE_HEADER,
39
+ SURFACE_TITLE_MAX,
40
+ SURFACE_WIDTH_HEADER,
41
+ isGuestApproved,
42
+ requestedGuestCapabilities,
43
+ } from '@openchamber/sdk';
44
+ import {
45
+ surfaceClipboardAnswerSchema,
46
+ surfaceResizeAnswerSchema,
47
+ surfaceViewerMessageSchema,
48
+ } from '@openchamber/sdk/schemas';
49
+
50
+ import { GuestServiceError, holdGuestService, openGuestServiceRequest } from './service.js';
51
+
52
+ const SURFACE_WS_PATH = /^\/api\/guests\/([a-z][a-z0-9-]*)\/surface\/ws$/;
53
+ /** Input batches and clipboard text; frames never travel viewer → host. */
54
+ const VIEWER_MESSAGE_MAX_BYTES = 256 * 1024;
55
+ const QUICK_REQUEST_TIMEOUT_MS = 10_000;
56
+ const FRAME_MIMES = new Set(SURFACE_FRAME_MIMES);
57
+
58
+ /** Whether this catalog row can show a surface right now. */
59
+ export const isSurfaceGuest = (guest) => (
60
+ Boolean(guest)
61
+ && guest.enabled !== false
62
+ && guest.service?.surface === true
63
+ && isGuestApproved({
64
+ requested: requestedGuestCapabilities(guest),
65
+ granted: Array.isArray(guest.capabilityGrants) ? guest.capabilityGrants : [],
66
+ })
67
+ );
68
+
69
+ const readDimension = (value) => {
70
+ const number = Number(value);
71
+ return Number.isInteger(number) && number > 0 ? number : null;
72
+ };
73
+
74
+ /**
75
+ * @param {{
76
+ * server: import('node:http').Server,
77
+ * uiAuthController: { enabled?: boolean, ensureSessionToken?: Function } | null,
78
+ * isRequestOriginAllowed: (req: import('node:http').IncomingMessage) => Promise<boolean>,
79
+ * rejectWebSocketUpgrade: (socket: import('node:stream').Duplex, status: number, message: string) => void,
80
+ * persistPath: string,
81
+ * findGuest: (id: string) => Promise<object | null>,
82
+ * idleStopMs: number,
83
+ * openServiceRequest?: typeof openGuestServiceRequest,
84
+ * holdService?: typeof holdGuestService,
85
+ * createViewerId?: () => string,
86
+ * setTimer?: typeof setTimeout,
87
+ * clearTimer?: typeof clearTimeout,
88
+ * }} deps
89
+ */
90
+ export const createGuestSurfaceRuntime = ({
91
+ server,
92
+ uiAuthController,
93
+ isRequestOriginAllowed,
94
+ rejectWebSocketUpgrade,
95
+ persistPath,
96
+ findGuest,
97
+ idleStopMs,
98
+ openServiceRequest = openGuestServiceRequest,
99
+ holdService = holdGuestService,
100
+ createViewerId = () => `viewer-${Math.random().toString(36).slice(2, 10)}`,
101
+ setTimer = setTimeout,
102
+ clearTimer = clearTimeout,
103
+ }) => {
104
+ /** @type {Map<string, Session>} */
105
+ const sessions = new Map();
106
+
107
+ /**
108
+ * @typedef {{
109
+ * id: string,
110
+ * ws: import('ws').WebSocket,
111
+ * sentSeq: number,
112
+ * awaitingAck: boolean,
113
+ * }} Viewer
114
+ * @typedef {{
115
+ * guestId: string,
116
+ * guest: object,
117
+ * viewers: Map<string, Viewer>,
118
+ * latest: { seq: number, width: number, height: number, mime: string, title?: string, agentActive: boolean, bytes: Buffer } | null,
119
+ * controller: 'none' | 'agent' | string,
120
+ * agentTimer: ReturnType<typeof setTimeout> | null,
121
+ * pumpAbort: AbortController | null,
122
+ * releaseHold: (() => void) | null,
123
+ * ended: boolean,
124
+ * }} Session
125
+ */
126
+
127
+ const send = (viewer, message) => {
128
+ if (viewer.ws.readyState !== 1) return;
129
+ try {
130
+ viewer.ws.send(JSON.stringify(message));
131
+ } catch {
132
+ // The close handler takes the viewer out of the session.
133
+ }
134
+ };
135
+
136
+ const controllerKind = (session) => (
137
+ session.controller === 'none' || session.controller === 'agent' ? session.controller : 'user'
138
+ );
139
+
140
+ const serviceRequest = (session, params) => openServiceRequest({
141
+ guestId: session.guestId,
142
+ guestName: session.guest.name,
143
+ packageRoot: session.guest.packageRoot,
144
+ service: session.guest.service,
145
+ granted: session.guest.capabilityGrants,
146
+ persistPath,
147
+ idleStopMs,
148
+ ...params,
149
+ });
150
+
151
+ /**
152
+ * Service calls that must land in order (input, control, resize, clipboard)
153
+ * run one after another per session: the socket delivers messages in
154
+ * order, and turning them into parallel HTTP requests would let a later
155
+ * batch reach the service before an earlier one. Work queued for an ended
156
+ * session is dropped, so closing a panel never starts the service again.
157
+ */
158
+ const enqueue = (session, task) => {
159
+ const run = session.queue.then(async () => {
160
+ if (session.ended) return;
161
+ await task();
162
+ }).catch(() => undefined);
163
+ session.queue = run;
164
+ return run;
165
+ };
166
+
167
+ const notifyService = async (session, controller) => {
168
+ if (session.ended) return;
169
+ try {
170
+ const { response, finished } = await serviceRequest(session, {
171
+ method: 'POST',
172
+ path: SURFACE_CONTROL_PATH,
173
+ body: JSON.stringify({ controller }),
174
+ timeoutMs: QUICK_REQUEST_TIMEOUT_MS,
175
+ });
176
+ await response.arrayBuffer().catch(() => undefined);
177
+ finished();
178
+ } catch {
179
+ // Advisory: the service pausing its automation is a courtesy, not a
180
+ // gate; the host refuses the browser provider's actions itself.
181
+ }
182
+ };
183
+
184
+ const broadcastControl = (session) => {
185
+ const kind = controllerKind(session);
186
+ for (const viewer of session.viewers.values()) {
187
+ send(viewer, { type: 'control', controller: kind, mine: session.controller === viewer.id });
188
+ }
189
+ void enqueue(session, () => notifyService(session, kind));
190
+ };
191
+
192
+ const clearAgentTimer = (session) => {
193
+ if (session.agentTimer) {
194
+ clearTimer(session.agentTimer);
195
+ session.agentTimer = null;
196
+ }
197
+ };
198
+
199
+ const setController = (session, controller) => {
200
+ if (session.controller === controller) return;
201
+ session.controller = controller;
202
+ clearAgentTimer(session);
203
+ broadcastControl(session);
204
+ };
205
+
206
+ const deliverLatest = (session, viewer) => {
207
+ const frame = session.latest;
208
+ if (!frame || viewer.awaitingAck || viewer.sentSeq >= frame.seq || viewer.ws.readyState !== 1) return;
209
+ viewer.awaitingAck = true;
210
+ viewer.sentSeq = frame.seq;
211
+ const meta = {
212
+ type: 'frame',
213
+ seq: frame.seq,
214
+ width: frame.width,
215
+ height: frame.height,
216
+ mime: frame.mime,
217
+ bytes: frame.bytes.length,
218
+ agentActive: frame.agentActive,
219
+ };
220
+ if (frame.title) meta.title = frame.title;
221
+ send(viewer, meta);
222
+ try {
223
+ viewer.ws.send(frame.bytes, { binary: true });
224
+ } catch {
225
+ // close handler cleans up
226
+ }
227
+ };
228
+
229
+ const endSession = (session, reason) => {
230
+ if (session.ended) return;
231
+ session.ended = true;
232
+ sessions.delete(session.guestId);
233
+ clearAgentTimer(session);
234
+ session.pumpAbort?.abort();
235
+ session.pumpAbort = null;
236
+ session.releaseHold?.();
237
+ session.releaseHold = null;
238
+ for (const viewer of session.viewers.values()) {
239
+ send(viewer, { type: 'ended', reason });
240
+ try {
241
+ viewer.ws.close(1000, reason);
242
+ } catch {
243
+ // already gone
244
+ }
245
+ }
246
+ session.viewers.clear();
247
+ };
248
+
249
+ const readFrame = async (response) => {
250
+ const declared = Number(response.headers.get('content-length'));
251
+ if (Number.isFinite(declared) && declared > SURFACE_FRAME_MAX_BYTES) return null;
252
+ const mime = (response.headers.get('content-type') || '').split(';')[0].trim();
253
+ if (!FRAME_MIMES.has(mime)) return null;
254
+ const seq = readDimension(response.headers.get(SURFACE_SEQ_HEADER));
255
+ const width = readDimension(response.headers.get(SURFACE_WIDTH_HEADER));
256
+ const height = readDimension(response.headers.get(SURFACE_HEIGHT_HEADER));
257
+ if (seq === null || width === null || height === null) return null;
258
+ const bytes = Buffer.from(await response.arrayBuffer());
259
+ if (bytes.length === 0 || bytes.length > SURFACE_FRAME_MAX_BYTES) return null;
260
+ const title = (response.headers.get(SURFACE_TITLE_HEADER) || '').slice(0, SURFACE_TITLE_MAX);
261
+ const frame = {
262
+ seq,
263
+ width,
264
+ height,
265
+ mime,
266
+ bytes,
267
+ agentActive: response.headers.get(SURFACE_AGENT_ACTIVE_HEADER) === '1',
268
+ };
269
+ if (title) frame.title = title;
270
+ return frame;
271
+ };
272
+
273
+ const runPump = async (session) => {
274
+ const abort = new AbortController();
275
+ session.pumpAbort = abort;
276
+ while (!abort.signal.aborted && session.viewers.size > 0) {
277
+ const after = session.latest?.seq ?? 0;
278
+ let opened;
279
+ try {
280
+ opened = await serviceRequest(session, {
281
+ method: 'GET',
282
+ path: SURFACE_FRAME_PATH,
283
+ query: { after: String(after), wait: String(SURFACE_FRAME_WAIT_MS) },
284
+ accept: SURFACE_FRAME_MIMES.join(', '),
285
+ timeoutMs: SURFACE_FRAME_WAIT_MS + QUICK_REQUEST_TIMEOUT_MS,
286
+ signal: abort.signal,
287
+ });
288
+ } catch (error) {
289
+ if (abort.signal.aborted) return;
290
+ const code = error instanceof GuestServiceError ? error.code : 'SERVICE_FAILED';
291
+ endSession(session, code === 'DISABLED' || code === 'NO_SERVICE' ? 'extension-unavailable' : 'service-stopped');
292
+ return;
293
+ }
294
+ const { response, finished } = opened;
295
+ if (response.status === 204) {
296
+ await response.arrayBuffer().catch(() => undefined);
297
+ finished();
298
+ continue;
299
+ }
300
+ if (response.status !== 200) {
301
+ await response.arrayBuffer().catch(() => undefined);
302
+ finished();
303
+ endSession(session, 'service-stopped');
304
+ return;
305
+ }
306
+ let frame;
307
+ try {
308
+ frame = await readFrame(response);
309
+ } catch {
310
+ frame = null;
311
+ }
312
+ finished();
313
+ if (abort.signal.aborted) return;
314
+ if (!frame) {
315
+ endSession(session, 'service-stopped');
316
+ return;
317
+ }
318
+ // A stale or repeated sequence is the service's bug, not a new picture;
319
+ // asking again with the same `after` would spin, so treat it as newer.
320
+ if (frame.seq <= after) frame.seq = after + 1;
321
+ session.latest = frame;
322
+ for (const viewer of session.viewers.values()) deliverLatest(session, viewer);
323
+ }
324
+ };
325
+
326
+ const ensureSession = (guestId, guest) => {
327
+ let session = sessions.get(guestId);
328
+ if (session) {
329
+ session.guest = guest;
330
+ return session;
331
+ }
332
+ session = {
333
+ guestId,
334
+ guest,
335
+ viewers: new Map(),
336
+ latest: null,
337
+ controller: 'none',
338
+ agentTimer: null,
339
+ pumpAbort: null,
340
+ releaseHold: holdService(guestId),
341
+ ended: false,
342
+ queue: Promise.resolve(),
343
+ handoffs: 0,
344
+ };
345
+ sessions.set(guestId, session);
346
+ return session;
347
+ };
348
+
349
+ /** Moving the pointer over the picture is looking, not acting. */
350
+ const isDeliberate = (events) => events.some((event) => (
351
+ event.type !== 'pointer' || event.action !== 'move' || event.buttons !== 0
352
+ ));
353
+
354
+ /**
355
+ * `handoff` is the session's hand-off counter as it was when the batch
356
+ * arrived. A batch that predates a release or a disconnect must not take
357
+ * control back: the user already let go, and re-taking it from a queued
358
+ * click would lock the agent out with nobody to press the button.
359
+ */
360
+ const handleInput = async (session, viewer, events, handoff) => {
361
+ if (viewer.gone) return;
362
+ if (session.controller !== viewer.id) {
363
+ if (session.controller !== 'none' && session.controller !== 'agent') {
364
+ // Someone else is in; say so instead of clicking under them.
365
+ send(viewer, { type: 'control', controller: 'user', mine: false });
366
+ return;
367
+ }
368
+ if (!isDeliberate(events) || handoff !== session.handoffs) return;
369
+ setController(session, viewer.id);
370
+ }
371
+ try {
372
+ const { response, finished } = await serviceRequest(session, {
373
+ method: 'POST',
374
+ path: SURFACE_INPUT_PATH,
375
+ body: JSON.stringify({ events }),
376
+ timeoutMs: QUICK_REQUEST_TIMEOUT_MS,
377
+ });
378
+ await response.arrayBuffer().catch(() => undefined);
379
+ finished();
380
+ if (response.status < 200 || response.status >= 300) {
381
+ send(viewer, { type: 'error', code: 'INPUT_REJECTED', message: `The extension refused this input (HTTP ${response.status}).` });
382
+ }
383
+ } catch (error) {
384
+ send(viewer, { type: 'error', code: 'INPUT_FAILED', message: error instanceof GuestServiceError ? error.message : 'The input could not be delivered.' });
385
+ }
386
+ };
387
+
388
+ const handleResize = async (session, viewer, width, height) => {
389
+ // The person in control decides the size; when nobody is, the last
390
+ // request wins, which is what a lone viewer expects.
391
+ if (session.controller !== viewer.id && session.controller !== 'none' && session.controller !== 'agent') return;
392
+ try {
393
+ const { response, finished } = await serviceRequest(session, {
394
+ method: 'POST',
395
+ path: SURFACE_RESIZE_PATH,
396
+ body: JSON.stringify({ width, height }),
397
+ timeoutMs: QUICK_REQUEST_TIMEOUT_MS,
398
+ });
399
+ const text = await response.text().catch(() => '');
400
+ finished();
401
+ if (response.status !== 200) return;
402
+ let parsed;
403
+ try {
404
+ parsed = surfaceResizeAnswerSchema.safeParse(JSON.parse(text));
405
+ } catch {
406
+ parsed = { success: false };
407
+ }
408
+ if (parsed.success) send(viewer, { type: 'resized', width: parsed.data.width, height: parsed.data.height });
409
+ } catch {
410
+ // The next frame carries whatever size the service kept.
411
+ }
412
+ };
413
+
414
+ const handleClipboardRead = async (session, viewer, id) => {
415
+ // What was copied inside the surface belongs to whoever is driving it;
416
+ // a viewer that only watches gets nothing, like every other input path.
417
+ if (session.controller !== viewer.id) {
418
+ send(viewer, { type: 'error', code: 'NOT_CONTROLLING', message: 'Take control of the surface to copy from it.' });
419
+ return;
420
+ }
421
+ try {
422
+ const { response, finished } = await serviceRequest(session, {
423
+ method: 'GET',
424
+ path: SURFACE_CLIPBOARD_PATH,
425
+ timeoutMs: QUICK_REQUEST_TIMEOUT_MS,
426
+ });
427
+ const text = await response.text().catch(() => '');
428
+ finished();
429
+ let parsed;
430
+ try {
431
+ parsed = response.status === 200 ? surfaceClipboardAnswerSchema.safeParse(JSON.parse(text)) : { success: false };
432
+ } catch {
433
+ parsed = { success: false };
434
+ }
435
+ if (!parsed.success) {
436
+ send(viewer, { type: 'error', code: 'CLIPBOARD_UNAVAILABLE', message: 'The extension did not provide clipboard text.' });
437
+ return;
438
+ }
439
+ send(viewer, { type: 'clipboard', id, text: parsed.data.text });
440
+ } catch {
441
+ send(viewer, { type: 'error', code: 'CLIPBOARD_UNAVAILABLE', message: 'The extension did not provide clipboard text.' });
442
+ }
443
+ };
444
+
445
+ const handleViewerMessage = (session, viewer, raw) => {
446
+ let parsed;
447
+ try {
448
+ parsed = surfaceViewerMessageSchema.safeParse(JSON.parse(String(raw)));
449
+ } catch {
450
+ parsed = { success: false };
451
+ }
452
+ if (!parsed.success) {
453
+ send(viewer, { type: 'error', code: 'BAD_MESSAGE', message: 'Not a surface message.' });
454
+ return;
455
+ }
456
+ const message = parsed.data;
457
+ switch (message.type) {
458
+ case 'ack':
459
+ if (message.seq === viewer.sentSeq) {
460
+ viewer.awaitingAck = false;
461
+ deliverLatest(session, viewer);
462
+ }
463
+ return;
464
+ case 'input': {
465
+ const handoff = session.handoffs;
466
+ void enqueue(session, () => handleInput(session, viewer, message.events, handoff));
467
+ return;
468
+ }
469
+ case 'release':
470
+ if (session.controller === viewer.id) {
471
+ session.handoffs += 1;
472
+ setController(session, 'none');
473
+ }
474
+ return;
475
+ case 'resize':
476
+ void enqueue(session, () => handleResize(session, viewer, message.width, message.height));
477
+ return;
478
+ case 'clipboard-read':
479
+ // Queued behind the copy chord that preceded it, so the service has
480
+ // already handled the copy when it is asked what was copied.
481
+ void enqueue(session, () => handleClipboardRead(session, viewer, message.id));
482
+ return;
483
+ }
484
+ };
485
+
486
+ const attachViewer = (guestId, guest, ws) => {
487
+ const session = ensureSession(guestId, guest);
488
+ const viewer = { id: createViewerId(), ws, sentSeq: 0, awaitingAck: false, gone: false };
489
+ session.viewers.set(viewer.id, viewer);
490
+ // Started with the first viewer in place: the pump's loop condition is
491
+ // the viewer count, and it is checked before the first await.
492
+ if (!session.pumpAbort) void runPump(session);
493
+ send(viewer, { type: 'hello', viewerId: viewer.id });
494
+ send(viewer, { type: 'control', controller: controllerKind(session), mine: false });
495
+ deliverLatest(session, viewer);
496
+
497
+ ws.on('message', (raw, isBinary) => {
498
+ if (isBinary) return;
499
+ handleViewerMessage(session, viewer, raw);
500
+ });
501
+ ws.on('close', () => {
502
+ viewer.gone = true;
503
+ session.viewers.delete(viewer.id);
504
+ // An ended session (pause, removal, withdrawn approval, service gone)
505
+ // must not talk to the service again: that would start it back up
506
+ // with the authorization this session was opened under.
507
+ if (session.ended) return;
508
+ const held = session.controller === viewer.id;
509
+ if (held) {
510
+ session.handoffs += 1;
511
+ setController(session, 'none');
512
+ }
513
+ if (session.viewers.size > 0) return;
514
+ // Nobody is watching: stop pulling frames. The session stays
515
+ // registered until its queue (including the final "none" above) has
516
+ // drained, so a deactivation in the meantime still finds it and
517
+ // cancels that work instead of letting it restart the service.
518
+ clearAgentTimer(session);
519
+ session.pumpAbort?.abort();
520
+ session.pumpAbort = null;
521
+ void session.queue.then(() => {
522
+ if (session.ended || session.viewers.size > 0) return;
523
+ session.ended = true;
524
+ sessions.delete(guestId);
525
+ session.releaseHold?.();
526
+ session.releaseHold = null;
527
+ });
528
+ });
529
+ ws.on('error', () => {
530
+ try {
531
+ ws.close();
532
+ } catch {
533
+ // ignore
534
+ }
535
+ });
536
+ };
537
+
538
+ const wsServer = new WebSocketServer({ noServer: true, maxPayload: VIEWER_MESSAGE_MAX_BYTES });
539
+
540
+ const upgradeHandler = (req, socket, head) => {
541
+ let pathname = '';
542
+ try {
543
+ pathname = new URL(req.url || '/', 'http://localhost').pathname;
544
+ } catch {
545
+ return;
546
+ }
547
+ const match = SURFACE_WS_PATH.exec(pathname);
548
+ if (!match) return;
549
+ const guestId = match[1];
550
+
551
+ const handleUpgrade = async () => {
552
+ try {
553
+ if (uiAuthController?.enabled) {
554
+ const sessionToken = await uiAuthController.ensureSessionToken?.(req, null);
555
+ if (!sessionToken) {
556
+ rejectWebSocketUpgrade(socket, 401, 'UI authentication required');
557
+ return;
558
+ }
559
+ if (!await isRequestOriginAllowed(req)) {
560
+ rejectWebSocketUpgrade(socket, 403, 'Invalid origin');
561
+ return;
562
+ }
563
+ }
564
+ const guest = await findGuest(guestId).catch(() => null);
565
+ if (!isSurfaceGuest(guest)) {
566
+ rejectWebSocketUpgrade(socket, 404, 'No surface for this extension');
567
+ return;
568
+ }
569
+ wsServer.handleUpgrade(req, socket, head, (ws) => {
570
+ attachViewer(guestId, guest, ws);
571
+ });
572
+ } catch {
573
+ rejectWebSocketUpgrade(socket, 500, 'Upgrade failed');
574
+ }
575
+ };
576
+ void handleUpgrade();
577
+ };
578
+
579
+ server.on('upgrade', upgradeHandler);
580
+
581
+ return {
582
+ /**
583
+ * Read by the browser provider before an action: a user holding the
584
+ * surface must not have the agent clicking under their hands.
585
+ */
586
+ userControls(guestId) {
587
+ const session = sessions.get(guestId);
588
+ return Boolean(session) && session.controller !== 'none' && session.controller !== 'agent';
589
+ },
590
+
591
+ /** The host ran an agent action against this extension's surface. */
592
+ noteAgentActivity(guestId) {
593
+ const session = sessions.get(guestId);
594
+ if (!session || (session.controller !== 'none' && session.controller !== 'agent')) return;
595
+ const first = session.controller !== 'agent';
596
+ session.controller = 'agent';
597
+ clearAgentTimer(session);
598
+ session.agentTimer = setTimer(() => {
599
+ session.agentTimer = null;
600
+ if (session.controller === 'agent') setController(session, 'none');
601
+ }, SURFACE_AGENT_HOLD_MS);
602
+ session.agentTimer.unref?.();
603
+ if (first) broadcastControl(session);
604
+ },
605
+
606
+ /** Pause, removal, or withdrawn approval: viewers are told and dropped. */
607
+ endForGuest(guestId) {
608
+ const session = sessions.get(guestId);
609
+ if (session) endSession(session, 'extension-unavailable');
610
+ },
611
+
612
+ /** Test seam. */
613
+ sessionOf(guestId) {
614
+ return sessions.get(guestId) ?? null;
615
+ },
616
+
617
+ stop() {
618
+ server.off('upgrade', upgradeHandler);
619
+ for (const session of [...sessions.values()]) endSession(session, 'host-shutdown');
620
+ for (const client of wsServer.clients) {
621
+ try {
622
+ client.close(1001, 'server shutting down');
623
+ } catch {
624
+ // ignore
625
+ }
626
+ }
627
+ wsServer.close();
628
+ },
629
+ };
630
+ };