@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,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode v2 wire events -> the vocabulary the server's own consumers speak.
|
|
3
|
+
*
|
|
4
|
+
* The browser gets raw wire payloads (the UI translates them itself in
|
|
5
|
+
* `packages/ui/src/lib/opencode/events.ts`). Everything that reacts on the
|
|
6
|
+
* server — notifications, the message queue, session activity, goal mode,
|
|
7
|
+
* permission auto-accept, Linear status — was written against OpenCode v1
|
|
8
|
+
* event names, so this module is the single place that maps v2 onto them.
|
|
9
|
+
* Keeping one translator means a consumer never has to know which OpenCode
|
|
10
|
+
* shape produced its event.
|
|
11
|
+
*
|
|
12
|
+
* Two differences from v1 are load-bearing and cannot be papered over:
|
|
13
|
+
*
|
|
14
|
+
* - v2 emits no `session.status` and no `session.idle` of its own. Live status
|
|
15
|
+
* comes from `session.execution.started|succeeded|interrupted|failed`, so
|
|
16
|
+
* those are what synthesize the status vocabulary here.
|
|
17
|
+
* - a v2 turn is a sequence of steps. Each `session.step.ended` becomes an
|
|
18
|
+
* assistant `message.updated`; only the last one carries `finish: "stop"`,
|
|
19
|
+
* which is the same signal v1 gave once per turn.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
23
|
+
|
|
24
|
+
const trimmed = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
25
|
+
|
|
26
|
+
const compact = (value) => {
|
|
27
|
+
const result = {};
|
|
28
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
29
|
+
if (entry !== undefined) result[key] = entry;
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Directory an event belongs to. Execution events carry no location. */
|
|
35
|
+
export const wireEventDirectory = (payload) => {
|
|
36
|
+
if (!isRecord(payload)) return '';
|
|
37
|
+
return trimmed(payload.location?.directory);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Structured errors travel as `{ type, message }`; consumers still read `name`. */
|
|
41
|
+
const toServerError = (error) => {
|
|
42
|
+
if (!isRecord(error)) return { name: 'Error', type: 'Error', message: typeof error === 'string' ? error : '' };
|
|
43
|
+
const type = trimmed(error.type) || 'Error';
|
|
44
|
+
return compact({ name: type, type, message: trimmed(error.message), status: error.status });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const ABORTED_ERROR = {
|
|
48
|
+
name: 'MessageAbortedError',
|
|
49
|
+
type: 'MessageAbortedError',
|
|
50
|
+
message: 'The running turn was interrupted.',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Translates one v2 wire payload into zero or more server-vocabulary events.
|
|
55
|
+
*
|
|
56
|
+
* Every returned event carries the source event id and the directory, because
|
|
57
|
+
* consumers deduplicate on the id and route their own OpenCode calls by
|
|
58
|
+
* directory.
|
|
59
|
+
*/
|
|
60
|
+
export function translateWireEvent(payload) {
|
|
61
|
+
if (!isRecord(payload)) return [];
|
|
62
|
+
const type = trimmed(payload.type);
|
|
63
|
+
if (!type) return [];
|
|
64
|
+
const data = isRecord(payload.data) ? payload.data : {};
|
|
65
|
+
const directory = wireEventDirectory(payload);
|
|
66
|
+
const created = Number.isFinite(payload.created) ? payload.created : Date.now();
|
|
67
|
+
|
|
68
|
+
const event = (outType, properties) => ({
|
|
69
|
+
type: outType,
|
|
70
|
+
id: typeof payload.id === 'string' ? payload.id : '',
|
|
71
|
+
created,
|
|
72
|
+
properties: compact({ ...properties, directory: directory || undefined }),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const sessionID = trimmed(data.sessionID);
|
|
76
|
+
|
|
77
|
+
const status = (statusValue) => event('session.status', { sessionID, status: statusValue });
|
|
78
|
+
|
|
79
|
+
switch (type) {
|
|
80
|
+
case 'server.connected':
|
|
81
|
+
return [event('server.connected', {})];
|
|
82
|
+
|
|
83
|
+
case 'installation.update-available':
|
|
84
|
+
return [event('installation.update-available', { version: trimmed(data.version) })];
|
|
85
|
+
|
|
86
|
+
// --- session lifecycle --------------------------------------------------
|
|
87
|
+
|
|
88
|
+
case 'session.created': {
|
|
89
|
+
if (!sessionID) return [];
|
|
90
|
+
const location = isRecord(data.location) ? data.location : {};
|
|
91
|
+
return [event('session.created', {
|
|
92
|
+
sessionID,
|
|
93
|
+
info: compact({
|
|
94
|
+
id: sessionID,
|
|
95
|
+
parentID: trimmed(data.parentID) || undefined,
|
|
96
|
+
projectID: trimmed(data.projectID) || undefined,
|
|
97
|
+
directory: trimmed(location.directory) || directory || undefined,
|
|
98
|
+
title: typeof data.title === 'string' ? data.title : undefined,
|
|
99
|
+
agent: trimmed(data.agent) || undefined,
|
|
100
|
+
model: data.model,
|
|
101
|
+
metadata: data.metadata,
|
|
102
|
+
permissions: data.permissions,
|
|
103
|
+
time: { created, updated: created },
|
|
104
|
+
}),
|
|
105
|
+
})];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'session.renamed':
|
|
109
|
+
if (!sessionID) return [];
|
|
110
|
+
return [event('session.updated', {
|
|
111
|
+
sessionID,
|
|
112
|
+
info: compact({ id: sessionID, title: typeof data.title === 'string' ? data.title : undefined, directory: directory || undefined, time: { updated: created } }),
|
|
113
|
+
})];
|
|
114
|
+
|
|
115
|
+
case 'session.moved': {
|
|
116
|
+
if (!sessionID) return [];
|
|
117
|
+
const location = isRecord(data.location) ? data.location : {};
|
|
118
|
+
return [event('session.updated', {
|
|
119
|
+
sessionID,
|
|
120
|
+
info: compact({ id: sessionID, directory: trimmed(location.directory) || undefined, projectID: trimmed(data.projectID) || undefined, time: { updated: created } }),
|
|
121
|
+
})];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case 'session.usage.updated':
|
|
125
|
+
if (!sessionID) return [];
|
|
126
|
+
return [event('session.updated', {
|
|
127
|
+
sessionID,
|
|
128
|
+
info: compact({ id: sessionID, directory: directory || undefined, cost: data.cost, tokens: data.tokens, time: { updated: created } }),
|
|
129
|
+
})];
|
|
130
|
+
|
|
131
|
+
case 'session.deleted':
|
|
132
|
+
if (!sessionID) return [];
|
|
133
|
+
return [event('session.deleted', { sessionID, info: { id: sessionID } })];
|
|
134
|
+
|
|
135
|
+
// --- live status --------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
// v2 still declares `session.status` and `session.idle`; a 2.0.2 server was
|
|
138
|
+
// observed never to emit them, so the execution events below are what
|
|
139
|
+
// actually drive status. Both paths are translated so a server that does
|
|
140
|
+
// emit them stays correct.
|
|
141
|
+
case 'session.status':
|
|
142
|
+
if (!sessionID || !isRecord(data.status)) return [];
|
|
143
|
+
return [status(data.status)];
|
|
144
|
+
|
|
145
|
+
case 'session.idle':
|
|
146
|
+
if (!sessionID) return [];
|
|
147
|
+
return [status({ type: 'idle' }), event('session.idle', { sessionID })];
|
|
148
|
+
|
|
149
|
+
case 'session.execution.started':
|
|
150
|
+
if (!sessionID) return [];
|
|
151
|
+
return [status({ type: 'busy' })];
|
|
152
|
+
|
|
153
|
+
case 'session.execution.succeeded':
|
|
154
|
+
if (!sessionID) return [];
|
|
155
|
+
return [status({ type: 'idle' }), event('session.idle', { sessionID })];
|
|
156
|
+
|
|
157
|
+
case 'session.execution.interrupted': {
|
|
158
|
+
if (!sessionID) return [];
|
|
159
|
+
const reason = trimmed(data.reason) || 'user';
|
|
160
|
+
// A shutdown is not the end of the turn: OpenCode keeps the execution
|
|
161
|
+
// claim and the resumed drain continues the same turn after restart
|
|
162
|
+
// (v2.0.14 `session/execution.ts` settled(), `projector.ts` projectIdle,
|
|
163
|
+
// `message-updater.ts`), and none of its own projections mark the
|
|
164
|
+
// session idle. Reporting it as an abort would make the goal runtime
|
|
165
|
+
// pause the goal as a user stop and the queue hold the next prompt, so
|
|
166
|
+
// it produces nothing: the session stays busy until OpenCode resumes it
|
|
167
|
+
// and reports the real terminal outcome.
|
|
168
|
+
if (reason === 'shutdown') return [];
|
|
169
|
+
// Every other interruption ends the turn without failing it: consumers
|
|
170
|
+
// that treat `session.error` as a failure must not fire, but the queue
|
|
171
|
+
// and goal runtimes still have to know the turn was aborted.
|
|
172
|
+
return [
|
|
173
|
+
status({ type: 'idle' }),
|
|
174
|
+
event('session.idle', { sessionID, aborted: true, reason, error: ABORTED_ERROR }),
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case 'session.execution.failed':
|
|
179
|
+
if (!sessionID) return [];
|
|
180
|
+
return [status({ type: 'idle' }), event('session.error', { sessionID, error: toServerError(data.error) })];
|
|
181
|
+
|
|
182
|
+
// --- messages -----------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
case 'session.inbox.enqueued': {
|
|
185
|
+
const item = isRecord(data.item) ? data.item : {};
|
|
186
|
+
const messageID = trimmed(data.inboxID);
|
|
187
|
+
if (!sessionID || !messageID) return [];
|
|
188
|
+
const itemPayload = isRecord(item.payload) ? item.payload : {};
|
|
189
|
+
if (item.type === 'user') {
|
|
190
|
+
const text = typeof itemPayload.text === 'string' ? itemPayload.text : '';
|
|
191
|
+
return [event('message.updated', {
|
|
192
|
+
sessionID,
|
|
193
|
+
info: compact({
|
|
194
|
+
id: messageID,
|
|
195
|
+
sessionID,
|
|
196
|
+
role: 'user',
|
|
197
|
+
text,
|
|
198
|
+
parts: text ? [{ type: 'text', text }] : [],
|
|
199
|
+
metadata: itemPayload.metadata,
|
|
200
|
+
time: { created },
|
|
201
|
+
}),
|
|
202
|
+
parts: text ? [{ type: 'text', text }] : [],
|
|
203
|
+
})];
|
|
204
|
+
}
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case 'session.step.started': {
|
|
209
|
+
const messageID = trimmed(data.assistantMessageID);
|
|
210
|
+
if (!sessionID || !messageID) return [];
|
|
211
|
+
const model = isRecord(data.model) ? data.model : {};
|
|
212
|
+
return [event('message.updated', {
|
|
213
|
+
sessionID,
|
|
214
|
+
info: compact({
|
|
215
|
+
id: messageID,
|
|
216
|
+
sessionID,
|
|
217
|
+
role: 'assistant',
|
|
218
|
+
agent: trimmed(data.agent) || undefined,
|
|
219
|
+
providerID: trimmed(model.providerID) || undefined,
|
|
220
|
+
modelID: trimmed(model.id) || undefined,
|
|
221
|
+
time: { created },
|
|
222
|
+
}),
|
|
223
|
+
})];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case 'session.step.ended': {
|
|
227
|
+
const messageID = trimmed(data.assistantMessageID);
|
|
228
|
+
if (!sessionID || !messageID) return [];
|
|
229
|
+
return [event('message.updated', {
|
|
230
|
+
sessionID,
|
|
231
|
+
info: compact({
|
|
232
|
+
id: messageID,
|
|
233
|
+
sessionID,
|
|
234
|
+
role: 'assistant',
|
|
235
|
+
finish: trimmed(data.finish) || undefined,
|
|
236
|
+
cost: data.cost,
|
|
237
|
+
tokens: data.tokens,
|
|
238
|
+
time: { completed: created },
|
|
239
|
+
}),
|
|
240
|
+
})];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
case 'session.step.failed': {
|
|
244
|
+
const messageID = trimmed(data.assistantMessageID);
|
|
245
|
+
if (!sessionID || !messageID) return [];
|
|
246
|
+
return [event('message.updated', {
|
|
247
|
+
sessionID,
|
|
248
|
+
info: compact({
|
|
249
|
+
id: messageID,
|
|
250
|
+
sessionID,
|
|
251
|
+
role: 'assistant',
|
|
252
|
+
finish: trimmed(data.finish) || 'error',
|
|
253
|
+
error: toServerError(data.error),
|
|
254
|
+
cost: data.cost,
|
|
255
|
+
tokens: data.tokens,
|
|
256
|
+
time: { completed: created },
|
|
257
|
+
}),
|
|
258
|
+
})];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// --- requests to the user -------------------------------------------------
|
|
262
|
+
|
|
263
|
+
case 'permission.asked': {
|
|
264
|
+
const requestID = trimmed(data.id);
|
|
265
|
+
if (!requestID || !sessionID) return [];
|
|
266
|
+
return [event('permission.asked', compact({
|
|
267
|
+
id: requestID,
|
|
268
|
+
sessionID,
|
|
269
|
+
action: trimmed(data.action),
|
|
270
|
+
resources: Array.isArray(data.resources) ? data.resources : [],
|
|
271
|
+
save: Array.isArray(data.save) ? data.save : undefined,
|
|
272
|
+
metadata: data.metadata,
|
|
273
|
+
source: data.source,
|
|
274
|
+
message: typeof data.message === 'string' ? data.message : undefined,
|
|
275
|
+
}))];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case 'permission.replied':
|
|
279
|
+
if (!sessionID) return [];
|
|
280
|
+
return [event('permission.replied', { sessionID, requestID: trimmed(data.requestID) })];
|
|
281
|
+
|
|
282
|
+
case 'form.created': {
|
|
283
|
+
const form = isRecord(data.form) ? data.form : null;
|
|
284
|
+
if (!form || !trimmed(form.id)) return [];
|
|
285
|
+
return [event('form.created', { sessionID: trimmed(form.sessionID), form })];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
case 'form.replied':
|
|
289
|
+
case 'form.cancelled':
|
|
290
|
+
if (!sessionID) return [];
|
|
291
|
+
return [event('form.settled', { sessionID, formID: trimmed(data.id) })];
|
|
292
|
+
|
|
293
|
+
// --- location-level notices ------------------------------------------------
|
|
294
|
+
|
|
295
|
+
case 'vcs.branch.updated':
|
|
296
|
+
return [event('vcs.branch.updated', { branch: typeof data.branch === 'string' ? data.branch : undefined })];
|
|
297
|
+
|
|
298
|
+
case 'mcp.status.changed':
|
|
299
|
+
return [event('mcp.status.changed', { server: trimmed(data.server) })];
|
|
300
|
+
|
|
301
|
+
default:
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Convenience for consumers that hand one payload to one handler: translates
|
|
308
|
+
* and forwards every resulting event in order.
|
|
309
|
+
*/
|
|
310
|
+
export function forwardTranslatedWireEvent(payload, handle) {
|
|
311
|
+
if (typeof handle !== 'function') return;
|
|
312
|
+
for (const translated of translateWireEvent(payload)) handle(translated);
|
|
313
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { forwardTranslatedWireEvent, translateWireEvent, wireEventDirectory } from './translate-v2.js';
|
|
3
|
+
|
|
4
|
+
const wire = (type, data, extra = {}) => ({
|
|
5
|
+
id: 'evt_1',
|
|
6
|
+
created: 1000,
|
|
7
|
+
type,
|
|
8
|
+
data,
|
|
9
|
+
location: { directory: '/repo' },
|
|
10
|
+
...extra,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('translateWireEvent', () => {
|
|
14
|
+
test('ignores non-objects and unknown types', () => {
|
|
15
|
+
expect(translateWireEvent(null)).toEqual([]);
|
|
16
|
+
expect(translateWireEvent('nope')).toEqual([]);
|
|
17
|
+
expect(translateWireEvent(wire('session.tool.progress', { sessionID: 's1' }))).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('tags every event with its source id and directory', () => {
|
|
21
|
+
const [event] = translateWireEvent(wire('session.idle', { sessionID: 's1' }));
|
|
22
|
+
expect(event.id).toBe('evt_1');
|
|
23
|
+
expect(event.created).toBe(1000);
|
|
24
|
+
expect(event.properties.directory).toBe('/repo');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('server.connected and update availability pass through', () => {
|
|
28
|
+
expect(translateWireEvent(wire('server.connected', {}))[0].type).toBe('server.connected');
|
|
29
|
+
const [update] = translateWireEvent(wire('installation.update-available', { version: '2.1.0' }));
|
|
30
|
+
expect(update).toMatchObject({ type: 'installation.update-available', properties: { version: '2.1.0' } });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('session.created carries the directory from its location', () => {
|
|
34
|
+
const [event] = translateWireEvent(wire('session.created', {
|
|
35
|
+
sessionID: 's1',
|
|
36
|
+
projectID: 'p1',
|
|
37
|
+
parentID: 's0',
|
|
38
|
+
title: 'Fix login',
|
|
39
|
+
agent: 'build',
|
|
40
|
+
location: { directory: '/other' },
|
|
41
|
+
}));
|
|
42
|
+
expect(event.type).toBe('session.created');
|
|
43
|
+
expect(event.properties.info).toMatchObject({
|
|
44
|
+
id: 's1',
|
|
45
|
+
parentID: 's0',
|
|
46
|
+
projectID: 'p1',
|
|
47
|
+
directory: '/other',
|
|
48
|
+
title: 'Fix login',
|
|
49
|
+
agent: 'build',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('rename, move and usage all read as session.updated', () => {
|
|
54
|
+
expect(translateWireEvent(wire('session.renamed', { sessionID: 's1', title: 'New' }))[0])
|
|
55
|
+
.toMatchObject({ type: 'session.updated', properties: { info: { id: 's1', title: 'New' } } });
|
|
56
|
+
expect(translateWireEvent(wire('session.moved', { sessionID: 's1', projectID: 'p2', location: { directory: '/moved' } }))[0])
|
|
57
|
+
.toMatchObject({ type: 'session.updated', properties: { info: { id: 's1', directory: '/moved', projectID: 'p2' } } });
|
|
58
|
+
expect(translateWireEvent(wire('session.usage.updated', { sessionID: 's1', cost: 0.5, tokens: { input: 10 } }))[0])
|
|
59
|
+
.toMatchObject({ type: 'session.updated', properties: { info: { id: 's1', cost: 0.5 } } });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('session.deleted keeps both the id and the legacy info shape', () => {
|
|
63
|
+
const [event] = translateWireEvent(wire('session.deleted', { sessionID: 's1' }));
|
|
64
|
+
expect(event).toMatchObject({ type: 'session.deleted', properties: { sessionID: 's1', info: { id: 's1' } } });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('execution events synthesize the status vocabulary v2 no longer emits', () => {
|
|
68
|
+
expect(translateWireEvent(wire('session.execution.started', { sessionID: 's1' })))
|
|
69
|
+
.toMatchObject([{ type: 'session.status', properties: { sessionID: 's1', status: { type: 'busy' } } }]);
|
|
70
|
+
|
|
71
|
+
const succeeded = translateWireEvent(wire('session.execution.succeeded', { sessionID: 's1' }));
|
|
72
|
+
expect(succeeded.map((entry) => entry.type)).toEqual(['session.status', 'session.idle']);
|
|
73
|
+
expect(succeeded[0].properties.status).toEqual({ type: 'idle' });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('an interruption ends the turn without reporting a failure', () => {
|
|
77
|
+
const events = translateWireEvent(wire('session.execution.interrupted', { sessionID: 's1', reason: 'user' }));
|
|
78
|
+
expect(events.map((entry) => entry.type)).toEqual(['session.status', 'session.idle']);
|
|
79
|
+
expect(events[1].properties).toMatchObject({ aborted: true, reason: 'user' });
|
|
80
|
+
expect(events[1].properties.error.name).toBe('MessageAbortedError');
|
|
81
|
+
expect(events.some((entry) => entry.type === 'session.error')).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('a shutdown interruption is not an abort: the turn resumes after restart', () => {
|
|
85
|
+
// OpenCode keeps the execution claim across a shutdown and continues the
|
|
86
|
+
// turn on restart, so nothing here may read as the user pressing Stop.
|
|
87
|
+
expect(translateWireEvent(wire('session.execution.interrupted', { sessionID: 's1', reason: 'shutdown' }))).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('a failed execution reports a structured error', () => {
|
|
91
|
+
const events = translateWireEvent(wire('session.execution.failed', {
|
|
92
|
+
sessionID: 's1',
|
|
93
|
+
error: { type: 'ProviderAuthError', message: 'bad key', status: 401 },
|
|
94
|
+
}));
|
|
95
|
+
expect(events.map((entry) => entry.type)).toEqual(['session.status', 'session.error']);
|
|
96
|
+
expect(events[1].properties.error).toEqual({
|
|
97
|
+
name: 'ProviderAuthError',
|
|
98
|
+
type: 'ProviderAuthError',
|
|
99
|
+
message: 'bad key',
|
|
100
|
+
status: 401,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('a passed-through session.status is kept as is', () => {
|
|
105
|
+
const [event] = translateWireEvent(wire('session.status', { sessionID: 's1', status: { type: 'retry', attempt: 2 } }));
|
|
106
|
+
expect(event.properties.status).toEqual({ type: 'retry', attempt: 2 });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('an enqueued user prompt becomes a user message with text parts', () => {
|
|
110
|
+
const [event] = translateWireEvent(wire('session.inbox.enqueued', {
|
|
111
|
+
sessionID: 's1',
|
|
112
|
+
inboxID: 'msg_1',
|
|
113
|
+
item: { type: 'user', payload: { text: 'hello' } },
|
|
114
|
+
}));
|
|
115
|
+
expect(event.type).toBe('message.updated');
|
|
116
|
+
expect(event.properties.info).toMatchObject({ id: 'msg_1', sessionID: 's1', role: 'user', text: 'hello' });
|
|
117
|
+
expect(event.properties.info.parts).toEqual([{ type: 'text', text: 'hello' }]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('non-user inbox items are not messages', () => {
|
|
121
|
+
expect(translateWireEvent(wire('session.inbox.enqueued', {
|
|
122
|
+
sessionID: 's1',
|
|
123
|
+
inboxID: 'msg_1',
|
|
124
|
+
item: { type: 'compaction', payload: {} },
|
|
125
|
+
}))).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('steps become assistant message updates', () => {
|
|
129
|
+
const [started] = translateWireEvent(wire('session.step.started', {
|
|
130
|
+
sessionID: 's1',
|
|
131
|
+
assistantMessageID: 'msg_2',
|
|
132
|
+
agent: 'build',
|
|
133
|
+
model: { id: 'gpt-5.6-luna', providerID: 'openai' },
|
|
134
|
+
}));
|
|
135
|
+
expect(started.properties.info).toMatchObject({
|
|
136
|
+
id: 'msg_2', role: 'assistant', agent: 'build', providerID: 'openai', modelID: 'gpt-5.6-luna',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const [ended] = translateWireEvent(wire('session.step.ended', {
|
|
140
|
+
sessionID: 's1', assistantMessageID: 'msg_2', finish: 'stop', cost: 1, tokens: { input: 2 },
|
|
141
|
+
}));
|
|
142
|
+
expect(ended.properties.info).toMatchObject({ id: 'msg_2', role: 'assistant', finish: 'stop' });
|
|
143
|
+
expect(ended.properties.info.time.completed).toBe(1000);
|
|
144
|
+
|
|
145
|
+
const [failed] = translateWireEvent(wire('session.step.failed', {
|
|
146
|
+
sessionID: 's1', assistantMessageID: 'msg_2', error: { type: 'Overloaded', message: 'busy' },
|
|
147
|
+
}));
|
|
148
|
+
expect(failed.properties.info).toMatchObject({ finish: 'error' });
|
|
149
|
+
expect(failed.properties.info.error.name).toBe('Overloaded');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('permission requests keep the v2 action and resources', () => {
|
|
153
|
+
const [event] = translateWireEvent(wire('permission.asked', {
|
|
154
|
+
id: 'per_1',
|
|
155
|
+
sessionID: 's1',
|
|
156
|
+
action: 'bash',
|
|
157
|
+
resources: ['rm -rf /'],
|
|
158
|
+
source: { type: 'tool', messageID: 'msg_1', id: 'call_1' },
|
|
159
|
+
}));
|
|
160
|
+
expect(event.properties).toMatchObject({
|
|
161
|
+
id: 'per_1', sessionID: 's1', action: 'bash', resources: ['rm -rf /'],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const [replied] = translateWireEvent(wire('permission.replied', { sessionID: 's1', requestID: 'per_1', reply: 'once' }));
|
|
165
|
+
expect(replied.properties).toMatchObject({ sessionID: 's1', requestID: 'per_1' });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('forms replace questions', () => {
|
|
169
|
+
const form = { id: 'frm_1', sessionID: 's1', title: 'Switch to plan mode?', fields: [] };
|
|
170
|
+
const [created] = translateWireEvent(wire('form.created', { form }));
|
|
171
|
+
expect(created).toMatchObject({ type: 'form.created', properties: { sessionID: 's1', form } });
|
|
172
|
+
|
|
173
|
+
expect(translateWireEvent(wire('form.replied', { id: 'frm_1', sessionID: 's1' }))[0])
|
|
174
|
+
.toMatchObject({ type: 'form.settled', properties: { sessionID: 's1', formID: 'frm_1' } });
|
|
175
|
+
expect(translateWireEvent(wire('form.cancelled', { id: 'frm_1', sessionID: 's1' }))[0])
|
|
176
|
+
.toMatchObject({ type: 'form.settled', properties: { sessionID: 's1', formID: 'frm_1' } });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('branch and mcp notices pass through', () => {
|
|
180
|
+
expect(translateWireEvent(wire('vcs.branch.updated', { branch: 'main' }))[0].properties.branch).toBe('main');
|
|
181
|
+
expect(translateWireEvent(wire('mcp.status.changed', { server: 'linear' }))[0].properties.server).toBe('linear');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('events without a session id are dropped rather than half-translated', () => {
|
|
185
|
+
expect(translateWireEvent(wire('session.execution.started', {}))).toEqual([]);
|
|
186
|
+
expect(translateWireEvent(wire('session.step.ended', { sessionID: 's1' }))).toEqual([]);
|
|
187
|
+
expect(translateWireEvent(wire('permission.asked', { sessionID: 's1' }))).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('wireEventDirectory', () => {
|
|
192
|
+
test('reads location.directory and tolerates its absence', () => {
|
|
193
|
+
expect(wireEventDirectory(wire('session.idle', { sessionID: 's1' }))).toBe('/repo');
|
|
194
|
+
expect(wireEventDirectory({ type: 'session.execution.started', data: {} })).toBe('');
|
|
195
|
+
expect(wireEventDirectory(null)).toBe('');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('forwardTranslatedWireEvent', () => {
|
|
200
|
+
test('forwards every translated event in order', () => {
|
|
201
|
+
const seen = [];
|
|
202
|
+
forwardTranslatedWireEvent(wire('session.execution.succeeded', { sessionID: 's1' }), (event) => seen.push(event.type));
|
|
203
|
+
expect(seen).toEqual(['session.status', 'session.idle']);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('does nothing without a handler', () => {
|
|
207
|
+
expect(() => forwardTranslatedWireEvent(wire('session.idle', { sessionID: 's1' }), null)).not.toThrow();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -65,12 +65,12 @@ describe('createUpstreamSseReader', () => {
|
|
|
65
65
|
let reader;
|
|
66
66
|
|
|
67
67
|
reader = createUpstreamSseReader({
|
|
68
|
-
buildUrl: () => 'http://127.0.0.1:4096/
|
|
68
|
+
buildUrl: () => 'http://127.0.0.1:4096/api/event',
|
|
69
69
|
reconnectDelayMs: 0,
|
|
70
70
|
fetchImpl: async (_url, options) => createSseResponse({
|
|
71
71
|
signal: options.signal,
|
|
72
72
|
blocks: [
|
|
73
|
-
'id:
|
|
73
|
+
'data: {"id":"evt-1","type":"session.renamed","location":{"directory":"/tmp/project"},"data":{"sessionID":"s1","title":"t"}}\r\n\r\n',
|
|
74
74
|
],
|
|
75
75
|
}),
|
|
76
76
|
onEvent(event) {
|
|
@@ -85,10 +85,10 @@ describe('createUpstreamSseReader', () => {
|
|
|
85
85
|
expect(events[0].eventId).toBe('evt-1');
|
|
86
86
|
expect(events[0].directory).toBe('/tmp/project');
|
|
87
87
|
expect(events[0].payload).toEqual({
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
},
|
|
88
|
+
id: 'evt-1',
|
|
89
|
+
type: 'session.renamed',
|
|
90
|
+
location: { directory: '/tmp/project' },
|
|
91
|
+
data: { sessionID: 's1', title: 't' },
|
|
92
92
|
});
|
|
93
93
|
expect(reader.getLastEventId()).toBe('evt-1');
|
|
94
94
|
});
|
|
@@ -100,7 +100,7 @@ describe('createUpstreamSseReader', () => {
|
|
|
100
100
|
let reader;
|
|
101
101
|
|
|
102
102
|
reader = createUpstreamSseReader({
|
|
103
|
-
buildUrl: () => 'http://127.0.0.1:4096/
|
|
103
|
+
buildUrl: () => 'http://127.0.0.1:4096/api/event',
|
|
104
104
|
stallTimeoutMs: 10,
|
|
105
105
|
reconnectDelayMs: 0,
|
|
106
106
|
fetchImpl: async (_url, options) => {
|
|
@@ -112,7 +112,7 @@ describe('createUpstreamSseReader', () => {
|
|
|
112
112
|
signal: options.signal,
|
|
113
113
|
holdOpen: true,
|
|
114
114
|
blocks: [
|
|
115
|
-
'id:
|
|
115
|
+
'data: {"id":"evt-1","type":"server.connected","properties":{}}\n\n',
|
|
116
116
|
],
|
|
117
117
|
});
|
|
118
118
|
}
|
|
@@ -120,7 +120,7 @@ describe('createUpstreamSseReader', () => {
|
|
|
120
120
|
return createSseResponse({
|
|
121
121
|
signal: options.signal,
|
|
122
122
|
blocks: [
|
|
123
|
-
'id:
|
|
123
|
+
'data: {"id":"evt-2","type":"session.updated","properties":{}}\n\n',
|
|
124
124
|
],
|
|
125
125
|
});
|
|
126
126
|
},
|
|
@@ -146,7 +146,7 @@ describe('createUpstreamSseReader', () => {
|
|
|
146
146
|
let reader;
|
|
147
147
|
|
|
148
148
|
reader = createUpstreamSseReader({
|
|
149
|
-
buildUrl: () => 'http://127.0.0.1:4096/
|
|
149
|
+
buildUrl: () => 'http://127.0.0.1:4096/api/event',
|
|
150
150
|
stallTimeoutMs: () => currentTimeout,
|
|
151
151
|
reconnectDelayMs: 0,
|
|
152
152
|
fetchImpl: async (_url, options) => {
|
|
@@ -158,7 +158,7 @@ describe('createUpstreamSseReader', () => {
|
|
|
158
158
|
signal: options.signal,
|
|
159
159
|
holdOpen: true,
|
|
160
160
|
blocks: [
|
|
161
|
-
'id:
|
|
161
|
+
'data: {"id":"evt-1","type":"server.connected","properties":{}}\n\n',
|
|
162
162
|
],
|
|
163
163
|
});
|
|
164
164
|
}
|
|
@@ -166,7 +166,7 @@ describe('createUpstreamSseReader', () => {
|
|
|
166
166
|
return createSseResponse({
|
|
167
167
|
signal: options.signal,
|
|
168
168
|
blocks: [
|
|
169
|
-
'id:
|
|
169
|
+
'data: {"id":"evt-2","type":"session.updated","properties":{}}\n\n',
|
|
170
170
|
],
|
|
171
171
|
});
|
|
172
172
|
},
|
|
@@ -191,7 +191,7 @@ describe('createUpstreamSseReader', () => {
|
|
|
191
191
|
let reader;
|
|
192
192
|
|
|
193
193
|
reader = createUpstreamSseReader({
|
|
194
|
-
buildUrl: () => 'http://127.0.0.1:4096/
|
|
194
|
+
buildUrl: () => 'http://127.0.0.1:4096/api/event',
|
|
195
195
|
reconnectDelayMs: 0,
|
|
196
196
|
fetchImpl: async (_url, options) => {
|
|
197
197
|
attempt += 1;
|
|
@@ -210,7 +210,7 @@ describe('createUpstreamSseReader', () => {
|
|
|
210
210
|
return createSseResponse({
|
|
211
211
|
signal: options.signal,
|
|
212
212
|
blocks: [
|
|
213
|
-
'id:
|
|
213
|
+
'data: {"id":"evt-1","type":"server.connected","properties":{}}\n\n',
|
|
214
214
|
],
|
|
215
215
|
});
|
|
216
216
|
},
|
|
@@ -240,7 +240,7 @@ describe('createUpstreamSseReader', () => {
|
|
|
240
240
|
let reader;
|
|
241
241
|
|
|
242
242
|
reader = createUpstreamSseReader({
|
|
243
|
-
buildUrl: () => 'http://127.0.0.1:4096/
|
|
243
|
+
buildUrl: () => 'http://127.0.0.1:4096/api/event',
|
|
244
244
|
reconnectDelayMs: 1,
|
|
245
245
|
signal: tracked.signal,
|
|
246
246
|
fetchImpl: async (_url, options) => {
|
|
@@ -258,7 +258,7 @@ describe('createUpstreamSseReader', () => {
|
|
|
258
258
|
return createSseResponse({
|
|
259
259
|
signal: options.signal,
|
|
260
260
|
blocks: [
|
|
261
|
-
'id:
|
|
261
|
+
'data: {"id":"evt-1","type":"server.connected","properties":{}}\n\n',
|
|
262
262
|
],
|
|
263
263
|
});
|
|
264
264
|
},
|
|
@@ -53,9 +53,11 @@ twelve children.
|
|
|
53
53
|
- Workspace checks accept, besides the active workspace and its worktrees, the **managed roots**: the OpenChamber config root and the managed chats root (`managedChatsRoot` dependency; `OPENCHAMBER_CHATS_DIR` upstream, default `<config root>/chats`). Chat worktrees may legitimately live outside every project workspace.
|
|
54
54
|
- `GET /api/fs/home` preserves `{ home, chatsRoot }` and adds `canonicalChatsRoot` and `canonicalLegacyChatsRoot`, resolved by the server's filesystem. If a root does not exist yet, it resolves the nearest existing ancestor and appends the missing segments, without creating directories. Errors resolving the configured root fail the request. A failed legacy lookup warns and omits only that optional alias, retaining exact legacy matching without blocking an accessible relocated root. Clients use confirmed aliases for exact membership while keeping the original roots as folder/scope identities. `chatsRoot` is the server-resolved managed chats root; clients must use it instead of joining `home` + the well-known segment (a relocated root does not contain that segment).
|
|
55
55
|
- Workspace authorization accepts both the configured managed paths and their filesystem-confirmed canonical roots. Canonical lookup runs only when lexical workspace/managed-root checks fail. One failed managed-root lookup cannot reject an independently authorized root or worktree. Path comparisons remain case-sensitive on POSIX: distinct Linux directories named `chats` and `Chats` never become aliases through string folding. Shared UI keeps exact root membership for classification and directory cleanup.
|
|
56
|
-
- Filesystem `EPERM`/`EACCES` failures use the stable `reason: "os-permission"` response marker.
|
|
56
|
+
- Filesystem `EPERM`/`EACCES` failures use the stable `reason: "os-permission"` response marker. Workspace-boundary policy denials must not use that marker because a native folder picker cannot remediate them.
|
|
57
57
|
- `GET /api/fs/directory-stat?path=...` uses one `stat` without listing contents or resolving project topology. It follows the same authenticated directory-discovery path policy as `/api/fs/list`, including targets outside the active workspace. A directory returns `{ isDirectory: true }`; `ENOENT` returns `not-found`, and a file or `ENOTDIR` returns `not-directory`. Permission and other failures remain distinct from a missing path. VS Code explicitly returns 501, so the shared client treats its probe as unknown.
|
|
58
|
-
-
|
|
58
|
+
- A page shown through `GET /api/fs/serve/…` loads its relative images, styles and scripts without a token; `ui-auth.js` accepts the page's own `oc_url_token` from the `Referer` for a subresource, but only when both URLs sit under `/api/fs/serve/`. Without this, HTML previews on desktop (no session cookie on the loopback API origin) show broken images.
|
|
59
|
+
- `GET /api/fs/raw` answers one `Range: bytes=…` span with `206`, `Content-Range` and a stream from disk (`byte-range.js` reduces the header to a span). Media elements send a span on every seek, and Chromium and WebKit refuse to seek without a `206`, so the viewer's audio and video players depend on this; a whole-file request still reads and sends the buffer. The MIME table (`FILE_MIME_MAP`) is shared with `/api/fs/serve` and covers images, PDF, audio, video, fonts, CSV/TSV and Mermaid.
|
|
60
|
+
- File read, raw and stat routes accept outside paths with `allowOutsideWorkspace=true` under the normal server authentication and OS permissions. They do not require a separate file grant. Legacy grant parameters are ignored. Workspace-scoped reads resolve symlinks after checking the requested path. Write routes keep canonical-target boundary checks.
|
|
59
61
|
- If adding new `/api/fs/*` endpoints, add them in `routes.js` and extend this document.
|
|
60
62
|
- `GET /api/fs/list` may resolve symlinks with `realpath` to read directory contents, but the response `path` and each entry `path` must stay in the caller's requested path space (`path.join(requestedPath, name)`). Returning real paths breaks file-tree expansion for directories reached through workspace symlinks.
|
|
61
63
|
- `POST /api/fs/upload` accepts one `application/octet-stream` body with `path` and optional `overwrite=true` query parameters. The body streams into a same-directory temp file with a 100 MiB default cap configurable through `OPENCHAMBER_FS_UPLOAD_MAX_BYTES`; failed and oversized uploads clean up that temp file. New files commit through an atomic no-replace link, existing files return `409` unless overwrite is explicit, directory targets are rejected, and the destination parent resolves before writing so uploads cannot escape through workspace symlinks.
|