@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
|
@@ -33,12 +33,19 @@ afterEach(() => {
|
|
|
33
33
|
*/
|
|
34
34
|
const createOpenCode = () => {
|
|
35
35
|
const state = {
|
|
36
|
-
|
|
36
|
+
// `/api/session/active` lists only running sessions; an absent id is idle.
|
|
37
|
+
active: {},
|
|
38
|
+
// Subagent sessions per parent, as `GET /api/session?parentID=` lists them.
|
|
39
|
+
children: {},
|
|
37
40
|
tail: [],
|
|
38
41
|
commands: [],
|
|
39
42
|
sent: [],
|
|
43
|
+
switched: [],
|
|
40
44
|
failNext: null,
|
|
41
45
|
};
|
|
46
|
+
// v2 wraps `/api/*` payloads in `{ location, data }`; a message page is
|
|
47
|
+
// `{ data, cursor }` and lists newest first.
|
|
48
|
+
const wrapped = (data) => Response.json({ location: { directory: DIRECTORY }, data });
|
|
42
49
|
const fetchImpl = vi.fn(async (url, init = {}) => {
|
|
43
50
|
const { pathname } = new URL(url);
|
|
44
51
|
const method = init.method ?? 'GET';
|
|
@@ -46,19 +53,29 @@ const createOpenCode = () => {
|
|
|
46
53
|
state.failNext = null;
|
|
47
54
|
return new Response('boom', { status: 500 });
|
|
48
55
|
}
|
|
49
|
-
|
|
50
|
-
if (pathname
|
|
51
|
-
if (pathname === '/
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
// Real shape: `{ data }` without a `location`, unlike directory-scoped routes.
|
|
57
|
+
if (pathname === '/api/session/active') return Response.json({ data: state.active });
|
|
58
|
+
if (pathname === '/api/session' && method === 'GET') {
|
|
59
|
+
const parentID = new URL(url).searchParams.get('parentID');
|
|
60
|
+
return Response.json({ data: (state.children[parentID] ?? []).map((id) => ({ id, parentID })), cursor: {} });
|
|
61
|
+
}
|
|
62
|
+
if (pathname.endsWith('/message')) return Response.json({ data: state.tail, cursor: {} });
|
|
63
|
+
if (pathname === '/api/command') return wrapped(state.commands);
|
|
64
|
+
if (method === 'POST' && (pathname.endsWith('/model') || pathname.endsWith('/agent'))) {
|
|
65
|
+
// Kept apart from `sent`: switching the session is not a message.
|
|
66
|
+
state.switched.push({ path: pathname, body: JSON.parse(init.body) });
|
|
54
67
|
return new Response(null, { status: 204 });
|
|
55
68
|
}
|
|
69
|
+
if (method === 'POST' && (pathname.endsWith('/prompt') || pathname.endsWith('/command') || pathname.endsWith('/synthetic'))) {
|
|
70
|
+
state.sent.push({ path: pathname, body: JSON.parse(init.body) });
|
|
71
|
+
return wrapped({ id: `msg_${state.sent.length}` });
|
|
72
|
+
}
|
|
56
73
|
return new Response('not found', { status: 404 });
|
|
57
74
|
});
|
|
58
75
|
return { state, fetchImpl };
|
|
59
76
|
};
|
|
60
77
|
|
|
61
|
-
const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), knowledge = null, retryDelayMs,
|
|
78
|
+
const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), knowledge = null, retryDelayMs, resolveAutoSelection, now } = {}) => {
|
|
62
79
|
let eventHandler = () => {};
|
|
63
80
|
let statusHandler = () => {};
|
|
64
81
|
const broadcasts = [];
|
|
@@ -79,7 +96,7 @@ const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), k
|
|
|
79
96
|
abortHoldMs: 50,
|
|
80
97
|
};
|
|
81
98
|
if (retryDelayMs) options.retryDelayMs = retryDelayMs;
|
|
82
|
-
if (
|
|
99
|
+
if (resolveAutoSelection) options.resolveAutoSelection = resolveAutoSelection;
|
|
83
100
|
if (now) options.now = now;
|
|
84
101
|
const runtime = createMessageQueueRuntime(options);
|
|
85
102
|
return {
|
|
@@ -88,7 +105,8 @@ const createRuntime = ({ dataDir = makeDataDir(), openCode = createOpenCode(), k
|
|
|
88
105
|
dataDir,
|
|
89
106
|
broadcasts,
|
|
90
107
|
promptSent,
|
|
91
|
-
|
|
108
|
+
// The hub hands server-side subscribers already-translated events.
|
|
109
|
+
emit: (payload, directory = DIRECTORY) => eventHandler({ payload, directory, translated: () => [payload] }),
|
|
92
110
|
connect: () => statusHandler({ type: 'connect' }),
|
|
93
111
|
};
|
|
94
112
|
};
|
|
@@ -98,13 +116,11 @@ const settle = async (ms = 30) => {
|
|
|
98
116
|
};
|
|
99
117
|
|
|
100
118
|
describe('auto routing', () => {
|
|
101
|
-
it('
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
});
|
|
107
|
-
const { runtime, openCode, emit } = createRuntime({ resolvePromptBody });
|
|
119
|
+
it('switches a queued prompt and a queued command onto the routed model and agent', async () => {
|
|
120
|
+
const resolveAutoSelection = vi.fn(async ({ model }) => (model?.id === 'auto'
|
|
121
|
+
? { model: { providerID: 'openai', id: 'gpt-6-astra' }, agent: 'plan', decision: {} }
|
|
122
|
+
: null));
|
|
123
|
+
const { runtime, openCode, emit } = createRuntime({ resolveAutoSelection });
|
|
108
124
|
runtime.start();
|
|
109
125
|
openCode.state.statuses = { [SESSION]: { type: 'busy' } };
|
|
110
126
|
openCode.state.commands = [{ name: 'review', template: 'Review $ARGUMENTS' }];
|
|
@@ -118,12 +134,16 @@ describe('auto routing', () => {
|
|
|
118
134
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
119
135
|
await settle();
|
|
120
136
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
137
|
+
// v2 carries no model in a prompt body: the session is switched first.
|
|
138
|
+
const switched = openCode.state.switched.filter((entry) => entry.path.endsWith('/model'));
|
|
139
|
+
expect(switched.map((entry) => entry.body.model)).toEqual([
|
|
140
|
+
{ providerID: 'openai', id: 'gpt-6-astra' },
|
|
141
|
+
{ providerID: 'openai', id: 'gpt-6-astra' },
|
|
124
142
|
]);
|
|
125
|
-
expect(
|
|
126
|
-
|
|
143
|
+
expect(openCode.state.switched.filter((entry) => entry.path.endsWith('/agent')).map((entry) => entry.body.agent))
|
|
144
|
+
.toEqual(['plan', 'plan']);
|
|
145
|
+
expect(resolveAutoSelection).toHaveBeenCalledTimes(2);
|
|
146
|
+
expect(resolveAutoSelection.mock.calls[0][0]).toMatchObject({ sessionId: SESSION, directory: DIRECTORY, requestText: 'plain' });
|
|
127
147
|
});
|
|
128
148
|
});
|
|
129
149
|
|
|
@@ -172,24 +192,22 @@ describe('message queue runtime', () => {
|
|
|
172
192
|
it('delivers the head of the queue when the session goes idle, in order', async () => {
|
|
173
193
|
const { runtime, openCode, emit, promptSent, broadcasts } = createRuntime();
|
|
174
194
|
runtime.start();
|
|
175
|
-
openCode.state.
|
|
195
|
+
openCode.state.active = { [SESSION]: { type: 'running' } };
|
|
176
196
|
|
|
177
197
|
await runtime.enqueue(SESSION, DIRECTORY, item({ content: 'first', text: 'first' }));
|
|
178
198
|
await runtime.enqueue(SESSION, DIRECTORY, item({ content: 'second', text: 'second' }));
|
|
179
199
|
await settle();
|
|
180
200
|
expect(openCode.state.sent).toHaveLength(0);
|
|
181
201
|
|
|
182
|
-
openCode.state.
|
|
202
|
+
openCode.state.active = {};
|
|
183
203
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
184
204
|
await settle();
|
|
185
205
|
|
|
186
206
|
expect(openCode.state.sent).toHaveLength(1);
|
|
187
|
-
expect(openCode.state.sent[0].path).toBe(`/session/${SESSION}/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
parts: [{ type: 'text', text: 'first' }],
|
|
192
|
-
});
|
|
207
|
+
expect(openCode.state.sent[0].path).toBe(`/api/session/${SESSION}/prompt`);
|
|
208
|
+
// v2 selects model and agent on the session, so the prompt carries text
|
|
209
|
+
// and attachments only.
|
|
210
|
+
expect(openCode.state.sent[0].body).toEqual({ text: 'first' });
|
|
193
211
|
expect(promptSent).toEqual([SESSION]);
|
|
194
212
|
expect(runtime.sessionSnapshot(SESSION).items.map((entry) => entry.content)).toEqual(['second']);
|
|
195
213
|
// Clients learned about the in-flight item and then the removal.
|
|
@@ -206,11 +224,32 @@ describe('message queue runtime', () => {
|
|
|
206
224
|
expect(runtime.sessionSnapshot(SESSION).items).toEqual([]);
|
|
207
225
|
});
|
|
208
226
|
|
|
227
|
+
it('waits for a background subagent before sending the queued message', async () => {
|
|
228
|
+
const { runtime, openCode, emit } = createRuntime();
|
|
229
|
+
runtime.start();
|
|
230
|
+
// The parent paused: it is idle while its background subagent works.
|
|
231
|
+
openCode.state.children = { [SESSION]: ['ses_child'] };
|
|
232
|
+
openCode.state.active = { ses_child: { type: 'running' } };
|
|
233
|
+
|
|
234
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: 'next', text: 'next' }));
|
|
235
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
236
|
+
await settle();
|
|
237
|
+
expect(openCode.state.sent).toHaveLength(0);
|
|
238
|
+
|
|
239
|
+
// The subagent finished and OpenCode ran the parent again with its result.
|
|
240
|
+
openCode.state.active = {};
|
|
241
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'busy' } } });
|
|
242
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
243
|
+
await settle();
|
|
244
|
+
expect(openCode.state.sent).toHaveLength(1);
|
|
245
|
+
expect(openCode.state.sent[0].body).toEqual({ text: 'next' });
|
|
246
|
+
});
|
|
247
|
+
|
|
209
248
|
it('does not send into a running turn even when the status event says idle', async () => {
|
|
210
249
|
const { runtime, openCode, emit } = createRuntime({ now: () => 10_000 });
|
|
211
250
|
runtime.start();
|
|
212
251
|
// Live unfinished turn: created after this runtime started.
|
|
213
|
-
openCode.state.tail = [{
|
|
252
|
+
openCode.state.tail = [{ type: 'assistant', time: { created: 10_001 } }];
|
|
214
253
|
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
215
254
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
216
255
|
await settle();
|
|
@@ -218,7 +257,7 @@ describe('message queue runtime', () => {
|
|
|
218
257
|
|
|
219
258
|
// The reply completes: that alone drains the queue (a missed idle event
|
|
220
259
|
// must not strand it).
|
|
221
|
-
openCode.state.tail = [{
|
|
260
|
+
openCode.state.tail = [{ type: 'assistant', time: { created: 10_001, completed: 10_002 } }];
|
|
222
261
|
emit({ type: 'message.updated', properties: { info: { role: 'assistant', sessionID: SESSION, time: { created: 10_001, completed: 10_002 } } } });
|
|
223
262
|
await settle();
|
|
224
263
|
expect(openCode.state.sent).toHaveLength(1);
|
|
@@ -230,19 +269,19 @@ describe('message queue runtime', () => {
|
|
|
230
269
|
// Assistant reply interrupted by a server restart: unfinished, but older
|
|
231
270
|
// than this runtime — no completion event will ever arrive for it, so it
|
|
232
271
|
// must not block a restored queue forever.
|
|
233
|
-
openCode.state.tail = [{
|
|
272
|
+
openCode.state.tail = [{ type: 'assistant', time: { created: 1 } }];
|
|
234
273
|
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
235
274
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
236
275
|
await settle();
|
|
237
276
|
expect(openCode.state.sent).toHaveLength(1);
|
|
238
|
-
expect(openCode.state.sent[0].path).toBe(`/session/${SESSION}/
|
|
277
|
+
expect(openCode.state.sent[0].path).toBe(`/api/session/${SESSION}/prompt`);
|
|
239
278
|
});
|
|
240
279
|
|
|
241
280
|
it('treats an unreachable OpenCode as unknown, not idle', async () => {
|
|
242
281
|
const { runtime, openCode, emit } = createRuntime({ retryDelayMs: () => 10 });
|
|
243
282
|
runtime.start();
|
|
244
283
|
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
245
|
-
openCode.state.failNext = /\/session\/
|
|
284
|
+
openCode.state.failNext = /\/api\/session\/active$/;
|
|
246
285
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
247
286
|
await settle(5);
|
|
248
287
|
expect(openCode.state.sent).toHaveLength(0);
|
|
@@ -255,7 +294,7 @@ describe('message queue runtime', () => {
|
|
|
255
294
|
const { runtime, openCode, emit, broadcasts } = createRuntime({ retryDelayMs: () => 20 });
|
|
256
295
|
runtime.start();
|
|
257
296
|
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
258
|
-
openCode.state.failNext =
|
|
297
|
+
openCode.state.failNext = /\/prompt$/;
|
|
259
298
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
260
299
|
await settle(10);
|
|
261
300
|
expect(openCode.state.sent).toHaveLength(0);
|
|
@@ -271,8 +310,10 @@ describe('message queue runtime', () => {
|
|
|
271
310
|
const { runtime, openCode, emit } = createRuntime();
|
|
272
311
|
runtime.start();
|
|
273
312
|
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
274
|
-
|
|
313
|
+
// v2 reports a user abort as `session.execution.interrupted`, which the
|
|
314
|
+
// translator turns into an aborted `session.idle`.
|
|
275
315
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
316
|
+
emit({ type: 'session.idle', properties: { sessionID: SESSION, aborted: true, reason: 'user' } });
|
|
276
317
|
await settle(10);
|
|
277
318
|
expect(openCode.state.sent).toHaveLength(0);
|
|
278
319
|
await settle(80);
|
|
@@ -297,7 +338,7 @@ describe('message queue runtime', () => {
|
|
|
297
338
|
const dataDir = makeDataDir();
|
|
298
339
|
const first = createRuntime({ dataDir });
|
|
299
340
|
first.runtime.start();
|
|
300
|
-
first.openCode.state.
|
|
341
|
+
first.openCode.state.active = { [SESSION]: { type: 'running' } };
|
|
301
342
|
await first.runtime.enqueue(SESSION, DIRECTORY, item({ content: 'persisted', text: 'persisted', contextPreview: 'Saved context preview' }));
|
|
302
343
|
await first.runtime.flush();
|
|
303
344
|
first.runtime.stop();
|
|
@@ -310,7 +351,7 @@ describe('message queue runtime', () => {
|
|
|
310
351
|
second.connect();
|
|
311
352
|
await settle();
|
|
312
353
|
expect(second.openCode.state.sent).toHaveLength(1);
|
|
313
|
-
expect(second.openCode.state.sent[0].body
|
|
354
|
+
expect(second.openCode.state.sent[0].body).toEqual({ text: 'persisted' });
|
|
314
355
|
});
|
|
315
356
|
|
|
316
357
|
it('moves an unreadable queue file aside instead of treating it as empty', async () => {
|
|
@@ -326,9 +367,13 @@ describe('message queue runtime', () => {
|
|
|
326
367
|
const { runtime, openCode, emit } = createRuntime();
|
|
327
368
|
runtime.start();
|
|
328
369
|
let release;
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
370
|
+
// active map, message tail, the subagent check (active map, children),
|
|
371
|
+
// model switch, then the prompt itself (held open until released)
|
|
372
|
+
openCode.fetchImpl.mockImplementationOnce(async () => Response.json({ location: {}, data: {} }))
|
|
373
|
+
.mockImplementationOnce(async () => Response.json({ data: [], cursor: {} }))
|
|
374
|
+
.mockImplementationOnce(async () => Response.json({ data: {} }))
|
|
375
|
+
.mockImplementationOnce(async () => Response.json({ data: [], cursor: {} }))
|
|
376
|
+
.mockImplementationOnce(async () => new Response(null, { status: 204 }))
|
|
332
377
|
.mockImplementationOnce(() => new Promise((resolve) => { release = () => resolve(new Response(null, { status: 204 })); }));
|
|
333
378
|
const { itemId } = await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
334
379
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
@@ -398,7 +443,7 @@ describe('message queue runtime', () => {
|
|
|
398
443
|
const { runtime, emit, broadcasts, openCode } = createRuntime();
|
|
399
444
|
runtime.start();
|
|
400
445
|
await runtime.enqueue(SESSION, DIRECTORY, item());
|
|
401
|
-
openCode.state.
|
|
446
|
+
openCode.state.active = {};
|
|
402
447
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
403
448
|
await settle();
|
|
404
449
|
|
|
@@ -434,11 +479,12 @@ describe('message queue runtime', () => {
|
|
|
434
479
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
435
480
|
await settle();
|
|
436
481
|
expect(openCode.state.sent).toHaveLength(1);
|
|
437
|
-
expect(openCode.state.sent[0].path).toBe(`/session/${SESSION}/command`);
|
|
438
|
-
|
|
482
|
+
expect(openCode.state.sent[0].path).toBe(`/api/session/${SESSION}/command`);
|
|
483
|
+
// v2's command route takes `text`, and the selection lives on the session.
|
|
484
|
+
expect(openCode.state.sent[0].body).toEqual({ name: 'review', text: 'src' });
|
|
439
485
|
});
|
|
440
486
|
|
|
441
|
-
it('delivers captured context as synthetic
|
|
487
|
+
it('delivers captured context as synthetic messages, instructions first, before project knowledge', async () => {
|
|
442
488
|
const knowledge = {
|
|
443
489
|
resolvePendingForSession: async () => ({ text: 'pinned notes', signature: 'sig-1' }),
|
|
444
490
|
recordDelivered: async () => {},
|
|
@@ -457,16 +503,29 @@ describe('message queue runtime', () => {
|
|
|
457
503
|
}));
|
|
458
504
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
459
505
|
await settle();
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
{
|
|
465
|
-
{
|
|
466
|
-
{
|
|
467
|
-
{
|
|
468
|
-
{
|
|
506
|
+
|
|
507
|
+
// v2 has no inline synthetic parts: everything attached to the message is
|
|
508
|
+
// admitted as its own synthetic message, before the prompt.
|
|
509
|
+
expect(openCode.state.sent.map((entry) => entry.path)).toEqual([
|
|
510
|
+
`/api/session/${SESSION}/synthetic`,
|
|
511
|
+
`/api/session/${SESSION}/synthetic`,
|
|
512
|
+
`/api/session/${SESSION}/synthetic`,
|
|
513
|
+
`/api/session/${SESSION}/synthetic`,
|
|
514
|
+
`/api/session/${SESSION}/synthetic`,
|
|
515
|
+
`/api/session/${SESSION}/prompt`,
|
|
516
|
+
]);
|
|
517
|
+
expect(openCode.state.sent.slice(0, 5).map((entry) => entry.body)).toEqual([
|
|
518
|
+
{ text: 'how to read it', resume: false },
|
|
519
|
+
{ text: 'the diff', resume: false, metadata },
|
|
520
|
+
{ text: 'conflict payload', resume: false },
|
|
521
|
+
{ text: 'use the skill', resume: false },
|
|
522
|
+
{ text: 'pinned notes', resume: false },
|
|
469
523
|
]);
|
|
524
|
+
expect(openCode.state.sent.at(-1).body).toEqual({
|
|
525
|
+
text: 'follow up',
|
|
526
|
+
files: [{ uri: 'data:text/plain,hi', name: 'f.txt' }],
|
|
527
|
+
agents: [{ name: 'reviewer' }],
|
|
528
|
+
});
|
|
470
529
|
});
|
|
471
530
|
|
|
472
531
|
it('keeps files on the command route, which is all that route accepts', async () => {
|
|
@@ -480,17 +539,21 @@ describe('message queue runtime', () => {
|
|
|
480
539
|
}));
|
|
481
540
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
482
541
|
await settle();
|
|
483
|
-
expect(openCode.state.sent[0].path).toBe(`/session/${SESSION}/command`);
|
|
484
|
-
expect(openCode.state.sent[0].body
|
|
542
|
+
expect(openCode.state.sent[0].path).toBe(`/api/session/${SESSION}/command`);
|
|
543
|
+
expect(openCode.state.sent[0].body).toEqual({
|
|
544
|
+
name: 'review',
|
|
545
|
+
text: '',
|
|
546
|
+
files: [{ uri: 'data:text/plain,hi', name: 'f.txt' }],
|
|
547
|
+
});
|
|
485
548
|
});
|
|
486
549
|
|
|
487
|
-
it('
|
|
488
|
-
// The command route
|
|
489
|
-
//
|
|
490
|
-
// the
|
|
550
|
+
it('admits captured context as synthetic messages and still runs a queued command through the command route', async () => {
|
|
551
|
+
// The command route takes attachments only, so the context goes ahead as
|
|
552
|
+
// synthetic messages; the command itself keeps its route, because sending
|
|
553
|
+
// "/review ..." as a prompt would skip the template OpenCode expands there.
|
|
491
554
|
const { runtime, openCode, emit } = createRuntime();
|
|
492
555
|
runtime.start();
|
|
493
|
-
openCode.state.commands = [{ name: 'review',
|
|
556
|
+
openCode.state.commands = [{ name: 'review', description: 'Review' }];
|
|
494
557
|
const metadata = { openchamberContext: { kind: 'chat-quote', quote: 'q', text: 'why?' } };
|
|
495
558
|
await runtime.enqueue(SESSION, DIRECTORY, item({
|
|
496
559
|
content: '/review src "error handling"',
|
|
@@ -499,30 +562,50 @@ describe('message queue runtime', () => {
|
|
|
499
562
|
}));
|
|
500
563
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
501
564
|
await settle();
|
|
502
|
-
|
|
503
|
-
expect(openCode.state.sent
|
|
504
|
-
{
|
|
505
|
-
{
|
|
565
|
+
|
|
566
|
+
expect(openCode.state.sent.map((entry) => entry.path)).toEqual([
|
|
567
|
+
`/api/session/${SESSION}/synthetic`,
|
|
568
|
+
`/api/session/${SESSION}/command`,
|
|
506
569
|
]);
|
|
570
|
+
expect(openCode.state.sent[0].body).toEqual({ text: 'quoted', resume: false, metadata });
|
|
571
|
+
expect(openCode.state.sent[1].body).toEqual({ name: 'review', text: 'src "error handling"' });
|
|
507
572
|
});
|
|
508
573
|
|
|
509
|
-
it('
|
|
574
|
+
it('delivers pending project knowledge ahead of a queued command and records it', async () => {
|
|
575
|
+
const recorded = [];
|
|
576
|
+
const knowledge = {
|
|
577
|
+
resolvePendingForSession: async () => ({ text: 'pinned notes', signature: 'sig-1' }),
|
|
578
|
+
recordDelivered: async (sessionId, directory, signature) => { recorded.push({ sessionId, directory, signature }); },
|
|
579
|
+
};
|
|
580
|
+
const { runtime, openCode, emit } = createRuntime({ knowledge });
|
|
581
|
+
runtime.start();
|
|
582
|
+
openCode.state.commands = [{ name: 'review' }];
|
|
583
|
+
await runtime.enqueue(SESSION, DIRECTORY, item({ content: '/review', text: '/review' }));
|
|
584
|
+
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
585
|
+
await settle();
|
|
586
|
+
|
|
587
|
+
expect(openCode.state.sent.map((entry) => entry.path)).toEqual([
|
|
588
|
+
`/api/session/${SESSION}/synthetic`,
|
|
589
|
+
`/api/session/${SESSION}/command`,
|
|
590
|
+
]);
|
|
591
|
+
expect(openCode.state.sent[0].body).toEqual({ text: 'pinned notes', resume: false });
|
|
592
|
+
expect(recorded).toEqual([{ sessionId: SESSION, directory: DIRECTORY, signature: 'sig-1' }]);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('admits nothing when the command lookup fails, so a retry cannot duplicate the context', async () => {
|
|
510
596
|
const { runtime, openCode, emit } = createRuntime();
|
|
511
597
|
runtime.start();
|
|
512
|
-
openCode.state.
|
|
598
|
+
openCode.state.failNext = /^\/api\/command$/;
|
|
513
599
|
await runtime.enqueue(SESSION, DIRECTORY, item({
|
|
514
|
-
content: '/
|
|
515
|
-
text: '/
|
|
516
|
-
context: [{ kind: '
|
|
600
|
+
content: '/review',
|
|
601
|
+
text: '/review',
|
|
602
|
+
context: [{ kind: 'context', text: 'quoted', metadata: {} }],
|
|
517
603
|
}));
|
|
518
604
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
519
605
|
await settle();
|
|
520
|
-
|
|
521
|
-
expect(openCode.state.sent
|
|
522
|
-
|
|
523
|
-
{ type: 'text', text: 'focus on tests', synthetic: true },
|
|
524
|
-
{ type: 'text', text: 'The user explicitly invoked the grill skill. Use the corresponding skill tool to handle this request.', synthetic: true },
|
|
525
|
-
]);
|
|
606
|
+
|
|
607
|
+
expect(openCode.state.sent).toEqual([]);
|
|
608
|
+
expect(runtime.sessionSnapshot(SESSION).items).toHaveLength(1);
|
|
526
609
|
});
|
|
527
610
|
|
|
528
611
|
it('keeps captured context out of snapshots and broadcasts, and hands it back on take', async () => {
|
|
@@ -548,12 +631,15 @@ describe('message queue runtime', () => {
|
|
|
548
631
|
await runtime.enqueue(SESSION, DIRECTORY, item({ agentMention: 'reviewer', attachments: [{ id: 'a', filename: 'f.txt', mimeType: 'text/plain', size: 1, source: 'local', dataUrl: 'data:text/plain,hi' }] }));
|
|
549
632
|
emit({ type: 'session.status', properties: { sessionID: SESSION, status: { type: 'idle' } } });
|
|
550
633
|
await settle();
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
634
|
+
// Knowledge is admitted as a synthetic message ahead of the prompt.
|
|
635
|
+
expect(openCode.state.sent[0].path).toBe(`/api/session/${SESSION}/synthetic`);
|
|
636
|
+
expect(openCode.state.sent[0].body).toEqual({ text: 'pinned notes', resume: false });
|
|
637
|
+
expect(openCode.state.sent[1].path).toBe(`/api/session/${SESSION}/prompt`);
|
|
638
|
+
expect(openCode.state.sent[1].body).toEqual({
|
|
639
|
+
text: 'follow up',
|
|
640
|
+
files: [{ uri: 'data:text/plain,hi', name: 'f.txt' }],
|
|
641
|
+
agents: [{ name: 'reviewer' }],
|
|
642
|
+
});
|
|
557
643
|
expect(recorded).toEqual([{ sessionId: SESSION, directory: DIRECTORY, signature: 'sig-1' }]);
|
|
558
644
|
});
|
|
559
645
|
});
|
|
@@ -47,6 +47,7 @@ This module provides notification message preparation utilities for the web serv
|
|
|
47
47
|
- Owns:
|
|
48
48
|
- completion/error/question/permission trigger routing; permission suppression consults the authoritative permission-auto-accept runtime
|
|
49
49
|
- session parent cache for subtask suppression
|
|
50
|
+
- no ready notification on a `session.idle` while a subagent of that session is still running (a background-subagent pause; OpenCode runs the parent again with the result and its next idle announces). The check comes from `../opencode/session-activity.js`; when it cannot be made, the idle announces as before
|
|
50
51
|
- template resolution and fallback behavior
|
|
51
52
|
- native notification fanout and web push payload fanout
|
|
52
53
|
- push suppression while any fresh UI visibility heartbeat reports a focused client
|
|
@@ -136,3 +137,30 @@ The `settings` parameter for `prepareNotificationLastMessage` supports `maxLastM
|
|
|
136
137
|
### Testing
|
|
137
138
|
- Run `bun run type-check`, `bun run lint`, and `bun run build` before finalizing changes.
|
|
138
139
|
- Unit tests should cover truncation behavior and edge cases (empty strings, invalid inputs).
|
|
140
|
+
|
|
141
|
+
## Browser event delivery
|
|
142
|
+
|
|
143
|
+
Web notifications subscribe to the shared `/api/openchamber/events` control
|
|
144
|
+
stream. The emitter sends each notification there and through the existing
|
|
145
|
+
global broadcaster. The broadcaster owns the main WebSocket and the legacy
|
|
146
|
+
`/api/notifications/stream` endpoint, which remains available to older clients.
|
|
147
|
+
Do not call both global and control broadcasters for one notification: they
|
|
148
|
+
share the WebSocket client set and would send each frame twice.
|
|
149
|
+
|
|
150
|
+
The web hook preserves `tag`, `sessionId`, `kind`, and the remaining notification
|
|
151
|
+
fields so the web notification API recognizes duplicate control-SSE and main-WS
|
|
152
|
+
delivery. Its existing five-second claim suppresses duplicate native alerts.
|
|
153
|
+
Native alerts remain best-effort live events, with no historical replay or
|
|
154
|
+
synthetic catch-up alerts. Authoritative session attention is owned by sync.
|
|
155
|
+
|
|
156
|
+
This removes one persistent HTTP connection per browser tab in both main-WS and
|
|
157
|
+
main-SSE modes. Two tabs use two control SSE connections, or four connections
|
|
158
|
+
when their main event pipelines also use SSE. It does not make HTTP/1.1 support
|
|
159
|
+
an unlimited number of tabs. The notification hook retains the app's background
|
|
160
|
+
work enablement, settings, focus, and runtime gates; it no longer opens a second
|
|
161
|
+
connection or switches sources when the main pipeline changes transport.
|
|
162
|
+
|
|
163
|
+
The control stream retains its browser-control capability declaration, runtime
|
|
164
|
+
switch cleanup, stale-source rejection, heartbeat, and reconnect-ready events.
|
|
165
|
+
Electron uses its native notification path; VS Code does not subscribe to this
|
|
166
|
+
server-only stream. Mobile shells retain their existing push behavior.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import { createUiAuth } from '../ui-auth/ui-auth.js';
|
|
5
|
+
import { registerScheduledTaskRoutes } from '../scheduled-tasks/routes.js';
|
|
6
|
+
import { createNotificationEmitterRuntime } from './emitter-runtime.js';
|
|
7
|
+
|
|
8
|
+
// Exercise the existing browser URL-token gate and the real SSE route, not an
|
|
9
|
+
// unauthenticated mock endpoint. Notification consolidation adds no auth bypass.
|
|
10
|
+
describe('notifications over the authenticated control stream', () => {
|
|
11
|
+
it('rejects anonymous readers and delivers to a scoped authenticated stream', async () => {
|
|
12
|
+
const auth = createUiAuth({
|
|
13
|
+
requireClientAuth: true,
|
|
14
|
+
clientAuthController: {
|
|
15
|
+
authenticateBearerToken: async (token) => token === 'test-client' ? { ok: true, clientId: 'fixture' } : null,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const clients = new Set();
|
|
19
|
+
const emitter = createNotificationEmitterRuntime({
|
|
20
|
+
process,
|
|
21
|
+
getDesktopNotifyEnabled: () => false,
|
|
22
|
+
desktopNotifyPrefix: '',
|
|
23
|
+
getUiNotificationClients: () => new Set(),
|
|
24
|
+
getOpenChamberEventClients: () => clients,
|
|
25
|
+
});
|
|
26
|
+
const app = express();
|
|
27
|
+
app.use(express.json());
|
|
28
|
+
app.post('/auth/url-token', auth.handleUrlAuthToken);
|
|
29
|
+
app.use('/api', auth.requireAuth);
|
|
30
|
+
registerScheduledTaskRoutes(app, {
|
|
31
|
+
getOpenChamberEventClients: () => clients,
|
|
32
|
+
writeSseEvent: emitter.writeSseEvent,
|
|
33
|
+
});
|
|
34
|
+
const server = createServer(app);
|
|
35
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
36
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
37
|
+
const abort = new AbortController();
|
|
38
|
+
try {
|
|
39
|
+
const denied = await fetch(`${base}/api/openchamber/events`);
|
|
40
|
+
expect(denied.status).toBe(401);
|
|
41
|
+
expect(clients.size).toBe(0);
|
|
42
|
+
const tokenResponse = await fetch(`${base}/auth/url-token`, {
|
|
43
|
+
method: 'POST', headers: { Authorization: 'Bearer test-client' },
|
|
44
|
+
});
|
|
45
|
+
expect(tokenResponse.status).toBe(200);
|
|
46
|
+
const { token } = await tokenResponse.json();
|
|
47
|
+
expect(token).toBeTruthy();
|
|
48
|
+
const response = await fetch(`${base}/api/openchamber/events?browser=1&oc_url_token=${encodeURIComponent(token)}`, { signal: abort.signal });
|
|
49
|
+
expect(response.status).toBe(200);
|
|
50
|
+
expect(response.headers.get('content-type')).toContain('text/event-stream');
|
|
51
|
+
const reader = response.body.getReader();
|
|
52
|
+
const ready = new TextDecoder().decode((await reader.read()).value);
|
|
53
|
+
expect(ready).toContain('openchamber:event-stream-ready');
|
|
54
|
+
expect(clients.size).toBe(1);
|
|
55
|
+
expect([...clients][0].openchamberBrowserCapable).toBe(true);
|
|
56
|
+
emitter.broadcastUiNotification({ title: 'Done', sessionId: 's1', kind: 'complete' });
|
|
57
|
+
const notification = new TextDecoder().decode((await reader.read()).value);
|
|
58
|
+
expect(notification).toContain('"type":"openchamber:notification"');
|
|
59
|
+
expect(notification).toContain('"sessionId":"s1"');
|
|
60
|
+
await reader.cancel();
|
|
61
|
+
} finally {
|
|
62
|
+
abort.abort();
|
|
63
|
+
server.closeAllConnections();
|
|
64
|
+
await new Promise((resolve) => server.close(resolve));
|
|
65
|
+
auth.dispose();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -4,6 +4,7 @@ export const createNotificationEmitterRuntime = (dependencies) => {
|
|
|
4
4
|
getDesktopNotifyEnabled,
|
|
5
5
|
desktopNotifyPrefix,
|
|
6
6
|
getUiNotificationClients,
|
|
7
|
+
getOpenChamberEventClients = () => new Set(),
|
|
7
8
|
getBroadcastGlobalUiEvent,
|
|
8
9
|
// Optional: in-process desktop shells (Electron main) inject a callback so
|
|
9
10
|
// notifications are delivered as a direct function call instead of a stdout
|
|
@@ -78,6 +79,20 @@ export const createNotificationEmitterRuntime = (dependencies) => {
|
|
|
78
79
|
},
|
|
79
80
|
};
|
|
80
81
|
|
|
82
|
+
// New clients share the control SSE; older clients retain their dedicated
|
|
83
|
+
// notification SSE. The global broadcaster still owns WebSocket delivery.
|
|
84
|
+
const controlClients = getOpenChamberEventClients();
|
|
85
|
+
if (controlClients.size > 0) {
|
|
86
|
+
const serializedPayload = JSON.stringify(syntheticPayload);
|
|
87
|
+
for (const client of controlClients) {
|
|
88
|
+
try {
|
|
89
|
+
writeSseEvent(client, syntheticPayload, serializedPayload);
|
|
90
|
+
} catch {
|
|
91
|
+
// One disconnected control client must not block other transports.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
const broadcastGlobalUiEvent = typeof getBroadcastGlobalUiEvent === 'function'
|
|
82
97
|
? getBroadcastGlobalUiEvent()
|
|
83
98
|
: null;
|
|
@@ -45,3 +45,35 @@ describe('notification emitter runtime', () => {
|
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
describe('shared control notification delivery', () => {
|
|
51
|
+
it('writes once to control SSE and once to the legacy/global broadcaster', () => {
|
|
52
|
+
const control = { write: vi.fn() };
|
|
53
|
+
const failed = { write: () => { throw new Error('closed'); } };
|
|
54
|
+
const broadcastGlobalUiEvent = vi.fn();
|
|
55
|
+
const runtime = createRuntime({
|
|
56
|
+
getOpenChamberEventClients: () => new Set([failed, control]),
|
|
57
|
+
getBroadcastGlobalUiEvent: () => broadcastGlobalUiEvent,
|
|
58
|
+
});
|
|
59
|
+
runtime.broadcastUiNotification({ title: 'Done', sessionId: 's1', kind: 'complete' });
|
|
60
|
+
expect(control.write).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(broadcastGlobalUiEvent).toHaveBeenCalledTimes(1);
|
|
62
|
+
const envelope = broadcastGlobalUiEvent.mock.calls[0][0];
|
|
63
|
+
expect(control.write).toHaveBeenCalledWith(`data: ${JSON.stringify(envelope)}\n\n`);
|
|
64
|
+
expect(envelope.properties.sessionId).toBe('s1');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('preserves legacy notification SSE without a global broadcaster', () => {
|
|
68
|
+
const control = { write: vi.fn() };
|
|
69
|
+
const legacy = { write: vi.fn() };
|
|
70
|
+
const runtime = createRuntime({
|
|
71
|
+
getOpenChamberEventClients: () => new Set([control]),
|
|
72
|
+
getUiNotificationClients: () => new Set([legacy]),
|
|
73
|
+
});
|
|
74
|
+
runtime.broadcastUiNotification({ title: 'Done' });
|
|
75
|
+
expect(control.write).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(legacy.write).toHaveBeenCalledTimes(1);
|
|
77
|
+
expect(control.write.mock.calls).toEqual(legacy.write.mock.calls);
|
|
78
|
+
});
|
|
79
|
+
});
|