@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mapGitLabCI, mapGitLabDiff, mapGitLabIssue, mapGitLabMergeRequest, mapGitLabNote, mapGitLabProject } from './mappers.js';
|
|
1
|
+
import { mapGitLabCI, mapGitLabCommit, mapGitLabDiff, mapGitLabIssue, mapGitLabMergeRequest, mapGitLabNote, mapGitLabProject, mapGitLabUser, mapGitLabVerdictNote } from './mappers.js';
|
|
2
2
|
import { resolveGitLabProjectsFromDirectory } from './repo.js';
|
|
3
3
|
import { isPlainObject, isString } from './validation.js';
|
|
4
4
|
|
|
@@ -14,6 +14,11 @@ const PER_PAGE = 20;
|
|
|
14
14
|
// end within this bound; past it the read fails instead of passing a capped
|
|
15
15
|
// list off as complete.
|
|
16
16
|
const COLLECTION_PER_PAGE = 100;
|
|
17
|
+
// List filters: the picker's names to GitLab's.
|
|
18
|
+
const LIST_STATES = { open: 'opened', closed: 'closed', merged: 'merged', all: 'all' };
|
|
19
|
+
const LIST_SCOPES = { any: 'all', assigned: 'assigned_to_me', created: 'created_by_me' };
|
|
20
|
+
// The preview's timeline shows the newest commits only.
|
|
21
|
+
const TIMELINE_COMMIT_LIMIT = 50;
|
|
17
22
|
const COLLECTION_MAX_PAGES = 20;
|
|
18
23
|
|
|
19
24
|
function requireMapped(value, message) {
|
|
@@ -197,9 +202,30 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
|
|
|
197
202
|
};
|
|
198
203
|
const constrainedProject = async (...args) => (await constrainedProjectNetwork(...args)).target;
|
|
199
204
|
|
|
205
|
+
// The account's own user, read once: review requests are filtered by name.
|
|
206
|
+
let currentUsername = null;
|
|
207
|
+
const readCurrentUsername = async () => {
|
|
208
|
+
currentUsername ??= client.Users.showCurrentUser().then((user) => {
|
|
209
|
+
if (!isPlainObject(user) || !isString(user.username) || !user.username.trim()) throw new Error('GitLab returned an invalid current user');
|
|
210
|
+
return user.username;
|
|
211
|
+
});
|
|
212
|
+
return currentUsername;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/** A list's state and whose items, as GitLab's list parameters. */
|
|
216
|
+
const listFilter = async (options, kind) => {
|
|
217
|
+
const state = LIST_STATES[options.state] ?? 'opened';
|
|
218
|
+
const people = options.people ?? 'any';
|
|
219
|
+
if (people === 'reviewRequested') {
|
|
220
|
+
return kind === 'pull' ? { state, scope: 'all', reviewerUsername: await readCurrentUsername() } : { state, scope: 'all' };
|
|
221
|
+
}
|
|
222
|
+
return { state: kind === 'issue' && state === 'merged' ? 'closed' : state, scope: LIST_SCOPES[people] ?? 'all' };
|
|
223
|
+
};
|
|
224
|
+
|
|
200
225
|
const listProjectMRs = (projectId, options) => client.MergeRequests.all({
|
|
201
226
|
projectId,
|
|
202
|
-
scope: 'all',
|
|
227
|
+
scope: options.scope ?? 'all',
|
|
228
|
+
reviewerUsername: options.reviewerUsername,
|
|
203
229
|
maxPages: 1,
|
|
204
230
|
perPage: options.perPage ?? 100,
|
|
205
231
|
page: options.page ?? 1,
|
|
@@ -391,6 +417,36 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
|
|
|
391
417
|
};
|
|
392
418
|
};
|
|
393
419
|
|
|
420
|
+
const postNote = async (create) => {
|
|
421
|
+
const raw = await create();
|
|
422
|
+
if (!isPlainObject(raw) || !Number.isInteger(raw.id)) throw new Error('GitLab returned an invalid note');
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Requesting changes exists only in GitLab's GraphQL API. The path is
|
|
426
|
+
// resolved against the REST base (/api/v4/), so it reaches /api/graphql
|
|
427
|
+
// through the same credential and no-redirect requester.
|
|
428
|
+
const REQUEST_CHANGES_MUTATION = `mutation($path: ID!, $iid: String!) {
|
|
429
|
+
mergeRequestRequestChanges(input: { projectPath: $path, iid: $iid }) {
|
|
430
|
+
mergeRequest { iid }
|
|
431
|
+
errors
|
|
432
|
+
}
|
|
433
|
+
}`;
|
|
434
|
+
const requestChanges = async (project, number) => {
|
|
435
|
+
const response = await client.MergeRequests.requester.post('../graphql', {
|
|
436
|
+
body: { query: REQUEST_CHANGES_MUTATION, variables: { path: `${project.owner}/${project.name}`, iid: String(number) } },
|
|
437
|
+
});
|
|
438
|
+
const payload = response?.body;
|
|
439
|
+
const result = payload?.data?.mergeRequestRequestChanges;
|
|
440
|
+
const errors = [
|
|
441
|
+
...(Array.isArray(payload?.errors) ? payload.errors.map((error) => error?.message) : []),
|
|
442
|
+
...(Array.isArray(result?.errors) ? result.errors : []),
|
|
443
|
+
].filter((message) => isString(message) && message.trim());
|
|
444
|
+
// GraphQL answers a refusal (not a reviewer, an older GitLab) with 200
|
|
445
|
+
// and errors: nothing was changed.
|
|
446
|
+
if (errors.length) throw Object.assign(new Error(errors.join('; ')), { status: 422 });
|
|
447
|
+
if (!isPlainObject(result?.mergeRequest)) throw new Error('GitLab returned an invalid request-changes result');
|
|
448
|
+
};
|
|
449
|
+
|
|
394
450
|
const resolveMutationActionTarget = async (payload) => {
|
|
395
451
|
if (payload.providerTarget) {
|
|
396
452
|
return {
|
|
@@ -432,6 +488,149 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
|
|
|
432
488
|
|
|
433
489
|
resolveChangeRequestMutation: readMutationRequest,
|
|
434
490
|
|
|
491
|
+
async resolveIssueMutation(context, expected) {
|
|
492
|
+
if (expected.head !== undefined || expected.base !== undefined || expected.headSha !== undefined) {
|
|
493
|
+
throw Object.assign(new Error('GitLab issue target is invalid'), { code: 'INVALID_SOURCE_CONTROL_MUTATION_CONTEXT', status: 400 });
|
|
494
|
+
}
|
|
495
|
+
const target = await constrainedProject(context.directory, expected.project, context.primaryRemote);
|
|
496
|
+
const issue = requireIssue(await client.Issues.show(expected.number, { projectId: target.project.id }), target.project);
|
|
497
|
+
if (issue.number !== expected.number) throw new Error('GitLab returned an invalid issue');
|
|
498
|
+
return {
|
|
499
|
+
providerTarget: { projectId: target.project.id, number: issue.number },
|
|
500
|
+
target: resolvedMutationTarget(context, target.project, { number: issue.number }),
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
/** Close or reopen; GitLab refuses to reopen a merged merge request. */
|
|
505
|
+
async setChangeRequestState(payload) {
|
|
506
|
+
const { projectId, number } = payload.providerTarget;
|
|
507
|
+
const raw = await client.MergeRequests.edit(projectId, number, { stateEvent: payload.state === 'closed' ? 'close' : 'reopen' });
|
|
508
|
+
const request = requireMutationResponse(raw, payload.targetProject, payload.providerTarget, payload.expectedTarget);
|
|
509
|
+
if (request.state !== payload.state) throw new Error('GitLab returned an unexpected merge request state');
|
|
510
|
+
return { state: request.state };
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
async setIssueState(payload) {
|
|
514
|
+
const { projectId, number } = payload.providerTarget;
|
|
515
|
+
const raw = await client.Issues.edit(projectId, number, { stateEvent: payload.state === 'closed' ? 'close' : 'reopen' });
|
|
516
|
+
const issue = requireIssue(raw, payload.targetProject);
|
|
517
|
+
if (issue.number !== number || issue.state !== payload.state) throw new Error('GitLab returned an unexpected issue state');
|
|
518
|
+
return { state: issue.state };
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
/** One page of the project's labels, for the board's picker. */
|
|
522
|
+
async listLabels(directory, selector, remote) {
|
|
523
|
+
const target = await constrainedProject(directory, selector, remote);
|
|
524
|
+
const raw = await client.ProjectLabels.all(target.project.id, { perPage: 100, maxPages: 1 });
|
|
525
|
+
if (!Array.isArray(raw)) throw new Error('GitLab returned an invalid label list');
|
|
526
|
+
return raw.flatMap((label) => {
|
|
527
|
+
if (!isPlainObject(label) || !isString(label.name) || !label.name) return [];
|
|
528
|
+
return [isString(label.color) && label.color ? { name: label.name, color: label.color.replace(/^#/, '') } : { name: label.name }];
|
|
529
|
+
});
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
/** One page of the project's active members, inherited ones too: who can be asked to review. */
|
|
533
|
+
async listReviewerCandidates(directory, selector, remote) {
|
|
534
|
+
const target = await constrainedProject(directory, selector, remote);
|
|
535
|
+
const raw = await client.ProjectMembers.all(target.project.id, { includeInherited: true, perPage: 100, maxPages: 1 });
|
|
536
|
+
if (!Array.isArray(raw)) throw new Error('GitLab returned an invalid member list');
|
|
537
|
+
return raw.flatMap((member) => {
|
|
538
|
+
const user = member?.state === 'active' ? mapGitLabUser(member, identity) : null;
|
|
539
|
+
if (!user) return [];
|
|
540
|
+
return [user.avatarUrl ? { id: user.id, login: user.username, avatarUrl: user.avatarUrl } : { id: user.id, login: user.username }];
|
|
541
|
+
});
|
|
542
|
+
},
|
|
543
|
+
|
|
544
|
+
/** The whole label set; an empty one clears them. */
|
|
545
|
+
async setLabels(payload) {
|
|
546
|
+
const { projectId, number } = payload.providerTarget;
|
|
547
|
+
const update = { labels: payload.labels.join(',') };
|
|
548
|
+
const raw = payload.kind === 'issue-labels'
|
|
549
|
+
? await client.Issues.edit(projectId, number, update)
|
|
550
|
+
: await client.MergeRequests.edit(projectId, number, update);
|
|
551
|
+
if (!isPlainObject(raw) || raw.iid !== number) throw new Error('GitLab returned an invalid edit result');
|
|
552
|
+
return {};
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
/** The whole reviewer set, by user id; an empty one removes them all. */
|
|
556
|
+
async setReviewers(payload) {
|
|
557
|
+
const { projectId, number } = payload.providerTarget;
|
|
558
|
+
const raw = await client.MergeRequests.edit(projectId, number, { reviewerIds: payload.reviewers.map(Number) });
|
|
559
|
+
requireMutationResponse(raw, payload.targetProject, payload.providerTarget, payload.expectedTarget);
|
|
560
|
+
return {};
|
|
561
|
+
},
|
|
562
|
+
|
|
563
|
+
/** Only a match is certain: another edit since may have moved the set either way. */
|
|
564
|
+
async reconcileSetMutation(providerTarget, kind, names) {
|
|
565
|
+
const { projectId, number } = providerTarget;
|
|
566
|
+
const raw = kind === 'issue-labels'
|
|
567
|
+
? await client.Issues.show(number, { projectId })
|
|
568
|
+
: await client.MergeRequests.show(projectId, number);
|
|
569
|
+
const current = kind === 'change-request-reviewers'
|
|
570
|
+
? (Array.isArray(raw?.reviewers) ? raw.reviewers.map((user) => String(user?.id)) : null)
|
|
571
|
+
: (Array.isArray(raw?.labels) ? raw.labels.map((label) => (isString(label) ? label : label?.name)) : null);
|
|
572
|
+
if (!current || current.length !== names.length || !current.every((entry) => names.includes(entry))) return { state: 'outcome-unknown' };
|
|
573
|
+
return { state: 'succeeded', result: {} };
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
async reconcileStateMutation(providerTarget, kind, state) {
|
|
577
|
+
const { projectId, number } = providerTarget;
|
|
578
|
+
const project = (await showProject(projectId)).project;
|
|
579
|
+
const current = kind === 'issue-state'
|
|
580
|
+
? requireIssue(await client.Issues.show(number, { projectId }), project).state
|
|
581
|
+
: requireMutationMergeRequest(await client.MergeRequests.show(projectId, number), project).state;
|
|
582
|
+
if (current === state) return { state: 'succeeded', result: { state } };
|
|
583
|
+
if (current === 'open' || current === 'closed') {
|
|
584
|
+
return { state: 'failed', result: { failureStatus: 409, failureCode: 'SOURCE_CONTROL_MUTATION_NOT_APPLIED' } };
|
|
585
|
+
}
|
|
586
|
+
return { state: 'outcome-unknown' };
|
|
587
|
+
},
|
|
588
|
+
|
|
589
|
+
commentChangeRequest(payload) {
|
|
590
|
+
const { projectId, number } = payload.providerTarget;
|
|
591
|
+
return postNote(() => client.MergeRequestNotes.create(projectId, number, payload.body));
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
commentIssue(payload) {
|
|
595
|
+
const { projectId, number } = payload.providerTarget;
|
|
596
|
+
return postNote(() => client.IssueNotes.create(projectId, number, payload.body));
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* The verdict first, then its text as a note: GitLab has no review that
|
|
601
|
+
* carries both. A verdict that lands with a note that does not is still
|
|
602
|
+
* a verdict, reported as `commented: false` so the text is not lost.
|
|
603
|
+
*/
|
|
604
|
+
async reviewChangeRequest(payload) {
|
|
605
|
+
const { projectId, number } = payload.providerTarget;
|
|
606
|
+
if (payload.verdict === 'approve') {
|
|
607
|
+
try {
|
|
608
|
+
// The approval is for the commit the user read.
|
|
609
|
+
await client.MergeRequestApprovals.approve(projectId, number, { sha: payload.expectedTarget.headSha });
|
|
610
|
+
} catch (error) {
|
|
611
|
+
const status = requestStatus(error);
|
|
612
|
+
if (status === 409) {
|
|
613
|
+
throw Object.assign(new Error('GitLab merge request headSha changed'), {
|
|
614
|
+
code: 'SOURCE_CONTROL_MUTATION_TARGET_MISMATCH', status: 409,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
// GitLab answers an approval it does not allow (your own merge
|
|
618
|
+
// request, one already approved) with 401; the account is fine.
|
|
619
|
+
if (status === 401) throw Object.assign(new Error('GitLab did not allow this approval'), { status: 403 });
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
await requestChanges(payload.targetProject, number);
|
|
624
|
+
}
|
|
625
|
+
if (payload.body === undefined) return { commented: false };
|
|
626
|
+
try {
|
|
627
|
+
await postNote(() => client.MergeRequestNotes.create(projectId, number, payload.body));
|
|
628
|
+
return { commented: true };
|
|
629
|
+
} catch {
|
|
630
|
+
return { commented: false };
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
|
|
435
634
|
async reconcileCreateMutation(providerTarget, target) {
|
|
436
635
|
const payload = readMergeRequestList(await listProjectMRs(providerTarget.targetProjectId, {
|
|
437
636
|
state: 'all', sourceBranch: target.head, perPage: 100,
|
|
@@ -598,7 +797,7 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
|
|
|
598
797
|
const targets = await expandTargets([resolved.projects[0]], { requireComplete: canonicalReads });
|
|
599
798
|
const target = targets.at(-1);
|
|
600
799
|
const page = readPage(await listProjectMRs(target.project.id, {
|
|
601
|
-
|
|
800
|
+
...await listFilter(options, 'pull'), page: options.page ?? 1, perPage: PER_PAGE, search: options.query, showExpanded: true,
|
|
602
801
|
}));
|
|
603
802
|
const rawItems = readMergeRequestList(page.items, target.project);
|
|
604
803
|
const items = rawItems.map((raw) => canonicalReads
|
|
@@ -612,7 +811,7 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
|
|
|
612
811
|
constrainToPrimary: options.constrainToPrimary,
|
|
613
812
|
remote: options.remote,
|
|
614
813
|
});
|
|
615
|
-
const [notes, diffs, ci, headProject] = await Promise.all([
|
|
814
|
+
const [notes, diffs, ci, headProject, commitPage] = await Promise.all([
|
|
616
815
|
readCollection((page) => client.MergeRequestNotes.all(target.project.id, number, page), 'GitLab merge request has more notes than OpenChamber reads'),
|
|
617
816
|
options.includeDiff
|
|
618
817
|
? readCollection((page) => client.MergeRequests.allDiffs(target.project.id, number, page), 'GitLab merge request has more changed files than OpenChamber reads')
|
|
@@ -622,6 +821,11 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
|
|
|
622
821
|
Number.isInteger(raw?.source_project_id) && raw.source_project_id !== target.raw?.id
|
|
623
822
|
? showProject(raw.source_project_id).then((item) => item.project)
|
|
624
823
|
: Promise.resolve(null),
|
|
824
|
+
// The newest commits, newest first as GitLab lists them; only the
|
|
825
|
+
// preview's timeline asks.
|
|
826
|
+
options.includeTimeline
|
|
827
|
+
? client.MergeRequests.allCommits(target.project.id, number, { maxPages: 1, perPage: TIMELINE_COMMIT_LIMIT, showExpanded: true }).then(readPage)
|
|
828
|
+
: Promise.resolve(null),
|
|
625
829
|
]);
|
|
626
830
|
if (canonicalReads && !Array.isArray(notes)) throw new Error('GitLab returned an invalid merge request note list');
|
|
627
831
|
if (canonicalReads && !Array.isArray(diffs)) throw new Error('GitLab returned an invalid merge request diff list');
|
|
@@ -652,7 +856,7 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
|
|
|
652
856
|
? requireMergeRequest(raw, target.project)
|
|
653
857
|
: requireMapped(mapGitLabMergeRequest(raw, identity, target.project), 'GitLab returned an invalid merge request');
|
|
654
858
|
if (headProject) changeRequest.headProject = headProject;
|
|
655
|
-
|
|
859
|
+
const context = {
|
|
656
860
|
identity,
|
|
657
861
|
project: target.project,
|
|
658
862
|
changeRequest,
|
|
@@ -662,13 +866,33 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
|
|
|
662
866
|
diff: files.map((file) => file.patch).filter(Boolean).join('\n'),
|
|
663
867
|
ci,
|
|
664
868
|
};
|
|
869
|
+
if (!options.includeTimeline) return context;
|
|
870
|
+
if (canonicalReads && !Array.isArray(commitPage?.items)) throw new Error('GitLab returned an invalid merge request commit list');
|
|
871
|
+
return {
|
|
872
|
+
...context,
|
|
873
|
+
commits: asArray(commitPage?.items).flatMap((commit) => {
|
|
874
|
+
const mapped = mapGitLabCommit(commit);
|
|
875
|
+
return mapped ? [mapped] : [];
|
|
876
|
+
}).reverse(),
|
|
877
|
+
// More than one page means the list above is only the newest ones.
|
|
878
|
+
commitsComplete: !commitPage?.hasMore,
|
|
879
|
+
// Who is asked to review, for the preview's reviewer picker.
|
|
880
|
+
reviewers: asArray(raw.reviewers).flatMap((user) => {
|
|
881
|
+
const mapped = mapGitLabUser(user, identity);
|
|
882
|
+
return mapped ? [mapped] : [];
|
|
883
|
+
}),
|
|
884
|
+
verdicts: asArray(notes).flatMap((note) => {
|
|
885
|
+
const mapped = mapGitLabVerdictNote(note, identity, raw.web_url);
|
|
886
|
+
return mapped ? [mapped] : [];
|
|
887
|
+
}),
|
|
888
|
+
};
|
|
665
889
|
},
|
|
666
890
|
|
|
667
891
|
async listIssues(directory, options = {}) {
|
|
668
892
|
const resolved = await resolveDirectoryProjects(directory, options.remote);
|
|
669
893
|
const target = resolved.projects[0];
|
|
670
894
|
const page = readPage(await client.Issues.all({
|
|
671
|
-
projectId: target.project.id,
|
|
895
|
+
projectId: target.project.id, ...await listFilter(options, 'issue'), page: options.page ?? 1,
|
|
672
896
|
perPage: PER_PAGE, maxPages: 1, search: options.query, showExpanded: true,
|
|
673
897
|
}));
|
|
674
898
|
if (canonicalReads && !Array.isArray(page.items)) throw new Error('GitLab returned an invalid issue list');
|
|
@@ -86,6 +86,52 @@ describe('GitLab resource service', () => {
|
|
|
86
86
|
await expect(result).rejects.toThrow('invalid merge request');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
it('lists by state and by whose items, reading the user once for review requests', async () => {
|
|
90
|
+
const all = list([openMR]);
|
|
91
|
+
const showCurrentUser = vi.fn(async () => ({ id: 3, username: 'sam' }));
|
|
92
|
+
const issuesAll = list([]);
|
|
93
|
+
const { service } = setup({
|
|
94
|
+
MergeRequests: { ...setup().client.MergeRequests, all },
|
|
95
|
+
Users: { showCurrentUser },
|
|
96
|
+
Issues: { ...setup().client.Issues, all: issuesAll },
|
|
97
|
+
});
|
|
98
|
+
await service.listChangeRequests('/repo', { state: 'merged', people: 'created' });
|
|
99
|
+
expect(all.mock.calls.at(-1)?.[0]).toMatchObject({ state: 'merged', scope: 'created_by_me' });
|
|
100
|
+
await service.listChangeRequests('/repo', { state: 'all', people: 'reviewRequested' });
|
|
101
|
+
await service.listChangeRequests('/repo', { people: 'reviewRequested' });
|
|
102
|
+
expect(all.mock.calls.at(-1)?.[0]).toMatchObject({ state: 'opened', scope: 'all', reviewerUsername: 'sam' });
|
|
103
|
+
expect(showCurrentUser).toHaveBeenCalledTimes(1);
|
|
104
|
+
await service.listIssues('/repo', { state: 'merged', people: 'assigned' });
|
|
105
|
+
expect(issuesAll.mock.calls.at(-1)?.[0]).toMatchObject({ state: 'closed', scope: 'assigned_to_me' });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('adds commits and review verdicts only when the timeline asks', async () => {
|
|
109
|
+
const commits = [
|
|
110
|
+
{ id: 'bbb', title: 'second', author_name: 'Sam', committed_date: '2026-10-02T10:00:00Z', web_url: `${origin}/team/repo/-/commit/bbb` },
|
|
111
|
+
{ id: 'aaa', title: 'first', author_name: 'Sam', committed_date: '2026-10-01T10:00:00Z', web_url: `${origin}/team/repo/-/commit/aaa` },
|
|
112
|
+
];
|
|
113
|
+
const notes = [
|
|
114
|
+
{ id: 4, body: 'Looks good', author: { id: 3, username: 'sam' } },
|
|
115
|
+
{ id: 5, body: 'approved this merge request', system: true, created_at: '2026-10-02T11:00:00Z', author: { id: 3, username: 'sam' } },
|
|
116
|
+
{ id: 6, body: 'added 1 commit', system: true, author: { id: 3, username: 'sam' } },
|
|
117
|
+
];
|
|
118
|
+
const allCommits = list(commits);
|
|
119
|
+
const { service } = setup({
|
|
120
|
+
MergeRequests: { ...setup().client.MergeRequests, allCommits, show: vi.fn(async () => ({ ...openMR, reviewers: [{ id: 7, username: 'ann' }, { username: 'no-id' }] })) },
|
|
121
|
+
MergeRequestNotes: { all: list(notes) },
|
|
122
|
+
});
|
|
123
|
+
const plain = await service.changeRequestContext('/repo', 5, {});
|
|
124
|
+
expect(plain).not.toHaveProperty('commits');
|
|
125
|
+
expect(allCommits).not.toHaveBeenCalled();
|
|
126
|
+
|
|
127
|
+
const result = await service.changeRequestContext('/repo', 5, { includeTimeline: true });
|
|
128
|
+
expect(result.commits.map((commit) => commit.headline)).toEqual(['first', 'second']);
|
|
129
|
+
expect(result.commitsComplete).toBe(true);
|
|
130
|
+
expect(result.verdicts).toMatchObject([{ state: 'approved', createdAt: '2026-10-02T11:00:00Z', author: { username: 'sam' } }]);
|
|
131
|
+
expect(result.issueComments.map((comment) => comment.body)).toEqual(['Looks good']);
|
|
132
|
+
expect(result.reviewers).toEqual([expect.objectContaining({ id: '7', username: 'ann' })]);
|
|
133
|
+
});
|
|
134
|
+
|
|
89
135
|
it('keeps merge-request context when CI jobs fail', async () => {
|
|
90
136
|
const { service } = setup({ Jobs: { all: vi.fn(async () => { throw new Error('jobs unavailable'); }) } });
|
|
91
137
|
const result = await service.changeRequestContext('/repo', 5, { includeDiff: true, includeCIDetails: true });
|
|
@@ -545,6 +591,139 @@ describe('GitLab resource service', () => {
|
|
|
545
591
|
});
|
|
546
592
|
});
|
|
547
593
|
|
|
594
|
+
describe('GitLab comments and reviews', () => {
|
|
595
|
+
const review = (overrides = {}) => ({
|
|
596
|
+
providerTarget: { projectId: '2', number: 5 },
|
|
597
|
+
targetProject: { id: '2', owner: 'team', name: 'repo' },
|
|
598
|
+
expectedTarget: { project: { id: '2', owner: 'team', name: 'repo' }, number: 5, headSha: 'abc' },
|
|
599
|
+
verdict: 'approve',
|
|
600
|
+
...overrides,
|
|
601
|
+
});
|
|
602
|
+
const notes = (create) => ({ MergeRequestNotes: { ...setup().client.MergeRequestNotes, create } });
|
|
603
|
+
|
|
604
|
+
it('approves the commit the user read, then posts the text as a note', async () => {
|
|
605
|
+
const approve = vi.fn(async () => ({}));
|
|
606
|
+
const create = vi.fn(async () => ({ id: 7 }));
|
|
607
|
+
const { service } = setup({ MergeRequestApprovals: { approve }, ...notes(create) });
|
|
608
|
+
|
|
609
|
+
await expect(service.reviewChangeRequest(review({ body: 'Nice' }))).resolves.toEqual({ commented: true });
|
|
610
|
+
await expect(service.reviewChangeRequest(review())).resolves.toEqual({ commented: false });
|
|
611
|
+
|
|
612
|
+
expect(approve).toHaveBeenCalledWith('2', 5, { sha: 'abc' });
|
|
613
|
+
expect(create).toHaveBeenCalledOnce();
|
|
614
|
+
expect(create).toHaveBeenCalledWith('2', 5, 'Nice');
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('keeps the verdict when its note fails and says the text did not post', async () => {
|
|
618
|
+
const create = vi.fn(async () => { throw Object.assign(new Error('locked'), { cause: { response: { status: 403 } } }); });
|
|
619
|
+
const { service } = setup({ MergeRequestApprovals: { approve: vi.fn(async () => ({})) }, ...notes(create) });
|
|
620
|
+
|
|
621
|
+
await expect(service.reviewChangeRequest(review({ body: 'Nice' }))).resolves.toEqual({ commented: false });
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('turns an approval GitLab does not allow into a refusal, not a lost sign-in', async () => {
|
|
625
|
+
const refused = Object.assign(new Error('401 Unauthorized'), { cause: { response: { status: 401 } } });
|
|
626
|
+
const moved = Object.assign(new Error('SHA does not match'), { cause: { response: { status: 409 } } });
|
|
627
|
+
const own = setup({ MergeRequestApprovals: { approve: vi.fn(async () => { throw refused; }) } });
|
|
628
|
+
const late = setup({ MergeRequestApprovals: { approve: vi.fn(async () => { throw moved; }) } });
|
|
629
|
+
|
|
630
|
+
await expect(own.service.reviewChangeRequest(review())).rejects.toMatchObject({ status: 403 });
|
|
631
|
+
await expect(late.service.reviewChangeRequest(review())).rejects.toMatchObject({ code: 'SOURCE_CONTROL_MUTATION_TARGET_MISMATCH', status: 409 });
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('requests changes through GraphQL and reports its errors as a refusal', async () => {
|
|
635
|
+
const post = vi.fn(async () => ({ body: { data: { mergeRequestRequestChanges: { mergeRequest: { iid: '5' }, errors: [] } } } }));
|
|
636
|
+
const { service } = setup({ MergeRequests: { ...setup().client.MergeRequests, requester: { post } } });
|
|
637
|
+
|
|
638
|
+
await expect(service.reviewChangeRequest(review({ verdict: 'request-changes' }))).resolves.toEqual({ commented: false });
|
|
639
|
+
expect(post).toHaveBeenCalledWith('../graphql', {
|
|
640
|
+
body: { query: expect.stringContaining('mergeRequestRequestChanges'), variables: { path: 'team/repo', iid: '5' } },
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
post.mockResolvedValueOnce({ body: { data: { mergeRequestRequestChanges: { mergeRequest: null, errors: ['Reviewer not found'] } } } });
|
|
644
|
+
await expect(service.reviewChangeRequest(review({ verdict: 'request-changes' }))).rejects.toMatchObject({ message: 'Reviewer not found', status: 422 });
|
|
645
|
+
post.mockResolvedValueOnce({ body: { data: null, errors: [{ message: "Field 'mergeRequestRequestChanges' doesn't exist" }] } });
|
|
646
|
+
await expect(service.reviewChangeRequest(review({ verdict: 'request-changes' }))).rejects.toMatchObject({ status: 422 });
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('reads labels and active members, and sets labels and reviewers as whole sets', async () => {
|
|
650
|
+
const editMR = vi.fn(async (_id, _iid, update) => ({ ...openMR, labels: update.labels?.split(',') ?? [] }));
|
|
651
|
+
const editIssue = vi.fn(async () => ({ iid: 3, title: 'Bug', web_url: `${origin}/me/repo/-/issues/3`, state: 'opened' }));
|
|
652
|
+
const { service } = setup({
|
|
653
|
+
MergeRequests: { ...setup().client.MergeRequests, edit: editMR, show: vi.fn(async () => ({ ...openMR, labels: ['bug'], reviewers: [{ id: 7, username: 'ann' }] })) },
|
|
654
|
+
Issues: { ...setup().client.Issues, edit: editIssue },
|
|
655
|
+
ProjectLabels: { all: vi.fn(async () => [{ name: 'bug', color: '#d73a4a' }, { name: '' }]) },
|
|
656
|
+
ProjectMembers: { all: vi.fn(async () => [
|
|
657
|
+
{ id: 7, username: 'ann', avatar_url: `${origin}/a.png`, state: 'active' },
|
|
658
|
+
{ id: 8, username: 'gone', state: 'blocked' },
|
|
659
|
+
]) },
|
|
660
|
+
});
|
|
661
|
+
const project = { id: '2', owner: 'team', name: 'repo' };
|
|
662
|
+
|
|
663
|
+
await expect(service.listLabels('/repo', { owner: 'team', name: 'repo' })).resolves.toEqual([{ name: 'bug', color: 'd73a4a' }]);
|
|
664
|
+
await expect(service.listReviewerCandidates('/repo', { owner: 'team', name: 'repo' }))
|
|
665
|
+
.resolves.toEqual([{ id: '7', login: 'ann', avatarUrl: `${origin}/a.png` }]);
|
|
666
|
+
|
|
667
|
+
await service.setLabels({ kind: 'change-request-labels', labels: ['bug', 'docs'], providerTarget: { projectId: '2', number: 5 } });
|
|
668
|
+
await service.setLabels({ kind: 'issue-labels', labels: [], providerTarget: { projectId: '1', number: 3 } });
|
|
669
|
+
await service.setReviewers({
|
|
670
|
+
reviewers: ['7'], providerTarget: { projectId: '2', number: 5 }, targetProject: project, expectedTarget: { project, number: 5 },
|
|
671
|
+
});
|
|
672
|
+
expect(editMR).toHaveBeenNthCalledWith(1, '2', 5, { labels: 'bug,docs' });
|
|
673
|
+
expect(editIssue).toHaveBeenCalledWith('1', 3, { labels: '' });
|
|
674
|
+
expect(editMR).toHaveBeenNthCalledWith(2, '2', 5, { reviewerIds: [7] });
|
|
675
|
+
|
|
676
|
+
await expect(service.reconcileSetMutation({ projectId: '2', number: 5 }, 'change-request-labels', ['bug'])).resolves.toMatchObject({ state: 'succeeded' });
|
|
677
|
+
await expect(service.reconcileSetMutation({ projectId: '2', number: 5 }, 'change-request-reviewers', ['7'])).resolves.toMatchObject({ state: 'succeeded' });
|
|
678
|
+
await expect(service.reconcileSetMutation({ projectId: '2', number: 5 }, 'change-request-labels', ['docs'])).resolves.toEqual({ state: 'outcome-unknown' });
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('closes and reopens through stateEvent and reconciles from the current state', async () => {
|
|
682
|
+
const editMR = vi.fn(async () => ({ ...openMR, state: 'closed' }));
|
|
683
|
+
const editIssue = vi.fn(async () => ({ iid: 3, title: 'Bug', web_url: `${origin}/me/repo/-/issues/3`, state: 'opened' }));
|
|
684
|
+
const { service } = setup({
|
|
685
|
+
MergeRequests: { ...setup().client.MergeRequests, edit: editMR },
|
|
686
|
+
Issues: { ...setup().client.Issues, edit: editIssue },
|
|
687
|
+
});
|
|
688
|
+
const project = { id: '2', owner: 'team', name: 'repo' };
|
|
689
|
+
|
|
690
|
+
await expect(service.setChangeRequestState({
|
|
691
|
+
state: 'closed', providerTarget: { projectId: '2', number: 5 }, targetProject: project, expectedTarget: { project, number: 5 },
|
|
692
|
+
})).resolves.toEqual({ state: 'closed' });
|
|
693
|
+
await expect(service.setIssueState({
|
|
694
|
+
state: 'open', providerTarget: { projectId: '1', number: 3 }, targetProject: { id: '1', owner: 'me', name: 'repo' },
|
|
695
|
+
})).resolves.toEqual({ state: 'open' });
|
|
696
|
+
expect(editMR).toHaveBeenCalledWith('2', 5, { stateEvent: 'close' });
|
|
697
|
+
expect(editIssue).toHaveBeenCalledWith('1', 3, { stateEvent: 'reopen' });
|
|
698
|
+
|
|
699
|
+
await expect(service.reconcileStateMutation({ projectId: '2', number: 5 }, 'change-request-state', 'open'))
|
|
700
|
+
.resolves.toEqual({ state: 'succeeded', result: { state: 'open' } });
|
|
701
|
+
await expect(service.reconcileStateMutation({ projectId: '1', number: 3 }, 'issue-state', 'closed'))
|
|
702
|
+
.resolves.toMatchObject({ state: 'failed' });
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('comments on merge requests and on issues in the bound project', async () => {
|
|
706
|
+
const mrNote = vi.fn(async () => ({ id: 8 }));
|
|
707
|
+
const issueNote = vi.fn(async () => ({ id: 9 }));
|
|
708
|
+
const { service, client } = setup({ ...notes(mrNote), IssueNotes: { ...setup().client.IssueNotes, create: issueNote } });
|
|
709
|
+
const context = { directory: '/repo', repositoryId: 'repo_one', bindingRevision: 3, primaryRemote: 'origin' };
|
|
710
|
+
|
|
711
|
+
const resolved = await service.resolveIssueMutation(context, { project: { owner: 'me', name: 'repo' }, number: 3 });
|
|
712
|
+
await service.commentIssue({ providerTarget: resolved.providerTarget, body: 'On it' });
|
|
713
|
+
await service.commentChangeRequest({ providerTarget: { projectId: '2', number: 5 }, body: 'Looks good' });
|
|
714
|
+
|
|
715
|
+
expect(resolved).toEqual({
|
|
716
|
+
providerTarget: { projectId: '1', number: 3 },
|
|
717
|
+
target: { repositoryId: 'repo_one', bindingRevision: 3, primaryRemote: 'origin', project: { id: '1', owner: 'me', name: 'repo' }, number: 3 },
|
|
718
|
+
});
|
|
719
|
+
expect(client.Issues.show).toHaveBeenCalledWith(3, { projectId: '1' });
|
|
720
|
+
expect(issueNote).toHaveBeenCalledWith('1', 3, 'On it');
|
|
721
|
+
expect(mrNote).toHaveBeenCalledWith('2', 5, 'Looks good');
|
|
722
|
+
await expect(service.resolveIssueMutation(context, { project: { owner: 'me', name: 'repo' }, number: 3, headSha: 'abc' }))
|
|
723
|
+
.rejects.toMatchObject({ code: 'INVALID_SOURCE_CONTROL_MUTATION_CONTEXT' });
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
548
727
|
describe('GitLab merge request comparison reads', () => {
|
|
549
728
|
const diffRefs = { base_sha: 'a'.repeat(40), head_sha: 'b'.repeat(40), start_sha: 'c'.repeat(40) };
|
|
550
729
|
|