@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
|
@@ -316,10 +316,42 @@ const callRead = async (handler, query) => {
|
|
|
316
316
|
return res;
|
|
317
317
|
};
|
|
318
318
|
|
|
319
|
+
const createStreamingResponse = () => {
|
|
320
|
+
const chunks = [];
|
|
321
|
+
const headers = new Map();
|
|
322
|
+
let statusCode = 200;
|
|
323
|
+
const res = new Writable({
|
|
324
|
+
write(chunk, _encoding, callback) {
|
|
325
|
+
chunks.push(Buffer.from(chunk));
|
|
326
|
+
callback();
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
Object.assign(res, {
|
|
330
|
+
status(code) { statusCode = code; return res; },
|
|
331
|
+
json(payload) { chunks.push(Buffer.from(JSON.stringify(payload))); return res; },
|
|
332
|
+
type() { return res; },
|
|
333
|
+
send(payload) { chunks.push(Buffer.from(payload)); return res; },
|
|
334
|
+
setHeader(name, value) { headers.set(name.toLowerCase(), value); return res; },
|
|
335
|
+
getHeader(name) { return headers.get(name.toLowerCase()); },
|
|
336
|
+
});
|
|
337
|
+
return {
|
|
338
|
+
res,
|
|
339
|
+
finished: new Promise((resolve) => res.on('finish', resolve)),
|
|
340
|
+
get statusCode() { return statusCode; },
|
|
341
|
+
get body() { return Buffer.concat(chunks).toString('utf8'); },
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// File bytes as `fs/promises` hands them to the raw route: an open handle that streams a span.
|
|
346
|
+
const openBytes = (bytes) => vi.fn(async () => ({
|
|
347
|
+
createReadStream: ({ start, end }) => Readable.from([bytes.subarray(start, end + 1)]),
|
|
348
|
+
}));
|
|
349
|
+
|
|
319
350
|
const callRaw = async (handler, query) => {
|
|
320
|
-
const
|
|
321
|
-
await handler({ query }, res);
|
|
322
|
-
|
|
351
|
+
const response = createStreamingResponse();
|
|
352
|
+
await handler({ query }, response.res);
|
|
353
|
+
await response.finished;
|
|
354
|
+
return response;
|
|
323
355
|
};
|
|
324
356
|
|
|
325
357
|
const callMkdir = async (handler, body) => {
|
|
@@ -575,6 +607,81 @@ describe('fs write', () => {
|
|
|
575
607
|
});
|
|
576
608
|
});
|
|
577
609
|
|
|
610
|
+
describe('fs rename', () => {
|
|
611
|
+
const registerRename = (fsPromises) => {
|
|
612
|
+
const { app, getRoute } = createRouteRegistry();
|
|
613
|
+
registerFsRoutes(app, {
|
|
614
|
+
os: { homedir: () => '/home/user' },
|
|
615
|
+
path: path.posix,
|
|
616
|
+
fsPromises: {
|
|
617
|
+
realpath: async (targetPath) => targetPath,
|
|
618
|
+
...fsPromises,
|
|
619
|
+
},
|
|
620
|
+
spawn: vi.fn(),
|
|
621
|
+
crypto: { randomUUID: () => 'job-0' },
|
|
622
|
+
normalizeDirectoryPath: (p) => p,
|
|
623
|
+
resolveProjectDirectory: async () => ({ directory: '/repo' }),
|
|
624
|
+
buildAugmentedPath: () => '/usr/bin',
|
|
625
|
+
resolveGitBinaryForSpawn: () => 'git',
|
|
626
|
+
openchamberUserConfigRoot: '/home/user/.config',
|
|
627
|
+
});
|
|
628
|
+
return getRoute('POST', '/api/fs/rename');
|
|
629
|
+
};
|
|
630
|
+
const missing = () => Object.assign(new Error('missing'), { code: 'ENOENT' });
|
|
631
|
+
|
|
632
|
+
it('renames when the destination is free', async () => {
|
|
633
|
+
const fsPromises = {
|
|
634
|
+
lstat: vi.fn(async (targetPath) => {
|
|
635
|
+
if (targetPath === '/repo/a.txt') return { dev: 1, ino: 10 };
|
|
636
|
+
throw missing();
|
|
637
|
+
}),
|
|
638
|
+
rename: vi.fn(async () => undefined),
|
|
639
|
+
};
|
|
640
|
+
const res = createMockResponse();
|
|
641
|
+
await registerRename(fsPromises)({ body: { oldPath: '/repo/a.txt', newPath: '/repo/b.txt' } }, res);
|
|
642
|
+
|
|
643
|
+
expect(res.body).toEqual({ success: true, path: '/repo/b.txt' });
|
|
644
|
+
expect(fsPromises.rename).toHaveBeenCalledWith('/repo/a.txt', '/repo/b.txt');
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('returns a conflict instead of replacing an existing destination', async () => {
|
|
648
|
+
const fsPromises = {
|
|
649
|
+
lstat: vi.fn(async (targetPath) => (targetPath === '/repo/a.txt' ? { dev: 1, ino: 10 } : { dev: 1, ino: 20 })),
|
|
650
|
+
rename: vi.fn(async () => undefined),
|
|
651
|
+
};
|
|
652
|
+
const res = createMockResponse();
|
|
653
|
+
await registerRename(fsPromises)({ body: { oldPath: '/repo/a.txt', newPath: '/repo/b.txt' } }, res);
|
|
654
|
+
|
|
655
|
+
expect(res.statusCode).toBe(409);
|
|
656
|
+
expect(res.body).toEqual({ error: 'Destination path already exists', reason: 'already-exists' });
|
|
657
|
+
expect(fsPromises.rename).not.toHaveBeenCalled();
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('allows a case-only rename that resolves to the source itself', async () => {
|
|
661
|
+
const fsPromises = {
|
|
662
|
+
lstat: vi.fn(async () => ({ dev: 1, ino: 10 })),
|
|
663
|
+
rename: vi.fn(async () => undefined),
|
|
664
|
+
};
|
|
665
|
+
const res = createMockResponse();
|
|
666
|
+
await registerRename(fsPromises)({ body: { oldPath: '/repo/Readme.md', newPath: '/repo/README.md' } }, res);
|
|
667
|
+
|
|
668
|
+
expect(res.body).toEqual({ success: true, path: '/repo/README.md' });
|
|
669
|
+
expect(fsPromises.rename).toHaveBeenCalledWith('/repo/Readme.md', '/repo/README.md');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('reports a missing source as not found', async () => {
|
|
673
|
+
const fsPromises = {
|
|
674
|
+
lstat: vi.fn(async () => { throw missing(); }),
|
|
675
|
+
rename: vi.fn(async () => undefined),
|
|
676
|
+
};
|
|
677
|
+
const res = createMockResponse();
|
|
678
|
+
await registerRename(fsPromises)({ body: { oldPath: '/repo/a.txt', newPath: '/repo/b.txt' } }, res);
|
|
679
|
+
|
|
680
|
+
expect(res.statusCode).toBe(404);
|
|
681
|
+
expect(fsPromises.rename).not.toHaveBeenCalled();
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
578
685
|
describe('fs upload', () => {
|
|
579
686
|
it('streams a binary file to temp storage before committing it without overwrite', async () => {
|
|
580
687
|
const write = vi.fn(async (_buffer, _offset, length) => ({ bytesWritten: length }));
|
|
@@ -820,7 +927,7 @@ describe('fs read', () => {
|
|
|
820
927
|
const fsPromises = {
|
|
821
928
|
realpath: vi.fn(async (targetPath) => targetPath),
|
|
822
929
|
stat: vi.fn(async () => ({ isFile: () => true, size: 6 })),
|
|
823
|
-
|
|
930
|
+
open: openBytes(Buffer.from('secret')),
|
|
824
931
|
};
|
|
825
932
|
const handler = registerRaw(fsPromises);
|
|
826
933
|
|
|
@@ -830,7 +937,7 @@ describe('fs read', () => {
|
|
|
830
937
|
});
|
|
831
938
|
|
|
832
939
|
expect(res.statusCode).toBe(200);
|
|
833
|
-
expect(res.getHeader('referrer-policy')).toBe('no-referrer');
|
|
940
|
+
expect(res.res.getHeader('referrer-policy')).toBe('no-referrer');
|
|
834
941
|
});
|
|
835
942
|
|
|
836
943
|
it('stats outside files without a grant', async () => {
|
|
@@ -1217,36 +1324,8 @@ describe('fs exec git-read cache', () => {
|
|
|
1217
1324
|
});
|
|
1218
1325
|
|
|
1219
1326
|
describe('fs raw byte ranges', () => {
|
|
1220
|
-
const createStreamingResponse = () => {
|
|
1221
|
-
const chunks = [];
|
|
1222
|
-
const headers = new Map();
|
|
1223
|
-
let statusCode = 200;
|
|
1224
|
-
const res = new Writable({
|
|
1225
|
-
write(chunk, _encoding, callback) {
|
|
1226
|
-
chunks.push(Buffer.from(chunk));
|
|
1227
|
-
callback();
|
|
1228
|
-
},
|
|
1229
|
-
});
|
|
1230
|
-
Object.assign(res, {
|
|
1231
|
-
status(code) { statusCode = code; return res; },
|
|
1232
|
-
json(payload) { chunks.push(Buffer.from(JSON.stringify(payload))); return res; },
|
|
1233
|
-
type() { return res; },
|
|
1234
|
-
send(payload) { chunks.push(Buffer.from(payload)); return res; },
|
|
1235
|
-
setHeader(name, value) { headers.set(name.toLowerCase(), value); return res; },
|
|
1236
|
-
getHeader(name) { return headers.get(name.toLowerCase()); },
|
|
1237
|
-
});
|
|
1238
|
-
return {
|
|
1239
|
-
res,
|
|
1240
|
-
finished: new Promise((resolve) => res.on('finish', resolve)),
|
|
1241
|
-
get statusCode() { return statusCode; },
|
|
1242
|
-
get body() { return Buffer.concat(chunks).toString('utf8'); },
|
|
1243
|
-
};
|
|
1244
|
-
};
|
|
1245
|
-
|
|
1246
1327
|
const registerRawWithFile = (bytes) => {
|
|
1247
|
-
const open =
|
|
1248
|
-
createReadStream: ({ start, end }) => Readable.from([bytes.subarray(start, end + 1)]),
|
|
1249
|
-
}));
|
|
1328
|
+
const open = openBytes(bytes);
|
|
1250
1329
|
const readFile = vi.fn(async () => bytes);
|
|
1251
1330
|
const handler = registerRaw({
|
|
1252
1331
|
stat: async () => ({ isFile: () => true, size: bytes.length }),
|
|
@@ -1264,7 +1343,7 @@ describe('fs raw byte ranges', () => {
|
|
|
1264
1343
|
await response.finished;
|
|
1265
1344
|
|
|
1266
1345
|
expect(response.statusCode).toBe(206);
|
|
1267
|
-
expect(response.
|
|
1346
|
+
expect(response.res.getHeader('content-range')).toBe('bytes 3-9/10');
|
|
1268
1347
|
expect(response.res.getHeader('content-length')).toBe('7');
|
|
1269
1348
|
expect(response.res.getHeader('accept-ranges')).toBe('bytes');
|
|
1270
1349
|
expect(response.body).toBe('3456789');
|
|
@@ -1272,18 +1351,117 @@ describe('fs raw byte ranges', () => {
|
|
|
1272
1351
|
expect(readFile).not.toHaveBeenCalled();
|
|
1273
1352
|
});
|
|
1274
1353
|
|
|
1275
|
-
it('
|
|
1276
|
-
const { handler, open } = registerRawWithFile(Buffer.from('0123456789'));
|
|
1277
|
-
const
|
|
1354
|
+
it('streams the whole file, advertising ranges, when no span is asked for', async () => {
|
|
1355
|
+
const { handler, open, readFile } = registerRawWithFile(Buffer.from('0123456789'));
|
|
1356
|
+
const response = createStreamingResponse();
|
|
1278
1357
|
|
|
1279
|
-
await handler({ query: { path: '/repo/clip.mp4' }, headers: {} }, res);
|
|
1358
|
+
await handler({ query: { path: '/repo/clip.mp4' }, headers: {} }, response.res);
|
|
1359
|
+
await response.finished;
|
|
1280
1360
|
|
|
1281
|
-
expect(
|
|
1282
|
-
expect(res.getHeader('accept-ranges')).toBe('bytes');
|
|
1283
|
-
expect(res.
|
|
1361
|
+
expect(response.statusCode).toBe(200);
|
|
1362
|
+
expect(response.res.getHeader('accept-ranges')).toBe('bytes');
|
|
1363
|
+
expect(response.res.getHeader('content-length')).toBe('10');
|
|
1364
|
+
expect(response.res.getHeader('content-range')).toBeUndefined();
|
|
1365
|
+
expect(response.body).toBe('0123456789');
|
|
1366
|
+
expect(open).toHaveBeenCalledWith('/repo/clip.mp4', 'r');
|
|
1367
|
+
expect(readFile).not.toHaveBeenCalled();
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
it('answers an empty file without opening it', async () => {
|
|
1371
|
+
const { handler, open } = registerRawWithFile(Buffer.alloc(0));
|
|
1372
|
+
const response = createStreamingResponse();
|
|
1373
|
+
|
|
1374
|
+
await handler({ query: { path: '/repo/empty.txt' }, headers: {} }, response.res);
|
|
1375
|
+
await response.finished;
|
|
1376
|
+
|
|
1377
|
+
expect(response.statusCode).toBe(200);
|
|
1378
|
+
expect(response.res.getHeader('content-length')).toBe('0');
|
|
1379
|
+
expect(response.body).toBe('');
|
|
1284
1380
|
expect(open).not.toHaveBeenCalled();
|
|
1285
1381
|
});
|
|
1286
1382
|
|
|
1383
|
+
it.each(['complete', 'disconnect', 'disconnect while opening'])('closes the real file handle on %s', async (scenario) => {
|
|
1384
|
+
const root = await mkdtemp(path.join(tmpdir(), 'oc-raw-stream-'));
|
|
1385
|
+
const file = path.join(root, 'clip.mp4');
|
|
1386
|
+
await nativeFs.writeFile(file, Buffer.alloc(256 * 1024));
|
|
1387
|
+
let handle;
|
|
1388
|
+
let stream;
|
|
1389
|
+
let closed;
|
|
1390
|
+
let bytesWritten = 0;
|
|
1391
|
+
const res = new Writable({
|
|
1392
|
+
write(chunk, _encoding, callback) {
|
|
1393
|
+
bytesWritten += chunk.length;
|
|
1394
|
+
if (scenario === 'disconnect') {
|
|
1395
|
+
// Leave a pending write, as with a slow client that then goes away.
|
|
1396
|
+
res.destroy();
|
|
1397
|
+
} else {
|
|
1398
|
+
callback();
|
|
1399
|
+
}
|
|
1400
|
+
},
|
|
1401
|
+
});
|
|
1402
|
+
Object.assign(res, {
|
|
1403
|
+
status() { return res; },
|
|
1404
|
+
type() { return res; },
|
|
1405
|
+
setHeader() {},
|
|
1406
|
+
});
|
|
1407
|
+
const handler = registerRaw({
|
|
1408
|
+
realpath: async () => file,
|
|
1409
|
+
stat: async () => nativeFs.stat(file),
|
|
1410
|
+
open: async () => {
|
|
1411
|
+
handle = await nativeFs.open(file, 'r');
|
|
1412
|
+
const createReadStream = handle.createReadStream.bind(handle);
|
|
1413
|
+
vi.spyOn(handle, 'createReadStream').mockImplementation((options) => {
|
|
1414
|
+
stream = createReadStream(options);
|
|
1415
|
+
closed = new Promise((resolve, reject) => {
|
|
1416
|
+
const timer = setTimeout(() => reject(new Error('file stream did not close')), 1000);
|
|
1417
|
+
stream.once('close', () => {
|
|
1418
|
+
clearTimeout(timer);
|
|
1419
|
+
resolve();
|
|
1420
|
+
});
|
|
1421
|
+
});
|
|
1422
|
+
return stream;
|
|
1423
|
+
});
|
|
1424
|
+
if (scenario === 'disconnect while opening') res.destroy();
|
|
1425
|
+
return handle;
|
|
1426
|
+
},
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
try {
|
|
1430
|
+
await handler({ query: { path: '/repo/clip.mp4' }, headers: { range: 'bytes=0-' } }, res);
|
|
1431
|
+
await closed;
|
|
1432
|
+
expect(handle.fd).toBe(-1);
|
|
1433
|
+
expect(res.listenerCount('close')).toBe(0);
|
|
1434
|
+
if (scenario === 'complete') expect(bytesWritten).toBe(256 * 1024);
|
|
1435
|
+
if (scenario === 'disconnect') expect(bytesWritten).toBeGreaterThan(0);
|
|
1436
|
+
if (scenario === 'disconnect while opening') expect(bytesWritten).toBe(0);
|
|
1437
|
+
} finally {
|
|
1438
|
+
// Also clean up the descriptor when running against the broken version.
|
|
1439
|
+
stream?.destroy();
|
|
1440
|
+
if (handle) await handle.close();
|
|
1441
|
+
await rm(root, { recursive: true, force: true });
|
|
1442
|
+
}
|
|
1443
|
+
}, 2000);
|
|
1444
|
+
|
|
1445
|
+
it('closes the file handle when stream creation throws', async () => {
|
|
1446
|
+
const close = vi.fn(async () => {});
|
|
1447
|
+
const handler = registerRaw({
|
|
1448
|
+
stat: async () => ({ isFile: () => true, size: 10 }),
|
|
1449
|
+
open: async () => ({
|
|
1450
|
+
createReadStream: () => { throw new Error('stream setup failed'); },
|
|
1451
|
+
close,
|
|
1452
|
+
}),
|
|
1453
|
+
});
|
|
1454
|
+
const res = createMockResponse();
|
|
1455
|
+
const log = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1456
|
+
try {
|
|
1457
|
+
await handler({ query: { path: '/repo/clip.mp4' }, headers: { range: 'bytes=0-' } }, res);
|
|
1458
|
+
expect(close).toHaveBeenCalledOnce();
|
|
1459
|
+
expect(res.statusCode).toBe(500);
|
|
1460
|
+
} finally {
|
|
1461
|
+
log.mockRestore();
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1287
1465
|
it('rejects a span past the end with 416 and the file size', async () => {
|
|
1288
1466
|
const { handler, open } = registerRawWithFile(Buffer.from('0123456789'));
|
|
1289
1467
|
const res = createMockResponse();
|
|
@@ -1302,8 +1480,8 @@ describe('fs raw download Content-Disposition', () => {
|
|
|
1302
1480
|
it('uses RFC 5987 filename*= encoding for non-ASCII filenames on download', async () => {
|
|
1303
1481
|
const fsPromises = {
|
|
1304
1482
|
realpath: vi.fn(async (targetPath) => targetPath),
|
|
1305
|
-
stat: vi.fn(async () => ({ isFile: () => true, size:
|
|
1306
|
-
|
|
1483
|
+
stat: vi.fn(async () => ({ isFile: () => true, size: 7 })),
|
|
1484
|
+
open: openBytes(Buffer.from('content')),
|
|
1307
1485
|
};
|
|
1308
1486
|
const handler = registerRaw(fsPromises);
|
|
1309
1487
|
|
|
@@ -1313,7 +1491,7 @@ describe('fs raw download Content-Disposition', () => {
|
|
|
1313
1491
|
});
|
|
1314
1492
|
|
|
1315
1493
|
expect(res.statusCode).toBe(200);
|
|
1316
|
-
const cd = res.getHeader('content-disposition');
|
|
1494
|
+
const cd = res.res.getHeader('content-disposition');
|
|
1317
1495
|
expect(cd).toContain("filename*=UTF-8''");
|
|
1318
1496
|
expect(cd).toContain(encodeURIComponent('文件.txt'));
|
|
1319
1497
|
// ASCII fallback strips non-ASCII chars, leaving extension
|
|
@@ -1323,15 +1501,15 @@ describe('fs raw download Content-Disposition', () => {
|
|
|
1323
1501
|
it('uses plain filename for ASCII-only filenames on download', async () => {
|
|
1324
1502
|
const fsPromises = {
|
|
1325
1503
|
realpath: vi.fn(async (targetPath) => targetPath),
|
|
1326
|
-
stat: vi.fn(async () => ({ isFile: () => true, size:
|
|
1327
|
-
|
|
1504
|
+
stat: vi.fn(async () => ({ isFile: () => true, size: 7 })),
|
|
1505
|
+
open: openBytes(Buffer.from('content')),
|
|
1328
1506
|
};
|
|
1329
1507
|
const handler = registerRaw(fsPromises);
|
|
1330
1508
|
|
|
1331
1509
|
const res = await callRaw(handler, { path: '/repo/readme.txt', download: 'true' });
|
|
1332
1510
|
|
|
1333
1511
|
expect(res.statusCode).toBe(200);
|
|
1334
|
-
const cd = res.getHeader('content-disposition');
|
|
1512
|
+
const cd = res.res.getHeader('content-disposition');
|
|
1335
1513
|
expect(cd).toContain('filename="readme.txt"');
|
|
1336
1514
|
expect(cd).toContain("filename*=UTF-8''readme.txt");
|
|
1337
1515
|
});
|
|
@@ -1758,10 +1936,13 @@ describe('fs stat directory error handling', () => {
|
|
|
1758
1936
|
try {
|
|
1759
1937
|
await mkdir(path.join(directory, 'fs'));
|
|
1760
1938
|
await mkdir(path.join(directory, 'git'));
|
|
1939
|
+
await mkdir(path.join(directory, 'shared'));
|
|
1761
1940
|
await copyFile(new URL('./routes.js', import.meta.url), path.join(directory, 'fs/routes.mjs'));
|
|
1762
1941
|
await copyFile(new URL('./byte-range.js', import.meta.url), path.join(directory, 'fs/byte-range.js'));
|
|
1942
|
+
await copyFile(new URL('./workspace-file-names.js', import.meta.url), path.join(directory, 'fs/workspace-file-names.js'));
|
|
1763
1943
|
await copyFile(new URL('../path-realpath-cache.js', import.meta.url), path.join(directory, 'path-realpath-cache.js'));
|
|
1764
1944
|
await copyFile(new URL('../git/redaction.js', import.meta.url), path.join(directory, 'git/redaction.js'));
|
|
1945
|
+
await copyFile(new URL('../shared/guards.js', import.meta.url), path.join(directory, 'shared/guards.js'));
|
|
1765
1946
|
expect(() => execFileSync('node', [
|
|
1766
1947
|
'--input-type=module',
|
|
1767
1948
|
'--eval',
|
|
@@ -2142,3 +2323,52 @@ describe('fs html preview grants', () => {
|
|
|
2142
2323
|
expect((await mint('/etc/passwd')).statusCode).toBe(400);
|
|
2143
2324
|
});
|
|
2144
2325
|
});
|
|
2326
|
+
|
|
2327
|
+
describe('GET /api/fs/find-by-name', () => {
|
|
2328
|
+
let repo;
|
|
2329
|
+
beforeEach(async () => {
|
|
2330
|
+
repo = await mkdtemp(path.join(tmpdir(), 'oc-find-by-name-'));
|
|
2331
|
+
execFileSync('git', ['init', '-q'], { cwd: repo });
|
|
2332
|
+
await mkdir(path.join(repo, 'src', 'chat'), { recursive: true });
|
|
2333
|
+
await mkdir(path.join(repo, 'lib'), { recursive: true });
|
|
2334
|
+
await nativeFs.writeFile(path.join(repo, 'src', 'chat', 'Renderer.tsx'), 'x');
|
|
2335
|
+
await nativeFs.writeFile(path.join(repo, 'lib', 'util.ts'), 'x');
|
|
2336
|
+
await nativeFs.writeFile(path.join(repo, 'src', 'util.ts'), 'x');
|
|
2337
|
+
await nativeFs.writeFile(path.join(repo, 'README.md'), 'x');
|
|
2338
|
+
execFileSync('git', ['add', 'src/chat/Renderer.tsx', 'lib/util.ts'], { cwd: repo });
|
|
2339
|
+
});
|
|
2340
|
+
afterEach(async () => {
|
|
2341
|
+
await rm(repo, { recursive: true, force: true });
|
|
2342
|
+
});
|
|
2343
|
+
|
|
2344
|
+
it('finds tracked and untracked files by name anywhere in the workspace', async () => {
|
|
2345
|
+
const { spawn } = await import('node:child_process');
|
|
2346
|
+
const { app, getRoute } = createRouteRegistry();
|
|
2347
|
+
registerFsRoutes(app, {
|
|
2348
|
+
os: { homedir: () => '/home/user' },
|
|
2349
|
+
path,
|
|
2350
|
+
fsPromises: nativeFs,
|
|
2351
|
+
spawn,
|
|
2352
|
+
crypto: { randomUUID: () => 'job-0' },
|
|
2353
|
+
normalizeDirectoryPath: (p) => p,
|
|
2354
|
+
resolveProjectDirectory: async () => ({ directory: repo }),
|
|
2355
|
+
buildAugmentedPath: () => '/usr/bin',
|
|
2356
|
+
resolveGitBinaryForSpawn: () => 'git',
|
|
2357
|
+
openchamberUserConfigRoot: '/home/user/.config',
|
|
2358
|
+
});
|
|
2359
|
+
const route = getRoute('GET', '/api/fs/find-by-name');
|
|
2360
|
+
const ask = async (name) => {
|
|
2361
|
+
const res = createMockResponse();
|
|
2362
|
+
await route({ query: { name }, headers: {} }, res);
|
|
2363
|
+
return res;
|
|
2364
|
+
};
|
|
2365
|
+
|
|
2366
|
+
expect((await ask('Renderer.tsx')).body).toEqual({ paths: [path.join(repo, 'src/chat/Renderer.tsx')] });
|
|
2367
|
+
expect((await ask('util.ts')).body.paths.sort()).toEqual([path.join(repo, 'lib/util.ts'), path.join(repo, 'src/util.ts')].sort());
|
|
2368
|
+
expect((await ask('README.md')).body).toEqual({ paths: [path.join(repo, 'README.md')] });
|
|
2369
|
+
expect((await ask('Missing.ts')).body).toEqual({ paths: [] });
|
|
2370
|
+
for (const bad of ['src/util.ts', '*.ts', '', '..']) {
|
|
2371
|
+
expect((await ask(bad)).statusCode, bad).toBe(400);
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Workspace files by name, for linking a bare file name an agent wrote
|
|
2
|
+
// (`Renderer.tsx:42`) to the one file it means.
|
|
3
|
+
//
|
|
4
|
+
// git is slow to start and, on Windows, slow to walk the working tree for
|
|
5
|
+
// untracked files, so one `git ls-files` per workspace serves every name for a
|
|
6
|
+
// short while, concurrent lookups share it, it has a deadline, and a listing
|
|
7
|
+
// too large to hold is dropped rather than kept. Outside a repository, or when
|
|
8
|
+
// git fails, every lookup answers no match.
|
|
9
|
+
|
|
10
|
+
import nodePath from 'node:path';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TTL_MS = 30_000;
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
14
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
15
|
+
const MAX_WORKSPACES = 8;
|
|
16
|
+
const MAX_MATCHES = 20;
|
|
17
|
+
|
|
18
|
+
export const createWorkspaceFileNameIndex = ({
|
|
19
|
+
spawn,
|
|
20
|
+
resolveGitBinary,
|
|
21
|
+
ttlMs = DEFAULT_TTL_MS,
|
|
22
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
23
|
+
maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
|
|
24
|
+
now = () => Date.now(),
|
|
25
|
+
}) => {
|
|
26
|
+
// directory -> { loadedAt, pending, byName: Map<name, relative[]> | null }
|
|
27
|
+
const workspaces = new Map();
|
|
28
|
+
|
|
29
|
+
const listFiles = (directory) => new Promise((resolve) => {
|
|
30
|
+
let child;
|
|
31
|
+
try {
|
|
32
|
+
child = spawn(resolveGitBinary(), ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
|
|
33
|
+
cwd: directory,
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
resolve(null);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let bytes = 0;
|
|
43
|
+
let settled = false;
|
|
44
|
+
const finish = (value) => {
|
|
45
|
+
if (settled) return;
|
|
46
|
+
settled = true;
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
resolve(value);
|
|
49
|
+
};
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
child.kill();
|
|
52
|
+
finish(null);
|
|
53
|
+
}, timeoutMs);
|
|
54
|
+
child.stdout.on('data', (data) => {
|
|
55
|
+
bytes += data.length;
|
|
56
|
+
if (bytes > maxOutputBytes) {
|
|
57
|
+
child.kill();
|
|
58
|
+
finish(null);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
chunks.push(data);
|
|
62
|
+
});
|
|
63
|
+
child.on('close', (code) => finish(code === 0 ? Buffer.concat(chunks).toString('utf8') : null));
|
|
64
|
+
child.on('error', () => finish(null));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const buildIndex = (output) => {
|
|
68
|
+
if (output === null) return null;
|
|
69
|
+
const byName = new Map();
|
|
70
|
+
for (const relative of output.split('\0')) {
|
|
71
|
+
if (!relative) continue;
|
|
72
|
+
const name = nodePath.posix.basename(relative);
|
|
73
|
+
const list = byName.get(name);
|
|
74
|
+
if (!list) byName.set(name, [relative]);
|
|
75
|
+
else if (list.length < MAX_MATCHES && !list.includes(relative)) list.push(relative);
|
|
76
|
+
}
|
|
77
|
+
return byName;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const load = (directory) => {
|
|
81
|
+
const existing = workspaces.get(directory);
|
|
82
|
+
if (existing?.pending) return existing.pending;
|
|
83
|
+
if (existing && now() - existing.loadedAt < ttlMs) return Promise.resolve(existing.byName);
|
|
84
|
+
const pending = listFiles(directory).then((output) => {
|
|
85
|
+
const byName = buildIndex(output);
|
|
86
|
+
workspaces.set(directory, { loadedAt: now(), pending: null, byName });
|
|
87
|
+
return byName;
|
|
88
|
+
});
|
|
89
|
+
workspaces.delete(directory);
|
|
90
|
+
workspaces.set(directory, { loadedAt: 0, pending, byName: null });
|
|
91
|
+
while (workspaces.size > MAX_WORKSPACES) {
|
|
92
|
+
workspaces.delete(workspaces.keys().next().value);
|
|
93
|
+
}
|
|
94
|
+
return pending;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Absolute paths of the workspace files called `name`, at most 20. */
|
|
98
|
+
const find = async (directory, name) => {
|
|
99
|
+
const byName = await load(directory);
|
|
100
|
+
return (byName?.get(name) ?? []).map((relative) => nodePath.join(directory, relative));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return { find };
|
|
104
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { createWorkspaceFileNameIndex } from './workspace-file-names.js';
|
|
6
|
+
|
|
7
|
+
// A git child that prints `output` and exits, or never finishes when output is null.
|
|
8
|
+
const fakeSpawn = (outputs) => {
|
|
9
|
+
const calls = [];
|
|
10
|
+
const spawn = () => {
|
|
11
|
+
calls.push(1);
|
|
12
|
+
const child = new EventEmitter();
|
|
13
|
+
child.stdout = new EventEmitter();
|
|
14
|
+
child.kill = () => { child.killed = true; };
|
|
15
|
+
const output = outputs[Math.min(calls.length - 1, outputs.length - 1)];
|
|
16
|
+
if (output !== null) {
|
|
17
|
+
queueMicrotask(() => {
|
|
18
|
+
child.stdout.emit('data', Buffer.from(output));
|
|
19
|
+
child.emit('close', 0);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return child;
|
|
23
|
+
};
|
|
24
|
+
return { spawn, calls };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const listing = ['src/chat/Renderer.tsx', 'lib/util.ts', 'src/util.ts', ''].join('\0');
|
|
28
|
+
|
|
29
|
+
describe('createWorkspaceFileNameIndex', () => {
|
|
30
|
+
it('answers every name from one listing and shares it between concurrent lookups', async () => {
|
|
31
|
+
const { spawn, calls } = fakeSpawn([listing]);
|
|
32
|
+
const index = createWorkspaceFileNameIndex({ spawn, resolveGitBinary: () => 'git' });
|
|
33
|
+
const [renderer, util, missing] = await Promise.all([
|
|
34
|
+
index.find('/repo', 'Renderer.tsx'),
|
|
35
|
+
index.find('/repo', 'util.ts'),
|
|
36
|
+
index.find('/repo', 'Missing.ts'),
|
|
37
|
+
]);
|
|
38
|
+
expect(renderer).toEqual([path.join('/repo', 'src/chat/Renderer.tsx')]);
|
|
39
|
+
expect(util).toHaveLength(2);
|
|
40
|
+
expect(missing).toEqual([]);
|
|
41
|
+
expect(calls).toHaveLength(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('lists again once the cached listing is older than the TTL', async () => {
|
|
45
|
+
let clock = 0;
|
|
46
|
+
const { spawn, calls } = fakeSpawn([listing, `${listing}src/New.ts\0`]);
|
|
47
|
+
const index = createWorkspaceFileNameIndex({ spawn, resolveGitBinary: () => 'git', ttlMs: 1000, now: () => clock });
|
|
48
|
+
expect(await index.find('/repo', 'New.ts')).toEqual([]);
|
|
49
|
+
clock = 500;
|
|
50
|
+
expect(await index.find('/repo', 'New.ts')).toEqual([]);
|
|
51
|
+
clock = 2000;
|
|
52
|
+
expect(await index.find('/repo', 'New.ts')).toEqual([path.join('/repo', 'src/New.ts')]);
|
|
53
|
+
expect(calls).toHaveLength(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('gives no match for a listing over the size cap or past the deadline', async () => {
|
|
57
|
+
const big = createWorkspaceFileNameIndex({ spawn: fakeSpawn([listing]).spawn, resolveGitBinary: () => 'git', maxOutputBytes: 8 });
|
|
58
|
+
expect(await big.find('/repo', 'util.ts')).toEqual([]);
|
|
59
|
+
const slow = createWorkspaceFileNameIndex({ spawn: fakeSpawn([null]).spawn, resolveGitBinary: () => 'git', timeoutMs: 10 });
|
|
60
|
+
expect(await slow.find('/repo', 'util.ts')).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
});
|