diff --git a/.github/skills/hygiene/SKILL.md b/.github/skills/hygiene/SKILL.md new file mode 100644 index 0000000000000..084b76c719c8d --- /dev/null +++ b/.github/skills/hygiene/SKILL.md @@ -0,0 +1,25 @@ +# Hygiene Checks + +VS Code runs a hygiene check as a git pre-commit hook. Commits will be rejected if hygiene fails. + +## What it checks + +The hygiene linter scans all staged `.ts` files for issues including (but not limited to): + +- **Unicode characters**: Non-ASCII characters (em-dashes, curly quotes, emoji, etc.) are rejected. Use ASCII equivalents in comments and code. +- **Double-quoted strings**: Only use `"double quotes"` for externalized (localized) strings. Use `'single quotes'` everywhere else. +- **Copyright headers**: All files must include the Microsoft copyright header. + +## How it runs + +The git pre-commit hook (via husky) runs `npm run precommit`, which executes: + +```bash +node --experimental-strip-types build/hygiene.ts +``` + +This scans only **staged files** (from `git diff --cached`). To run it manually: + +```bash +npm run precommit +``` diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c6af7adaa7ae4..831a02068fb3d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -86,7 +86,7 @@ jobs: uses: ./.github/workflows/pr-linux-cli-test.yml with: job_name: CLI - rustup_toolchain: 1.85 + rustup_toolchain: 1.88 linux-electron-tests: name: Linux diff --git a/build/azure-pipelines/cli/cli-compile.yml b/build/azure-pipelines/cli/cli-compile.yml index 2abefa7b6a438..6ca100f6cf946 100644 --- a/build/azure-pipelines/cli/cli-compile.yml +++ b/build/azure-pipelines/cli/cli-compile.yml @@ -20,12 +20,55 @@ steps: - script: echo "##vso[task.setvariable variable=VSCODE_CLI_PRODUCT_JSON]$(Build.SourcesDirectory)/.build/distro/mixin/${{ parameters.VSCODE_QUALITY }}/product.json" displayName: Set product.json path + - task: Cache@2 + displayName: Restore sccache cache + inputs: + key: 'sccache | "$(Agent.OS)" | "${{ parameters.VSCODE_CLI_TARGET }}" | $(Build.SourcesDirectory)/cli/Cargo.toml | $(Build.SourcesDirectory)/build/.cachesalt' + path: $(Pipeline.Workspace)/sccache + + - ${{ if contains(parameters.VSCODE_CLI_TARGET, '-windows-') }}: + - pwsh: | + $version = "0.14.0" + $url = "https://github.com/mozilla/sccache/releases/download/v$version/sccache-v$version-x86_64-pc-windows-msvc.zip" + Invoke-WebRequest -Uri $url -OutFile "$env:TEMP\sccache.zip" + Expand-Archive -Path "$env:TEMP\sccache.zip" -DestinationPath "$env:TEMP\sccache" -Force + $sccacheDir = Get-ChildItem -Path "$env:TEMP\sccache" -Directory | Select-Object -First 1 + Copy-Item "$($sccacheDir.FullName)\sccache.exe" -Destination "$env:USERPROFILE\.cargo\bin\sccache.exe" + sccache --version + displayName: Install sccache + - ${{ else }}: + - script: | + set -e + SCCACHE_VERSION="0.14.0" + ARCH=$(uname -m) + OS=$(uname -s) + if [ "$OS" = "Darwin" ]; then + TARGET="aarch64-apple-darwin" + elif [ "$OS" = "Linux" ]; then + if [ "$ARCH" = "aarch64" ]; then + TARGET="aarch64-unknown-linux-musl" + else + TARGET="x86_64-unknown-linux-musl" + fi + fi + FILENAME="sccache-v${SCCACHE_VERSION}-${TARGET}" + URL="https://github.com/mozilla/sccache/releases/download/v${SCCACHE_VERSION}/${FILENAME}.tar.gz" + echo "Downloading sccache from $URL" + curl -fsSL "$URL" -o /tmp/sccache.tar.gz + tar -xzf /tmp/sccache.tar.gz -C /tmp + sudo cp "/tmp/${FILENAME}/sccache" /usr/local/bin/sccache + sudo chmod +x /usr/local/bin/sccache + sccache --version + displayName: Install sccache + - ${{ if parameters.VSCODE_CHECK_ONLY }}: - script: cargo clippy --target ${{ parameters.VSCODE_CLI_TARGET }} --bin=code displayName: Lint ${{ parameters.VSCODE_CLI_TARGET }} workingDirectory: $(Build.SourcesDirectory)/cli env: CARGO_NET_GIT_FETCH_WITH_CLI: true + RUSTC_WRAPPER: sccache + SCCACHE_DIR: $(Pipeline.Workspace)/sccache ${{ each pair in parameters.VSCODE_CLI_ENV }}: ${{ pair.key }}: ${{ pair.value }} @@ -93,6 +136,8 @@ steps: CARGO_NET_GIT_FETCH_WITH_CLI: true VSCODE_CLI_COMMIT: $(Build.SourceVersion) GITHUB_TOKEN: "$(github-distro-mixin-password)" + RUSTC_WRAPPER: sccache + SCCACHE_DIR: $(Pipeline.Workspace)/sccache ${{ each pair in parameters.VSCODE_CLI_ENV }}: ${{ pair.key }}: ${{ pair.value }} @@ -103,6 +148,8 @@ steps: env: CARGO_NET_GIT_FETCH_WITH_CLI: true VSCODE_CLI_COMMIT: $(Build.SourceVersion) + RUSTC_WRAPPER: sccache + SCCACHE_DIR: $(Pipeline.Workspace)/sccache ${{ each pair in parameters.VSCODE_CLI_ENV }}: ${{ pair.key }}: ${{ pair.value }} @@ -161,3 +208,9 @@ steps: archiveType: tar tarCompression: gz archiveFile: $(Build.ArtifactStagingDirectory)/${{ parameters.VSCODE_CLI_ARTIFACT }}.tar.gz + + - script: sccache --show-stats + displayName: sccache stats + condition: succeededOrFailed() + env: + SCCACHE_DIR: $(Pipeline.Workspace)/sccache diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 5209f73ad91a6..cd9b8de6afba6 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -3,35 +3,20 @@ version = 4 [[package]] -name = "addr2line" -version = "0.21.0" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -43,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -58,38 +43,45 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + [[package]] name = "async-broadcast" version = "0.5.1" @@ -102,12 +94,12 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", - "event-listener-strategy 0.5.2", + "event-listener-strategy", "futures-core", "pin-project-lite", ] @@ -126,7 +118,7 @@ dependencies = [ "log", "parking", "polling 2.8.0", - "rustix 0.37.27", + "rustix 0.37.28", "slab", "socket2 0.4.10", "waker-fn", @@ -134,21 +126,20 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock 3.3.0", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.3.0", + "futures-lite 2.6.1", "parking", - "polling 3.7.0", - "rustix 0.38.34", + "polling 3.11.0", + "rustix 1.1.3", "slab", - "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -162,12 +153,12 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.3.0" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", + "event-listener 5.4.1", + "event-listener-strategy", "pin-project-lite", ] @@ -184,7 +175,7 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.34", + "rustix 0.38.44", "windows-sys 0.48.0", ] @@ -196,25 +187,25 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] name = "async-signal" -version = "0.2.6" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.6.0", + "async-lock 3.4.2", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.34", + "rustix 1.1.3", "signal-hook-registry", "slab", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -225,13 +216,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] @@ -242,24 +233,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "backtrace" -version = "0.3.71" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -281,9 +257,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" @@ -303,25 +279,33 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "blocking" -version = "1.6.0" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", - "async-lock 3.3.0", "async-task", "futures-io", - "futures-lite 2.3.0", + "futures-lite 2.6.1", "piper", ] [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byteorder" @@ -337,34 +321,43 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.0.98" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.5", + "windows-link", ] [[package]] name = "clap" -version = "4.5.4" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" dependencies = [ "clap_builder", "clap_derive", @@ -372,33 +365,39 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ "anstream", "anstyle", - "clap_lex", + "clap_lex 1.0.0", "strsim", ] [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "clap_lex" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "code-cli" @@ -410,7 +409,7 @@ dependencies = [ "cfg-if", "chrono", "clap", - "clap_lex", + "clap_lex 0.7.7", "console", "const_format", "core-foundation", @@ -454,9 +453,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" @@ -469,31 +468,31 @@ dependencies = [ [[package]] name = "console" -version = "0.15.8" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", - "lazy_static", "libc", + "once_cell", "unicode-width", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "const_format" -version = "0.2.32" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" dependencies = [ "proc-macro2", "quote", @@ -512,24 +511,24 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ebf8d6963185c7625d2c3c3962d99eb8936637b1427536d21dc36ae402ebad" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -545,15 +544,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -561,15 +560,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ "powerfmt", ] @@ -649,6 +648,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -657,29 +666,29 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "enumflags2" -version = "0.7.9" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", @@ -687,29 +696,29 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.9" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -731,20 +740,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.3.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -753,21 +751,11 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.3.0", + "event-listener 5.4.1", "pin-project-lite", ] @@ -782,27 +770,32 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" -version = "0.2.23" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", - "windows-sys 0.52.0", + "libredox", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flate2" -version = "1.0.30" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "libz-sys", @@ -815,6 +808,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -832,18 +831,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -856,9 +855,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -866,15 +865,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -883,9 +882,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -904,9 +903,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "futures-core", "pin-project-lite", @@ -914,32 +913,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -986,26 +985,45 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "r-efi", + "wasip2", ] [[package]] -name = "gimli" -version = "0.28.1" +name = "getrandom" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -1013,7 +1031,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.6", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1028,9 +1046,18 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -1044,6 +1071,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1089,9 +1122,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1101,9 +1134,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1116,7 +1149,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.7", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -1138,14 +1171,15 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -1161,21 +1195,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1184,104 +1219,72 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] [[package]] -name = "icu_provider_macros" -version = "1.5.0" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.65", -] +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1290,9 +1293,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1310,32 +1313,34 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] name = "indicatif" -version = "0.17.8" +version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ "console", - "instant", "number_prefix", "portable-atomic", "unicode-width", + "web-time", ] [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", @@ -1356,16 +1361,16 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] [[package]] name = "ipnet" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-docker" @@ -1388,22 +1393,23 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1423,31 +1429,38 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.10.0", "libc", + "redox_syscall 0.7.1", ] [[package]] name = "libz-sys" -version = "1.1.16" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "pkg-config", @@ -1460,7 +1473,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.10.0", "libc", ] @@ -1472,31 +1485,36 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.21" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "md5" @@ -1506,9 +1524,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.7.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -1536,31 +1554,31 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", + "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", ] [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "6cdede44f9a69cab2899a2049e2c3bd49bf911a157f6a3353d4a91c61abbce44" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -1584,11 +1602,23 @@ dependencies = [ "memoffset 0.7.1", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi", ] @@ -1609,9 +1639,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", @@ -1629,9 +1659,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -1673,16 +1703,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "number_prefix" version = "0.4.0" @@ -1690,70 +1710,226 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] -name = "object" -version = "0.32.2" +name = "objc2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ - "memchr", + "objc2-encode", ] [[package]] -name = "once_cell" -version = "1.19.0" +name = "objc2-cloud-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-foundation", +] [[package]] -name = "open" -version = "4.2.0" +name = "objc2-core-data" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a083c0c7e5e4a8ec4176346cf61f67ac674e8bfb059d9226e1c54a96b377c12" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "is-wsl", - "libc", - "pathdiff", + "objc2", + "objc2-foundation", ] [[package]] -name = "openssl" -version = "0.10.72" +name = "objc2-core-foundation" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.5.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", + "bitflags 2.10.0", + "dispatch2", + "objc2", ] [[package]] -name = "openssl-macros" -version = "0.1.1" +name = "objc2-core-graphics" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.65", + "bitflags 2.10.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] -name = "openssl-probe" -version = "0.1.5" +name = "objc2-core-image" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] [[package]] -name = "openssl-sys" -version = "0.9.107" +name = "objc2-core-location" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "cc", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "open" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a083c0c7e5e4a8ec4176346cf61f67ac674e8bfb059d9226e1c54a96b377c12" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", "libc", "pkg-config", "vcpkg", @@ -1822,25 +1998,30 @@ dependencies = [ [[package]] name = "os_info" -version = "3.8.2" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" dependencies = [ + "android_system_properties", "log", - "windows-sys 0.52.0", + "nix 0.30.1", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "windows-sys 0.61.2", ] [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1848,60 +2029,54 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.1", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.5", + "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1911,20 +2086,20 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.1.0", + "fastrand 2.3.0", "futures-io", ] [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polling" @@ -1944,24 +2119,32 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.5.2", "pin-project-lite", - "rustix 0.38.34", - "tracing", - "windows-sys 0.52.0", + "rustix 1.1.3", + "windows-sys 0.61.2", ] [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] [[package]] name = "powerfmt" @@ -1971,9 +2154,22 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.115", +] [[package]] name = "proc-macro-crate" @@ -1987,22 +2183,28 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.7.3" @@ -2062,7 +2264,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", ] [[package]] @@ -2076,38 +2278,38 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", ] [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.10.0", ] [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2117,9 +2319,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2128,9 +2330,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reqwest" @@ -2176,22 +2378,19 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" dependencies = [ - "byteorder", "num-traits", - "paste", ] [[package]] name = "rmp-serde" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" dependencies = [ - "byteorder", "rmp", "serde", ] @@ -2261,17 +2460,11 @@ dependencies = [ "yasna", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustix" -version = "0.37.27" +version = "0.37.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" dependencies = [ "bitflags 1.3.2", "errno", @@ -2283,15 +2476,28 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", ] [[package]] @@ -2303,19 +2509,25 @@ dependencies = [ "base64", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2326,9 +2538,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "secret-service" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da1a5ad4d28c03536f82f77d9f36603f5e37d8869ac98f0a750d5b5686d8d95" +checksum = "b5204d39df37f06d1944935232fd2dfe05008def7ca599bf28c0800366c8a8f9" dependencies = [ "futures-util", "generic-array", @@ -2342,11 +2554,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", @@ -2355,63 +2567,82 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" -version = "1.0.202" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.14" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", - "ryu", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_repr" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] @@ -2439,9 +2670,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -2456,33 +2687,43 @@ checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" [[package]] name = "shell-words" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" @@ -2496,19 +2737,29 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -2524,9 +2775,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -2541,9 +2792,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.65" +version = "2.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" dependencies = [ "proc-macro2", "quote", @@ -2558,13 +2809,13 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] @@ -2604,9 +2855,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.40" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" dependencies = [ "filetime", "libc", @@ -2615,60 +2866,61 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ - "cfg-if", - "fastrand 2.1.0", - "rustix 0.38.34", - "windows-sys 0.52.0", + "fastrand 2.3.0", + "getrandom 0.4.1", + "once_cell", + "rustix 1.1.3", + "windows-sys 0.61.2", ] [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] name = "time" -version = "0.3.36" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", ] [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -2676,33 +2928,31 @@ dependencies = [ [[package]] name = "tokio" -version = "1.38.2" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68722da18b0fc4a05fdc1120b302b82051265792a1e1b399086e9b204b10ad3d" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.7", + "socket2 0.6.2", "tokio-macros", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] @@ -2717,9 +2967,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -2742,9 +2992,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2756,9 +3006,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_edit" @@ -2766,22 +3016,22 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.13.0", "toml_datetime", "winnow", ] [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2790,20 +3040,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -2864,9 +3114,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uds_windows" @@ -2881,31 +3131,32 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -2920,12 +3171,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -2934,18 +3179,20 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.8.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ - "getrandom 0.2.15", - "serde", + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", ] [[package]] @@ -2956,9 +3203,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "waker-fn" @@ -2983,52 +3230,60 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.92" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "bumpalo", - "log", + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", "once_cell", - "proc-macro2", - "quote", - "syn 2.0.65", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3036,28 +3291,53 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.65", - "wasm-bindgen-backend", + "syn 2.0.115", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] [[package]] name = "wasm-streams" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -3066,11 +3346,33 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -3100,11 +3402,61 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets 0.52.5", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", ] [[package]] @@ -3122,7 +3474,34 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -3142,18 +3521,35 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3164,9 +3560,15 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -3176,9 +3578,15 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -3188,15 +3596,27 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -3206,9 +3626,15 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -3218,9 +3644,15 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -3230,9 +3662,15 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -3242,9 +3680,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -3275,36 +3719,117 @@ dependencies = [ ] [[package]] -name = "write16" -version = "1.0.0" +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.115", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.115", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xattr" -version = "1.3.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "linux-raw-sys 0.4.14", - "rustix 0.38.34", + "rustix 1.1.3", ] [[package]] name = "xdg-home" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" dependencies = [ "libc", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -3319,11 +3844,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3331,13 +3855,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", "synstructure", ] @@ -3359,7 +3883,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.26.4", "once_cell", "ordered-stream", "rand 0.8.5", @@ -3402,38 +3926,69 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", "synstructure", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -3442,13 +3997,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.65", + "syn 2.0.115", ] [[package]] @@ -3464,6 +4019,12 @@ dependencies = [ "time", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zvariant" version = "3.15.2" diff --git a/cli/src/json_rpc.rs b/cli/src/json_rpc.rs index 57baac01c5e41..1ef0ecf01ffda 100644 --- a/cli/src/json_rpc.rs +++ b/cli/src/json_rpc.rs @@ -69,7 +69,7 @@ pub async fn start_json_rpc( n = read.read_line(&mut read_buf) => { let r = match n { Ok(0) => return Ok(None), - Ok(n) => dispatcher.dispatch(read_buf[..n].as_bytes()), + Ok(n) => dispatcher.dispatch(&read_buf.as_bytes()[..n]), Err(e) => return Err(e) }; diff --git a/cli/src/util/http.rs b/cli/src/util/http.rs index e49120578a77c..9658ec1fcbd62 100644 --- a/cli/src/util/http.rs +++ b/cli/src/util/http.rs @@ -66,7 +66,7 @@ impl SimpleResponse { pub fn url_path_basename(&self) -> Option { self.url.as_ref().and_then(|u| { u.path_segments() - .and_then(|s| s.last().map(|s| s.to_owned())) + .and_then(|mut s| s.next_back().map(|s| s.to_owned())) }) } } @@ -176,7 +176,7 @@ impl SimpleHttp for ReqwestSimpleHttp { url: Some(res.url().clone()), read: Box::pin( res.bytes_stream() - .map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e)) + .map_err(futures::io::Error::other) .into_async_read() .compat(), ), diff --git a/extensions/microsoft-authentication/src/node/authServer.ts b/extensions/microsoft-authentication/src/node/authServer.ts deleted file mode 100644 index 2d6a8d03861e7..0000000000000 --- a/extensions/microsoft-authentication/src/node/authServer.ts +++ /dev/null @@ -1,207 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as http from 'http'; -import { URL } from 'url'; -import * as fs from 'fs'; -import * as path from 'path'; -import { randomBytes } from 'crypto'; - -function sendFile(res: http.ServerResponse, filepath: string) { - fs.readFile(filepath, (err, body) => { - if (err) { - console.error(err); - res.writeHead(404); - res.end(); - } else { - res.writeHead(200, { - 'content-length': body.length, - }); - res.end(body); - } - }); -} - -interface IOAuthResult { - code: string; - state: string; -} - -interface ILoopbackServer { - /** - * If undefined, the server is not started yet. - */ - port: number | undefined; - - /** - * The nonce used - */ - nonce: string; - - /** - * The state parameter used in the OAuth flow. - */ - state: string | undefined; - - /** - * Starts the server. - * @returns The port to listen on. - * @throws If the server fails to start. - * @throws If the server is already started. - */ - start(): Promise; - /** - * Stops the server. - * @throws If the server is not started. - * @throws If the server fails to stop. - */ - stop(): Promise; - /** - * Returns a promise that resolves to the result of the OAuth flow. - */ - waitForOAuthResponse(): Promise; -} - -export class LoopbackAuthServer implements ILoopbackServer { - private readonly _server: http.Server; - private readonly _resultPromise: Promise; - private _startingRedirect: URL; - - public nonce = randomBytes(16).toString('base64'); - public port: number | undefined; - - public set state(state: string | undefined) { - if (state) { - this._startingRedirect.searchParams.set('state', state); - } else { - this._startingRedirect.searchParams.delete('state'); - } - } - public get state(): string | undefined { - return this._startingRedirect.searchParams.get('state') ?? undefined; - } - - constructor(serveRoot: string, startingRedirect: string) { - if (!serveRoot) { - throw new Error('serveRoot must be defined'); - } - if (!startingRedirect) { - throw new Error('startingRedirect must be defined'); - } - this._startingRedirect = new URL(startingRedirect); - let deferred: { resolve: (result: IOAuthResult) => void; reject: (reason: any) => void }; - this._resultPromise = new Promise((resolve, reject) => deferred = { resolve, reject }); - - this._server = http.createServer((req, res) => { - const reqUrl = new URL(req.url!, `http://${req.headers.host}`); - switch (reqUrl.pathname) { - case '/signin': { - const receivedNonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+'); - if (receivedNonce !== this.nonce) { - res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` }); - res.end(); - } - res.writeHead(302, { location: this._startingRedirect.toString() }); - res.end(); - break; - } - case '/callback': { - const code = reqUrl.searchParams.get('code') ?? undefined; - const state = reqUrl.searchParams.get('state') ?? undefined; - const nonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+'); - const error = reqUrl.searchParams.get('error') ?? undefined; - if (error) { - res.writeHead(302, { location: `/?error=${reqUrl.searchParams.get('error_description')}` }); - res.end(); - deferred.reject(new Error(error)); - break; - } - if (!code || !state || !nonce) { - res.writeHead(400); - res.end(); - break; - } - if (this.state !== state) { - res.writeHead(302, { location: `/?error=${encodeURIComponent('State does not match.')}` }); - res.end(); - deferred.reject(new Error('State does not match.')); - break; - } - if (this.nonce !== nonce) { - res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` }); - res.end(); - deferred.reject(new Error('Nonce does not match.')); - break; - } - deferred.resolve({ code, state }); - res.writeHead(302, { location: '/' }); - res.end(); - break; - } - // Serve the static files - case '/': - sendFile(res, path.join(serveRoot, 'index.html')); - break; - default: - // substring to get rid of leading '/' - sendFile(res, path.join(serveRoot, reqUrl.pathname.substring(1))); - break; - } - }); - } - - public start(): Promise { - return new Promise((resolve, reject) => { - if (this._server.listening) { - throw new Error('Server is already started'); - } - const portTimeout = setTimeout(() => { - reject(new Error('Timeout waiting for port')); - }, 5000); - this._server.on('listening', () => { - const address = this._server.address(); - if (typeof address === 'string') { - this.port = parseInt(address); - } else if (address instanceof Object) { - this.port = address.port; - } else { - throw new Error('Unable to determine port'); - } - - clearTimeout(portTimeout); - - // set state which will be used to redirect back to vscode - this.state = `http://127.0.0.1:${this.port}/callback?nonce=${encodeURIComponent(this.nonce)}`; - - resolve(this.port); - }); - this._server.on('error', err => { - reject(new Error(`Error listening to server: ${err}`)); - }); - this._server.on('close', () => { - reject(new Error('Closed')); - }); - this._server.listen(0, '127.0.0.1'); - }); - } - - public stop(): Promise { - return new Promise((resolve, reject) => { - if (!this._server.listening) { - throw new Error('Server is not started'); - } - this._server.close((err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - public waitForOAuthResponse(): Promise { - return this._resultPromise; - } -} diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 3ed64c055bf87..36c310315e65e 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -20,10 +20,10 @@ "button.background": "#0069CC", "button.foreground": "#FFFFFF", "button.hoverBackground": "#0063C1", - "button.border": "#F2F3F4FF", + "button.border": "#EEEEF1", "button.secondaryBackground": "#EDEDED", "button.secondaryForeground": "#202020", - "button.secondaryHoverBackground": "#F3F3F3", + "button.secondaryHoverBackground": "#EEEEEE", "checkbox.background": "#EDEDED", "checkbox.border": "#D8D8D8", "checkbox.foreground": "#202020", @@ -64,7 +64,7 @@ "list.activeSelectionForeground": "#202020", "list.inactiveSelectionBackground": "#E0E0E0", "list.inactiveSelectionForeground": "#202020", - "list.hoverBackground": "#F3F3F3", + "list.hoverBackground": "#EEEEEE", "list.hoverForeground": "#202020", "list.dropBackground": "#0069CC15", "list.focusBackground": "#0069CC1A", @@ -101,12 +101,12 @@ "menu.foreground": "#202020", "menu.selectionBackground": "#0069CC1A", "menu.selectionForeground": "#202020", - "menu.separatorBackground": "#F7F7F7", + "menu.separatorBackground": "#EEEEF1", "menu.border": "#F2F3F4FF", "commandCenter.foreground": "#202020", "commandCenter.activeForeground": "#202020", "commandCenter.background": "#FAFAFD", - "commandCenter.activeBackground": "#F3F3F3", + "commandCenter.activeBackground": "#EEEEEE", "commandCenter.border": "#D8D8D8", "editor.background": "#FFFFFF", "editor.foreground": "#202020", @@ -127,7 +127,7 @@ "editorLink.activeForeground": "#0069CC", "editorWhitespace.foreground": "#66666640", "editorIndentGuide.background": "#F7F7F740", - "editorIndentGuide.activeBackground": "#F3F3F3", + "editorIndentGuide.activeBackground": "#EEEEEE", "editorRuler.foreground": "#F7F7F7", "editorCodeLens.foreground": "#666666", "editorBracketMatch.background": "#0069CC40", @@ -179,8 +179,8 @@ "statusBar.debuggingForeground": "#FFFFFF", "statusBar.noFolderBackground": "#F0F0F3", "statusBar.noFolderForeground": "#666666", - "statusBarItem.activeBackground": "#F3F3F3", - "statusBarItem.hoverBackground": "#F3F3F3", + "statusBarItem.activeBackground": "#EEEEEE", + "statusBarItem.hoverBackground": "#EEEEEE", "statusBarItem.focusBorder": "#0069CCFF", "statusBarItem.prominentBackground": "#0069CCDD", "statusBarItem.prominentForeground": "#FFFFFF", @@ -193,7 +193,7 @@ "tab.lastPinnedBorder": "#F2F3F4FF", "tab.activeBorder": "#FAFAFD", "tab.activeBorderTop": "#000000", - "tab.hoverBackground": "#F3F3F3", + "tab.hoverBackground": "#EEEEEE", "tab.hoverForeground": "#202020", "tab.unfocusedActiveBackground": "#FAFAFD", "tab.unfocusedActiveForeground": "#666666", @@ -224,7 +224,7 @@ "extensionButton.prominentBackground": "#0069CC", "extensionButton.prominentForeground": "#FFFFFF", "extensionButton.prominentHoverBackground": "#0064CC", - "pickerGroup.border": "#F2F3F4FF", + "pickerGroup.border": "#EEEEF1", "pickerGroup.foreground": "#202020", "quickInput.background": "#F0F0F3", "quickInput.foreground": "#202020", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 31bcd567e3129..f95851a38e33a 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -7,28 +7,22 @@ --radius-sm: 4px; --radius-md: 6px; --radius-lg: 8px; - /* --radius-lg: 12px; */ - --shadow-xs: 0 0 2px rgba(0, 0, 0, 0.06); --shadow-sm: 0 0 4px rgba(0, 0, 0, 0.08); --shadow-md: 0 0 6px rgba(0, 0, 0, 0.08); --shadow-lg: 0 0 12px rgba(0, 0, 0, 0.14); --shadow-xl: 0 0 20px rgba(0, 0, 0, 0.15); - --shadow-2xl: 0 0 20px rgba(0, 0, 0, 0.18); --shadow-hover: 0 0 8px rgba(0, 0, 0, 0.12); --shadow-sm-strong: 0 0 4px rgba(0, 0, 0, 0.18); --shadow-button-active: inset 0 1px 2px rgba(0, 0, 0, 0.1); - --shadow-inset-white: inset 0 0 4px rgba(255, 255, 255, 0.1); --shadow-active-tab: 0 8px 12px rgba(0, 0, 0, 0.02); - --backdrop-blur-sm: blur(12px); --backdrop-blur-md: blur(20px) saturate(180%); --backdrop-blur-lg: blur(40px) saturate(180%); } /* Dark theme: add brightness reduction for contrast-safe luminosity blending over bright backgrounds */ .monaco-workbench.vs-dark { - --backdrop-blur-sm: blur(12px) brightness(0.55); --backdrop-blur-md: blur(20px) saturate(180%) brightness(0.55); --backdrop-blur-lg: blur(40px) saturate(180%) brightness(0.55); } @@ -131,7 +125,6 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { box-shadow: inset var(--shadow-active-tab); - /* background: var(--vs) */ position: relative; z-index: 5; border-radius: 0; @@ -194,7 +187,7 @@ .monaco-workbench .quick-input-widget .quick-input-action, .monaco-workbench .quick-input-widget .quick-input-message, .monaco-workbench .quick-input-widget .monaco-list, -.monaco-workbench .quick-input-widget .monaco-list-row { +.monaco-workbench .quick-input-widget .monaco-list-row:not(:has(.quick-input-list-separator-border)) { border-color: transparent !important; outline: none !important; } @@ -211,6 +204,10 @@ border-radius: var(--radius-lg); } +.monaco-workbench .quick-input-widget .monaco-list-rows { + background: transparent !important; +} + .monaco-workbench .quick-input-widget .monaco-inputbox { box-shadow: none !important; background: transparent !important; @@ -347,7 +344,7 @@ /* Dialog */ .monaco-workbench .monaco-dialog-box { - box-shadow: var(--shadow-2xl); + box-shadow: var(--shadow-xl); border-radius: var(--radius-lg); backdrop-filter: var(--backdrop-blur-lg); -webkit-backdrop-filter: var(--backdrop-blur-lg); @@ -362,8 +359,8 @@ .monaco-workbench .monaco-editor .peekview-widget { box-shadow: var(--shadow-hover); background: color-mix(in srgb, var(--vscode-peekViewEditor-background) 60%, transparent) !important; - backdrop-filter: var(--backdrop-blur-sm); - -webkit-backdrop-filter: var(--backdrop-blur-sm); + backdrop-filter: var(--backdrop-blur-md); + -webkit-backdrop-filter: var(--backdrop-blur-md); } .monaco-workbench.vs-dark .monaco-editor .peekview-widget { @@ -475,13 +472,6 @@ } /* Buttons */ -.monaco-workbench .monaco-button { - box-shadow: var(--shadow-xs); -} - -.monaco-workbench .monaco-button:hover { - box-shadow: var(--shadow-sm); -} .monaco-workbench .monaco-button:active { box-shadow: var(--shadow-button-active); @@ -516,11 +506,6 @@ border-radius: var(--radius-lg); } -/* Terminal */ -.monaco-workbench.vs .pane-body.integrated-terminal { - box-shadow: var(--shadow-inset-white); -} - /* SCM */ .monaco-workbench .scm-view .scm-provider { box-shadow: var(--shadow-sm); @@ -681,10 +666,6 @@ background: transparent; } -.monaco-workbench .quick-input-list .quick-input-list-entry.quick-input-list-separator-border { - border-top-width: 0; -} - /* Quick Input List - use descriptionForeground color for descriptions */ .monaco-workbench .quick-input-list .monaco-icon-label .label-description { opacity: 1; diff --git a/package.json b/package.json index 5de9732104172..e8a6f0832f2d9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.110.0", - "distro": "c3c943b0a748aa10a5d6818ab8e7c9e34e458d83", + "distro": "68c946526275fa6c9bec4d4cfe1eb331f1062ee4", "author": { "name": "Microsoft Corporation" }, @@ -243,4 +243,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index f667738b74818..ef9b1ba246278 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -9,10 +9,9 @@ import { Event } from '../../../common/event.js'; import { Disposable } from '../../../common/lifecycle.js'; import './gridview.css'; import { Box, GridView, IGridViewOptions, IGridViewStyles, IView as IGridViewView, IViewSize, orthogonal, Sizing as GridViewSizing, GridLocation } from './gridview.js'; -import type { SplitView, AutoSizing as SplitViewAutoSizing, IViewVisibilityAnimationOptions } from '../splitview/splitview.js'; +import type { SplitView, AutoSizing as SplitViewAutoSizing } from '../splitview/splitview.js'; export type { IViewSize }; -export type { IViewVisibilityAnimationOptions } from '../splitview/splitview.js'; export { LayoutPriority, Orientation, orthogonal } from './gridview.js'; export const enum Direction { @@ -651,12 +650,10 @@ export class Grid extends Disposable { * Set the visibility state of a {@link IView view}. * * @param view The {@link IView view}. - * @param visible Whether the view should be visible. - * @param animation Optional animation options. */ - setViewVisible(view: T, visible: boolean, animation?: IViewVisibilityAnimationOptions): void { + setViewVisible(view: T, visible: boolean): void { const location = this.getViewLocation(view); - this.gridview.setViewVisible(location, visible, animation); + this.gridview.setViewVisible(location, visible); } /** diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 03c416cfd896f..112d7231f1aeb 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -5,7 +5,7 @@ import { $ } from '../../dom.js'; import { IBoundarySashes, Orientation, Sash } from '../sash/sash.js'; -import { DistributeSizing, ISplitViewStyles, IView as ISplitView, IViewVisibilityAnimationOptions, LayoutPriority, Sizing, AutoSizing, SplitView } from '../splitview/splitview.js'; +import { DistributeSizing, ISplitViewStyles, IView as ISplitView, LayoutPriority, Sizing, AutoSizing, SplitView } from '../splitview/splitview.js'; import { equals as arrayEquals, tail } from '../../../common/arrays.js'; import { Color } from '../../../common/color.js'; import { Emitter, Event, Relay } from '../../../common/event.js'; @@ -615,7 +615,7 @@ class BranchNode implements ISplitView, IDisposable { return this.splitview.isViewVisible(index); } - setChildVisible(index: number, visible: boolean, animation?: IViewVisibilityAnimationOptions): void { + setChildVisible(index: number, visible: boolean): void { index = validateIndex(index, this.children.length); if (this.splitview.isViewVisible(index) === visible) { @@ -623,7 +623,7 @@ class BranchNode implements ISplitView, IDisposable { } const wereAllChildrenHidden = this.splitview.contentSize === 0; - this.splitview.setViewVisible(index, visible, animation); + this.splitview.setViewVisible(index, visible); const areAllChildrenHidden = this.splitview.contentSize === 0; // If all children are hidden then the parent should hide the entire splitview @@ -1663,7 +1663,7 @@ export class GridView implements IDisposable { * * @param location The {@link GridLocation location} of the view. */ - setViewVisible(location: GridLocation, visible: boolean, animation?: IViewVisibilityAnimationOptions): void { + setViewVisible(location: GridLocation, visible: boolean): void { if (this.hasMaximizedView()) { this.exitMaximizedView(); return; @@ -1676,7 +1676,7 @@ export class GridView implements IDisposable { throw new Error('Invalid from location'); } - parent.setChildVisible(index, visible, animation); + parent.setChildVisible(index, visible); } /** diff --git a/src/vs/base/browser/ui/motion/motion.css b/src/vs/base/browser/ui/motion/motion.css deleted file mode 100644 index 69e257be2d380..0000000000000 --- a/src/vs/base/browser/ui/motion/motion.css +++ /dev/null @@ -1,9 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/* Utility class applied during panel animations to prevent content overflow */ -.monaco-split-view2 > .split-view-container > .split-view-view.motion-animating { - overflow: hidden; -} diff --git a/src/vs/base/browser/ui/motion/motion.ts b/src/vs/base/browser/ui/motion/motion.ts deleted file mode 100644 index c2e8a045d417e..0000000000000 --- a/src/vs/base/browser/ui/motion/motion.ts +++ /dev/null @@ -1,155 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import './motion.css'; - -//#region Easing Curves - -/** - * A pre-parsed cubic bezier easing curve that can be evaluated directly - * without reparsing a CSS string on every frame. - * - * Given control points `(x1, y1)` and `(x2, y2)` (the CSS `cubic-bezier` - * parameters), {@link solve} finds the bezier parameter `u` such that - * `Bx(u) = t` using Newton's method, then returns `By(u)`. - */ -export class CubicBezierCurve { - - constructor( - readonly x1: number, - readonly y1: number, - readonly x2: number, - readonly y2: number, - ) { } - - /** - * Evaluate the curve at time `t` (0-1), returning the eased value. - */ - solve(t: number): number { - if (t <= 0) { - return 0; - } - if (t >= 1) { - return 1; - } - - // Newton's method to find u where Bx(u) = t - let u = t; // initial guess - for (let i = 0; i < 8; i++) { - const currentX = bezierComponent(u, this.x1, this.x2); - const error = currentX - t; - if (Math.abs(error) < 1e-6) { - break; - } - const dx = bezierComponentDerivative(u, this.x1, this.x2); - if (Math.abs(dx) < 1e-6) { - break; - } - u -= error / dx; - } - - u = Math.max(0, Math.min(1, u)); - return bezierComponent(u, this.y1, this.y2); - } - - /** - * Returns the CSS `cubic-bezier(…)` string representation, for use in - * CSS `transition` or `animation` properties. - */ - toCssString(): string { - return `cubic-bezier(${this.x1}, ${this.y1}, ${this.x2}, ${this.y2})`; - } -} - -/** - * Fluent 2 ease-out curve - default for entrances and expansions. - * Starts fast and decelerates to a stop. - */ -export const EASE_OUT = new CubicBezierCurve(0.1, 0.9, 0.2, 1); - -/** - * Fluent 2 ease-in curve - for exits and collapses. - * Starts slow and accelerates out. - */ -export const EASE_IN = new CubicBezierCurve(0.9, 0.1, 1, 0.2); - -//#endregion - -//#region Cubic Bezier Evaluation - -/** - * Parses a CSS `cubic-bezier(x1, y1, x2, y2)` string into a - * {@link CubicBezierCurve}. Returns a linear curve on parse failure. - */ -export function parseCubicBezier(css: string): CubicBezierCurve { - const match = css.match(/cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/); - if (!match) { - return new CubicBezierCurve(0, 0, 1, 1); - } - return new CubicBezierCurve(parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3]), parseFloat(match[4])); -} - -/** Evaluates one component of a cubic bezier: B(u) with control points p1, p2, endpoints 0 and 1. */ -function bezierComponent(u: number, p1: number, p2: number): number { - // B(u) = 3(1-u)^2*u*p1 + 3(1-u)*u^2*p2 + u^3 - const oneMinusU = 1 - u; - return 3 * oneMinusU * oneMinusU * u * p1 + 3 * oneMinusU * u * u * p2 + u * u * u; -} - -/** First derivative of a bezier component: B'(u). */ -function bezierComponentDerivative(u: number, p1: number, p2: number): number { - // B'(u) = 3(1-u)^2*p1 + 6(1-u)*u*(p2-p1) + 3*u^2*(1-p2) - const oneMinusU = 1 - u; - return 3 * oneMinusU * oneMinusU * p1 + 6 * oneMinusU * u * (p2 - p1) + 3 * u * u * (1 - p2); -} - -//#endregion - -//#region Duration Scaling - -/** - * Reference pixel distance at which the base duration constants apply. - * Duration scales linearly: a 600px animation takes twice as long as a 300px - * one, keeping perceived velocity constant. - */ -const REFERENCE_DISTANCE = 300; - -/** Minimum animation duration in milliseconds (avoids sub-frame flickers). */ -const MIN_DURATION = 50; - -/** Maximum animation duration in milliseconds (avoids sluggish feel). */ -const MAX_DURATION = 300; - -/** - * Scales a base animation duration proportionally to the pixel distance - * being animated, so that perceived velocity stays constant regardless of - * panel width. - * - * @param baseDuration The duration (ms) that applies at {@link REFERENCE_DISTANCE} pixels. - * @param pixelDistance The actual number of pixels the view will resize. - * @returns The scaled duration, clamped to [{@link MIN_DURATION}, {@link MAX_DURATION}]. - */ -export function scaleDuration(baseDuration: number, pixelDistance: number): number { - if (pixelDistance <= 0) { - return baseDuration; - } - const scaled = baseDuration * (pixelDistance / REFERENCE_DISTANCE); - return Math.round(Math.max(MIN_DURATION, Math.min(MAX_DURATION, scaled))); -} - -//#endregion - -//#region Utility Functions - -/** - * Checks whether motion is reduced by looking for the `monaco-reduce-motion` - * class on an ancestor element. This integrates with VS Code's existing - * accessibility infrastructure in {@link AccessibilityService}. - */ -export function isMotionReduced(element: HTMLElement): boolean { - return element.closest('.monaco-reduce-motion') !== null; -} - -//#endregion diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 2c72bd8d86976..35f2724c1a8e4 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -8,41 +8,15 @@ import { DomEmitter } from '../../event.js'; import { ISashEvent as IBaseSashEvent, Orientation, Sash, SashState } from '../sash/sash.js'; import { SmoothScrollableElement } from '../scrollbar/scrollableElement.js'; import { pushToEnd, pushToStart, range } from '../../../common/arrays.js'; -import { CancellationToken } from '../../../common/cancellation.js'; import { Color } from '../../../common/color.js'; import { Emitter, Event } from '../../../common/event.js'; import { combinedDisposable, Disposable, dispose, IDisposable, toDisposable } from '../../../common/lifecycle.js'; import { clamp } from '../../../common/numbers.js'; import { Scrollable, ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js'; import * as types from '../../../common/types.js'; -import { CubicBezierCurve, isMotionReduced, scaleDuration } from '../motion/motion.js'; import './splitview.css'; export { Orientation } from '../sash/sash.js'; -/** - * Options for animating a view visibility change in a {@link SplitView}. - */ -export interface IViewVisibilityAnimationOptions { - - /** Transition duration in milliseconds. */ - readonly duration: number; - - /** The easing curve applied to the animation. */ - readonly easing: CubicBezierCurve; - - /** - * Optional callback invoked when the animation finishes naturally. - * NOT called if the animation is cancelled via the {@link token}. - */ - readonly onComplete?: () => void; - - /** - * A cancellation token that allows the caller to stop the animation. - * When cancellation is requested the animation snaps to its final state. - */ - readonly token: CancellationToken; -} - export interface ISplitViewStyles { readonly separatorBorder: Color; } @@ -828,38 +802,14 @@ export class SplitView= this.viewItems.length) { throw new Error('Index out of bounds'); } - // Cancel any in-flight animation before changing visibility. - // An animated visibility change interpolates ALL view sizes each - // frame, so a concurrent change on a different view would be - // overwritten on the next frame. Snapping first prevents that. - this._cleanupMotion?.(); - this._cleanupMotion = undefined; - - if (animation && !animation.token.isCancellationRequested && !isMotionReduced(this.el) && this.viewItems[index].visible !== visible) { - this._setViewVisibleAnimated(index, visible, animation); - } else { - this._setViewVisibleInstant(index, visible); - } - } - - /** - * Apply the visibility change to the model without animation. - */ - private _setViewVisibleInstant(index: number, visible: boolean): void { const viewItem = this.viewItems[index]; viewItem.setVisible(visible); @@ -868,167 +818,6 @@ export class SplitView v.size); - - // 2. Apply the target visibility to the model instantly. - // This computes final sizes, fires events, updates sashes, etc. - this._setViewVisibleInstant(index, visible); - - // 3. Snapshot sizes AFTER the visibility change (the animation end state) - const finalSizes = this.viewItems.map(v => v.size); - - // 4. Restore start sizes so we can animate FROM them - for (let i = 0; i < this.viewItems.length; i++) { - this.viewItems[i].size = startSizes[i]; - } - - // 5. For hiding: the target container lost .visible class (→ display:none). - // Restore it so content stays visible during the animation. - if (!visible) { - container.classList.add('visible'); - } - - // 6. Clip overflow on the animating container so that the view - // content (rendered at its full target size) is revealed/hidden - // smoothly as the container width animates. - container.style.overflow = 'hidden'; - - // The target size is the full (non-zero) panel size. During - // animation we re-layout the animating view at this fixed size - // after each frame so its content never reflows at intermediate - // widths/heights (prevents chat, extension icons, etc. from - // jumping around). Sibling views still receive interpolated sizes - // so the editor / bottom panel remain responsive. - const viewTargetSize = visible ? finalSizes[index] : startSizes[index]; - - // 6b. Set initial opacity for fade effect - container.style.opacity = visible ? '0' : '1'; - - // 7. Scale duration based on pixel distance for consistent perceived velocity - const pixelDistance = Math.abs(finalSizes[index] - startSizes[index]); - const duration = scaleDuration(baseDuration, pixelDistance); - - // 8. Render the start state - this.layoutViews(); - try { - this.viewItems[index].view.layout(viewTargetSize, 0, this.layoutContext); - } catch (e) { - console.error('Splitview: Failed to layout view during animation'); - console.error(e); - } - - // 9. Easing curve is pre-parsed - ready for JS evaluation - - // Helper: snap all sizes to final state and clean up - const applyFinalState = () => { - for (let i = 0; i < this.viewItems.length; i++) { - this.viewItems[i].size = finalSizes[i]; - } - container.style.opacity = ''; - container.style.overflow = ''; - if (!visible) { - container.classList.remove('visible'); - } - this.layoutViews(); - this.saveProportions(); - }; - - const cleanup = (completed: boolean) => { - if (disposed) { - return; - } - disposed = true; - tokenListener.dispose(); - if (rafId !== undefined) { - window.cancelAnimationFrame(rafId); - rafId = undefined; - } - applyFinalState(); - this._cleanupMotion = undefined; - if (completed) { - onComplete?.(); - } - }; - this._cleanupMotion = () => cleanup(false); - - // Listen to the cancellation token so the caller can stop the animation - const tokenListener = token.onCancellationRequested(() => cleanup(false)); - - // 10. Animate via requestAnimationFrame - const startTime = performance.now(); - const totalSize = this.size; - - const animate = () => { - if (disposed) { - return; - } - - const elapsed = performance.now() - startTime; - const t = Math.min(elapsed / duration, 1); - const easedT = easing.solve(t); - - // Interpolate opacity for fade effect - container.style.opacity = String(visible ? easedT : 1 - easedT); - - // Interpolate all view sizes - let runningTotal = 0; - for (let i = 0; i < this.viewItems.length; i++) { - if (i === this.viewItems.length - 1) { - // Last item absorbs rounding errors to maintain total = this.size - this.viewItems[i].size = totalSize - runningTotal; - } else { - const size = Math.round( - startSizes[i] + (finalSizes[i] - startSizes[i]) * easedT - ); - this.viewItems[i].size = size; - runningTotal += size; - } - } - - this.layoutViews(); - - // Re-layout the animating view at its full target size so its - // content does not reflow at intermediate sizes. The container - // is already at the interpolated size with overflow:hidden. - try { - this.viewItems[index].view.layout(viewTargetSize, 0, this.layoutContext); - } catch (e) { - console.error('Splitview: Failed to layout view during animation'); - console.error(e); - } - - if (t < 1) { - rafId = window.requestAnimationFrame(animate); - } else { - cleanup(true); - } - }; - - rafId = window.requestAnimationFrame(animate); - } - - private _cleanupMotion: (() => void) | undefined; - /** * Returns the {@link IView view}'s size previously to being hidden. * diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 9afd0964022b0..bd509719a3cce 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -525,35 +525,3 @@ outline: 1px solid var(--vscode-list-focusOutline) !important; outline-offset: -1px; } - -/* Entrance animation */ -@keyframes quick-input-entrance { - from { - opacity: 0; - transform: translateY(-8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.quick-input-widget.animating-entrance { - animation: quick-input-entrance 150ms cubic-bezier(0.1, 0.9, 0.2, 1) forwards; -} - -/* Exit animation */ -@keyframes quick-input-exit { - from { - opacity: 1; - transform: translateY(0); - } - to { - opacity: 0; - transform: translateY(-8px); - } -} - -.quick-input-widget.animating-exit { - animation: quick-input-exit 50ms cubic-bezier(0.9, 0.1, 1, 0.2) forwards; -} diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 11964ea5a30bf..8e5283ef9ad92 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -37,7 +37,6 @@ import { TriStateCheckbox, createToggleActionViewItemProvider } from '../../../b import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; import { QuickInputTreeController } from './tree/quickInputTreeController.js'; import { QuickTree } from './tree/quickTree.js'; -import { isMotionReduced } from '../../../base/browser/ui/motion/motion.js'; import { AnchorAlignment, AnchorPosition, layout2d } from '../../../base/common/layout.js'; import { getAnchorRect } from '../../../base/browser/ui/contextview/contextview.js'; @@ -81,7 +80,6 @@ export class QuickInputController extends Disposable { private viewState: QuickInputViewState | undefined; private dndController: QuickInputDragAndDropController | undefined; - private _cancelExitAnimation: (() => void) | undefined; private readonly inQuickInputContext: IContextKey; private readonly quickInputTypeContext: IContextKey; @@ -713,26 +711,12 @@ export class QuickInputController extends Disposable { const backKeybindingLabel = this.options.backKeybindingLabel(); backButton.tooltip = backKeybindingLabel ? localize('quickInput.backWithKeybinding', "Back ({0})", backKeybindingLabel) : localize('quickInput.back', "Back"); - const wasVisible = ui.container.style.display !== 'none'; ui.container.style.display = ''; - // Cancel any in-flight exit animation that would set display:none - this._cancelExitAnimation?.(); - this._cancelExitAnimation = undefined; this.updateLayout(); this.dndController?.setEnabled(!controller.anchor); this.dndController?.layoutContainer(); ui.inputBox.setFocus(); this.quickInputTypeContext.set(controller.type); - - // Animate entrance: fade in + slide down (only when first appearing) - if (!wasVisible && !isMotionReduced(ui.container)) { - ui.container.classList.add('animating-entrance'); - const onAnimationEnd = () => { - ui.container.classList.remove('animating-entrance'); - ui.container.removeEventListener('animationend', onAnimationEnd); - }; - ui.container.addEventListener('animationend', onAnimationEnd); - } } isVisible(): boolean { @@ -799,24 +783,7 @@ export class QuickInputController extends Disposable { this.controller = null; this.onHideEmitter.fire(); if (container) { - // Animate exit: fade out + slide up (faster than open) - if (!isMotionReduced(container)) { - container.classList.add('animating-exit'); - const cleanupAnimation = () => { - container.classList.remove('animating-exit'); - container.removeEventListener('animationend', onAnimationEnd); - this._cancelExitAnimation = undefined; - }; - const onAnimationEnd = () => { - // Set display after animation completes to actually hide the element - container.style.display = 'none'; - cleanupAnimation(); - }; - this._cancelExitAnimation = cleanupAnimation; - container.addEventListener('animationend', onAnimationEnd); - } else { - container.style.display = 'none'; - } + container.style.display = 'none'; } if (!focusChanged) { let currentElement = this.previousFocusElement; diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index c7679ab51cb73..b51e004b0457f 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -23,7 +23,7 @@ import { IHostService } from '../services/host/browser/host.js'; import { IBrowserWorkbenchEnvironmentService } from '../services/environment/browser/environmentService.js'; import { IEditorService } from '../services/editor/common/editorService.js'; import { EditorGroupLayout, GroupActivationReason, GroupOrientation, GroupsOrder, IEditorGroupsService } from '../services/editor/common/editorGroupsService.js'; -import { SerializableGrid, ISerializableView, ISerializedGrid, Orientation, ISerializedNode, ISerializedLeafNode, Direction, IViewSize, Sizing, IViewVisibilityAnimationOptions } from '../../base/browser/ui/grid/grid.js'; +import { SerializableGrid, ISerializableView, ISerializedGrid, Orientation, ISerializedNode, ISerializedLeafNode, Direction, IViewSize, Sizing } from '../../base/browser/ui/grid/grid.js'; import { Part } from './part.js'; import { IStatusbarService } from '../services/statusbar/browser/statusbar.js'; import { IFileService } from '../../platform/files/common/files.js'; @@ -47,8 +47,6 @@ import { AuxiliaryBarPart } from './parts/auxiliarybar/auxiliaryBarPart.js'; import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js'; import { IAuxiliaryWindowService } from '../services/auxiliaryWindow/browser/auxiliaryWindowService.js'; import { CodeWindow, mainWindow } from '../../base/browser/window.js'; -import { EASE_OUT, EASE_IN } from '../../base/browser/ui/motion/motion.js'; -import { CancellationToken } from '../../base/common/cancellation.js'; //#region Layout Implementation @@ -1870,32 +1868,27 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.stateModel.setRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN, hidden); - // Adjust CSS - for hiding, defer adding the class until animation - // completes so the part stays visible during the exit animation. - if (!hidden) { + // Adjust CSS + if (hidden) { + this.mainContainer.classList.add(LayoutClasses.SIDEBAR_HIDDEN); + } else { this.mainContainer.classList.remove(LayoutClasses.SIDEBAR_HIDDEN); } // Propagate to grid - this.workbenchGrid.setViewVisible( - this.sideBarPartView, - !hidden, - createViewVisibilityAnimation(hidden, () => { - if (!hidden) { return; } - // Deferred to after close animation - this.mainContainer.classList.add(LayoutClasses.SIDEBAR_HIDDEN); - if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { - this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Sidebar); - - if (!this.isAuxiliaryBarMaximized()) { - this.focusPanelOrEditor(); // do not auto focus when auxiliary bar is maximized - } - } - }) - ); + this.workbenchGrid.setViewVisible(this.sideBarPartView, !hidden); + + // If sidebar becomes hidden, also hide the current active Viewlet if any + if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Sidebar); + + if (!this.isAuxiliaryBarMaximized()) { + this.focusPanelOrEditor(); // do not auto focus when auxiliary bar is maximized + } + } // If sidebar becomes visible, show last active Viewlet or default viewlet - if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { + else if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { const viewletToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.Sidebar); if (viewletToOpen) { this.openViewContainer(ViewContainerLocation.Sidebar, viewletToOpen); @@ -2019,6 +2012,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const panelOpensMaximized = this.panelOpensMaximized(); + // Adjust CSS + if (hidden) { + this.mainContainer.classList.add(LayoutClasses.PANEL_HIDDEN); + } else { + this.mainContainer.classList.remove(LayoutClasses.PANEL_HIDDEN); + } + // If maximized and in process of hiding, unmaximize FIRST before // changing visibility to prevent conflict with setEditorHidden // which would force panel visible again (fixes #281772) @@ -2026,30 +2026,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.toggleMaximizedPanel(); } - // Adjust CSS - for hiding, defer adding the class until animation - // completes because `.nopanel .part.panel { display: none !important }` - // would instantly hide the panel content mid-animation. - if (!hidden) { - this.mainContainer.classList.remove(LayoutClasses.PANEL_HIDDEN); - } - // Propagate layout changes to grid - this.workbenchGrid.setViewVisible( - this.panelPartView, - !hidden, - createViewVisibilityAnimation(hidden, () => { - if (!hidden) { return; } - // Deferred to after close animation - this.mainContainer.classList.add(LayoutClasses.PANEL_HIDDEN); - if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)) { - this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Panel); - } - }) - ); + this.workbenchGrid.setViewVisible(this.panelPartView, !hidden); - // If panel part becomes hidden, focus the editor after animation starts + // If panel part becomes hidden, also hide the current active panel if any let focusEditor = false; if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Panel); if ( !isIOS && // do not auto focus on iOS (https://github.com/microsoft/vscode/issues/127832) !this.isAuxiliaryBarMaximized() // do not auto focus when auxiliary bar is maximized @@ -2223,30 +2206,24 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN, hidden); - // Adjust CSS - for hiding, defer adding the class until animation - // completes because `.noauxiliarybar .part.auxiliarybar { display: none !important }` - // would instantly hide the content mid-animation. - if (!hidden) { + // Adjust CSS + if (hidden) { + this.mainContainer.classList.add(LayoutClasses.AUXILIARYBAR_HIDDEN); + } else { this.mainContainer.classList.remove(LayoutClasses.AUXILIARYBAR_HIDDEN); } // Propagate to grid - this.workbenchGrid.setViewVisible( - this.auxiliaryBarPartView, - !hidden, - createViewVisibilityAnimation(hidden, () => { - if (!hidden) { return; } - // Deferred to after close animation - this.mainContainer.classList.add(LayoutClasses.AUXILIARYBAR_HIDDEN); - if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { - this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.AuxiliaryBar); - this.focusPanelOrEditor(); - } - }) - ); + this.workbenchGrid.setViewVisible(this.auxiliaryBarPartView, !hidden); + + // If auxiliary bar becomes hidden, also hide the current active pane composite if any + if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.AuxiliaryBar); + this.focusPanelOrEditor(); + } // If auxiliary bar becomes visible, show last active pane composite or default pane composite - if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { + else if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { let viewletToOpen: string | undefined = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.AuxiliaryBar); // verify that the viewlet we try to open has views before we default to it @@ -2739,21 +2716,6 @@ function getZenModeConfiguration(configurationService: IConfigurationService): Z return configurationService.getValue(WorkbenchLayoutSettings.ZEN_MODE_CONFIG); } -/** Duration (ms) for panel/sidebar open (entrance) animations. */ -const PANEL_OPEN_DURATION = 135; - -/** Duration (ms) for panel/sidebar close (exit) animations. */ -const PANEL_CLOSE_DURATION = 35; - -function createViewVisibilityAnimation(hidden: boolean, onComplete?: () => void, token: CancellationToken = CancellationToken.None): IViewVisibilityAnimationOptions { - return { - duration: hidden ? PANEL_CLOSE_DURATION : PANEL_OPEN_DURATION, - easing: hidden ? EASE_IN : EASE_OUT, - token, - onComplete, - }; -} - //#endregion //#region Layout State Model diff --git a/src/vs/workbench/browser/media/motion.css b/src/vs/workbench/browser/media/motion.css deleted file mode 100644 index fbd8215265a84..0000000000000 --- a/src/vs/workbench/browser/media/motion.css +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/* Motion custom properties -- only active when motion is enabled */ -.monaco-workbench.monaco-enable-motion { - --vscode-motion-panel-open-duration: 175ms; - --vscode-motion-panel-close-duration: 75ms; - --vscode-motion-quick-input-open-duration: 175ms; - --vscode-motion-quick-input-close-duration: 75ms; - --vscode-motion-ease-out: cubic-bezier(0.1, 0.9, 0.2, 1); - --vscode-motion-ease-in: cubic-bezier(0.9, 0.1, 1, 0.2); -} - -/* Disable all motion durations when reduced motion is active */ -.monaco-workbench.monaco-reduce-motion { - --vscode-motion-panel-open-duration: 0ms; - --vscode-motion-panel-close-duration: 0ms; - --vscode-motion-quick-input-open-duration: 0ms; - --vscode-motion-quick-input-close-duration: 0ms; -} diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index bb02c39dc9600..d32082b4e1272 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -114,6 +114,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { ViewContainerLocation.AuxiliaryBar, Extensions.Auxiliary, MenuId.AuxiliaryBarTitle, + undefined, notificationService, storageService, contextMenuService, @@ -150,7 +151,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { private onDidChangeAutoHideViewContainers(e: { before: number; after: number }): void { // Only update if auto-hide is enabled and composite bar would show const autoHide = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_AUTO_HIDE); - if (autoHide && (this.configuration.position !== ActivityBarPosition.HIDDEN)) { + if (autoHide && (this.configuration.position === ActivityBarPosition.TOP || this.configuration.position === ActivityBarPosition.BOTTOM)) { const visibleBefore = e.before > 1; const visibleAfter = e.after > 1; if (visibleBefore !== visibleAfter) { @@ -265,14 +266,17 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { } // Check if auto-hide is enabled and there's only one visible view container - const autoHide = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_AUTO_HIDE); - if (autoHide) { - // Use visible composite count from the composite bar if available (considers pinned state), - // otherwise fall back to the tracker's count (based on active view descriptors). - // Note: We access paneCompositeBar directly to avoid circular calls with getVisiblePaneCompositeIds() - const visibleCount = this.visibleViewContainersTracker.visibleCount; - if (visibleCount <= 1) { - return false; + // while the activity bar is configured to be top or bottom. + if (this.configuration.position === ActivityBarPosition.TOP || this.configuration.position === ActivityBarPosition.BOTTOM) { + const autoHide = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_AUTO_HIDE); + if (autoHide) { + // Use visible composite count from the composite bar if available (considers pinned state), + // otherwise fall back to the tracker's count (based on active view descriptors). + // Note: We access paneCompositeBar directly to avoid circular calls with getVisiblePaneCompositeIds() + const visibleCount = this.visibleViewContainersTracker.visibleCount; + if (visibleCount <= 1) { + return false; + } } } diff --git a/src/vs/workbench/browser/parts/paneCompositePart.ts b/src/vs/workbench/browser/parts/paneCompositePart.ts index e46ba514858cb..1f1187c362507 100644 --- a/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -145,6 +145,7 @@ export abstract class AbstractPaneCompositePart extends CompositePart this.actionViewItemProvider(action, options), orientation: ActionsOrientation.HORIZONTAL, @@ -708,14 +707,6 @@ export abstract class AbstractPaneCompositePart extends CompositePart { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand('workbench.action.chat.open', { + mode: 'agent', + query: '/init', + isPartialQuery: false, + }); + } + }); + + registerAction2(class GenerateInstructionAction extends Action2 { constructor() { super({ - id: 'workbench.action.chat.generateInstructions', - title: localize2('generateInstructions', "Generate Workspace Instructions File"), - shortTitle: localize2('generateInstructions.short', "Generate Chat Instructions"), + id: GENERATE_INSTRUCTION_COMMAND_ID, + title: localize2('generateInstruction', "Generate On-demand Instruction with Agent"), + shortTitle: localize2('generateInstruction.short', "Generate Instruction with Agent"), category: CHAT_CATEGORY, icon: Codicon.sparkle, f1: true, @@ -1109,31 +1138,79 @@ export function registerChatActions() { async run(accessor: ServicesAccessor): Promise { const commandService = accessor.get(ICommandService); + await commandService.executeCommand('workbench.action.chat.open', { + mode: 'agent', + query: '/create-instruction ', + isPartialQuery: true, + }); + } + }); - // Use chat command to open and send the query - const query = `Analyze this codebase to generate or update \`.github/copilot-instructions.md\` for guiding AI coding agents. + registerAction2(class GeneratePromptAction extends Action2 { + constructor() { + super({ + id: GENERATE_PROMPT_COMMAND_ID, + title: localize2('generatePrompt', "Generate Prompt File with Agent"), + shortTitle: localize2('generatePrompt.short', "Generate Prompt with Agent"), + category: CHAT_CATEGORY, + icon: Codicon.sparkle, + f1: true, + precondition: ChatContextKeys.enabled + }); + } -Focus on discovering the essential knowledge that would help an AI agents be immediately productive in this codebase. Consider aspects like: -- The "big picture" architecture that requires reading multiple files to understand - major components, service boundaries, data flows, and the "why" behind structural decisions -- Critical developer workflows (builds, tests, debugging) especially commands that aren't obvious from file inspection alone -- Project-specific conventions and patterns that differ from common practices -- Integration points, external dependencies, and cross-component communication patterns + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand('workbench.action.chat.open', { + mode: 'agent', + query: '/create-prompt ', + isPartialQuery: true, + }); + } + }); -Source existing AI conventions from \`**/{.github/copilot-instructions.md,AGENT.md,AGENTS.md,CLAUDE.md,.cursorrules,.windsurfrules,.clinerules,.cursor/rules/**,.windsurf/rules/**,.clinerules/**,README.md}\` (do one glob search). + registerAction2(class GenerateSkillAction extends Action2 { + constructor() { + super({ + id: GENERATE_SKILL_COMMAND_ID, + title: localize2('generateSkill', "Generate Skill with Agent"), + shortTitle: localize2('generateSkill.short', "Generate Skill with Agent"), + category: CHAT_CATEGORY, + icon: Codicon.sparkle, + f1: true, + precondition: ChatContextKeys.enabled + }); + } -Guidelines (read more at https://aka.ms/vscode-instructions-docs): -- If \`.github/copilot-instructions.md\` exists, merge intelligently - preserve valuable content while updating outdated sections -- Write concise, actionable instructions (~20-50 lines) using markdown structure -- Include specific examples from the codebase when describing patterns -- Avoid generic advice ("write tests", "handle errors") - focus on THIS project's specific approaches -- Document only discoverable patterns, not aspirational practices -- Reference key files/directories that exemplify important patterns + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand('workbench.action.chat.open', { + mode: 'agent', + query: '/create-skill ', + isPartialQuery: true, + }); + } + }); -Update \`.github/copilot-instructions.md\` for the user, then ask for feedback on any unclear or incomplete sections to iterate.`; + registerAction2(class GenerateAgentAction extends Action2 { + constructor() { + super({ + id: GENERATE_AGENT_COMMAND_ID, + title: localize2('generateAgent', "Generate Custom Agent with Agent"), + shortTitle: localize2('generateAgent.short', "Generate Agent with Agent"), + category: CHAT_CATEGORY, + icon: Codicon.sparkle, + f1: true, + precondition: ChatContextKeys.enabled + }); + } + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); await commandService.executeCommand('workbench.action.chat.open', { mode: 'agent', - query: query, + query: '/create-agent ', + isPartialQuery: true, }); } }); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index ff81e1273fa91..7fc854738a297 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -9,7 +9,6 @@ import { URI } from '../../../../../base/common/uri.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { IChatSessionTiming } from '../../common/chatService/chatService.js'; -import { IChatSessionsExtensionPoint } from '../../common/chatSessionsService.js'; import { foreground, listActiveSelectionForeground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; import { getChatSessionType } from '../../common/model/chatUri.js'; @@ -37,7 +36,6 @@ export function getAgentSessionProvider(sessionResource: URI | string): AgentSes case AgentSessionProviders.Cloud: case AgentSessionProviders.Claude: case AgentSessionProviders.Codex: - case AgentSessionProviders.Growth: return type; default: return undefined; @@ -97,11 +95,7 @@ export function isFirstPartyAgentSessionProvider(provider: AgentSessionProviders } } -export function getAgentCanContinueIn(provider: AgentSessionProviders, contribution?: IChatSessionsExtensionPoint): boolean { - // Read-only sessions (e.g., Growth) are passive/informational and cannot be delegation targets - if (contribution?.isReadOnly) { - return false; - } +export function getAgentCanContinueIn(provider: AgentSessionProviders): boolean { switch (provider) { case AgentSessionProviders.Local: case AgentSessionProviders.Background: @@ -127,7 +121,7 @@ export function getAgentSessionProviderDescription(provider: AgentSessionProvide case AgentSessionProviders.Codex: return localize('chat.session.providerDescription.codex', "Opens a new Codex session in the editor. Codex sessions can be managed from the chat sessions view."); case AgentSessionProviders.Growth: - return localize('chat.session.providerDescription.growth', "Educational messages to help you learn Copilot features."); + return localize('chat.session.providerDescription.growth', "Learn about Copilot features."); } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index aad7733c40fcf..326ebf4e787ab 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1101,6 +1101,15 @@ configurationRegistry.registerConfiguration({ mode: 'auto' } }, + [ChatConfiguration.GrowthNotificationEnabled]: { + type: 'boolean', + description: nls.localize('chat.growthNotification', "Controls whether to show a growth notification in the agent sessions view to encourage new users to try Copilot."), + default: false, + tags: ['experimental'], + experiment: { + mode: 'auto' + } + }, [ChatConfiguration.RestoreLastPanelSession]: { type: 'boolean', description: nls.localize('chat.restoreLastPanelSession', "Controls whether the last session is restored in panel after restart."), diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index b25f186b1e481..388edd3f00c23 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -25,7 +25,6 @@ import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contex import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { EditorActivation } from '../../../../../platform/editor/common/editor.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IEditorPane } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js'; @@ -896,71 +895,3 @@ CommandsRegistry.registerCommand('_chat.editSessions.accept', async (accessor: S await editingSession.accept(...uris); } }); - -//#region View as Tree / View as List toggle - -export const CHAT_EDITS_VIEW_MODE_STORAGE_KEY = 'chat.editsViewMode'; -export const ChatEditsViewAsTreeActionId = 'chatEditing.viewAsTree'; -export const ChatEditsViewAsListActionId = 'chatEditing.viewAsList'; - -registerAction2(class ChatEditsViewAsTreeAction extends Action2 { - constructor() { - super({ - id: ChatEditsViewAsTreeActionId, - title: localize2('chatEditing.viewAsTree', "View as Tree"), - icon: Codicon.listFlat, - category: CHAT_CATEGORY, - menu: [ - { - id: MenuId.ChatEditingWidgetToolbar, - group: 'navigation', - order: 5, - when: ContextKeyExpr.and(hasAppliedChatEditsContextKey, ChatContextKeys.chatEditsInTreeView.negate()), - }, - { - id: MenuId.ChatEditingSessionChangesToolbar, - group: 'navigation', - order: 5, - when: ContextKeyExpr.and(ChatContextKeys.hasAgentSessionChanges, ChatContextKeys.chatEditsInTreeView.negate()), - }, - ], - }); - } - - run(accessor: ServicesAccessor): void { - const storageService = accessor.get(IStorageService); - storageService.store(CHAT_EDITS_VIEW_MODE_STORAGE_KEY, 'tree', StorageScope.PROFILE, StorageTarget.USER); - } -}); - -registerAction2(class ChatEditsViewAsListAction extends Action2 { - constructor() { - super({ - id: ChatEditsViewAsListActionId, - title: localize2('chatEditing.viewAsList', "View as List"), - icon: Codicon.listTree, - category: CHAT_CATEGORY, - menu: [ - { - id: MenuId.ChatEditingWidgetToolbar, - group: 'navigation', - order: 5, - when: ContextKeyExpr.and(hasAppliedChatEditsContextKey, ChatContextKeys.chatEditsInTreeView), - }, - { - id: MenuId.ChatEditingSessionChangesToolbar, - group: 'navigation', - order: 5, - when: ContextKeyExpr.and(ChatContextKeys.hasAgentSessionChanges, ChatContextKeys.chatEditsInTreeView), - }, - ], - }); - } - - run(accessor: ServicesAccessor): void { - const storageService = accessor.get(IStorageService); - storageService.store(CHAT_EDITS_VIEW_MODE_STORAGE_KEY, 'list', StorageScope.PROFILE, StorageTarget.USER); - } -}); - -//#endregion diff --git a/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts b/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts index 695e5151d6bab..e5848bf941ace 100644 --- a/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts +++ b/src/vs/workbench/contrib/chat/browser/chatRepoInfo.ts @@ -9,7 +9,8 @@ import { URI } from '../../../../base/common/uri.js'; import { linesDiffComputers } from '../../../../editor/common/diff/linesDiffComputers.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; +import { IFileService, FileOperationError, FileOperationResult } from '../../../../platform/files/common/files.js'; +import { detectEncodingFromBuffer } from '../../../services/textfile/common/encoding.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; @@ -23,6 +24,7 @@ import * as nls from '../../../../nls.js'; const MAX_CHANGES = 100; const MAX_DIFFS_SIZE_BYTES = 900 * 1024; const MAX_SESSIONS_WITH_FULL_DIFFS = 5; +const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024; // 1 MB per file /** * Regex to match `url = ` lines in git config. */ @@ -121,9 +123,16 @@ async function generateUnifiedDiff( if (originalUri && changeType !== 'added') { try { - const originalFile = await fileService.readFile(originalUri); + const originalFile = await fileService.readFile(originalUri, { limits: { size: MAX_FILE_SIZE_BYTES } }); + const detected = detectEncodingFromBuffer({ buffer: originalFile.value, bytesRead: originalFile.value.byteLength }); + if (detected.seemsBinary) { + return undefined; // skip binary files + } originalContent = originalFile.value.toString(); - } catch { + } catch (e) { + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { + return undefined; // skip files exceeding size limit + } if (changeType === 'modified') { return undefined; } @@ -132,9 +141,16 @@ async function generateUnifiedDiff( if (changeType !== 'deleted') { try { - const modifiedFile = await fileService.readFile(modifiedUri); + const modifiedFile = await fileService.readFile(modifiedUri, { limits: { size: MAX_FILE_SIZE_BYTES } }); + const detected = detectEncodingFromBuffer({ buffer: modifiedFile.value, bytesRead: modifiedFile.value.byteLength }); + if (detected.seemsBinary) { + return undefined; // skip binary files + } modifiedContent = modifiedFile.value.toString(); - } catch { + } catch (e) { + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { + return undefined; // skip files exceeding size limit + } return undefined; } } @@ -597,7 +613,7 @@ export class ChatRepoInfoContribution extends Disposable implements IWorkbenchCo [ChatConfiguration.RepoInfoEnabled]: { type: 'boolean', description: nls.localize('chat.repoInfo.enabled', "Controls whether repository information (branch, commit, working tree diffs) is captured at the start of chat sessions for internal diagnostics."), - default: true, + default: false, } } }); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index a5da0877ddac1..1bb18e323323b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -204,11 +204,6 @@ const extensionPoint = ExtensionsRegistry.registerExtensionPoint this._register(this.instantiationService.createInstance(ChatSetupController, context, requests))); this.registerSetupAgents(context, controller); + this.registerGrowthSession(chatEntitlementService); this.registerActions(context, requests, controller); this.registerUrlLinkHandler(); this.checkExtensionInstallation(context); @@ -171,6 +176,37 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr this._register(Event.runAndSubscribe(context.onDidChange, () => updateRegistration())); } + private registerGrowthSession(chatEntitlementService: ChatEntitlementService): void { + const growthSessionDisposables = markAsSingleton(new MutableDisposable()); + + const updateGrowthSession = () => { + const experimentEnabled = this.configurationService.getValue(ChatConfiguration.GrowthNotificationEnabled) === true; + // Show for users who don't have the Copilot extension installed yet. + // Additional conditions (e.g., anonymous, entitlement) can be layered here. + const shouldShow = experimentEnabled && !chatEntitlementService.sentiment.installed; + if (shouldShow && !growthSessionDisposables.value) { + const disposables = new DisposableStore(); + const controller = disposables.add(this.instantiationService.createInstance(GrowthSessionController)); + if (!controller.isDismissed) { + disposables.add(registerGrowthSession(this.chatSessionsService, controller)); + // Fully unregister when dismissed to prevent cached session from + // appearing during filtered model updates from other providers. + disposables.add(controller.onDidDismiss(() => { + growthSessionDisposables.clear(); + })); + growthSessionDisposables.value = disposables; + } else { + disposables.dispose(); + } + } else if (!shouldShow) { + growthSessionDisposables.clear(); + } + }; + + this._register(chatEntitlementService.onDidChangeSentiment(() => updateGrowthSession())); + updateGrowthSession(); + } + private registerActions(context: ChatEntitlementContext, requests: ChatEntitlementRequests, controller: Lazy): void { //#region Global Chat Setup Actions diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupGrowthSession.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupGrowthSession.ts new file mode 100644 index 0000000000000..1563924cdb3ea --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupGrowthSession.ts @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { ILifecycleService, LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; +import { ChatSessionStatus, IChatSessionItem, IChatSessionItemController, IChatSessionsService } from '../../common/chatSessionsService.js'; +import { AgentSessionProviders } from '../agentSessions/agentSessions.js'; +import { IAgentSession } from '../agentSessions/agentSessionsModel.js'; +import { ISessionOpenerParticipant, ISessionOpenOptions, sessionOpenerRegistry } from '../agentSessions/agentSessionsOpener.js'; +import { IChatWidgetService } from '../chat.js'; +import { CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../actions/chatActions.js'; + +/** + * Core-side growth session controller that shows a single "attention needed" + * session item in the agent sessions view for anonymous/new users. + * + * When the user clicks the session, we open the chat panel (which triggers the + * anonymous setup flow). When the user opens chat at all, the badge is cleared. + * + * The session is shown at most once, tracked via a storage flag. + */ +export class GrowthSessionController extends Disposable implements IChatSessionItemController { + + static readonly STORAGE_KEY = 'chat.growthSession.dismissed'; + + private static readonly SESSION_URI = URI.from({ scheme: AgentSessionProviders.Growth, path: '/growth-welcome' }); + + private readonly _onDidChangeChatSessionItems = this._register(new Emitter()); + readonly onDidChangeChatSessionItems: Event = this._onDidChangeChatSessionItems.event; + + private readonly _onDidDismiss = this._register(new Emitter()); + readonly onDidDismiss: Event = this._onDidDismiss.event; + + private readonly _created = Date.now(); + + private _dismissed: boolean; + get isDismissed(): boolean { return this._dismissed; } + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @ILogService private readonly logService: ILogService, + ) { + super(); + + this._dismissed = this.storageService.getBoolean(GrowthSessionController.STORAGE_KEY, StorageScope.APPLICATION, false); + + // Dismiss the growth session when the user opens chat. + // Wait until the workbench is fully restored so we skip widgets + // that were restored from a previous session at startup. + this.lifecycleService.when(LifecyclePhase.Restored).then(() => { + if (this._store.isDisposed || this._dismissed) { + return; + } + this._register(this.chatWidgetService.onDidAddWidget(() => { + this.dismiss(); + })); + }); + } + + get items(): readonly IChatSessionItem[] { + if (this._dismissed) { + return []; + } + + return [{ + resource: GrowthSessionController.SESSION_URI, + label: localize('growthSession.label', "Try Copilot"), + description: localize('growthSession.description', "GitHub Copilot is available. Try it for free."), + status: ChatSessionStatus.NeedsInput, + iconPath: Codicon.lightbulb, + timing: { + created: this._created, + lastRequestStarted: undefined, + lastRequestEnded: undefined, + }, + }]; + } + + async refresh(): Promise { + // Nothing to refresh -- this is a static, local-only session item + } + + private dismiss(): void { + if (this._dismissed) { + return; + } + + this.logService.trace('[GrowthSession] Dismissing growth session'); + this._dismissed = true; + this.storageService.store(GrowthSessionController.STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.USER); + + // Fire change event first so that listeners (like the model) see empty items + this._onDidChangeChatSessionItems.fire(); + // Then fire dismiss event which triggers unregistration of the controller. + this._onDidDismiss.fire(); + } +} + +/** + * Handles clicks on the growth session item in the agent sessions view. + * Opens a new local chat session with a pre-seeded welcome message. + * The user can then send messages that go through the normal agent. + */ +export class GrowthSessionOpenerParticipant implements ISessionOpenerParticipant { + + async handleOpenSession(accessor: ServicesAccessor, session: IAgentSession, _openOptions?: ISessionOpenOptions): Promise { + if (session.providerType !== AgentSessionProviders.Growth) { + return false; + } + + const commandService = accessor.get(ICommandService); + const opts: IChatViewOpenOptions = { + query: '', + isPartialQuery: true, + previousRequests: [{ + request: localize('growthSession.previousRequest', "Tell me about GitHub Copilot!"), + // allow-any-unicode-next-line + response: localize('growthSession.previousResponse', "Welcome to GitHub Copilot, your AI coding assistant! Here are some things you can try:\n\n- 🐛 *\"Help me debug this error\"* — paste an error message and get a fix\n- 🧪 *\"Write tests for my function\"* — select code and ask for unit tests\n- 💡 *\"Explain this code\"* — highlight something unfamiliar and ask what it does\n- 🚀 *\"Scaffold a REST API\"* — describe what you want and let Agent mode build it\n- 🎨 *\"Refactor this to be more readable\"* — select messy code and clean it up\n\nType anything below to get started!"), + }], + }; + await commandService.executeCommand(CHAT_OPEN_ACTION_ID, opts); + return true; + } +} + +/** + * Registers the growth session controller and opener participant. + * Returns a disposable that cleans up all registrations. + */ +export function registerGrowthSession(chatSessionsService: IChatSessionsService, growthController: GrowthSessionController): IDisposable { + const disposables = new DisposableStore(); + + // Register as session item controller so it appears in the sessions view + disposables.add(chatSessionsService.registerChatSessionItemController(AgentSessionProviders.Growth, growthController)); + + // Register opener participant so clicking the growth session opens chat + disposables.add(sessionOpenerRegistry.registerParticipant(new GrowthSessionOpenerParticipant())); + + return disposables; +} + +// #region Developer Actions + +registerAction2(class ResetGrowthSessionAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.resetGrowthSession', + title: localize2('resetGrowthSession', "Reset Growth Session Notification"), + category: localize2('developer', "Developer"), + f1: true, + }); + } + + run(accessor: ServicesAccessor): void { + const storageService = accessor.get(IStorageService); + storageService.remove(GrowthSessionController.STORAGE_KEY, StorageScope.APPLICATION); + } +}); + +// #endregion diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 45a0d7a5724ae..deed7959ada39 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -20,6 +20,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { localize } from '../../../../nls.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js'; +import { localChatSessionType } from '../common/chatSessionsService.js'; export const IChatTipService = createDecorator('chatTipService'); @@ -52,20 +53,8 @@ export interface IChatTipService { */ readonly onDidDisableTips: Event; - /** - * Gets a tip to show for a request, or undefined if a tip has already been shown this session. - * Only one tip is shown per conversation session (resets when switching conversations). - * Tips are suppressed if a welcome tip was already shown in this session. - * Tips are only shown for requests created after the current session started. - * @param requestId The unique ID of the request (used for stable rerenders). - * @param requestTimestamp The timestamp when the request was created. - * @param contextKeyService The context key service to evaluate tip eligibility. - */ - getNextTip(requestId: string, requestTimestamp: number, contextKeyService: IContextKeyService): IChatTip | undefined; - /** * Gets a tip to show on the welcome/getting-started view. - * Unlike {@link getNextTip}, this does not require a request and skips request-timestamp checks. * Returns the same tip on repeated calls for stable rerenders. */ getWelcomeTip(contextKeyService: IContextKeyService): IChatTip | undefined; @@ -104,6 +93,11 @@ export interface IChatTipService { * @param contextKeyService The context key service to evaluate tip eligibility. */ navigateToPreviousTip(contextKeyService: IContextKeyService): IChatTip | undefined; + + /** + * Clears all dismissed tips so they can be shown again. + */ + clearDismissedTips(): void; } export interface ITipDefinition { @@ -177,9 +171,12 @@ const TIP_CATALOG: ITipDefinition[] = [ { id: 'tip.undoChanges', message: localize('tip.undoChanges', "Tip: Select Restore Checkpoint to undo changes until that point in the chat conversation."), - when: ContextKeyExpr.or( - ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), - ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Edit), + when: ContextKeyExpr.and( + ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType), + ContextKeyExpr.or( + ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), + ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Edit), + ), ), excludeWhenCommandsExecuted: ['workbench.action.chat.restoreCheckpoint', 'workbench.action.chat.restoreLastCheckpoint'], }, @@ -509,19 +506,6 @@ export class ChatTipService extends Disposable implements IChatTipService { private readonly _onDidDisableTips = this._register(new Emitter()); readonly onDidDisableTips = this._onDidDisableTips.event; - /** - * Timestamp when the current session started. - * Used to only show tips for requests created after this time. - * Resets on each {@link resetSession} call. - */ - private _sessionStartedAt = Date.now(); - - /** - * Whether a chatResponse tip has already been shown in this conversation - * session. Only one response tip is shown per session. - */ - private _hasShownRequestTip = false; - /** * The request ID that was assigned a tip (for stable rerenders). */ @@ -548,10 +532,8 @@ export class ChatTipService extends Disposable implements IChatTipService { } resetSession(): void { - this._hasShownRequestTip = false; this._shownTip = undefined; this._tipRequestId = undefined; - this._sessionStartedAt = Date.now(); } dismissTip(): void { @@ -560,7 +542,13 @@ export class ChatTipService extends Disposable implements IChatTipService { dismissed.push(this._shownTip.id); this._storageService.store(ChatTipService._DISMISSED_TIP_KEY, JSON.stringify(dismissed), StorageScope.PROFILE, StorageTarget.MACHINE); } - this._hasShownRequestTip = false; + this._shownTip = undefined; + this._tipRequestId = undefined; + this._onDidDismissTip.fire(); + } + + clearDismissedTips(): void { + this._storageService.remove(ChatTipService._DISMISSED_TIP_KEY, StorageScope.PROFILE); this._shownTip = undefined; this._tipRequestId = undefined; this._onDidDismissTip.fire(); @@ -592,21 +580,19 @@ export class ChatTipService extends Disposable implements IChatTipService { } hideTip(): void { - this._hasShownRequestTip = false; this._shownTip = undefined; this._tipRequestId = undefined; this._onDidHideTip.fire(); } async disableTips(): Promise { - this._hasShownRequestTip = false; this._shownTip = undefined; this._tipRequestId = undefined; await this._configurationService.updateValue('chat.tips.enabled', false); this._onDidDisableTips.fire(); } - getNextTip(requestId: string, requestTimestamp: number, contextKeyService: IContextKeyService): IChatTip | undefined { + getWelcomeTip(contextKeyService: IContextKeyService): IChatTip | undefined { // Check if tips are enabled if (!this._configurationService.getValue('chat.tips.enabled')) { return undefined; @@ -622,46 +608,8 @@ export class ChatTipService extends Disposable implements IChatTipService { return undefined; } - // Check if this is the request that was assigned a tip (for stable rerenders) - if (this._tipRequestId === requestId && this._shownTip) { - return this._createTip(this._shownTip); - } - - // A new request arrived while we already showed a tip, hide the old one - if (this._hasShownRequestTip && this._tipRequestId && this._tipRequestId !== requestId) { - this._shownTip = undefined; - this._tipRequestId = undefined; - this._onDidDismissTip.fire(); - return undefined; - } - - // Only show one tip per session - if (this._hasShownRequestTip) { - return undefined; - } - - // Only show tips for requests created after the current session started. - // This prevents showing tips for old requests being re-rendered. - if (requestTimestamp < this._sessionStartedAt) { - return undefined; - } - - return this._pickTip(requestId, contextKeyService); - } - - getWelcomeTip(contextKeyService: IContextKeyService): IChatTip | undefined { - // Check if tips are enabled - if (!this._configurationService.getValue('chat.tips.enabled')) { - return undefined; - } - - // Only show tips for Copilot - if (!this._isCopilotEnabled()) { - return undefined; - } - - // Only show tips in the main chat panel, not in terminal/editor inline chat - if (!this._isChatLocation(contextKeyService)) { + // Don't show tips when chat quota is exceeded, the upgrade widget is more relevant + if (this._isChatQuotaExceeded(contextKeyService)) { return undefined; } @@ -722,7 +670,6 @@ export class ChatTipService extends Disposable implements IChatTipService { this._storageService.store(ChatTipService._LAST_TIP_ID_KEY, selectedTip.id, StorageScope.PROFILE, StorageTarget.USER); // Record that we've shown a tip this session - this._hasShownRequestTip = sourceId !== 'welcome'; this._tipRequestId = sourceId; this._shownTip = selectedTip; @@ -780,6 +727,10 @@ export class ChatTipService extends Disposable implements IChatTipService { return !location || location === ChatAgentLocation.Chat; } + private _isChatQuotaExceeded(contextKeyService: IContextKeyService): boolean { + return contextKeyService.getContextKeyValue(ChatContextKeys.chatQuotaExceeded.key) === true; + } + private _isCopilotEnabled(): boolean { const defaultChatAgent = this._productService.defaultChatAgent; return !!defaultChatAgent?.chatExtensionId; diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts index 5b3393e59d23c..15f6f5b34f604 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts @@ -172,6 +172,9 @@ function getDefaultContentSnippet(promptType: PromptsType, name: string | undefi `name: ${name ?? '${1:prompt-name}'}`, `description: \${2:Describe when to use this prompt}`, `---`, + ``, + ``, + ``, `\${3:Define the prompt content here. You can include instructions, examples, and any other relevant information to guide the AI's responses.}`, ].join('\n'); case PromptsType.instructions: @@ -182,14 +185,20 @@ function getDefaultContentSnippet(promptType: PromptsType, name: string | undefi `paths:`, `. - "src/**/*.ts"`, `---`, + ``, + ``, + ``, `\${2:Provide coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`, ].join('\n'); } else { return [ `---`, - `description: \${1:Describe when these instructions should be loaded}`, + `description: \${1:Describe when these instructions should be loaded by the agent based on task context}`, `# applyTo: '\${1|**,**/*.ts|}' # when provided, instructions will automatically be added to the request context when the pattern matches an attached file`, `---`, + ``, + ``, + ``, `\${2:Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`, ].join('\n'); } @@ -201,6 +210,9 @@ function getDefaultContentSnippet(promptType: PromptsType, name: string | undefi `description: \${2:Describe what this custom agent does and when to use it.}`, `tools: Read, Grep, Glob, Bash # specify the tools this agent can use. If not set, all enabled tools are allowed.`, `---`, + ``, + ``, + ``, `\${4:Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation.}`, ].join('\n'); } else { @@ -211,6 +223,9 @@ function getDefaultContentSnippet(promptType: PromptsType, name: string | undefi `argument-hint: \${3:The inputs this agent expects, e.g., "a task to implement" or "a question to answer".}`, `# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. If not set, all enabled tools are allowed.`, `---`, + ``, + ``, + ``, `\${4:Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation.}`, ].join('\n'); } @@ -220,6 +235,9 @@ function getDefaultContentSnippet(promptType: PromptsType, name: string | undefi `name: ${name ?? '${1:skill-name}'}`, `description: \${2:Describe what this skill does and when to use it. Include keywords that help agents identify relevant tasks.}`, `---`, + ``, + ``, + ``, `\${3:Define the functionality provided by this skill, including detailed instructions and examples}`, ].join('\n'); default: diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts index d79e8dad50eab..da74b4da74dac 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts @@ -17,6 +17,7 @@ import { ICommandService } from '../../../../../../platform/commands/common/comm import { getCleanPromptName } from '../../../common/promptSyntax/config/promptFileLocations.js'; import { PromptsType, INSTRUCTIONS_DOCUMENTATION_URL, AGENT_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL, SKILL_DOCUMENTATION_URL, HOOK_DOCUMENTATION_URL } from '../../../common/promptSyntax/promptTypes.js'; import { NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, NEW_AGENT_COMMAND_ID, NEW_SKILL_COMMAND_ID } from '../newPromptFileActions.js'; +import { GENERATE_INSTRUCTIONS_COMMAND_ID, GENERATE_INSTRUCTION_COMMAND_ID, GENERATE_PROMPT_COMMAND_ID, GENERATE_SKILL_COMMAND_ID, GENERATE_AGENT_COMMAND_ID } from '../../actions/chatActions.js'; import { IKeyMods, IQuickInputButton, IQuickInputService, IQuickPick, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from '../../../../../../platform/quickinput/common/quickInput.js'; import { askForPromptFileName } from './askForPromptName.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; @@ -168,18 +169,33 @@ const NEW_INSTRUCTIONS_FILE_OPTION: IPromptPickerQuickPickItem = { }; /** - * A quick pick item that starts the 'Update Instructions' command. + * A quick pick item that starts the 'Generate Workspace Instructions' command. */ -const UPDATE_INSTRUCTIONS_OPTION: IPromptPickerQuickPickItem = { +const GENERATE_WORKSPACE_INSTRUCTIONS_OPTION: IPromptPickerQuickPickItem = { type: 'item', - label: `$(refresh) ${localize( - 'commands.update-instructions.select-dialog.label', - 'Generate agent instructions...', + label: `$(sparkle) ${localize( + 'commands.generate-workspace-instructions.select-dialog.label', + 'Generate workspace instructions with agent...', )}`, pickable: false, alwaysShow: true, buttons: [newHelpButton(PromptsType.instructions)], - commandId: 'workbench.action.chat.generateInstructions', + commandId: GENERATE_INSTRUCTIONS_COMMAND_ID, +}; + +/** + * A quick pick item that starts the 'Generate On-demand Instruction' command. + */ +const GENERATE_INSTRUCTION_OPTION: IPromptPickerQuickPickItem = { + type: 'item', + label: `$(sparkle) ${localize( + 'commands.generate-instruction.select-dialog.label', + 'Generate on-demand instruction with agent...', + )}`, + pickable: false, + alwaysShow: true, + buttons: [newHelpButton(PromptsType.instructions)], + commandId: GENERATE_INSTRUCTION_COMMAND_ID, }; /** @@ -212,6 +228,51 @@ const NEW_SKILL_FILE_OPTION: IPromptPickerQuickPickItem = { commandId: NEW_SKILL_COMMAND_ID, }; +/** + * A quick pick item that generates a prompt file with agent. + */ +const GENERATE_PROMPT_OPTION: IPromptPickerQuickPickItem = { + type: 'item', + label: `$(sparkle) ${localize( + 'commands.generate-prompt.select-dialog.label', + 'Generate prompt with agent...', + )}`, + pickable: false, + alwaysShow: true, + buttons: [newHelpButton(PromptsType.prompt)], + commandId: GENERATE_PROMPT_COMMAND_ID, +}; + +/** + * A quick pick item that generates a skill with agent. + */ +const GENERATE_SKILL_OPTION: IPromptPickerQuickPickItem = { + type: 'item', + label: `$(sparkle) ${localize( + 'commands.generate-skill.select-dialog.label', + 'Generate skill with agent...', + )}`, + pickable: false, + alwaysShow: true, + buttons: [newHelpButton(PromptsType.skill)], + commandId: GENERATE_SKILL_COMMAND_ID, +}; + +/** + * A quick pick item that generates a custom agent with agent. + */ +const GENERATE_AGENT_OPTION: IPromptPickerQuickPickItem = { + type: 'item', + label: `$(sparkle) ${localize( + 'commands.generate-agent.select-dialog.label', + 'Generate agent with agent...', + )}`, + pickable: false, + alwaysShow: true, + buttons: [newHelpButton(PromptsType.agent)], + commandId: GENERATE_AGENT_COMMAND_ID, +}; + /** * Button that opens a prompt file in the editor. */ @@ -462,13 +523,13 @@ export class PromptFilePickers { private _getNewItems(type: PromptsType): IPromptPickerQuickPickItem[] { switch (type) { case PromptsType.prompt: - return [NEW_PROMPT_FILE_OPTION]; + return [NEW_PROMPT_FILE_OPTION, GENERATE_PROMPT_OPTION]; case PromptsType.instructions: - return [NEW_INSTRUCTIONS_FILE_OPTION, UPDATE_INSTRUCTIONS_OPTION]; + return [NEW_INSTRUCTIONS_FILE_OPTION, GENERATE_INSTRUCTION_OPTION, GENERATE_WORKSPACE_INSTRUCTIONS_OPTION]; case PromptsType.agent: - return [NEW_AGENT_FILE_OPTION]; + return [NEW_AGENT_FILE_OPTION, GENERATE_AGENT_OPTION]; case PromptsType.skill: - return [NEW_SKILL_FILE_OPTION]; + return [NEW_SKILL_FILE_OPTION, GENERATE_SKILL_OPTION]; default: throw new Error(`Unknown prompt type '${type}'.`); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts index f94a9c168cd51..d62abbb822cb4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts @@ -299,7 +299,7 @@ class CollapsibleListDelegate implements IListVirtualDelegate { +class CollapsibleListRenderer implements IListRenderer { static TEMPLATE_ID = 'chatCollapsibleListRenderer'; readonly templateId: string = CollapsibleListRenderer.TEMPLATE_ID; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts index 9125e269fb32b..c9e3d0c4a962e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSuggestNextWidget.ts @@ -132,7 +132,7 @@ export class ChatSuggestNextWidget extends Disposable { return false; } const provider = getAgentSessionProvider(c.type); - return provider !== undefined && getAgentCanContinueIn(provider, c); + return provider !== undefined && getAgentCanContinueIn(provider); }); if (showContinueOn && availableContributions.length > 0) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts index 7ccef8a28bed8..7a9859e7a5837 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts @@ -6,17 +6,19 @@ import './media/chatTipContent.css'; import * as dom from '../../../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; -import { status } from '../../../../../../base/browser/ui/aria/aria.js'; import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { localize, localize2 } from '../../../../../../nls.js'; import { getFlatContextMenuActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; @@ -64,6 +66,7 @@ export class ChatTipContentPart extends Disposable { const nextTip = this._getNextTip(); if (nextTip) { this._renderTip(nextTip); + this.focus(); } else { this._onDidHide.fire(); } @@ -71,6 +74,7 @@ export class ChatTipContentPart extends Disposable { this._register(this._chatTipService.onDidNavigateTip(tip => { this._renderTip(tip); + this.focus(); })); this._register(this._chatTipService.onDidHideTip(() => { @@ -123,10 +127,9 @@ export class ChatTipContentPart extends Disposable { const textContent = markdownContent.element.textContent ?? localize('chatTip', "Chat tip"); const hasLink = /\[.*?\]\(.*?\)/.test(tip.content.value); const ariaLabel = hasLink - ? localize('chatTipWithAction', "{0} Tab to the action.", textContent) + ? localize('chatTipWithAction', "{0} Tab to reach the action.", textContent) : textContent; this.domNode.setAttribute('aria-label', ariaLabel); - status(ariaLabel); } } @@ -136,7 +139,7 @@ registerAction2(class PreviousTipAction extends Action2 { constructor() { super({ id: 'workbench.action.chat.previousTip', - title: localize2('chatTip.previous', "Previous Tip"), + title: localize2('chatTip.previous', "Previous tip"), icon: Codicon.chevronLeft, f1: false, menu: [{ @@ -158,7 +161,7 @@ registerAction2(class NextTipAction extends Action2 { constructor() { super({ id: 'workbench.action.chat.nextTip', - title: localize2('chatTip.next', "Next Tip"), + title: localize2('chatTip.next', "Next tip"), icon: Codicon.chevronRight, f1: false, menu: [{ @@ -180,7 +183,7 @@ registerAction2(class DismissTipToolbarAction extends Action2 { constructor() { super({ id: 'workbench.action.chat.dismissTipToolbar', - title: localize2('chatTip.dismissButton', "Dismiss Tip"), + title: localize2('chatTip.dismissButton', "Dismiss tip"), icon: Codicon.check, f1: false, menu: [{ @@ -196,26 +199,6 @@ registerAction2(class DismissTipToolbarAction extends Action2 { } }); -registerAction2(class CloseTipToolbarAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.closeTip', - title: localize2('chatTip.close', "Close Tips"), - icon: Codicon.close, - f1: false, - menu: [{ - id: MenuId.ChatTipToolbar, - group: 'navigation', - order: 4, - }] - }); - } - - override async run(accessor: ServicesAccessor): Promise { - accessor.get(IChatTipService).hideTip(); - } -}); - //#endregion //#region Tip context menu actions @@ -244,17 +227,51 @@ registerAction2(class DisableTipsAction extends Action2 { super({ id: 'workbench.action.chat.disableTips', title: localize2('chatTip.disableTips', "Disable tips"), + icon: Codicon.bellSlash, f1: false, menu: [{ id: MenuId.ChatTipContext, group: 'chatTip', order: 2, + }, { + id: MenuId.ChatTipToolbar, + group: 'navigation', + order: 5, }] }); } override async run(accessor: ServicesAccessor): Promise { - await accessor.get(IChatTipService).disableTips(); + const dialogService = accessor.get(IDialogService); + const chatTipService = accessor.get(IChatTipService); + const commandService = accessor.get(ICommandService); + + const { result } = await dialogService.prompt({ + message: localize('chatTip.disableConfirmTitle', "Disable tips?"), + custom: { + markdownDetails: [{ + markdown: new MarkdownString(localize('chatTip.disableConfirmDetail', "New tips are added frequently to help you get the most out of Copilot. You can re-enable tips anytime from the `chat.tips.enabled` setting.")), + }], + }, + buttons: [ + { + label: localize('chatTip.disableConfirmButton', "Disable tips"), + run: () => true, + }, + { + label: localize('chatTip.openSettingButton', "Open Setting"), + run: () => { + commandService.executeCommand('workbench.action.openSettings', 'chat.tips.enabled'); + return false; + }, + }, + ], + cancelButton: true, + }); + + if (result) { + await chatTipService.disableTips(); + } } }); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css index 67ecd94b482f8..33cd5d5783163 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css @@ -3,49 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.interactive-item-container .chat-tip-widget { - display: flex; - align-items: center; - gap: 4px; - margin-bottom: 8px; - padding: 6px 10px; - border-radius: 4px; - border: 1px solid var(--vscode-editorWidget-border, var(--vscode-input-border, transparent)); - background-color: var(--vscode-editorWidget-background); - font-size: var(--vscode-chat-font-size-body-s); - font-family: var(--vscode-chat-font-family, inherit); - color: var(--vscode-descriptionForeground); - position: relative; -} - -.interactive-item-container .chat-tip-widget .codicon-lightbulb { - font-size: 12px; - color: var(--vscode-notificationsWarningIcon-foreground); -} - -.interactive-item-container .chat-tip-widget a { - color: var(--vscode-textLink-foreground); -} - -.interactive-item-container .chat-tip-widget a:hover, -.interactive-item-container .chat-tip-widget a:active { - color: var(--vscode-textLink-activeForeground); -} - -.interactive-item-container .chat-tip-widget .rendered-markdown p { - margin: 0; -} - -.interactive-item-container .chat-tip-widget .chat-tip-toolbar, .chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar { opacity: 0; pointer-events: none; } -.interactive-item-container .chat-tip-widget .chat-tip-toolbar > .monaco-toolbar, .chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar > .monaco-toolbar { position: absolute; - top: -15px; + top: -20px; right: 10px; height: 26px; line-height: 26px; @@ -56,27 +21,23 @@ transition: opacity 0.1s ease-in-out; } -.interactive-item-container .chat-tip-widget:hover .chat-tip-toolbar, -.interactive-item-container .chat-tip-widget:focus-within .chat-tip-toolbar, .chat-getting-started-tip-container .chat-tip-widget:hover .chat-tip-toolbar, .chat-getting-started-tip-container .chat-tip-widget:focus-within .chat-tip-toolbar { opacity: 1; pointer-events: auto; } -.interactive-item-container .chat-tip-widget .chat-tip-toolbar .action-item, .chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar .action-item { height: 24px; width: 24px; margin: 1px 2px; } -.interactive-item-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label, .chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label { color: var(--vscode-descriptionForeground); + padding: 4px; } -.interactive-item-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label:hover, .chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label:hover { background-color: var(--vscode-toolbar-hoverBackground); color: var(--vscode-foreground); @@ -122,17 +83,3 @@ .chat-getting-started-tip-container .chat-tip-widget .rendered-markdown p { margin: 0; } - -/* Override bubble styling for tip widget's rendered markdown in chat editor */ -.interactive-session:not(.chat-widget > .interactive-session) { - .interactive-item-container.interactive-request .value .chat-tip-widget .rendered-markdown { - background-color: transparent; - border-radius: 0; - padding: 0; - max-width: none; - margin-left: 0; - width: auto; - margin-bottom: 0; - position: static; - } -} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index df048af089739..47d1d634c6c47 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -96,7 +96,6 @@ import { ChatTaskContentPart } from './chatContentParts/chatTaskContentPart.js'; import { ChatTextEditContentPart } from './chatContentParts/chatTextEditContentPart.js'; import { ChatThinkingContentPart } from './chatContentParts/chatThinkingContentPart.js'; import { ChatSubagentContentPart } from './chatContentParts/chatSubagentContentPart.js'; -import { ChatTipContentPart } from './chatContentParts/chatTipContentPart.js'; import { ChatTreeContentPart, TreePool } from './chatContentParts/chatTreeContentPart.js'; import { ChatWorkspaceEditContentPart } from './chatContentParts/chatWorkspaceEditContentPart.js'; import { ChatToolInvocationPart } from './chatContentParts/toolInvocationParts/chatToolInvocationPart.js'; @@ -105,7 +104,6 @@ import { ChatEditorOptions } from './chatOptions.js'; import { ChatCodeBlockContentProvider, CodeBlockPart } from './chatContentParts/codeBlockPart.js'; import { autorun, observableValue } from '../../../../../base/common/observable.js'; import { isEqual } from '../../../../../base/common/resources.js'; -import { IChatTipService } from '../chatTipService.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; import { ChatHookContentPart } from './chatContentParts/chatHookContentPart.js'; import { ChatPendingDragController } from './chatPendingDragAndDrop.js'; @@ -188,8 +186,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer(); private readonly _autoReply: ChatQuestionCarouselAutoReply; - private _activeTipPart: ChatTipContentPart | undefined; - private readonly _notifiedQuestionCarousels = new Set(); private readonly _questionCarouselToast = this._register(new DisposableStore()); @@ -259,7 +255,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.chatTipService.getNextTip(element.id, element.timestamp, this.contextKeyService), - ); - templateData.value.appendChild(tipPart.domNode); - this._activeTipPart = tipPart; - templateData.elementDisposables.add(tipPart); - templateData.elementDisposables.add(tipPart.onDidHide(() => { - tipPart.domNode.remove(); - if (this._activeTipPart === tipPart) { - this._activeTipPart = undefined; - } - })); - templateData.elementDisposables.add({ - dispose: () => { - if (this._activeTipPart === tipPart) { - this._activeTipPart = undefined; - } - } - }); - } - let inlineSlashCommandRendered = false; content.forEach((data, contentIndex) => { const context: IChatContentPartRenderContext = { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts index 50b40d5941999..45d54dedc1560 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts @@ -789,20 +789,7 @@ export class ChatListWidget extends Disposable { return this._renderer.editorsInUse(); } - /** - * Whether the active tip currently has focus. - */ - hasTipFocus(): boolean { - return this._renderer.hasTipFocus(); - } - /** - * Focus the active tip, if any. - * @returns Whether a tip was focused. - */ - focusTip(): boolean { - return this._renderer.focusTip(); - } /** * Get template data for a request ID. diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index fe9b64f5b8578..5c144cd73aebf 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -13,6 +13,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; +import { hash } from '../../../../../base/common/hash.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../../base/common/iterator.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, thenIfNotDisposed } from '../../../../../base/common/lifecycle.js'; @@ -67,7 +68,7 @@ import { ILanguageModelToolsService, isToolSet } from '../../common/tools/langua import { ComputeAutomaticInstructions } from '../../common/promptSyntax/computeAutomaticInstructions.js'; import { PromptsConfig } from '../../common/promptSyntax/config/config.js'; import { IHandOff, PromptHeader } from '../../common/promptSyntax/promptFileParser.js'; -import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js'; +import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { handleModeSwitch } from '../actions/chatActions.js'; import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewModelChangeEvent, IChatWidgetViewOptions, isIChatResourceViewContext, isIChatViewViewContext } from '../chat.js'; import { ChatAttachmentModel } from '../attachments/chatAttachmentModel.js'; @@ -153,6 +154,22 @@ type ChatHandoffWidgetShownClassification = { handoffCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of handoff options shown to the user' }; }; +type ChatPromptRunEvent = { + storage: PromptsStorage; + extensionId?: string; + promptName?: string; + promptNameHash?: string; +}; + +type ChatPromptRunClassification = { + owner: 'digitarald'; + comment: 'Event fired when a prompt slash command is resolved into a follow instructions request'; + storage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Where the prompt is stored (local, user, extension).' }; + extensionId?: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Identifier of the extension that contributed the prompt, when applicable.' }; + promptName?: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Name of the core or extension-contributed prompt.' }; + promptNameHash?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Hashed name of local or user prompt for privacy.' }; +}; + const supportsAllAttachments: Required = { supportsFileAttachments: true, supportsToolAttachments: true, @@ -238,6 +255,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private readonly welcomePart: MutableDisposable = this._register(new MutableDisposable()); private readonly _gettingStartedTipPart = this._register(new MutableDisposable()); + private _gettingStartedTipPartRef: ChatTipContentPart | undefined; private readonly chatSuggestNextWidget: ChatSuggestNextWidget; @@ -402,6 +420,23 @@ export class ChatWidget extends Disposable implements IChatWidget { this._register(this.chatEntitlementService.onDidChangeAnonymous(() => this.renderWelcomeViewContentIfNeeded())); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('chat.tips.enabled')) { + if (!this.configurationService.getValue('chat.tips.enabled')) { + // Clear the existing tip so it doesn't linger + if (this.inputPart) { + this._gettingStartedTipPartRef = undefined; + this._gettingStartedTipPart.clear(); + const tipContainer = this.inputPart.gettingStartedTipContainerElement; + dom.clearNode(tipContainer); + dom.setVisibility(false, tipContainer); + } + } else { + this.updateChatViewVisibility(); + } + } + })); + this._register(bindContextKey(decidedChatEditingResourceContextKey, contextKeyService, (reader) => { const currentSession = this._editingSession.read(reader); if (!currentSession) { @@ -751,12 +786,16 @@ export class ChatWidget extends Disposable implements IChatWidget { } toggleTipFocus(): boolean { - if (this.listWidget.hasTipFocus()) { + if (this._gettingStartedTipPartRef?.hasFocus()) { this.focusInput(); return true; } - return this.listWidget.focusTip(); + if (!this._gettingStartedTipPartRef) { + return false; + } + this._gettingStartedTipPartRef.focus(); + return true; } hasInputFocus(): boolean { @@ -862,6 +901,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } else { // Dispose the cached tip part so the next empty state picks a // fresh (rotated) tip instead of re-showing the stale one. + this._gettingStartedTipPartRef = undefined; this._gettingStartedTipPart.clear(); dom.clearNode(tipContainer); dom.setVisibility(false, tipContainer); @@ -955,11 +995,14 @@ export class ChatWidget extends Disposable implements IChatWidget { () => this.chatTipService.getWelcomeTip(this.contextKeyService), )); tipContainer.appendChild(tipPart.domNode); + this._gettingStartedTipPartRef = tipPart; store.add(tipPart.onDidHide(() => { tipPart.domNode.remove(); + this._gettingStartedTipPartRef = undefined; this._gettingStartedTipPart.clear(); dom.setVisibility(false, tipContainer); + this.focusInput(); })); this._gettingStartedTipPart.value = store; @@ -2174,6 +2217,18 @@ export class ChatWidget extends Disposable implements IChatWidget { // remove the slash command from the input requestInput.input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim(); + const promptPath = slashCommand.promptPath; + const promptRunEvent: ChatPromptRunEvent = { + storage: promptPath.storage, + }; + if (promptPath.storage === PromptsStorage.extension) { + promptRunEvent.extensionId = promptPath.extension.identifier.value; + promptRunEvent.promptName = slashCommand.name; + } else { + promptRunEvent.promptNameHash = hash(slashCommand.name).toString(16); + } + this.telemetryService.publicLog2('chat.promptRun', promptRunEvent); + const input = requestInput.input.trim(); requestInput.input = `Follow instructions in [${basename(parseResult.uri)}](${parseResult.uri.toString()}).`; if (input) { @@ -2185,6 +2240,30 @@ export class ChatWidget extends Disposable implements IChatWidget { } } + private hasPendingQuestionCarousel(response: IChatResponseModel | undefined): boolean { + return Boolean(response?.response.value.some(part => part.kind === 'questionCarousel' && !part.isUsed)); + } + + + private dismissPendingQuestionCarousel(): void { + if (!this.viewModel) { + return; + } + + const responseId = this.input.questionCarouselResponseId; + if (!responseId || this.viewModel.model.lastRequest?.id !== responseId) { + return; + } + + const carouselPart = this.input.questionCarousel; + if (!carouselPart) { + return; + } + + carouselPart.ignore(); + this.input.clearQuestionCarousel(responseId); + } + private async _acceptInput(query: { query: string } | undefined, options: IChatAcceptInputOptions = {}): Promise { if (!query && this.input.generating) { // if the user submits the input and generation finishes quickly, just submit it for them @@ -2240,6 +2319,15 @@ export class ChatWidget extends Disposable implements IChatWidget { } const model = this.viewModel.model; + + // Enable steering while a question carousel is pending, useful for when the questions are off track and the user needs to course correct. + const hasPendingQuestionCarousel = this.hasPendingQuestionCarousel(model.lastRequest?.response); + const shouldAutoSteer = hasPendingQuestionCarousel && options.queue === undefined; + if (shouldAutoSteer) { + options.queue = ChatRequestQueueKind.Steering; + this.dismissPendingQuestionCarousel(); + } + const requestInProgress = model.requestInProgress.get(); if (requestInProgress) { options.queue ??= ChatRequestQueueKind.Queued; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatEditsTree.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatEditsTree.ts deleted file mode 100644 index 8917b4edfa8fe..0000000000000 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatEditsTree.ts +++ /dev/null @@ -1,636 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../../../base/browser/dom.js'; -import { addDisposableListener } from '../../../../../../base/browser/dom.js'; -import { ITreeRenderer, ITreeNode, IObjectTreeElement, ObjectTreeElementCollapseState } from '../../../../../../base/browser/ui/tree/tree.js'; -import { IIdentityProvider, IListVirtualDelegate } from '../../../../../../base/browser/ui/list/list.js'; -import { Codicon } from '../../../../../../base/common/codicons.js'; -import { comparePaths } from '../../../../../../base/common/comparers.js'; -import { Emitter, Event } from '../../../../../../base/common/event.js'; -import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; -import { matchesSomeScheme, Schemas } from '../../../../../../base/common/network.js'; -import { basename } from '../../../../../../base/common/path.js'; -import { basenameOrAuthority, dirname, isEqual, isEqualAuthority, isEqualOrParent } from '../../../../../../base/common/resources.js'; -import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js'; -import { ThemeIcon } from '../../../../../../base/common/themables.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { localize } from '../../../../../../nls.js'; -import { MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; -import { MenuId } from '../../../../../../platform/actions/common/actions.js'; -import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; -import { FileKind } from '../../../../../../platform/files/common/files.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; -import { ILabelService } from '../../../../../../platform/label/common/label.js'; -import { IOpenEvent, WorkbenchList, WorkbenchObjectTree } from '../../../../../../platform/list/browser/listService.js'; -import { IProductService } from '../../../../../../platform/product/common/productService.js'; -import { IStorageService, StorageScope } from '../../../../../../platform/storage/common/storage.js'; -import { isDark } from '../../../../../../platform/theme/common/theme.js'; -import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; -import { IResourceLabel, ResourceLabels } from '../../../../../browser/labels.js'; -import { SETTINGS_AUTHORITY } from '../../../../../services/preferences/common/preferences.js'; -import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; -import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../../common/chatService/chatService.js'; -import { chatEditingWidgetFileStateContextKey, IChatEditingSession } from '../../../common/editing/chatEditingService.js'; -import { CHAT_EDITS_VIEW_MODE_STORAGE_KEY } from '../../chatEditing/chatEditingActions.js'; -import { createFileIconThemableTreeContainerScope } from '../../../../files/browser/views/explorerView.js'; -import { CollapsibleListPool, IChatCollapsibleListItem, ICollapsibleListTemplate } from '../chatContentParts/chatReferencesContentPart.js'; -import { IDisposableReference } from '../chatContentParts/chatCollections.js'; - -const $ = dom.$; - -/** - * Represents a folder node in the tree view. - */ -export interface IChatEditsFolderElement { - readonly kind: 'folder'; - readonly uri: URI; - readonly children: IChatCollapsibleListItem[]; -} - -/** - * Union type for elements in the chat edits tree. - */ -export type IChatEditsTreeElement = IChatCollapsibleListItem | IChatEditsFolderElement; - -/** - * Find the common ancestor directory among a set of URIs. - * Returns undefined if the URIs have no common ancestor (different schemes/authorities). - */ -function findCommonAncestorUri(uris: readonly URI[]): URI | undefined { - if (uris.length === 0) { - return undefined; - } - let common = uris[0]; - for (let i = 1; i < uris.length; i++) { - while (!isEqualOrParent(uris[i], common)) { - const parent = dirname(common); - if (isEqual(parent, common)) { - return undefined; // reached filesystem root - } - common = parent; - } - } - return common; -} - -/** - * Convert a flat list of chat edits items into a tree grouped by directory. - * Files at the common ancestor directory are shown at the root level without a folder row. - */ -export function buildEditsTree(items: readonly IChatCollapsibleListItem[]): IObjectTreeElement[] { - // Group files by their directory - const folderMap = new Map(); - const itemsWithoutUri: IChatCollapsibleListItem[] = []; - - for (const item of items) { - if (item.kind === 'reference' && URI.isUri(item.reference)) { - const folderUri = dirname(item.reference); - const key = folderUri.toString(); - let group = folderMap.get(key); - if (!group) { - group = { uri: folderUri, items: [] }; - folderMap.set(key, group); - } - group.items.push(item); - } else { - itemsWithoutUri.push(item); - } - } - - const result: IObjectTreeElement[] = []; - - // Add items without URIs as top-level items (e.g., warnings) - for (const item of itemsWithoutUri) { - result.push({ element: item }); - } - - if (folderMap.size === 0) { - return result; - } - - // Find common ancestor so we can flatten files at the root level - const folderUris = [...folderMap.values()].map(f => f.uri); - const commonAncestor = findCommonAncestorUri(folderUris); - - // Sort folders by path - const sortedFolders = [...folderMap.values()].sort((a, b) => - comparePaths(a.uri.fsPath, b.uri.fsPath) - ); - - // Emit folders first, then root-level files (matching search tree behavior) - const rootFiles: IObjectTreeElement[] = []; - for (const folder of sortedFolders) { - const isAtCommonAncestor = commonAncestor && isEqual(folder.uri, commonAncestor); - if (isAtCommonAncestor) { - // Files at the common ancestor go at the root level, after all folders - for (const item of folder.items) { - rootFiles.push({ element: item }); - } - } else { - const folderElement: IChatEditsFolderElement = { - kind: 'folder', - uri: folder.uri, - children: folder.items, - }; - result.push({ - element: folderElement, - children: folder.items.map(item => ({ element: item as IChatEditsTreeElement })), - collapsible: true, - collapsed: ObjectTreeElementCollapseState.PreserveOrExpanded, - }); - } - } - - // Root-level files come after folders - result.push(...rootFiles); - - return result; -} - -/** - * Convert a flat list into tree elements without grouping (list mode). - */ -export function buildEditsList(items: readonly IChatCollapsibleListItem[]): IObjectTreeElement[] { - return items.map(item => ({ element: item as IChatEditsTreeElement })); -} - -/** - * Delegate for the chat edits tree that returns element heights and template IDs. - */ -export class ChatEditsTreeDelegate implements IListVirtualDelegate { - getHeight(_element: IChatEditsTreeElement): number { - return 22; - } - - getTemplateId(element: IChatEditsTreeElement): string { - if (element.kind === 'folder') { - return ChatEditsFolderRenderer.TEMPLATE_ID; - } - return ChatEditsFileTreeRenderer.TEMPLATE_ID; - } -} - -/** - * Identity provider for the chat edits tree. - * Provides stable string IDs so the tree can preserve collapse/selection state across updates. - */ -export class ChatEditsTreeIdentityProvider implements IIdentityProvider { - getId(element: IChatEditsTreeElement): string { - if (element.kind === 'folder') { - return `folder:${element.uri.toString()}`; - } - if (element.kind === 'warning') { - return `warning:${element.content.value}`; - } - const ref = element.reference; - if (typeof ref === 'string') { - return `ref:${ref}`; - } else if (URI.isUri(ref)) { - return `file:${ref.toString()}`; - } else { - // eslint-disable-next-line local/code-no-in-operator - return `file:${'uri' in ref ? ref.uri.toString() : String(ref)}`; - } - } -} - -interface IChatEditsFolderTemplate { - readonly label: IResourceLabel; - readonly templateDisposables: DisposableStore; -} - -/** - * Renderer for folder elements in the chat edits tree. - */ -export class ChatEditsFolderRenderer implements ITreeRenderer { - static readonly TEMPLATE_ID = 'chatEditsFolderRenderer'; - readonly templateId = ChatEditsFolderRenderer.TEMPLATE_ID; - - constructor( - private readonly labels: ResourceLabels, - private readonly labelService: ILabelService, - ) { } - - renderTemplate(container: HTMLElement): IChatEditsFolderTemplate { - const templateDisposables = new DisposableStore(); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true, supportIcons: true })); - return { label, templateDisposables }; - } - - renderElement(node: ITreeNode, _index: number, templateData: IChatEditsFolderTemplate): void { - const element = node.element; - if (element.kind !== 'folder') { - return; - } - const relativeLabel = this.labelService.getUriLabel(element.uri, { relative: true }); - templateData.label.setResource( - { resource: element.uri, name: relativeLabel || basename(element.uri.path) }, - { fileKind: FileKind.FOLDER, fileDecorations: undefined } - ); - } - - disposeTemplate(templateData: IChatEditsFolderTemplate): void { - templateData.templateDisposables.dispose(); - } -} - -/** - * Tree renderer for file elements in the chat edits tree. - * Adapted from CollapsibleListRenderer to work with ITreeNode. - */ -export class ChatEditsFileTreeRenderer implements ITreeRenderer { - static readonly TEMPLATE_ID = 'chatEditsFileRenderer'; - readonly templateId = ChatEditsFileTreeRenderer.TEMPLATE_ID; - - constructor( - private readonly labels: ResourceLabels, - private readonly menuId: MenuId | undefined, - @IThemeService private readonly themeService: IThemeService, - @IProductService private readonly productService: IProductService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - ) { } - - renderTemplate(container: HTMLElement): ICollapsibleListTemplate { - const templateDisposables = new DisposableStore(); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true, supportIcons: true })); - - const fileDiffsContainer = $('.working-set-line-counts'); - const addedSpan = dom.$('.working-set-lines-added'); - const removedSpan = dom.$('.working-set-lines-removed'); - fileDiffsContainer.appendChild(addedSpan); - fileDiffsContainer.appendChild(removedSpan); - label.element.appendChild(fileDiffsContainer); - - let toolbar; - let actionBarContainer; - let contextKeyService; - if (this.menuId) { - actionBarContainer = $('.chat-collapsible-list-action-bar'); - contextKeyService = templateDisposables.add(this.contextKeyService.createScoped(actionBarContainer)); - const scopedInstantiationService = templateDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]))); - toolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, this.menuId, { menuOptions: { shouldForwardArgs: true, arg: undefined } })); - label.element.appendChild(actionBarContainer); - } - - return { templateDisposables, label, toolbar, actionBarContainer, contextKeyService, fileDiffsContainer, addedSpan, removedSpan }; - } - - private getReferenceIcon(data: IChatContentReference): URI | ThemeIcon | undefined { - if (ThemeIcon.isThemeIcon(data.iconPath)) { - return data.iconPath; - } else { - return isDark(this.themeService.getColorTheme().type) && data.iconPath?.dark - ? data.iconPath?.dark - : data.iconPath?.light; - } - } - - renderElement(node: ITreeNode, _index: number, templateData: ICollapsibleListTemplate): void { - const data = node.element; - if (data.kind === 'folder') { - return; - } - - if (data.kind === 'warning') { - templateData.label.setResource({ name: data.content.value }, { icon: Codicon.warning }); - return; - } - - const reference = data.reference; - const icon = this.getReferenceIcon(data); - templateData.label.element.style.display = 'flex'; - let arg: URI | undefined; - // eslint-disable-next-line local/code-no-in-operator - if (typeof reference === 'object' && 'variableName' in reference) { - if (reference.value) { - const uri = URI.isUri(reference.value) ? reference.value : reference.value.uri; - templateData.label.setResource( - { - resource: uri, - name: basenameOrAuthority(uri), - description: `#${reference.variableName}`, - // eslint-disable-next-line local/code-no-in-operator - range: 'range' in reference.value ? reference.value.range : undefined, - }, { icon, title: data.options?.status?.description ?? data.title }); - } else if (reference.variableName.startsWith('kernelVariable')) { - const variable = reference.variableName.split(':')[1]; - const asVariableName = `${variable}`; - const label = `Kernel variable`; - templateData.label.setLabel(label, asVariableName, { title: data.options?.status?.description }); - } else { - templateData.label.setLabel('Unknown variable type: ' + reference.variableName); - } - } else if (typeof reference === 'string') { - templateData.label.setLabel(reference, undefined, { iconPath: URI.isUri(icon) ? icon : undefined, title: data.options?.status?.description ?? data.title }); - } else { - // eslint-disable-next-line local/code-no-in-operator - const uri = 'uri' in reference ? reference.uri : reference; - arg = uri; - if (uri.scheme === 'https' && isEqualAuthority(uri.authority, 'github.com') && uri.path.includes('/tree/')) { - templateData.label.setResource({ resource: uri, name: basename(uri.path) }, { icon: Codicon.github, title: data.title }); - } else if (uri.scheme === this.productService.urlProtocol && isEqualAuthority(uri.authority, SETTINGS_AUTHORITY)) { - const settingId = uri.path.substring(1); - templateData.label.setResource({ resource: uri, name: settingId }, { icon: Codicon.settingsGear, title: localize('setting.hover', "Open setting '{0}'", settingId) }); - } else if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) { - templateData.label.setResource({ resource: uri, name: uri.toString(true) }, { icon: icon ?? Codicon.globe, title: data.options?.status?.description ?? data.title ?? uri.toString(true) }); - } else { - templateData.label.setFile(uri, { - fileKind: FileKind.FILE, - fileDecorations: undefined, - // eslint-disable-next-line local/code-no-in-operator - range: 'range' in reference ? reference.range : undefined, - title: data.options?.status?.description ?? data.title, - }); - } - } - - for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { - // eslint-disable-next-line no-restricted-syntax - const element = templateData.label.element.querySelector(selector); - if (element) { - if (data.options?.status?.kind === ChatResponseReferencePartStatusKind.Omitted || data.options?.status?.kind === ChatResponseReferencePartStatusKind.Partial) { - element.classList.add('warning'); - } else { - element.classList.remove('warning'); - } - } - } - - if (data.state !== undefined) { - if (templateData.actionBarContainer) { - const diffMeta = data?.options?.diffMeta; - if (diffMeta) { - if (!templateData.fileDiffsContainer || !templateData.addedSpan || !templateData.removedSpan) { - return; - } - templateData.addedSpan.textContent = `+${diffMeta.added}`; - templateData.removedSpan.textContent = `-${diffMeta.removed}`; - templateData.fileDiffsContainer.setAttribute('aria-label', localize('chatEditingSession.fileCounts', '{0} lines added, {1} lines removed', diffMeta.added, diffMeta.removed)); - } - // eslint-disable-next-line no-restricted-syntax - templateData.label.element.querySelector('.monaco-icon-name-container')?.classList.add('modified'); - } - if (templateData.toolbar) { - templateData.toolbar.context = arg; - } - if (templateData.contextKeyService) { - chatEditingWidgetFileStateContextKey.bindTo(templateData.contextKeyService).set(data.state); - } - } - } - - disposeTemplate(templateData: ICollapsibleListTemplate): void { - templateData.templateDisposables.dispose(); - } -} - -/** - * Widget that renders the chat edits file list, supporting both flat list and tree views. - * Manages the lifecycle of the underlying tree or list widget, and handles toggling between modes. - */ -export class ChatEditsListWidget extends Disposable { - private readonly _onDidFocus = this._register(new Emitter()); - readonly onDidFocus: Event = this._onDidFocus.event; - - private readonly _onDidOpen = this._register(new Emitter>()); - readonly onDidOpen: Event> = this._onDidOpen.event; - - private _tree: WorkbenchObjectTree | undefined; - private _list: IDisposableReference> | undefined; - - private readonly _listPool: CollapsibleListPool; - private readonly _widgetDisposables = this._register(new DisposableStore()); - private readonly _chatEditsInTreeView: IContextKey; - - private _currentContainer: HTMLElement | undefined; - private _currentSession: IChatEditingSession | null = null; - private _lastEntries: readonly IChatCollapsibleListItem[] = []; - - get currentSession(): IChatEditingSession | null { - return this._currentSession; - } - - get selectedElements(): URI[] { - const edits: URI[] = []; - if (this._tree) { - for (const element of this._tree.getSelection()) { - if (element && element.kind === 'reference' && URI.isUri(element.reference)) { - edits.push(element.reference); - } - } - } else if (this._list) { - for (const element of this._list.object.getSelectedElements()) { - if (element.kind === 'reference' && URI.isUri(element.reference)) { - edits.push(element.reference); - } - } - } - return edits; - } - - constructor( - private readonly onDidChangeVisibility: Event, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IStorageService private readonly storageService: IStorageService, - @IThemeService private readonly themeService: IThemeService, - @ILabelService private readonly labelService: ILabelService, - ) { - super(); - - this._listPool = this._register(this.instantiationService.createInstance( - CollapsibleListPool, - this.onDidChangeVisibility, - MenuId.ChatEditingWidgetModifiedFilesToolbar, - { verticalScrollMode: ScrollbarVisibility.Visible }, - )); - - this._chatEditsInTreeView = ChatContextKeys.chatEditsInTreeView.bindTo(contextKeyService); - this._chatEditsInTreeView.set(this._isTreeMode); - - this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, CHAT_EDITS_VIEW_MODE_STORAGE_KEY, this._store)(() => { - const isTree = this._isTreeMode; - this._chatEditsInTreeView.set(isTree); - if (this._currentContainer) { - this.create(this._currentContainer, this._currentSession); - this.setEntries(this._lastEntries); - } - })); - } - - private get _isTreeMode(): boolean { - return this.storageService.get(CHAT_EDITS_VIEW_MODE_STORAGE_KEY, StorageScope.PROFILE, 'list') === 'tree'; - } - - /** - * Creates the appropriate widget (tree or list) inside the given container. - * Must be called before {@link setEntries}. - */ - create(container: HTMLElement, chatEditingSession: IChatEditingSession | null): void { - this._currentContainer = container; - this._currentSession = chatEditingSession; - this.clear(); - dom.clearNode(container); - - if (this._isTreeMode) { - this._createTree(container, chatEditingSession); - } else { - this._createList(container, chatEditingSession); - } - } - - /** - * Rebuild the widget (e.g. after a view mode toggle). - */ - rebuild(container: HTMLElement, chatEditingSession: IChatEditingSession | null): void { - this.create(container, chatEditingSession); - } - - /** - * Whether the current view mode has changed since the widget was last created. - */ - get needsRebuild(): boolean { - if (this._isTreeMode) { - return !this._tree; - } - return !this._list; - } - - /** - * Update the displayed entries. - */ - setEntries(entries: readonly IChatCollapsibleListItem[]): void { - this._lastEntries = entries; - if (this._tree) { - const treeElements = this._isTreeMode - ? buildEditsTree(entries) - : buildEditsList(entries); - - // Use the file entry count for height, not the tree-expanded count, - // so height stays consistent when toggling between tree and list modes - const maxItemsShown = 6; - const itemsShown = Math.min(entries.length, maxItemsShown); - const height = itemsShown * 22; - this._tree.layout(height); - this._tree.getHTMLElement().style.height = `${height}px`; - this._tree.setChildren(null, treeElements); - } else if (this._list) { - const maxItemsShown = 6; - const itemsShown = Math.min(entries.length, maxItemsShown); - const height = itemsShown * 22; - const list = this._list.object; - list.layout(height); - list.getHTMLElement().style.height = `${height}px`; - list.splice(0, list.length, entries); - } - } - - /** - * Dispose the current tree or list widget without disposing the outer widget. - */ - clear(): void { - this._widgetDisposables.clear(); - this._tree = undefined; - this._list = undefined; - } - - private _createTree(container: HTMLElement, chatEditingSession: IChatEditingSession | null): void { - const resourceLabels = this._widgetDisposables.add(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeVisibility })); - const treeContainer = dom.$('.chat-used-context-list'); - this._widgetDisposables.add(createFileIconThemableTreeContainerScope(treeContainer, this.themeService)); - - const tree = this._widgetDisposables.add(this.instantiationService.createInstance( - WorkbenchObjectTree, - 'ChatEditsTree', - treeContainer, - new ChatEditsTreeDelegate(), - [ - new ChatEditsFolderRenderer(resourceLabels, this.labelService), - this.instantiationService.createInstance(ChatEditsFileTreeRenderer, resourceLabels, MenuId.ChatEditingWidgetModifiedFilesToolbar), - ], - { - alwaysConsumeMouseWheel: false, - accessibilityProvider: { - getAriaLabel: (element: IChatEditsTreeElement) => { - if (element.kind === 'folder') { - return this.labelService.getUriLabel(element.uri, { relative: true }); - } - if (element.kind === 'warning') { - return element.content.value; - } - const reference = element.reference; - if (typeof reference === 'string') { - return reference; - } else if (URI.isUri(reference)) { - return this.labelService.getUriBasenameLabel(reference); - // eslint-disable-next-line local/code-no-in-operator - } else if ('uri' in reference) { - return this.labelService.getUriBasenameLabel(reference.uri); - } else { - return ''; - } - }, - getWidgetAriaLabel: () => localize('chatEditsTree', "Changed Files"), - }, - identityProvider: new ChatEditsTreeIdentityProvider(), - verticalScrollMode: ScrollbarVisibility.Visible, - hideTwistiesOfChildlessElements: true, - } - )); - - tree.updateOptions({ enableStickyScroll: false }); - - this._tree = tree; - - this._widgetDisposables.add(tree.onDidChangeFocus(() => { - this._onDidFocus.fire(); - })); - - this._widgetDisposables.add(tree.onDidOpen(e => { - this._onDidOpen.fire(e); - })); - - this._widgetDisposables.add(addDisposableListener(tree.getHTMLElement(), 'click', () => { - this._onDidFocus.fire(); - }, true)); - - dom.append(container, tree.getHTMLElement()); - } - - private _createList(container: HTMLElement, chatEditingSession: IChatEditingSession | null): void { - this._list = this._listPool.get(); - const list = this._list.object; - this._widgetDisposables.add(this._list); - - this._widgetDisposables.add(list.onDidFocus(() => { - this._onDidFocus.fire(); - })); - - this._widgetDisposables.add(list.onDidOpen(async (e) => { - if (e.element) { - this._onDidOpen.fire({ - element: e.element as IChatEditsTreeElement, - editorOptions: e.editorOptions, - sideBySide: e.sideBySide, - browserEvent: e.browserEvent, - }); - } - })); - - this._widgetDisposables.add(addDisposableListener(list.getHTMLElement(), 'click', () => { - this._onDidFocus.fire(); - }, true)); - - dom.append(container, list.getHTMLElement()); - } - - override dispose(): void { - this.clear(); - super.dispose(); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 839f0e04caada..f4ed42bfbc577 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -31,6 +31,7 @@ import { mixin } from '../../../../../../base/common/objects.js'; import { autorun, derived, derivedOpts, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; import { isMacintosh } from '../../../../../../base/common/platform.js'; import { isEqual } from '../../../../../../base/common/resources.js'; +import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js'; import { assertType } from '../../../../../../base/common/types.js'; import { URI } from '../../../../../../base/common/uri.js'; import { IEditorConstructionOptions } from '../../../../../../editor/browser/config/editorConfiguration.js'; @@ -62,6 +63,7 @@ import { registerAndCreateHistoryNavigationContext } from '../../../../../../pla import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { WorkbenchList } from '../../../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { ObservableMemento, observableMemento } from '../../../../../../platform/observable/common/observableMemento.js'; import { bindContextKey } from '../../../../../../platform/observable/common/platformObservableUtils.js'; @@ -102,17 +104,17 @@ import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmen import { ChatImplicitContexts } from '../../attachments/chatImplicitContext.js'; import { ImplicitContextAttachmentWidget } from '../../attachments/implicitContextAttachment.js'; import { IChatWidget, ISessionTypePickerDelegate, isIChatResourceViewContext, isIChatViewViewContext, IWorkspacePickerDelegate } from '../../chat.js'; -import { ChatEditingShowChangesAction, ChatEditsViewAsListActionId, ChatEditsViewAsTreeActionId, ViewAllSessionChangesAction, ViewPreviousEditsAction } from '../../chatEditing/chatEditingActions.js'; +import { ChatEditingShowChangesAction, ViewAllSessionChangesAction, ViewPreviousEditsAction } from '../../chatEditing/chatEditingActions.js'; import { resizeImage } from '../../chatImageUtils.js'; import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../chatSessions/chatSessionPickerActionItem.js'; import { SearchableOptionPickerActionItem } from '../../chatSessions/searchableOptionPickerActionItem.js'; import { IChatContextService } from '../../contextContrib/chatContextService.js'; +import { IDisposableReference } from '../chatContentParts/chatCollections.js'; import { ChatQuestionCarouselPart, IChatQuestionCarouselOptions } from '../chatContentParts/chatQuestionCarouselPart.js'; import { IChatContentPartRenderContext } from '../chatContentParts/chatContentParts.js'; -import { IChatCollapsibleListItem } from '../chatContentParts/chatReferencesContentPart.js'; +import { CollapsibleListPool, IChatCollapsibleListItem } from '../chatContentParts/chatReferencesContentPart.js'; import { ChatTodoListWidget } from '../chatContentParts/chatTodoListWidget.js'; import { ChatDragAndDrop } from '../chatDragAndDrop.js'; -import { ChatEditsListWidget } from './chatEditsTree.js'; import { ChatFollowups } from './chatFollowups.js'; import { ChatInputPartWidgetController } from './chatInputPartWidgets.js'; import { IChatInputPickerOptions } from './chatInputPickerActionItem.js'; @@ -428,11 +430,21 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _workingSetLinesRemovedSpan = new Lazy(() => dom.$('.working-set-lines-removed')); private readonly _chatEditsActionsDisposables: DisposableStore = this._register(new DisposableStore()); + private readonly _chatEditsDisposables: DisposableStore = this._register(new DisposableStore()); private readonly _renderingChatEdits = this._register(new MutableDisposable()); - private readonly _chatEditsListWidget = this._register(new MutableDisposable()); + private _chatEditsListPool: CollapsibleListPool; + private _chatEditList: IDisposableReference> | undefined; get selectedElements(): URI[] { - return this._chatEditsListWidget.value?.selectedElements ?? []; + const edits = []; + const editsList = this._chatEditList?.object; + const selectedElements = editsList?.getSelectedElements() ?? []; + for (const element of selectedElements) { + if (element.kind === 'reference' && URI.isUri(element.reference)) { + edits.push(element.reference); + } + } + return edits; } private _attemptedWorkingSetEntriesCount: number = 0; @@ -588,6 +600,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.inputEditor.updateOptions(newOptions); })); + this._chatEditsListPool = this._register(this.instantiationService.createInstance(CollapsibleListPool, this._onDidChangeVisibility.event, MenuId.ChatEditingWidgetModifiedFilesToolbar, { verticalScrollMode: ScrollbarVisibility.Visible })); + this._hasFileAttachmentContextKey = ChatContextKeys.hasFileAttachments.bindTo(contextKeyService); this.initSelectedModel(); @@ -2584,7 +2598,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ); } else { dom.clearNode(this.chatEditingSessionWidgetContainer); - this._chatEditsListWidget.value?.clear(); + this._chatEditsDisposables.clear(); + this._chatEditList = undefined; } }); } @@ -2677,8 +2692,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }) : undefined, disableWhileRunning: isSessionMenu, buttonConfigProvider: (action) => { - if (action.id === ChatEditingShowChangesAction.ID || action.id === ViewPreviousEditsAction.Id || action.id === ViewAllSessionChangesAction.ID - || action.id === ChatEditsViewAsTreeActionId || action.id === ChatEditsViewAsListActionId) { + if (action.id === ChatEditingShowChangesAction.ID || action.id === ViewPreviousEditsAction.Id || action.id === ViewAllSessionChangesAction.ID) { return { showIcon: true, showLabel: false, isSecondary: true }; } return undefined; @@ -2729,51 +2743,54 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge workingSetContainer.classList.toggle('collapsed', collapsed); })); - if (!this._chatEditsListWidget.value || this._chatEditsListWidget.value.needsRebuild) { - if (!this._chatEditsListWidget.value) { - const widget = this.instantiationService.createInstance(ChatEditsListWidget, this._onDidChangeVisibility.event); - this._chatEditsListWidget.value = widget; - this._register(widget.onDidFocus(() => this._onDidFocus.fire())); - this._register(widget.onDidOpen(async (e) => { - const element = e.element; - if (!element || element.kind === 'folder' || element.kind === 'warning') { + if (!this._chatEditList) { + this._chatEditList = this._chatEditsListPool.get(); + const list = this._chatEditList.object; + this._chatEditsDisposables.add(this._chatEditList); + this._chatEditsDisposables.add(list.onDidFocus(() => { + this._onDidFocus.fire(); + })); + this._chatEditsDisposables.add(list.onDidOpen(async (e) => { + if (e.element?.kind === 'reference' && URI.isUri(e.element.reference)) { + const modifiedFileUri = e.element.reference; + const originalUri = e.element.options?.originalUri; + + if (e.element.options?.isDeletion && originalUri) { + await this.editorService.openEditor({ + resource: originalUri, // instead of modified, because modified will not exist + options: e.editorOptions + }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); return; } - if (element.kind === 'reference' && URI.isUri(element.reference)) { - const modifiedFileUri = element.reference; - const originalUri = element.options?.originalUri; - - if (element.options?.isDeletion && originalUri) { - await this.editorService.openEditor({ - resource: originalUri, - options: e.editorOptions - }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); - return; - } - if (originalUri) { - await this.editorService.openEditor({ - original: { resource: originalUri }, - modified: { resource: modifiedFileUri }, - options: e.editorOptions - }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); - return; - } - - // Use the widget's current session, not a stale closure - const entry = widget.currentSession?.getEntry(modifiedFileUri); - const pane = await this.editorService.openEditor({ - resource: modifiedFileUri, + // If there's a originalUri, open as diff editor + if (originalUri) { + await this.editorService.openEditor({ + original: { resource: originalUri }, + modified: { resource: modifiedFileUri }, options: e.editorOptions }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + return; + } - if (pane) { - entry?.getEditorIntegration(pane).reveal(true, e.editorOptions.preserveFocus); - } + const entry = chatEditingSession?.getEntry(modifiedFileUri); + + const pane = await this.editorService.openEditor({ + resource: modifiedFileUri, + options: e.editorOptions + }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + + if (pane) { + entry?.getEditorIntegration(pane).reveal(true, e.editorOptions.preserveFocus); } - })); - } - this._chatEditsListWidget.value.rebuild(workingSetContainer, chatEditingSession); + } + })); + this._chatEditsDisposables.add(addDisposableListener(list.getHTMLElement(), 'click', e => { + if (!this.hasFocus()) { + this._onDidFocus.fire(); + } + }, true)); + dom.append(workingSetContainer, list.getHTMLElement()); dom.append(innerContainer, workingSetContainer); } @@ -2786,7 +2803,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // entries, while background chat sessions use session file changes. const allEntries = editEntries.concat(sessionFileEntries); - this._chatEditsListWidget.value?.setEntries(allEntries); + const maxItemsShown = 6; + const itemsShown = Math.min(allEntries.length, maxItemsShown); + const height = itemsShown * 22; + const list = this._chatEditList!.object; + list.layout(height); + list.getHTMLElement().style.height = `${height}px`; + list.splice(0, list.length, allEntries); })); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts index 37a075a3970bc..9e5218f2afbf1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts @@ -54,8 +54,7 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt return true; // Always show active session type } - const contribution = this.chatSessionsService.getChatSessionContribution(type); - return getAgentCanContinueIn(type, contribution); + return getAgentCanContinueIn(type); } protected override _getSessionCategory(sessionTypeItem: ISessionTypeItem) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index c14a5416cda22..c5ab3aae3f321 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -163,10 +163,6 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { const contributions = this.chatSessionsService.getAllChatSessionContributions(); for (const contribution of contributions) { - if (contribution.isReadOnly) { - continue; // Read-only sessions are not interactive and should not appear in session target picker - } - const agentSessionType = getAgentSessionProvider(contribution.type); if (!agentSessionType) { continue; diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 8ed7dc44ac561..ff8f0fc8f16da 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -2108,12 +2108,6 @@ have to be updated for changes to the rules above, or to support more deeply nes display: none; } -/* Tree view: remove twistie indent for leaf (non-collapsible) file rows */ -.interactive-session .chat-editing-session-list .monaco-tl-twistie:not(.collapsible) { - width: 0; - padding-right: 0; -} - .interactive-session .chat-summary-list .monaco-list .monaco-list-row { border-radius: 4px; } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts index 5b48266f16a4a..db2711bec0cd3 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts @@ -227,11 +227,21 @@ export class ChatContextUsageWidget extends Disposable { update(lastRequest: IChatRequestModel | undefined): void { this._lastRequestDisposable.clear(); - if (!lastRequest?.response || !lastRequest.modelId) { + if (!lastRequest) { + // New/empty chat session clear everything + this.currentData = undefined; this.hide(); return; } + if (!lastRequest.response || !lastRequest.modelId) { + // Pending request keep old data visible if available + if (!this.currentData) { + this.hide(); + } + return; + } + const response = lastRequest.response; const modelId = lastRequest.modelId; @@ -251,7 +261,9 @@ export class ChatContextUsageWidget extends Disposable { const maxOutputTokens = modelMetadata?.maxOutputTokens; if (!usage || !maxInputTokens || maxInputTokens <= 0 || !maxOutputTokens || maxOutputTokens <= 0) { - this.hide(); + if (!this.currentData) { + this.hide(); + } return; } diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 94bb20e75affc..c8f138be639a8 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -120,8 +120,6 @@ export namespace ChatContextKeys { export const hasMultipleAgentSessionsSelected = new RawContextKey('agentSessionHasMultipleSelected', false, { type: 'boolean', description: localize('agentSessionHasMultipleSelected', "True when multiple agent sessions are selected.") }); export const hasAgentSessionChanges = new RawContextKey('agentSessionHasChanges', false, { type: 'boolean', description: localize('agentSessionHasChanges', "True when the current agent session item has changes.") }); - export const chatEditsInTreeView = new RawContextKey('chatEditsInTreeView', false, { type: 'boolean', description: localize('chatEditsInTreeView', "True when the chat edits working set is displayed as a tree.") }); - export const isKatexMathElement = new RawContextKey('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") }); export const contextUsageHasBeenOpened = new RawContextKey('chatContextUsageHasBeenOpened', false, { type: 'boolean', description: localize('chatContextUsageHasBeenOpened', "True when the user has opened the context window usage details.") }); diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 839510819e7c1..9f280a98fb88e 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -85,7 +85,6 @@ export interface IChatSessionsExtensionPoint { readonly capabilities?: IChatAgentAttachmentCapabilities; readonly commands?: IChatSessionCommandContribution[]; readonly canDelegate?: boolean; - readonly isReadOnly?: boolean; /** * When set, the chat session will show a filtered mode picker with custom agents * that have a matching `target` property. This enables contributed chat sessions diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 2440311f09bba..ab2631dc4bbff 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -49,6 +49,7 @@ export enum ChatConfiguration { ExitAfterDelegation = 'chat.exitAfterDelegation', AgentsControlClickBehavior = 'chat.agentsControl.clickBehavior', ExplainChangesEnabled = 'chat.editing.explainChanges.enabled', + GrowthNotificationEnabled = 'chat.growthNotification.enabled', } /** diff --git a/src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts b/src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts index e16e8af4d8b2f..7de3b5ea61e8d 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts @@ -44,7 +44,7 @@ export function registerChatExportZipAction() { const fileService = accessor.get(IFileService); const configurationService = accessor.get(IConfigurationService); - const repoInfoEnabled = configurationService.getValue(ChatConfiguration.RepoInfoEnabled) ?? true; + const repoInfoEnabled = configurationService.getValue(ChatConfiguration.RepoInfoEnabled) ?? false; const widget = widgetService.lastFocusedWidget; if (!widget || !widget.viewModel) { diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index f7fb12e4c80c1..5c75ce87559fc 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -2100,13 +2100,8 @@ suite('AgentSessions', () => { suite('AgentSessionsViewModel - getAgentCanContinueIn', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('should return false when contribution.isReadOnly is true', () => { - const result = getAgentCanContinueIn(AgentSessionProviders.Cloud, { type: 'test', name: 'test', displayName: 'Test', description: 'test', isReadOnly: true }); - assert.strictEqual(result, false); - }); - - test('should return true for Cloud when contribution is not read-only', () => { - const result = getAgentCanContinueIn(AgentSessionProviders.Cloud, { type: 'test', name: 'test', displayName: 'Test', description: 'test', isReadOnly: false }); + test('should return true for Cloud provider', () => { + const result = getAgentCanContinueIn(AgentSessionProviders.Cloud); assert.strictEqual(result, true); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatSetup/chatSetupGrowthSession.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatSetup/chatSetupGrowthSession.test.ts new file mode 100644 index 0000000000000..d9da6ade3e0f9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatSetup/chatSetupGrowthSession.test.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILifecycleService } from '../../../../../services/lifecycle/common/lifecycle.js'; +import { TestLifecycleService, workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { ChatSessionStatus } from '../../../common/chatSessionsService.js'; +import { AgentSessionProviders } from '../../../browser/agentSessions/agentSessions.js'; +import { IAgentSession } from '../../../browser/agentSessions/agentSessionsModel.js'; +import { GrowthSessionController, GrowthSessionOpenerParticipant } from '../../../browser/chatSetup/chatSetupGrowthSession.js'; +import { IChatWidget, IChatWidgetService } from '../../../browser/chat.js'; +import { MockChatWidgetService } from '../widget/mockChatWidget.js'; +import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; + +class TestMockChatWidgetService extends MockChatWidgetService { + + private readonly _onDidAddWidget = new Emitter(); + override readonly onDidAddWidget = this._onDidAddWidget.event; + + fireDidAddWidget(): void { + this._onDidAddWidget.fire(undefined!); + } + + dispose(): void { + this._onDidAddWidget.dispose(); + } +} + +suite('GrowthSessionController', () => { + + const disposables = new DisposableStore(); + let instantiationService: TestInstantiationService; + let mockWidgetService: TestMockChatWidgetService; + + setup(() => { + instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + mockWidgetService = new TestMockChatWidgetService(); + disposables.add({ dispose: () => mockWidgetService.dispose() }); + const mockLifecycleService = disposables.add(new TestLifecycleService()); + instantiationService.stub(IChatWidgetService, mockWidgetService); + instantiationService.stub(ILifecycleService, mockLifecycleService); + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should return a single NeedsInput session item', () => { + const controller = disposables.add(instantiationService.createInstance(GrowthSessionController)); + + const items = controller.items; + assert.strictEqual(items.length, 1); + assert.strictEqual(items[0].status, ChatSessionStatus.NeedsInput); + assert.strictEqual(items[0].label, 'Try Copilot'); + assert.ok(items[0].resource.scheme === AgentSessionProviders.Growth); + }); + + test('should return empty items after dismiss', async () => { + const controller = disposables.add(instantiationService.createInstance(GrowthSessionController)); + assert.strictEqual(controller.items.length, 1); + + // Allow the lifecycle.when() promise to resolve and register the listener + await new Promise(r => setTimeout(r, 0)); + + // Fire widget add — should dismiss + mockWidgetService.fireDidAddWidget(); + assert.strictEqual(controller.items.length, 0); + }); + + test('should fire onDidChangeChatSessionItems on dismiss', async () => { + const controller = disposables.add(instantiationService.createInstance(GrowthSessionController)); + + let fired = false; + disposables.add(controller.onDidChangeChatSessionItems(() => { + fired = true; + })); + + await new Promise(r => setTimeout(r, 0)); + + mockWidgetService.fireDidAddWidget(); + assert.strictEqual(fired, true); + }); + + test('should not fire onDidChangeChatSessionItems twice', async () => { + const controller = disposables.add(instantiationService.createInstance(GrowthSessionController)); + + let fireCount = 0; + disposables.add(controller.onDidChangeChatSessionItems(() => { + fireCount++; + })); + + await new Promise(r => setTimeout(r, 0)); + + mockWidgetService.fireDidAddWidget(); + mockWidgetService.fireDidAddWidget(); + assert.strictEqual(fireCount, 1); + }); + + test('refresh is a no-op', async () => { + const controller = disposables.add(instantiationService.createInstance(GrowthSessionController)); + await controller.refresh(); + assert.strictEqual(controller.items.length, 1); + }); +}); + +suite('GrowthSessionOpenerParticipant', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should return false for non-Growth sessions', async () => { + const participant = new GrowthSessionOpenerParticipant(); + const session: IAgentSession = { + providerType: AgentSessionProviders.Local, + providerLabel: 'Local', + resource: URI.parse('local://session-1'), + status: ChatSessionStatus.Completed, + label: 'Test Session', + icon: Codicon.vm, + timing: { created: Date.now(), lastRequestStarted: undefined, lastRequestEnded: undefined }, + isArchived: () => false, + setArchived: () => { }, + isRead: () => true, + setRead: () => { }, + }; + + // The participant checks providerType before touching the accessor, + // so a stub accessor is sufficient for this test path. + const stubAccessor = { get: () => undefined } as unknown as ServicesAccessor; + const result = await participant.handleOpenSession(stubAccessor, session); + assert.strictEqual(result, false); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts index e923a89075113..28f9b609fa15e 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -78,49 +78,22 @@ suite('ChatTipService', () => { instantiationService.stub(ILanguageModelToolsService, testDisposables.add(new MockLanguageModelToolsService())); }); - test('returns a tip for new requests with timestamp after service creation', () => { + test('returns a welcome tip', () => { const service = createService(); - const now = Date.now(); - // Request created after service initialization - const tip = service.getNextTip('request-1', now + 1000, contextKeyService); - assert.ok(tip, 'Should return a tip for requests created after service instantiation'); + const tip = service.getWelcomeTip(contextKeyService); + assert.ok(tip, 'Should return a welcome tip'); assert.ok(tip.id.startsWith('tip.'), 'Tip should have a valid ID'); assert.ok(tip.content.value.length > 0, 'Tip should have content'); }); - test('returns undefined for old requests with timestamp before service creation', () => { + test('returns same welcome tip on rerender', () => { const service = createService(); - const now = Date.now(); - // Request created before service initialization (simulating restored chat) - const tip = service.getNextTip('old-request', now - 10000, contextKeyService); - assert.strictEqual(tip, undefined, 'Should not return a tip for requests created before service instantiation'); - }); - - test('only shows one tip per session', () => { - const service = createService(); - const now = Date.now(); - - // First request gets a tip - const tip1 = service.getNextTip('request-1', now + 1000, contextKeyService); - assert.ok(tip1, 'First request should get a tip'); - - // Second request does not get a tip - const tip2 = service.getNextTip('request-2', now + 2000, contextKeyService); - assert.strictEqual(tip2, undefined, 'Second request should not get a tip'); - }); - - test('returns same tip on rerender of same request', () => { - const service = createService(); - const now = Date.now(); - - // First call gets a tip - const tip1 = service.getNextTip('request-1', now + 1000, contextKeyService); + const tip1 = service.getWelcomeTip(contextKeyService); assert.ok(tip1); - // Same request ID gets the same tip on rerender - const tip2 = service.getNextTip('request-1', now + 1000, contextKeyService); + const tip2 = service.getWelcomeTip(contextKeyService); assert.ok(tip2); assert.strictEqual(tip1.id, tip2.id, 'Should return same tip for stable rerender'); assert.strictEqual(tip1.content.value, tip2.content.value); @@ -128,93 +101,56 @@ suite('ChatTipService', () => { test('returns undefined when Copilot is not enabled', () => { const service = createService(/* hasCopilot */ false); - const now = Date.now(); - const tip = service.getNextTip('request-1', now + 1000, contextKeyService); + const tip = service.getWelcomeTip(contextKeyService); assert.strictEqual(tip, undefined, 'Should not return a tip when Copilot is not enabled'); }); test('returns undefined when tips setting is disabled', () => { const service = createService(/* hasCopilot */ true, /* tipsEnabled */ false); - const now = Date.now(); - const tip = service.getNextTip('request-1', now + 1000, contextKeyService); + const tip = service.getWelcomeTip(contextKeyService); assert.strictEqual(tip, undefined, 'Should not return a tip when tips setting is disabled'); }); test('returns undefined when location is terminal', () => { const service = createService(); - const now = Date.now(); const terminalContextKeyService = new MockContextKeyServiceWithRulesMatching(); terminalContextKeyService.createKey(ChatContextKeys.location.key, ChatAgentLocation.Terminal); - const tip = service.getNextTip('request-1', now + 1000, terminalContextKeyService); + const tip = service.getWelcomeTip(terminalContextKeyService); assert.strictEqual(tip, undefined, 'Should not return a tip in terminal inline chat'); }); test('returns undefined when location is editor inline', () => { const service = createService(); - const now = Date.now(); const editorContextKeyService = new MockContextKeyServiceWithRulesMatching(); editorContextKeyService.createKey(ChatContextKeys.location.key, ChatAgentLocation.EditorInline); - const tip = service.getNextTip('request-1', now + 1000, editorContextKeyService); + const tip = service.getWelcomeTip(editorContextKeyService); assert.strictEqual(tip, undefined, 'Should not return a tip in editor inline chat'); }); - test('old requests do not consume the session tip allowance', () => { - const service = createService(); - const now = Date.now(); - - // Old request should not consume the tip allowance - const oldTip = service.getNextTip('old-request', now - 10000, contextKeyService); - assert.strictEqual(oldTip, undefined); - - // New request should still be able to get a tip - const newTip = service.getNextTip('new-request', now + 1000, contextKeyService); - assert.ok(newTip, 'New request should get a tip after old request was skipped'); - }); - - test('multiple old requests do not affect new request tip', () => { - const service = createService(); - const now = Date.now(); - - // Simulate multiple restored requests being rendered - service.getNextTip('old-1', now - 30000, contextKeyService); - service.getNextTip('old-2', now - 20000, contextKeyService); - service.getNextTip('old-3', now - 10000, contextKeyService); - - // New request should still get a tip - const tip = service.getNextTip('new-request', now + 1000, contextKeyService); - assert.ok(tip, 'New request should get a tip after multiple old requests'); - }); - test('dismissTip excludes the dismissed tip and allows a new one', () => { const service = createService(); - const now = Date.now(); - // Get a tip - const tip1 = service.getNextTip('request-1', now + 1000, contextKeyService); + const tip1 = service.getWelcomeTip(contextKeyService); assert.ok(tip1); - // Dismiss it service.dismissTip(); - // Next call should return a different tip (since the dismissed one is excluded) - const tip2 = service.getNextTip('request-1', now + 1000, contextKeyService); + const tip2 = service.getWelcomeTip(contextKeyService); if (tip2) { assert.notStrictEqual(tip1.id, tip2.id, 'Dismissed tip should not be shown again'); } - // tip2 may be undefined if it was the only eligible tip — that's also valid }); test('dismissTip fires onDidDismissTip event', () => { const service = createService(); - const now = Date.now(); - service.getNextTip('request-1', now + 1000, contextKeyService); + service.getWelcomeTip(contextKeyService); let fired = false; testDisposables.add(service.onDidDismissTip(() => { fired = true; })); @@ -225,9 +161,8 @@ suite('ChatTipService', () => { test('disableTips fires onDidDisableTips event', async () => { const service = createService(); - const now = Date.now(); - service.getNextTip('request-1', now + 1000, contextKeyService); + service.getWelcomeTip(contextKeyService); let fired = false; testDisposables.add(service.onDidDisableTips(() => { fired = true; })); @@ -238,20 +173,15 @@ suite('ChatTipService', () => { test('disableTips resets state so re-enabling works', async () => { const service = createService(); - const now = Date.now(); - // Show a tip - const tip1 = service.getNextTip('request-1', now + 1000, contextKeyService); + const tip1 = service.getWelcomeTip(contextKeyService); assert.ok(tip1); - // Disable tips await service.disableTips(); - // Re-enable tips configurationService.setUserConfiguration('chat.tips.enabled', true); - // Should be able to get a tip again on a new request - const tip2 = service.getNextTip('request-2', now + 2000, contextKeyService); + const tip2 = service.getWelcomeTip(contextKeyService); assert.ok(tip2, 'Should return a tip after disabling and re-enabling'); }); @@ -501,37 +431,16 @@ suite('ChatTipService', () => { assert.strictEqual(tracker2.isExcluded(tip), true, 'New tracker should read persisted mode exclusion from workspace storage'); }); - test('resetSession allows tips in a new conversation', () => { + test('resetSession allows a new welcome tip', () => { const service = createService(); - const now = Date.now(); - // Show a tip in the first conversation - const tip1 = service.getNextTip('request-1', now + 1000, contextKeyService); - assert.ok(tip1, 'First request should get a tip'); + const tip1 = service.getWelcomeTip(contextKeyService); + assert.ok(tip1, 'Should get a welcome tip'); - // Second request — no tip (one per session) - const tip2 = service.getNextTip('request-2', now + 2000, contextKeyService); - assert.strictEqual(tip2, undefined, 'Second request should not get a tip'); - - // Start a new conversation service.resetSession(); - // New request after reset should get a tip - const tip3 = service.getNextTip('request-3', Date.now() + 1000, contextKeyService); - assert.ok(tip3, 'First request after resetSession should get a tip'); - }); - - test('chatResponse tip shows regardless of welcome tip', () => { - const service = createService(); - const now = Date.now(); - - // Show a welcome tip (simulating the getting-started view) - const welcomeTip = service.getWelcomeTip(contextKeyService); - assert.ok(welcomeTip, 'Welcome tip should be shown'); - - // First new request should still get a chatResponse tip - const tip = service.getNextTip('request-1', now + 1000, contextKeyService); - assert.ok(tip, 'ChatResponse tip should show even when welcome tip was shown'); + const tip2 = service.getWelcomeTip(contextKeyService); + assert.ok(tip2, 'Should get a welcome tip after resetSession'); }); test('excludes tip when tracked tool has been invoked', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatEditsTree.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatEditsTree.test.ts deleted file mode 100644 index 82a8283135d9d..0000000000000 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatEditsTree.test.ts +++ /dev/null @@ -1,275 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import assert from 'assert'; -import { URI } from '../../../../../../../base/common/uri.js'; -import { DisposableStore } from '../../../../../../../base/common/lifecycle.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; -import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { ContextKeyService } from '../../../../../../../platform/contextkey/browser/contextKeyService.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../../../../platform/storage/common/storage.js'; -import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; -import { IChatCollapsibleListItem } from '../../../../browser/widget/chatContentParts/chatReferencesContentPart.js'; -import { buildEditsList, buildEditsTree, ChatEditsListWidget, ChatEditsTreeIdentityProvider, IChatEditsFolderElement } from '../../../../browser/widget/input/chatEditsTree.js'; -import { CHAT_EDITS_VIEW_MODE_STORAGE_KEY } from '../../../../browser/chatEditing/chatEditingActions.js'; -import { ModifiedFileEntryState, IChatEditingSession } from '../../../../common/editing/chatEditingService.js'; -import { Event } from '../../../../../../../base/common/event.js'; - -function makeFileItem(path: string, added = 0, removed = 0): IChatCollapsibleListItem { - return { - reference: URI.file(path), - state: ModifiedFileEntryState.Modified, - kind: 'reference', - options: { - status: undefined, - diffMeta: { added, removed }, - } - }; -} - -suite('ChatEditsTree', () => { - - ensureNoDisposablesAreLeakedInTestSuite(); - - suite('buildEditsList', () => { - test('wraps items as flat tree elements', () => { - const items = [ - makeFileItem('/src/a.ts'), - makeFileItem('/src/b.ts'), - ]; - const result = buildEditsList(items); - assert.strictEqual(result.length, 2); - assert.strictEqual(result[0].children, undefined); - assert.strictEqual(result[1].children, undefined); - }); - - test('returns empty array for empty input', () => { - assert.deepStrictEqual(buildEditsList([]), []); - }); - }); - - suite('buildEditsTree', () => { - test('groups files by directory', () => { - const items = [ - makeFileItem('/project/src/a.ts'), - makeFileItem('/project/src/b.ts'), - makeFileItem('/project/lib/c.ts'), - ]; - const result = buildEditsTree(items); - - // Should have 2 folder elements - assert.strictEqual(result.length, 2); - - const folders = result.map(r => r.element).filter((e): e is IChatEditsFolderElement => e.kind === 'folder'); - assert.strictEqual(folders.length, 2); - - // Each folder should have children - for (const r of result) { - assert.ok(r.children); - assert.ok(r.collapsible); - } - }); - - test('skips folder grouping for single file in single folder', () => { - const items = [makeFileItem('/project/src/a.ts')]; - const result = buildEditsTree(items); - - // Single file should not be wrapped in a folder - assert.strictEqual(result.length, 1); - assert.notStrictEqual(result[0].element.kind, 'folder'); - }); - - test('still groups when there are multiple folders even with single files', () => { - const items = [ - makeFileItem('/project/src/a.ts'), - makeFileItem('/project/lib/b.ts'), - ]; - const result = buildEditsTree(items); - - assert.strictEqual(result.length, 2); - const folders = result.map(r => r.element).filter((e): e is IChatEditsFolderElement => e.kind === 'folder'); - assert.strictEqual(folders.length, 2); - }); - - test('handles items without URIs as top-level elements', () => { - const warning: IChatCollapsibleListItem = { - kind: 'warning', - content: { value: 'Something went wrong' }, - }; - const items: IChatCollapsibleListItem[] = [ - warning, - makeFileItem('/src/a.ts'), - ]; - const result = buildEditsTree(items); - - // Warning at top level + single file at root (common ancestor is /src/) - assert.strictEqual(result.length, 2); - assert.strictEqual(result[0].element.kind, 'warning'); - assert.strictEqual(result[1].element.kind, 'reference'); - }); - - test('flattens files at common ancestor and shows subfolders', () => { - const items = [ - makeFileItem('/project/root/hello.py'), - makeFileItem('/project/root/README.md'), - makeFileItem('/project/root/test.py'), - makeFileItem('/project/root/js/test2.js'), - ]; - const result = buildEditsTree(items); - - // Common ancestor is /project/root/ — files there go to root level, - // js/ becomes a folder node - const rootFiles = result.filter(r => r.element.kind === 'reference'); - const folders = result.filter(r => r.element.kind === 'folder'); - assert.strictEqual(rootFiles.length, 3, 'three files at root level'); - assert.strictEqual(folders.length, 1, 'one subfolder'); - assert.strictEqual((folders[0].element as IChatEditsFolderElement).children.length, 1); - - // Folders should come before files (like search) - const firstFolderIndex = result.findIndex(r => r.element.kind === 'folder'); - const firstFileIndex = result.findIndex(r => r.element.kind === 'reference'); - assert.ok(firstFolderIndex < firstFileIndex, 'folders should appear before files'); - }); - - test('all files in same directory produces no folder row', () => { - const items = [ - makeFileItem('/project/src/a.ts'), - makeFileItem('/project/src/b.ts'), - makeFileItem('/project/src/c.ts'), - ]; - const result = buildEditsTree(items); - - // All files in the same directory — common ancestor is /project/src/ - // No folder row needed - assert.strictEqual(result.length, 3); - assert.ok(result.every(r => r.element.kind === 'reference')); - }); - }); - - suite('ChatEditsTreeIdentityProvider', () => { - test('provides stable IDs for folders', () => { - const provider = new ChatEditsTreeIdentityProvider(); - const folder: IChatEditsFolderElement = { - kind: 'folder', - uri: URI.file('/src'), - children: [], - }; - const id = provider.getId(folder); - assert.strictEqual(id, `folder:${URI.file('/src').toString()}`); - }); - - test('provides stable IDs for file references', () => { - const provider = new ChatEditsTreeIdentityProvider(); - const item = makeFileItem('/src/a.ts'); - const id = provider.getId(item); - assert.strictEqual(id, `file:${URI.file('/src/a.ts').toString()}`); - }); - - test('same element produces same ID', () => { - const provider = new ChatEditsTreeIdentityProvider(); - const item1 = makeFileItem('/src/a.ts'); - const item2 = makeFileItem('/src/a.ts'); - assert.strictEqual(provider.getId(item1), provider.getId(item2)); - }); - - test('different elements produce different IDs', () => { - const provider = new ChatEditsTreeIdentityProvider(); - const item1 = makeFileItem('/src/a.ts'); - const item2 = makeFileItem('/src/b.ts'); - assert.notStrictEqual(provider.getId(item1), provider.getId(item2)); - }); - }); - - suite('ChatEditsListWidget lifecycle', () => { - let store: DisposableStore; - let storageService: IStorageService; - let widget: ChatEditsListWidget; - - setup(() => { - store = new DisposableStore(); - const instaService = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(new TestConfigurationService)), - }, store); - store.add(instaService); - - storageService = instaService.get(IStorageService); - widget = store.add(instaService.createInstance(ChatEditsListWidget, Event.None)); - }); - - teardown(() => { - store.dispose(); - }); - - test.skip('storage listener fires after clear', () => { - // Stub create to avoid DOM/widget side effects in tests - let createCallCount = 0; - const origCreate = widget.create.bind(widget); - widget.create = (c, s) => { - createCallCount++; - // Update stored refs without actually building widgets - (widget as unknown as { _currentContainer: HTMLElement | undefined })._currentContainer = c; - (widget as unknown as { _currentSession: IChatEditingSession | null })._currentSession = s; - }; - - const container = document.createElement('div'); - widget.create(container, null); - assert.strictEqual(createCallCount, 1); - - // Simulate session switch - widget.clear(); - - // Toggle view mode — storage listener must still fire after clear() - createCallCount = 0; - storageService.store(CHAT_EDITS_VIEW_MODE_STORAGE_KEY, 'tree', StorageScope.PROFILE, StorageTarget.USER); - assert.strictEqual(createCallCount, 1, 'storage listener should trigger create after clear()'); - - widget.create = origCreate; - }); - - test.skip('currentSession is updated on rebuild', () => { - // Stub create - widget.create = (c, s) => { - (widget as unknown as { _currentContainer: HTMLElement | undefined })._currentContainer = c; - (widget as unknown as { _currentSession: IChatEditingSession | null })._currentSession = s; - }; - - const container = document.createElement('div'); - widget.create(container, null); - assert.strictEqual(widget.currentSession, null); - - const mockSession = {} as IChatEditingSession; - widget.rebuild(container, mockSession); - assert.strictEqual(widget.currentSession, mockSession); - }); - - test.skip('setEntries replays after view mode toggle', () => { - // Stub create and track setEntries calls - widget.create = (c, s) => { - (widget as unknown as { _currentContainer: HTMLElement | undefined })._currentContainer = c; - (widget as unknown as { _currentSession: IChatEditingSession | null })._currentSession = s; - }; - - const container = document.createElement('div'); - widget.create(container, null); - - const entries = [makeFileItem('/src/a.ts'), makeFileItem('/src/b.ts')]; - widget.setEntries(entries); - - const setEntriesCalls: readonly IChatCollapsibleListItem[][] = []; - const origSetEntries = widget.setEntries.bind(widget); - widget.setEntries = (e) => { - (setEntriesCalls as IChatCollapsibleListItem[][]).push([...e]); - origSetEntries(e); - }; - - // Toggle to tree mode — should replay entries - storageService.store(CHAT_EDITS_VIEW_MODE_STORAGE_KEY, 'tree', StorageScope.PROFILE, StorageTarget.USER); - assert.strictEqual(setEntriesCalls.length, 1, 'setEntries should have been replayed'); - assert.strictEqual(setEntriesCalls[0].length, 2, 'should have replayed the 2 entries'); - - widget.setEntries = origSetEntries; - }); - }); -}); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index c4bcfd5b70bd3..b13ee6aded3f4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -834,6 +834,7 @@ export interface ITerminalInstance extends IBaseTerminalInstance { readonly fixedCols?: number; readonly fixedRows?: number; readonly domElement: HTMLElement; + readonly isVisible: boolean; readonly icon?: TerminalIcon; readonly color?: string; readonly reconnectionProperties?: IReconnectionProperties; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 3ec8c50c4fd7e..7a71b3c59e170 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -150,7 +150,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _latestXtermParseData: number = 0; private _isExiting: boolean; private _hadFocusOnExit: boolean; - private _isVisible: boolean; private _exitCode: number | undefined; private _exitReason: TerminalExitReason | undefined; private _skipTerminalCommands: string[]; @@ -218,6 +217,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._shellLaunchConfig.waitOnExit = value; } + private _isVisible: boolean; + get isVisible(): boolean { return this._isVisible; } + private _targetRef: ImmortalReference = new ImmortalReference(undefined); get targetRef(): IReference { return this._targetRef; } diff --git a/src/vs/workbench/contrib/terminal/terminal.all.ts b/src/vs/workbench/contrib/terminal/terminal.all.ts index 6f08c6293479b..c586b200ef6cf 100644 --- a/src/vs/workbench/contrib/terminal/terminal.all.ts +++ b/src/vs/workbench/contrib/terminal/terminal.all.ts @@ -24,6 +24,7 @@ import '../terminalContrib/commandGuide/browser/terminal.commandGuide.contributi import '../terminalContrib/history/browser/terminal.history.contribution.js'; import '../terminalContrib/inlineHint/browser/terminal.initialHint.contribution.js'; import '../terminalContrib/links/browser/terminal.links.contribution.js'; +import '../terminalContrib/notification/browser/terminal.notification.contribution.js'; import '../terminalContrib/zoom/browser/terminal.zoom.contribution.js'; import '../terminalContrib/stickyScroll/browser/terminal.stickyScroll.contribution.js'; import '../terminalContrib/quickAccess/browser/terminal.quickAccess.contribution.js'; diff --git a/src/vs/workbench/contrib/terminal/terminalContribExports.ts b/src/vs/workbench/contrib/terminal/terminalContribExports.ts index 5692332ebc9eb..a24b204a899d5 100644 --- a/src/vs/workbench/contrib/terminal/terminalContribExports.ts +++ b/src/vs/workbench/contrib/terminal/terminalContribExports.ts @@ -14,6 +14,7 @@ import { terminalCommandGuideConfiguration } from '../terminalContrib/commandGui import { TerminalDeveloperCommandId } from '../terminalContrib/developer/common/terminal.developer.js'; import { defaultTerminalFindCommandToSkipShell } from '../terminalContrib/find/common/terminal.find.js'; import { defaultTerminalHistoryCommandsToSkipShell, terminalHistoryConfiguration } from '../terminalContrib/history/common/terminal.history.js'; +import { terminalOscNotificationsConfiguration } from '../terminalContrib/notification/common/terminalNotificationConfiguration.js'; import { TerminalStickyScrollSettingId, terminalStickyScrollConfiguration } from '../terminalContrib/stickyScroll/common/terminalStickyScrollConfiguration.js'; import { defaultTerminalSuggestCommandsToSkipShell } from '../terminalContrib/suggest/common/terminal.suggest.js'; import { TerminalSuggestSettingId, terminalSuggestConfiguration } from '../terminalContrib/suggest/common/terminalSuggestConfiguration.js'; @@ -65,6 +66,7 @@ export const terminalContribConfiguration: IConfigurationNode['properties'] = { ...terminalInitialHintConfiguration, ...terminalCommandGuideConfiguration, ...terminalHistoryConfiguration, + ...terminalOscNotificationsConfiguration, ...terminalStickyScrollConfiguration, ...terminalSuggestConfiguration, ...terminalTypeAheadConfiguration, diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 5903834e459cd..059b63a92c5e5 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -206,6 +206,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary` and `--no-pager` immediately after `git` '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+status\\b/': true, '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+log\\b/': true, + '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+log\\b.*\\s--output(=|\\s|$)/': false, '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+show\\b/': true, '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+diff\\b/': true, '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+ls-files\\b/': true, diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index 896b2d3ebca3e..e362782b44b24 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -319,6 +319,9 @@ suite('RunInTerminalTool', () => { 'docker compose events', ]; const confirmationRequiredTestCases = [ + // git log file output + 'git log --output=log.txt', + // Dangerous file operations 'rm README.md', 'rmdir folder', diff --git a/src/vs/workbench/contrib/terminalContrib/notification/browser/terminal.notification.contribution.ts b/src/vs/workbench/contrib/terminalContrib/notification/browser/terminal.notification.contribution.ts new file mode 100644 index 0000000000000..85e3b84aa0a10 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/notification/browser/terminal.notification.contribution.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; +import * as dom from '../../../../../base/browser/dom.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { INotificationService } from '../../../../../platform/notification/common/notification.js'; +import { ITerminalLogService } from '../../../../../platform/terminal/common/terminal.js'; +import type { ITerminalContribution, ITerminalInstance, IXtermTerminal } from '../../../terminal/browser/terminal.js'; +import { registerTerminalContribution, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; +import { TerminalOscNotificationsSettingId } from '../common/terminalNotificationConfiguration.js'; +import { TerminalNotificationHandler } from './terminalNotificationHandler.js'; + + +class TerminalOscNotificationsContribution extends Disposable implements ITerminalContribution { + static readonly ID = 'terminal.oscNotifications'; + + private readonly _handler: TerminalNotificationHandler; + + constructor( + private readonly _ctx: ITerminalContributionContext, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @INotificationService private readonly _notificationService: INotificationService, + @ITerminalLogService private readonly _logService: ITerminalLogService, + ) { + super(); + this._handler = this._register(new TerminalNotificationHandler({ + isEnabled: () => this._configurationService.getValue(TerminalOscNotificationsSettingId.EnableNotifications) === true, + isWindowFocused: () => dom.getActiveWindow().document.hasFocus(), + isTerminalVisible: () => this._ctx.instance.isVisible, + focusTerminal: () => this._ctx.instance.focus(true), + notify: notification => this._notificationService.notify(notification), + updateEnableNotifications: value => this._configurationService.updateValue(TerminalOscNotificationsSettingId.EnableNotifications, value), + logWarn: message => this._logService.warn(message), + writeToProcess: data => { void this._ctx.instance.sendText(data, false); } + })); + } + + xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { + this._register(xterm.raw.parser.registerOscHandler(99, data => this._handler.handleSequence(data))); + } +} + +registerTerminalContribution(TerminalOscNotificationsContribution.ID, TerminalOscNotificationsContribution); + +export function getTerminalOscNotifications(instance: ITerminalInstance): TerminalOscNotificationsContribution | null { + return instance.getContribution(TerminalOscNotificationsContribution.ID); +} diff --git a/src/vs/workbench/contrib/terminalContrib/notification/browser/terminalNotificationHandler.ts b/src/vs/workbench/contrib/terminalContrib/notification/browser/terminalNotificationHandler.ts new file mode 100644 index 0000000000000..74c18acb5b24a --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/notification/browser/terminalNotificationHandler.ts @@ -0,0 +1,528 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action, IAction } from '../../../../../base/common/actions.js'; +import { disposableTimeout } from '../../../../../base/common/async.js'; +import { decodeBase64 } from '../../../../../base/common/buffer.js'; +import { Disposable, DisposableStore, type IDisposable } from '../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../nls.js'; +import { NotificationPriority, Severity, type INotification, type INotificationHandle } from '../../../../../platform/notification/common/notification.js'; + +const enum Osc99PayloadType { + Title = 'title', + Body = 'body', + Buttons = 'buttons', + Close = 'close', + Query = '?', + Alive = 'alive' +} + +type Osc99Occasion = 'always' | 'unfocused' | 'invisible'; +type Osc99CloseReason = 'button' | 'secondary' | 'auto' | 'protocol'; + +interface IOsc99NotificationState { + id: string | undefined; + title: string; + body: string; + buttonsPayload: string; + focusOnActivate: boolean; + reportOnActivate: boolean; + reportOnClose: boolean; + urgency: number | undefined; + autoCloseMs: number | undefined; + occasion: Osc99Occasion | undefined; +} + +interface IOsc99ActiveNotification { + id: string | undefined; + handle: INotificationHandle; + actionStore: DisposableStore; + autoCloseDisposable: IDisposable | undefined; + reportOnActivate: boolean; + reportOnClose: boolean; + focusOnActivate: boolean; + closeReason: Osc99CloseReason | undefined; +} + +export interface IOsc99NotificationHost { + isEnabled(): boolean; + isWindowFocused(): boolean; + isTerminalVisible(): boolean; + focusTerminal(): void; + notify(notification: INotification): INotificationHandle; + updateEnableNotifications(value: boolean): Promise; + logWarn(message: string): void; + writeToProcess(data: string): void; +} + +export class TerminalNotificationHandler extends Disposable { + private readonly _osc99PendingNotifications = new Map(); + private _osc99PendingAnonymous: IOsc99NotificationState | undefined; + private readonly _osc99ActiveNotifications = new Map(); + + constructor( + private readonly _host: IOsc99NotificationHost + ) { + super(); + } + + handleSequence(data: string): boolean { + const { metadata, payload } = this._splitOsc99Data(data); + const metadataEntries = this._parseOsc99Metadata(metadata); + const payloadTypes = metadataEntries.get('p'); + const rawPayloadType = payloadTypes && payloadTypes.length > 0 ? payloadTypes[payloadTypes.length - 1] : undefined; + const payloadType = rawPayloadType && rawPayloadType.length > 0 ? rawPayloadType : Osc99PayloadType.Title; + const id = this._sanitizeOsc99Id(metadataEntries.get('i')?.[0]); + + if (!this._host.isEnabled()) { + return true; + } + + switch (payloadType) { + case Osc99PayloadType.Query: + this._sendOsc99QueryResponse(id); + return true; + case Osc99PayloadType.Alive: + this._sendOsc99AliveResponse(id); + return true; + case Osc99PayloadType.Close: + this._closeOsc99Notification(id); + return true; + } + + const state = this._getOrCreateOsc99State(id); + this._updateOsc99StateFromMetadata(state, metadataEntries); + + const isEncoded = metadataEntries.get('e')?.[0] === '1'; + const payloadText = this._decodeOsc99Payload(payload, isEncoded); + const isDone = metadataEntries.get('d')?.[0] !== '0'; + + switch (payloadType) { + case Osc99PayloadType.Title: + state.title += payloadText; + break; + case Osc99PayloadType.Body: + state.body += payloadText; + break; + case Osc99PayloadType.Buttons: + state.buttonsPayload += payloadText; + break; + default: + return true; + } + + if (!isDone) { + return true; + } + if (!this._shouldHonorOsc99Occasion(state.occasion)) { + this._clearOsc99PendingState(id); + return true; + } + + if (this._showOsc99Notification(state)) { + this._clearOsc99PendingState(id); + } + return true; + } + + private _splitOsc99Data(data: string): { metadata: string; payload: string } { + const separatorIndex = data.indexOf(';'); + if (separatorIndex === -1) { + return { metadata: data, payload: '' }; + } + return { + metadata: data.substring(0, separatorIndex), + payload: data.substring(separatorIndex + 1) + }; + } + + private _parseOsc99Metadata(metadata: string): Map { + const result = new Map(); + if (!metadata) { + return result; + } + for (const entry of metadata.split(':')) { + if (!entry) { + continue; + } + const separatorIndex = entry.indexOf('='); + if (separatorIndex === -1) { + continue; + } + const key = entry.substring(0, separatorIndex); + const value = entry.substring(separatorIndex + 1); + if (!key) { + continue; + } + let values = result.get(key); + if (!values) { + values = []; + result.set(key, values); + } + values.push(value); + } + return result; + } + + private _decodeOsc99Payload(payload: string, isEncoded: boolean): string { + if (!isEncoded) { + return payload; + } + try { + return decodeBase64(payload).toString(); + } catch { + this._host.logWarn('Failed to decode OSC 99 payload'); + return ''; + } + } + + private _sanitizeOsc99Id(rawId: string | undefined): string | undefined { + if (!rawId) { + return undefined; + } + const sanitized = rawId.replace(/[^a-zA-Z0-9_\-+.]/g, ''); + return sanitized.length > 0 ? sanitized : undefined; + } + + private _sanitizeOsc99MessageText(text: string): string { + return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + } + + private _getOrCreateOsc99State(id: string | undefined): IOsc99NotificationState { + if (!id) { + if (!this._osc99PendingAnonymous) { + this._osc99PendingAnonymous = this._createOsc99State(undefined); + } + return this._osc99PendingAnonymous; + } + let state = this._osc99PendingNotifications.get(id); + if (!state) { + state = this._createOsc99State(id); + this._osc99PendingNotifications.set(id, state); + } + return state; + } + + private _createOsc99State(id: string | undefined): IOsc99NotificationState { + return { + id, + title: '', + body: '', + buttonsPayload: '', + focusOnActivate: true, + reportOnActivate: false, + reportOnClose: false, + urgency: undefined, + autoCloseMs: undefined, + occasion: undefined + }; + } + + private _clearOsc99PendingState(id: string | undefined): void { + if (!id) { + this._osc99PendingAnonymous = undefined; + return; + } + this._osc99PendingNotifications.delete(id); + } + + private _updateOsc99StateFromMetadata(state: IOsc99NotificationState, metadataEntries: Map): void { + const actionValues = metadataEntries.get('a'); + const actionValue = actionValues && actionValues.length > 0 ? actionValues[actionValues.length - 1] : undefined; + if (actionValue !== undefined) { + const actions = this._parseOsc99Actions(actionValue); + state.focusOnActivate = actions.focusOnActivate; + state.reportOnActivate = actions.reportOnActivate; + } + const closeValues = metadataEntries.get('c'); + const closeValue = closeValues && closeValues.length > 0 ? closeValues[closeValues.length - 1] : undefined; + if (closeValue !== undefined) { + state.reportOnClose = closeValue === '1'; + } + const urgencyValues = metadataEntries.get('u'); + const urgencyValue = urgencyValues && urgencyValues.length > 0 ? urgencyValues[urgencyValues.length - 1] : undefined; + if (urgencyValue !== undefined) { + const urgency = Number.parseInt(urgencyValue, 10); + if (!Number.isNaN(urgency)) { + state.urgency = urgency; + } + } + const autoCloseValues = metadataEntries.get('w'); + const autoCloseValue = autoCloseValues && autoCloseValues.length > 0 ? autoCloseValues[autoCloseValues.length - 1] : undefined; + if (autoCloseValue !== undefined) { + const autoClose = Number.parseInt(autoCloseValue, 10); + if (!Number.isNaN(autoClose)) { + state.autoCloseMs = autoClose; + } + } + const occasionValues = metadataEntries.get('o'); + const occasionValue = occasionValues && occasionValues.length > 0 ? occasionValues[occasionValues.length - 1] : undefined; + if (occasionValue === 'always' || occasionValue === 'unfocused' || occasionValue === 'invisible') { + state.occasion = occasionValue; + } + } + + private _parseOsc99Actions(value: string): { focusOnActivate: boolean; reportOnActivate: boolean } { + let focusOnActivate = true; + let reportOnActivate = false; + for (const token of value.split(',')) { + switch (token) { + case 'focus': + focusOnActivate = true; + break; + case '-focus': + focusOnActivate = false; + break; + case 'report': + reportOnActivate = true; + break; + case '-report': + reportOnActivate = false; + break; + } + } + return { focusOnActivate, reportOnActivate }; + } + + private _shouldHonorOsc99Occasion(occasion: Osc99Occasion | undefined): boolean { + if (!occasion || occasion === 'always') { + return true; + } + const windowFocused = this._host.isWindowFocused(); + switch (occasion) { + case 'unfocused': + return !windowFocused; + case 'invisible': + return !windowFocused && !this._host.isTerminalVisible(); + default: + return true; + } + } + + private _showOsc99Notification(state: IOsc99NotificationState): boolean { + const message = this._getOsc99NotificationMessage(state); + if (!message) { + return false; + } + + const severity = state.urgency === 2 ? Severity.Warning : Severity.Info; + const priority = this._getOsc99NotificationPriority(state.urgency); + const source = { + id: 'terminal', + label: localize('terminalNotificationSource', 'Terminal') + }; + const buttons = state.buttonsPayload.length > 0 ? state.buttonsPayload.split('\u2028') : []; + const actionStore = this._register(new DisposableStore()); + + const handleRef: { current: INotificationHandle | undefined } = { current: undefined }; + const activeRef: { current: IOsc99ActiveNotification | undefined } = { current: undefined }; + const reportActivation = (buttonIndex?: number, forceFocus?: boolean) => { + if (forceFocus || state.focusOnActivate) { + this._host.focusTerminal(); + } + if (state.reportOnActivate) { + this._sendOsc99ActivationReport(state.id, buttonIndex); + } + }; + + const primaryActions: IAction[] = []; + for (let i = 0; i < buttons.length; i++) { + const label = buttons[i]; + if (!label) { + continue; + } + const action = actionStore.add(new Action(`terminal.osc99.button.${i}`, label, undefined, true, () => { + if (activeRef.current) { + activeRef.current.closeReason = 'button'; + } + reportActivation(i + 1); + handleRef.current?.close(); + })); + primaryActions.push(action); + } + + const secondaryActions: IAction[] = []; + secondaryActions.push(actionStore.add(new Action( + 'terminal.osc99.dismiss', + localize('terminalNotificationDismiss', 'Dismiss'), + undefined, + true, + () => { + if (activeRef.current) { + activeRef.current.closeReason = 'secondary'; + } + handleRef.current?.close(); + } + ))); + secondaryActions.push(actionStore.add(new Action( + 'terminal.osc99.disable', + localize('terminalNotificationDisable', 'Disable Terminal Notifications'), + undefined, + true, + async () => { + await this._host.updateEnableNotifications(false); + if (activeRef.current) { + activeRef.current.closeReason = 'secondary'; + } + handleRef.current?.close(); + } + ))); + + const actions = { primary: primaryActions, secondary: secondaryActions }; + + if (state.id) { + const existing = this._osc99ActiveNotifications.get(state.id); + if (existing) { + activeRef.current = existing; + handleRef.current = existing.handle; + existing.handle.updateMessage(message); + existing.handle.updateSeverity(severity); + existing.handle.updateActions(actions); + existing.actionStore.dispose(); + existing.actionStore = actionStore; + existing.focusOnActivate = state.focusOnActivate; + existing.reportOnActivate = state.reportOnActivate; + existing.reportOnClose = state.reportOnClose; + existing.autoCloseDisposable?.dispose(); + existing.autoCloseDisposable = this._scheduleOsc99AutoClose(existing, state.autoCloseMs); + return true; + } + } + + const handle = this._host.notify({ + id: state.id ? `terminal.osc99.${state.id}` : undefined, + severity, + message, + source, + actions, + priority + }); + handleRef.current = handle; + + const active: IOsc99ActiveNotification = { + id: state.id, + handle, + actionStore, + autoCloseDisposable: undefined, + reportOnActivate: state.reportOnActivate, + reportOnClose: state.reportOnClose, + focusOnActivate: state.focusOnActivate, + closeReason: undefined + }; + activeRef.current = active; + active.autoCloseDisposable = this._scheduleOsc99AutoClose(active, state.autoCloseMs); + this._register(handle.onDidClose(() => { + if (active.reportOnActivate && active.closeReason === undefined) { + if (active.focusOnActivate) { + this._host.focusTerminal(); + } + this._sendOsc99ActivationReport(active.id); + } + if (active.reportOnClose) { + this._sendOsc99CloseReport(active.id); + } + active.actionStore.dispose(); + active.autoCloseDisposable?.dispose(); + if (active.id) { + this._osc99ActiveNotifications.delete(active.id); + } + })); + + if (active.id) { + this._osc99ActiveNotifications.set(active.id, active); + } + return true; + } + + private _getOsc99NotificationMessage(state: IOsc99NotificationState): string | undefined { + const title = this._sanitizeOsc99MessageText(state.title); + const body = this._sanitizeOsc99MessageText(state.body); + const hasTitle = title.trim().length > 0; + const hasBody = body.trim().length > 0; + if (hasTitle && hasBody) { + return `${title}: ${body}`; + } + if (hasTitle) { + return title; + } + if (hasBody) { + return body; + } + return undefined; + } + + private _getOsc99NotificationPriority(urgency: number | undefined): NotificationPriority | undefined { + switch (urgency) { + case 0: + return NotificationPriority.SILENT; + case 1: + return NotificationPriority.DEFAULT; + case 2: + return NotificationPriority.URGENT; + default: + return undefined; + } + } + + private _scheduleOsc99AutoClose(active: IOsc99ActiveNotification, autoCloseMs: number | undefined): IDisposable | undefined { + if (autoCloseMs === undefined || autoCloseMs <= 0) { + return undefined; + } + return disposableTimeout(() => { + active.closeReason = 'auto'; + active.handle.close(); + }, autoCloseMs, this._store); + } + + private _closeOsc99Notification(id: string | undefined): void { + if (!id) { + return; + } + const active = this._osc99ActiveNotifications.get(id); + if (active) { + active.closeReason = 'protocol'; + active.handle.close(); + } + this._osc99PendingNotifications.delete(id); + } + + private _sendOsc99QueryResponse(id: string | undefined): void { + const requestId = id ?? '0'; + this._sendOsc99Response([ + `i=${requestId}`, + 'p=?', + 'a=report,focus', + 'c=1', + 'o=always,unfocused,invisible', + 'p=title,body,buttons,close,alive,?', + 'u=0,1,2', + 'w=1' + ]); + } + + private _sendOsc99AliveResponse(id: string | undefined): void { + const requestId = id ?? '0'; + const aliveIds = Array.from(this._osc99ActiveNotifications.keys()).join(','); + this._sendOsc99Response([ + `i=${requestId}`, + 'p=alive' + ], aliveIds); + } + + private _sendOsc99ActivationReport(id: string | undefined, buttonIndex?: number): void { + const reportId = id ?? '0'; + this._sendOsc99Response([`i=${reportId}`], buttonIndex !== undefined ? String(buttonIndex) : ''); + } + + private _sendOsc99CloseReport(id: string | undefined): void { + const reportId = id ?? '0'; + this._sendOsc99Response([`i=${reportId}`, 'p=close']); + } + + private _sendOsc99Response(metadataParts: string[], payload: string = ''): void { + const metadata = metadataParts.join(':'); + this._host.writeToProcess(`\x1b]99;${metadata};${payload}\x1b\\`); + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/notification/common/terminalNotificationConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/notification/common/terminalNotificationConfiguration.ts new file mode 100644 index 0000000000000..f4e1e8dc3c20c --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/notification/common/terminalNotificationConfiguration.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { IStringDictionary } from '../../../../../base/common/collections.js'; +import { localize } from '../../../../../nls.js'; +import type { IConfigurationPropertySchema } from '../../../../../platform/configuration/common/configurationRegistry.js'; + +export const enum TerminalOscNotificationsSettingId { + EnableNotifications = 'terminal.integrated.enableNotifications', +} + +export const terminalOscNotificationsConfiguration: IStringDictionary = { + [TerminalOscNotificationsSettingId.EnableNotifications]: { + description: localize('terminal.integrated.enableNotifications', "Controls whether notifications sent from the terminal via OSC 99 are shown. This uses notifications inside the product instead of desktop notifications. Sounds, icons and filtering are not supported."), + type: 'boolean', + default: true + }, +}; diff --git a/src/vs/workbench/contrib/terminalContrib/notification/test/browser/terminalNotification.test.ts b/src/vs/workbench/contrib/terminalContrib/notification/test/browser/terminalNotification.test.ts new file mode 100644 index 0000000000000..a44395c5e94a7 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/notification/test/browser/terminalNotification.test.ts @@ -0,0 +1,262 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { strictEqual } from 'assert'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { NotificationPriority, Severity, type INotification, type INotificationActions, type INotificationHandle, type INotificationProgress, type NotificationMessage } from '../../../../../../platform/notification/common/notification.js'; +import { TerminalNotificationHandler, type IOsc99NotificationHost } from '../../browser/terminalNotificationHandler.js'; + +class TestNotificationProgress implements INotificationProgress { + infinite(): void { } + total(_value: number): void { } + worked(_value: number): void { } + done(): void { } +} + +class TestNotificationHandle implements INotificationHandle { + private readonly _onDidClose = new Emitter(); + readonly onDidClose = this._onDidClose.event; + readonly onDidChangeVisibility = Event.None; + readonly progress = new TestNotificationProgress(); + closed = false; + message: NotificationMessage; + severity: Severity; + actions?: INotificationActions; + priority?: NotificationPriority; + source?: string | { id: string; label: string }; + + constructor(notification: INotification) { + this.message = notification.message; + this.severity = notification.severity; + this.actions = notification.actions; + this.priority = notification.priority; + this.source = notification.source; + } + + updateSeverity(severity: Severity): void { + this.severity = severity; + } + + updateMessage(message: NotificationMessage): void { + this.message = message; + } + + updateActions(actions?: INotificationActions): void { + this._disposeActions(this.actions); + this.actions = actions; + } + + close(): void { + if (this.closed) { + return; + } + this.closed = true; + this._disposeActions(this.actions); + this._onDidClose.fire(); + } + + private _disposeActions(actions: INotificationActions | undefined): void { + for (const action of actions?.primary ?? []) { + const disposable = action as { dispose?: () => void }; + if (typeof disposable.dispose === 'function') { + disposable.dispose(); + } + } + for (const action of actions?.secondary ?? []) { + const disposable = action as { dispose?: () => void }; + if (typeof disposable.dispose === 'function') { + disposable.dispose(); + } + } + } +} + +class TestOsc99Host implements IOsc99NotificationHost { + enabled = true; + windowFocused = false; + terminalVisible = false; + writes: string[] = []; + notifications: TestNotificationHandle[] = []; + focusCalls = 0; + updatedEnableNotifications: boolean[] = []; + logMessages: string[] = []; + + isEnabled(): boolean { + return this.enabled; + } + + isWindowFocused(): boolean { + return this.windowFocused; + } + + isTerminalVisible(): boolean { + return this.terminalVisible; + } + + focusTerminal(): void { + this.focusCalls++; + } + + notify(notification: INotification): INotificationHandle { + const handle = new TestNotificationHandle(notification); + this.notifications.push(handle); + return handle; + } + + async updateEnableNotifications(value: boolean): Promise { + this.enabled = value; + this.updatedEnableNotifications.push(value); + } + + logWarn(message: string): void { + this.logMessages.push(message); + } + + writeToProcess(data: string): void { + this.writes.push(data); + } +} + +suite('Terminal OSC 99 notifications', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let host: TestOsc99Host; + let handler: TerminalNotificationHandler; + + setup(() => { + host = new TestOsc99Host(); + handler = store.add(new TerminalNotificationHandler(host)); + }); + + teardown(() => { + for (const notification of host.notifications) { + notification.close(); + } + }); + + test('ignores notifications when disabled', () => { + host.enabled = false; + + handler.handleSequence(';Hello'); + strictEqual(host.notifications.length, 0); + strictEqual(host.writes.length, 0); + }); + + test('creates notification for title and body and updates', () => { + handler.handleSequence('i=1:d=0:p=title;Hello'); + strictEqual(host.notifications.length, 0); + + handler.handleSequence('i=1:p=body;World'); + strictEqual(host.notifications.length, 1); + strictEqual(host.notifications[0].message, 'Hello: World'); + }); + + test('decodes base64 payloads', () => { + handler.handleSequence('e=1:p=title;SGVsbG8='); + strictEqual(host.notifications.length, 1); + strictEqual(host.notifications[0].message, 'Hello'); + }); + + test('sanitizes markdown links in payloads', () => { + handler.handleSequence('i=link:d=0:p=title;Click [run](command:workbench.action.reloadWindow)'); + handler.handleSequence('i=link:p=body;See [docs](https://example.com)'); + strictEqual(host.notifications.length, 1); + strictEqual(host.notifications[0].message, 'Click run: See docs'); + }); + + test('defers display until done', () => { + handler.handleSequence('i=chunk:d=0:p=title;Hello '); + strictEqual(host.notifications.length, 0); + + handler.handleSequence('i=chunk:d=1:p=title;World'); + strictEqual(host.notifications.length, 1); + strictEqual(host.notifications[0].message, 'Hello World'); + }); + + test('reports activation on button click', async () => { + handler.handleSequence('i=btn:d=0:a=report:p=title;Hi'); + handler.handleSequence('i=btn:p=buttons;Yes'); + + const actions = host.notifications[0].actions; + if (!actions?.primary || actions.primary.length === 0) { + throw new Error('Expected primary actions'); + } + await actions.primary[0].run(); + strictEqual(host.writes[0], '\x1b]99;i=btn;1\x1b\\'); + }); + + test('supports buttons before title and reports body activation', async () => { + handler.handleSequence('i=btn:p=buttons;One\u2028Two'); + handler.handleSequence('i=btn:a=report;Buttons test'); + + strictEqual(host.notifications.length, 1); + const actions = host.notifications[0].actions; + if (!actions?.primary || actions.primary.length !== 2) { + throw new Error('Expected two primary actions'); + } + strictEqual(actions.primary[0].label, 'One'); + strictEqual(actions.primary[1].label, 'Two'); + + await actions.primary[1].run(); + strictEqual(host.writes[0], '\x1b]99;i=btn;2\x1b\\'); + }); + + test('reports activation when notification closes without button action', () => { + handler.handleSequence('i=btn:p=buttons;One\u2028Two'); + handler.handleSequence('i=btn:a=report;Buttons test'); + + host.notifications[0].close(); + strictEqual(host.writes[0], '\x1b]99;i=btn;\x1b\\'); + }); + + test('sends close report when requested', () => { + handler.handleSequence('i=close:c=1:p=title;Bye'); + strictEqual(host.notifications.length, 1); + host.notifications[0].close(); + strictEqual(host.writes[0], '\x1b]99;i=close:p=close;\x1b\\'); + }); + + test('responds to query and alive', () => { + handler.handleSequence('i=a:p=title;A'); + handler.handleSequence('i=b:p=title;B'); + handler.handleSequence('i=q:p=?;'); + handler.handleSequence('i=q:p=alive;'); + + strictEqual(host.writes[0], '\x1b]99;i=q:p=?:a=report,focus:c=1:o=always,unfocused,invisible:p=title,body,buttons,close,alive,?:u=0,1,2:w=1;\x1b\\'); + strictEqual(host.writes[1], '\x1b]99;i=q:p=alive;a,b\x1b\\'); + }); + + test('honors occasion for visibility and focus', () => { + host.windowFocused = true; + host.terminalVisible = true; + handler.handleSequence('o=unfocused:p=title;Hidden'); + strictEqual(host.notifications.length, 0); + + host.windowFocused = false; + host.terminalVisible = true; + handler.handleSequence('o=invisible:p=title;Hidden'); + strictEqual(host.notifications.length, 0); + + host.terminalVisible = false; + handler.handleSequence('o=invisible:p=title;Shown'); + strictEqual(host.notifications.length, 1); + }); + + test('closes notifications via close payload', () => { + handler.handleSequence('i=closeme:p=title;Close'); + strictEqual(host.notifications.length, 1); + strictEqual(host.notifications[0].closed, false); + + handler.handleSequence('i=closeme:p=close;'); + strictEqual(host.notifications[0].closed, true); + }); + + test('maps urgency to severity and priority', () => { + handler.handleSequence('u=2:p=title;Urgent'); + strictEqual(host.notifications[0].severity, Severity.Warning); + strictEqual(host.notifications[0].priority, NotificationPriority.URGENT); + }); +});