@vantran-se/openchamber-web 2.1.1 → 2.2.0
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 +32 -24
- package/bin/cli.js +1 -3
- package/bin/cli.test.js +40 -16
- package/bin/lib/DOCUMENTATION.md +6 -5
- package/bin/lib/cli-args.js +6 -1
- package/bin/lib/cli-args.test.js +34 -0
- package/bin/lib/cli-lifecycle.js +0 -1
- package/bin/lib/cli-network.js +8 -2
- package/bin/lib/cli-process.js +1 -1
- package/bin/lib/cli-settings-accessors.test.js +21 -13
- package/bin/lib/cli-startup.js +17 -11
- package/bin/lib/cli-startup.test.js +92 -2
- package/bin/lib/commands-connect-url.js +8 -1
- package/bin/lib/commands-schedule.js +8 -2
- package/bin/lib/commands-serve.js +15 -21
- package/bin/lib/commands-update.js +12 -24
- package/bin/lib/commands-update.test.js +14 -46
- package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
- package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
- package/dist/assets/FilesView-BVoD8I2x.js +14 -0
- package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
- package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
- package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
- package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
- package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
- package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
- package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
- package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
- package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
- package/dist/assets/PlanView-RCBadcY7.js +5 -0
- package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
- package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
- package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
- package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
- package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
- package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
- package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
- package/dist/assets/de-YKfk5MmC.js +14 -0
- package/dist/assets/es-_gwT0kr-.js +20 -0
- package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
- package/dist/assets/fr-DqzvXmIm.js +18 -0
- package/dist/assets/index-BLaHKdtx.js +1 -0
- package/dist/assets/index-BkMXRrVQ.css +1 -0
- package/dist/assets/index-DxNa7xYB.js +65 -0
- package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
- package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
- package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
- package/dist/assets/ja-B-GFYhQE.js +20 -0
- package/dist/assets/ko-mzksfqDC.js +20 -0
- package/dist/assets/main-C5Xx8dn_.js +2 -0
- package/dist/assets/main-CfBSQM1H.js +990 -0
- package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
- package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
- package/dist/assets/mobile-DeEQd0wZ.js +2 -0
- package/dist/assets/nl-CWQr6NSq.js +20 -0
- package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
- package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
- package/dist/assets/pl-UmRcDUX2.js +20 -0
- package/dist/assets/pt-BR-BSLybvVF.js +20 -0
- package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
- package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
- package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
- package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
- package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
- package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
- package/dist/assets/space-apply-DM_2pRW7.js +1 -0
- package/dist/assets/space-close-Bpomt8z0.js +1 -0
- package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
- package/dist/assets/tr-gaTrX_HK.js +11 -0
- package/dist/assets/uk-CnJF6ykl.js +20 -0
- package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
- package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
- package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
- package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
- package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
- package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
- package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
- package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
- package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
- package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
- package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
- package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
- package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
- package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
- package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
- package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
- package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
- package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
- package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
- package/dist/assets/zh-CN-t2vigSB1.js +20 -0
- package/dist/assets/zh-TW-jAgqBokB.js +20 -0
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +8 -5
- package/server/index.js +80 -36
- package/server/lib/agent-memory/actions.js +1 -6
- package/server/lib/agent-memory/routes.js +1 -2
- package/server/lib/agent-memory/runtime.js +1 -8
- package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
- package/server/lib/agent-tool/runtime.js +3 -6
- package/server/lib/agent-tool/runtime.test.js +0 -2
- package/server/lib/browser-control/DOCUMENTATION.md +6 -0
- package/server/lib/changelog/update-notes.js +1 -1
- package/server/lib/context-obligatory/runtime.js +1 -2
- package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
- package/server/lib/dev-tunnel/client.js +21 -10
- package/server/lib/dev-tunnel/tunnel.test.js +34 -0
- package/server/lib/dispatch-results/runtime.js +1 -1
- package/server/lib/enterprise-mode.d.ts +1 -0
- package/server/lib/enterprise-mode.js +13 -2
- package/server/lib/enterprise-mode.test.js +21 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/index.js +1 -3
- package/server/lib/event-stream/translate-v2.js +1 -9
- package/server/lib/event-stream/translate-v2.test.js +0 -11
- package/server/lib/fs/DOCUMENTATION.md +5 -2
- package/server/lib/fs/routes.js +62 -15
- package/server/lib/fs/routes.test.js +278 -48
- package/server/lib/fs/workspace-file-names.js +104 -0
- package/server/lib/fs/workspace-file-names.test.js +62 -0
- package/server/lib/git/DOCUMENTATION.md +17 -5
- package/server/lib/git/contributor-provenance-storage.js +9 -6
- package/server/lib/git/contributor-provenance-storage.test.js +29 -0
- package/server/lib/git/credential-broker.js +1 -1
- package/server/lib/git/credential-resolver.js +1 -4
- package/server/lib/git/credentials.js +1 -0
- package/server/lib/git/discovery-endpoint.js +1 -1
- package/server/lib/git/identity-provisioning.js +2 -1
- package/server/lib/git/identity-storage.js +3 -4
- package/server/lib/git/lfs-discovery.js +14 -14
- package/server/lib/git/lfs-discovery.test.js +16 -6
- package/server/lib/git/network-operation-plan.js +50 -62
- package/server/lib/git/network-operation-plan.test.js +103 -1
- package/server/lib/git/network-operation-registry.js +4 -6
- package/server/lib/git/network-operation-storage.js +4 -6
- package/server/lib/git/network-operations.js +12 -4
- package/server/lib/git/network-operations.test.js +19 -6
- package/server/lib/git/redaction.js +2 -1
- package/server/lib/git/repository-credential-runtime.js +5 -3
- package/server/lib/git/repository-credential-runtime.test.js +22 -0
- package/server/lib/git/routes.js +31 -137
- package/server/lib/git/routes.test.js +2 -2
- package/server/lib/git/service.js +143 -27
- package/server/lib/git/service.test.js +215 -1
- package/server/lib/git/ssh-credential-storage.js +1 -4
- package/server/lib/git/submodule-discovery.js +1 -1
- package/server/lib/git/worktree-bootstrap-storage.js +3 -4
- package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
- package/server/lib/git/worktree-creation.js +194 -0
- package/server/lib/git/worktree-creation.test.js +155 -0
- package/server/lib/github/DOCUMENTATION.md +11 -6
- package/server/lib/github/auth.js +2 -4
- package/server/lib/github/device-flow.js +2 -2
- package/server/lib/github/gh-cli-credential.js +1 -0
- package/server/lib/github/octokit.js +31 -0
- package/server/lib/github/pr-status.js +35 -18
- package/server/lib/github/pr-status.test.js +271 -3
- package/server/lib/github/reference-search.js +102 -13
- package/server/lib/github/reference-search.test.js +48 -7
- package/server/lib/github/routes.auth.test.js +18 -0
- package/server/lib/github/routes.js +360 -6
- package/server/lib/github/routes.mutations.test.js +239 -0
- package/server/lib/github/routes.test.js +134 -2
- package/server/lib/gitlab/DOCUMENTATION.md +5 -1
- package/server/lib/gitlab/auth-storage.test.js +1 -0
- package/server/lib/gitlab/mappers.js +30 -1
- package/server/lib/gitlab/resources.js +230 -6
- package/server/lib/gitlab/resources.test.js +179 -0
- package/server/lib/gitlab/routes.js +200 -6
- package/server/lib/gitlab/routes.test.js +186 -2
- package/server/lib/gitlab/validation.js +1 -5
- package/server/lib/guests/DOCUMENTATION.md +1 -1
- package/server/lib/guests/oauth.js +2 -2
- package/server/lib/guests/service.js +1 -1
- package/server/lib/guests/sockets.js +1 -1
- package/server/lib/injected-env.d.ts +8 -0
- package/server/lib/injected-env.js +50 -0
- package/server/lib/injected-env.test.js +56 -0
- package/server/lib/linear/DOCUMENTATION.md +5 -6
- package/server/lib/linear/issues.js +62 -4
- package/server/lib/linear/issues.test.js +68 -0
- package/server/lib/linear/oauth.js +5 -5
- package/server/lib/linear/parse.js +2 -9
- package/server/lib/message-queue/DOCUMENTATION.md +23 -1
- package/server/lib/message-queue/runtime.js +85 -12
- package/server/lib/message-queue/runtime.test.js +172 -2
- package/server/lib/notifications/apns-runtime.js +5 -6
- package/server/lib/notifications/apns-runtime.test.js +20 -4
- package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
- package/server/lib/openchamber-control/actions.js +2 -1
- package/server/lib/openchamber-control/service.js +119 -20
- package/server/lib/openchamber-control/service.test.js +166 -0
- package/server/lib/openchamber-sessions/archive-store.js +1 -6
- package/server/lib/openchamber-sessions/routes.js +47 -12
- package/server/lib/openchamber-sessions/routes.test.js +65 -2
- package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
- package/server/lib/opencode/DOCUMENTATION.md +22 -8
- package/server/lib/opencode/agents.js +62 -0
- package/server/lib/opencode/auth-state-runtime.js +3 -1
- package/server/lib/opencode/auth.d.ts +1 -1
- package/server/lib/opencode/auth.js +8 -0
- package/server/lib/opencode/auth.test.js +31 -0
- package/server/lib/opencode/config-entity-routes.js +16 -0
- package/server/lib/opencode/config-entity-v2.test.js +16 -1
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.js +2 -4
- package/server/lib/opencode/feature-routes-runtime.js +89 -49
- package/server/lib/opencode/lifecycle.js +41 -21
- package/server/lib/opencode/lifecycle.test.js +50 -2
- package/server/lib/opencode/model-discovery.js +1 -1
- package/server/lib/opencode/model-discovery.test.js +17 -0
- package/server/lib/opencode/models-metadata.js +0 -2
- package/server/lib/opencode/openchamber-routes.test.js +3 -3
- package/server/lib/opencode/plugins.js +6 -9
- package/server/lib/opencode/provider-source-route.test.js +101 -3
- package/server/lib/opencode/providers-disabled.test.js +59 -0
- package/server/lib/opencode/providers.js +34 -0
- package/server/lib/opencode/proxy.js +11 -3
- package/server/lib/opencode/routes.js +40 -2
- package/server/lib/opencode/settings-helpers.js +27 -1
- package/server/lib/opencode/settings-helpers.test.js +19 -7
- package/server/lib/opencode/settings-registry.json +27 -16
- package/server/lib/opencode/settings-runtime.test.js +18 -0
- package/server/lib/opencode/shared-service-runtime.js +23 -104
- package/server/lib/opencode/shared.js +77 -46
- package/server/lib/opencode/shared.test.js +12 -12
- package/server/lib/opencode/shutdown-runtime.test.js +2 -3
- package/server/lib/opencode/skill-routes.js +123 -38
- package/server/lib/opencode/skill-routes.test.js +201 -0
- package/server/lib/opencode/skills.js +81 -34
- package/server/lib/opencode/skills.test.js +176 -21
- package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
- package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
- package/server/lib/opencode/watcher.test.js +0 -54
- package/server/lib/outbound-proxy.js +52 -0
- package/server/lib/outbound-proxy.test.js +55 -0
- package/server/lib/package-manager.js +23 -17
- package/server/lib/package-manager.test.js +5 -18
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
- package/server/lib/permission-auto-accept/runtime.js +19 -47
- package/server/lib/permission-auto-accept/runtime.test.js +2 -2
- package/server/lib/project-context/DOCUMENTATION.md +7 -1
- package/server/lib/project-context/routes.js +1 -2
- package/server/lib/project-context/runtime.js +12 -14
- package/server/lib/project-context/runtime.test.js +78 -2
- package/server/lib/projects/project-config.js +30 -15
- package/server/lib/projects/project-config.test.js +30 -0
- package/server/lib/projects/project-setup.js +1 -2
- package/server/lib/projects/routes.js +1 -2
- package/server/lib/quota/DOCUMENTATION.md +35 -7
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/claude/index.js +1 -1
- package/server/lib/quota/providers/cline-pass.js +1 -1
- package/server/lib/quota/providers/exe-dev.js +1 -1
- package/server/lib/quota/providers/google/index.js +1 -1
- package/server/lib/quota/providers/hyper.js +1 -1
- package/server/lib/quota/providers/kimi.js +116 -36
- package/server/lib/quota/providers/kimi.test.js +128 -3
- package/server/lib/quota/providers/opencode-go.js +144 -21
- package/server/lib/quota/providers/opencode-go.test.js +229 -6
- package/server/lib/quota/providers/openrouter.js +1 -1
- package/server/lib/quota/providers/zenmux.js +1 -1
- package/server/lib/quota/utils/auth.js +1 -0
- package/server/lib/quota/utils/transformers.js +2 -6
- package/server/lib/relay/DOCUMENTATION.md +18 -3
- package/server/lib/relay/e2ee.js +7 -7
- package/server/lib/relay/host-client.js +84 -13
- package/server/lib/relay/host-client.test.js +201 -1
- package/server/lib/relay/identity.js +6 -29
- package/server/lib/relay/identity.test.js +18 -28
- package/server/lib/relay/key-store.js +141 -0
- package/server/lib/relay/key-store.test.js +138 -0
- package/server/lib/relay/service.js +8 -8
- package/server/lib/relay/service.test.js +10 -5
- package/server/lib/relay/signing-key.js +13 -36
- package/server/lib/relay/tunnel-codec.js +10 -10
- package/server/lib/route-module-exports.test.js +22 -0
- package/server/lib/routing/DOCUMENTATION.md +23 -3
- package/server/lib/routing/cache.js +87 -0
- package/server/lib/routing/defaults.js +11 -0
- package/server/lib/routing/history.js +12 -4
- package/server/lib/routing/runtime.js +39 -3
- package/server/lib/routing/runtime.test.js +81 -2
- package/server/lib/routing/store.js +12 -1
- package/server/lib/routing/store.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
- package/server/lib/scheduled-tasks/existing-session.js +119 -0
- package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
- package/server/lib/scheduled-tasks/loops.js +1 -8
- package/server/lib/scheduled-tasks/routes.js +8 -8
- package/server/lib/scheduled-tasks/runtime.js +103 -13
- package/server/lib/scheduled-tasks/runtime.test.js +48 -0
- package/server/lib/scheduled-tasks/service.js +33 -11
- package/server/lib/scheduled-tasks/service.test.js +128 -2
- package/server/lib/scheduled-tasks/session-defaults.js +27 -0
- package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
- package/server/lib/session-assist/context.js +1 -0
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-goal/objectives.js +1 -1
- package/server/lib/session-knowledge/routes.js +2 -2
- package/server/lib/session-knowledge/runtime.js +1 -2
- package/server/lib/session-work/DOCUMENTATION.md +39 -14
- package/server/lib/session-work/questions.js +31 -2
- package/server/lib/session-work/runtime.js +22 -13
- package/server/lib/session-work/runtime.test.js +43 -26
- package/server/lib/session-work/state.js +10 -4
- package/server/lib/session-work/state.test.js +8 -1
- package/server/lib/shared/DOCUMENTATION.md +89 -0
- package/server/lib/shared/guards.js +53 -0
- package/server/lib/shared/guards.test.js +112 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
- package/server/lib/skills-catalog/cache.js +3 -2
- package/server/lib/skills-catalog/curated-sources.js +5 -1
- package/server/lib/skills-catalog/git.js +107 -20
- package/server/lib/skills-catalog/git.test.js +78 -0
- package/server/lib/skills-catalog/install.js +21 -12
- package/server/lib/skills-catalog/scan.js +25 -15
- package/server/lib/skills-catalog/source.js +26 -1
- package/server/lib/skills-catalog/source.test.js +27 -0
- package/server/lib/source-control/DOCUMENTATION.md +2 -1
- package/server/lib/source-control/audit-storage.js +7 -6
- package/server/lib/source-control/audit-storage.test.js +11 -0
- package/server/lib/source-control/binding-service.js +2 -5
- package/server/lib/source-control/change-request-worktree.js +90 -0
- package/server/lib/source-control/change-request-worktree.test.js +121 -0
- package/server/lib/source-control/mutation-executor.js +1 -1
- package/server/lib/source-control/mutation-storage.js +11 -5
- package/server/lib/source-control/provider-instance.js +1 -2
- package/server/lib/source-control/repository-identity.js +1 -1
- package/server/lib/source-control/routes.js +14 -2
- package/server/lib/spaces/DOCUMENTATION.md +42 -17
- package/server/lib/spaces/command-stream.test.js +15 -0
- package/server/lib/spaces/dispatcher.js +7 -0
- package/server/lib/spaces/dispatcher.test.js +17 -0
- package/server/lib/spaces/hardening.js +2 -2
- package/server/lib/spaces/hardening.test.js +1 -1
- package/server/lib/spaces/host.js +2 -0
- package/server/lib/spaces/journey.js +66 -5
- package/server/lib/spaces/journey.test.js +95 -4
- package/server/lib/spaces/layout.js +1 -1
- package/server/lib/spaces/places/docker-disk.js +38 -14
- package/server/lib/spaces/places/docker-disk.test.js +80 -1
- package/server/lib/spaces/places/docker.js +35 -11
- package/server/lib/spaces/places/fake-docker.js +22 -3
- package/server/lib/spaces/places/memory-place.js +3 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
- package/server/lib/spaces/routes.js +5 -0
- package/server/lib/spaces/routes.test.js +4 -2
- package/server/lib/spaces/run-command.js +9 -2
- package/server/lib/spaces/tools.js +4 -5
- package/server/lib/spaces/tools.test.js +2 -2
- package/server/lib/terminal/runtime.js +18 -6
- package/server/lib/terminal/runtime.test.js +66 -0
- package/server/lib/tts/index.js +3 -0
- package/server/lib/tts/service.js +4 -2
- package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
- package/server/lib/walkthrough/index.js +2 -3
- package/server/lib/walkthrough/pr-source.test.js +22 -0
- package/server/lib/walkthrough/sources.js +6 -1
- package/server/opencode-proxy.test.js +127 -0
- package/bin/lib/commands-startup.test.js +0 -15
- package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
- package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
- package/dist/assets/FilesView-DT0uEYgT.js +0 -17
- package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
- package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
- package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
- package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
- package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
- package/dist/assets/PlanView-DsRWnsOI.js +0 -5
- package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
- package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
- package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
- package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
- package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
- package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
- package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
- package/dist/assets/de-CBla4hjd.js +0 -14
- package/dist/assets/es-BmnZtxTD.js +0 -20
- package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
- package/dist/assets/fr-OVKIBUZO.js +0 -18
- package/dist/assets/index-CKCFKmjF.js +0 -1
- package/dist/assets/index-Dbx4V4GN.css +0 -1
- package/dist/assets/index-vQ1Pg1cr.js +0 -61
- package/dist/assets/ja-6v-yr-gE.js +0 -20
- package/dist/assets/ko-I_ZLvgQ9.js +0 -20
- package/dist/assets/main-CmO4dRtR.js +0 -2
- package/dist/assets/main-b9vqhlYM.js +0 -999
- package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
- package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
- package/dist/assets/mobile-CVtShif9.js +0 -2
- package/dist/assets/nl-DcvIJf46.js +0 -20
- package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
- package/dist/assets/pl-JxCUl0sd.js +0 -20
- package/dist/assets/pt-BR-L89mjja2.js +0 -20
- package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
- package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
- package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
- package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
- package/dist/assets/space-apply-1trGVIl8.js +0 -1
- package/dist/assets/space-close-y7j-NuBv.js +0 -1
- package/dist/assets/tr-ka5J1flt.js +0 -11
- package/dist/assets/uk-PyGe6Q_u.js +0 -20
- package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
- package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
- package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
- package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
- package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
- package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
- package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
- package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
|
@@ -55,6 +55,16 @@ const createService = (overrides = {}) => {
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
describe('OpenChamber control service', () => {
|
|
58
|
+
it('creates a target task with inherited selection and accepts retargeting on update', async () => {
|
|
59
|
+
const { service, scheduledTaskService } = createService();
|
|
60
|
+
scheduledTaskService.upsert.mockImplementation(async (_project, task) => ({ task }));
|
|
61
|
+
const created = await service.execute('schedule.create', { name: 'Reuse', prompt: 'Continue', daily: '09:00', targetSessionId: 'ses_target' }, '/repo');
|
|
62
|
+
expect(created.task).toMatchObject({ targetSessionId: 'ses_target', execution: { useDefaults: true } });
|
|
63
|
+
scheduledTaskService.list.mockResolvedValue([{ ...created.task, id: 'task-1' }]);
|
|
64
|
+
await service.execute('schedule.update', { taskId: 'task-1', targetSessionId: 'ses_second', agent: 'plan' }, '/repo');
|
|
65
|
+
expect(scheduledTaskService.upsert.mock.calls[1][1]).toMatchObject({ targetSessionId: 'ses_second', execution: { agent: 'plan' } });
|
|
66
|
+
expect(scheduledTaskService.upsert.mock.calls[1][1].execution.useDefaults).toBeUndefined();
|
|
67
|
+
});
|
|
58
68
|
it('serves project and model projections without an HTTP or CLI round trip', async () => {
|
|
59
69
|
const { service } = createService();
|
|
60
70
|
await expect(service.execute('projects.list')).resolves.toEqual({
|
|
@@ -86,6 +96,46 @@ describe('OpenChamber control service', () => {
|
|
|
86
96
|
}));
|
|
87
97
|
});
|
|
88
98
|
|
|
99
|
+
it('updates a scheduled task in place, changing only the named fields', async () => {
|
|
100
|
+
const { service, scheduledTaskService } = createService();
|
|
101
|
+
const existing = {
|
|
102
|
+
id: 'task-1',
|
|
103
|
+
name: 'Nightly',
|
|
104
|
+
enabled: true,
|
|
105
|
+
schedule: { kind: 'daily', times: ['09:00'], timezone: 'UTC' },
|
|
106
|
+
execution: { prompt: 'Old', providerID: 'a', modelID: 'm', variant: 'high', agent: 'build' },
|
|
107
|
+
state: { lastRunAt: 5, lastStatus: 'success' },
|
|
108
|
+
};
|
|
109
|
+
scheduledTaskService.list.mockResolvedValue([existing]);
|
|
110
|
+
scheduledTaskService.upsert.mockImplementation(async (_project, task) => ({ task, created: false }));
|
|
111
|
+
|
|
112
|
+
await service.execute('schedule.update', { taskId: 'task-1', prompt: 'New', model: 'b/n', cron: '0 * * * *' }, '/repo');
|
|
113
|
+
const saved = scheduledTaskService.upsert.mock.calls[0][1];
|
|
114
|
+
expect(saved).toEqual({
|
|
115
|
+
id: 'task-1',
|
|
116
|
+
name: 'Nightly',
|
|
117
|
+
enabled: true,
|
|
118
|
+
schedule: { kind: 'cron', cron: '0 * * * *' },
|
|
119
|
+
execution: { prompt: 'New', providerID: 'b', modelID: 'n', agent: 'build' },
|
|
120
|
+
});
|
|
121
|
+
// No state in the patch: the stored run history is kept.
|
|
122
|
+
expect(saved.state).toBeUndefined();
|
|
123
|
+
|
|
124
|
+
await service.execute('schedule.update', { taskId: 'task-1', timezone: 'Europe/Kyiv', disabled: true }, '/repo');
|
|
125
|
+
expect(scheduledTaskService.upsert.mock.calls[1][1]).toMatchObject({
|
|
126
|
+
enabled: false,
|
|
127
|
+
schedule: { kind: 'daily', times: ['09:00'], timezone: 'Europe/Kyiv' },
|
|
128
|
+
execution: existing.execution,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('refuses to update a missing task or one driven by a loop file', async () => {
|
|
133
|
+
const { service, scheduledTaskService } = createService();
|
|
134
|
+
await expect(service.execute('schedule.update', { taskId: 'nope', prompt: 'x' }, '/repo')).rejects.toThrow('Scheduled task not found');
|
|
135
|
+
scheduledTaskService.list.mockResolvedValue([{ id: 'loop:project:x', loopFile: '/repo/.agents/loops/x.md', name: 'x', execution: {}, schedule: {} }]);
|
|
136
|
+
await expect(service.execute('schedule.update', { taskId: 'loop:project:x', prompt: 'x' }, '/repo')).rejects.toThrow('change that file instead');
|
|
137
|
+
});
|
|
138
|
+
|
|
89
139
|
it('does not combine an explicit schedule project with the tool context directory', async () => {
|
|
90
140
|
const { service, scheduledTaskService } = createService();
|
|
91
141
|
await service.execute('schedule.list', { projectId: ' project-1 ' }, '/current-session');
|
|
@@ -112,6 +162,15 @@ describe('OpenChamber control service', () => {
|
|
|
112
162
|
expect(scheduledTaskService.setEnabled).toHaveBeenCalledWith('project-1', 'task-1', false);
|
|
113
163
|
});
|
|
114
164
|
|
|
165
|
+
it('reports the enabled state that was saved, not the one requested', async () => {
|
|
166
|
+
const { service, scheduledTaskService } = createService();
|
|
167
|
+
scheduledTaskService.setEnabled.mockResolvedValue({ id: 'task-1', enabled: false });
|
|
168
|
+
await expect(service.execute('schedule.toggle', { taskId: 'task-1', disabled: false }, '/repo')).resolves.toEqual({
|
|
169
|
+
task: { id: 'task-1', enabled: false },
|
|
170
|
+
enabled: false,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
115
174
|
it('returns an actionable taskId error before resolving schedule scope', async () => {
|
|
116
175
|
const { service, scheduledTaskService } = createService();
|
|
117
176
|
await expect(service.execute('schedule.run', {}, '/repo')).rejects.toThrow('taskId is required');
|
|
@@ -131,6 +190,23 @@ describe('OpenChamber control service', () => {
|
|
|
131
190
|
expect(sessionService.create).toHaveBeenCalledWith({ directory: '/repo', title: 'From tool' });
|
|
132
191
|
});
|
|
133
192
|
|
|
193
|
+
it('hands a pull request number to session creation with the folder and branch names it may carry', async () => {
|
|
194
|
+
const { service, sessionService } = createService();
|
|
195
|
+
await service.execute('session.create', { pullRequest: 42 }, '/repo');
|
|
196
|
+
await service.execute('session.create', { pullRequest: 42, worktree: ' review ', branch: 'review-42' }, '/repo');
|
|
197
|
+
expect(sessionService.create.mock.calls).toEqual([
|
|
198
|
+
[{ directory: '/repo', worktree: {}, pullRequest: 42 }],
|
|
199
|
+
[{ directory: '/repo', worktree: { name: 'review', branchName: 'review-42' }, pullRequest: 42 }],
|
|
200
|
+
]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('refuses a pull request number on a send or fork, which would ignore it', async () => {
|
|
204
|
+
const { service, sessionService } = createService();
|
|
205
|
+
await expect(service.execute('session.send', { sessionId: 'ses_1', prompt: 'Go', pullRequest: 42 }, '/repo'))
|
|
206
|
+
.rejects.toThrow('pullRequest applies only to session.create');
|
|
207
|
+
expect(sessionService.send).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
134
210
|
it.each([
|
|
135
211
|
['session.send', 'send'],
|
|
136
212
|
['session.fork', 'fork'],
|
|
@@ -313,6 +389,96 @@ describe('OpenChamber control service', () => {
|
|
|
313
389
|
});
|
|
314
390
|
});
|
|
315
391
|
|
|
392
|
+
it('follows the message cursor so all: true returns the whole history', async () => {
|
|
393
|
+
const { service, client } = createService();
|
|
394
|
+
const page = (data, next) => ({ data, cursor: { next } });
|
|
395
|
+
// Newest page first (the API pages with order: 'desc'), then older pages
|
|
396
|
+
// reachable only through cursor.next.
|
|
397
|
+
client.message.list.mockImplementation(async (input) => {
|
|
398
|
+
if (!input.cursor) {
|
|
399
|
+
return page([
|
|
400
|
+
{ id: 'msg_new_user', type: 'user', time: { created: 40 }, text: 'Recent question' },
|
|
401
|
+
{ id: 'msg_new_assistant', type: 'assistant', time: { created: 50, completed: 55 }, content: [{ type: 'text', text: 'Recent answer' }] },
|
|
402
|
+
], 'cursor-page-2');
|
|
403
|
+
}
|
|
404
|
+
if (input.cursor === 'cursor-page-2') {
|
|
405
|
+
return page([
|
|
406
|
+
{ id: 'msg_old_user', type: 'user', time: { created: 10 }, text: 'Older requirement' },
|
|
407
|
+
], undefined);
|
|
408
|
+
}
|
|
409
|
+
throw new Error(`unexpected cursor ${input.cursor}`);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const result = await service.execute('session.messages', {
|
|
413
|
+
sessionId: 'ses_1',
|
|
414
|
+
directory: '/repo',
|
|
415
|
+
role: 'all',
|
|
416
|
+
all: true,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(client.message.list).toHaveBeenCalledTimes(2);
|
|
420
|
+
expect(client.message.list.mock.calls[1][0]).toMatchObject({ sessionID: 'ses_1', cursor: 'cursor-page-2' });
|
|
421
|
+
expect(result.messages.map((message) => message.id)).toEqual([
|
|
422
|
+
'msg_old_user', 'msg_new_user', 'msg_new_assistant',
|
|
423
|
+
]);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('pages a bounded read with the cursor alone until it has enough text messages', async () => {
|
|
427
|
+
const { service, client } = createService();
|
|
428
|
+
const page = (data, next) => ({ data, cursor: { next } });
|
|
429
|
+
client.message.list.mockImplementation(async (input) => {
|
|
430
|
+
if (!input.cursor) {
|
|
431
|
+
return page([
|
|
432
|
+
{ id: 'msg_new_assistant', type: 'assistant', time: { created: 50, completed: 55 }, content: [{ type: 'text', text: 'Recent answer' }] },
|
|
433
|
+
{ id: 'msg_new_user', type: 'user', time: { created: 40 }, text: 'Recent question' },
|
|
434
|
+
], 'cursor-page-2');
|
|
435
|
+
}
|
|
436
|
+
if (input.cursor === 'cursor-page-2') {
|
|
437
|
+
return page([
|
|
438
|
+
{ id: 'msg_mid_user', type: 'user', time: { created: 20 }, text: 'Earlier question' },
|
|
439
|
+
{ id: 'msg_old_user', type: 'user', time: { created: 10 }, text: 'First question' },
|
|
440
|
+
], 'cursor-page-3');
|
|
441
|
+
}
|
|
442
|
+
throw new Error(`unexpected cursor ${input.cursor}`);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const result = await service.execute('session.messages', {
|
|
446
|
+
sessionId: 'ses_1',
|
|
447
|
+
directory: '/repo',
|
|
448
|
+
role: 'user',
|
|
449
|
+
limit: 2,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
expect(client.message.list).toHaveBeenCalledTimes(2);
|
|
453
|
+
expect(client.message.list.mock.calls[0][0]).toMatchObject({ sessionID: 'ses_1', order: 'desc' });
|
|
454
|
+
expect(client.message.list.mock.calls[1][0]).toMatchObject({ sessionID: 'ses_1', cursor: 'cursor-page-2' });
|
|
455
|
+
expect(client.message.list.mock.calls[1][0]).not.toHaveProperty('order');
|
|
456
|
+
expect(result.messages.map((message) => message.id)).toEqual(['msg_mid_user', 'msg_new_user']);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('stops paging when the message cursor repeats or a page comes back empty', async () => {
|
|
460
|
+
const { service, client } = createService();
|
|
461
|
+
client.message.list.mockImplementation(async (input) => {
|
|
462
|
+
if (!input.cursor) {
|
|
463
|
+
return { data: [{ id: 'msg_new_user', type: 'user', time: { created: 20 }, text: 'Question' }], cursor: { next: 'cursor-loop' } };
|
|
464
|
+
}
|
|
465
|
+
return { data: [{ id: 'msg_old_user', type: 'user', time: { created: 10 }, text: 'Older question' }], cursor: { next: 'cursor-loop' } };
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const looped = await service.execute('session.messages', { sessionId: 'ses_1', directory: '/repo', all: true });
|
|
469
|
+
expect(client.message.list).toHaveBeenCalledTimes(2);
|
|
470
|
+
expect(looped.messages.map((message) => message.id)).toEqual(['msg_old_user', 'msg_new_user']);
|
|
471
|
+
|
|
472
|
+
client.message.list.mockReset();
|
|
473
|
+
client.message.list.mockImplementation(async (input) => (input.cursor
|
|
474
|
+
? { data: [], cursor: { next: `${input.cursor}-next` } }
|
|
475
|
+
: { data: [{ id: 'msg_user', type: 'user', time: { created: 10 }, text: 'Question' }], cursor: { next: 'cursor-empty' } }));
|
|
476
|
+
|
|
477
|
+
const drained = await service.execute('session.messages', { sessionId: 'ses_1', directory: '/repo', all: true });
|
|
478
|
+
expect(client.message.list).toHaveBeenCalledTimes(2);
|
|
479
|
+
expect(drained.messages.map((message) => message.id)).toEqual(['msg_user']);
|
|
480
|
+
});
|
|
481
|
+
|
|
316
482
|
it('rejects actions outside the fixed contract', async () => {
|
|
317
483
|
const { service } = createService();
|
|
318
484
|
await expect(service.execute('session.delete')).rejects.toThrow('Unsupported OpenChamber action');
|
|
@@ -11,17 +11,12 @@
|
|
|
11
11
|
* does not mention keeps whatever OpenCode says.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { asNonEmptyString } from '../shared/guards.js';
|
|
14
15
|
import fsDefault from 'node:fs';
|
|
15
16
|
import pathDefault from 'node:path';
|
|
16
17
|
|
|
17
18
|
const ARCHIVE_FILE_NAME = 'sessions-archive.json';
|
|
18
19
|
|
|
19
|
-
const asNonEmptyString = (value) => {
|
|
20
|
-
if (typeof value !== 'string') return null;
|
|
21
|
-
const trimmed = value.trim();
|
|
22
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
20
|
const asTimestamp = (value) => (Number.isSafeInteger(value) && value > 0 ? value : null);
|
|
26
21
|
|
|
27
22
|
const asIdList = (ids) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asNonEmptyString } from '../shared/guards.js';
|
|
1
2
|
import express from 'express';
|
|
2
3
|
import {
|
|
3
4
|
createWorktree as createWorktreeDefault,
|
|
@@ -16,12 +17,6 @@ import { applyForkInheritance } from './fork-inheritance.js';
|
|
|
16
17
|
import { createOpenCodeClient as defaultCreateOpenCodeClient } from './opencode-client.js';
|
|
17
18
|
import { createSessionMetadataStore, createOpenCodeSessionMetadata } from './session-metadata-store.js';
|
|
18
19
|
|
|
19
|
-
const asNonEmptyString = (value) => {
|
|
20
|
-
if (typeof value !== 'string') return null;
|
|
21
|
-
const trimmed = value.trim();
|
|
22
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
20
|
const asList = (value) => (Array.isArray(value) ? value : []);
|
|
26
21
|
|
|
27
22
|
const splitModel = (value) => {
|
|
@@ -253,7 +248,6 @@ const applySessionSelection = async ({ client, sessionID, model, agent, variant
|
|
|
253
248
|
const createSession = async ({ client, directory, title }) => {
|
|
254
249
|
const session = await client.session.create({
|
|
255
250
|
location: { directory },
|
|
256
|
-
metadata: { openchamber: { adopted: true } },
|
|
257
251
|
...(title ? { title } : {}),
|
|
258
252
|
});
|
|
259
253
|
const sessionID = asNonEmptyString(session?.id);
|
|
@@ -389,6 +383,27 @@ const resolveRequestedDirectory = async ({ payload, readSettingsFromDiskMigrated
|
|
|
389
383
|
const WORKTREE_BOOTSTRAP_TIMEOUT_MS = 60_000;
|
|
390
384
|
const WORKTREE_BOOTSTRAP_POLL_MS = 150;
|
|
391
385
|
|
|
386
|
+
/**
|
|
387
|
+
* A worktree from a pull or merge request: its number, and the folder and
|
|
388
|
+
* branch names when the caller chose them. The head, the fork and its remote
|
|
389
|
+
* come from the provider; a start ref or upstream would contradict them.
|
|
390
|
+
*/
|
|
391
|
+
const resolvePullRequestInput = (payload) => {
|
|
392
|
+
if (payload?.pullRequest === undefined) return null;
|
|
393
|
+
if (!Number.isSafeInteger(payload.pullRequest) || payload.pullRequest < 1) {
|
|
394
|
+
throw new OpenChamberControlError('pullRequest must be a pull or merge request number, such as 42', 400);
|
|
395
|
+
}
|
|
396
|
+
if (payload.worktree?.startRef !== undefined || payload.setUpstream !== undefined) {
|
|
397
|
+
throw new OpenChamberControlError('pullRequest checks out the pull request\'s own branch; do not combine it with startRef or setUpstream', 400);
|
|
398
|
+
}
|
|
399
|
+
const input = { number: payload.pullRequest };
|
|
400
|
+
const name = asNonEmptyString(payload.worktree?.name);
|
|
401
|
+
if (name) input.name = name;
|
|
402
|
+
const branchName = asNonEmptyString(payload.worktree?.branchName);
|
|
403
|
+
if (branchName) input.branchName = branchName;
|
|
404
|
+
return input;
|
|
405
|
+
};
|
|
406
|
+
|
|
392
407
|
const resolveWorktreeInput = (payload) => {
|
|
393
408
|
if (!payload?.worktree || typeof payload.worktree !== 'object') return null;
|
|
394
409
|
const name = asNonEmptyString(payload.worktree.name);
|
|
@@ -418,6 +433,9 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
418
433
|
sessionKnowledgeRuntime = null,
|
|
419
434
|
worktreeBootstrapStore,
|
|
420
435
|
hydrateWorktreeCheckout,
|
|
436
|
+
// Creates a worktree from a pull or merge request through the Git routes'
|
|
437
|
+
// contributor pipeline. Absent where contributor worktrees are not served.
|
|
438
|
+
createChangeRequestWorktree = null,
|
|
421
439
|
dataDir = null,
|
|
422
440
|
archiveStore: injectedArchiveStore = null,
|
|
423
441
|
sessionMetadataStore: injectedSessionMetadataStore = null,
|
|
@@ -817,10 +835,11 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
817
835
|
throw new OpenChamberControlError(resolvedDirectory.error, resolvedDirectory.status || 400);
|
|
818
836
|
}
|
|
819
837
|
|
|
820
|
-
const
|
|
838
|
+
const pullRequestInput = resolvePullRequestInput(payload);
|
|
839
|
+
const worktreeInput = pullRequestInput ? null : resolveWorktreeInput(payload);
|
|
821
840
|
let worktree = null;
|
|
822
841
|
let sessionDirectory = resolvedDirectory.directory;
|
|
823
|
-
if (payload?.worktree && !worktreeInput) {
|
|
842
|
+
if (payload?.worktree && !worktreeInput && !pullRequestInput) {
|
|
824
843
|
throw new OpenChamberControlError('worktree.name is required when worktree is provided', 400);
|
|
825
844
|
}
|
|
826
845
|
|
|
@@ -835,7 +854,25 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
835
854
|
});
|
|
836
855
|
}
|
|
837
856
|
|
|
838
|
-
if (
|
|
857
|
+
if (pullRequestInput) {
|
|
858
|
+
// Never a plain branch worktree instead: without the contributor
|
|
859
|
+
// pipeline the request is refused.
|
|
860
|
+
if (!(createChangeRequestWorktree instanceof Function)
|
|
861
|
+
|| !(worktreeBootstrapStore?.read instanceof Function)
|
|
862
|
+
|| !(worktreeBootstrapStore?.write instanceof Function)) {
|
|
863
|
+
throw new OpenChamberControlError('Worktrees from pull requests are not available here', 501);
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
worktree = await createChangeRequestWorktree({ directory: resolvedDirectory.directory, ...pullRequestInput });
|
|
867
|
+
} catch (error) {
|
|
868
|
+
throw asControlError(error, `Failed to create a worktree from pull request ${pullRequestInput.number}`);
|
|
869
|
+
}
|
|
870
|
+
sessionDirectory = worktree.path;
|
|
871
|
+
await waitForWorktreeBootstrapReady({
|
|
872
|
+
directory: sessionDirectory,
|
|
873
|
+
bootstrapStore: worktreeBootstrapStore,
|
|
874
|
+
});
|
|
875
|
+
} else if (worktreeInput) {
|
|
839
876
|
if (!(hydrateWorktreeCheckout instanceof Function)
|
|
840
877
|
|| !(worktreeBootstrapStore?.read instanceof Function)
|
|
841
878
|
|| !(worktreeBootstrapStore?.write instanceof Function)) {
|
|
@@ -974,8 +1011,6 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
974
1011
|
});
|
|
975
1012
|
}
|
|
976
1013
|
|
|
977
|
-
await writeMetadata(targetSessionID, { openchamber: { adopted: true } }, directory);
|
|
978
|
-
|
|
979
1014
|
const baselineAssistantMessageId = await latestCompletedAssistantMessageID({
|
|
980
1015
|
client,
|
|
981
1016
|
sessionID: targetSessionID,
|
|
@@ -466,7 +466,6 @@ describe('openchamber session routes', () => {
|
|
|
466
466
|
expect(response.body.promptDispatched).toBe(false);
|
|
467
467
|
expect(sessionCreateMock).toHaveBeenCalledWith({
|
|
468
468
|
location: { directory: '/repo/app' },
|
|
469
|
-
metadata: { openchamber: { adopted: true } },
|
|
470
469
|
title: 'Side task',
|
|
471
470
|
});
|
|
472
471
|
});
|
|
@@ -884,6 +883,70 @@ describe('openchamber session routes', () => {
|
|
|
884
883
|
expect(sessionPromptMock).not.toHaveBeenCalled();
|
|
885
884
|
});
|
|
886
885
|
|
|
886
|
+
it('starts the session in a worktree made from a pull request', async () => {
|
|
887
|
+
const createChangeRequestWorktree = vi.fn(async () => ({
|
|
888
|
+
name: 'feature-fix', branch: 'feature/fix', path: '/repo/worktrees/feature-fix',
|
|
889
|
+
provenance: { kind: 'contributor-fork' },
|
|
890
|
+
}));
|
|
891
|
+
const { app } = createApp({ createChangeRequestWorktree });
|
|
892
|
+
const response = await request(app)
|
|
893
|
+
.post('/api/openchamber/sessions')
|
|
894
|
+
.send({ directory: '/repo/app', pullRequest: 42, worktree: { name: 'review-42' }, prompt: 'Review it', model: 'openai/gpt-5.5' })
|
|
895
|
+
.expect(200);
|
|
896
|
+
|
|
897
|
+
expect(createChangeRequestWorktree).toHaveBeenCalledExactlyOnceWith({ directory: '/repo/app', number: 42, name: 'review-42' });
|
|
898
|
+
expect(createWorktreeMock).not.toHaveBeenCalled();
|
|
899
|
+
expect(getWorktreeBootstrapStatusMock.mock.invocationCallOrder[0])
|
|
900
|
+
.toBeLessThan(sessionCreateMock.mock.invocationCallOrder[0]);
|
|
901
|
+
expect(sessionCreateMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
902
|
+
location: { directory: '/repo/worktrees/feature-fix' },
|
|
903
|
+
}));
|
|
904
|
+
expect(response.body.directory).toBe('/repo/worktrees/feature-fix');
|
|
905
|
+
expect(response.body.worktree.branch).toBe('feature/fix');
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('refuses a pull request it cannot make a worktree for and creates no session', async () => {
|
|
909
|
+
const notFound = Object.assign(new Error('Pull request #42 was not found in acme/app'), { status: 404 });
|
|
910
|
+
const { app } = createApp({ createChangeRequestWorktree: vi.fn(async () => { throw notFound; }) });
|
|
911
|
+
|
|
912
|
+
await request(app)
|
|
913
|
+
.post('/api/openchamber/sessions')
|
|
914
|
+
.send({ directory: '/repo/app', pullRequest: 42, prompt: 'Review it' })
|
|
915
|
+
.expect(404, { error: 'Pull request #42 was not found in acme/app' });
|
|
916
|
+
|
|
917
|
+
expect(sessionCreateMock).not.toHaveBeenCalled();
|
|
918
|
+
expect(getWorktreeBootstrapStatusMock).not.toHaveBeenCalled();
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('refuses a pull request where contributor worktrees are not served, with no plain worktree instead', async () => {
|
|
922
|
+
const { app } = createApp();
|
|
923
|
+
|
|
924
|
+
await request(app)
|
|
925
|
+
.post('/api/openchamber/sessions')
|
|
926
|
+
.send({ directory: '/repo/app', pullRequest: 42 })
|
|
927
|
+
.expect(501, { error: 'Worktrees from pull requests are not available here' });
|
|
928
|
+
|
|
929
|
+
expect(createWorktreeMock).not.toHaveBeenCalled();
|
|
930
|
+
expect(sessionCreateMock).not.toHaveBeenCalled();
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it('rejects a malformed pull request number or a start ref beside it', async () => {
|
|
934
|
+
const createChangeRequestWorktree = vi.fn();
|
|
935
|
+
const { app } = createApp({ createChangeRequestWorktree });
|
|
936
|
+
|
|
937
|
+
await request(app)
|
|
938
|
+
.post('/api/openchamber/sessions')
|
|
939
|
+
.send({ directory: '/repo/app', pullRequest: '#42' })
|
|
940
|
+
.expect(400, { error: 'pullRequest must be a pull or merge request number, such as 42' });
|
|
941
|
+
await request(app)
|
|
942
|
+
.post('/api/openchamber/sessions')
|
|
943
|
+
.send({ directory: '/repo/app', pullRequest: 42, worktree: { startRef: 'main' } })
|
|
944
|
+
.expect(400, { error: 'pullRequest checks out the pull request\'s own branch; do not combine it with startRef or setUpstream' });
|
|
945
|
+
|
|
946
|
+
expect(createChangeRequestWorktree).not.toHaveBeenCalled();
|
|
947
|
+
expect(sessionCreateMock).not.toHaveBeenCalled();
|
|
948
|
+
});
|
|
949
|
+
|
|
887
950
|
it('does not treat a missing bootstrap record as ready', async () => {
|
|
888
951
|
getWorktreeBootstrapStatusMock
|
|
889
952
|
.mockResolvedValueOnce(null)
|
|
@@ -1080,7 +1143,7 @@ describe('openchamber session routes', () => {
|
|
|
1080
1143
|
.send({ directory: '/repo/app', prompt: 'Carry on', model: 'openai/gpt-5.5', agent: 'build' })
|
|
1081
1144
|
.expect(200);
|
|
1082
1145
|
|
|
1083
|
-
expect(sessionMetadataStore.entries.get('ses_fork')).toEqual({ openchamber: {
|
|
1146
|
+
expect(sessionMetadataStore.entries.get('ses_fork')).toEqual({ openchamber: { assist: { recap: 'kept' } } });
|
|
1084
1147
|
});
|
|
1085
1148
|
|
|
1086
1149
|
it('rejects send and fork requests without a prompt before calling OpenCode', async () => {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* so nothing is lost if a migration turns out wrong.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { asNonEmptyString } from '../shared/guards.js';
|
|
24
25
|
import fsDefault from 'node:fs';
|
|
25
26
|
import pathDefault from 'node:path';
|
|
26
27
|
|
|
@@ -28,12 +29,6 @@ import { createOpenCodeClient as createOpenCodeClientDefault } from './opencode-
|
|
|
28
29
|
|
|
29
30
|
const LEGACY_FILE_NAME = 'sessions-metadata.json';
|
|
30
31
|
|
|
31
|
-
const asNonEmptyString = (value) => {
|
|
32
|
-
if (typeof value !== 'string') return null;
|
|
33
|
-
const trimmed = value.trim();
|
|
34
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
32
|
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
38
33
|
|
|
39
34
|
/**
|
|
@@ -11,7 +11,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
11
11
|
- `packages/web/server/lib/opencode/cli-entry-runtime.js`: CLI entrypoint runtime that detects direct execution, parses CLI options, and starts server bootstrap.
|
|
12
12
|
- `packages/web/server/lib/opencode/routes.js`: OpenCode/provider settings and auth-related route registration.
|
|
13
13
|
- `packages/web/server/lib/opencode/v1-migration-topup.js`: re-arms OpenCode's own V1 -> V2 session import for V1 sessions changed by 1.x after the last completed import; runs only before a managed spawn. See "v1-migration-topup.js" below.
|
|
14
|
-
- `packages/web/server/lib/opencode/lifecycle.js`: OpenCode process lifecycle runtime (startup, restart, readiness, health monitoring). After readiness it warms the last-used directory only (first entry of the `getWarmupDirectories` dep, best-effort) because OpenCode initializes each directory lazily on first request and that cost would otherwise be paid by the user's first interactive session open. It warms no other projects: on OpenCode 2 the first directory-scoped read boots that location's whole MCP fleet. The warmed directory is also `getDefaultOpenCodeDirectory()`, the scope for server-side reads that have no directory of their own (integrations in `auth.js`, the small model's model and provider lists, the agent check after a restart): v2 answers a location read without one for its own working directory, the user's home for a managed OpenCode, and would start an MCP fleet there. The getter revalidates that directory on every read: once it is no longer a readable directory (deleted, moved, replaced by a file, or stripped of read/execute permission) it is never sent, and the lifecycle advances to another directory the warm pass validated or re-reads `getWarmupDirectories` (coalesced, with a short cooldown) so a currently-valid `lastDirectory` replaces the stale one. A stale cached path would otherwise make every directory-scoped read answer `500 FileSystem.realPath ... ENOENT` — the failure that took the quota credential read down with it.
|
|
14
|
+
- `packages/web/server/lib/opencode/lifecycle.js`: OpenCode process lifecycle runtime (startup, restart, readiness, health monitoring). After readiness it warms the last-used directory only (first entry of the `getWarmupDirectories` dep, best-effort) because OpenCode initializes each directory lazily on first request and that cost would otherwise be paid by the user's first interactive session open. It warms no other projects: on OpenCode 2 the first directory-scoped read boots that location's whole MCP fleet. The warmed directory is also `getDefaultOpenCodeDirectory()`, the scope for server-side reads that have no directory of their own (integrations in `auth.js`, the small model's model and provider lists, the agent check after a restart): v2 answers a location read without one for its own working directory, the user's home for a managed OpenCode, and would start an MCP fleet there. While no warmed directory is usable (no project open), the getter answers the `noProjectDirectory` dep instead, the managed chats root, which the UI also works in then. The getter revalidates that directory on every read: once it is no longer a readable directory (deleted, moved, replaced by a file, or stripped of read/execute permission) it is never sent, and the lifecycle advances to another directory the warm pass validated or re-reads `getWarmupDirectories` (coalesced, with a short cooldown) so a currently-valid `lastDirectory` replaces the stale one. A stale cached path would otherwise make every directory-scoped read answer `500 FileSystem.realPath ... ENOENT` — the failure that took the quota credential read down with it.
|
|
15
15
|
- The managed OpenCode's environment also carries the user's variables from Settings and from `opencode service set env` (`getUserEnvironment`, see `../environment`), over the inherited environment and under everything OpenChamber sets for OpenCode itself; enterprise mode passes none. `getManagedOpenCodeProcessEnv()` therefore includes them, so a provider key given this way is visible to the credential reader like any other variable OpenCode reports.
|
|
16
16
|
- `packages/web/server/lib/opencode/provider-env-aliases.js`: mirrors known provider credential env aliases into the managed OpenCode process environment (for example `GEMINI_API_KEY` → `GOOGLE_GENERATIVE_AI_API_KEY`) so OpenCode connection detection and the upstream AI SDK agree on the same key names. Canonical implementation shared by web lifecycle and the VS Code managed spawn path (`packages/vscode/src/provider-env-aliases.ts` re-exports this module).
|
|
17
17
|
- `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
|
|
@@ -59,12 +59,12 @@ unless an output schema is declared; and a tool call no longer receives
|
|
|
59
59
|
and OpenChamber resolves the directory itself.
|
|
60
60
|
- `packages/web/server/lib/opencode/server-utils-runtime.js`: shared server runtime utilities for OpenCode proxy wiring, OpenCode port/readiness helpers, and snapshot fetchers.
|
|
61
61
|
- `packages/web/server/lib/opencode/openchamber-routes.js`: OpenChamber update and models metadata route registration.
|
|
62
|
-
- `packages/web/server/lib/opencode/model-discovery.js`: bounded custom-provider `/models` discovery and optional models.dev enrichment. It accepts only http(s), does not follow redirects, filters transport headers, never returns credentials, and treats models.dev failure as unenriched success. Edit requests may name the provider so the host can
|
|
62
|
+
- `packages/web/server/lib/opencode/model-discovery.js`: bounded custom-provider `/models` discovery and optional models.dev enrichment. It accepts only http(s), does not follow redirects, filters transport headers, never returns credentials, and treats models.dev failure as unenriched success. Edit requests may name the provider so the host can fall back to a stored credential when the form leaves the key blank — the key from OpenCode's credential store or, failing that, the provider's stored config entry (`settings.apiKey`, else the first `env` variable name that is set in the server environment) — but only while the form's base URL still matches the saved one, so a changed URL never receives the stored credential; an explicitly entered replacement key wins.
|
|
63
63
|
- `packages/web/server/lib/opencode/pwa-manifest-routes.js`: PWA manifest route registration with recent-session shortcut resolution and short-lived caching.
|
|
64
64
|
- `packages/web/server/lib/opencode/project-icon-routes.js`: project icon upload/read/discovery route registration and icon storage orchestration.
|
|
65
65
|
- `packages/web/server/lib/opencode/skill-routes.js`: route registration for skill config CRUD, supporting files, and skills catalog scan/install flows.
|
|
66
66
|
- `packages/web/server/lib/opencode/settings-runtime.js`: Settings persistence runtime (disk IO, migrations, normalization, project validation, and persisted update serialization).
|
|
67
|
-
- `packages/web/server/lib/opencode/settings-helpers.js`: Settings payload sanitization/format helpers runtime for response shaping and persisted merge prep.
|
|
67
|
+
- `packages/web/server/lib/opencode/settings-helpers.js`: Settings payload sanitization/format helpers runtime for response shaping and persisted merge prep. `lastDirectory: ''` is kept as written: the UI sends it when the last project is removed, and every reader (the warm-up, the legacy last-directory-to-project migration, request directory resolution) treats it as unset.
|
|
68
68
|
- `packages/web/server/lib/opencode/settings-normalization-runtime.js`: path/settings/tunnel normalization and sanitization helpers runtime used by settings/routes/config wiring.
|
|
69
69
|
- `packages/web/server/lib/opencode/theme-runtime.js`: custom theme JSON validation and theme directory loading runtime for settings utility routes.
|
|
70
70
|
|
|
@@ -138,7 +138,10 @@ exists in OpenCode's store.
|
|
|
138
138
|
- `readOpenCodeCredentials()`: each integration's active credential, keyed by
|
|
139
139
|
integration id (the provider id for providers), in the legacy `auth.json`
|
|
140
140
|
entry shape (`{ type: 'api', key }` /
|
|
141
|
-
`{ type: 'oauth', access, refresh, expires, accountId?, enterpriseUrl? }`).
|
|
141
|
+
`{ type: 'oauth', access, refresh, expires, accountId?, enterpriseUrl?, server?, orgID? }`).
|
|
142
|
+
The OAuth projection keeps only `server` and `orgID` from OpenCode Console
|
|
143
|
+
credential metadata, so quota providers can identify a Console sign-in
|
|
144
|
+
without carrying account names or emails.
|
|
142
145
|
- `getProviderAuth(providerId)`: that map's entry for one provider, or null.
|
|
143
146
|
- `projectCredentialEntries(entries)`: the wire-to-legacy projection.
|
|
144
147
|
- `projectEnvironmentKeys(integrations, environment)`: variable values for the env connections OpenCode reports, by integration id.
|
|
@@ -214,7 +217,7 @@ Hard rules, verified against v2.0.8 (the completion stamp against v2.0.16)
|
|
|
214
217
|
- `OPENCODE_CONFIG_DIR`, `AGENT_DIR`, `COMMAND_DIR`, `SKILL_DIR`, `CONFIG_FILE`: Path constants rooted at `$XDG_CONFIG_HOME/opencode` when `XDG_CONFIG_HOME` is non-empty, otherwise `~/.config/opencode`. These constants are evaluated when the module loads; no files are migrated. `OPENCODE_CONFIG` remains a separate explicit config-file path and is resolved at call time for the custom config layer; it does not replace the global config directory.
|
|
215
218
|
- `AGENT_SCOPE`, `COMMAND_SCOPE`, `SKILL_SCOPE`: Scope constants with USER and PROJECT values.
|
|
216
219
|
- `ensureDirs()`: Creates required OpenCode directories.
|
|
217
|
-
- `parseMdFile(filePath)`, `writeMdFile(filePath, frontmatter, body)`: Markdown file operations with YAML frontmatter.
|
|
220
|
+
- `parseMdFile(filePath)`, `parseMdFileAsync(filePath)`, `writeMdFile(filePath, frontmatter, body)`: Markdown file operations with YAML frontmatter.
|
|
218
221
|
- `getConfigPaths(workingDirectory)`, `readConfigLayers(workingDirectory)`, `readConfig(workingDirectory)`: Config file operations with layer merging (user, project, custom). `readConfigLayers` isolates `INVALID_JSONC` per layer: a broken file is omitted from the merge (`{}` for that layer only), recorded on `layerErrors`, and does not block valid sibling layers. Writes still refuse to overwrite the broken file.
|
|
219
222
|
- `readConfigFile(filePath)`: Reads one config file. Missing, whitespace-only, and comment-only files return `{}`; a comment-only file is recognized by `ValueExpected` being the only parse error. A `jsonc-parser` error that produces a partial or non-object tree throws `INVALID_JSONC` — partial parse trees must never be treated as authoritative (avoids rewriting a `$schema`-only stub over a full config). Content that yields no JSON value for any other reason (YAML, plain text) also throws instead of reading as empty.
|
|
220
223
|
- `readConfigLayer(filePath)`: Same parse as `readConfigFile`, but isolates `INVALID_JSONC` to `{ config: {}, error }` so plugin/MCP/agent readers can skip one broken layer without aborting valid siblings. Writes still refuse to overwrite the broken file.
|
|
@@ -223,10 +226,10 @@ Hard rules, verified against v2.0.8 (the completion stamp against v2.0.16)
|
|
|
223
226
|
- `getJsonWriteTarget(layers, preferredScope)`: Determines write target for config updates. Throws `INVALID_JSONC` when the chosen target file is the unparseable layer.
|
|
224
227
|
- `getAncestors(startDir, stopDir)`, `findWorktreeRoot(startDir)`: Git worktree helpers.
|
|
225
228
|
- `isPromptFileReference(value)`, `resolvePromptFilePath(reference)`, `writePromptFile(filePath, content)`: Prompt file reference handling.
|
|
226
|
-
- `walkSkillMdFiles(rootDir)`: Recursively finds all SKILL.md files.
|
|
227
|
-
- `addSkillFromMdFile(skillsMap, skillMdPath, scope, source)`:
|
|
229
|
+
- `walkSkillMdFiles(rootDir)`: Async. Recursively finds all SKILL.md files at any depth, following links and skipping a link back to its own ancestor. Sibling directories are read in parallel, at most 8 file-system calls at once, and the result keeps depth-first readdir order: discovery lets a later file win a duplicate name, so the order is part of the contract.
|
|
230
|
+
- `addSkillFromMdFile(skillsMap, skillMdPath, parsed, scope, source)`: Indexes an already parsed skill file; `parsed` null (unreadable file) adds nothing.
|
|
228
231
|
- `resolveSkillSearchDirectories(workingDirectory)`: Returns skill search path order (config, project, home, custom).
|
|
229
|
-
- `listSkillSupportingFiles(skillDir)
|
|
232
|
+
- `listSkillSupportingFiles(skillDir)` (async), `readSkillSupportingFile(skillDir, relativePath)`, `writeSkillSupportingFile(skillDir, relativePath, content)`, `deleteSkillSupportingFile(skillDir, relativePath)`: Skill supporting file management.
|
|
230
233
|
|
|
231
234
|
## Public exports (routes.js)
|
|
232
235
|
- `registerOpenCodeRoutes(app, dependencies)`: Registers OpenCode-owned HTTP routes and internal module runtime:
|
|
@@ -349,6 +352,14 @@ OpenChamber tool injection. Managed launch env strips AppImage `ARGV0` before
|
|
|
349
352
|
spawn so zsh-backed OpenCode tools do not rewrite child argv[0] to the AppImage
|
|
350
353
|
path (#2588).
|
|
351
354
|
|
|
355
|
+
Everything the lifecycle and `auth-state-runtime.js` set for OpenCode goes
|
|
356
|
+
through `assignInjectedEnv` (`../injected-env.js`), which records the names of
|
|
357
|
+
the variables it changed in `OPENCHAMBER_INJECTED_ENV`. The agent's shell
|
|
358
|
+
inherits that record, so `openchamber startup enable` run there leaves the
|
|
359
|
+
managed config and password out of the service while the user's own
|
|
360
|
+
`OPENCODE_*` exports carry over (#4604). A value the environment already had is
|
|
361
|
+
not recorded: a user-provided password passed through stays the user's.
|
|
362
|
+
|
|
352
363
|
Before spawn, `applyProviderEnvAliases` fills unset Google credential aliases
|
|
353
364
|
from any present sibling (`GOOGLE_GENERATIVE_AI_API_KEY`, `GOOGLE_API_KEY`,
|
|
354
365
|
`GEMINI_API_KEY`) so a shell that only exports `GEMINI_API_KEY` still satisfies
|
|
@@ -882,6 +893,9 @@ within a ten-minute overall deadline.
|
|
|
882
893
|
- Skills config CRUD and metadata under `/api/config/skills*`
|
|
883
894
|
- Skill rename via `PATCH /api/config/skills/:name` with `{ renameTo }` (directory rename preserves `SKILL.md` body and supporting files; restricted to managed skill roots under `.opencode/skills|skill`, `.claude/skills`, and `.agents/skills`)
|
|
884
895
|
- Skill list responses include authoritative `renamable` derived from the same managed-root policy used by rename
|
|
896
|
+
- `GET /api/config/skills` leaves `sources.md.supportingFiles` out: listing every file of every skill cost seconds when a skill bundles a `.venv` or `node_modules`, and no list consumer reads it. `GET /api/config/skills/:name` still returns it
|
|
897
|
+
- Discovery and skill reads are async (`fs.promises`), and the list enriches one skill at a time, so a large skills tree no longer stalls other requests on the server's event loop
|
|
898
|
+
- Create, edit and rename run one at a time per process, so an existence check cannot go stale before its own write; create also writes `SKILL.md` with `wx` and reports an existing file as "already exists" instead of overwriting it
|
|
885
899
|
- `disableModelInvocation` (detail `sources.md`, create/update body) is "run only when called": it writes both `disable-model-invocation: true` and `metadata.opencode/autoinvoke: false` so every supported OpenCode 2.x and Claude Code honour it, reads back the way OpenCode resolves the pair (`opencode/autoinvoke` wins), and clearing it removes both keys while keeping other `metadata`. The VS Code runtime mirrors this in `opencodeConfig.ts`
|
|
886
900
|
- Skills catalog listing/source pagination, scan, and install routes
|
|
887
901
|
- Supporting skill file read/write/delete routes
|
|
@@ -539,7 +539,69 @@ function deleteAgent(agentName, workingDirectory, scope) {
|
|
|
539
539
|
throw new Error(`Agent ${agentName} is built-in or not deletable`);
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
+
// ============== DISABLED ==============
|
|
543
|
+
|
|
544
|
+
/** Agent ids under one agents directory, nested ones as `group/name`. */
|
|
545
|
+
function collectAgentIds(dir, prefix, ids) {
|
|
546
|
+
let entries;
|
|
547
|
+
try {
|
|
548
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
549
|
+
} catch {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
for (const entry of entries) {
|
|
553
|
+
if (entry.isDirectory()) {
|
|
554
|
+
collectAgentIds(path.join(dir, entry.name), `${prefix}${entry.name}/`, ids);
|
|
555
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
556
|
+
ids.add(`${prefix}${entry.name.slice(0, -3)}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Agents switched off with `disabled: true`. OpenCode drops them from its
|
|
563
|
+
* agent list, so Settings learns about them here to offer turning them back
|
|
564
|
+
* on. Every agent defined in a file or a config section is read the way the
|
|
565
|
+
* editor reads it and kept when its own config says disabled.
|
|
566
|
+
*/
|
|
567
|
+
function listDisabledAgents(workingDirectory) {
|
|
568
|
+
const lookupCache = createAgentLookupCache();
|
|
569
|
+
const names = new Set();
|
|
570
|
+
|
|
571
|
+
buildUserAgentIndex(lookupCache);
|
|
572
|
+
for (const name of lookupCache.userAgentIndexByName.keys()) names.add(name);
|
|
573
|
+
|
|
574
|
+
if (workingDirectory) {
|
|
575
|
+
const worktreeRoot = findWorktreeRoot(workingDirectory) || path.resolve(workingDirectory);
|
|
576
|
+
for (const base of getAncestors(workingDirectory, worktreeRoot)) {
|
|
577
|
+
for (const dirName of PROJECT_AGENT_DIR_NAMES) {
|
|
578
|
+
collectAgentIds(path.join(base, '.opencode', dirName), '', names);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const { mergedConfig } = readConfigLayers(workingDirectory);
|
|
584
|
+
for (const sectionKey of ['agents', 'agent', 'mode']) {
|
|
585
|
+
const section = mergedConfig?.[sectionKey];
|
|
586
|
+
if (isRecord(section)) for (const name of Object.keys(section)) names.add(name);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const disabled = [];
|
|
590
|
+
for (const name of [...names].sort((a, b) => a.localeCompare(b))) {
|
|
591
|
+
const entry = getAgentConfig(name, workingDirectory, lookupCache);
|
|
592
|
+
if (entry.config.disabled !== true) continue;
|
|
593
|
+
disabled.push({
|
|
594
|
+
name,
|
|
595
|
+
scope: entry.scope,
|
|
596
|
+
path: entry.path,
|
|
597
|
+
...(typeof entry.config.description === 'string' ? { description: entry.config.description } : {}),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return disabled;
|
|
601
|
+
}
|
|
602
|
+
|
|
542
603
|
export {
|
|
604
|
+
listDisabledAgents,
|
|
543
605
|
getAgentSources,
|
|
544
606
|
getAgentConfig,
|
|
545
607
|
getAgentPermissions,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { assignInjectedEnv } from '../injected-env.js';
|
|
2
|
+
|
|
1
3
|
export const createOpenCodeAuthStateRuntime = (dependencies) => {
|
|
2
4
|
const {
|
|
3
5
|
crypto,
|
|
@@ -39,7 +41,7 @@ export const createOpenCodeAuthStateRuntime = (dependencies) => {
|
|
|
39
41
|
|
|
40
42
|
setAuthPassword(normalized);
|
|
41
43
|
setAuthSource(source);
|
|
42
|
-
process.env
|
|
44
|
+
assignInjectedEnv(process.env, { OPENCODE_SERVER_PASSWORD: normalized });
|
|
43
45
|
syncToHmrState();
|
|
44
46
|
return normalized;
|
|
45
47
|
};
|
|
@@ -2,7 +2,7 @@ import type { CredentialEntry, IntegrationInfo, JsonValue } from '@opencode/clie
|
|
|
2
2
|
|
|
3
3
|
export type LegacyAuthEntry =
|
|
4
4
|
| { type: 'api'; key: string; metadata?: { [key: string]: JsonValue } }
|
|
5
|
-
| { type: 'oauth'; access: string; refresh: string; expires: number; accountId?: string; enterpriseUrl?: string };
|
|
5
|
+
| { type: 'oauth'; access: string; refresh: string; expires: number; accountId?: string; enterpriseUrl?: string; server?: string; orgID?: string };
|
|
6
6
|
export type LegacyAuthFile = Record<string, LegacyAuthEntry>;
|
|
7
7
|
|
|
8
8
|
/** Where credentials come from: the running OpenCode in production, a fixture in tests. */
|
|
@@ -93,8 +93,16 @@ const projectCredentialValue = (value) => {
|
|
|
93
93
|
const entry = { type: 'oauth', access: value.access, refresh: value.refresh, expires: value.expires };
|
|
94
94
|
const accountId = metadataString.safeParse(value.metadata?.accountID).data;
|
|
95
95
|
const enterpriseUrl = metadataString.safeParse(value.metadata?.enterpriseUrl).data;
|
|
96
|
+
// OpenCode Console signs in through the `opencode` integration and records the
|
|
97
|
+
// Console server and selected organization on the credential. Only these two
|
|
98
|
+
// travel on: the rest of the metadata (email, account name) is not needed by
|
|
99
|
+
// any consumer, and account identifiers stay out of the read path.
|
|
100
|
+
const server = metadataString.safeParse(value.metadata?.server).data;
|
|
101
|
+
const orgID = metadataString.safeParse(value.metadata?.orgID).data;
|
|
96
102
|
if (accountId !== undefined) entry.accountId = accountId;
|
|
97
103
|
if (enterpriseUrl !== undefined) entry.enterpriseUrl = enterpriseUrl;
|
|
104
|
+
if (server !== undefined) entry.server = server;
|
|
105
|
+
if (orgID !== undefined) entry.orgID = orgID;
|
|
98
106
|
return entry;
|
|
99
107
|
};
|
|
100
108
|
|