@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
|
@@ -15,7 +15,8 @@ export const createPushRuntime = (deps) => {
|
|
|
15
15
|
const {
|
|
16
16
|
fsPromises,
|
|
17
17
|
path,
|
|
18
|
-
|
|
18
|
+
// Resolves the web-push module; it is loaded on first use, not with the server.
|
|
19
|
+
loadWebPush,
|
|
19
20
|
PUSH_SUBSCRIPTIONS_FILE_PATH,
|
|
20
21
|
readSettingsFromDiskMigrated,
|
|
21
22
|
writeSettingsToDisk,
|
|
@@ -86,6 +87,7 @@ export const createPushRuntime = (deps) => {
|
|
|
86
87
|
return { publicKey: existing.publicKey, privateKey: existing.privateKey };
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
const webPush = await loadWebPush();
|
|
89
91
|
const generated = webPush.generateVAPIDKeys();
|
|
90
92
|
const next = {
|
|
91
93
|
...settings,
|
|
@@ -210,6 +212,7 @@ export const createPushRuntime = (deps) => {
|
|
|
210
212
|
};
|
|
211
213
|
|
|
212
214
|
try {
|
|
215
|
+
const webPush = await loadWebPush();
|
|
213
216
|
await webPush.sendNotification(pushSubscription, body);
|
|
214
217
|
} catch (error) {
|
|
215
218
|
const statusCode = typeof error?.statusCode === 'number' ? error.statusCode : null;
|
|
@@ -344,6 +347,7 @@ export const createPushRuntime = (deps) => {
|
|
|
344
347
|
console.warn('[Push] No public origin configured for VAPID; set OPENCHAMBER_VAPID_SUBJECT or enable push once from a real origin.');
|
|
345
348
|
}
|
|
346
349
|
|
|
350
|
+
const webPush = await loadWebPush();
|
|
347
351
|
webPush.setVapidDetails(subject, keys.publicKey, keys.privateKey);
|
|
348
352
|
pushInitialized = true;
|
|
349
353
|
};
|
|
@@ -9,11 +9,11 @@ const createRuntime = () => createPushRuntime({
|
|
|
9
9
|
writeFile: vi.fn(async () => {}),
|
|
10
10
|
},
|
|
11
11
|
path: { dirname: () => '/tmp' },
|
|
12
|
-
|
|
12
|
+
loadWebPush: async () => ({
|
|
13
13
|
generateVAPIDKeys: vi.fn(() => ({ publicKey: 'public', privateKey: 'private' })),
|
|
14
14
|
sendNotification: vi.fn(async () => {}),
|
|
15
15
|
setVapidDetails: vi.fn(),
|
|
16
|
-
},
|
|
16
|
+
}),
|
|
17
17
|
PUSH_SUBSCRIPTIONS_FILE_PATH: '/tmp/push-subscriptions.json',
|
|
18
18
|
readSettingsFromDiskMigrated: vi.fn(async () => ({})),
|
|
19
19
|
writeSettingsToDisk: vi.fn(async () => {}),
|
|
@@ -297,7 +297,7 @@ export const registerNotificationRoutes = (app, dependencies) => {
|
|
|
297
297
|
});
|
|
298
298
|
|
|
299
299
|
// Cross-project seed for clients that do not initialize every directory:
|
|
300
|
-
// live status per session plus the permission and
|
|
300
|
+
// live status per session plus the permission requests and forms still
|
|
301
301
|
// waiting for an answer. Both come from the server's single upstream stream.
|
|
302
302
|
app.get('/api/sessions/status', async (_req, res) => {
|
|
303
303
|
await ensureSessionWatcher();
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { unwrapOpenCodeResponse } from '../opencode/response-envelope.js';
|
|
2
|
+
import { createSessionActivityProbe } from '../opencode/session-activity.js';
|
|
3
|
+
|
|
4
|
+
|
|
1
5
|
export const createNotificationTriggerRuntime = (deps) => {
|
|
2
6
|
const {
|
|
3
7
|
readSettingsFromDisk,
|
|
@@ -14,8 +18,22 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
14
18
|
isAnyInteractiveClientVisible,
|
|
15
19
|
buildOpenCodeUrl,
|
|
16
20
|
getOpenCodeAuthHeaders,
|
|
21
|
+
// OpenChamber's own session metadata (the goal lives there, not on the
|
|
22
|
+
// OpenCode record). Optional: without it no goal can suppress a push.
|
|
23
|
+
readSessionMetadata = null,
|
|
17
24
|
} = deps;
|
|
18
25
|
let getIsSessionAutoAccepting = deps.getIsSessionAutoAccepting;
|
|
26
|
+
const activityProbe = createSessionActivityProbe({ buildOpenCodeUrl, getOpenCodeAuthHeaders, timeoutMs: 2000 });
|
|
27
|
+
|
|
28
|
+
// A parent goes idle while a background subagent still works; OpenCode then
|
|
29
|
+
// hands the result back and the parent runs again. That first idle is a
|
|
30
|
+
// pause, so it announces nothing. When the check cannot be made, the idle is
|
|
31
|
+
// announced as before: a missed "ready" is worse than an early one.
|
|
32
|
+
const isPausedForSubagents = async (sessionId) => {
|
|
33
|
+
const statuses = await activityProbe.fetchActiveSessionStatuses();
|
|
34
|
+
if (!statuses) return false;
|
|
35
|
+
return (await activityProbe.hasWorkingChildren(sessionId, statuses)) === true;
|
|
36
|
+
};
|
|
19
37
|
const setGetIsSessionAutoAccepting = (resolver) => {
|
|
20
38
|
getIsSessionAutoAccepting = typeof resolver === 'function' ? resolver : undefined;
|
|
21
39
|
};
|
|
@@ -157,8 +175,57 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
157
175
|
const getParentIdFromPayload = (payload) => {
|
|
158
176
|
if (!payload || typeof payload !== 'object') return undefined;
|
|
159
177
|
if (payload.type !== 'session.created' && payload.type !== 'session.updated') return undefined;
|
|
160
|
-
const parentID = payload.properties?.info?.parentID
|
|
161
|
-
|
|
178
|
+
const parentID = payload.properties?.info?.parentID;
|
|
179
|
+
if (typeof parentID === 'string' && parentID.length > 0) return parentID;
|
|
180
|
+
// Only the full record from `session.created` proves a session has no
|
|
181
|
+
// parent. `session.updated` also carries partial records (usage, title)
|
|
182
|
+
// without `parentID`, and reading those as "no parent" turned a subagent
|
|
183
|
+
// into a main session, so its finish announced "ready" with subagent
|
|
184
|
+
// notifications turned off.
|
|
185
|
+
return payload.type === 'session.created' ? null : undefined;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// v2 splits one assistant turn over two events: `session.step.started`
|
|
189
|
+
// names the agent and model, `session.step.ended` carries the finish. The
|
|
190
|
+
// translator hands both over as `message.updated` on the same message id,
|
|
191
|
+
// so the start is remembered here and folded into the finish, which is the
|
|
192
|
+
// event the "ready" push is built from. Bounded: a session emits a handful
|
|
193
|
+
// of steps and the map is per process.
|
|
194
|
+
const ASSISTANT_STEP_CACHE_LIMIT = 500;
|
|
195
|
+
const assistantStepMeta = new Map();
|
|
196
|
+
// The reply a turn ended on: the idle event names only the session, so the
|
|
197
|
+
// ready notification borrows this step's id for its agent, model and text.
|
|
198
|
+
const lastAssistantStepBySession = new Map();
|
|
199
|
+
const rememberAssistantStep = (info) => {
|
|
200
|
+
if (!info || info.role !== 'assistant') return;
|
|
201
|
+
const id = String(info.id ?? '');
|
|
202
|
+
const agent = String(info.agent ?? info.mode ?? '');
|
|
203
|
+
const modelID = String(info.modelID ?? '');
|
|
204
|
+
if (!id || (!agent && !modelID)) return;
|
|
205
|
+
assistantStepMeta.delete(id);
|
|
206
|
+
assistantStepMeta.set(id, { agent, modelID });
|
|
207
|
+
const sessionId = String(info.sessionID ?? '');
|
|
208
|
+
if (sessionId) {
|
|
209
|
+
lastAssistantStepBySession.delete(sessionId);
|
|
210
|
+
lastAssistantStepBySession.set(sessionId, id);
|
|
211
|
+
while (lastAssistantStepBySession.size > ASSISTANT_STEP_CACHE_LIMIT) {
|
|
212
|
+
lastAssistantStepBySession.delete(lastAssistantStepBySession.keys().next().value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
while (assistantStepMeta.size > ASSISTANT_STEP_CACHE_LIMIT) {
|
|
216
|
+
assistantStepMeta.delete(assistantStepMeta.keys().next().value);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const withRememberedStep = (info) => {
|
|
220
|
+
const remembered = info ? assistantStepMeta.get(String(info.id ?? '')) : undefined;
|
|
221
|
+
if (!remembered) return info;
|
|
222
|
+
const merged = { ...info };
|
|
223
|
+
if (!merged.agent && !merged.mode) {
|
|
224
|
+
merged.agent = remembered.agent;
|
|
225
|
+
merged.mode = remembered.agent;
|
|
226
|
+
}
|
|
227
|
+
if (!merged.modelID) merged.modelID = remembered.modelID;
|
|
228
|
+
return merged;
|
|
162
229
|
};
|
|
163
230
|
|
|
164
231
|
const maybeCacheSessionParentFromPayload = (payload) => {
|
|
@@ -177,7 +244,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
177
244
|
if (cached !== undefined) return cached;
|
|
178
245
|
|
|
179
246
|
try {
|
|
180
|
-
const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
|
|
247
|
+
const base = buildOpenCodeUrl(`/api/session/${encodeURIComponent(sessionId)}`, '');
|
|
181
248
|
const url = directory ? `${base}?directory=${encodeURIComponent(directory)}` : base;
|
|
182
249
|
const response = await fetch(url, {
|
|
183
250
|
method: 'GET',
|
|
@@ -190,7 +257,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
190
257
|
if (!response.ok) {
|
|
191
258
|
return undefined;
|
|
192
259
|
}
|
|
193
|
-
const session = await response.json().catch(() => null);
|
|
260
|
+
const session = unwrapOpenCodeResponse(await response.json().catch(() => null));
|
|
194
261
|
if (!session || typeof session !== 'object') {
|
|
195
262
|
return undefined;
|
|
196
263
|
}
|
|
@@ -281,19 +348,11 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
281
348
|
// A session with an ACTIVE goal suppresses per-turn ready notifications;
|
|
282
349
|
// the session-goal runtime sends its own notification when the goal
|
|
283
350
|
// settles. Fetch failures fall through to normal notification behavior.
|
|
284
|
-
const hasActiveSessionGoal = async (sessionId
|
|
285
|
-
if (!sessionId) return false;
|
|
351
|
+
const hasActiveSessionGoal = async (sessionId) => {
|
|
352
|
+
if (!sessionId || typeof readSessionMetadata !== 'function') return false;
|
|
286
353
|
try {
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
const response = await fetch(url, {
|
|
290
|
-
method: 'GET',
|
|
291
|
-
headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
|
|
292
|
-
signal: AbortSignal.timeout(2000),
|
|
293
|
-
});
|
|
294
|
-
if (!response.ok) return false;
|
|
295
|
-
const session = await response.json().catch(() => null);
|
|
296
|
-
const goal = session?.metadata?.openchamber?.goal;
|
|
354
|
+
const metadata = await readSessionMetadata(sessionId);
|
|
355
|
+
const goal = metadata?.openchamber?.goal;
|
|
297
356
|
return Boolean(goal && typeof goal === 'object' && goal.status === 'active');
|
|
298
357
|
} catch {
|
|
299
358
|
return false;
|
|
@@ -309,7 +368,16 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
309
368
|
|
|
310
369
|
const sessionId = extractSessionIdFromPayload(payload);
|
|
311
370
|
const notificationDirectory = extractDirectoryFromPayload(payload);
|
|
371
|
+
// A user abort arrives as `session.idle` with `aborted: true`: the turn
|
|
372
|
+
// did not finish, so there is nothing to announce as ready.
|
|
373
|
+
if (payload.type === 'session.idle' && payload.properties?.aborted === true) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
312
377
|
if ((payload.type === 'session.idle' || payload.type === 'session.error') && sessionId) {
|
|
378
|
+
if (payload.type === 'session.idle' && await isPausedForSubagents(sessionId)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
313
381
|
const error = payload.properties?.error;
|
|
314
382
|
const errorText = typeof error?.message === 'string'
|
|
315
383
|
? error.message
|
|
@@ -319,7 +387,10 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
319
387
|
type: 'message.updated',
|
|
320
388
|
properties: {
|
|
321
389
|
...payload.properties,
|
|
390
|
+
// Only a turn end announces readiness; a step's `stop` does not.
|
|
391
|
+
turnEnded: true,
|
|
322
392
|
info: {
|
|
393
|
+
id: lastAssistantStepBySession.get(sessionId),
|
|
323
394
|
sessionID: sessionId,
|
|
324
395
|
role: 'assistant',
|
|
325
396
|
finish: payload.type === 'session.error' ? 'error' : 'stop',
|
|
@@ -331,8 +402,13 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
331
402
|
}
|
|
332
403
|
|
|
333
404
|
if (payload.type === 'message.updated') {
|
|
334
|
-
|
|
335
|
-
|
|
405
|
+
rememberAssistantStep(payload.properties?.info);
|
|
406
|
+
const info = withRememberedStep(payload.properties?.info);
|
|
407
|
+
// v2 ends every step with a `message.updated`; the last one says `stop`,
|
|
408
|
+
// but the execution can still drain steering input after it. Readiness
|
|
409
|
+
// is announced from the execution's terminal `session.idle` instead.
|
|
410
|
+
if (info?.role === 'assistant' && info?.finish === 'stop' && sessionId && payload.properties?.turnEnded === true) {
|
|
411
|
+
payload = { ...payload, properties: { ...payload.properties, info } };
|
|
336
412
|
const settings = await readSettingsFromDisk();
|
|
337
413
|
|
|
338
414
|
if (settings.notifyOnSubtasks === false) {
|
|
@@ -353,7 +429,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
353
429
|
// While a goal drives the session, per-turn "ready" notifications are
|
|
354
430
|
// noise produced by the goal loop itself — the goal's own settle
|
|
355
431
|
// notification (complete/blocked/budget) is the final word instead.
|
|
356
|
-
if (await hasActiveSessionGoal(sessionId
|
|
432
|
+
if (await hasActiveSessionGoal(sessionId)) {
|
|
357
433
|
return;
|
|
358
434
|
}
|
|
359
435
|
|
|
@@ -504,7 +580,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
504
580
|
return;
|
|
505
581
|
}
|
|
506
582
|
|
|
507
|
-
if (payload.type === '
|
|
583
|
+
if (payload.type === 'form.created' && sessionId) {
|
|
508
584
|
const existingTimer = pushQuestionDebounceTimers.get(sessionId);
|
|
509
585
|
if (existingTimer) {
|
|
510
586
|
clearTimeout(existingTimer);
|
|
@@ -522,9 +598,14 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
522
598
|
return;
|
|
523
599
|
}
|
|
524
600
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
601
|
+
// v2 replaced questions with forms: the form's title is the ask, and
|
|
602
|
+
// the first field carries the detail a question used to put in its body.
|
|
603
|
+
const form = payload.properties?.form;
|
|
604
|
+
const header = typeof form?.title === 'string' ? form.title.trim() : '';
|
|
605
|
+
const firstField = Array.isArray(form?.fields) ? form.fields[0] : null;
|
|
606
|
+
const fieldTitle = typeof firstField?.title === 'string' ? firstField.title.trim() : '';
|
|
607
|
+
const fieldDescription = typeof firstField?.description === 'string' ? firstField.description.trim() : '';
|
|
608
|
+
const questionText = fieldDescription || fieldTitle;
|
|
528
609
|
|
|
529
610
|
let title = /plan\s*mode/i.test(header)
|
|
530
611
|
? 'Switch to plan mode'
|
|
@@ -604,7 +685,15 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
604
685
|
|
|
605
686
|
if (payload.type === 'permission.asked' && sessionId) {
|
|
606
687
|
const requestId = payload.properties?.id ?? payload.properties?.requestID ?? payload.properties?.requestId;
|
|
607
|
-
|
|
688
|
+
// v2 describes a permission request as an action plus the resources it
|
|
689
|
+
// touches; the v1 single `permission` string is gone.
|
|
690
|
+
const permissionAction = typeof payload.properties?.action === 'string' ? payload.properties.action.trim() : '';
|
|
691
|
+
const permissionResource = Array.isArray(payload.properties?.resources)
|
|
692
|
+
? String(payload.properties.resources[0] ?? '').trim()
|
|
693
|
+
: '';
|
|
694
|
+
const permissionMessage = typeof payload.properties?.message === 'string' ? payload.properties.message.trim() : '';
|
|
695
|
+
const permission = permissionMessage
|
|
696
|
+
|| (permissionAction && permissionResource ? `${permissionAction}: ${permissionResource}` : permissionAction);
|
|
608
697
|
const requestKey = typeof requestId === 'string' ? `${sessionId}:${requestId}` : null;
|
|
609
698
|
if (requestKey && notifiedPermissionRequests.has(requestKey)) {
|
|
610
699
|
return;
|
|
@@ -713,7 +802,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
713
802
|
const sendGoalSettlePush = async ({ sessionId, directory, status, title, body }) => {
|
|
714
803
|
let sessionName = '';
|
|
715
804
|
try {
|
|
716
|
-
const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
|
|
805
|
+
const base = buildOpenCodeUrl(`/api/session/${encodeURIComponent(sessionId)}`, '');
|
|
717
806
|
const url = directory ? `${base}?directory=${encodeURIComponent(directory)}` : base;
|
|
718
807
|
const response = await fetch(url, {
|
|
719
808
|
method: 'GET',
|
|
@@ -721,7 +810,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
721
810
|
signal: AbortSignal.timeout(2000),
|
|
722
811
|
});
|
|
723
812
|
if (response.ok) {
|
|
724
|
-
const session = await response.json().catch(() => null);
|
|
813
|
+
const session = unwrapOpenCodeResponse(await response.json().catch(() => null));
|
|
725
814
|
if (typeof session?.title === 'string') sessionName = session.title.trim();
|
|
726
815
|
}
|
|
727
816
|
} catch {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { summarizeText as summarizeSharedText } from '../text/summarization.js';
|
|
2
|
+
import { unwrapOpenCodeResponse } from '../opencode/response-envelope.js';
|
|
2
3
|
|
|
3
4
|
export const createNotificationTemplateRuntime = (deps) => {
|
|
4
5
|
const {
|
|
@@ -136,7 +137,7 @@ export const createNotificationTemplateRuntime = (deps) => {
|
|
|
136
137
|
if (!sessionId) return '';
|
|
137
138
|
|
|
138
139
|
try {
|
|
139
|
-
const url = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, '');
|
|
140
|
+
const url = buildOpenCodeUrl(`/api/session/${encodeURIComponent(sessionId)}/message`, '');
|
|
140
141
|
const response = await fetch(`${url}?limit=5`, {
|
|
141
142
|
method: 'GET',
|
|
142
143
|
headers: {
|
|
@@ -148,26 +149,23 @@ export const createNotificationTemplateRuntime = (deps) => {
|
|
|
148
149
|
|
|
149
150
|
if (!response.ok) return '';
|
|
150
151
|
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
// v2 pages messages as `{ data, cursor }`, newest first, and a message is
|
|
153
|
+
// a flat record: an assistant one carries `content[]`, not `parts`.
|
|
154
|
+
const page = await response.json().catch(() => null);
|
|
155
|
+
const messages = Array.isArray(page?.data) ? page.data : null;
|
|
156
|
+
if (!messages) return '';
|
|
153
157
|
|
|
154
158
|
let target = null;
|
|
155
159
|
if (messageId) {
|
|
156
|
-
target = messages.find((message) => message?.
|
|
160
|
+
target = messages.find((message) => message?.id === messageId && message?.type === 'assistant');
|
|
157
161
|
}
|
|
158
162
|
if (!target) {
|
|
159
|
-
|
|
160
|
-
const message = messages[i];
|
|
161
|
-
if (message?.info?.role === 'assistant' && message?.info?.finish === 'stop') {
|
|
162
|
-
target = message;
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
163
|
+
target = messages.find((message) => message?.type === 'assistant' && message?.finish === 'stop') ?? null;
|
|
166
164
|
}
|
|
167
165
|
|
|
168
|
-
if (!target || !Array.isArray(target.
|
|
166
|
+
if (!target || !Array.isArray(target.content)) return '';
|
|
169
167
|
|
|
170
|
-
return extractTextFromParts(target.
|
|
168
|
+
return extractTextFromParts(target.content, maxLength);
|
|
171
169
|
} catch {
|
|
172
170
|
return '';
|
|
173
171
|
}
|
|
@@ -201,7 +199,7 @@ export const createNotificationTemplateRuntime = (deps) => {
|
|
|
201
199
|
}
|
|
202
200
|
|
|
203
201
|
try {
|
|
204
|
-
const url = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
|
|
202
|
+
const url = buildOpenCodeUrl(`/api/session/${encodeURIComponent(sessionId)}`, '');
|
|
205
203
|
const response = await fetch(url, {
|
|
206
204
|
method: 'GET',
|
|
207
205
|
headers: { Accept: 'application/json' },
|
|
@@ -211,7 +209,7 @@ export const createNotificationTemplateRuntime = (deps) => {
|
|
|
211
209
|
console.warn(`[Notification] fetchSessionInfo: ${response.status} for session ${sessionId}`);
|
|
212
210
|
return null;
|
|
213
211
|
}
|
|
214
|
-
const data = await response.json().catch(() => null);
|
|
212
|
+
const data = unwrapOpenCodeResponse(await response.json().catch(() => null));
|
|
215
213
|
if (data && typeof data === 'object') {
|
|
216
214
|
sessionInfoCache.set(sessionId, { data, at: Date.now() });
|
|
217
215
|
return data;
|
|
@@ -69,15 +69,22 @@ describe('notification template message extraction', () => {
|
|
|
69
69
|
|
|
70
70
|
it('excludes reasoning parts when fetching assistant messages', async () => {
|
|
71
71
|
const runtime = createRuntime();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
// v2 pages messages as `{ data, cursor }` and an assistant message is a
|
|
73
|
+
// flat record carrying `content[]`.
|
|
74
|
+
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
|
|
75
|
+
data: [
|
|
76
|
+
{
|
|
77
|
+
id: 'msg-1',
|
|
78
|
+
type: 'assistant',
|
|
79
|
+
finish: 'stop',
|
|
80
|
+
content: [
|
|
81
|
+
{ type: 'reasoning', text: 'private chain of thought' },
|
|
82
|
+
{ type: 'text', text: 'final answer' },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
cursor: {},
|
|
87
|
+
})));
|
|
81
88
|
|
|
82
89
|
await expect(runtime.fetchLastAssistantMessageText('session-1', 'msg-1')).resolves.toBe('final answer');
|
|
83
90
|
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createNotificationTriggerRuntime } from './runtime.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The "ready" push is built from the translated `message.updated` events.
|
|
7
|
+
* v2 splits a turn: the step start names agent and model, the step end
|
|
8
|
+
* carries the finish. What is pinned here: the finish is announced with the
|
|
9
|
+
* agent and model from the start, a user abort announces nothing, and an
|
|
10
|
+
* active goal (which lives in OpenChamber's own metadata) silences the push.
|
|
11
|
+
*/
|
|
12
|
+
const makeRuntime = ({ metadata = {} } = {}) => {
|
|
13
|
+
const emitDesktopNotification = vi.fn(() => true);
|
|
14
|
+
const runtime = createNotificationTriggerRuntime({
|
|
15
|
+
readSettingsFromDisk: async () => ({ nativeNotificationsEnabled: true, notificationMode: 'always', notifyOnCompletion: true }),
|
|
16
|
+
prepareNotificationLastMessage: async ({ message }) => message,
|
|
17
|
+
buildTemplateVariables: async () => ({}),
|
|
18
|
+
extractLastMessageText: () => 'done',
|
|
19
|
+
fetchLastAssistantMessageText: async () => 'done',
|
|
20
|
+
resolveNotificationTemplate: () => '',
|
|
21
|
+
shouldApplyResolvedTemplateMessage: () => false,
|
|
22
|
+
emitDesktopNotification,
|
|
23
|
+
broadcastUiNotification: vi.fn(),
|
|
24
|
+
sendPushToAllUiSessions: vi.fn(async () => undefined),
|
|
25
|
+
sendApnsToAllUiSessions: vi.fn(async () => undefined),
|
|
26
|
+
isAnyInteractiveClientVisible: () => true,
|
|
27
|
+
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
28
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
29
|
+
readSessionMetadata: async () => metadata,
|
|
30
|
+
});
|
|
31
|
+
return { runtime, emitDesktopNotification };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const stepStarted = (sessionID, id) => ({
|
|
35
|
+
type: 'message.updated',
|
|
36
|
+
properties: { sessionID, info: { id, sessionID, role: 'assistant', agent: 'build', providerID: 'anthropic', modelID: 'claude-sonnet-5', time: { created: 1 } } },
|
|
37
|
+
});
|
|
38
|
+
const stepEnded = (sessionID, id) => ({
|
|
39
|
+
type: 'message.updated',
|
|
40
|
+
properties: { sessionID, info: { id, sessionID, role: 'assistant', finish: 'stop', time: { completed: 2 } } },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const turnEnded = (sessionID) => ({ type: 'session.idle', properties: { sessionID } });
|
|
44
|
+
|
|
45
|
+
describe('ready notification on v2 step events', () => {
|
|
46
|
+
it('announces the turn end with the agent and model of its last step', async () => {
|
|
47
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
48
|
+
const { runtime, emitDesktopNotification } = makeRuntime();
|
|
49
|
+
|
|
50
|
+
await runtime.maybeSendPushForTrigger(stepStarted('ses_1', 'msg_1'));
|
|
51
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
52
|
+
|
|
53
|
+
// A step's `stop` is not the end of the turn: the execution may still
|
|
54
|
+
// drain steering input, so only its idle event announces readiness.
|
|
55
|
+
await runtime.maybeSendPushForTrigger(stepEnded('ses_1', 'msg_1'));
|
|
56
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
57
|
+
|
|
58
|
+
await runtime.maybeSendPushForTrigger(turnEnded('ses_1'));
|
|
59
|
+
expect(emitDesktopNotification).toHaveBeenCalledTimes(1);
|
|
60
|
+
expect(emitDesktopNotification.mock.calls[0][0]).toMatchObject({
|
|
61
|
+
kind: 'ready',
|
|
62
|
+
title: 'Build agent is ready',
|
|
63
|
+
body: 'Claude Sonnet 5 completed the task',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('announces nothing for a user abort', async () => {
|
|
68
|
+
const { runtime, emitDesktopNotification } = makeRuntime();
|
|
69
|
+
|
|
70
|
+
await runtime.maybeSendPushForTrigger({
|
|
71
|
+
type: 'session.idle',
|
|
72
|
+
properties: { sessionID: 'ses_2', aborted: true, reason: 'user', error: { name: 'MessageAbortedError', message: 'aborted' } },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('stays quiet while a goal from OpenChamber\'s own metadata is active', async () => {
|
|
79
|
+
const { runtime, emitDesktopNotification } = makeRuntime({
|
|
80
|
+
metadata: { openchamber: { goal: { id: 'g', status: 'active', objective: 'x' } } },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await runtime.maybeSendPushForTrigger(stepStarted('ses_3', 'msg_3'));
|
|
84
|
+
await runtime.maybeSendPushForTrigger(stepEnded('ses_3', 'msg_3'));
|
|
85
|
+
await runtime.maybeSendPushForTrigger(turnEnded('ses_3'));
|
|
86
|
+
|
|
87
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('ready notification while background subagents run', () => {
|
|
92
|
+
const stubOpenCode = ({ active, children }) => {
|
|
93
|
+
vi.stubGlobal('fetch', vi.fn(async (input) => {
|
|
94
|
+
const url = new URL(String(input));
|
|
95
|
+
if (url.pathname === '/api/session/active') return Response.json({ data: active() });
|
|
96
|
+
if (url.pathname === '/api/session') return Response.json({ data: children, cursor: {} });
|
|
97
|
+
return Response.json({});
|
|
98
|
+
}));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
it('stays silent on the pause and announces the real turn end', async () => {
|
|
102
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
103
|
+
let active = { ses_child: { type: 'running' } };
|
|
104
|
+
stubOpenCode({ active: () => active, children: [{ id: 'ses_child', parentID: 'ses_1' }] });
|
|
105
|
+
const { runtime, emitDesktopNotification } = makeRuntime();
|
|
106
|
+
|
|
107
|
+
await runtime.maybeSendPushForTrigger(stepStarted('ses_1', 'msg_1'));
|
|
108
|
+
await runtime.maybeSendPushForTrigger(turnEnded('ses_1'));
|
|
109
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
110
|
+
|
|
111
|
+
// The subagent finished and OpenCode ran the parent again with its result.
|
|
112
|
+
active = {};
|
|
113
|
+
await runtime.maybeSendPushForTrigger(turnEnded('ses_1'));
|
|
114
|
+
expect(emitDesktopNotification).toHaveBeenCalledTimes(1);
|
|
115
|
+
vi.unstubAllGlobals();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('announces the idle when the subagent check cannot be made', async () => {
|
|
119
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
120
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response('down', { status: 503 })));
|
|
121
|
+
const { runtime, emitDesktopNotification } = makeRuntime();
|
|
122
|
+
|
|
123
|
+
await runtime.maybeSendPushForTrigger(stepStarted('ses_1', 'msg_1'));
|
|
124
|
+
await runtime.maybeSendPushForTrigger(turnEnded('ses_1'));
|
|
125
|
+
expect(emitDesktopNotification).toHaveBeenCalledTimes(1);
|
|
126
|
+
vi.unstubAllGlobals();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('subagent finish with subagent notifications off', () => {
|
|
131
|
+
const makeSubtaskRuntime = () => {
|
|
132
|
+
const emitDesktopNotification = vi.fn(() => true);
|
|
133
|
+
const runtime = createNotificationTriggerRuntime({
|
|
134
|
+
readSettingsFromDisk: async () => ({ nativeNotificationsEnabled: true, notificationMode: 'always', notifyOnCompletion: true, notifyOnSubtasks: false }),
|
|
135
|
+
prepareNotificationLastMessage: async ({ message }) => message,
|
|
136
|
+
buildTemplateVariables: async () => ({}),
|
|
137
|
+
extractLastMessageText: () => 'done',
|
|
138
|
+
fetchLastAssistantMessageText: async () => 'done',
|
|
139
|
+
resolveNotificationTemplate: () => '',
|
|
140
|
+
shouldApplyResolvedTemplateMessage: () => false,
|
|
141
|
+
emitDesktopNotification,
|
|
142
|
+
broadcastUiNotification: vi.fn(),
|
|
143
|
+
sendPushToAllUiSessions: vi.fn(async () => undefined),
|
|
144
|
+
sendApnsToAllUiSessions: vi.fn(async () => undefined),
|
|
145
|
+
isAnyInteractiveClientVisible: () => true,
|
|
146
|
+
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
147
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
148
|
+
readSessionMetadata: async () => ({}),
|
|
149
|
+
});
|
|
150
|
+
return { runtime, emitDesktopNotification };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// OpenCode answers `GET /api/session/:id` as `{ data }` with no `location`.
|
|
154
|
+
const stubSessionRecord = () => {
|
|
155
|
+
vi.stubGlobal('fetch', vi.fn(async (input) => {
|
|
156
|
+
const url = new URL(String(input));
|
|
157
|
+
if (url.pathname === '/api/session/active') return Response.json({ data: {} });
|
|
158
|
+
if (url.pathname === '/api/session/ses_child') return Response.json({ data: { id: 'ses_child', parentID: 'ses_parent' } });
|
|
159
|
+
return Response.json({ data: [], cursor: {} });
|
|
160
|
+
}));
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
it('reads the parent from the session record envelope', async () => {
|
|
164
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
165
|
+
stubSessionRecord();
|
|
166
|
+
const { runtime, emitDesktopNotification } = makeSubtaskRuntime();
|
|
167
|
+
|
|
168
|
+
await runtime.maybeSendPushForTrigger(stepStarted('ses_child', 'msg_c'));
|
|
169
|
+
await runtime.maybeSendPushForTrigger(turnEnded('ses_child'));
|
|
170
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
171
|
+
vi.unstubAllGlobals();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('a partial session update does not erase a known parent', async () => {
|
|
175
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
176
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response('down', { status: 503 })));
|
|
177
|
+
const { runtime, emitDesktopNotification } = makeSubtaskRuntime();
|
|
178
|
+
|
|
179
|
+
await runtime.maybeSendPushForTrigger({ type: 'session.created', properties: { sessionID: 'ses_child', info: { id: 'ses_child', parentID: 'ses_parent' } } });
|
|
180
|
+
// Usage updates arrive as `session.updated` with a partial record.
|
|
181
|
+
await runtime.maybeSendPushForTrigger({ type: 'session.updated', properties: { sessionID: 'ses_child', info: { id: 'ses_child', cost: 1 } } });
|
|
182
|
+
await runtime.maybeSendPushForTrigger(stepStarted('ses_child', 'msg_c'));
|
|
183
|
+
await runtime.maybeSendPushForTrigger(turnEnded('ses_child'));
|
|
184
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
185
|
+
vi.unstubAllGlobals();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -59,6 +59,16 @@ other.
|
|
|
59
59
|
directory and does not erase other session results.
|
|
60
60
|
- Destructive session/worktree deletion and project-path registration are not
|
|
61
61
|
part of the action contract.
|
|
62
|
+
- `file.open` shows a file in the user's viewer. `file-open.js` resolves a
|
|
63
|
+
relative path against the session directory (an explicit `directory` wins),
|
|
64
|
+
refuses a relative path with no directory at all, checks the target is an
|
|
65
|
+
existing file, then hands `{ path, directory, sessionId }` to the injected
|
|
66
|
+
`emit`, which `index.js` writes to every UI control stream as
|
|
67
|
+
`openchamber:file-open-request`. Nothing comes back: opening a tab does not
|
|
68
|
+
fail quietly on a client, so the count of clients reached is the signal, and
|
|
69
|
+
zero is a 503, never a claimed success. Paths outside the workspace are
|
|
70
|
+
allowed on purpose: screenshots and recordings often land in a temp
|
|
71
|
+
directory, and the viewer already reads such files.
|
|
62
72
|
- `browser.capture` writes its image on the server, into
|
|
63
73
|
`.openchamber/screenshots/` under the scoped project directory, and returns
|
|
64
74
|
the project-relative path rather than the image bytes. The client that took
|
|
@@ -22,6 +22,7 @@ export const OPENCHAMBER_CONTROL_ACTION_DEFINITIONS = Object.freeze([
|
|
|
22
22
|
{ action: 'schedule.run', title: 'Run a scheduled task', description: 'Run taskId; scope with projectId or directory' },
|
|
23
23
|
{ action: 'schedule.delete', title: 'Delete a scheduled task', description: 'Delete taskId; scope with projectId or directory' },
|
|
24
24
|
{ action: 'schedule.toggle', title: 'Enable or disable a scheduled task', description: 'Enable or disable taskId; requires the disabled boolean' },
|
|
25
|
+
{ action: 'file.open', title: 'Show a file to the user', description: 'Open path in the user\'s file panel, in front of whatever they had open, so they can look at a result you produced: a screenshot, report, CSV, recording, or generated page. Relative to the session directory. Use it only when seeing the file moves the work forward, not for every file you touch' },
|
|
25
26
|
]);
|
|
26
27
|
|
|
27
28
|
const OPENCHAMBER_CONTROL_ACTIONS = Object.freeze(
|
|
@@ -65,9 +66,9 @@ export const OPENCHAMBER_WEB_ACTIONS = Object.freeze(
|
|
|
65
66
|
* reads as a complete fact is exactly the one whose conditions get lost.
|
|
66
67
|
*/
|
|
67
68
|
export const OPENCHAMBER_MEMORY_ACTION_DEFINITIONS = Object.freeze([
|
|
68
|
-
{ action: 'memory.read', title: 'Read a stored memory', description: 'Read the full text of one memory listed in the session index. The index shows titles only, and a title omits the conditions that decide how the memory applies, so read before acting rather than working from the title. Requires title (as the index spells it) or memoryId; scope is optional and both stores are searched without it' },
|
|
69
|
+
{ action: 'memory.read', title: 'Read a stored memory', description: 'Read the full text of one memory listed in the session index. The index shows titles only, and a title omits the conditions that decide how the memory applies, so read before acting rather than working from the title. Once read, an entry stays in your context; do not read it again in the same conversation. Requires title (as the index spells it) or memoryId; scope is optional and both stores are searched without it' },
|
|
69
70
|
{ action: 'memory.list', title: 'List stored memories', description: 'List stored memory titles when the session index is missing or stale; scope is global, project, or both (default)' },
|
|
70
|
-
{ action: 'memory.save', title: 'Remember something', description: 'Store a durable fact, preference, or reference; requires title and body, plus scope global (about the user) or project (about this codebase). Restating something already stored updates it. Do not store secrets, one-off task state, or anything the user asked you not to keep' },
|
|
71
|
+
{ action: 'memory.save', title: 'Remember something', description: 'Store a durable fact, preference, or reference; requires title and body, plus scope global (about the user) or project (about this codebase). Restating something already stored updates it. Do not store secrets, one-off task state, or anything the user asked you not to keep; when the user explicitly asks you to remember something, store it unless it is a secret' },
|
|
71
72
|
{ action: 'memory.delete', title: 'Forget a memory', description: 'Delete a memory that turned out to be wrong or obsolete; requires memoryId and scope' },
|
|
72
73
|
]);
|
|
73
74
|
|
|
@@ -10,7 +10,7 @@ export class OpenChamberControlError extends Error {
|
|
|
10
10
|
export const asControlError = (error, fallbackMessage, fallbackStatus = 500) => {
|
|
11
11
|
if (error instanceof OpenChamberControlError) return error;
|
|
12
12
|
const message = error instanceof Error ? error.message : fallbackMessage;
|
|
13
|
-
return new OpenChamberControlError(message || fallbackMessage, Number(error?.statusCode) || fallbackStatus, {
|
|
13
|
+
return new OpenChamberControlError(message || fallbackMessage, Number(error?.statusCode) || Number(error?.status) || fallbackStatus, {
|
|
14
14
|
...(error?.goalConfigured === true ? { goalConfigured: true } : {}),
|
|
15
15
|
});
|
|
16
16
|
};
|