@vantran-se/openchamber-web 1.24.4 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -8
- package/bin/lib/commands-connect-url.js +3 -8
- package/bin/lib/commands-tunnel.js +3 -0
- package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-B82Oye3V.js} +1 -1
- package/dist/assets/DiffView-DJLmWn6Z.js +4 -0
- package/dist/assets/FilesView-DjksmZRo.js +17 -0
- package/dist/assets/{GitView-Byu2DY7s.js → GitView-DiUsUIgr.js} +4 -4
- package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-CYwWYLr3.js} +1 -1
- package/dist/assets/GuestSurfacePane-CmctyRI4.js +1 -0
- package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-DOHHcbgJ.js} +1 -1
- package/dist/assets/LinearIssuesView-DaqnylcV.js +1 -0
- package/dist/assets/MarkdownImageGallery-HJNvWt7L.js +1 -0
- package/dist/assets/MarkdownRendererImpl-nDZRGg2I.js +12 -0
- package/dist/assets/OnboardingScreen-C2B5Bsof.js +1 -0
- package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-C4FLBjpt.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-C_1S9c9n.js} +1 -1
- package/dist/assets/PlanView-_eZvb-G5.js +5 -0
- package/dist/assets/PluginPane-sM8fcZIV.js +3 -0
- package/dist/assets/SettingsView-BRfr46hb.js +29 -0
- package/dist/assets/SettingsWindow-CcUDwQHg.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-CrN1ZGwl.js +1 -0
- package/dist/assets/ToolOutputDialog-Cij4jI09.js +15 -0
- package/dist/assets/ToolPartDiffPreview-BXzwQ-JY.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-DSWTLJNH.js} +4 -4
- package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-CpUUgY8H.js} +2 -2
- package/dist/assets/appThemeRegistry-B5tHTkR8.js +1 -0
- package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-D7D8qV8p.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-cq9uQ30i.js} +1 -1
- package/dist/assets/de-CUO1-KqF.js +10 -0
- package/dist/assets/es-BlbC6sGJ.js +16 -0
- package/dist/assets/fr-D2nwe_0W.js +14 -0
- package/dist/assets/index-BQOoYIOu.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D0obJ2p8.js} +1 -1
- package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-DB3_rjyW.js} +1 -1
- package/dist/assets/ja-CA7vnDsZ.js +16 -0
- package/dist/assets/ko-Chy6a9l-.js +16 -0
- package/dist/assets/main-B9JFK3vF.js +896 -0
- package/dist/assets/main-Bhiqnrhd.js +2 -0
- package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-tfqOFaBd.js} +4 -4
- package/dist/assets/miniChat-B0UOL5zA.js +2 -0
- package/dist/assets/mobile-BJppFIEo.js +2 -0
- package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-Db6KuwCP.js} +3 -3
- package/dist/assets/pl-CTR5A_1P.js +16 -0
- package/dist/assets/pt-BR-BE1O1KEx.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-Yz5sfoFh.js +2 -0
- package/dist/assets/renderMobileApp-QQ-yB8aY.js +25 -0
- package/dist/assets/runtimeEndpointReset-DKDdzRzm.js +27 -0
- package/dist/assets/scheduledTasksApi-C5sCuNqf.js +1 -0
- package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-xURoo5t_.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-BZLGuYTU.js} +1 -1
- package/dist/assets/tr-Bh5vK6cW.js +7 -0
- package/dist/assets/uk-CLDkZFy_.js +16 -0
- package/dist/assets/useAppFontEffects-BxrtCnfR.js +7337 -0
- package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CeXuR-SR.js} +2 -2
- package/dist/assets/useRootScrollLock-DgWxezc1.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-DOuaQUZ0.js} +2 -2
- package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
- package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
- package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
- package/dist/assets/{web-components-DxX8bZ1o.js → web-components-63H1h21j.js} +2 -2
- package/dist/assets/zh-CN-DODgcmYj.js +16 -0
- package/dist/assets/zh-TW-ByNfIwLA.js +16 -0
- package/dist/index.html +13 -7
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +6 -5
- package/server/index.d.ts +1 -0
- package/server/index.js +246 -91
- package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
- package/server/lib/agent-tool/runtime.js +135 -54
- package/server/lib/agent-tool/runtime.test.js +155 -87
- package/server/lib/browser-control/DOCUMENTATION.md +37 -2
- package/server/lib/browser-control/provider.js +217 -0
- package/server/lib/browser-control/provider.test.js +251 -0
- package/server/lib/context-obligatory/runtime.js +61 -50
- package/server/lib/context-obligatory/runtime.test.js +62 -162
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +68 -57
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-hub.js +10 -1
- package/server/lib/event-stream/global-hub.test.js +27 -26
- package/server/lib/event-stream/index.js +6 -0
- package/server/lib/event-stream/protocol.js +26 -19
- package/server/lib/event-stream/protocol.test.js +18 -7
- package/server/lib/event-stream/rebind.test.js +5 -5
- package/server/lib/event-stream/runtime.test.js +17 -15
- package/server/lib/event-stream/translate-v2.js +313 -0
- package/server/lib/event-stream/translate-v2.test.js +209 -0
- package/server/lib/event-stream/upstream-reader.test.js +16 -16
- package/server/lib/fs/DOCUMENTATION.md +4 -2
- package/server/lib/fs/byte-range.js +35 -0
- package/server/lib/fs/byte-range.test.js +31 -0
- package/server/lib/fs/routes.js +47 -42
- package/server/lib/fs/routes.test.js +123 -18
- package/server/lib/git/service.js +12 -0
- package/server/lib/github/DOCUMENTATION.md +1 -1
- package/server/lib/github/pr-status.js +22 -2
- package/server/lib/github/pr-status.test.js +27 -14
- package/server/lib/guests/DOCUMENTATION.md +13 -0
- package/server/lib/guests/catalog.js +12 -0
- package/server/lib/guests/catalog.test.js +33 -0
- package/server/lib/guests/routes.js +10 -1
- package/server/lib/guests/service.js +192 -14
- package/server/lib/guests/service.test.js +201 -1
- package/server/lib/guests/surface.js +630 -0
- package/server/lib/guests/surface.test.js +486 -0
- package/server/lib/markdown-image-grants/routes.js +18 -5
- package/server/lib/markdown-image-grants/routes.test.js +9 -6
- package/server/lib/message-queue/DOCUMENTATION.md +33 -22
- package/server/lib/message-queue/runtime.js +155 -100
- package/server/lib/message-queue/runtime.test.js +168 -82
- package/server/lib/notifications/DOCUMENTATION.md +28 -0
- package/server/lib/notifications/control-stream.http.test.js +68 -0
- package/server/lib/notifications/emitter-runtime.js +15 -0
- package/server/lib/notifications/emitter-runtime.test.js +32 -0
- package/server/lib/notifications/push-runtime.js +5 -1
- package/server/lib/notifications/push-runtime.test.js +2 -2
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +115 -26
- package/server/lib/notifications/template-runtime.js +13 -15
- package/server/lib/notifications/template-runtime.test.js +16 -9
- package/server/lib/notifications/trigger-runtime.test.js +187 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
- package/server/lib/openchamber-control/actions.js +3 -2
- package/server/lib/openchamber-control/error.js +1 -1
- package/server/lib/openchamber-control/file-open.js +59 -0
- package/server/lib/openchamber-control/file-open.test.js +57 -0
- package/server/lib/openchamber-control/routes.test.js +12 -0
- package/server/lib/openchamber-control/service.js +102 -49
- package/server/lib/openchamber-control/service.test.js +84 -44
- package/server/lib/openchamber-sessions/archive-store.js +224 -0
- package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
- package/server/lib/openchamber-sessions/opencode-client.js +22 -0
- package/server/lib/openchamber-sessions/routes.js +398 -328
- package/server/lib/openchamber-sessions/routes.test.js +777 -740
- package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
- package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
- package/server/lib/opencode/DOCUMENTATION.md +449 -29
- package/server/lib/opencode/agents.js +233 -327
- package/server/lib/opencode/auth.js +41 -44
- package/server/lib/opencode/auth.test.js +92 -0
- package/server/lib/opencode/cli-upgrade.d.ts +4 -0
- package/server/lib/opencode/cli-upgrade.js +18 -0
- package/server/lib/opencode/cli-upgrade.test.js +36 -0
- package/server/lib/opencode/commands.js +164 -145
- package/server/lib/opencode/compatibility.d.ts +16 -0
- package/server/lib/opencode/compatibility.js +88 -0
- package/server/lib/opencode/compatibility.test.js +62 -0
- package/server/lib/opencode/config-entity-routes.js +58 -29
- package/server/lib/opencode/config-entity-v2.test.js +290 -0
- package/server/lib/opencode/config-mutation-response.js +17 -7
- package/server/lib/opencode/config-mutation-response.test.js +4 -7
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.d.ts +183 -0
- package/server/lib/opencode/config-v2.js +801 -0
- package/server/lib/opencode/config-v2.test.js +466 -0
- package/server/lib/opencode/credential-db.js +144 -0
- package/server/lib/opencode/credential-db.test.js +104 -0
- package/server/lib/opencode/env-runtime.js +48 -9
- package/server/lib/opencode/env-runtime.test.js +45 -0
- package/server/lib/opencode/feature-routes-runtime.js +15 -5
- package/server/lib/opencode/lifecycle-process.test.js +7 -4
- package/server/lib/opencode/lifecycle.js +85 -30
- package/server/lib/opencode/lifecycle.test.js +80 -8
- package/server/lib/opencode/login-shell-env.js +27 -0
- package/server/lib/opencode/managed-config-file.js +119 -0
- package/server/lib/opencode/managed-config-file.test.js +152 -0
- package/server/lib/opencode/managed-plugin-config.js +33 -13
- package/server/lib/opencode/mcp.js +43 -149
- package/server/lib/opencode/network-runtime.js +6 -7
- package/server/lib/opencode/openchamber-routes.test.js +2 -5
- package/server/lib/opencode/plugin-routes.js +3 -3
- package/server/lib/opencode/plugin-routes.test.js +6 -24
- package/server/lib/opencode/plugins.js +91 -40
- package/server/lib/opencode/plugins.test.js +28 -15
- package/server/lib/opencode/providers.js +63 -83
- package/server/lib/opencode/providers.test.js +128 -59
- package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
- package/server/lib/opencode/proxy.js +179 -129
- package/server/lib/opencode/pwa-manifest-routes.js +4 -2
- package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
- package/server/lib/opencode/response-envelope.js +14 -0
- package/server/lib/opencode/response-envelope.test.js +23 -0
- package/server/lib/opencode/routes-upgrade.test.js +73 -116
- package/server/lib/opencode/routes.js +121 -428
- package/server/lib/opencode/server-utils-runtime.js +10 -4
- package/server/lib/opencode/session-activity.js +78 -0
- package/server/lib/opencode/session-runtime.js +21 -10
- package/server/lib/opencode/session-runtime.test.js +9 -9
- package/server/lib/opencode/settings-helpers.js +9 -9
- package/server/lib/opencode/settings-helpers.test.js +7 -12
- package/server/lib/opencode/settings-registry.json +6 -8
- package/server/lib/opencode/settings-runtime.js +18 -1
- package/server/lib/opencode/settings-runtime.test.js +31 -2
- package/server/lib/opencode/shared.js +54 -22
- package/server/lib/opencode/shared.test.js +8 -4
- package/server/lib/opencode/shutdown-runtime.js +54 -7
- package/server/lib/opencode/shutdown-runtime.test.js +224 -30
- package/server/lib/opencode/skill-routes.js +23 -24
- package/server/lib/opencode/skills.js +7 -5
- package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
- package/server/lib/opencode/upgrade-capability.test.js +1 -1
- package/server/lib/opencode/v1-migration-topup.js +288 -0
- package/server/lib/opencode/v1-migration-topup.test.js +266 -0
- package/server/lib/opencode/v2-install.d.ts +8 -0
- package/server/lib/opencode/v2-install.js +158 -0
- package/server/lib/opencode/v2-install.test.js +124 -0
- package/server/lib/opencode/watcher.js +10 -3
- package/server/lib/opencode/watcher.test.js +17 -21
- package/server/lib/opencode/websearch-config.js +66 -0
- package/server/lib/package-manager.js +55 -68
- package/server/lib/package-manager.test.js +241 -75
- package/server/lib/permission-auto-accept/runtime.js +38 -22
- package/server/lib/permission-auto-accept/runtime.test.js +39 -17
- package/server/lib/quota/DOCUMENTATION.md +4 -0
- package/server/lib/quota/providers/xai.js +4 -4
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +7 -0
- package/server/lib/routing/DOCUMENTATION.md +52 -21
- package/server/lib/routing/defaults.js +23 -6
- package/server/lib/routing/jev.js +17 -7
- package/server/lib/routing/routes.http.test.js +68 -33
- package/server/lib/routing/routes.js +65 -22
- package/server/lib/routing/runtime.js +145 -48
- package/server/lib/routing/runtime.test.js +79 -55
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
- package/server/lib/scheduled-tasks/runtime.js +81 -74
- package/server/lib/scheduled-tasks/runtime.test.js +53 -1
- package/server/lib/session-assist/DOCUMENTATION.md +8 -3
- package/server/lib/session-assist/context.js +122 -46
- package/server/lib/session-assist/context.test.js +117 -49
- package/server/lib/session-assist/runtime.js +52 -22
- package/server/lib/session-assist/runtime.test.js +145 -173
- package/server/lib/session-goal/DOCUMENTATION.md +23 -15
- package/server/lib/session-goal/create.js +9 -12
- package/server/lib/session-goal/create.test.js +27 -28
- package/server/lib/session-goal/runtime.js +164 -77
- package/server/lib/session-goal/runtime.test.js +271 -519
- package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
- package/server/lib/session-knowledge/runtime.js +59 -41
- package/server/lib/session-knowledge/runtime.test.js +90 -20
- package/server/lib/small-model/DOCUMENTATION.md +154 -208
- package/server/lib/small-model/client.js +113 -0
- package/server/lib/small-model/index.js +296 -158
- package/server/lib/small-model/index.test.js +423 -256
- package/server/lib/small-model/routes.js +1 -1
- package/server/lib/spaces/DOCUMENTATION.md +807 -0
- package/server/lib/spaces/code-in-bait.js +185 -0
- package/server/lib/spaces/code-in.js +641 -0
- package/server/lib/spaces/code-in.test.js +1048 -0
- package/server/lib/spaces/errors.js +8 -0
- package/server/lib/spaces/exec-http.js +94 -0
- package/server/lib/spaces/gatekeeper-channel.js +202 -0
- package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
- package/server/lib/spaces/gatekeeper-program.cjs +914 -0
- package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
- package/server/lib/spaces/hardening.js +457 -0
- package/server/lib/spaces/hardening.test.js +368 -0
- package/server/lib/spaces/host-git.js +127 -0
- package/server/lib/spaces/host-git.test.js +137 -0
- package/server/lib/spaces/labels.js +234 -0
- package/server/lib/spaces/labels.test.js +219 -0
- package/server/lib/spaces/layout.js +184 -0
- package/server/lib/spaces/layout.test.js +30 -0
- package/server/lib/spaces/manager.js +82 -0
- package/server/lib/spaces/manager.test.js +114 -0
- package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
- package/server/lib/spaces/places/contract-suite.js +156 -0
- package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
- package/server/lib/spaces/places/contract.test.js +6 -0
- package/server/lib/spaces/places/docker-engine.js +126 -0
- package/server/lib/spaces/places/docker-live-support.js +202 -0
- package/server/lib/spaces/places/docker-tools.js +186 -0
- package/server/lib/spaces/places/docker.js +587 -0
- package/server/lib/spaces/places/docker.test.js +1610 -0
- package/server/lib/spaces/places/escape-suite.js +1018 -0
- package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
- package/server/lib/spaces/places/fake-docker.js +375 -0
- package/server/lib/spaces/places/memory-place.js +103 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
- package/server/lib/spaces/places/registry.js +43 -0
- package/server/lib/spaces/places/registry.test.js +50 -0
- package/server/lib/spaces/places/server.docker.live.test.js +225 -0
- package/server/lib/spaces/run-command.js +261 -0
- package/server/lib/spaces/run-command.test.js +396 -0
- package/server/lib/spaces/space-server.js +190 -0
- package/server/lib/spaces/space-server.test.js +335 -0
- package/server/lib/spaces/tools-filler.js +82 -0
- package/server/lib/spaces/tools-filler.test.js +148 -0
- package/server/lib/spaces/tools-pack.js +49 -0
- package/server/lib/spaces/tools-pack.test.js +57 -0
- package/server/lib/spaces/tools.js +138 -0
- package/server/lib/spaces/tools.test.js +153 -0
- package/server/lib/tts/openai-sdk.js +11 -0
- package/server/lib/tts/service.js +7 -5
- package/server/lib/tts/stt.js +3 -2
- package/server/lib/tunnels/DOCUMENTATION.md +2 -0
- package/server/lib/tunnels/routes.js +7 -0
- package/server/lib/tunnels/routes.test.js +55 -0
- package/server/lib/ui-auth/ui-auth.js +38 -2
- package/server/lib/ui-auth/ui-auth.test.js +42 -0
- package/server/lib/ui-auth/ui-passkeys.js +12 -10
- package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
- package/server/lib/walkthrough/pull-request.js +61 -8
- package/server/lib/walkthrough/pull-request.test.js +54 -1
- package/server/lib/walkthrough/routes.js +20 -0
- package/server/lib/walkthrough/routes.test.js +19 -0
- package/server/opencode-proxy.test.js +190 -166
- package/server/opencode-worktree-readiness.test.js +5 -4
- package/dist/assets/DiffView-B2CD9wC8.js +0 -4
- package/dist/assets/FilesView-C4vMFclc.js +0 -13
- package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
- package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
- package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
- package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
- package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
- package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
- package/dist/assets/SettingsView-CSinuH-1.js +0 -29
- package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
- package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
- package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
- package/dist/assets/de-Bro6_zyl.js +0 -10
- package/dist/assets/es-Bb9XQZvd.js +0 -16
- package/dist/assets/fr-Dgfzn-IL.js +0 -14
- package/dist/assets/index-CtQNmqUL.js +0 -1
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/index-Dqc6USl3.js +0 -40
- package/dist/assets/ja-BYsakGFh.js +0 -16
- package/dist/assets/ko-BSaQz9aN.js +0 -16
- package/dist/assets/main-B62_pjL5.js +0 -2
- package/dist/assets/main-BEdcOI2P.js +0 -896
- package/dist/assets/miniChat-LT8Rqofr.js +0 -2
- package/dist/assets/mobile-CqarEynh.js +0 -2
- package/dist/assets/pl-OIviPdoj.js +0 -16
- package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
- package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
- package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
- package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
- package/dist/assets/tr-CQI-KPre.js +0 -7
- package/dist/assets/uk-CqW7R4DA.js +0 -16
- package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
- package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
- package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
- package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
- package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
- package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
- package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
- package/server/lib/routing/feature-flag.js +0 -11
- package/server/lib/small-model/call.js +0 -851
- package/server/lib/small-model/call.test.js +0 -1292
- package/server/lib/small-model/catalog.js +0 -13
- package/server/lib/small-model/resolve.js +0 -131
- package/server/lib/small-model/resolve.test.js +0 -197
- package/server/lib/small-model/runtime-providers.js +0 -164
- package/server/lib/small-model/runtime-providers.test.js +0 -118
- package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
- package/server/lib/system-prompt/runtime.js +0 -49
- package/server/lib/system-prompt/runtime.test.js +0 -76
- package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent asking the user to look at a file.
|
|
3
|
+
*
|
|
4
|
+
* The viewer lives in a renderer, so the server cannot open anything itself;
|
|
5
|
+
* it checks that the file is really there and tells every connected client to
|
|
6
|
+
* show it. No answer comes back: opening a tab is not something that fails
|
|
7
|
+
* quietly on the client, and the agent only needs to know whether anyone was
|
|
8
|
+
* there to see it.
|
|
9
|
+
*/
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import fsPromises from 'node:fs/promises';
|
|
12
|
+
|
|
13
|
+
import { OpenChamberControlError } from './error.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{
|
|
17
|
+
* emit: (request: { path: string, directory: string | null, sessionId: string | null }) => number,
|
|
18
|
+
* stat?: (target: string) => Promise<{ isFile(): boolean, size: number }>,
|
|
19
|
+
* }} dependencies
|
|
20
|
+
*/
|
|
21
|
+
export const createFileOpenRequester = ({ emit, stat = (target) => fsPromises.stat(target) }) => {
|
|
22
|
+
/** `path` arrives already reduced to a trimmed string or null by the service. */
|
|
23
|
+
const request = async ({ path: raw, directory, sessionId }) => {
|
|
24
|
+
if (!raw) throw new OpenChamberControlError('path is required for file.open', 400);
|
|
25
|
+
|
|
26
|
+
// A relative path means "in the project I am working in"; without a
|
|
27
|
+
// project there is nothing to resolve it against.
|
|
28
|
+
let absolute;
|
|
29
|
+
if (path.isAbsolute(raw)) {
|
|
30
|
+
absolute = path.normalize(raw);
|
|
31
|
+
} else if (directory) {
|
|
32
|
+
absolute = path.resolve(directory, raw);
|
|
33
|
+
} else {
|
|
34
|
+
throw new OpenChamberControlError('path must be absolute when no session directory is known', 400);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let stats;
|
|
38
|
+
try {
|
|
39
|
+
stats = await stat(absolute);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error?.code === 'ENOENT') {
|
|
42
|
+
throw new OpenChamberControlError(`File not found: ${absolute}`, 404);
|
|
43
|
+
}
|
|
44
|
+
throw new OpenChamberControlError(`Cannot read ${absolute}: ${error instanceof Error ? error.message : String(error)}`, 500);
|
|
45
|
+
}
|
|
46
|
+
if (!stats.isFile()) {
|
|
47
|
+
throw new OpenChamberControlError(`Not a file: ${absolute}`, 400);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const delivered = emit({ path: absolute, directory: directory ?? null, sessionId: sessionId ?? null });
|
|
51
|
+
if (delivered === 0) {
|
|
52
|
+
throw new OpenChamberControlError('No OpenChamber window is connected to show the file', 503);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { path: absolute, size: stats.size, opened: true };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return { request };
|
|
59
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createFileOpenRequester } from './file-open.js';
|
|
4
|
+
|
|
5
|
+
const file = { isFile: () => true, size: 42 };
|
|
6
|
+
|
|
7
|
+
describe('createFileOpenRequester', () => {
|
|
8
|
+
it('resolves a relative path against the session directory and tells the clients', async () => {
|
|
9
|
+
const emit = vi.fn(() => 2);
|
|
10
|
+
const stat = vi.fn(async () => file);
|
|
11
|
+
const requester = createFileOpenRequester({ emit, stat });
|
|
12
|
+
|
|
13
|
+
const result = await requester.request({ path: 'out/report.csv', directory: '/repo', sessionId: 'ses_1' });
|
|
14
|
+
|
|
15
|
+
expect(stat).toHaveBeenCalledWith('/repo/out/report.csv');
|
|
16
|
+
expect(emit).toHaveBeenCalledWith({ path: '/repo/out/report.csv', directory: '/repo', sessionId: 'ses_1' });
|
|
17
|
+
expect(result).toEqual({ path: '/repo/out/report.csv', size: 42, opened: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('keeps an absolute path outside the project, which is where screenshots and recordings often land', async () => {
|
|
21
|
+
const emit = vi.fn(() => 1);
|
|
22
|
+
const requester = createFileOpenRequester({ emit, stat: async () => file });
|
|
23
|
+
|
|
24
|
+
const result = await requester.request({ path: '/tmp/shot.png', directory: '/repo', sessionId: null });
|
|
25
|
+
|
|
26
|
+
expect(result.path).toBe('/tmp/shot.png');
|
|
27
|
+
expect(emit).toHaveBeenCalledWith({ path: '/tmp/shot.png', directory: '/repo', sessionId: null });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('refuses a relative path when no directory is known, instead of guessing one', async () => {
|
|
31
|
+
const requester = createFileOpenRequester({ emit: () => 1, stat: async () => file });
|
|
32
|
+
await expect(requester.request({ path: 'report.csv', directory: null, sessionId: null }))
|
|
33
|
+
.rejects.toMatchObject({ statusCode: 400 });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('reports a missing file and a directory before asking anyone to open them', async () => {
|
|
37
|
+
const emit = vi.fn(() => 1);
|
|
38
|
+
const missing = createFileOpenRequester({
|
|
39
|
+
emit,
|
|
40
|
+
stat: async () => { throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); },
|
|
41
|
+
});
|
|
42
|
+
await expect(missing.request({ path: '/repo/gone.png', directory: '/repo' }))
|
|
43
|
+
.rejects.toMatchObject({ statusCode: 404 });
|
|
44
|
+
|
|
45
|
+
const folder = createFileOpenRequester({ emit, stat: async () => ({ isFile: () => false, size: 0 }) });
|
|
46
|
+
await expect(folder.request({ path: '/repo/src', directory: '/repo' }))
|
|
47
|
+
.rejects.toMatchObject({ statusCode: 400 });
|
|
48
|
+
|
|
49
|
+
expect(emit).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('fails with 503 when no client is connected rather than claiming the file was shown', async () => {
|
|
53
|
+
const requester = createFileOpenRequester({ emit: () => 0, stat: async () => file });
|
|
54
|
+
await expect(requester.request({ path: '/repo/a.png', directory: '/repo' }))
|
|
55
|
+
.rejects.toMatchObject({ statusCode: 503 });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -2,6 +2,7 @@ import express from 'express';
|
|
|
2
2
|
import request from 'supertest';
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
|
+
import { BrowserControlError } from '../browser-control/broker.js';
|
|
5
6
|
import { OpenChamberControlError } from './error.js';
|
|
6
7
|
import { registerOpenChamberControlRoutes } from './routes.js';
|
|
7
8
|
|
|
@@ -43,4 +44,15 @@ describe('OpenChamber control route', () => {
|
|
|
43
44
|
directory: '/repo',
|
|
44
45
|
});
|
|
45
46
|
});
|
|
47
|
+
|
|
48
|
+
it('keeps the status of a browser refusal instead of reporting 500', async () => {
|
|
49
|
+
const execute = vi.fn(async () => {
|
|
50
|
+
throw new BrowserControlError('The user is interacting with this page in the panel right now.', 409);
|
|
51
|
+
});
|
|
52
|
+
const response = await request(createApp(execute))
|
|
53
|
+
.post('/api/openchamber/control')
|
|
54
|
+
.send({ action: 'browser.click', input: {} })
|
|
55
|
+
.expect(409);
|
|
56
|
+
expect(response.body).toEqual({ error: 'The user is interacting with this page in the panel right now.' });
|
|
57
|
+
});
|
|
46
58
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { OpenCode } from '@opencode/client';
|
|
3
3
|
import { OpenChamberControlError, asControlError } from './error.js';
|
|
4
4
|
import { OPENCHAMBER_ALL_ACTIONS } from './actions.js';
|
|
5
5
|
import { writeScreenshot } from './screenshots.js';
|
|
@@ -38,23 +38,32 @@ const normalizeWaitTimeoutMs = (value) => {
|
|
|
38
38
|
return seconds * 1000;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
// v2 messages are not { info, parts }: the record itself is the message, a
|
|
42
|
+
// user message carries `text`, and an assistant message carries `content[]`
|
|
43
|
+
// whose text items are the visible answer.
|
|
44
|
+
const messageText = (record) => {
|
|
45
|
+
if (record.type === 'user') return typeof record.text === 'string' ? record.text.trim() : '';
|
|
46
|
+
return (Array.isArray(record.content) ? record.content : [])
|
|
47
|
+
.filter((item) => item?.type === 'text' && typeof item.text === 'string')
|
|
48
|
+
.map((item) => item.text)
|
|
49
|
+
.join('')
|
|
50
|
+
.trim();
|
|
51
|
+
};
|
|
52
|
+
|
|
41
53
|
const extractTextMessages = (messages, role = 'all') => {
|
|
42
54
|
const result = [];
|
|
43
55
|
for (const record of Array.isArray(messages) ? messages : []) {
|
|
44
|
-
const
|
|
45
|
-
const messageRole = info?.role;
|
|
56
|
+
const messageRole = record?.type;
|
|
46
57
|
if ((messageRole !== 'user' && messageRole !== 'assistant') || (role !== 'all' && role !== messageRole)) continue;
|
|
47
|
-
const text =
|
|
48
|
-
? record.parts.filter((part) => part?.type === 'text' && typeof part.text === 'string').map((part) => part.text).join('').trim()
|
|
49
|
-
: '';
|
|
58
|
+
const text = messageText(record);
|
|
50
59
|
if (!text) continue;
|
|
51
|
-
const providerID = asNonEmptyString(
|
|
52
|
-
const modelID = asNonEmptyString(
|
|
60
|
+
const providerID = asNonEmptyString(record.model?.providerID);
|
|
61
|
+
const modelID = asNonEmptyString(record.model?.id);
|
|
53
62
|
result.push({
|
|
54
|
-
id: asNonEmptyString(
|
|
63
|
+
id: asNonEmptyString(record.id) || '',
|
|
55
64
|
role: messageRole,
|
|
56
|
-
createdAt: Number.isFinite(
|
|
57
|
-
completedAt: Number.isFinite(
|
|
65
|
+
createdAt: Number.isFinite(record?.time?.created) ? record.time.created : null,
|
|
66
|
+
completedAt: Number.isFinite(record?.time?.completed) ? record.time.completed : null,
|
|
58
67
|
model: providerID && modelID ? `${providerID}/${modelID}` : null,
|
|
59
68
|
text,
|
|
60
69
|
});
|
|
@@ -144,8 +153,12 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
144
153
|
sessionService,
|
|
145
154
|
scheduledTaskService,
|
|
146
155
|
browserControl = null,
|
|
156
|
+
fileOpen = null,
|
|
147
157
|
agentMemoryActions = null,
|
|
148
|
-
|
|
158
|
+
// Archive lives in OpenChamber's own store now — v2 has no route that sets
|
|
159
|
+
// Session.time.archived — so an unwired store simply means nothing is archived.
|
|
160
|
+
archiveStore = null,
|
|
161
|
+
createClient = OpenCode.make,
|
|
149
162
|
sleep = (duration) => new Promise((resolve) => setTimeout(resolve, duration)),
|
|
150
163
|
now = Date.now,
|
|
151
164
|
} = dependencies;
|
|
@@ -169,14 +182,25 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
169
182
|
});
|
|
170
183
|
};
|
|
171
184
|
|
|
172
|
-
|
|
185
|
+
// Every v2 route lives under /api and the client appends it, so it only
|
|
186
|
+
// wants the origin. Per-directory scoping is a request header.
|
|
187
|
+
const getClient = async (directory = '') => {
|
|
173
188
|
if (typeof waitForOpenCodeReady === 'function') await waitForOpenCodeReady(10_000, 250);
|
|
174
189
|
return createClient({
|
|
175
|
-
baseUrl: buildOpenCodeUrl('/', '').
|
|
176
|
-
headers:
|
|
190
|
+
baseUrl: new URL(buildOpenCodeUrl('/api/info', '')).origin,
|
|
191
|
+
headers: {
|
|
192
|
+
...getOpenCodeAuthHeaders(),
|
|
193
|
+
...(directory ? { 'x-opencode-directory': encodeURIComponent(directory) } : {}),
|
|
194
|
+
},
|
|
195
|
+
fetch,
|
|
177
196
|
});
|
|
178
197
|
};
|
|
179
198
|
|
|
199
|
+
const archivedAt = (sessionID) => {
|
|
200
|
+
if (!archiveStore || typeof archiveStore.isArchived !== 'function') return null;
|
|
201
|
+
return archiveStore.isArchived(sessionID) || null;
|
|
202
|
+
};
|
|
203
|
+
|
|
180
204
|
const projects = async () => {
|
|
181
205
|
const settings = await readSettingsFromDiskMigrated();
|
|
182
206
|
return sanitizeProjects(settings?.projects || []).map((project) => ({
|
|
@@ -197,22 +221,32 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
197
221
|
};
|
|
198
222
|
};
|
|
199
223
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
224
|
+
// /api/session/active is global in v2 and reports only `{ type: 'running' }`.
|
|
225
|
+
// The action contract stays busy/idle, so running is reported as busy.
|
|
226
|
+
const activeSessions = async (client) => {
|
|
227
|
+
const statuses = await client.session.active();
|
|
203
228
|
if (!statuses || typeof statuses !== 'object' || Array.isArray(statuses)) {
|
|
204
229
|
throw new OpenChamberControlError('Invalid session status response', 500);
|
|
205
230
|
}
|
|
206
|
-
return statuses
|
|
231
|
+
return statuses;
|
|
207
232
|
};
|
|
208
233
|
|
|
209
|
-
const
|
|
234
|
+
const asPublicStatus = (active) => (active ? { type: 'busy' } : { type: 'idle' });
|
|
235
|
+
|
|
236
|
+
const sessionStatus = async (client, sessionID) => {
|
|
237
|
+
const statuses = await activeSessions(client);
|
|
238
|
+
return asPublicStatus(statuses[sessionID]);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// `order: 'desc'` puts the newest messages in the limited page;
|
|
242
|
+
// extractTextMessages re-sorts them oldest-first for the caller.
|
|
243
|
+
const sessionMessages = async (client, sessionID, role, limit) => {
|
|
210
244
|
const fetchLimit = limit === undefined ? undefined : Math.max(100, limit * 4);
|
|
211
|
-
let response = await client.
|
|
245
|
+
let response = await client.message.list({ sessionID, ...(fetchLimit ? { limit: fetchLimit, order: 'desc' } : {}) });
|
|
212
246
|
let raw = Array.isArray(response?.data) ? response.data : [];
|
|
213
247
|
let messages = extractTextMessages(raw, role);
|
|
214
248
|
if (limit !== undefined && messages.length < limit && raw.length >= fetchLimit) {
|
|
215
|
-
response = await client.
|
|
249
|
+
response = await client.message.list({ sessionID });
|
|
216
250
|
raw = Array.isArray(response?.data) ? response.data : [];
|
|
217
251
|
messages = extractTextMessages(raw, role);
|
|
218
252
|
}
|
|
@@ -224,13 +258,13 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
224
258
|
let observedActivity = false;
|
|
225
259
|
while (true) {
|
|
226
260
|
if (signal?.aborted) throw new OpenChamberControlError('OpenChamber action was cancelled', 499);
|
|
227
|
-
const status = await sessionStatus(client, sessionID
|
|
228
|
-
if (status.type === 'busy'
|
|
261
|
+
const status = await sessionStatus(client, sessionID);
|
|
262
|
+
if (status.type === 'busy') {
|
|
229
263
|
observedActivity = true;
|
|
230
264
|
} else if (!requireActivity || observedActivity) {
|
|
231
265
|
return status;
|
|
232
266
|
} else {
|
|
233
|
-
const messages = await sessionMessages(client, sessionID,
|
|
267
|
+
const messages = await sessionMessages(client, sessionID, 'assistant', 1);
|
|
234
268
|
const message = messages[0];
|
|
235
269
|
if (message?.completedAt && (baselineMessageID ? message.id !== baselineMessageID : message.completedAt >= startedAt)) {
|
|
236
270
|
return status;
|
|
@@ -245,17 +279,17 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
245
279
|
};
|
|
246
280
|
|
|
247
281
|
// session.send/fork default the directory to the caller's context directory,
|
|
248
|
-
// which is wrong for sessions living in other worktrees:
|
|
282
|
+
// which is wrong for sessions living in other worktrees: the prompt then
|
|
249
283
|
// targets an instance that does not hold the session and the run dies with
|
|
250
284
|
// UnknownError. Resolve the target session's directory from the global
|
|
251
285
|
// session list when the caller did not scope explicitly.
|
|
252
286
|
const resolveSessionDirectory = async (sessionID) => {
|
|
253
287
|
try {
|
|
254
288
|
const client = await getClient();
|
|
255
|
-
const response = await client.
|
|
289
|
+
const response = await client.session.list({});
|
|
256
290
|
const sessions = Array.isArray(response?.data) ? response.data : [];
|
|
257
291
|
const session = sessions.find((item) => item?.id === sessionID);
|
|
258
|
-
return asNonEmptyString(session?.directory) || null;
|
|
292
|
+
return asNonEmptyString(session?.location?.directory) || null;
|
|
259
293
|
} catch {
|
|
260
294
|
return null;
|
|
261
295
|
}
|
|
@@ -305,7 +339,7 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
305
339
|
delete publicResult.baselineAssistantMessageId;
|
|
306
340
|
return publicResult;
|
|
307
341
|
}
|
|
308
|
-
const client = await getClient();
|
|
342
|
+
const client = await getClient(result.directory);
|
|
309
343
|
const status = await waitForIdle({
|
|
310
344
|
client,
|
|
311
345
|
sessionID: result.sessionId,
|
|
@@ -319,7 +353,7 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
319
353
|
const publicResult = { ...result, sessionStatus: status };
|
|
320
354
|
delete publicResult.baselineAssistantMessageId;
|
|
321
355
|
if (input.lastAssistant === true) {
|
|
322
|
-
publicResult.lastAssistantMessage = (await sessionMessages(client, result.sessionId,
|
|
356
|
+
publicResult.lastAssistantMessage = (await sessionMessages(client, result.sessionId, 'assistant', 1))[0] || null;
|
|
323
357
|
}
|
|
324
358
|
return publicResult;
|
|
325
359
|
};
|
|
@@ -329,7 +363,7 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
329
363
|
* should come back as a usage error the agent can correct, without waking a
|
|
330
364
|
* client or waiting for a round trip.
|
|
331
365
|
*/
|
|
332
|
-
const browserAction = async (action, input, signal, contextDirectory) => {
|
|
366
|
+
const browserAction = async (action, input, signal, contextDirectory, contextSessionId) => {
|
|
333
367
|
const parameters = {};
|
|
334
368
|
|
|
335
369
|
const readViewport = (required) => {
|
|
@@ -417,7 +451,13 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
417
451
|
// exceed the client's own wait; sharing one timeout with the quick actions
|
|
418
452
|
// made a slow page indistinguishable from an unreachable browser.
|
|
419
453
|
const timeoutMs = action === 'browser.open' ? 45_000 : 20_000;
|
|
420
|
-
|
|
454
|
+
// Where the call came from, for a provider that keeps one browser per
|
|
455
|
+
// project or chat. Filled by the tool plugin, never by the model.
|
|
456
|
+
const context = {
|
|
457
|
+
directory: asNonEmptyString(contextDirectory),
|
|
458
|
+
sessionId: asNonEmptyString(contextSessionId),
|
|
459
|
+
};
|
|
460
|
+
const result = await browserControl.request(action, parameters, { signal, timeoutMs, context });
|
|
421
461
|
|
|
422
462
|
// The image is written here rather than in the renderer: the file belongs
|
|
423
463
|
// beside the code it documents, and the client that took it may be on a
|
|
@@ -469,7 +509,17 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
469
509
|
if (!browserControl) {
|
|
470
510
|
throw new OpenChamberControlError('The in-app browser is not available on this server', 503);
|
|
471
511
|
}
|
|
472
|
-
return browserAction(action, input, options.signal, contextDirectory);
|
|
512
|
+
return browserAction(action, input, options.signal, contextDirectory, options.contextSessionId);
|
|
513
|
+
}
|
|
514
|
+
if (action === 'file.open') {
|
|
515
|
+
if (!fileOpen) {
|
|
516
|
+
throw new OpenChamberControlError('The file viewer is not available on this server', 503);
|
|
517
|
+
}
|
|
518
|
+
return fileOpen.request({
|
|
519
|
+
path: asNonEmptyString(input.path),
|
|
520
|
+
directory: asNonEmptyString(input.directory) || asNonEmptyString(contextDirectory),
|
|
521
|
+
sessionId: asNonEmptyString(options.contextSessionId),
|
|
522
|
+
});
|
|
473
523
|
}
|
|
474
524
|
if (action === 'projects.list') return { projects: await projects() };
|
|
475
525
|
if (action === 'models.list') return models();
|
|
@@ -514,24 +564,24 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
514
564
|
if (action.startsWith('session.')) {
|
|
515
565
|
const directory = asNonEmptyString(input.directory) || asNonEmptyString(contextDirectory);
|
|
516
566
|
const sessionID = asNonEmptyString(input.sessionId);
|
|
517
|
-
const client = await getClient();
|
|
567
|
+
const client = await getClient(directory);
|
|
518
568
|
if (action === 'session.list') {
|
|
519
569
|
const limit = positiveInteger(input.limit, 10, 'limit');
|
|
520
570
|
const response = await client.session.list(directory ? { directory } : {});
|
|
521
|
-
|
|
571
|
+
// Archive is OpenChamber state; overlay it so callers keep reading
|
|
572
|
+
// it off the session the way OpenCode used to report it.
|
|
573
|
+
let sessions = (Array.isArray(response?.data) ? response.data : []).map((session) => {
|
|
574
|
+
const archived = archivedAt(session?.id);
|
|
575
|
+
return archived ? { ...session, time: { ...session.time, archived } } : session;
|
|
576
|
+
});
|
|
522
577
|
if (input.all !== true) sessions = sessions.filter((session) => !session?.time?.archived);
|
|
523
578
|
sessions = sessions.slice(0, limit);
|
|
524
579
|
if (input.withStatus === true) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const statusRequest = client.session.status({ directory: sessionDirectory }).catch(() => null);
|
|
531
|
-
cache.set(sessionDirectory, statusRequest);
|
|
532
|
-
}
|
|
533
|
-
const statusResponse = await cache.get(sessionDirectory);
|
|
534
|
-
return { ...session, status: statusResponse?.data?.[session.id] || (statusResponse ? { type: 'idle' } : { type: 'unknown' }) };
|
|
580
|
+
// One global call now: v2 reports active sessions across directories.
|
|
581
|
+
const statuses = await activeSessions(client).catch(() => null);
|
|
582
|
+
sessions = sessions.map((session) => ({
|
|
583
|
+
...session,
|
|
584
|
+
status: statuses ? asPublicStatus(statuses[session.id]) : { type: 'unknown' },
|
|
535
585
|
}));
|
|
536
586
|
}
|
|
537
587
|
return { sessions, limit, directory, archived: input.all === true ? 'included' : 'excluded' };
|
|
@@ -539,7 +589,7 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
539
589
|
if (!sessionID) throw new OpenChamberControlError('sessionId is required', 400);
|
|
540
590
|
if (!directory) throw new OpenChamberControlError('directory is required', 400);
|
|
541
591
|
if (action === 'session.status') {
|
|
542
|
-
return { sessionId: sessionID, directory, sessionStatus: await sessionStatus(client, sessionID
|
|
592
|
+
return { sessionId: sessionID, directory, sessionStatus: await sessionStatus(client, sessionID) };
|
|
543
593
|
}
|
|
544
594
|
if (action === 'session.messages') {
|
|
545
595
|
if (input.timeout !== undefined && input.wait !== true) throw new OpenChamberControlError('timeout requires wait', 400);
|
|
@@ -550,9 +600,9 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
550
600
|
if (last && input.limit !== undefined) throw new OpenChamberControlError('last cannot be combined with limit', 400);
|
|
551
601
|
const currentStatus = input.wait === true
|
|
552
602
|
? await waitForIdle({ client, sessionID, directory, timeoutMs: normalizeWaitTimeoutMs(input.timeout), requireActivity: false, startedAt: now(), signal: options.signal })
|
|
553
|
-
: await sessionStatus(client, sessionID
|
|
603
|
+
: await sessionStatus(client, sessionID);
|
|
554
604
|
const limit = input.all === true ? undefined : (last ? 1 : positiveInteger(input.limit, 10, 'limit'));
|
|
555
|
-
return { sessionId: sessionID, directory, role, sessionStatus: currentStatus, messages: await sessionMessages(client, sessionID,
|
|
605
|
+
return { sessionId: sessionID, directory, role, sessionStatus: currentStatus, messages: await sessionMessages(client, sessionID, role, limit) };
|
|
556
606
|
}
|
|
557
607
|
}
|
|
558
608
|
throw new OpenChamberControlError(`Unsupported OpenChamber action: ${action || 'missing'}`, 400);
|
|
@@ -561,5 +611,8 @@ export const createOpenChamberControlService = (dependencies) => {
|
|
|
561
611
|
}
|
|
562
612
|
};
|
|
563
613
|
|
|
564
|
-
|
|
614
|
+
// The managed agent-tool plugin can no longer report the session's directory
|
|
615
|
+
// (v2 dropped `context.directory` from a tool call), so it sends the session
|
|
616
|
+
// id and the directory is resolved here, where it is authoritative.
|
|
617
|
+
return { execute, resolveSessionDirectory };
|
|
565
618
|
};
|
|
@@ -10,8 +10,10 @@ const createService = (overrides = {}) => {
|
|
|
10
10
|
const client = {
|
|
11
11
|
session: {
|
|
12
12
|
list: vi.fn(async () => ({ data: [] })),
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
active: vi.fn(async () => ({})),
|
|
14
|
+
},
|
|
15
|
+
message: {
|
|
16
|
+
list: vi.fn(async () => ({ data: [] })),
|
|
15
17
|
},
|
|
16
18
|
};
|
|
17
19
|
const sessionService = {
|
|
@@ -137,20 +139,12 @@ describe('OpenChamber control service', () => {
|
|
|
137
139
|
});
|
|
138
140
|
|
|
139
141
|
it('resolves the target session directory from the global session list when send omits it', async () => {
|
|
140
|
-
const { service, sessionService, client } = createService(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
data: [
|
|
147
|
-
{ id: 'ses_other', directory: '/repo/worktrees/other' },
|
|
148
|
-
{ id: 'ses_target', directory: '/repo/worktrees/target' },
|
|
149
|
-
],
|
|
150
|
-
})),
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
}),
|
|
142
|
+
const { service, sessionService, client } = createService();
|
|
143
|
+
client.session.list.mockResolvedValue({
|
|
144
|
+
data: [
|
|
145
|
+
{ id: 'ses_other', location: { directory: '/repo/worktrees/other' } },
|
|
146
|
+
{ id: 'ses_target', location: { directory: '/repo/worktrees/target' } },
|
|
147
|
+
],
|
|
154
148
|
});
|
|
155
149
|
sessionService.send.mockResolvedValue({ sessionId: 'ses_target', directory: '/repo/worktrees/target', promptDispatched: true });
|
|
156
150
|
|
|
@@ -160,12 +154,7 @@ describe('OpenChamber control service', () => {
|
|
|
160
154
|
});
|
|
161
155
|
|
|
162
156
|
it('falls back to the context directory when the session is not in the global list', async () => {
|
|
163
|
-
const { service, sessionService
|
|
164
|
-
createClient: () => ({
|
|
165
|
-
...client,
|
|
166
|
-
experimental: { session: { list: vi.fn(async () => ({ data: [] })) } },
|
|
167
|
-
}),
|
|
168
|
-
});
|
|
157
|
+
const { service, sessionService } = createService();
|
|
169
158
|
sessionService.send.mockResolvedValue({ sessionId: 'ses_unknown', directory: '/repo', promptDispatched: true });
|
|
170
159
|
|
|
171
160
|
await service.execute('session.send', { sessionId: 'ses_unknown', prompt: 'Continue' }, '/repo');
|
|
@@ -185,11 +174,11 @@ describe('OpenChamber control service', () => {
|
|
|
185
174
|
promptDispatched: true,
|
|
186
175
|
baselineAssistantMessageId: 'msg_old',
|
|
187
176
|
});
|
|
188
|
-
client.session.
|
|
189
|
-
client.
|
|
190
|
-
.mockResolvedValueOnce({ data: [{
|
|
191
|
-
.mockResolvedValueOnce({ data: [{
|
|
192
|
-
.mockResolvedValueOnce({ data: [{
|
|
177
|
+
client.session.active.mockResolvedValue({});
|
|
178
|
+
client.message.list
|
|
179
|
+
.mockResolvedValueOnce({ data: [{ id: 'msg_old', type: 'assistant', time: { completed: 900 }, content: [{ type: 'text', text: 'old' }] }] })
|
|
180
|
+
.mockResolvedValueOnce({ data: [{ id: 'msg_new', type: 'assistant', time: { completed: 1500 }, content: [{ type: 'text', text: 'done' }] }] })
|
|
181
|
+
.mockResolvedValueOnce({ data: [{ id: 'msg_new', type: 'assistant', time: { completed: 1500 }, content: [{ type: 'text', text: 'done' }] }] });
|
|
193
182
|
|
|
194
183
|
await expect(service.execute('session.create', {
|
|
195
184
|
directory: '/repo',
|
|
@@ -201,24 +190,24 @@ describe('OpenChamber control service', () => {
|
|
|
201
190
|
sessionStatus: { type: 'idle' },
|
|
202
191
|
lastAssistantMessage: expect.objectContaining({ id: 'msg_new', text: 'done' }),
|
|
203
192
|
}));
|
|
204
|
-
expect(client.session.
|
|
193
|
+
expect(client.session.active).toHaveBeenCalledTimes(2);
|
|
205
194
|
});
|
|
206
195
|
|
|
207
|
-
it('filters archived
|
|
208
|
-
const { service, client } = createService(
|
|
196
|
+
it('filters sessions archived in OpenChamber state and adds global statuses', async () => {
|
|
197
|
+
const { service, client } = createService({
|
|
198
|
+
archiveStore: { isArchived: (id) => (id === 'ses_archived' ? 100 : null) },
|
|
199
|
+
});
|
|
209
200
|
client.session.list.mockResolvedValue({ data: [
|
|
210
|
-
{ id: 'ses_active', directory: '/repo', time: {} },
|
|
211
|
-
{ id: 'ses_archived', directory: '/repo', time: {
|
|
212
|
-
{ id: 'ses_other', directory: '/other', time: {} },
|
|
201
|
+
{ id: 'ses_active', location: { directory: '/repo' }, time: {} },
|
|
202
|
+
{ id: 'ses_archived', location: { directory: '/repo' }, time: {} },
|
|
203
|
+
{ id: 'ses_other', location: { directory: '/other' }, time: {} },
|
|
213
204
|
] });
|
|
214
|
-
client.session.
|
|
215
|
-
.mockResolvedValueOnce({ data: { ses_active: { type: 'busy' } } })
|
|
216
|
-
.mockRejectedValueOnce(new Error('unavailable'));
|
|
205
|
+
client.session.active.mockResolvedValue({ ses_active: { type: 'running' } });
|
|
217
206
|
|
|
218
207
|
await expect(service.execute('session.list', { limit: 10, withStatus: true })).resolves.toEqual({
|
|
219
208
|
sessions: [
|
|
220
|
-
{ id: 'ses_active', directory: '/repo', time: {}, status: { type: 'busy' } },
|
|
221
|
-
{ id: 'ses_other', directory: '/other', time: {}, status: { type: '
|
|
209
|
+
{ id: 'ses_active', location: { directory: '/repo' }, time: {}, status: { type: 'busy' } },
|
|
210
|
+
{ id: 'ses_other', location: { directory: '/other' }, time: {}, status: { type: 'idle' } },
|
|
222
211
|
],
|
|
223
212
|
limit: 10,
|
|
224
213
|
directory: null,
|
|
@@ -226,21 +215,34 @@ describe('OpenChamber control service', () => {
|
|
|
226
215
|
});
|
|
227
216
|
});
|
|
228
217
|
|
|
218
|
+
it('reports unknown status when the active-session read fails', async () => {
|
|
219
|
+
const { service, client } = createService();
|
|
220
|
+
client.session.list.mockResolvedValue({ data: [{ id: 'ses_active', location: { directory: '/repo' }, time: {} }] });
|
|
221
|
+
client.session.active.mockRejectedValue(new Error('unavailable'));
|
|
222
|
+
|
|
223
|
+
await expect(service.execute('session.list', { limit: 10, withStatus: true })).resolves.toEqual({
|
|
224
|
+
sessions: [{ id: 'ses_active', location: { directory: '/repo' }, time: {}, status: { type: 'unknown' } }],
|
|
225
|
+
limit: 10,
|
|
226
|
+
directory: null,
|
|
227
|
+
archived: 'excluded',
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
229
231
|
it('names limit in positive-integer validation errors', async () => {
|
|
230
232
|
const { service, client } = createService();
|
|
231
233
|
await expect(service.execute('session.list', { limit: 0 })).rejects.toThrow('limit must be a positive integer');
|
|
232
234
|
expect(client.session.list).not.toHaveBeenCalled();
|
|
233
235
|
});
|
|
234
236
|
|
|
235
|
-
it('projects only ordered text
|
|
237
|
+
it('projects only ordered text content from session messages', async () => {
|
|
236
238
|
const { service, client } = createService();
|
|
237
|
-
client.
|
|
239
|
+
client.message.list.mockResolvedValue({ data: [
|
|
238
240
|
{
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
id: 'msg_assistant', type: 'assistant', model: { providerID: 'openai', id: 'gpt-5.4-mini' }, time: { created: 20, completed: 30 },
|
|
242
|
+
content: [{ type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'First ' }, { type: 'tool' }, { type: 'text', text: 'answer' }],
|
|
241
243
|
},
|
|
242
|
-
{
|
|
243
|
-
{
|
|
244
|
+
{ id: 'msg_user', type: 'user', time: { created: 10 }, text: 'Question' },
|
|
245
|
+
{ id: 'msg_tool', type: 'assistant', time: { created: 15 }, content: [{ type: 'tool' }] },
|
|
244
246
|
] });
|
|
245
247
|
|
|
246
248
|
await expect(service.execute('session.messages', {
|
|
@@ -266,6 +268,32 @@ describe('OpenChamber control service', () => {
|
|
|
266
268
|
});
|
|
267
269
|
});
|
|
268
270
|
|
|
271
|
+
describe('file.open', () => {
|
|
272
|
+
it('hands the path, the session directory and the session to the file viewer', async () => {
|
|
273
|
+
const request = vi.fn(async () => ({ path: '/repo/out.csv', size: 3, opened: true }));
|
|
274
|
+
const { service } = createService({ fileOpen: { request } });
|
|
275
|
+
|
|
276
|
+
const result = await service.execute('file.open', { path: 'out.csv' }, '/repo', { contextSessionId: 'ses_1' });
|
|
277
|
+
|
|
278
|
+
expect(request).toHaveBeenCalledWith({ path: 'out.csv', directory: '/repo', sessionId: 'ses_1' });
|
|
279
|
+
expect(result).toEqual({ path: '/repo/out.csv', size: 3, opened: true });
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('lets an explicit directory win over the session directory', async () => {
|
|
283
|
+
const request = vi.fn(async () => ({ path: '/other/out.csv', size: 3, opened: true }));
|
|
284
|
+
const { service } = createService({ fileOpen: { request } });
|
|
285
|
+
|
|
286
|
+
await service.execute('file.open', { path: 'out.csv', directory: '/other' }, '/repo');
|
|
287
|
+
|
|
288
|
+
expect(request).toHaveBeenCalledWith({ path: 'out.csv', directory: '/other', sessionId: null });
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('answers 503 when this server has no file viewer wired', async () => {
|
|
292
|
+
const { service } = createService({});
|
|
293
|
+
await expect(service.execute('file.open', { path: 'out.csv' }, '/repo')).rejects.toMatchObject({ statusCode: 503 });
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
269
297
|
describe('browser capture', () => {
|
|
270
298
|
const pixel = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
|
|
271
299
|
|
|
@@ -315,4 +343,16 @@ describe('browser capture', () => {
|
|
|
315
343
|
await service.execute('browser.capture', { label: 'before' }, directory);
|
|
316
344
|
expect(request).toHaveBeenCalledWith('browser.capture', { label: 'before' }, expect.anything());
|
|
317
345
|
});
|
|
346
|
+
|
|
347
|
+
it('tells the browser which project and chat the action came from', async () => {
|
|
348
|
+
const { service, directory, request } = await createBrowserService({ base64: pixel, mime: 'image/png' });
|
|
349
|
+
await service.execute('browser.capture', {}, directory, { contextSessionId: 'ses_1' });
|
|
350
|
+
expect(request).toHaveBeenCalledWith('browser.capture', {}, expect.objectContaining({
|
|
351
|
+
context: { directory, sessionId: 'ses_1' },
|
|
352
|
+
}));
|
|
353
|
+
await service.execute('browser.capture', {}, directory);
|
|
354
|
+
expect(request).toHaveBeenLastCalledWith('browser.capture', {}, expect.objectContaining({
|
|
355
|
+
context: { directory, sessionId: null },
|
|
356
|
+
}));
|
|
357
|
+
});
|
|
318
358
|
});
|