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