@vantran-se/openchamber-web 1.24.4 → 2.0.2
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.
- package/README.md +2 -8
- package/bin/lib/commands-connect-url.js +3 -8
- package/bin/lib/commands-tunnel.js +3 -0
- package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-B82Oye3V.js} +1 -1
- package/dist/assets/DiffView-DJLmWn6Z.js +4 -0
- package/dist/assets/FilesView-DjksmZRo.js +17 -0
- package/dist/assets/{GitView-Byu2DY7s.js → GitView-DiUsUIgr.js} +4 -4
- package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-CYwWYLr3.js} +1 -1
- package/dist/assets/GuestSurfacePane-CmctyRI4.js +1 -0
- package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-DOHHcbgJ.js} +1 -1
- package/dist/assets/LinearIssuesView-DaqnylcV.js +1 -0
- package/dist/assets/MarkdownImageGallery-HJNvWt7L.js +1 -0
- package/dist/assets/MarkdownRendererImpl-nDZRGg2I.js +12 -0
- package/dist/assets/OnboardingScreen-C2B5Bsof.js +1 -0
- package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-C4FLBjpt.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-C_1S9c9n.js} +1 -1
- package/dist/assets/PlanView-_eZvb-G5.js +5 -0
- package/dist/assets/PluginPane-sM8fcZIV.js +3 -0
- package/dist/assets/SettingsView-BRfr46hb.js +29 -0
- package/dist/assets/SettingsWindow-CcUDwQHg.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-CrN1ZGwl.js +1 -0
- package/dist/assets/ToolOutputDialog-Cij4jI09.js +15 -0
- package/dist/assets/ToolPartDiffPreview-BXzwQ-JY.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-DSWTLJNH.js} +4 -4
- package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-CpUUgY8H.js} +2 -2
- package/dist/assets/appThemeRegistry-B5tHTkR8.js +1 -0
- package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-D7D8qV8p.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-cq9uQ30i.js} +1 -1
- package/dist/assets/de-CUO1-KqF.js +10 -0
- package/dist/assets/es-BlbC6sGJ.js +16 -0
- package/dist/assets/fr-D2nwe_0W.js +14 -0
- package/dist/assets/index-BQOoYIOu.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D0obJ2p8.js} +1 -1
- package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-DB3_rjyW.js} +1 -1
- package/dist/assets/ja-CA7vnDsZ.js +16 -0
- package/dist/assets/ko-Chy6a9l-.js +16 -0
- package/dist/assets/main-B9JFK3vF.js +896 -0
- package/dist/assets/main-Bhiqnrhd.js +2 -0
- package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-tfqOFaBd.js} +4 -4
- package/dist/assets/miniChat-B0UOL5zA.js +2 -0
- package/dist/assets/mobile-BJppFIEo.js +2 -0
- package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-Db6KuwCP.js} +3 -3
- package/dist/assets/pl-CTR5A_1P.js +16 -0
- package/dist/assets/pt-BR-BE1O1KEx.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-Yz5sfoFh.js +2 -0
- package/dist/assets/renderMobileApp-QQ-yB8aY.js +25 -0
- package/dist/assets/runtimeEndpointReset-DKDdzRzm.js +27 -0
- package/dist/assets/scheduledTasksApi-C5sCuNqf.js +1 -0
- package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-xURoo5t_.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-BZLGuYTU.js} +1 -1
- package/dist/assets/tr-Bh5vK6cW.js +7 -0
- package/dist/assets/uk-CLDkZFy_.js +16 -0
- package/dist/assets/useAppFontEffects-BxrtCnfR.js +7337 -0
- package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CeXuR-SR.js} +2 -2
- package/dist/assets/useRootScrollLock-DgWxezc1.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-DOuaQUZ0.js} +2 -2
- package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
- package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
- package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
- package/dist/assets/{web-components-DxX8bZ1o.js → web-components-63H1h21j.js} +2 -2
- package/dist/assets/zh-CN-DODgcmYj.js +16 -0
- package/dist/assets/zh-TW-ByNfIwLA.js +16 -0
- package/dist/index.html +13 -7
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +6 -5
- package/server/index.d.ts +1 -0
- package/server/index.js +246 -91
- package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
- package/server/lib/agent-tool/runtime.js +135 -54
- package/server/lib/agent-tool/runtime.test.js +155 -87
- package/server/lib/browser-control/DOCUMENTATION.md +37 -2
- package/server/lib/browser-control/provider.js +217 -0
- package/server/lib/browser-control/provider.test.js +251 -0
- package/server/lib/context-obligatory/runtime.js +61 -50
- package/server/lib/context-obligatory/runtime.test.js +62 -162
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +68 -57
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-hub.js +10 -1
- package/server/lib/event-stream/global-hub.test.js +27 -26
- package/server/lib/event-stream/index.js +6 -0
- package/server/lib/event-stream/protocol.js +26 -19
- package/server/lib/event-stream/protocol.test.js +18 -7
- package/server/lib/event-stream/rebind.test.js +5 -5
- package/server/lib/event-stream/runtime.test.js +17 -15
- package/server/lib/event-stream/translate-v2.js +313 -0
- package/server/lib/event-stream/translate-v2.test.js +209 -0
- package/server/lib/event-stream/upstream-reader.test.js +16 -16
- package/server/lib/fs/DOCUMENTATION.md +4 -2
- package/server/lib/fs/byte-range.js +35 -0
- package/server/lib/fs/byte-range.test.js +31 -0
- package/server/lib/fs/routes.js +47 -42
- package/server/lib/fs/routes.test.js +123 -18
- package/server/lib/git/service.js +12 -0
- package/server/lib/github/DOCUMENTATION.md +1 -1
- package/server/lib/github/pr-status.js +22 -2
- package/server/lib/github/pr-status.test.js +27 -14
- package/server/lib/guests/DOCUMENTATION.md +13 -0
- package/server/lib/guests/catalog.js +12 -0
- package/server/lib/guests/catalog.test.js +33 -0
- package/server/lib/guests/routes.js +10 -1
- package/server/lib/guests/service.js +192 -14
- package/server/lib/guests/service.test.js +201 -1
- package/server/lib/guests/surface.js +630 -0
- package/server/lib/guests/surface.test.js +486 -0
- package/server/lib/markdown-image-grants/routes.js +18 -5
- package/server/lib/markdown-image-grants/routes.test.js +9 -6
- package/server/lib/message-queue/DOCUMENTATION.md +33 -22
- package/server/lib/message-queue/runtime.js +155 -100
- package/server/lib/message-queue/runtime.test.js +168 -82
- package/server/lib/notifications/DOCUMENTATION.md +28 -0
- package/server/lib/notifications/control-stream.http.test.js +68 -0
- package/server/lib/notifications/emitter-runtime.js +15 -0
- package/server/lib/notifications/emitter-runtime.test.js +32 -0
- package/server/lib/notifications/push-runtime.js +5 -1
- package/server/lib/notifications/push-runtime.test.js +2 -2
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +115 -26
- package/server/lib/notifications/template-runtime.js +13 -15
- package/server/lib/notifications/template-runtime.test.js +16 -9
- package/server/lib/notifications/trigger-runtime.test.js +187 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
- package/server/lib/openchamber-control/actions.js +3 -2
- package/server/lib/openchamber-control/error.js +1 -1
- package/server/lib/openchamber-control/file-open.js +59 -0
- package/server/lib/openchamber-control/file-open.test.js +57 -0
- package/server/lib/openchamber-control/routes.test.js +12 -0
- package/server/lib/openchamber-control/service.js +102 -49
- package/server/lib/openchamber-control/service.test.js +84 -44
- package/server/lib/openchamber-sessions/archive-store.js +224 -0
- package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
- package/server/lib/openchamber-sessions/opencode-client.js +22 -0
- package/server/lib/openchamber-sessions/routes.js +398 -328
- package/server/lib/openchamber-sessions/routes.test.js +777 -740
- package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
- package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
- package/server/lib/opencode/DOCUMENTATION.md +449 -29
- package/server/lib/opencode/agents.js +233 -327
- package/server/lib/opencode/auth.js +41 -44
- package/server/lib/opencode/auth.test.js +92 -0
- package/server/lib/opencode/cli-upgrade.d.ts +4 -0
- package/server/lib/opencode/cli-upgrade.js +18 -0
- package/server/lib/opencode/cli-upgrade.test.js +36 -0
- package/server/lib/opencode/commands.js +164 -145
- package/server/lib/opencode/compatibility.d.ts +16 -0
- package/server/lib/opencode/compatibility.js +88 -0
- package/server/lib/opencode/compatibility.test.js +62 -0
- package/server/lib/opencode/config-entity-routes.js +58 -29
- package/server/lib/opencode/config-entity-v2.test.js +290 -0
- package/server/lib/opencode/config-mutation-response.js +17 -7
- package/server/lib/opencode/config-mutation-response.test.js +4 -7
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.d.ts +183 -0
- package/server/lib/opencode/config-v2.js +801 -0
- package/server/lib/opencode/config-v2.test.js +466 -0
- package/server/lib/opencode/credential-db.js +144 -0
- package/server/lib/opencode/credential-db.test.js +104 -0
- package/server/lib/opencode/env-runtime.js +48 -9
- package/server/lib/opencode/env-runtime.test.js +45 -0
- package/server/lib/opencode/feature-routes-runtime.js +15 -5
- package/server/lib/opencode/lifecycle-process.test.js +7 -4
- package/server/lib/opencode/lifecycle.js +85 -30
- package/server/lib/opencode/lifecycle.test.js +80 -8
- package/server/lib/opencode/login-shell-env.js +27 -0
- package/server/lib/opencode/managed-config-file.js +119 -0
- package/server/lib/opencode/managed-config-file.test.js +152 -0
- package/server/lib/opencode/managed-plugin-config.js +33 -13
- package/server/lib/opencode/mcp.js +43 -149
- package/server/lib/opencode/network-runtime.js +6 -7
- package/server/lib/opencode/openchamber-routes.test.js +2 -5
- package/server/lib/opencode/plugin-routes.js +3 -3
- package/server/lib/opencode/plugin-routes.test.js +6 -24
- package/server/lib/opencode/plugins.js +91 -40
- package/server/lib/opencode/plugins.test.js +28 -15
- package/server/lib/opencode/providers.js +63 -83
- package/server/lib/opencode/providers.test.js +128 -59
- package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
- package/server/lib/opencode/proxy.js +179 -129
- package/server/lib/opencode/pwa-manifest-routes.js +4 -2
- package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
- package/server/lib/opencode/response-envelope.js +14 -0
- package/server/lib/opencode/response-envelope.test.js +23 -0
- package/server/lib/opencode/routes-upgrade.test.js +73 -116
- package/server/lib/opencode/routes.js +121 -428
- package/server/lib/opencode/server-utils-runtime.js +10 -4
- package/server/lib/opencode/session-activity.js +78 -0
- package/server/lib/opencode/session-runtime.js +21 -10
- package/server/lib/opencode/session-runtime.test.js +9 -9
- package/server/lib/opencode/settings-helpers.js +9 -9
- package/server/lib/opencode/settings-helpers.test.js +7 -12
- package/server/lib/opencode/settings-registry.json +6 -8
- package/server/lib/opencode/settings-runtime.js +18 -1
- package/server/lib/opencode/settings-runtime.test.js +31 -2
- package/server/lib/opencode/shared.js +54 -22
- package/server/lib/opencode/shared.test.js +8 -4
- package/server/lib/opencode/shutdown-runtime.js +54 -7
- package/server/lib/opencode/shutdown-runtime.test.js +224 -30
- package/server/lib/opencode/skill-routes.js +23 -24
- package/server/lib/opencode/skills.js +7 -5
- package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
- package/server/lib/opencode/upgrade-capability.test.js +1 -1
- package/server/lib/opencode/v1-migration-topup.js +288 -0
- package/server/lib/opencode/v1-migration-topup.test.js +266 -0
- package/server/lib/opencode/v2-install.d.ts +8 -0
- package/server/lib/opencode/v2-install.js +158 -0
- package/server/lib/opencode/v2-install.test.js +124 -0
- package/server/lib/opencode/watcher.js +10 -3
- package/server/lib/opencode/watcher.test.js +17 -21
- package/server/lib/opencode/websearch-config.js +66 -0
- package/server/lib/package-manager.js +55 -68
- package/server/lib/package-manager.test.js +241 -75
- package/server/lib/permission-auto-accept/runtime.js +38 -22
- package/server/lib/permission-auto-accept/runtime.test.js +39 -17
- package/server/lib/quota/DOCUMENTATION.md +4 -0
- package/server/lib/quota/providers/xai.js +4 -4
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +7 -0
- package/server/lib/routing/DOCUMENTATION.md +52 -21
- package/server/lib/routing/defaults.js +23 -6
- package/server/lib/routing/jev.js +17 -7
- package/server/lib/routing/routes.http.test.js +68 -33
- package/server/lib/routing/routes.js +65 -22
- package/server/lib/routing/runtime.js +145 -48
- package/server/lib/routing/runtime.test.js +79 -55
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
- package/server/lib/scheduled-tasks/runtime.js +81 -74
- package/server/lib/scheduled-tasks/runtime.test.js +53 -1
- package/server/lib/session-assist/DOCUMENTATION.md +8 -3
- package/server/lib/session-assist/context.js +122 -46
- package/server/lib/session-assist/context.test.js +117 -49
- package/server/lib/session-assist/runtime.js +52 -22
- package/server/lib/session-assist/runtime.test.js +145 -173
- package/server/lib/session-goal/DOCUMENTATION.md +23 -15
- package/server/lib/session-goal/create.js +9 -12
- package/server/lib/session-goal/create.test.js +27 -28
- package/server/lib/session-goal/runtime.js +164 -77
- package/server/lib/session-goal/runtime.test.js +271 -519
- package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
- package/server/lib/session-knowledge/runtime.js +59 -41
- package/server/lib/session-knowledge/runtime.test.js +90 -20
- package/server/lib/small-model/DOCUMENTATION.md +154 -208
- package/server/lib/small-model/client.js +113 -0
- package/server/lib/small-model/index.js +296 -158
- package/server/lib/small-model/index.test.js +423 -256
- package/server/lib/small-model/routes.js +1 -1
- package/server/lib/spaces/DOCUMENTATION.md +807 -0
- package/server/lib/spaces/code-in-bait.js +185 -0
- package/server/lib/spaces/code-in.js +641 -0
- package/server/lib/spaces/code-in.test.js +1048 -0
- package/server/lib/spaces/errors.js +8 -0
- package/server/lib/spaces/exec-http.js +94 -0
- package/server/lib/spaces/gatekeeper-channel.js +202 -0
- package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
- package/server/lib/spaces/gatekeeper-program.cjs +914 -0
- package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
- package/server/lib/spaces/hardening.js +457 -0
- package/server/lib/spaces/hardening.test.js +368 -0
- package/server/lib/spaces/host-git.js +127 -0
- package/server/lib/spaces/host-git.test.js +137 -0
- package/server/lib/spaces/labels.js +234 -0
- package/server/lib/spaces/labels.test.js +219 -0
- package/server/lib/spaces/layout.js +184 -0
- package/server/lib/spaces/layout.test.js +30 -0
- package/server/lib/spaces/manager.js +82 -0
- package/server/lib/spaces/manager.test.js +114 -0
- package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
- package/server/lib/spaces/places/contract-suite.js +156 -0
- package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
- package/server/lib/spaces/places/contract.test.js +6 -0
- package/server/lib/spaces/places/docker-engine.js +126 -0
- package/server/lib/spaces/places/docker-live-support.js +202 -0
- package/server/lib/spaces/places/docker-tools.js +186 -0
- package/server/lib/spaces/places/docker.js +587 -0
- package/server/lib/spaces/places/docker.test.js +1610 -0
- package/server/lib/spaces/places/escape-suite.js +1018 -0
- package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
- package/server/lib/spaces/places/fake-docker.js +375 -0
- package/server/lib/spaces/places/memory-place.js +103 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
- package/server/lib/spaces/places/registry.js +43 -0
- package/server/lib/spaces/places/registry.test.js +50 -0
- package/server/lib/spaces/places/server.docker.live.test.js +225 -0
- package/server/lib/spaces/run-command.js +261 -0
- package/server/lib/spaces/run-command.test.js +396 -0
- package/server/lib/spaces/space-server.js +190 -0
- package/server/lib/spaces/space-server.test.js +335 -0
- package/server/lib/spaces/tools-filler.js +82 -0
- package/server/lib/spaces/tools-filler.test.js +148 -0
- package/server/lib/spaces/tools-pack.js +49 -0
- package/server/lib/spaces/tools-pack.test.js +57 -0
- package/server/lib/spaces/tools.js +138 -0
- package/server/lib/spaces/tools.test.js +153 -0
- package/server/lib/tts/openai-sdk.js +11 -0
- package/server/lib/tts/service.js +7 -5
- package/server/lib/tts/stt.js +3 -2
- package/server/lib/tunnels/DOCUMENTATION.md +2 -0
- package/server/lib/tunnels/routes.js +7 -0
- package/server/lib/tunnels/routes.test.js +55 -0
- package/server/lib/ui-auth/ui-auth.js +38 -2
- package/server/lib/ui-auth/ui-auth.test.js +42 -0
- package/server/lib/ui-auth/ui-passkeys.js +12 -10
- package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
- package/server/lib/walkthrough/pull-request.js +61 -8
- package/server/lib/walkthrough/pull-request.test.js +54 -1
- package/server/lib/walkthrough/routes.js +20 -0
- package/server/lib/walkthrough/routes.test.js +19 -0
- package/server/opencode-proxy.test.js +190 -166
- package/server/opencode-worktree-readiness.test.js +5 -4
- package/dist/assets/DiffView-B2CD9wC8.js +0 -4
- package/dist/assets/FilesView-C4vMFclc.js +0 -13
- package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
- package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
- package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
- package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
- package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
- package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
- package/dist/assets/SettingsView-CSinuH-1.js +0 -29
- package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
- package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
- package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
- package/dist/assets/de-Bro6_zyl.js +0 -10
- package/dist/assets/es-Bb9XQZvd.js +0 -16
- package/dist/assets/fr-Dgfzn-IL.js +0 -14
- package/dist/assets/index-CtQNmqUL.js +0 -1
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/index-Dqc6USl3.js +0 -40
- package/dist/assets/ja-BYsakGFh.js +0 -16
- package/dist/assets/ko-BSaQz9aN.js +0 -16
- package/dist/assets/main-B62_pjL5.js +0 -2
- package/dist/assets/main-BEdcOI2P.js +0 -896
- package/dist/assets/miniChat-LT8Rqofr.js +0 -2
- package/dist/assets/mobile-CqarEynh.js +0 -2
- package/dist/assets/pl-OIviPdoj.js +0 -16
- package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
- package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
- package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
- package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
- package/dist/assets/tr-CQI-KPre.js +0 -7
- package/dist/assets/uk-CqW7R4DA.js +0 -16
- package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
- package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
- package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
- package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
- package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
- package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
- package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
- package/server/lib/routing/feature-flag.js +0 -11
- package/server/lib/small-model/call.js +0 -851
- package/server/lib/small-model/call.test.js +0 -1292
- package/server/lib/small-model/catalog.js +0 -13
- package/server/lib/small-model/resolve.js +0 -131
- package/server/lib/small-model/resolve.test.js +0 -197
- package/server/lib/small-model/runtime-providers.js +0 -164
- package/server/lib/small-model/runtime-providers.test.js +0 -118
- package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
- package/server/lib/system-prompt/runtime.js +0 -49
- package/server/lib/system-prompt/runtime.test.js +0 -76
- 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
|
+
};
|