@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,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chooses who answers the agent's `browser.*` actions: the in-app browser view
|
|
3
|
+
* (through the broker) or an extension service that provides the browser
|
|
4
|
+
* (`contributes.service.provides: ["browser"]`).
|
|
5
|
+
*
|
|
6
|
+
* The choice is the `browserProvider` setting: `builtin`, or the id of an
|
|
7
|
+
* installed extension. It is read for every action, so a change in Settings
|
|
8
|
+
* applies to the next action without a restart. A selected extension that can
|
|
9
|
+
* no longer serve — paused, removed, approval withdrawn, or a newer version
|
|
10
|
+
* without the role — is not silently kept: the setting goes back to `builtin`,
|
|
11
|
+
* every client is told so it can say so, and the action runs in-app. Doing
|
|
12
|
+
* that at the moment the extension is deactivated (`handleGuestDeactivated`)
|
|
13
|
+
* gives the user the notice right away; doing it again on the next action
|
|
14
|
+
* covers everything else, such as a folder install that went missing.
|
|
15
|
+
*
|
|
16
|
+
* The service is a host-spawned loopback process, so the proxy that panels
|
|
17
|
+
* use serves here too. It starts the process on the first action, which is
|
|
18
|
+
* what lets an agent browse with no panel open, and stops it after
|
|
19
|
+
* `BROWSER_PROVIDER_IDLE_MS` without actions.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
BROWSER_PROVIDER_ACTION_TIMEOUT_MS,
|
|
24
|
+
BROWSER_PROVIDER_IDLE_MS,
|
|
25
|
+
BROWSER_PROVIDER_OPEN_TIMEOUT_MS,
|
|
26
|
+
BROWSER_PROVIDER_PATH,
|
|
27
|
+
BROWSER_PROVIDER_RESPONSE_MAX,
|
|
28
|
+
isGuestApproved,
|
|
29
|
+
requestedGuestCapabilities,
|
|
30
|
+
serviceProvides,
|
|
31
|
+
} from '@openchamber/sdk';
|
|
32
|
+
import { browserProviderResultSchema } from '@openchamber/sdk/schemas';
|
|
33
|
+
|
|
34
|
+
import { GuestServiceError, proxyGuestServiceRequest } from '../guests/service.js';
|
|
35
|
+
import { BrowserControlError } from './broker.js';
|
|
36
|
+
|
|
37
|
+
const BUILTIN_BROWSER_PROVIDER = 'builtin';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether this catalog row can answer browser actions right now. Mirrors what
|
|
41
|
+
* the Settings dropdown offers: enabled, fully approved, and declaring the
|
|
42
|
+
* role. The `service` grant is implied by full approval.
|
|
43
|
+
*/
|
|
44
|
+
/** The control service builds the caller's scope; a caller without one (tests, CLI) means unknown. */
|
|
45
|
+
const UNKNOWN_CONTEXT = Object.freeze({ directory: null, sessionId: null });
|
|
46
|
+
|
|
47
|
+
export const isBrowserProviderGuest = (guest) => (
|
|
48
|
+
Boolean(guest)
|
|
49
|
+
&& guest.enabled !== false
|
|
50
|
+
&& serviceProvides(guest.service, 'browser')
|
|
51
|
+
&& isGuestApproved({
|
|
52
|
+
requested: requestedGuestCapabilities(guest),
|
|
53
|
+
granted: Array.isArray(guest.capabilityGrants) ? guest.capabilityGrants : [],
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {{
|
|
59
|
+
* broker: { request: Function },
|
|
60
|
+
* readSettings: () => Promise<Record<string, unknown> | null>,
|
|
61
|
+
* persistSettings: (changes: Record<string, unknown>) => Promise<unknown>,
|
|
62
|
+
* findGuest: (id: string) => Promise<object | null>,
|
|
63
|
+
* persistPath: string,
|
|
64
|
+
* emitProviderReset: (event: { guestId: string, guestName: string }) => void,
|
|
65
|
+
* createId: () => string,
|
|
66
|
+
* proxyServiceRequest?: typeof proxyGuestServiceRequest,
|
|
67
|
+
* surfaceControl?: { userControls: (guestId: string) => boolean, noteAgentActivity: (guestId: string) => void },
|
|
68
|
+
* }} deps `surfaceControl` is the shared-surface lease: an action is refused
|
|
69
|
+
* while the user is driving the extension's surface, and every action the
|
|
70
|
+
* provider runs counts as agent activity there.
|
|
71
|
+
*/
|
|
72
|
+
export const createBrowserControlRouter = ({
|
|
73
|
+
broker,
|
|
74
|
+
readSettings,
|
|
75
|
+
persistSettings,
|
|
76
|
+
findGuest,
|
|
77
|
+
persistPath,
|
|
78
|
+
emitProviderReset,
|
|
79
|
+
createId,
|
|
80
|
+
proxyServiceRequest = proxyGuestServiceRequest,
|
|
81
|
+
surfaceControl = { userControls: () => false, noteAgentActivity: () => undefined },
|
|
82
|
+
}) => {
|
|
83
|
+
// `browserProvider` was sanitized on write (settings-helpers.js keeps a
|
|
84
|
+
// trimmed non-empty string), so the only decision left is builtin or not.
|
|
85
|
+
// A read that fails is not "builtin": the action must not run somewhere
|
|
86
|
+
// the user did not choose.
|
|
87
|
+
const selectedProviderId = async () => {
|
|
88
|
+
let settings;
|
|
89
|
+
try {
|
|
90
|
+
settings = await readSettings();
|
|
91
|
+
} catch {
|
|
92
|
+
throw new BrowserControlError(
|
|
93
|
+
'OpenChamber could not read which browser answers agent actions, so the action was not run. Try again.',
|
|
94
|
+
503,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const value = settings?.browserProvider ?? BUILTIN_BROWSER_PROVIDER;
|
|
98
|
+
return value === BUILTIN_BROWSER_PROVIDER ? null : String(value);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const resetToBuiltin = async ({ guestId, guestName }) => {
|
|
102
|
+
await persistSettings({ browserProvider: BUILTIN_BROWSER_PROVIDER });
|
|
103
|
+
emitProviderReset({ guestId, guestName });
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const requestFromProvider = async (guest, action, parameters, { signal, timeoutMs, context }) => {
|
|
107
|
+
if (surfaceControl.userControls(guest.id)) {
|
|
108
|
+
// Read by the agent: the page is being used by a person right now.
|
|
109
|
+
throw new BrowserControlError(
|
|
110
|
+
'The user is interacting with this page in the panel right now, so the action was not run. '
|
|
111
|
+
+ 'Wait for them to hand control back, or ask them to. Nothing was changed.',
|
|
112
|
+
409,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
surfaceControl.noteAgentActivity(guest.id);
|
|
116
|
+
const requestId = createId();
|
|
117
|
+
let proxied;
|
|
118
|
+
try {
|
|
119
|
+
proxied = await proxyServiceRequest({
|
|
120
|
+
guestId: guest.id,
|
|
121
|
+
guestName: guest.name,
|
|
122
|
+
packageRoot: guest.packageRoot,
|
|
123
|
+
service: guest.service,
|
|
124
|
+
granted: guest.capabilityGrants,
|
|
125
|
+
persistPath,
|
|
126
|
+
method: 'POST',
|
|
127
|
+
path: BROWSER_PROVIDER_PATH,
|
|
128
|
+
body: JSON.stringify({ requestId, action, parameters, context: context ?? UNKNOWN_CONTEXT }),
|
|
129
|
+
timeoutMs: timeoutMs ?? (action === 'browser.open' ? BROWSER_PROVIDER_OPEN_TIMEOUT_MS : BROWSER_PROVIDER_ACTION_TIMEOUT_MS),
|
|
130
|
+
responseMax: BROWSER_PROVIDER_RESPONSE_MAX,
|
|
131
|
+
idleStopMs: BROWSER_PROVIDER_IDLE_MS,
|
|
132
|
+
signal,
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof GuestServiceError) {
|
|
136
|
+
// Read by the agent. A request that was sent and lost may have been
|
|
137
|
+
// acted on; only a service that never took it leaves the page as it was.
|
|
138
|
+
const outcome = error.code === 'REQUEST_FAILED' || error.code === 'CANCELLED'
|
|
139
|
+
? 'The action was sent but no answer came back, so it may or may not have run; read the page before repeating it.'
|
|
140
|
+
: 'Nothing was changed.';
|
|
141
|
+
throw new BrowserControlError(
|
|
142
|
+
`The browser provider "${guest.name}" could not complete this action (${error.code}): ${error.message} ${outcome}`,
|
|
143
|
+
error.code === 'REQUEST_FAILED' ? 504 : 503,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
if (proxied.status !== 200) {
|
|
149
|
+
throw new BrowserControlError(
|
|
150
|
+
`The browser provider "${guest.name}" answered HTTP ${proxied.status} instead of a result. Nothing is known about the page.`,
|
|
151
|
+
502,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
parsed = browserProviderResultSchema.safeParse(JSON.parse(proxied.body));
|
|
157
|
+
} catch {
|
|
158
|
+
parsed = { success: false };
|
|
159
|
+
}
|
|
160
|
+
if (!parsed.success) {
|
|
161
|
+
throw new BrowserControlError(
|
|
162
|
+
`The browser provider "${guest.name}" answered something that is not a browser result. Nothing is known about the page.`,
|
|
163
|
+
502,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (!parsed.data.ok) {
|
|
167
|
+
throw new BrowserControlError(parsed.data.error, 400);
|
|
168
|
+
}
|
|
169
|
+
return parsed.data.data;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
/** Same signature as the broker; the control service does not know which path ran. */
|
|
174
|
+
async request(action, parameters = {}, options = {}) {
|
|
175
|
+
const providerId = await selectedProviderId();
|
|
176
|
+
if (!providerId) {
|
|
177
|
+
return broker.request(action, parameters, options);
|
|
178
|
+
}
|
|
179
|
+
let guest;
|
|
180
|
+
try {
|
|
181
|
+
guest = await findGuest(providerId);
|
|
182
|
+
} catch {
|
|
183
|
+
// The catalog could not be read; that says nothing about the
|
|
184
|
+
// extension, so neither the choice nor the target browser changes.
|
|
185
|
+
throw new BrowserControlError(
|
|
186
|
+
'OpenChamber could not read the extension catalog, so the action was not run. Try again.',
|
|
187
|
+
503,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (!isBrowserProviderGuest(guest)) {
|
|
191
|
+
await resetToBuiltin({ guestId: providerId, guestName: guest?.name ?? providerId });
|
|
192
|
+
return broker.request(action, parameters, options);
|
|
193
|
+
}
|
|
194
|
+
return requestFromProvider(guest, action, parameters, options);
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Called when an extension is paused, removed, or loses approval. The
|
|
199
|
+
* catalog may not reflect the change yet, so this does not re-check it:
|
|
200
|
+
* the selection alone decides.
|
|
201
|
+
*/
|
|
202
|
+
async handleGuestDeactivated({ guestId, guestName }) {
|
|
203
|
+
// The pause or removal already happened; a failure here must not turn
|
|
204
|
+
// it into an error response. The next action re-checks the catalog
|
|
205
|
+
// and resets the setting then.
|
|
206
|
+
try {
|
|
207
|
+
const providerId = await selectedProviderId();
|
|
208
|
+
if (providerId !== guestId) return false;
|
|
209
|
+
await resetToBuiltin({ guestId, guestName: guestName || guestId });
|
|
210
|
+
return true;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.warn('[browser-provider] could not reset the provider after a deactivation:', error instanceof Error ? error.message : error);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { BROWSER_PROVIDER_IDLE_MS, BROWSER_PROVIDER_OPEN_TIMEOUT_MS, BROWSER_PROVIDER_PATH } from '@openchamber/sdk';
|
|
4
|
+
|
|
5
|
+
import { BrowserControlError } from './broker.js';
|
|
6
|
+
import { createBrowserControlRouter, isBrowserProviderGuest } from './provider.js';
|
|
7
|
+
import { GuestServiceError } from '../guests/service.js';
|
|
8
|
+
|
|
9
|
+
const providerGuest = (overrides = {}) => ({
|
|
10
|
+
id: 'server-chrome',
|
|
11
|
+
name: 'Server Chrome',
|
|
12
|
+
packageRoot: '/ext/server-chrome',
|
|
13
|
+
enabled: true,
|
|
14
|
+
service: { entry: 'service/main.js', runtime: 'host', provides: ['browser'] },
|
|
15
|
+
capabilityGrants: ['service'],
|
|
16
|
+
panel: { id: 'server-chrome', name: 'Server Chrome', icon: 'window' },
|
|
17
|
+
...overrides,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const createRouter = ({
|
|
21
|
+
settings = { browserProvider: 'server-chrome' },
|
|
22
|
+
guest = providerGuest(),
|
|
23
|
+
serviceAnswer = { status: 200, body: JSON.stringify({ ok: true, data: { url: 'http://localhost:3000/', title: 'App' } }) },
|
|
24
|
+
userControls = false,
|
|
25
|
+
readSettingsError = null,
|
|
26
|
+
findGuestError = null,
|
|
27
|
+
} = {}) => {
|
|
28
|
+
const brokerCalls = [];
|
|
29
|
+
const proxied = [];
|
|
30
|
+
const persisted = [];
|
|
31
|
+
const resets = [];
|
|
32
|
+
const agentActivity = [];
|
|
33
|
+
let current = { ...settings };
|
|
34
|
+
const router = createBrowserControlRouter({
|
|
35
|
+
surfaceControl: {
|
|
36
|
+
userControls: () => userControls,
|
|
37
|
+
noteAgentActivity: (guestId) => agentActivity.push(guestId),
|
|
38
|
+
},
|
|
39
|
+
broker: {
|
|
40
|
+
request: async (action, parameters, options) => {
|
|
41
|
+
brokerCalls.push({ action, parameters, options });
|
|
42
|
+
return { from: 'broker' };
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
readSettings: async () => {
|
|
46
|
+
if (readSettingsError) throw readSettingsError;
|
|
47
|
+
return current;
|
|
48
|
+
},
|
|
49
|
+
persistSettings: async (changes) => {
|
|
50
|
+
persisted.push(changes);
|
|
51
|
+
current = { ...current, ...changes };
|
|
52
|
+
},
|
|
53
|
+
findGuest: async (id) => {
|
|
54
|
+
if (findGuestError) throw findGuestError;
|
|
55
|
+
return guest && guest.id === id ? guest : null;
|
|
56
|
+
},
|
|
57
|
+
persistPath: '/data/extensions.json',
|
|
58
|
+
emitProviderReset: (event) => resets.push(event),
|
|
59
|
+
createId: () => 'req-1',
|
|
60
|
+
proxyServiceRequest: async (params) => {
|
|
61
|
+
proxied.push(params);
|
|
62
|
+
if (serviceAnswer instanceof Error) throw serviceAnswer;
|
|
63
|
+
return serviceAnswer;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return { router, brokerCalls, proxied, persisted, resets, agentActivity };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
describe('isBrowserProviderGuest', () => {
|
|
70
|
+
test('needs enabled, approved, and the browser role', () => {
|
|
71
|
+
expect(isBrowserProviderGuest(providerGuest())).toBe(true);
|
|
72
|
+
expect(isBrowserProviderGuest(providerGuest({ enabled: false }))).toBe(false);
|
|
73
|
+
expect(isBrowserProviderGuest(providerGuest({ capabilityGrants: [] }))).toBe(false);
|
|
74
|
+
expect(isBrowserProviderGuest(providerGuest({ service: { entry: 'service/main.js', runtime: 'host' } }))).toBe(false);
|
|
75
|
+
expect(isBrowserProviderGuest(null)).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('browser control router', () => {
|
|
80
|
+
test('sends the action to the in-app broker when no provider is selected', async () => {
|
|
81
|
+
const { router, brokerCalls, proxied } = createRouter({ settings: {} });
|
|
82
|
+
const result = await router.request('browser.snapshot', {}, { timeoutMs: 20_000 });
|
|
83
|
+
expect(result).toEqual({ from: 'broker' });
|
|
84
|
+
expect(brokerCalls).toHaveLength(1);
|
|
85
|
+
expect(proxied).toHaveLength(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('"builtin" is the in-app broker as well', async () => {
|
|
89
|
+
const { router, brokerCalls } = createRouter({ settings: { browserProvider: 'builtin' } });
|
|
90
|
+
await router.request('browser.snapshot', {});
|
|
91
|
+
expect(brokerCalls).toHaveLength(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('posts the action to the selected provider service and returns its data', async () => {
|
|
95
|
+
const { router, brokerCalls, proxied } = createRouter();
|
|
96
|
+
const result = await router.request('browser.open', { url: 'http://localhost:3000/' }, {
|
|
97
|
+
timeoutMs: 45_000,
|
|
98
|
+
context: { directory: '/repo', sessionId: 'ses_1' },
|
|
99
|
+
});
|
|
100
|
+
expect(result).toEqual({ url: 'http://localhost:3000/', title: 'App' });
|
|
101
|
+
expect(brokerCalls).toHaveLength(0);
|
|
102
|
+
expect(proxied).toHaveLength(1);
|
|
103
|
+
const call = proxied[0];
|
|
104
|
+
expect(call.guestId).toBe('server-chrome');
|
|
105
|
+
expect(call.method).toBe('POST');
|
|
106
|
+
expect(call.path).toBe(BROWSER_PROVIDER_PATH);
|
|
107
|
+
expect(JSON.parse(call.body)).toEqual({
|
|
108
|
+
requestId: 'req-1',
|
|
109
|
+
action: 'browser.open',
|
|
110
|
+
parameters: { url: 'http://localhost:3000/' },
|
|
111
|
+
context: { directory: '/repo', sessionId: 'ses_1' },
|
|
112
|
+
});
|
|
113
|
+
expect(call.idleStopMs).toBe(BROWSER_PROVIDER_IDLE_MS);
|
|
114
|
+
expect(call.granted).toEqual(['service']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('a slow page gets the open budget when the caller sets none', async () => {
|
|
118
|
+
const { router, proxied } = createRouter();
|
|
119
|
+
await router.request('browser.open', { url: 'http://a/' });
|
|
120
|
+
expect(proxied[0].timeoutMs).toBe(BROWSER_PROVIDER_OPEN_TIMEOUT_MS);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a provider error becomes the agent-visible error, not a transport failure', async () => {
|
|
124
|
+
const { router } = createRouter({
|
|
125
|
+
serviceAnswer: { status: 200, body: JSON.stringify({ ok: false, error: 'No element matches #save' }) },
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
await router.request('browser.click', { selector: '#save' });
|
|
129
|
+
throw new Error('expected rejection');
|
|
130
|
+
} catch (error) {
|
|
131
|
+
expect(error).toBeInstanceOf(BrowserControlError);
|
|
132
|
+
expect(error.message).toBe('No element matches #save');
|
|
133
|
+
expect(error.status).toBe(400);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('a non-200 or malformed answer is reported as unknown page state', async () => {
|
|
138
|
+
for (const serviceAnswer of [
|
|
139
|
+
{ status: 500, body: 'boom' },
|
|
140
|
+
{ status: 200, body: 'not json' },
|
|
141
|
+
{ status: 200, body: JSON.stringify({ ok: true }) },
|
|
142
|
+
]) {
|
|
143
|
+
const { router } = createRouter({ serviceAnswer });
|
|
144
|
+
await expect(router.request('browser.snapshot', {})).rejects.toMatchObject({ status: 502 });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('a service that will not start tells the agent nothing changed', async () => {
|
|
149
|
+
const { router } = createRouter({
|
|
150
|
+
serviceAnswer: new GuestServiceError('Guest service failed to become ready.', 'SERVICE_FAILED'),
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
await router.request('browser.snapshot', {});
|
|
154
|
+
throw new Error('expected rejection');
|
|
155
|
+
} catch (error) {
|
|
156
|
+
expect(error).toBeInstanceOf(BrowserControlError);
|
|
157
|
+
expect(error.status).toBe(503);
|
|
158
|
+
expect(error.message).toContain('Server Chrome');
|
|
159
|
+
expect(error.message).toContain('Nothing was changed');
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('a selected extension that cannot serve resets the setting, tells clients, and runs in-app', async () => {
|
|
164
|
+
const { router, brokerCalls, persisted, resets } = createRouter({ guest: providerGuest({ enabled: false }) });
|
|
165
|
+
const result = await router.request('browser.snapshot', {});
|
|
166
|
+
expect(result).toEqual({ from: 'broker' });
|
|
167
|
+
expect(brokerCalls).toHaveLength(1);
|
|
168
|
+
expect(persisted).toEqual([{ browserProvider: 'builtin' }]);
|
|
169
|
+
expect(resets).toEqual([{ guestId: 'server-chrome', guestName: 'Server Chrome' }]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('a removed extension resets by id when no name is known', async () => {
|
|
173
|
+
const { router, resets } = createRouter({ guest: null });
|
|
174
|
+
await router.request('browser.snapshot', {});
|
|
175
|
+
expect(resets).toEqual([{ guestId: 'server-chrome', guestName: 'server-chrome' }]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('deactivating the selected extension resets right away; another one does not', async () => {
|
|
179
|
+
const { router, persisted, resets } = createRouter();
|
|
180
|
+
expect(await router.handleGuestDeactivated({ guestId: 'other', guestName: 'Other' })).toBe(false);
|
|
181
|
+
expect(persisted).toHaveLength(0);
|
|
182
|
+
expect(await router.handleGuestDeactivated({ guestId: 'server-chrome', guestName: 'Server Chrome' })).toBe(true);
|
|
183
|
+
expect(persisted).toEqual([{ browserProvider: 'builtin' }]);
|
|
184
|
+
expect(resets).toEqual([{ guestId: 'server-chrome', guestName: 'Server Chrome' }]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('an action is refused while the user drives the shared surface', async () => {
|
|
188
|
+
const { router, proxied, agentActivity } = createRouter({ userControls: true });
|
|
189
|
+
try {
|
|
190
|
+
await router.request('browser.click', { selector: '#save' });
|
|
191
|
+
throw new Error('expected rejection');
|
|
192
|
+
} catch (error) {
|
|
193
|
+
expect(error).toBeInstanceOf(BrowserControlError);
|
|
194
|
+
expect(error.status).toBe(409);
|
|
195
|
+
expect(error.message).toContain('Nothing was changed');
|
|
196
|
+
}
|
|
197
|
+
expect(proxied).toHaveLength(0);
|
|
198
|
+
expect(agentActivity).toHaveLength(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('a provider action counts as agent activity on the surface', async () => {
|
|
202
|
+
const { router, agentActivity } = createRouter();
|
|
203
|
+
await router.request('browser.snapshot', {});
|
|
204
|
+
expect(agentActivity).toEqual(['server-chrome']);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('a settings read that fails runs nothing anywhere and changes nothing', async () => {
|
|
208
|
+
const { router, brokerCalls, proxied, persisted, resets } = createRouter({ readSettingsError: new Error('disk') });
|
|
209
|
+
await expect(router.request('browser.click', { selector: '#save' })).rejects.toMatchObject({ status: 503 });
|
|
210
|
+
expect(brokerCalls).toHaveLength(0);
|
|
211
|
+
expect(proxied).toHaveLength(0);
|
|
212
|
+
expect(persisted).toHaveLength(0);
|
|
213
|
+
expect(resets).toHaveLength(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('a catalog read that fails runs nothing anywhere and keeps the choice', async () => {
|
|
217
|
+
const { router, brokerCalls, proxied, persisted, resets } = createRouter({ findGuestError: new Error('catalog') });
|
|
218
|
+
await expect(router.request('browser.click', { selector: '#save' })).rejects.toMatchObject({ status: 503 });
|
|
219
|
+
expect(brokerCalls).toHaveLength(0);
|
|
220
|
+
expect(proxied).toHaveLength(0);
|
|
221
|
+
expect(persisted).toHaveLength(0);
|
|
222
|
+
expect(resets).toHaveLength(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('a request that was sent and lost is reported as unknown, not as unchanged', async () => {
|
|
226
|
+
const { router } = createRouter({
|
|
227
|
+
serviceAnswer: new GuestServiceError('Guest service request failed.', 'REQUEST_FAILED'),
|
|
228
|
+
});
|
|
229
|
+
try {
|
|
230
|
+
await router.request('browser.click', { selector: '#save' });
|
|
231
|
+
throw new Error('expected rejection');
|
|
232
|
+
} catch (error) {
|
|
233
|
+
expect(error).toBeInstanceOf(BrowserControlError);
|
|
234
|
+
expect(error.status).toBe(504);
|
|
235
|
+
expect(error.message).toContain('may or may not have run');
|
|
236
|
+
expect(error.message).not.toContain('Nothing was changed');
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('a settings read that fails during deactivation is logged, not raised', async () => {
|
|
241
|
+
const { router, persisted } = createRouter({ readSettingsError: new Error('disk') });
|
|
242
|
+
const warn = console.warn;
|
|
243
|
+
console.warn = () => undefined;
|
|
244
|
+
try {
|
|
245
|
+
expect(await router.handleGuestDeactivated({ guestId: 'server-chrome', guestName: 'Server Chrome' })).toBe(false);
|
|
246
|
+
} finally {
|
|
247
|
+
console.warn = warn;
|
|
248
|
+
}
|
|
249
|
+
expect(persisted).toHaveLength(0);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unwrapOpenCodeResponse } from '../opencode/response-envelope.js';
|
|
1
2
|
const FETCH_TIMEOUT_MS = 15_000;
|
|
2
3
|
const MESSAGE_FETCH_LIMIT = 20;
|
|
3
4
|
|
|
@@ -30,10 +31,20 @@ const buildContextPrompt = (entries) => {
|
|
|
30
31
|
].join('\n');
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Re-injects the messages the user pinned as obligatory after a compaction.
|
|
36
|
+
*
|
|
37
|
+
* Both the pinned list and the cursor live in OpenChamber's own session
|
|
38
|
+
* metadata store — v2 accepts session metadata only at create time.
|
|
39
|
+
* `readSessionMetadata` reads it and `persistContextCursor` records how far we
|
|
40
|
+
* got; without both seams the runtime stays inert.
|
|
41
|
+
*/
|
|
33
42
|
export const createContextObligatoryRuntime = ({
|
|
34
43
|
buildOpenCodeUrl,
|
|
35
44
|
getOpenCodeAuthHeaders,
|
|
36
45
|
sessionKnowledgeRuntime = null,
|
|
46
|
+
persistContextCursor = null,
|
|
47
|
+
readSessionMetadata = null,
|
|
37
48
|
}) => {
|
|
38
49
|
const inflight = new Set();
|
|
39
50
|
let stopped = false;
|
|
@@ -53,13 +64,15 @@ export const createContextObligatoryRuntime = ({
|
|
|
53
64
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
54
65
|
});
|
|
55
66
|
if (!response.ok) throw new Error(`OpenCode ${method} ${fetchPath} failed with ${response.status}`);
|
|
56
|
-
return response.json().catch(() => null);
|
|
67
|
+
return unwrapOpenCodeResponse(await response.json().catch(() => null));
|
|
57
68
|
};
|
|
58
69
|
|
|
59
70
|
const tick = async (sessionId, directory) => {
|
|
60
|
-
const session = await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, { directory });
|
|
71
|
+
const session = await openCodeFetch(`/api/session/${encodeURIComponent(sessionId)}`, { directory });
|
|
61
72
|
if (session?.parentID) return;
|
|
62
|
-
|
|
73
|
+
// Pins and the cursor are OpenChamber's, not OpenCode's.
|
|
74
|
+
const stored = { metadata: await readSessionMetadata(sessionId) };
|
|
75
|
+
const state = readContextState(stored);
|
|
63
76
|
|
|
64
77
|
/**
|
|
65
78
|
* Project knowledge rides along with the pinned messages. Compaction takes
|
|
@@ -74,32 +87,40 @@ export const createContextObligatoryRuntime = ({
|
|
|
74
87
|
// Compaction removed the previously delivered block, so its stored
|
|
75
88
|
// signature is no longer evidence that the session still carries it.
|
|
76
89
|
'',
|
|
77
|
-
sessionKnowledgeRuntime.readPins(
|
|
90
|
+
sessionKnowledgeRuntime.readPins(stored),
|
|
78
91
|
)
|
|
79
92
|
.catch(() => ({ text: '', signature: '' }))
|
|
80
93
|
: { text: '', signature: '' };
|
|
81
94
|
|
|
82
95
|
if (state.messages.length === 0 && !knowledge.text) return;
|
|
83
96
|
|
|
84
|
-
const recent = await openCodeFetch(`/session/${encodeURIComponent(sessionId)}/message`, {
|
|
97
|
+
const recent = await openCodeFetch(`/api/session/${encodeURIComponent(sessionId)}/message`, {
|
|
85
98
|
directory,
|
|
86
99
|
query: { limit: String(MESSAGE_FETCH_LIMIT) },
|
|
87
100
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
// v2 pages messages as `{ data, cursor }`, newest first, and a compaction
|
|
102
|
+
// is its own message role rather than an assistant message flagged
|
|
103
|
+
// `summary`.
|
|
104
|
+
const recentMessages = Array.isArray(recent?.data) ? recent.data : [];
|
|
105
|
+
if (recentMessages.length === 0) return;
|
|
106
|
+
const summary = recentMessages.find((message) => message?.type === 'compaction' && message?.status === 'completed');
|
|
91
107
|
if (!summary?.id || !summary?.time?.completed) return;
|
|
92
108
|
if (state.openchamber.context_obligatory_last_compaction_message_id === summary.id) return;
|
|
93
109
|
|
|
94
110
|
const fetched = await Promise.allSettled(state.messages.map(async (pinned) => {
|
|
95
111
|
const message = await openCodeFetch(
|
|
96
|
-
`/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(pinned.id)}`,
|
|
112
|
+
`/api/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(pinned.id)}`,
|
|
97
113
|
{ directory },
|
|
98
114
|
);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
: '';
|
|
115
|
+
// A user message carries `text`; an assistant one carries `content[]`.
|
|
116
|
+
const parts = Array.isArray(message?.content)
|
|
117
|
+
? message.content
|
|
118
|
+
: (typeof message?.text === 'string' ? [{ type: 'text', text: message.text }] : []);
|
|
119
|
+
const text = parts
|
|
120
|
+
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
121
|
+
.map((part) => part.text.trim())
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join('\n\n');
|
|
103
124
|
return { pinned, text };
|
|
104
125
|
}));
|
|
105
126
|
const entries = fetched
|
|
@@ -108,52 +129,42 @@ export const createContextObligatoryRuntime = ({
|
|
|
108
129
|
.sort((left, right) => left.pinned.createdAt - right.pinned.createdAt);
|
|
109
130
|
if (entries.length === 0 && !knowledge.text) return;
|
|
110
131
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const modelID = typeof executionInfo?.modelID === 'string' ? executionInfo.modelID : '';
|
|
115
|
-
if (!providerID || !modelID) throw new Error('no pre-compaction assistant provider/model');
|
|
116
|
-
const agent = typeof executionInfo.agent === 'string' ? executionInfo.agent : executionInfo.mode;
|
|
117
|
-
await openCodeFetch(`/session/${encodeURIComponent(sessionId)}/prompt_async`, {
|
|
132
|
+
// v2 has no inline synthetic parts, and the model/agent selection lives on
|
|
133
|
+
// the session, so the restored context is simply its own synthetic message.
|
|
134
|
+
await openCodeFetch(`/api/session/${encodeURIComponent(sessionId)}/synthetic`, {
|
|
118
135
|
directory,
|
|
119
136
|
method: 'POST',
|
|
120
137
|
body: {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
text: [knowledge.text, entries.length > 0 ? buildContextPrompt(entries) : '']
|
|
126
|
-
.filter(Boolean)
|
|
127
|
-
.join('\n\n---\n\n'),
|
|
128
|
-
synthetic: true,
|
|
129
|
-
}],
|
|
138
|
+
text: [knowledge.text, entries.length > 0 ? buildContextPrompt(entries) : '']
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join('\n\n---\n\n'),
|
|
141
|
+
resume: false,
|
|
130
142
|
},
|
|
131
143
|
});
|
|
132
144
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
context_obligatory_last_compaction_message_id: summary.id,
|
|
144
|
-
// Recorded together with the cursor: the session now carries this
|
|
145
|
-
// knowledge again, so the next send must not repeat it.
|
|
146
|
-
...(knowledge.signature
|
|
147
|
-
? { [sessionKnowledgeRuntime.metadataKey]: knowledge.signature }
|
|
148
|
-
: {}),
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
});
|
|
145
|
+
// A merge patch: the store folds this into whatever else the session's
|
|
146
|
+
// `openchamber` namespace holds, so a concurrent goal or assist write is
|
|
147
|
+
// not clobbered.
|
|
148
|
+
const patch = { context_obligatory_last_compaction_message_id: summary.id };
|
|
149
|
+
if (knowledge.signature) {
|
|
150
|
+
// Recorded together with the cursor: the session now carries this
|
|
151
|
+
// knowledge again, so the next send must not repeat it.
|
|
152
|
+
patch[sessionKnowledgeRuntime.metadataKey] = knowledge.signature;
|
|
153
|
+
}
|
|
154
|
+
await persistContextCursor(sessionId, directory, { openchamber: patch });
|
|
153
155
|
};
|
|
154
156
|
|
|
157
|
+
let parkedNoticeLogged = false;
|
|
155
158
|
const processPayload = (payload, directoryHint = '') => {
|
|
156
|
-
if (stopped
|
|
159
|
+
if (stopped) return;
|
|
160
|
+
if (typeof persistContextCursor !== 'function' || typeof readSessionMetadata !== 'function') {
|
|
161
|
+
if (!parkedNoticeLogged) {
|
|
162
|
+
parkedNoticeLogged = true;
|
|
163
|
+
console.log('[context-obligatory] parked: no session metadata store is wired, so the compaction cursor cannot be saved');
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (payload?.type !== 'session.compacted') return;
|
|
157
168
|
const sessionId = payload?.properties?.sessionID;
|
|
158
169
|
if (typeof sessionId !== 'string' || inflight.has(sessionId)) return;
|
|
159
170
|
const directory = payload?.properties?.directory || directoryHint;
|