diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index c21f5a3373a..db3814aa46f 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -1,10 +1,7 @@ -name: Copilot Setup Steps - -# This workflow customizes the ephemeral GitHub Copilot coding agent environment. -# It preinstalls dependencies and warms caches to speed up iterative agent tasks. -# NOTE: The single job MUST be named `copilot-setup-steps`. -# Supported customizations: runs-on, permissions, steps, container, services, snapshot, timeout-minutes. +name: "Copilot Setup Steps" +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab on: workflow_dispatch: push: @@ -15,68 +12,98 @@ on: - .github/workflows/copilot-setup-steps.yml jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. copilot-setup-steps: runs-on: macos-14-xlarge - timeout-minutes: 30 + + # Set the permissions to the lowest permissions possible needed for your steps. + # Copilot will be given its own token for its operations. permissions: + # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. contents: read - env: - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - + # You can define any steps you want, and they will run before the agent starts. + # If you do not check out your code, Copilot will do this for you. steps: - - name: Checkout repository + - name: Checkout microsoft/vscode uses: actions/checkout@v5 - - name: Setup Node.js (from .nvmrc + npm cache) + - name: Setup Node.js uses: actions/setup-node@v5 with: node-version-file: .nvmrc - cache: npm - - name: Compute node_modules cache key + - name: Setup system services run: | set -e - mkdir -p .build - node build/azure-pipelines/common/computeNodeModulesCacheKey.js copilot $(node -p process.arch) > .build/packagelockhash + # Start X server + ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get update + ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y pkg-config \ + xvfb \ + libgtk-3-0 \ + libxkbfile-dev \ + libkrb5-dev \ + libgbm1 \ + rpm + sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb + sudo chmod +x /etc/init.d/xvfb + sudo update-rc.d xvfb defaults + sudo service xvfb start + + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux x64 $(node -p process.arch) > .build/packagelockhash - name: Restore node_modules cache id: cache-node-modules uses: actions/cache/restore@v4 with: path: .build/node_modules_cache - key: node_modules-copilot-${{ hashFiles('.build/packagelockhash') }} + key: "node_modules-linux-${{ hashFiles('.build/packagelockhash') }}" - name: Extract node_modules cache if: steps.cache-node-modules.outputs.cache-hit == 'true' run: tar -xzf .build/node_modules_cache/cache.tgz - - name: Install build essentials (cache miss only) + - name: Install build dependencies if: steps.cache-node-modules.outputs.cache-hit != 'true' + working-directory: build run: | set -e - sudo apt-get update -y - sudo apt-get install -y --no-install-recommends \ - build-essential pkg-config libx11-dev libx11-xcb-dev libxkbfile-dev libkrb5-dev libnotify-bin - - name: Install dependencies (retry up to 5x) - if: steps.cache-node-modules.outputs.cache-hit != 'true' - run: | - set -e - for i in {1..5}; do - if npm ci; then - break - fi + for i in {1..5}; do # try 5 times + npm ci && break if [ $i -eq 5 ]; then - echo "npm ci failed too many times" >&2 + echo "Npm install failed too many times" >&2 exit 1 fi - echo "Retrying npm ci ($i)..." - sleep 5 + echo "Npm install failed $i, trying again..." done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create node_modules archive (cache miss only) + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + + source ./build/azure-pipelines/linux/setup-env.sh + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + npm_config_arch: x64 + VSCODE_ARCH: x64 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create node_modules archive if: steps.cache-node-modules.outputs.cache-hit != 'true' run: | set -e @@ -84,11 +111,10 @@ jobs: mkdir -p .build/node_modules_cache tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt - - name: Transpile sources (non-blocking) - continue-on-error: true - run: npm run gulp transpile-client-esbuild transpile-extensions + - name: Create .build folder + run: mkdir -p .build - - name: Compute built-in extensions cache key + - name: Prepare built-in extensions cache key run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash - name: Restore built-in extensions cache @@ -97,15 +123,138 @@ jobs: with: enableCrossOsArchive: true path: .build/builtInExtensions - key: builtin-extensions-${{ hashFiles('.build/builtindepshash') }} + key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" - - name: Download built-in extensions (if cache miss) + - name: Download built-in extensions if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' run: node build/lib/builtInExtensions.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Environment summary + # - name: Transpile client and extensions + # run: npm run gulp transpile-client-esbuild transpile-extensions + + - name: Download Electron and Playwright run: | - echo "Node: $(node -v)" || true - echo "NPM: $(npm -v)" || true - du -sh node_modules 2>/dev/null || true - df -h || true + set -e + + for i in {1..3}; do # try 3 times (matching retryCountOnTaskFailure: 3) + if npm exec -- npm-run-all -lp "electron x64" "playwright-install"; then + echo "Download successful on attempt $i" + break + fi + + if [ $i -eq 3 ]; then + echo "Download failed after 3 attempts" >&2 + exit 1 + fi + + echo "Download failed on attempt $i, retrying..." + sleep 5 # optional: add a small delay between retries + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # - name: 🧪 Run unit tests (Electron) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 15 + # run: ./scripts/test.sh --tfs "Unit Tests" + # env: + # DISPLAY: ":10" + + # - name: 🧪 Run unit tests (node.js) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 15 + # run: npm run test-node + + # - name: 🧪 Run unit tests (Browser, Chromium) + # if: ${{ inputs.browser_tests }} + # timeout-minutes: 30 + # run: npm run test-browser-no-install -- --browser chromium --tfs "Browser Unit Tests" + # env: + # DEBUG: "*browser*" + + # - name: Build integration tests + # run: | + # set -e + # npm run gulp \ + # compile-extension:configuration-editing \ + # compile-extension:css-language-features-server \ + # compile-extension:emmet \ + # compile-extension:git \ + # compile-extension:github-authentication \ + # compile-extension:html-language-features-server \ + # compile-extension:ipynb \ + # compile-extension:notebook-renderers \ + # compile-extension:json-language-features-server \ + # compile-extension:markdown-language-features \ + # compile-extension-media \ + # compile-extension:microsoft-authentication \ + # compile-extension:typescript-language-features \ + # compile-extension:vscode-api-tests \ + # compile-extension:vscode-colorize-tests \ + # compile-extension:vscode-colorize-perf-tests \ + # compile-extension:vscode-test-resolver + + # - name: 🧪 Run integration tests (Electron) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 20 + # run: ./scripts/test-integration.sh --tfs "Integration Tests" + # env: + # DISPLAY: ":10" + + # - name: 🧪 Run integration tests (Browser, Chromium) + # if: ${{ inputs.browser_tests }} + # timeout-minutes: 20 + # run: ./scripts/test-web-integration.sh --browser chromium + + # - name: 🧪 Run integration tests (Remote) + # if: ${{ inputs.remote_tests }} + # timeout-minutes: 20 + # run: ./scripts/test-remote-integration.sh + # env: + # DISPLAY: ":10" + + # - name: Compile smoke tests + # working-directory: test/smoke + # run: npm run compile + + # - name: Compile extensions for smoke tests + # run: npm run gulp compile-extension-media + + # - name: Diagnostics before smoke test run (processes, max_user_watches, number of opened file handles) + # run: | + # set -e + # ps -ef + # cat /proc/sys/fs/inotify/max_user_watches + # lsof | wc -l + # continue-on-error: true + # if: always() + + # - name: 🧪 Run smoke tests (Electron) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 20 + # run: npm run smoketest-no-compile -- --tracing + # env: + # DISPLAY: ":10" + + # - name: 🧪 Run smoke tests (Browser, Chromium) + # if: ${{ inputs.browser_tests }} + # timeout-minutes: 20 + # run: npm run smoketest-no-compile -- --web --tracing --headless + + # - name: 🧪 Run smoke tests (Remote) + # if: ${{ inputs.remote_tests }} + # timeout-minutes: 20 + # run: npm run smoketest-no-compile -- --remote --tracing + # env: + # DISPLAY: ":10" + + # - name: Diagnostics after smoke test run (processes, max_user_watches, number of opened file handles) + # run: | + # set -e + # ps -ef + # cat /proc/sys/fs/inotify/max_user_watches + # lsof | wc -l + # continue-on-error: true + # if: always() diff --git a/build/azure-pipelines/alpine/cli-build-alpine.yml b/build/azure-pipelines/alpine/cli-build-alpine.yml index 95dee012c06..6180a7efd10 100644 --- a/build/azure-pipelines/alpine/cli-build-alpine.yml +++ b/build/azure-pipelines/alpine/cli-build-alpine.yml @@ -12,6 +12,8 @@ parameters: default: false steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml index 303db76fd0f..aa5dd83b64a 100644 --- a/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/build/azure-pipelines/alpine/product-build-alpine.yml @@ -1,4 +1,6 @@ steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/common/checkout.yml b/build/azure-pipelines/common/checkout.yml new file mode 100644 index 00000000000..6f57a9ad9b4 --- /dev/null +++ b/build/azure-pipelines/common/checkout.yml @@ -0,0 +1,5 @@ +steps: + - checkout: self + fetchDepth: 1 + fetchTags: false + displayName: Checkout microsoft/vscode diff --git a/build/azure-pipelines/common/publish-artifact.yml b/build/azure-pipelines/common/publish-artifact.yml index b18dc8d4c7f..ba4d9f13355 100644 --- a/build/azure-pipelines/common/publish-artifact.yml +++ b/build/azure-pipelines/common/publish-artifact.yml @@ -18,9 +18,6 @@ parameters: - name: sbomPackageVersion type: string default: "" - - name: isProduction - type: boolean - default: true - name: condition type: string default: succeeded() @@ -80,7 +77,6 @@ steps: targetPath: ${{ parameters.targetPath }} artifactName: $(ARTIFACT_NAME) sbomEnabled: ${{ parameters.sbomEnabled }} - isProduction: ${{ parameters.isProduction }} ${{ if ne(parameters.sbomBuildDropPath, '') }}: sbomBuildDropPath: ${{ parameters.sbomBuildDropPath }} ${{ if ne(parameters.sbomPackageName, '') }}: diff --git a/build/azure-pipelines/darwin/cli-build-darwin.yml b/build/azure-pipelines/darwin/cli-build-darwin.yml index 730918f5da1..fe44f2827fd 100644 --- a/build/azure-pipelines/darwin/cli-build-darwin.yml +++ b/build/azure-pipelines/darwin/cli-build-darwin.yml @@ -12,6 +12,8 @@ parameters: default: false steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile @@ -71,9 +73,7 @@ steps: targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_darwin_x64_cli.zip artifactName: unsigned_vscode_cli_darwin_x64_cli displayName: Publish unsigned_vscode_cli_darwin_x64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code macOS x64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) + sbomEnabled: false - ${{ if eq(parameters.VSCODE_BUILD_MACOS_ARM64, true) }}: - template: ../common/publish-artifact.yml@self @@ -81,6 +81,4 @@ steps: targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_darwin_arm64_cli.zip artifactName: unsigned_vscode_cli_darwin_arm64_cli displayName: Publish unsigned_vscode_cli_darwin_arm64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code macOS arm64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) + sbomEnabled: false diff --git a/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml b/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml index 505534093d0..a622547e2de 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml @@ -5,6 +5,8 @@ parameters: type: boolean steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml index 3d1dfdf8ea3..f2b5e697c4d 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -132,7 +132,6 @@ steps: ${{ else }}: artifactName: crash-dump-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Crash Reports" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -147,7 +146,6 @@ steps: ${{ else }}: artifactName: node-modules-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Node Modules" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -160,7 +158,6 @@ steps: ${{ else }}: artifactName: logs-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Log Files" - isProduction: false sbomEnabled: false continueOnError: true condition: succeededOrFailed() diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml index 60ce6a82954..b1153dc8c8b 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -1,4 +1,6 @@ steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 7d2849681c5..dfb8284426a 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -17,6 +17,8 @@ parameters: default: "" steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/linux/cli-build-linux.yml b/build/azure-pipelines/linux/cli-build-linux.yml index b29c4259433..c79c00ecf89 100644 --- a/build/azure-pipelines/linux/cli-build-linux.yml +++ b/build/azure-pipelines/linux/cli-build-linux.yml @@ -15,6 +15,8 @@ parameters: type: string steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/linux/product-build-linux-test.yml b/build/azure-pipelines/linux/product-build-linux-test.yml index 4e882b78d25..e4dbfecd91b 100644 --- a/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-test.yml @@ -147,7 +147,6 @@ steps: ${{ else }}: artifactName: crash-dump-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Crash Reports" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -162,7 +161,6 @@ steps: ${{ else }}: artifactName: node-modules-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Node Modules" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -175,7 +173,6 @@ steps: ${{ else }}: artifactName: logs-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Log Files" - isProduction: false sbomEnabled: false continueOnError: true condition: succeededOrFailed() diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index f1e92a60c1f..40d9fb7f383 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -22,6 +22,8 @@ parameters: default: "" steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index c0a346333c1..ab508dc065c 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -691,12 +691,10 @@ extends: - name: skipComponentGovernanceDetection value: true - - ${{ if or(and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)), and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true))) }}: - stage: Release dependsOn: - Publish - - ${{ if and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)) }}: - - ApproveRelease + - ApproveRelease pool: name: 1es-ubuntu-22.04-x64 os: linux diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 19e96932301..f6b2358b7e1 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -1,4 +1,6 @@ steps: + - template: ./common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index 90cd06c5459..c905b5015e4 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -1,4 +1,6 @@ steps: + - template: ./common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile @@ -93,3 +95,11 @@ steps: displayName: Publish the artifacts processed for this stage attempt sbomEnabled: false condition: always() + + - ${{ if and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true)) }}: + - script: node build/azure-pipelines/common/releaseBuild.js + env: + AZURE_TENANT_ID: "$(AZURE_TENANT_ID)" + AZURE_CLIENT_ID: "$(AZURE_CLIENT_ID)" + AZURE_ID_TOKEN: "$(AZURE_ID_TOKEN)" + displayName: Release build diff --git a/build/azure-pipelines/product-release.yml b/build/azure-pipelines/product-release.yml index d7b51aa8a92..bb54d50fdda 100644 --- a/build/azure-pipelines/product-release.yml +++ b/build/azure-pipelines/product-release.yml @@ -3,6 +3,8 @@ parameters: type: boolean steps: + - template: ./common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index e35af2b87aa..a372a0e8bae 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -1,4 +1,6 @@ steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/win32/cli-build-win32.yml b/build/azure-pipelines/win32/cli-build-win32.yml index 1914cb7cf6c..51484abb7c0 100644 --- a/build/azure-pipelines/win32/cli-build-win32.yml +++ b/build/azure-pipelines/win32/cli-build-win32.yml @@ -12,6 +12,8 @@ parameters: type: string steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile @@ -74,9 +76,7 @@ steps: targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_win32_arm64_cli.zip artifactName: unsigned_vscode_cli_win32_arm64_cli displayName: Publish unsigned_vscode_cli_win32_arm64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code Windows arm64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) + sbomEnabled: false - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - template: ../common/publish-artifact.yml@self @@ -84,6 +84,4 @@ steps: targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_win32_x64_cli.zip artifactName: unsigned_vscode_cli_win32_x64_cli displayName: Publish unsigned_vscode_cli_win32_x64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code Windows x64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) + sbomEnabled: false diff --git a/build/azure-pipelines/win32/product-build-win32-cli-sign.yml b/build/azure-pipelines/win32/product-build-win32-cli-sign.yml index 520931ffa48..1b0224f6cae 100644 --- a/build/azure-pipelines/win32/product-build-win32-cli-sign.yml +++ b/build/azure-pipelines/win32/product-build-win32-cli-sign.yml @@ -5,6 +5,8 @@ parameters: type: boolean steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 displayName: "Use Node.js" inputs: diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index 7d5222e347f..154ddcf4485 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -149,7 +149,6 @@ steps: artifactName: crash-dump-windows-$(VSCODE_ARCH)-$(System.JobAttempt) ${{ else }}: artifactName: crash-dump-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) - isProduction: false sbomEnabled: false displayName: "Publish Crash Reports" continueOnError: true @@ -164,7 +163,6 @@ steps: artifactName: node-modules-windows-$(VSCODE_ARCH)-$(System.JobAttempt) ${{ else }}: artifactName: node-modules-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) - isProduction: false sbomEnabled: false displayName: "Publish Node Modules" continueOnError: true @@ -177,7 +175,6 @@ steps: artifactName: logs-windows-$(VSCODE_ARCH)-$(System.JobAttempt) ${{ else }}: artifactName: logs-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) - isProduction: false sbomEnabled: false displayName: "Publish Log Files" continueOnError: true diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index a0b040c52c2..ad3ddb573f1 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -19,6 +19,8 @@ parameters: default: "" steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/win32/sdl-scan-win32.yml b/build/azure-pipelines/win32/sdl-scan-win32.yml index ba60b881392..dba656eff53 100644 --- a/build/azure-pipelines/win32/sdl-scan-win32.yml +++ b/build/azure-pipelines/win32/sdl-scan-win32.yml @@ -5,6 +5,8 @@ parameters: type: string steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index a4c5036255f..730c76909b7 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri, DiagnosticSeverity, env } from 'vscode'; -import { dirname, sep, relative } from 'path'; +import { dirname, normalize, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; import byline from 'byline'; @@ -299,10 +299,16 @@ function normalizePath(path: string): string { // Windows & Mac are currently being handled // as case insensitive file systems in VS Code. if (isWindows || isMacintosh) { - return path.toLowerCase(); + path = path.toLowerCase(); } - return path; + // Remove trailing separator + if (path.charAt(path.length - 1) === sep) { + path = path.substring(0, path.length - 1); + } + + // Normalize the path + return normalize(path); } export function isDescendant(parent: string, descendant: string): boolean { @@ -310,11 +316,16 @@ export function isDescendant(parent: string, descendant: string): boolean { return true; } + // Normalize the paths + parent = normalizePath(parent); + descendant = normalizePath(descendant); + + // Ensure parent ends with separator if (parent.charAt(parent.length - 1) !== sep) { parent += sep; } - return normalizePath(descendant).startsWith(normalizePath(parent)); + return descendant.startsWith(parent); } export function pathEquals(a: string, b: string): boolean { diff --git a/package.json b/package.json index 6ad06e6a153..fe77b2af4b0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.105.0", - "distro": "8103a60ed6fb457dbf0ba180a67be3341dce4a23", + "distro": "a1ef855abff98ce65dc9890dd2dedb4108ce19d3", "author": { "name": "Microsoft Corporation" }, @@ -237,4 +237,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} +} \ No newline at end of file diff --git a/src/vs/base/common/cancellation.ts b/src/vs/base/common/cancellation.ts index c5b8133ae6c..3be4a90a103 100644 --- a/src/vs/base/common/cancellation.ts +++ b/src/vs/base/common/cancellation.ts @@ -146,3 +146,61 @@ export function cancelOnDispose(store: DisposableStore): CancellationToken { store.add({ dispose() { source.cancel(); } }); return source.token; } + +/** + * A pool that aggregates multiple cancellation tokens. The pool's own token + * (accessible via `pool.token`) is cancelled only after every token added + * to the pool has been cancelled. Adding tokens after the pool token has + * been cancelled has no effect. + */ +export class CancellationTokenPool { + + private readonly _source = new CancellationTokenSource(); + private readonly _listeners = new DisposableStore(); + + private _total: number = 0; + private _cancelled: number = 0; + private _isDone: boolean = false; + + get token(): CancellationToken { + return this._source.token; + } + + /** + * Add a token to the pool. If the token is already cancelled it is counted + * immediately. Tokens added after the pool token has been cancelled are ignored. + */ + add(token: CancellationToken): void { + if (this._isDone) { + return; + } + + this._total++; + + if (token.isCancellationRequested) { + this._cancelled++; + this._check(); + return; + } + + const d = token.onCancellationRequested(() => { + d.dispose(); + this._cancelled++; + this._check(); + }); + this._listeners.add(d); + } + + private _check(): void { + if (!this._isDone && this._total > 0 && this._total === this._cancelled) { + this._isDone = true; + this._listeners.dispose(); + this._source.cancel(); + } + } + + dispose(): void { + this._listeners.dispose(); + this._source.dispose(); + } +} diff --git a/src/vs/base/test/common/cancellation.test.ts b/src/vs/base/test/common/cancellation.test.ts index 38ed33f8d62..7e5ad1060fd 100644 --- a/src/vs/base/test/common/cancellation.test.ts +++ b/src/vs/base/test/common/cancellation.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { CancellationToken, CancellationTokenSource } from '../../common/cancellation.js'; +import { CancellationToken, CancellationTokenSource, CancellationTokenPool } from '../../common/cancellation.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; suite('CancellationToken', function () { @@ -125,3 +125,155 @@ suite('CancellationToken', function () { parent.dispose(); }); }); + +suite('CancellationTokenPool', function () { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('empty pool token is not cancelled', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + assert.strictEqual(pool.token.isCancellationRequested, false); + }); + + test('pool token cancels when all tokens are cancelled', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source1 = new CancellationTokenSource(); + const source2 = new CancellationTokenSource(); + const source3 = new CancellationTokenSource(); + + pool.add(source1.token); + pool.add(source2.token); + pool.add(source3.token); + + assert.strictEqual(pool.token.isCancellationRequested, false); + + source1.cancel(); + assert.strictEqual(pool.token.isCancellationRequested, false); + + source2.cancel(); + assert.strictEqual(pool.token.isCancellationRequested, false); + + source3.cancel(); + assert.strictEqual(pool.token.isCancellationRequested, true); + + source1.dispose(); + source2.dispose(); + source3.dispose(); + }); + + test('pool token fires cancellation event when all tokens are cancelled', function () { + return new Promise(resolve => { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source1 = new CancellationTokenSource(); + const source2 = new CancellationTokenSource(); + + pool.add(source1.token); + pool.add(source2.token); + + store.add(pool.token.onCancellationRequested(() => resolve())); + + source1.cancel(); + source2.cancel(); + + source1.dispose(); + source2.dispose(); + }); + }); + + test('adding already cancelled token counts immediately', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source1 = new CancellationTokenSource(); + const source2 = new CancellationTokenSource(); + + source1.cancel(); // Cancel before adding to pool + + pool.add(source1.token); + assert.strictEqual(pool.token.isCancellationRequested, true); // 1 of 1 cancelled, so pool is cancelled + + pool.add(source2.token); // Adding after pool is done should have no effect + assert.strictEqual(pool.token.isCancellationRequested, true); + + source2.cancel(); // This should have no effect since pool is already done + assert.strictEqual(pool.token.isCancellationRequested, true); + + source1.dispose(); + source2.dispose(); + }); + + test('adding single already cancelled token cancels pool immediately', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source = new CancellationTokenSource(); + source.cancel(); + + pool.add(source.token); + assert.strictEqual(pool.token.isCancellationRequested, true); // 1 of 1 cancelled + + source.dispose(); + }); + + test('adding token after pool is done has no effect', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source1 = new CancellationTokenSource(); + const source2 = new CancellationTokenSource(); + + pool.add(source1.token); + source1.cancel(); // Pool should be done now + + assert.strictEqual(pool.token.isCancellationRequested, true); + + // Adding another token should have no effect + pool.add(source2.token); + source2.cancel(); + + assert.strictEqual(pool.token.isCancellationRequested, true); + + source1.dispose(); + source2.dispose(); + }); + + test('single token pool behaviour', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source = new CancellationTokenSource(); + pool.add(source.token); + + assert.strictEqual(pool.token.isCancellationRequested, false); + + source.cancel(); + assert.strictEqual(pool.token.isCancellationRequested, true); + + source.dispose(); + }); + + test('pool with only cancelled tokens', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source1 = new CancellationTokenSource(); + const source2 = new CancellationTokenSource(); + + source1.cancel(); + source2.cancel(); + + pool.add(source1.token); + pool.add(source2.token); + + assert.strictEqual(pool.token.isCancellationRequested, true); + + source1.dispose(); + source2.dispose(); + }); +}); diff --git a/src/vs/base/test/common/yaml.test.ts b/src/vs/base/test/common/yaml.test.ts index cba621bf023..be4ced9d65a 100644 --- a/src/vs/base/test/common/yaml.test.ts +++ b/src/vs/base/test/common/yaml.test.ts @@ -209,9 +209,9 @@ suite('YAML Parser', () => { ' database:', ' host:localhost', ' port: 5432', - ' credentials:', - ' username:admin', - ' password: secret123' + ' abcde123456:', + ' logger12:admin', + ' memory12: a23123112' ], { type: 'object', start: pos(0, 0), end: pos(6, 25), properties: [ @@ -232,16 +232,16 @@ suite('YAML Parser', () => { value: { type: 'number', start: pos(3, 10), end: pos(3, 14), value: 5432 } }, { - key: { type: 'string', start: pos(4, 4), end: pos(4, 15), value: 'credentials' }, + key: { type: 'string', start: pos(4, 4), end: pos(4, 15), value: 'abcde123456' }, value: { type: 'object', start: pos(5, 6), end: pos(6, 25), properties: [ { - key: { type: 'string', start: pos(5, 6), end: pos(5, 14), value: 'username' }, + key: { type: 'string', start: pos(5, 6), end: pos(5, 14), value: 'logger12' }, value: { type: 'string', start: pos(5, 15), end: pos(5, 20), value: 'admin' } }, { - key: { type: 'string', start: pos(6, 6), end: pos(6, 14), value: 'password' }, - value: { type: 'string', start: pos(6, 16), end: pos(6, 25), value: 'secret123' } + key: { type: 'string', start: pos(6, 6), end: pos(6, 14), value: 'memory12' }, + value: { type: 'string', start: pos(6, 16), end: pos(6, 25), value: 'a23123112' } } ] } diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.contribution.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.contribution.ts new file mode 100644 index 00000000000..014050a2119 --- /dev/null +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.contribution.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './floatingMenu.css'; +import { registerEditorContribution, EditorContributionInstantiation } from '../../../browser/editorExtensions.js'; +import { FloatingEditorToolbar } from './floatingMenu.js'; + +registerEditorContribution(FloatingEditorToolbar.ID, FloatingEditorToolbar, EditorContributionInstantiation.AfterFirstRender); diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css new file mode 100644 index 00000000000..07ab3a69c58 --- /dev/null +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.floating-menu-overlay-widget { + padding: 0px; + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + border-radius: 2px; + border: 1px solid var(--vscode-contrastBorder); + display: flex; + align-items: center; + z-index: 10; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + overflow: hidden; + + .action-item > .action-label { + padding: 5px; + font-size: 12px; + } + + .action-item:first-child > .action-label { + padding-left: 7px; + } + + .action-item:last-child > .action-label { + padding-right: 7px; + } + + .action-item .action-label.separator { + background-color: var(--vscode-menu-separatorBackground); + } +} diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts new file mode 100644 index 00000000000..4d9c8d3a82a --- /dev/null +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h } from '../../../../base/browser/dom.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, constObservable, observableFromEvent } from '../../../../base/common/observable.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ICodeEditor, OverlayWidgetPositionPreference } from '../../../browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; +import { IEditorContribution } from '../../../common/editorCommon.js'; + +export class FloatingEditorToolbar extends Disposable implements IEditorContribution { + static readonly ID = 'editor.contrib.floatingToolbar'; + + constructor( + editor: ICodeEditor, + @IInstantiationService instantiationService: IInstantiationService, + @IMenuService menuService: IMenuService + ) { + super(); + + const editorObs = this._register(observableCodeEditor(editor)); + + const menu = this._register(menuService.createMenu(MenuId.EditorContent, editor.contextKeyService)); + const menuIsEmptyObs = observableFromEvent(this, menu.onDidChange, () => menu.getActions().length === 0); + + this._register(autorun(reader => { + const menuIsEmpty = menuIsEmptyObs.read(reader); + if (menuIsEmpty) { + return; + } + + const container = h('div.floating-menu-overlay-widget'); + + // Set height explicitly to ensure that the floating menu element + // is rendered in the lower right corner at the correct position. + container.root.style.height = '28px'; + + // Toolbar + reader.store.add(instantiationService.createInstance(MenuWorkbenchToolBar, container.root, MenuId.EditorContent, { + hiddenItemStrategy: HiddenItemStrategy.Ignore, + menuOptions: { + arg: editor.getModel()?.uri, + shouldForwardArgs: true + }, + telemetrySource: 'editor.overlayToolbar', + toolbarOptions: { + primaryGroup: () => true, + useSeparatorsInPrimaryActions: true + }, + })); + + // Overlay widget + reader.store.add(editorObs.createOverlayWidget({ + allowEditorOverflow: false, + domNode: container.root, + minContentWidthInPx: constObservable(0), + position: constObservable({ + preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER + }) + })); + })); + } +} diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 1f42a726b7a..916cc40a980 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -63,6 +63,7 @@ import './contrib/wordOperations/browser/wordOperations.js'; import './contrib/wordPartOperations/browser/wordPartOperations.js'; import './contrib/readOnlyMessage/browser/contribution.js'; import './contrib/diffEditorBreadcrumbs/browser/contribution.js'; +import './contrib/floatingMenu/browser/floatingMenu.contribution.js'; // Load up these strings even in VSCode, even if they are not used // in order to get them translated diff --git a/src/vs/platform/mcp/common/mcpGalleryService.ts b/src/vs/platform/mcp/common/mcpGalleryService.ts index 7c5017f21d9..70f2634c268 100644 --- a/src/vs/platform/mcp/common/mcpGalleryService.ts +++ b/src/vs/platform/mcp/common/mcpGalleryService.ts @@ -44,18 +44,19 @@ interface IMcpRegistryInfo { } interface IGitHubInfo { - readonly 'name': string; - readonly 'name_with_owner': string; - readonly 'is_in_organization'?: boolean; - readonly 'license'?: string; - readonly 'opengraph_image_url'?: string; - readonly 'owner_avatar_url'?: string; - readonly 'primary_language'?: string; - readonly 'primary_language_color'?: string; - readonly 'pushed_at'?: string; - readonly 'stargazer_count'?: number; - readonly 'topics'?: readonly string[]; - readonly 'uses_custom_opengraph_image'?: boolean; + readonly name: string; + readonly name_with_owner: string; + readonly display_name?: string; + readonly is_in_organization?: boolean; + readonly license?: string; + readonly opengraph_image_url?: string; + readonly owner_avatar_url?: string; + readonly primary_language?: string; + readonly primary_language_color?: string; + readonly pushed_at?: string; + readonly stargazer_count?: number; + readonly topics?: readonly string[]; + readonly uses_custom_opengraph_image?: boolean; } interface IRawGalleryMcpServerMetaData { @@ -336,6 +337,10 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService displayName = nameParts[nameParts.length - 1].split('-').map(s => uppercaseFirstLetter(s)).join(' '); } + if (githubInfo?.display_name) { + displayName = githubInfo.display_name; + } + const icon: { light: string; dark: string } | undefined = githubInfo?.owner_avatar_url ? { light: githubInfo.owner_avatar_url, dark: githubInfo.owner_avatar_url diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index ff002feb111..8e754163168 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -11,10 +11,12 @@ import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuth import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js'; import Severity from '../../../base/common/severity.js'; import { INotificationService } from '../../../platform/notification/common/notification.js'; +import { ActivationKind, IExtensionService } from '../../services/extensions/common/extensions.js'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { IAuthenticationAccessService } from '../../services/authentication/browser/authenticationAccessService.js'; import { IAuthenticationUsageService } from '../../services/authentication/browser/authenticationUsageService.js'; +import { getAuthenticationProviderActivationEvent } from '../../services/authentication/browser/authenticationService.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { IOpenerService } from '../../../platform/opener/common/opener.js'; import { CancellationError } from '../../../base/common/errors.js'; @@ -116,6 +118,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu @IAuthenticationUsageService private readonly authenticationUsageService: IAuthenticationUsageService, @IDialogService private readonly dialogService: IDialogService, @INotificationService private readonly notificationService: INotificationService, + @IExtensionService private readonly extensionService: IExtensionService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IOpenerService private readonly openerService: IOpenerService, @ILogService private readonly logService: ILogService, @@ -200,6 +203,12 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } } + async $ensureProvider(id: string): Promise { + if (!this.authenticationService.isAuthenticationProviderRegistered(id)) { + return await this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id), ActivationKind.Immediate); + } + } + async $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): Promise { const obj = this._registrations.get(providerId); if (obj instanceof Emitter) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 12e3fcdcdf4..277ddeb381c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -189,6 +189,7 @@ export interface AuthenticationGetSessionOptions { export interface MainThreadAuthenticationShape extends IDisposable { $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedAuthorizationServers?: UriComponents[], supportsChallenges?: boolean): Promise; $unregisterAuthenticationProvider(id: string): Promise; + $ensureProvider(id: string): Promise; $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): Promise; $getSession(providerId: string, scopeListOrRequest: ReadonlyArray | IAuthenticationWwwAuthenticateRequest, extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise; $getAccounts(providerId: string): Promise>; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index dd829afab53..39814f50421 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -112,12 +112,14 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { } return await this._getSessionTaskSingler.getOrCreate(singlerKey, async () => { + await this._proxy.$ensureProvider(providerId); const extensionName = requestingExtension.displayName || requestingExtension.name; return this._proxy.$getSession(providerId, scopesOrRequest, extensionId, extensionName, options); }); } async getAccounts(providerId: string) { + await this._proxy.$ensureProvider(providerId); return await this._proxy.$getAccounts(providerId); } diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 9157f122f09..7fdea24ffcd 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -59,8 +59,6 @@ import { inQuickPickContext, getQuickNavigateHandler } from '../../quickaccess.j import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { isMacintosh } from '../../../../base/common/platform.js'; -import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import { FloatingEditorClickMenu } from '../../codeeditor.js'; import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { EditorAutoSave } from './editorAutoSave.js'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from '../../../../platform/quickinput/common/quickAccess.js'; @@ -136,8 +134,6 @@ registerWorkbenchContribution2(EditorStatusContribution.ID, EditorStatusContribu registerWorkbenchContribution2(UntitledTextEditorWorkingCopyEditorHandler.ID, UntitledTextEditorWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(DynamicEditorConfigurations.ID, DynamicEditorConfigurations, WorkbenchPhase.BlockRestore); -registerEditorContribution(FloatingEditorClickMenu.ID, FloatingEditorClickMenu, EditorContributionInstantiation.AfterFirstRender); - //#endregion //#region Quick Access diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 21be375a627..6ce21ebf240 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -25,6 +25,7 @@ import { IModelService } from '../../../../editor/common/services/model.js'; import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { AccessibilityHelpNLS } from '../../../../editor/common/standaloneStrings.js'; import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; +import { FloatingEditorToolbar } from '../../../../editor/contrib/floatingMenu/browser/floatingMenu.js'; import { localize } from '../../../../nls.js'; import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol, isIAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; import { ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX, IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; @@ -134,7 +135,8 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this._container.classList.add('hide'); } const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeActionController.ID && c.id !== FloatingEditorClickMenu.ID) + contributions: EditorExtensionsRegistry.getEditorContributions() + .filter(c => c.id !== CodeActionController.ID && c.id !== FloatingEditorClickMenu.ID && c.id !== FloatingEditorToolbar.ID) }; const titleBar = document.createElement('div'); titleBar.classList.add('accessible-view-title-bar'); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index fdc5b2ba208..44c7ec7faaf 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1690,6 +1690,7 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben const chatSentiment = chatEntitlementService.sentiment; const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0; const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown; + const anonymous = chatEntitlementService.anonymous; const free = chatEntitlementService.entitlement === ChatEntitlement.Free; const isAuxiliaryWindow = windowId !== mainWindow.vscodeWindowId; @@ -1697,7 +1698,7 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben let primaryActionTitle = isAuxiliaryWindow ? localize('openChat', "Open Chat") : localize('toggleChat', "Toggle Chat"); let primaryActionIcon = Codicon.chatSparkle; if (chatSentiment.installed && !chatSentiment.disabled) { - if (signedOut) { + if (signedOut && !anonymous) { primaryActionId = CHAT_SETUP_ACTION_ID; primaryActionTitle = localize('signInToChatSetup', "Sign in to use AI features..."); primaryActionIcon = Codicon.chatSparkleError; @@ -1715,7 +1716,8 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben }, Event.any( chatEntitlementService.onDidChangeSentiment, chatEntitlementService.onDidChangeQuotaExceeded, - chatEntitlementService.onDidChangeEntitlement + chatEntitlementService.onDidChangeEntitlement, + chatEntitlementService.onDidChangeAnonymous )); // Reduces flicker a bit on reload/restart diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index aefdbc56bf6..131cbf43732 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -16,25 +16,41 @@ import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../chatAttachmentWidgets.js'; +export interface IChatAttachmentsContentPartOptions { + readonly variables: IChatRequestVariableEntry[]; + readonly contentReferences?: ReadonlyArray; + readonly domNode?: HTMLElement; + readonly limit?: number; +} + export class ChatAttachmentsContentPart extends Disposable { private readonly attachedContextDisposables = this._register(new DisposableStore()); private readonly _onDidChangeVisibility = this._register(new Emitter()); private readonly _contextResourceLabels: ResourceLabels; + private _showingAll = false; + + private readonly variables: IChatRequestVariableEntry[]; + private readonly contentReferences: ReadonlyArray; + private readonly limit?: number; + public readonly domNode: HTMLElement | undefined; public contextMenuHandler?: (attachment: IChatRequestVariableEntry, event: MouseEvent) => void; constructor( - private readonly variables: IChatRequestVariableEntry[], - private readonly contentReferences: ReadonlyArray = [], - public readonly domNode: HTMLElement | undefined = dom.$('.chat-attached-context'), + options: IChatAttachmentsContentPartOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); + this.variables = options.variables; + this.contentReferences = options.contentReferences ?? []; + this.limit = options.limit; + this.domNode = options.domNode ?? dom.$('.chat-attached-context'); + this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); - this.initAttachedContext(domNode); - if (!domNode.childElementCount) { + this.initAttachedContext(this.domNode); + if (!this.domNode.childElementCount) { this.domNode = undefined; } } @@ -44,75 +60,130 @@ export class ChatAttachmentsContentPart extends Disposable { this.attachedContextDisposables.clear(); const hoverDelegate = this.attachedContextDisposables.add(createInstantHoverDelegate()); - for (const attachment of this.variables) { - const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; - const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; - const correspondingContentReference = this.contentReferences.find((ref) => (typeof ref.reference === 'object' && 'variableName' in ref.reference && ref.reference.variableName === attachment.name) || (URI.isUri(ref.reference) && basename(ref.reference.path) === attachment.name)); - const isAttachmentOmitted = correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Omitted; - const isAttachmentPartialOrOmitted = isAttachmentOmitted || correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Partial; + const visibleAttachments = this.getVisibleAttachments(); + const hasMoreAttachments = this.limit && this.variables.length > this.limit && !this._showingAll; - let widget; - if (attachment.kind === 'tool' || attachment.kind === 'toolset') { - widget = this.instantiationService.createInstance(ToolSetOrToolItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isElementVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isImageVariableEntry(attachment)) { - attachment.omittedState = isAttachmentPartialOrOmitted ? OmittedState.Full : attachment.omittedState; - widget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isPromptFileVariableEntry(attachment)) { - if (attachment.automaticallyAdded) { - continue; - } - widget = this.instantiationService.createInstance(PromptFileAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isPromptTextVariableEntry(attachment)) { - if (attachment.automaticallyAdded) { - continue; - } - widget = this.instantiationService.createInstance(PromptTextAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) { - widget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isPasteVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (resource && isNotebookOutputVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isSCMHistoryItemVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isSCMHistoryItemChangeVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(SCMHistoryItemChangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else { - widget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } + for (const attachment of visibleAttachments) { + this.renderAttachment(attachment, container, hoverDelegate); + } - let ariaLabel: string | null = null; - - if (isAttachmentPartialOrOmitted) { - widget.element.classList.add('warning'); - } - const description = correspondingContentReference?.options?.status?.description; - if (isAttachmentPartialOrOmitted) { - ariaLabel = `${ariaLabel}${description ? ` ${description}` : ''}`; - for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { - const element = widget.label.element.querySelector(selector); - if (element) { - element.classList.add('warning'); - } - } - } - - this._register(dom.addDisposableListener(widget.element, 'contextmenu', e => this.contextMenuHandler?.(attachment, e))); - - if (this.attachedContextDisposables.isDisposed) { - widget.dispose(); - return; - } - - if (ariaLabel) { - widget.element.ariaLabel = ariaLabel; - } - - this.attachedContextDisposables.add(widget); + if (hasMoreAttachments) { + this.renderShowMoreButton(container); } } + + private getVisibleAttachments(): IChatRequestVariableEntry[] { + if (!this.limit || this._showingAll) { + return this.variables; + } + return this.variables.slice(0, this.limit); + } + + private renderShowMoreButton(container: HTMLElement) { + const remainingCount = this.variables.length - (this.limit ?? 0); + + // Create a button that looks like the attachment pills + const showMoreButton = dom.$('div.chat-attached-context-attachment.chat-attachments-show-more-button'); + showMoreButton.setAttribute('role', 'button'); + showMoreButton.setAttribute('tabindex', '0'); + showMoreButton.style.cursor = 'pointer'; + + // Add pill icon (ellipsis) + const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-ellipsis')); + + // Add text label + const textLabel = dom.$('span.chat-attached-context-custom-text'); + textLabel.textContent = `${remainingCount} more`; + + showMoreButton.appendChild(pillIcon); + showMoreButton.appendChild(textLabel); + + // Add click and keyboard event handlers + const clickHandler = () => { + this._showingAll = true; + this.initAttachedContext(container); + }; + + this.attachedContextDisposables.add(dom.addDisposableListener(showMoreButton, 'click', clickHandler)); + this.attachedContextDisposables.add(dom.addDisposableListener(showMoreButton, 'keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + clickHandler(); + } + })); + + container.appendChild(showMoreButton); + this.attachedContextDisposables.add({ dispose: () => showMoreButton.remove() }); + } + + private renderAttachment(attachment: IChatRequestVariableEntry, container: HTMLElement, hoverDelegate: any) { + const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; + const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; + const correspondingContentReference = this.contentReferences.find((ref) => (typeof ref.reference === 'object' && 'variableName' in ref.reference && ref.reference.variableName === attachment.name) || (URI.isUri(ref.reference) && basename(ref.reference.path) === attachment.name)); + const isAttachmentOmitted = correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Omitted; + const isAttachmentPartialOrOmitted = isAttachmentOmitted || correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Partial; + + let widget; + if (attachment.kind === 'tool' || attachment.kind === 'toolset') { + widget = this.instantiationService.createInstance(ToolSetOrToolItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isElementVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isImageVariableEntry(attachment)) { + attachment.omittedState = isAttachmentPartialOrOmitted ? OmittedState.Full : attachment.omittedState; + widget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isPromptFileVariableEntry(attachment)) { + if (attachment.automaticallyAdded) { + return; // Skip automatically added prompt files + } + widget = this.instantiationService.createInstance(PromptFileAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isPromptTextVariableEntry(attachment)) { + if (attachment.automaticallyAdded) { + return; // Skip automatically added prompt text + } + widget = this.instantiationService.createInstance(PromptTextAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) { + widget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isPasteVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (resource && isNotebookOutputVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemChangeVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(SCMHistoryItemChangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else { + widget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } + + let ariaLabel: string | null = null; + + if (isAttachmentPartialOrOmitted) { + widget.element.classList.add('warning'); + } + const description = correspondingContentReference?.options?.status?.description; + if (isAttachmentPartialOrOmitted) { + ariaLabel = `${ariaLabel}${description ? ` ${description}` : ''}`; + for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { + const element = widget.label.element.querySelector(selector); + if (element) { + element.classList.add('warning'); + } + } + } + + this._register(dom.addDisposableListener(widget.element, 'contextmenu', e => this.contextMenuHandler?.(attachment, e))); + + if (this.attachedContextDisposables.isDisposed) { + widget.dispose(); + return; + } + + if (ariaLabel) { + widget.element.ariaLabel = ariaLabel; + } + + this.attachedContextDisposables.add(widget); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatPullRequestContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatPullRequestContentPart.ts index 2d8d045c1f7..d3df1823d78 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatPullRequestContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatPullRequestContentPart.ts @@ -16,6 +16,7 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { addDisposableListener } from '../../../../../base/browser/dom.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; +import { renderMarkdown } from '../../../../../base/browser/markdownRenderer.js'; export class ChatPullRequestContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; @@ -41,7 +42,8 @@ export class ChatPullRequestContentPart extends Disposable implements IChatConte const descriptionElement = dom.append(contentContainer, dom.$('.description')); const descriptionWrapper = dom.append(descriptionElement, dom.$('.description-wrapper')); - descriptionWrapper.textContent = this.pullRequestContent.description; + const markdown = this._register(renderMarkdown({ value: this.pullRequestContent.description })); + dom.append(descriptionWrapper, markdown.element); const seeMoreContainer = dom.append(descriptionElement, dom.$('.see-more')); const seeMore: HTMLAnchorElement = dom.append(seeMoreContainer, dom.$('a')); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts index 0525980d6f1..b52a35e54ee 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts @@ -103,6 +103,7 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IFileService private readonly _fileService: IFileService, ) { super(); this._currentWidth = width; @@ -220,19 +221,30 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { dom.h('.chat-collapsible-io-resource-actions@actions'), ]); - const entries = parts.map((part): IChatRequestVariableEntry => { + this.fillInResourceGroup(parts, el.items, el.actions).then(() => this._onDidChangeHeight.fire()); + + container.appendChild(el.root); + return el.root; + } + + private async fillInResourceGroup(parts: IChatCollapsibleIODataPart[], itemsContainer: HTMLElement, actionsContainer: HTMLElement) { + const entries = await Promise.all(parts.map(async (part): Promise => { if (part.mimeType && getAttachableImageExtension(part.mimeType)) { - return { kind: 'image', id: generateUuid(), name: basename(part.uri), value: part.value, mimeType: part.mimeType, isURL: false, references: [{ kind: 'reference', reference: part.uri }] }; + const value = part.value ?? await this._fileService.readFile(part.uri).then(f => f.value.buffer, () => undefined); + return { kind: 'image', id: generateUuid(), name: basename(part.uri), value, mimeType: part.mimeType, isURL: false, references: [{ kind: 'reference', reference: part.uri }] }; } else { return { kind: 'file', id: generateUuid(), name: basename(part.uri), fullName: part.uri.path, value: part.uri }; } - }); + })); const attachments = this._register(this._instantiationService.createInstance( ChatAttachmentsContentPart, - entries, - undefined, - undefined, + { + variables: entries, + limit: 5, + contentReferences: undefined, + domNode: undefined + } )); attachments.contextMenuHandler = (attachment, event) => { @@ -251,19 +263,17 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { } }; - el.items.appendChild(attachments.domNode!); + itemsContainer.appendChild(attachments.domNode!); - const toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, el.actions, MenuId.ChatToolOutputResourceToolbar, { + const toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, actionsContainer, MenuId.ChatToolOutputResourceToolbar, { menuOptions: { shouldForwardArgs: true, }, })); toolbar.context = { parts } satisfies IChatToolOutputResourceToolbarContext; - - container.appendChild(el.root); - return el.root; } + private addCodeBlock(part: IChatCollapsibleIOCodePart, container: HTMLElement) { const data: ICodeBlockData = { languageId: part.languageId, diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatPullRequestContent.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatPullRequestContent.css index df72d9464d9..345cdae7c23 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatPullRequestContent.css +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatPullRequestContent.css @@ -35,6 +35,10 @@ border-bottom: 1px solid var(--vscode-chat-requestBorder); } + p { + margin: 0px; + } + .description .see-more { display: none; position: absolute; diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 99e438866b2..7c9af6dba3c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -1451,7 +1451,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer | undefined, templateData: IChatListItemTemplate) { - return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, undefined); + return this.instantiationService.createInstance(ChatAttachmentsContentPart, { + variables, + contentReferences, + domNode: undefined + }); } private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index ffd7c78f2e4..2495835e597 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -44,6 +44,8 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { URI } from '../../../../base/common/uri.js'; import { IInlineCompletionsService } from '../../../../editor/browser/services/inlineCompletionsService.js'; import { IChatSessionsService } from '../common/chatSessionsService.js'; +import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; const gaugeForeground = registerColor('gauge.foreground', { dark: inputValidationInfoBorder, @@ -102,6 +104,9 @@ const defaultChat = { nextEditSuggestionsSetting: product.defaultChatAgent?.nextEditSuggestionsSetting ?? '', manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', manageOverageUrl: product.defaultChatAgent?.manageOverageUrl ?? '', + provider: product.defaultChatAgent?.provider ?? { default: { id: '', name: '' }, enterprise: { id: '', name: '' }, apple: { id: '', name: '' }, google: { id: '', name: '' } }, + termsStatementUrl: product.defaultChatAgent?.termsStatementUrl ?? '', + privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '' }; export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution { @@ -179,10 +184,6 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu let ariaLabel = localize('chatStatus', "Copilot Status"); let kind: StatusbarEntryKind | undefined; - // Check if there are any chat sessions in progress - const inProgress = this.chatSessionsService.getInProgress(); - const hasInProgressSessions = inProgress.some(item => item.count > 0); - if (isNewUser(this.chatEntitlementService)) { const entitlement = this.chatEntitlementService.entitlement; @@ -202,6 +203,7 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu } else { const chatQuotaExceeded = this.chatEntitlementService.quotas.chat?.percentRemaining === 0; const completionsQuotaExceeded = this.chatEntitlementService.quotas.completions?.percentRemaining === 0; + const chatSessionsInProgressCount = this.chatSessionsService.getInProgress().reduce((total, item) => total + item.count, 0); // Disabled if (this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted) { @@ -209,11 +211,21 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu ariaLabel = localize('copilotDisabledStatus', "Copilot Disabled"); } + // Sessions in progress + else if (chatSessionsInProgressCount > 0) { + text = `$(loading~spin)`; + if (chatSessionsInProgressCount > 1) { + ariaLabel = localize('chatSessionsInProgressStatus', "{0} chat sessions in progress", chatSessionsInProgressCount); + } else { + ariaLabel = localize('chatSessionInProgressStatus', "1 chat session in progress"); + } + } + // Signed out else if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { const signedOutWarning = localize('notSignedIntoCopilot', "Signed out"); - text = `$(copilot-not-connected) ${signedOutWarning}`; + text = `${this.chatEntitlementService.anonymous ? '$(copilot)' : '$(copilot-not-connected)'} ${signedOutWarning}`; ariaLabel = signedOutWarning; kind = 'prominent'; } @@ -247,14 +259,6 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu } } - // Show progress indicator when chat sessions are in progress - if (hasInProgressSessions) { - text = `$(loading~spin)\u00A0${text}`; - // Update aria label to include progress information - const sessionCount = inProgress.reduce((total, item) => total + item.count, 0); - ariaLabel = `${ariaLabel}, ${sessionCount} chat session${sessionCount === 1 ? '' : 's'} in progress`; - } - const baseResult = { name: localize('chatStatus', "Copilot Status"), text, @@ -345,7 +349,8 @@ class ChatStatusDashboard extends Disposable { @ITelemetryService private readonly telemetryService: ITelemetryService, @ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService, @IInlineCompletionsService private readonly inlineCompletionsService: IInlineCompletionsService, - @IChatSessionsService private readonly chatSessionsService: IChatSessionsService + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); } @@ -414,12 +419,24 @@ class ChatStatusDashboard extends Disposable { })(); } + // Anonymous Indicator + else if (this.chatEntitlementService.anonymous && this.chatEntitlementService.sentiment.installed) { + addSeparator(localize('anonymousTitle', "Copilot Usage")); + + this.createQuotaIndicator(this.element, disposables, undefined, localize('completionsLabel', "Code completions"), false); + this.createQuotaIndicator(this.element, disposables, undefined, localize('chatsLabel', "Chat messages"), false); + + this.element.appendChild($('div.description', undefined, localize('anonymousFooter', "Sign in to increase allowance."))); + } + // Chat sessions { let chatSessionsElement: HTMLElement | undefined; + const updateStatus = () => { const inProgress = this.chatSessionsService.getInProgress(); if (inProgress.some(item => item.count > 0)) { + addSeparator(localize('chatSessionsTitle', "Chat Sessions"), toAction({ id: 'workbench.view.chat.status.sessions', label: localize('viewChatSessionsLabel', "View Chat Sessions"), @@ -430,22 +447,17 @@ class ChatStatusDashboard extends Disposable { for (const { displayName, count } of inProgress) { if (count > 0) { - let lowerCaseName = displayName.toLocaleLowerCase(); - // Very specific case for providers that end in session/sessions to ensure we pluralize correctly - if (lowerCaseName.endsWith('session') || lowerCaseName.endsWith('sessions')) { - lowerCaseName = lowerCaseName.replace(/session$|sessions$/g, count > 1 ? 'sessions' : 'session'); - } - const text = localize('inProgressChatSession', "$(loading~spin) {0} {1} in progress", count, lowerCaseName); + const text = localize('inProgressChatSession', "$(loading~spin) {0} in progress", displayName); chatSessionsElement = this.element.appendChild($('div.description')); const parts = renderLabelWithIcons(text); chatSessionsElement.append(...parts); } } - } - else { + } else { chatSessionsElement?.remove(); } }; + updateStatus(); disposables.add(this.chatSessionsService.onDidChangeInProgress(updateStatus)); } @@ -497,14 +509,19 @@ class ChatStatusDashboard extends Disposable { // New to Copilot / Signed out { const newUser = isNewUser(this.chatEntitlementService); + const anonymousUser = this.chatEntitlementService.anonymous; const disabled = this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted; const signedOut = this.chatEntitlementService.entitlement === ChatEntitlement.Unknown; if (newUser || signedOut || disabled) { addSeparator(); - let descriptionText: string; - if (newUser) { + let descriptionText: string | MarkdownString; + if (newUser && anonymousUser) { + descriptionText = new MarkdownString(localize('activateDescriptionAnonymous', "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", defaultChat.provider.default.name, defaultChat.provider.default.name, defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl), { isTrusted: true }); + } else if (newUser) { descriptionText = localize('activateDescription', "Set up Copilot to use AI features."); + } else if (anonymousUser) { + descriptionText = localize('enableMoreDescription', "Sign in to enable more Copilot AI features."); } else if (disabled) { descriptionText = localize('enableDescription', "Enable Copilot to use AI features."); } else { @@ -514,17 +531,29 @@ class ChatStatusDashboard extends Disposable { let buttonLabel: string; if (newUser) { buttonLabel = localize('activateCopilotButton', "Set up Copilot"); + } else if (anonymousUser) { + buttonLabel = localize('enableMoreCopilotButton', "Enable more AI Features"); } else if (disabled) { buttonLabel = localize('enableCopilotButton', "Enable Copilot"); } else { buttonLabel = localize('signInToUseCopilotButton', "Sign in to use Copilot"); } - this.element.appendChild($('div.description', undefined, descriptionText)); + let setupArgs: { forceAnonymous: boolean } | undefined = undefined; + if (newUser && anonymousUser) { + setupArgs = { forceAnonymous: true }; + } + + if (typeof descriptionText === 'string') { + this.element.appendChild($('div.description', undefined, descriptionText)); + } else { + const markdown = this.instantiationService.createInstance(MarkdownRenderer, {}); + this.element.appendChild($('div.description', undefined, disposables.add(markdown.render(descriptionText)).element)); + } const button = disposables.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate })); button.label = buttonLabel; - disposables.add(button.onDidClick(() => this.runCommandAndClose('workbench.action.chat.triggerSetup'))); + disposables.add(button.onDidClick(() => this.runCommandAndClose('workbench.action.chat.triggerSetup', undefined, setupArgs))); } } @@ -579,18 +608,18 @@ class ChatStatusDashboard extends Disposable { } } - private runCommandAndClose(commandOrFn: string | Function): void { + private runCommandAndClose(commandOrFn: string | Function, ...args: any[]): void { if (typeof commandOrFn === 'function') { - commandOrFn(); + commandOrFn(...args); } else { this.telemetryService.publicLog2('workbenchActionExecuted', { id: commandOrFn, from: 'chat-status' }); - this.commandService.executeCommand(commandOrFn); + this.commandService.executeCommand(commandOrFn, ...args); } this.hoverService.hideHover(true); } - private createQuotaIndicator(container: HTMLElement, disposables: DisposableStore, quota: IQuotaSnapshot, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot) => void { + private createQuotaIndicator(container: HTMLElement, disposables: DisposableStore, quota: IQuotaSnapshot | undefined, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot) => void { const quotaValue = $('span.quota-value'); const quotaBit = $('div.quota-bit'); const overageLabel = $('span.overage-label'); @@ -614,18 +643,20 @@ class ChatStatusDashboard extends Disposable { disposables.add(manageOverageButton.onDidClick(() => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageOverageUrl))))); } - const update = (quota: IQuotaSnapshot) => { + const update = (quota: IQuotaSnapshot | undefined) => { quotaIndicator.classList.remove('error'); quotaIndicator.classList.remove('warning'); let usedPercentage: number; - if (quota.unlimited) { + if (!quota || quota.unlimited) { usedPercentage = 0; } else { usedPercentage = Math.max(0, 100 - quota.percentRemaining); } - if (quota.unlimited) { + if (!quota) { + quotaValue.textContent = localize('quotaLimited', "Limited"); + } else if (quota.unlimited) { quotaValue.textContent = localize('quotaUnlimited', "Included"); } else if (quota.overageCount) { quotaValue.textContent = localize('quotaDisplayWithOverage', "+{0} requests", this.quotaOverageFormatter.value.format(quota.overageCount)); @@ -642,7 +673,7 @@ class ChatStatusDashboard extends Disposable { } if (supportsOverage) { - if (quota.overageEnabled) { + if (quota?.overageEnabled) { overageLabel.textContent = localize('additionalUsageEnabled', "Additional paid premium requests enabled."); } else { overageLabel.textContent = localize('additionalUsageDisabled', "Additional paid premium requests disabled."); diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 67bcf14c5a8..9e48e0c80d1 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -2885,6 +2885,8 @@ have to be updated for changes to the rules above, or to support more deeply nes display: flex; align-items: center; width: 100%; + border: none; + outline: none; &:hover { background: var(--vscode-toolbar-hoverBackground); @@ -3008,3 +3010,24 @@ have to be updated for changes to the rules above, or to support more deeply nes .editor-instance .chat-todo-list-widget { background-color: var(--vscode-editor-background); } + +/* Show more attachments button styling */ +.chat-attachments-show-more-button { + opacity: 0.8; + transition: opacity 0.2s ease; +} + +.chat-attachments-show-more-button:hover { + opacity: 1; + background-color: var(--vscode-list-hoverBackground) !important; +} + +.chat-attachments-show-more-button:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.chat-attachments-show-more-button .chat-attached-context-custom-text { + font-style: italic; + color: var(--vscode-descriptionForeground); +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index 05d5035fa47..c126ca3628a 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -30,6 +30,10 @@ .chat-status-bar-entry-tooltip div.description { font-size: 11px; color: var(--vscode-descriptionForeground); + max-width: 250px; + display: flex; + align-items: center; + gap: 3px; } .chat-status-bar-entry-tooltip .monaco-button { diff --git a/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts b/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts index 19392dfdd2f..ee3b966e05a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { sumBy } from '../../../../base/common/arrays.js'; +import { disposableTimeout } from '../../../../base/common/async.js'; import { decodeBase64, VSBuffer } from '../../../../base/common/buffer.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenPool, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../base/common/map.js'; import { autorun } from '../../../../base/common/observable.js'; import { newWriteableStream, ReadableStreamEvents } from '../../../../base/common/stream.js'; import { equalsIgnoreCase } from '../../../../base/common/strings.js'; @@ -21,6 +23,14 @@ import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; import { IMcpService, McpCapability, McpResourceURI } from './mcpTypes.js'; import { MCP } from './modelContextProtocol.js'; +const MOMENTARY_CACHE_DURATION = 3000; + +interface IReadData { + contents: (MCP.TextResourceContents | MCP.BlobResourceContents)[]; + resourceURI: URL; + forSameURI: (MCP.TextResourceContents | MCP.BlobResourceContents)[]; +} + export class McpResourceFilesystem extends Disposable implements IWorkbenchContribution, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileAtomicReadCapability, @@ -28,6 +38,14 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr /** Defer getting the MCP service since this is a BlockRestore and no need to make it unnecessarily. */ private readonly _mcpServiceLazy = new Lazy(() => this._instantiationService.invokeFunction(a => a.get(IMcpService))); + /** + * For many file operations we re-read the resources quickly (e.g. stat + * before reading the file) and would prefer to avoid spamming the MCP + * with multiple reads. This is a very short-duration cache + * to solve that. + */ + private readonly _momentaryCache = new ResourceMap<{ pool: CancellationTokenPool; promise: Promise }>(); + private get _mcpService() { return this._mcpServiceLazy.value; } @@ -232,6 +250,28 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr } private async _readURI(uri: URI, token?: CancellationToken) { + const cached = this._momentaryCache.get(uri); + if (cached) { + cached.pool.add(token || CancellationToken.None); + return cached.promise; + } + + const pool = this._store.add(new CancellationTokenPool()); + pool.add(token || CancellationToken.None); + + const promise = this._readURIInner(uri, pool.token); + this._momentaryCache.set(uri, { pool, promise }); + + const disposable = this._store.add(disposableTimeout(() => { + this._momentaryCache.delete(uri); + this._store.delete(disposable); + this._store.delete(pool); + }, MOMENTARY_CACHE_DURATION)); + + return promise; + } + + private async _readURIInner(uri: URI, token?: CancellationToken): Promise { const { resourceURI, server } = this._decodeURI(uri); const res = await McpServer.callOn(server, r => r.readResource({ uri: resourceURI.toString() }, token), token); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 4158b0e5842..2f3fe9d654d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -105,6 +105,7 @@ import { NotebookHorizontalTracker } from './viewParts/notebookHorizontalTracker import { NotebookCellEditorPool } from './view/notebookCellEditorPool.js'; import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; import { NotebookCellLayoutManager } from './notebookCellLayoutManager.js'; +import { FloatingEditorToolbar } from '../../../../editor/contrib/floatingMenu/browser/floatingMenu.js'; const $ = DOM.$; @@ -113,6 +114,7 @@ export function getDefaultNotebookCreationOptions(): INotebookEditorCreationOpti const skipContributions = [ 'editor.contrib.review', FloatingEditorClickMenu.ID, + FloatingEditorToolbar.ID, 'editor.contrib.dirtydiff', 'editor.contrib.testingOutputPeek', 'editor.contrib.testingDecorations', diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts index cb40a3ddafa..9c4e641b049 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts @@ -60,10 +60,7 @@ export class GettingStartedInput extends EditorInput { return true; } - if (other instanceof GettingStartedInput) { - return other.selectedCategory === this.selectedCategory; - } - return false; + return other instanceof GettingStartedInput; } constructor( diff --git a/src/vs/workbench/services/accounts/common/defaultAccount.ts b/src/vs/workbench/services/accounts/common/defaultAccount.ts index 6d40eecba40..44131f363c1 100644 --- a/src/vs/workbench/services/accounts/common/defaultAccount.ts +++ b/src/vs/workbench/services/accounts/common/defaultAccount.ts @@ -214,7 +214,7 @@ export class DefaultAccountManagementContribution extends Disposable implements this.getTokenEntitlements(session.accessToken, tokenEntitlementUrl), ]); - const mcpRegistryProvider = this.productService.quality !== 'stable' && tokenEntitlements.mcp && this.configurationService.getValue('chat.mcp.enterprise.registry.enabled') === true ? await this.getMcpRegistryProvider(session.accessToken, mcpRegistryDataUrl) : undefined; + const mcpRegistryProvider = this.productService.quality !== 'stable' && tokenEntitlements.mcp ? await this.getMcpRegistryProvider(session.accessToken, mcpRegistryDataUrl) : undefined; return { sessionId: session.id, diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 0eaf7b78dd3..f7ff49aa653 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -396,12 +396,7 @@ export class AuthenticationService extends Disposable implements IAuthentication } private async tryActivateProvider(providerId: string, activateImmediate: boolean): Promise { - try { - await this._extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal); - } catch (e) { - this._logService.error(`Extension Service failed to activate authentication provider '${providerId}':`, e); - throw e; - } + await this._extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal); let provider = this._authenticationProviders.get(providerId); if (provider) { return provider; diff --git a/src/vs/workbench/services/chat/common/chatEntitlementService.ts b/src/vs/workbench/services/chat/common/chatEntitlementService.ts index d348bba0551..d66d41beda7 100644 --- a/src/vs/workbench/services/chat/common/chatEntitlementService.ts +++ b/src/vs/workbench/services/chat/common/chatEntitlementService.ts @@ -344,9 +344,8 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme } })); - this._register(this.onDidChangeEntitlement(() => { - updateAnonymousUsage(); - })); + this._register(this.onDidChangeEntitlement(() => updateAnonymousUsage())); + this._register(this.onDidChangeSentiment(() => updateAnonymousUsage())); } acceptQuotas(quotas: IQuotas): void { @@ -419,6 +418,10 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme return false; // only consider signed out users } + if (this.sentiment.hidden || this.sentiment.disabled || this.sentiment.untrusted) { + return false; // only consider enabled scenarios + } + return true; }