diff --git a/build/azure-pipelines/alpine/product-build-alpine-cli.yml b/build/azure-pipelines/alpine/product-build-alpine-cli.yml index 8b3920b5237..9f3f60a6b24 100644 --- a/build/azure-pipelines/alpine/product-build-alpine-cli.yml +++ b/build/azure-pipelines/alpine/product-build-alpine-cli.yml @@ -34,9 +34,6 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ../common/bump-insiders-version.yml@self - - template: ../cli/cli-apply-patches.yml@self - script: | diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml index ddf226b4306..c6d5ba27eda 100644 --- a/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/build/azure-pipelines/alpine/product-build-alpine.yml @@ -1,6 +1,4 @@ parameters: - - name: VSCODE_QUALITY - type: string - name: VSCODE_ARCH type: string @@ -57,9 +55,6 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ../common/bump-insiders-version.yml@self - - template: ../distro/download-distro.yml@self - task: AzureKeyVault@2 diff --git a/build/azure-pipelines/common/bump-insiders-version.yml b/build/azure-pipelines/common/bump-insiders-version.yml deleted file mode 100644 index 3cb9aa88128..00000000000 --- a/build/azure-pipelines/common/bump-insiders-version.yml +++ /dev/null @@ -1,23 +0,0 @@ -steps: - - script: | - set -e - BUILD_NAME="$(Build.BuildNumber)" # example "20251114.34 (insider)" - VSCODE_PATCH_VERSION="$(echo $BUILD_NAME | cut -d' ' -f1 | awk -F. '{printf "%s%03d", $1, $2}')" - VSCODE_MAJOR_MINOR_VERSION="$(node -p "require('./package.json').version.replace(/\.\d+$/, '')")" - VSCODE_INSIDERS_VERSION="${VSCODE_MAJOR_MINOR_VERSION}.${VSCODE_PATCH_VERSION}" - echo "Setting Insiders version to: $VSCODE_INSIDERS_VERSION" - node -e "require('fs').writeFileSync('package.json', JSON.stringify({...require('./package.json'), version: process.argv[1]}, null, 2))" $VSCODE_INSIDERS_VERSION - displayName: Override Insiders Version - condition: and(succeeded(), not(contains(variables['Agent.OS'], 'windows'))) - - - pwsh: | - $ErrorActionPreference = "Stop" - $buildName = "$(Build.BuildNumber)" # example "20251114.34 (insider)" - $buildParts = ($buildName -split ' ')[0] -split '\.' - $patchVersion = "{0}{1:000}" -f $buildParts[0], [int]$buildParts[1] - $majorMinorVersion = node -p "require('./package.json').version.replace(/\.\d+$/, '')" - $insidersVersion = "$majorMinorVersion.$patchVersion" - Write-Host "Setting Insiders version to: $insidersVersion" - node -e "require('fs').writeFileSync('package.json', JSON.stringify({...require('./package.json'), version: process.argv[1]}, null, 2))" $insidersVersion - displayName: Override Insiders Version - condition: and(succeeded(), contains(variables['Agent.OS'], 'windows')) diff --git a/build/azure-pipelines/darwin/product-build-darwin-ci.yml b/build/azure-pipelines/darwin/product-build-darwin-ci.yml index 93ea356295d..3920c4ec799 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-ci.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-ci.yml @@ -1,6 +1,4 @@ parameters: - - name: VSCODE_QUALITY - type: string - name: VSCODE_CIBUILD type: boolean - name: VSCODE_TEST_SUITE @@ -38,7 +36,6 @@ jobs: steps: - template: ./steps/product-build-darwin-compile.yml@self parameters: - VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_ARCH: arm64 VSCODE_CIBUILD: ${{ parameters.VSCODE_CIBUILD }} ${{ if eq(parameters.VSCODE_TEST_SUITE, 'Electron') }}: diff --git a/build/azure-pipelines/darwin/product-build-darwin-cli.yml b/build/azure-pipelines/darwin/product-build-darwin-cli.yml index 667cf016ffd..35a9b3566ce 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-cli.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-cli.yml @@ -35,9 +35,6 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ../common/bump-insiders-version.yml@self - - template: ../cli/cli-apply-patches.yml@self - task: Npm@1 diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml index a41494beb3d..23c85dc714a 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -1,7 +1,3 @@ -parameters: - - name: VSCODE_QUALITY - type: string - jobs: - job: macOSUniversal displayName: macOS (UNIVERSAL) @@ -26,9 +22,6 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ../common/bump-insiders-version.yml@self - - template: ../distro/download-distro.yml@self - task: AzureKeyVault@2 diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 34d70ac79d1..770a54f7925 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -1,6 +1,4 @@ parameters: - - name: VSCODE_QUALITY - type: string - name: VSCODE_ARCH type: string - name: VSCODE_CIBUILD @@ -74,7 +72,6 @@ jobs: steps: - template: ./steps/product-build-darwin-compile.yml@self parameters: - VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_ARCH: ${{ parameters.VSCODE_ARCH }} VSCODE_CIBUILD: ${{ parameters.VSCODE_CIBUILD }} VSCODE_RUN_ELECTRON_TESTS: ${{ parameters.VSCODE_RUN_ELECTRON_TESTS }} diff --git a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml index 523548e469e..d1d431505f6 100644 --- a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml +++ b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml @@ -1,6 +1,4 @@ parameters: - - name: VSCODE_QUALITY - type: string - name: VSCODE_ARCH type: string - name: VSCODE_CIBUILD @@ -23,9 +21,6 @@ steps: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ../../common/bump-insiders-version.yml@self - - template: ../../distro/download-distro.yml@self - task: AzureKeyVault@2 diff --git a/build/azure-pipelines/linux/product-build-linux-cli.yml b/build/azure-pipelines/linux/product-build-linux-cli.yml index 548bc04acb6..9052a29e18e 100644 --- a/build/azure-pipelines/linux/product-build-linux-cli.yml +++ b/build/azure-pipelines/linux/product-build-linux-cli.yml @@ -34,9 +34,6 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ../common/bump-insiders-version.yml@self - - template: ../cli/cli-apply-patches.yml@self - task: Npm@1 diff --git a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml index c0d65917d33..9dc3f9e120b 100644 --- a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml +++ b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml @@ -26,9 +26,6 @@ steps: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ../../common/bump-insiders-version.yml@self - - template: ../../distro/download-distro.yml@self - task: AzureKeyVault@2 diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 02acdef21e8..e9c8f74e659 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -192,8 +192,6 @@ extends: - stage: Compile jobs: - template: build/azure-pipelines/product-compile.yml@self - parameters: - VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: - stage: CompileCLI @@ -411,12 +409,10 @@ extends: - ${{ if eq(parameters.VSCODE_BUILD_ALPINE, true) }}: - template: build/azure-pipelines/alpine/product-build-alpine.yml@self parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_ARCH: x64 - ${{ if eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true) }}: - template: build/azure-pipelines/alpine/product-build-alpine.yml@self parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_ARCH: arm64 - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_MACOS'], true)) }}: @@ -434,31 +430,26 @@ extends: - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: - template: build/azure-pipelines/darwin/product-build-darwin-ci.yml@self parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} VSCODE_TEST_SUITE: Electron - template: build/azure-pipelines/darwin/product-build-darwin-ci.yml@self parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} VSCODE_TEST_SUITE: Browser - template: build/azure-pipelines/darwin/product-build-darwin-ci.yml@self parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} VSCODE_TEST_SUITE: Remote - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS, true)) }}: - template: build/azure-pipelines/darwin/product-build-darwin.yml@self parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_ARCH: x64 VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: - template: build/azure-pipelines/darwin/product-build-darwin.yml@self parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_ARCH: arm64 VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} VSCODE_RUN_ELECTRON_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} @@ -467,8 +458,6 @@ extends: - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(variables['VSCODE_BUILD_MACOS_UNIVERSAL'], true)) }}: - template: build/azure-pipelines/darwin/product-build-darwin-universal.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), or(eq(parameters.VSCODE_BUILD_MACOS, true), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true))) }}: - template: build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml@self @@ -482,8 +471,6 @@ extends: - Compile jobs: - template: build/azure-pipelines/web/product-build-web.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - ${{ if eq(variables['VSCODE_PUBLISH'], 'true') }}: - stage: Publish diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index c3d705fc077..e025e84f911 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -1,7 +1,3 @@ -parameters: - - name: VSCODE_QUALITY - type: string - jobs: - job: Compile timeoutInMinutes: 60 @@ -24,9 +20,6 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ./common/bump-insiders-version.yml@self - - template: ./distro/download-distro.yml@self - task: AzureKeyVault@2 diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index 89cf3fabc0d..aa0727a1988 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -31,9 +31,6 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ./common/bump-insiders-version.yml@self - - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 61ba3263107..d4f1af2d0e0 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -1,7 +1,3 @@ -parameters: - - name: VSCODE_QUALITY - type: string - jobs: - job: Web displayName: Web @@ -28,9 +24,6 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ../common/bump-insiders-version.yml@self - - template: ../distro/download-distro.yml@self - task: AzureKeyVault@2 diff --git a/build/azure-pipelines/win32/product-build-win32-cli.yml b/build/azure-pipelines/win32/product-build-win32-cli.yml index 26ab6ee247b..5dd69c3b50d 100644 --- a/build/azure-pipelines/win32/product-build-win32-cli.yml +++ b/build/azure-pipelines/win32/product-build-win32-cli.yml @@ -34,9 +34,6 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ../common/bump-insiders-version.yml@self - - template: ../cli/cli-apply-patches.yml@self - task: Npm@1 diff --git a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml index 44a1f060aaa..bdc807fdae5 100644 --- a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml @@ -23,9 +23,6 @@ steps: versionSource: fromFile versionFilePath: .nvmrc - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - template: ../../common/bump-insiders-version.yml@self - - task: UsePythonVersion@0 inputs: versionSpec: "3.x" diff --git a/build/gulpfile.vscode.mjs b/build/gulpfile.vscode.mjs index 89e9ec08dd4..8f5a7b0d516 100644 --- a/build/gulpfile.vscode.mjs +++ b/build/gulpfile.vscode.mjs @@ -417,9 +417,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op if (quality === 'stable' || quality === 'insider') { result = es.merge(result, gulp.src('.build/win32/appx/**', { base: '.build/win32' })); const rawVersion = version.replace(/-\w+$/, '').split('.'); - - // AppX doesn't support versions like `1.0.107.20251114039`, so we bring it back down to zero - const appxVersion = `${rawVersion[0]}.0.${rawVersion[1]}.${quality === 'insider' ? '0' : rawVersion[2]}`; + const appxVersion = `${rawVersion[0]}.0.${rawVersion[1]}.${rawVersion[2]}`; result = es.merge(result, gulp.src('resources/win32/appx/AppxManifest.xml', { base: '.' }) .pipe(replace('@@AppxPackageName@@', product.win32AppUserModelId)) .pipe(replace('@@AppxPackageVersion@@', appxVersion)) diff --git a/build/gulpfile.vscode.win32.mjs b/build/gulpfile.vscode.win32.mjs index cc32aa2564f..c10201dfc10 100644 --- a/build/gulpfile.vscode.win32.mjs +++ b/build/gulpfile.vscode.win32.mjs @@ -83,19 +83,12 @@ function buildWin32Setup(arch, target) { fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t')); const quality = product.quality || 'dev'; - let RawVersion = pkg.version.replace(/-\w+$/, ''); - - // InnoSetup doesn't support versions like `1.0.107.20251114039`, so we bring it back down to zero - if (quality === 'insider') { - RawVersion = RawVersion.replace(/(\d+)$/, '0'); - } - const definitions = { NameLong: product.nameLong, NameShort: product.nameShort, DirName: product.win32DirName, Version: pkg.version, - RawVersion, + RawVersion: pkg.version.replace(/-\w+$/, ''), NameVersion: product.win32NameVersion + (target === 'user' ? ' (User)' : ''), ExeBasename: product.nameShort, RegValueName: product.win32RegValueName, diff --git a/eslint.config.js b/eslint.config.js index f35fadddf8a..9d83f9269e3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -274,7 +274,6 @@ export default tseslint.config( 'src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts', 'src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts', 'src/vs/workbench/contrib/chat/browser/chatSessions/common.ts', - 'src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts', 'src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts', 'src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts', 'src/vs/workbench/contrib/chat/common/annotations.ts', diff --git a/package-lock.json b/package-lock.json index 235d59bb5c2..2e65906c148 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,7 +80,7 @@ "@types/yauzl": "^2.10.0", "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", - "@typescript/native-preview": "^7.0.0-dev.20251117", + "@typescript/native-preview": "^7.0.0-dev.20250812.1", "@vscode/gulp-electron": "^1.38.2", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", @@ -152,7 +152,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^6.0.0-dev.20251117", + "typescript": "^6.0.0-dev.20251110", "typescript-eslint": "^8.45.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -2509,28 +2509,28 @@ } }, "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20251117.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20251117.1.tgz", - "integrity": "sha512-JgKY4Q6jRCszCJ46c8tVrGVnmdiRPSKTW0UQvcyxdI7LG9NYMchJ/W7iUyFZVjG8BV1iUTl3DYml1xErPHLKeg==", + "version": "7.0.0-dev.20251110.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20251110.1.tgz", + "integrity": "sha512-yzCDN6wUV1kibefOTwxw1MdeIgaJOgN5/a06cMyUlEDcXBriV4O2v+yeXY8c3yzUaVVVO8CKtHPbCMwro4j1Dw==", "dev": true, "license": "Apache-2.0", "bin": { "tsgo": "bin/tsgo.js" }, "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251117.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251117.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20251117.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251117.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20251117.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251117.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20251117.1" + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251110.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251110.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20251110.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251110.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20251110.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251110.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20251110.1" } }, "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20251117.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20251117.1.tgz", - "integrity": "sha512-O7Hhb9m8AZJCAUSBbGmZs7Vm890Kh5Z3xAAASs+L4thtPM0oRckeaoXLvHeE9Qy1p8qG//EmZ3+uSdtUTV4wqg==", + "version": "7.0.0-dev.20251110.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20251110.1.tgz", + "integrity": "sha512-x3DskzZCgk5qA7BCcCC/8XuZiycvZk5reeqkNTuDYeWyF1ZCKa8WWZRbW5LaunaOtXV6UsAPRCqRC8Wx34mMCg==", "cpu": [ "arm64" ], @@ -2542,9 +2542,9 @@ ] }, "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20251117.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20251117.1.tgz", - "integrity": "sha512-/I/iWWvUvuy8BK0bXn5Kz6z2QwknwD2kl2estQxgsz9VgHHyLSyjAg7c18pX/re0Z9ISPz7wutEKabzdtRW8Uw==", + "version": "7.0.0-dev.20251110.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20251110.1.tgz", + "integrity": "sha512-tuS4akGtsPs+RTiVXEXOT41+as23DXCOhzeOEtYYVdhWVuMBYLHksdTx5PGoQrCc4SfETp5jDwhyqUaVYLDGcA==", "cpu": [ "x64" ], @@ -2556,9 +2556,9 @@ ] }, "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20251117.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20251117.1.tgz", - "integrity": "sha512-Mfnc8CytGICsYJCMbu3FwE/KDcVg4/QTFix6O31oUkj9ERp3zbSePVMQulkJTH2vuhDvJnVISHzIYawtq5QPTQ==", + "version": "7.0.0-dev.20251110.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20251110.1.tgz", + "integrity": "sha512-I9zOzHXFqIQIcTcf2Sx9EF6gLOKXUCMo5gsjoQm4/R22+19+TMLeAs7Q1aTvd8CX8kFCtpI1eeyNzIf76rxELA==", "cpu": [ "arm" ], @@ -2570,9 +2570,9 @@ ] }, "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20251117.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20251117.1.tgz", - "integrity": "sha512-YSkmJb4/WrS6ZMEJSDbv5o2Garms3+3yKsH+Y3JLUab0namf1Br7T53ydW7ijV2rE7j9DgJs9P+GNu8753St3Q==", + "version": "7.0.0-dev.20251110.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20251110.1.tgz", + "integrity": "sha512-IvSeQ1iw4uvBZ8+XrO9z80J9KfbkbTzfXliPHUsjZqEtpOJTf/Mv7xzMbv4mN4xOEGVUyBG47p846oW2HknogA==", "cpu": [ "arm64" ], @@ -2584,9 +2584,9 @@ ] }, "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20251117.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20251117.1.tgz", - "integrity": "sha512-R5KvnKuGsbozjHbmA+zPa4xVkQSutvtU9/PQJ7vjJL0xsvSsRUgOE2V2jlT+KnfjAhYVoIg2njtHdf0uv5k9Ow==", + "version": "7.0.0-dev.20251110.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20251110.1.tgz", + "integrity": "sha512-OWy32tgpP70rSRvmQZ6OgJpuv1pi4mQdng00eF3tfHheHluX3mvqqe86H0FOv5B9PuxlGwOZSUot1XHWadhAWg==", "cpu": [ "x64" ], @@ -2598,9 +2598,9 @@ ] }, "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20251117.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20251117.1.tgz", - "integrity": "sha512-xfEwDD9BwCm2gFf0AePfvXxjgQ/EDBDLRbSejtShTSFwrgdnRJ7iW63/ns/i31qLesTzGZaLxeAV8zgh6C2Ibg==", + "version": "7.0.0-dev.20251110.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20251110.1.tgz", + "integrity": "sha512-u/Bo0gIcQCv/4MDnV5f2FZR1dEdN2jk3MfkmJLKGG1zwbak4MY7sWNzvSRJHihwK2SxtcJEHus4tKb2ra2Rhig==", "cpu": [ "arm64" ], @@ -2612,9 +2612,9 @@ ] }, "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20251117.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20251117.1.tgz", - "integrity": "sha512-GhJ4GIygHSU86gZw6NkOnJKi/XW0Yw+1quanZ6BaOAZ+HY6aftuESy+NlbC6nUSGE2xmbvxqJgqchCIlC6YPoA==", + "version": "7.0.0-dev.20251110.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20251110.1.tgz", + "integrity": "sha512-1CysgwFRuNjR0bBYv6RI3fbXtAwzD5OlbxqOQFhf2lUulMZRIkP1w4eCChSndLVCTfnUEt5Bnmn1JEUauIE+kQ==", "cpu": [ "x64" ], @@ -17308,9 +17308,9 @@ "dev": true }, "node_modules/typescript": { - "version": "6.0.0-dev.20251117", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.0-dev.20251117.tgz", - "integrity": "sha512-BJkVdQDGWE8KxtuSLvWLQ/ju+n6FdSM8rq/2B9myrmKXeKa9HRG36MOTMgfZQUWDmPd2f5+U8fhU7xO2+WNa3g==", + "version": "6.0.0-dev.20251110", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.0-dev.20251110.tgz", + "integrity": "sha512-tHG+EJXTSaUCMbTNApOuVE3WmgOmEqUwQiAXnmwsF/sVKhPFHQA0+S1hml0Ro8kpayvD0d9AX5iC2S2s+TIQxQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index efb504f4ffd..c273e1f685b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.107.20251119", - "distro": "3ee33b7862b5e018538b730ae631f35747f57a2c", + "distro": "70b452e51d85528e38164c11d783791ead374f43", "author": { "name": "Microsoft Corporation" }, @@ -69,7 +69,7 @@ "extensions-ci": "node ./node_modules/gulp/bin/gulp.js extensions-ci", "extensions-ci-pr": "node ./node_modules/gulp/bin/gulp.js extensions-ci-pr", "perf": "node scripts/code-perf.js", - "update-build-ts-version": "npm install -D typescript@next @typescript/native-preview && (cd build && npm run compile)" + "update-build-ts-version": "npm install -D typescript@next && npm install -D @typescript/native-preview && (cd build && npm run compile)" }, "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", @@ -142,7 +142,7 @@ "@types/yauzl": "^2.10.0", "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", - "@typescript/native-preview": "^7.0.0-dev.20251117", + "@typescript/native-preview": "^7.0.0-dev.20250812.1", "@vscode/gulp-electron": "^1.38.2", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", @@ -214,7 +214,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^6.0.0-dev.20251117", + "typescript": "^6.0.0-dev.20251110", "typescript-eslint": "^8.45.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -240,4 +240,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index e19dde49541..ec188d02721 100644 --- a/src/main.ts +++ b/src/main.ts @@ -528,7 +528,8 @@ function configureCrashReporter(): void { productName: process.env['VSCODE_DEV'] ? `${productName} Dev` : productName, submitURL, uploadToServer, - compress: true + compress: true, + ignoreSystemCrashHandler: true }); } diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 28f3db1cc81..7048151d08a 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css index dcb66d31c5a..4d2fb516f20 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css @@ -42,21 +42,12 @@ } .monaco-select-box-dropdown-container > .select-box-details-pane { - padding: 5px; + padding: 5px 6px; } .monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row { cursor: pointer; -} - -.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:first-child { - border-top-left-radius: 5px; - border-top-right-radius: 5px; -} - -.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:last-child { - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; + padding-left: 2px; } .monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .option-text { diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 233ea2a4dfd..869b78e4eca 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -639,4 +639,5 @@ export const codiconsLibrary = { searchLarge: register('search-large', 0xec70), terminalGitBash: register('terminal-git-bash', 0xec71), windowActive: register('window-active', 0xec72), + forward: register('forward', 0xec73), } as const; diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 9529eb95910..f21630823a0 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -213,7 +213,7 @@ export interface IProductConfiguration { readonly enterpriseProviderId: string; readonly enterpriseProviderConfig: string; readonly enterpriseProviderUriSetting: string; - readonly scopes: string[]; + readonly scopes: string[][]; }; readonly tokenEntitlementUrl: string; readonly chatEntitlementUrl: string; diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index c341d98e26a..6299a251b89 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -143,14 +143,16 @@ export function ltrim(haystack: string, needle: string): string { } const needleLen = needle.length; - if (needleLen === 0 || haystack.length === 0) { - return haystack; - } - let offset = 0; - - while (haystack.indexOf(needle, offset) === offset) { - offset = offset + needleLen; + if (needleLen === 1) { + const ch = needle.charCodeAt(0); + while (offset < haystack.length && haystack.charCodeAt(offset) === ch) { + offset++; + } + } else { + while (haystack.startsWith(needle, offset)) { + offset += needleLen; + } } return haystack.substring(offset); } @@ -168,22 +170,18 @@ export function rtrim(haystack: string, needle: string): string { const needleLen = needle.length, haystackLen = haystack.length; - if (needleLen === 0 || haystackLen === 0) { - return haystack; + if (needleLen === 1) { + let end = haystackLen; + const ch = needle.charCodeAt(0); + while (end > 0 && haystack.charCodeAt(end - 1) === ch) { + end--; + } + return haystack.substring(0, end); } - let offset = haystackLen, - idx = -1; - - while (true) { - idx = haystack.lastIndexOf(needle, offset - 1); - if (idx === -1 || idx + needleLen !== offset) { - break; - } - if (idx === 0) { - return ''; - } - offset = idx; + let offset = haystackLen; + while (offset > 0 && haystack.endsWith(needle, offset)) { + offset -= needleLen; } return haystack.substring(0, offset); @@ -1197,10 +1195,9 @@ export class AmbiguousCharacters { ); }); - private static readonly cache = new LRUCachedFunction< - string[], - AmbiguousCharacters - >({ getCacheKey: JSON.stringify }, (locales) => { + private static readonly cache = new LRUCachedFunction((localesStr) => { + const locales = localesStr.split(','); + function arrayToMap(arr: number[]): Map { const result = new Map(); for (let i = 0; i < arr.length; i += 2) { @@ -1257,8 +1254,8 @@ export class AmbiguousCharacters { return new AmbiguousCharacters(map); }); - public static getInstance(locales: Set): AmbiguousCharacters { - return AmbiguousCharacters.cache.get(Array.from(locales)); + public static getInstance(locales: Iterable): AmbiguousCharacters { + return AmbiguousCharacters.cache.get(Array.from(locales).join(',')); } private static _locales = new Lazy(() => diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index cfdf7836392..cfde5423e06 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -215,6 +215,11 @@ suite('Strings', () => { assert.strictEqual(strings.ltrim('///', '/'), ''); assert.strictEqual(strings.ltrim('', ''), ''); assert.strictEqual(strings.ltrim('', '/'), ''); + // Multi-character needle with consecutive repetitions + assert.strictEqual(strings.ltrim('---hello', '---'), 'hello'); + assert.strictEqual(strings.ltrim('------hello', '---'), 'hello'); + assert.strictEqual(strings.ltrim('---------hello', '---'), 'hello'); + assert.strictEqual(strings.ltrim('hello---', '---'), 'hello---'); }); test('rtrim', () => { @@ -228,6 +233,13 @@ suite('Strings', () => { assert.strictEqual(strings.rtrim('///', '/'), ''); assert.strictEqual(strings.rtrim('', ''), ''); assert.strictEqual(strings.rtrim('', '/'), ''); + // Multi-character needle with consecutive repetitions (bug fix) + assert.strictEqual(strings.rtrim('hello---', '---'), 'hello'); + assert.strictEqual(strings.rtrim('hello------', '---'), 'hello'); + assert.strictEqual(strings.rtrim('hello---------', '---'), 'hello'); + assert.strictEqual(strings.rtrim('---hello', '---'), '---hello'); + assert.strictEqual(strings.rtrim('hello world' + '---'.repeat(10), '---'), 'hello world'); + assert.strictEqual(strings.rtrim('path/to/file///', '//'), 'path/to/file/'); }); test('trim', () => { diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index b417161930f..53c4692efa6 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -33,6 +33,7 @@ import { IME } from '../../../../../base/common/ime.js'; import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; +import { inputLatency } from '../../../../../base/browser/performance.js'; // Corresponds to classes in nativeEditContext.css enum CompositionClassName { @@ -125,12 +126,16 @@ export class NativeEditContext extends AbstractEditContext { this.logService.trace('NativeEditContext#cut (before viewController.cut)'); this._viewController.cut(); })); + this._register(addDisposableListener(this.domNode.domNode, 'selectionchange', () => { + inputLatency.onSelectionChange(); + })); this._register(addDisposableListener(this.domNode.domNode, 'keyup', (e) => this._onKeyUp(e))); this._register(addDisposableListener(this.domNode.domNode, 'keydown', async (e) => this._onKeyDown(e))); this._register(addDisposableListener(this._imeTextArea.domNode, 'keyup', (e) => this._onKeyUp(e))); this._register(addDisposableListener(this._imeTextArea.domNode, 'keydown', async (e) => this._onKeyDown(e))); this._register(addDisposableListener(this.domNode.domNode, 'beforeinput', async (e) => { + inputLatency.onBeforeInput(); if (e.inputType === 'insertParagraph' || e.inputType === 'insertLineBreak') { this._onType(this._viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 }); } @@ -166,6 +171,7 @@ export class NativeEditContext extends AbstractEditContext { this._register(editContextAddDisposableListener(this._editContext, 'characterboundsupdate', (e) => this._updateCharacterBounds(e))); let highSurrogateCharacter: string | undefined; this._register(editContextAddDisposableListener(this._editContext, 'textupdate', (e) => { + inputLatency.onInput(); const text = e.text; if (text.length === 1) { const charCode = text.charCodeAt(0); @@ -355,10 +361,12 @@ export class NativeEditContext extends AbstractEditContext { // --- Private methods --- private _onKeyUp(e: KeyboardEvent) { + inputLatency.onKeyUp(); this._viewController.emitKeyUp(new StandardKeyboardEvent(e)); } private _onKeyDown(e: KeyboardEvent) { + inputLatency.onKeyDown(); const standardKeyboardEvent = new StandardKeyboardEvent(e); // When the IME is visible, the keys, like arrow-left and arrow-right, should be used to navigate in the IME, and should not be propagated further if (standardKeyboardEvent.keyCode === KeyCode.KEY_IN_COMPOSITION) { diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css index ea9e47d9e2a..425b87c5258 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css @@ -39,6 +39,6 @@ } .action-item .action-label.separator { - background-color: var(--vscode-menu-separatorBackground); + background-color: var(--vscode-button-separator); } } diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index df5658c8258..ef8841dc25b 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -69,7 +69,7 @@ export class TelemetryService implements ITelemetryService { this._sendErrorTelemetry = !!config.sendErrorTelemetry; // static cleanup pattern for: `vscode-file:///DANGEROUS/PATH/resources/app/Useful/Information` - this._cleanupPatterns = [/(vscode-)?file:\/\/\/.*?\/resources\/app\//gi]; + this._cleanupPatterns = [/(vscode-)?file:\/\/.*?\/resources\/app\//gi]; for (const piiPath of this._piiPaths) { this._cleanupPatterns.push(new RegExp(escapeRegExpCharacters(piiPath), 'gi')); diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index bf9522a7616..d9e00af00b0 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -743,5 +743,240 @@ suite('TelemetryService', () => { service.dispose(); }); + test('Unexpected Error Telemetry removes Windows PII but preserves code path', sinonTestFn(function (this: any) { + const origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + Errors.setUnexpectedErrorHandler(() => { }); + + try { + const testAppender = new TestTelemetryAppender(); + const service = new TestErrorTelemetryService({ appenders: [testAppender] }); + const errorTelemetry = new ErrorTelemetry(service); + + const windowsUserPath = 'c:/Users/bpasero/AppData/Local/Programs/Microsoft%20VS%20Code%20Insiders/resources/app/'; + const codePath = 'out/vs/workbench/workbench.desktop.main.js'; + const stack = [ + ` at cTe.gc (vscode-file://vscode-app/${windowsUserPath}${codePath}:2724:81492)`, + ` at async cTe.setInput (vscode-file://vscode-app/${windowsUserPath}${codePath}:2724:80650)`, + ` at async qJe.S (vscode-file://vscode-app/${windowsUserPath}${codePath}:698:58520)`, + ` at async qJe.L (vscode-file://vscode-app/${windowsUserPath}${codePath}:698:57080)`, + ` at async qJe.openEditor (vscode-file://vscode-app/${windowsUserPath}${codePath}:698:56162)` + ]; + + const windowsError: any = new Error('The editor could not be opened because the file was not found.'); + windowsError.stack = stack.join('\n'); + + Errors.onUnexpectedError(windowsError); + this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + assert.strictEqual(testAppender.getEventsCount(), 1); + // Verify PII (username and path) is removed + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('bpasero'), -1); + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('Users'), -1); + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('c:/Users'), -1); + // Verify important code path is preserved + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(codePath), -1); + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('out/vs/workbench'), -1); + + errorTelemetry.dispose(); + service.dispose(); + } finally { + Errors.setUnexpectedErrorHandler(origErrorHandler); + } + })); + + test('Uncaught Error Telemetry removes Windows PII but preserves code path', sinonTestFn(function (this: any) { + const errorStub = sinon.stub(); + mainWindow.onerror = errorStub; + + const testAppender = new TestTelemetryAppender(); + const service = new TestErrorTelemetryService({ appenders: [testAppender] }); + const errorTelemetry = new ErrorTelemetry(service); + + const windowsUserPath = 'c:/Users/bpasero/AppData/Local/Programs/Microsoft%20VS%20Code%20Insiders/resources/app/'; + const codePath = 'out/vs/workbench/workbench.desktop.main.js'; + const stack = [ + ` at cTe.gc (vscode-file://vscode-app/${windowsUserPath}${codePath}:2724:81492)`, + ` at async cTe.setInput (vscode-file://vscode-app/${windowsUserPath}${codePath}:2724:80650)`, + ` at async qJe.S (vscode-file://vscode-app/${windowsUserPath}${codePath}:698:58520)` + ]; + + const windowsError: any = new Error('The editor could not be opened because the file was not found.'); + windowsError.stack = stack.join('\n'); + + mainWindow.onerror('The editor could not be opened because the file was not found.', 'test.js', 2, 42, windowsError); + this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + assert.strictEqual(errorStub.callCount, 1); + // Verify PII (username and path) is removed + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('bpasero'), -1); + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('Users'), -1); + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('c:/Users'), -1); + // Verify important code path is preserved + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(codePath), -1); + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('out/vs/workbench'), -1); + + errorTelemetry.dispose(); + service.dispose(); + sinon.restore(); + })); + + test('Unexpected Error Telemetry removes macOS PII but preserves code path', sinonTestFn(function (this: any) { + const origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + Errors.setUnexpectedErrorHandler(() => { }); + + try { + const testAppender = new TestTelemetryAppender(); + const service = new TestErrorTelemetryService({ appenders: [testAppender] }); + const errorTelemetry = new ErrorTelemetry(service); + + const macUserPath = 'Applications/Visual%20Studio%20Code%20-%20Insiders.app/Contents/Resources/app/'; + const codePath = 'out/vs/workbench/workbench.desktop.main.js'; + const stack = [ + ` at uTe.gc (vscode-file://vscode-app/${macUserPath}${codePath}:2720:81492)`, + ` at async uTe.setInput (vscode-file://vscode-app/${macUserPath}${codePath}:2720:80650)`, + ` at async JJe.S (vscode-file://vscode-app/${macUserPath}${codePath}:698:58520)`, + ` at async JJe.L (vscode-file://vscode-app/${macUserPath}${codePath}:698:57080)`, + ` at async JJe.openEditor (vscode-file://vscode-app/${macUserPath}${codePath}:698:56162)` + ]; + + const macError: any = new Error('The editor could not be opened because the file was not found.'); + macError.stack = stack.join('\n'); + + Errors.onUnexpectedError(macError); + this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + assert.strictEqual(testAppender.getEventsCount(), 1); + // Verify PII (application path) is removed + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('Applications/Visual'), -1); + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('Visual%20Studio%20Code'), -1); + // Verify important code path is preserved + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(codePath), -1); + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('out/vs/workbench'), -1); + + errorTelemetry.dispose(); + service.dispose(); + } finally { + Errors.setUnexpectedErrorHandler(origErrorHandler); + } + })); + + test('Uncaught Error Telemetry removes macOS PII but preserves code path', sinonTestFn(function (this: any) { + const errorStub = sinon.stub(); + mainWindow.onerror = errorStub; + + const testAppender = new TestTelemetryAppender(); + const service = new TestErrorTelemetryService({ appenders: [testAppender] }); + const errorTelemetry = new ErrorTelemetry(service); + + const macUserPath = 'Applications/Visual%20Studio%20Code%20-%20Insiders.app/Contents/Resources/app/'; + const codePath = 'out/vs/workbench/workbench.desktop.main.js'; + const stack = [ + ` at uTe.gc (vscode-file://vscode-app/${macUserPath}${codePath}:2720:81492)`, + ` at async uTe.setInput (vscode-file://vscode-app/${macUserPath}${codePath}:2720:80650)`, + ` at async JJe.S (vscode-file://vscode-app/${macUserPath}${codePath}:698:58520)` + ]; + + const macError: any = new Error('The editor could not be opened because the file was not found.'); + macError.stack = stack.join('\n'); + + mainWindow.onerror('The editor could not be opened because the file was not found.', 'test.js', 2, 42, macError); + this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + assert.strictEqual(errorStub.callCount, 1); + // Verify PII (application path) is removed + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('Applications/Visual'), -1); + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('Visual%20Studio%20Code'), -1); + // Verify important code path is preserved + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(codePath), -1); + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('out/vs/workbench'), -1); + + errorTelemetry.dispose(); + service.dispose(); + sinon.restore(); + })); + + test('Unexpected Error Telemetry removes Linux PII but preserves code path', sinonTestFn(function (this: any) { + const origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + Errors.setUnexpectedErrorHandler(() => { }); + + try { + const testAppender = new TestTelemetryAppender(); + const service = new TestErrorTelemetryService({ appenders: [testAppender] }); + const errorTelemetry = new ErrorTelemetry(service); + + const linuxUserPath = '/home/parallels/GitDevelopment/vscode-node-sqlite3-perf/'; + const linuxSystemPath = 'usr/share/code-insiders/resources/app/'; + const codePath = 'out/vs/workbench/workbench.desktop.main.js'; + const stack = [ + ` at _kt.G (vscode-file://vscode-app/${linuxSystemPath}${codePath}:3825:65940)`, + ` at _kt.F (vscode-file://vscode-app/${linuxSystemPath}${codePath}:3825:65765)`, + ` at async axt.L (vscode-file://vscode-app/${linuxSystemPath}${codePath}:3830:9998)`, + ` at async axt.readStream (vscode-file://vscode-app/${linuxSystemPath}${codePath}:3830:9773)`, + ` at async mye.Eb (vscode-file://vscode-app/${linuxSystemPath}${codePath}:1313:12359)` + ]; + + const linuxError: any = new Error(`Invalid fake file 'git:${linuxUserPath}index.js.git?{"path":"${linuxUserPath}index.js","ref":""}' (Canceled: Canceled)`); + linuxError.stack = stack.join('\n'); + + Errors.onUnexpectedError(linuxError); + this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + assert.strictEqual(testAppender.getEventsCount(), 1); + // Verify PII (username and home directory) is removed + assert.strictEqual(testAppender.events[0].data.msg.indexOf('parallels'), -1); + assert.strictEqual(testAppender.events[0].data.msg.indexOf('/home/parallels'), -1); + assert.strictEqual(testAppender.events[0].data.msg.indexOf('GitDevelopment'), -1); + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('parallels'), -1); + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('/home/parallels'), -1); + // Verify important code path is preserved + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(codePath), -1); + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('out/vs/workbench'), -1); + + errorTelemetry.dispose(); + service.dispose(); + } finally { + Errors.setUnexpectedErrorHandler(origErrorHandler); + } + })); + + test('Uncaught Error Telemetry removes Linux PII but preserves code path', sinonTestFn(function (this: any) { + const errorStub = sinon.stub(); + mainWindow.onerror = errorStub; + + const testAppender = new TestTelemetryAppender(); + const service = new TestErrorTelemetryService({ appenders: [testAppender] }); + const errorTelemetry = new ErrorTelemetry(service); + + const linuxUserPath = '/home/parallels/GitDevelopment/vscode-node-sqlite3-perf/'; + const linuxSystemPath = 'usr/share/code-insiders/resources/app/'; + const codePath = 'out/vs/workbench/workbench.desktop.main.js'; + const stack = [ + ` at _kt.G (vscode-file://vscode-app/${linuxSystemPath}${codePath}:3825:65940)`, + ` at _kt.F (vscode-file://vscode-app/${linuxSystemPath}${codePath}:3825:65765)`, + ` at async axt.L (vscode-file://vscode-app/${linuxSystemPath}${codePath}:3830:9998)` + ]; + + const linuxError: any = new Error(`Unable to read file 'git:${linuxUserPath}index.js.git'`); + linuxError.stack = stack.join('\n'); + + mainWindow.onerror(`Unable to read file 'git:${linuxUserPath}index.js.git'`, 'test.js', 2, 42, linuxError); + this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); + + assert.strictEqual(errorStub.callCount, 1); + // Verify PII (username and home directory) is removed + assert.strictEqual(testAppender.events[0].data.msg.indexOf('parallels'), -1); + assert.strictEqual(testAppender.events[0].data.msg.indexOf('/home/parallels'), -1); + assert.strictEqual(testAppender.events[0].data.msg.indexOf('GitDevelopment'), -1); + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('parallels'), -1); + assert.strictEqual(testAppender.events[0].data.callstack.indexOf('/home/parallels'), -1); + // Verify important code path is preserved + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(codePath), -1); + assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('out/vs/workbench'), -1); + + errorTelemetry.dispose(); + service.dispose(); + sinon.restore(); + })); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index e741ebf88ae..089ce8b729e 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -180,8 +180,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA try { return await this._proxy.$invokeAgent(handle, request, { history, - chatSessionContext: chatSession?.contributedChatSession, - chatSummary: request.chatSummary + chatSessionContext: chatSession?.contributedChatSession }, token) ?? {}; } finally { this._pendingProgress.delete(request.requestId); diff --git a/src/vs/workbench/api/browser/mainThreadChatContext.ts b/src/vs/workbench/api/browser/mainThreadChatContext.ts index 38cab5806a0..c17babbcc46 100644 --- a/src/vs/workbench/api/browser/mainThreadChatContext.ts +++ b/src/vs/workbench/api/browser/mainThreadChatContext.ts @@ -15,7 +15,7 @@ import { URI } from '../../../base/common/uri.js'; @extHostNamedCustomer(MainContext.MainThreadChatContext) export class MainThreadChatContext extends Disposable implements MainThreadChatContextShape { private readonly _proxy: ExtHostChatContextShape; - private readonly _providers = new Map(); + private readonly _providers = new Map(); constructor( extHostContext: IExtHostContext, @@ -25,7 +25,7 @@ export class MainThreadChatContext extends Disposable implements MainThreadChatC this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatContext); } - $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[], _options: { icon: ThemeIcon }, support: IChatContextSupport): void { + $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[] | undefined, _options: { icon: ThemeIcon }, support: IChatContextSupport): void { this._providers.set(handle, { selector, support, id }); this._chatContextService.registerChatContextProvider(id, selector, { provideChatContext: (token: CancellationToken) => { @@ -48,4 +48,12 @@ export class MainThreadChatContext extends Disposable implements MainThreadChatC this._chatContextService.unregisterChatContextProvider(provider.id); this._providers.delete(handle); } + + $updateWorkspaceContextItems(handle: number, items: IChatContextItem[]): void { + const provider = this._providers.get(handle); + if (!provider) { + return; + } + this._chatContextService.updateWorkspaceContextItems(provider.id, items); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index da16fe2d2e4..a976c20189f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1537,9 +1537,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatOutputRenderer'); return extHostChatOutputRenderer.registerChatOutputRenderer(extension, viewType, renderer); }, - registerChatContextProvider(selector: vscode.DocumentSelector, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { + registerChatContextProvider(selector: vscode.DocumentSelector | undefined, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatContextProvider'); - return extHostChatContext.registerChatContextProvider(checkSelector(selector), `${extension.id}-${id}`, provider); + return extHostChatContext.registerChatContextProvider(selector ? checkSelector(selector) : undefined, `${extension.id}-${id}`, provider); }, }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 5b8ca6ea90f..027f7debe74 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1329,8 +1329,9 @@ export interface ExtHostChatContextShape { } export interface MainThreadChatContextShape extends IDisposable { - $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[], options: {}, support: IChatContextSupport): void; + $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[] | undefined, options: {}, support: IChatContextSupport): void; $unregisterChatContextProvider(handle: number): void; + $updateWorkspaceContextItems(handle: number, items: IChatContextItem[]): void; } export interface MainThreadEmbeddingsShape extends IDisposable { @@ -1420,7 +1421,7 @@ export interface IChatSessionContextDto { } export interface ExtHostChatAgentsShape2 { - $invokeAgent(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContextDto; chatSummary?: { prompt?: string; history?: string } }, token: CancellationToken): Promise; + $invokeAgent(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContextDto }, token: CancellationToken): Promise; $provideFollowups(request: Dto, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $acceptFeedback(handle: number, result: IChatAgentResult, voteAction: IChatVoteAction): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index b724e66b454..3c0b9afcd65 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -559,7 +559,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS this._onDidChangeChatRequestTools.fire(request.extRequest); } - async $invokeAgent(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContextDto; chatSummary?: { prompt?: string; history?: string } }, token: CancellationToken): Promise { + async $invokeAgent(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContextDto }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { throw new Error(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`); @@ -606,11 +606,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS }; } - const chatContext: vscode.ChatContext = { - history, - chatSessionContext, - chatSummary: context.chatSummary - }; + const chatContext: vscode.ChatContext = { history, chatSessionContext }; const task = agent.invoke( extRequest, chatContext, diff --git a/src/vs/workbench/api/common/extHostChatContext.ts b/src/vs/workbench/api/common/extHostChatContext.ts index 8ad7bbd595e..e2ee3c42e36 100644 --- a/src/vs/workbench/api/common/extHostChatContext.ts +++ b/src/vs/workbench/api/common/extHostChatContext.ts @@ -10,18 +10,20 @@ import { ExtHostChatContextShape, MainContext, MainThreadChatContextShape } from import { DocumentSelector } from './extHostTypeConverters.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import { IChatContextItem } from '../../contrib/chat/common/chatContext.js'; +import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; -export class ExtHostChatContext implements ExtHostChatContextShape { +export class ExtHostChatContext extends Disposable implements ExtHostChatContextShape { declare _serviceBrand: undefined; private _proxy: MainThreadChatContextShape; private _handlePool: number = 0; - private _providers: Map = new Map(); + private _providers: Map = new Map(); private _itemPool: number = 0; private _items: Map> = new Map(); // handle -> itemHandle -> item constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService, ) { + super(); this._proxy = extHostRpc.getProxy(MainContext.MainThreadChatContext); } @@ -83,16 +85,7 @@ export class ExtHostChatContext implements ExtHostChatContextShape { return item; } - async $resolveChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise { - const provider = this._getProvider(handle); - - if (!provider.resolveChatContext) { - throw new Error('resolveChatContext not implemented'); - } - const extItem = this._items.get(handle)?.get(context.handle); - if (!extItem) { - throw new Error('Chat context item not found'); - } + private async _doResolve(provider: vscode.ChatContextProvider, context: IChatContextItem, extItem: vscode.ChatContextItem, token: CancellationToken): Promise { const extResult = await provider.resolveChatContext(extItem, token); const result = extResult ?? context; return { @@ -104,23 +97,69 @@ export class ExtHostChatContext implements ExtHostChatContextShape { }; } - registerChatContextProvider(selector: vscode.DocumentSelector, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { + async $resolveChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise { + const provider = this._getProvider(handle); + + if (!provider.resolveChatContext) { + throw new Error('resolveChatContext not implemented'); + } + const extItem = this._items.get(handle)?.get(context.handle); + if (!extItem) { + throw new Error('Chat context item not found'); + } + return this._doResolve(provider, context, extItem, token); + } + + registerChatContextProvider(selector: vscode.DocumentSelector | undefined, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { const handle = this._handlePool++; - this._providers.set(handle, provider); - this._proxy.$registerChatContextProvider(handle, `${id}`, DocumentSelector.from(selector), {}, { supportsResource: !!provider.provideChatContextForResource, supportsResolve: !!provider.resolveChatContext }); + const disposables = new DisposableStore(); + this._listenForWorkspaceContextChanges(handle, provider, disposables); + this._providers.set(handle, { provider, disposables }); + this._proxy.$registerChatContextProvider(handle, `${id}`, selector ? DocumentSelector.from(selector) : undefined, {}, { supportsResource: !!provider.provideChatContextForResource, supportsResolve: !!provider.resolveChatContext }); return { dispose: () => { this._providers.delete(handle); this._proxy.$unregisterChatContextProvider(handle); + disposables.dispose(); } }; } + private _listenForWorkspaceContextChanges(handle: number, provider: vscode.ChatContextProvider, disposables: DisposableStore): void { + if (!provider.onDidChangeWorkspaceChatContext || !provider.provideWorkspaceChatContext) { + return; + } + disposables.add(provider.onDidChangeWorkspaceChatContext(async () => { + const workspaceContexts = await provider.provideWorkspaceChatContext!(CancellationToken.None); + const resolvedContexts: IChatContextItem[] = []; + for (const item of workspaceContexts ?? []) { + const contextItem: IChatContextItem = { + icon: item.icon, + label: item.label, + modelDescription: item.modelDescription, + value: item.value, + handle: this._itemPool++ + }; + const resolved = await this._doResolve(provider, contextItem, item, CancellationToken.None); + resolvedContexts.push(resolved); + } + + this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts); + })); + } + private _getProvider(handle: number): vscode.ChatContextProvider { if (!this._providers.has(handle)) { throw new Error('Chat context provider not found'); } - return this._providers.get(handle)!; + return this._providers.get(handle)!.provider; + } + + public override dispose(): void { + super.dispose(); + for (const { disposables } of this._providers.values()) { + disposables.dispose(); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts index 09bbab020b0..4b02e2e1f00 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts @@ -130,7 +130,7 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV } protected override renderLabel(element: HTMLElement): IDisposable | null { - const icon = this.contextKeyService.contextMatchesRules(ChatContextKeys.remoteJobCreating) ? Codicon.sync : Codicon.indent; + const icon = this.contextKeyService.contextMatchesRules(ChatContextKeys.remoteJobCreating) ? Codicon.sync : Codicon.forward; element.classList.add(...ThemeIcon.asClassNameArray(icon)); return super.renderLabel(element); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts index 118dd173191..33531fda4c9 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts @@ -11,11 +11,12 @@ import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { URI } from '../../../../../base/common/uri.js'; +import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; -import { ChatSessionStatus, IChatSessionItemProvider, IChatSessionsExtensionPoint, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; +import { ChatSessionStatus, IChatSessionsExtensionPoint, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js'; import { AgentSessionsViewFilter } from './agentSessionsViewFilter.js'; @@ -35,7 +36,7 @@ export interface IAgentSessionsViewModel { export interface IAgentSessionViewModel { - readonly provider: IChatSessionItemProvider; + readonly providerType: string; readonly providerLabel: string; readonly resource: URI; @@ -65,7 +66,7 @@ export interface IAgentSessionViewModel { } export function isLocalAgentSessionItem(session: IAgentSessionViewModel): boolean { - return session.provider.chatSessionType === localChatSessionType; + return session.providerType === localChatSessionType; } export function isAgentSession(obj: IAgentSessionsViewModel | IAgentSessionViewModel): obj is IAgentSessionViewModel { @@ -114,20 +115,25 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions }>(); private readonly filter: AgentSessionsViewFilter; + private readonly cache: AgentSessionsCache; constructor( options: IAgentSessionsViewModelOptions, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IStorageService private readonly storageService: IStorageService, ) { super(); this.filter = this._register(this.instantiationService.createInstance(AgentSessionsViewFilter, { filterMenuId: options.filterMenuId })); - this.registerListeners(); + this.cache = this.instantiationService.createInstance(AgentSessionsCache); + this._sessions = this.cache.loadCachedSessions(); this.resolve(undefined); + + this.registerListeners(); } private registerListeners(): void { @@ -135,6 +141,7 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions this._register(this.chatSessionsService.onDidChangeAvailability(() => this.resolve(undefined))); this._register(this.chatSessionsService.onDidChangeSessionItems(provider => this.resolve(provider))); this._register(this.filter.onDidChange(() => this._onDidChangeSessions.fire())); + this._register(this.storageService.onWillSaveState(() => this.cache.saveCachedSessions(this._sessions))); } async resolve(provider: string | string[] | undefined): Promise { @@ -169,19 +176,16 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions mapSessionContributionToType.set(contribution.type, contribution); } + const resolvedProviders = new Set(); const sessions = new ResourceMap(); for (const provider of this.chatSessionsService.getAllChatSessionItemProviders()) { if (!providersToResolve.includes(undefined) && !providersToResolve.includes(provider.chatSessionType)) { - for (const session of this._sessions) { - if (session.provider.chatSessionType === provider.chatSessionType) { - sessions.set(session.resource, session); - } - } - - continue; // skipped for resolving, preserve existing ones + continue; // skip: not considered for resolving } const providerSessions = await provider.provideChatSessionItems(token); + resolvedProviders.add(provider.chatSessionType); + if (token.isCancellationRequested) { return; } @@ -240,7 +244,7 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions } sessions.set(session.resource, { - provider, + providerType: provider.chatSessionType, providerLabel, resource: session.resource, label: session.label, @@ -260,6 +264,12 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions } } + for (const session of this._sessions) { + if (!resolvedProviders.has(session.providerType)) { + sessions.set(session.resource, session); // fill in existing sessions for providers that did not resolve + } + } + this._sessions.length = 0; this._sessions.push(...sessions.values()); @@ -272,3 +282,111 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions this._onDidChangeSessions.fire(); } } + +//#region Sessions Cache + +interface ISerializedAgentSessionViewModel { + + readonly providerType: string; + readonly providerLabel: string; + + readonly resource: UriComponents; + + readonly icon: string; + + readonly label: string; + + readonly description?: string | IMarkdownString; + readonly tooltip?: string | IMarkdownString; + + readonly status: ChatSessionStatus; + readonly archived: boolean; + + readonly timing: { + readonly startTime: number; + readonly endTime?: number; + }; + + readonly statistics?: { + readonly files: number; + readonly insertions: number; + readonly deletions: number; + }; +} + +class AgentSessionsCache { + + private static readonly STORAGE_KEY = 'agentSessions.cache'; + + constructor(@IStorageService private readonly storageService: IStorageService) { } + + saveCachedSessions(sessions: IAgentSessionViewModel[]): void { + const serialized: ISerializedAgentSessionViewModel[] = sessions + .filter(session => + // Only consider providers that we own where we know that + // we can also invalidate the data after startup + // Other providers are bound to a different lifecycle (extensions) + session.providerType === AgentSessionProviders.Local || + session.providerType === AgentSessionProviders.Background || + session.providerType === AgentSessionProviders.Cloud + ) + .map(session => ({ + providerType: session.providerType, + providerLabel: session.providerLabel, + + resource: session.resource.toJSON(), + + icon: session.icon.id, + label: session.label, + description: session.description, + tooltip: session.tooltip, + + status: session.status, + archived: session.archived, + + timing: { + startTime: session.timing.startTime, + endTime: session.timing.endTime, + }, + + statistics: session.statistics, + })); + this.storageService.store(AgentSessionsCache.STORAGE_KEY, JSON.stringify(serialized), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + + loadCachedSessions(): IAgentSessionViewModel[] { + const sessionsCache = this.storageService.get(AgentSessionsCache.STORAGE_KEY, StorageScope.WORKSPACE); + if (!sessionsCache) { + return []; + } + + try { + const cached = JSON.parse(sessionsCache) as ISerializedAgentSessionViewModel[]; + return cached.map(session => ({ + providerType: session.providerType, + providerLabel: session.providerLabel, + + resource: URI.revive(session.resource), + + icon: ThemeIcon.fromId(session.icon), + label: session.label, + description: session.description, + tooltip: session.tooltip, + + status: session.status, + archived: session.archived, + + timing: { + startTime: session.timing.startTime, + endTime: session.timing.endTime, + }, + + statistics: session.statistics, + })); + } catch { + return []; // invalid data in storage, fallback to empty sessions list + } + } +} + +//#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 2393f73fc9c..95c1e742e25 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -8,15 +8,17 @@ import { localize, localize2 } from '../../../../../nls.js'; import { IAgentSessionViewModel } from './agentSessionViewModel.js'; import { Action, IAction } from '../../../../../base/common/actions.js'; import { ActionViewItem, IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js'; import { EventHelper, h, hide, show } from '../../../../../base/browser/dom.js'; import { assertReturnsDefined } from '../../../../../base/common/types.js'; import { ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { ViewAction } from '../../../../browser/parts/views/viewPane.js'; -import { AGENT_SESSIONS_VIEW_ID } from './agentSessions.js'; +import { AGENT_SESSIONS_VIEW_ID, AgentSessionProviders } from './agentSessions.js'; import { AgentSessionsView } from './agentSessionsView.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IChatService } from '../../common/chatService.js'; //#region Diff Statistics Action @@ -103,10 +105,17 @@ export class AgentSessionDiffActionViewItem extends ActionViewItem { const session = this.action.getSession(); - this.commandService.executeCommand(`agentSession.${session.provider.chatSessionType}.openChanges`, this.action.getSession().resource); + this.commandService.executeCommand(`agentSession.${session.providerType}.openChanges`, this.action.getSession().resource); } } +CommandsRegistry.registerCommand(`agentSession.${AgentSessionProviders.Local}.openChanges`, async (accessor: ServicesAccessor, resource: URI) => { + const chatService = accessor.get(IChatService); + + const session = chatService.getSession(resource); + session?.editingSession?.show(); +}); + //#endregion //#region View Actions diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts index 8194158f99a..766335bd7f4 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts @@ -84,7 +84,9 @@ export class AgentSessionsView extends ViewPane { container.classList.add('agent-sessions-view'); // New Session - this.createNewSessionButton(container); + if (!this.configurationService.getValue('chat.hideNewButtonInAgentSessionsView')) { + this.createNewSessionButton(container); + } // Sessions List this.createList(container); @@ -144,18 +146,22 @@ export class AgentSessionsView extends ViewPane { ...e.editorOptions, }; + await this.chatSessionsService.activateChatSessionItemProvider(session.providerType); // ensure provider is activated before trying to open + const group = e.sideBySide ? SIDE_GROUP : undefined; await this.chatWidgetService.openSession(session.resource, group, options); } - private showContextMenu({ element: session, anchor }: ITreeContextMenuEvent): void { + private async showContextMenu({ element: session, anchor }: ITreeContextMenuEvent): Promise { if (!session) { return; } + const provider = await this.chatSessionsService.activateChatSessionItemProvider(session.providerType); + const menu = this.menuService.createMenu(MenuId.ChatSessionsMenu, this.contextKeyService.createOverlay(getSessionItemContextOverlay( session, - session.provider, + provider, this.chatWidgetService, this.chatService, this.editorGroupsService diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewFilter.ts index 49722b0e60d..f87407f61fc 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewFilter.ts @@ -232,7 +232,7 @@ export class AgentSessionsViewFilter extends Disposable { return true; } - if (this.excludes.providers.includes(session.provider.chatSessionType)) { + if (this.excludes.providers.includes(session.providerType)) { return true; } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 1417702b724..e76068d8825 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -258,7 +258,7 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer { compare(sessionA: IAgentSessionViewModel, sessionB: IAgentSessionViewModel): number { - const aHasEndTime = !!sessionA.timing.endTime; - const bHasEndTime = !!sessionB.timing.endTime; + const aInProgress = sessionA.status === ChatSessionStatus.InProgress; + const bInProgress = sessionB.status === ChatSessionStatus.InProgress; - if (!aHasEndTime && bHasEndTime) { + if (aInProgress && !bInProgress) { return -1; // a (in-progress) comes before b (finished) } - if (aHasEndTime && !bHasEndTime) { + if (!aInProgress && bInProgress) { return 1; // a (finished) comes after b (in-progress) } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 10e41c8d803..a0d69c265d3 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -132,6 +132,9 @@ import { ConfigureToolSets, UserToolSetsContributions } from './tools/toolSetsCo import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler.js'; import { ChatWidgetService } from './chatWidgetService.js'; +const toolReferenceNameEnumValues: string[] = []; +const toolReferenceNameEnumDescriptions: string[] = []; + // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ @@ -269,7 +272,7 @@ configurationRegistry.registerConfiguration({ '**/.vscode/*.json': false, '**/.git/**': false, '**/{package.json,package-lock.json,server.xml,build.rs,web.config,.gitattributes,.env}': false, - '**/*.{csproj,fsproj,vbproj,vcxproj,proj,targets,props}': false, + '**/*.{code-workspace,csproj,fsproj,vbproj,vcxproj,proj,targets,props}': false, }, markdownDescription: nls.localize('chat.tools.autoApprove.edits', "Controls whether edits made by chat are automatically approved. The default is to approve all edits except those made to certain files which have the potential to cause immediate unintended side-effects, such as `**/.vscode/*.json`.\n\nSet to `true` to automatically approve edits to matching files, `false` to always require explicit approval. The last pattern matching a given file will determine whether the edit is automatically approved."), type: 'object', @@ -298,6 +301,10 @@ configurationRegistry.registerConfiguration({ default: {}, markdownDescription: nls.localize('chat.tools.eligibleForAutoApproval', 'Controls which tools are eligible for automatic approval. Tools set to \'false\' will always present a confirmation and will never offer the option to auto-approve. The default behavior (or setting a tool to \'true\') may result in the tool offering auto-approval options.'), type: 'object', + propertyNames: { + enum: toolReferenceNameEnumValues, + enumDescriptions: toolReferenceNameEnumDescriptions, + }, additionalProperties: { type: 'boolean', }, @@ -765,6 +772,12 @@ configurationRegistry.registerConfiguration({ mode: 'auto' } }, + 'chat.hideNewButtonInAgentSessionsView': { // TODO@bpasero remove me eventually + type: 'boolean', + description: nls.localize('chat.hideNewButtonInAgentSessionsView', "Controls whether the new session button is hidden in the Agent Sessions view."), + default: false, + tags: ['preview'] + }, 'chat.signInWithAlternateScopes': { // TODO@bpasero remove me eventually type: 'boolean', description: nls.localize('chat.signInWithAlternateScopes', "Controls whether sign-in with alternate scopes is used."), @@ -915,6 +928,43 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr } } +class ToolReferenceNamesContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.toolReferenceNames'; + + constructor( + @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, + ) { + super(); + this._updateToolReferenceNames(); + this._register(this._languageModelToolsService.onDidChangeTools(() => this._updateToolReferenceNames())); + } + + private _updateToolReferenceNames(): void { + const tools = + Array.from(this._languageModelToolsService.getTools()) + .filter((tool): tool is typeof tool & { toolReferenceName: string } => typeof tool.toolReferenceName === 'string') + .sort((a, b) => a.toolReferenceName.localeCompare(b.toolReferenceName)); + toolReferenceNameEnumValues.length = 0; + toolReferenceNameEnumDescriptions.length = 0; + for (const tool of tools) { + toolReferenceNameEnumValues.push(tool.toolReferenceName); + toolReferenceNameEnumDescriptions.push(nls.localize( + 'chat.toolReferenceName.description', + "{0} - {1}", + tool.toolReferenceName, + tool.userDescription || tool.displayName + )); + } + configurationRegistry.notifyConfigurationSchemaUpdated({ + id: 'chatSidebar', + properties: { + [ChatConfiguration.EligibleForAutoApproval]: {} + } + }); + } +} + AccessibleViewRegistry.register(new ChatTerminalOutputAccessibleView()); AccessibleViewRegistry.register(new ChatResponseAccessibleView()); AccessibleViewRegistry.register(new PanelChatAccessibilityHelp()); @@ -1019,6 +1069,7 @@ registerWorkbenchContribution2(ChatTeardownContribution.ID, ChatTeardownContribu registerWorkbenchContribution2(ChatStatusBarEntry.ID, ChatStatusBarEntry, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(BuiltinToolsContribution.ID, BuiltinToolsContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatAgentSettingContribution.ID, ChatAgentSettingContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(ToolReferenceNamesContribution.ID, ToolReferenceNamesContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingEditorAccessibility.ID, ChatEditingEditorAccessibility, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingEditorOverlay.ID, ChatEditingEditorOverlay, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(SimpleBrowserOverlay.ID, SimpleBrowserOverlay, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index b7972a34222..56f5b0c247f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -11,7 +11,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ResourceLabels } from '../../../../browser/labels.js'; -import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isTerminalVariableEntry, OmittedState } from '../../common/chatVariableEntries.js'; +import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isTerminalVariableEntry, isWorkspaceVariableEntry, OmittedState } from '../../common/chatVariableEntries.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../chatAttachmentWidgets.js'; @@ -153,6 +153,9 @@ export class ChatAttachmentsContentPart extends Disposable { widget = this.instantiationService.createInstance(SCMHistoryItemChangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels); } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { widget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels); + } else if (isWorkspaceVariableEntry(attachment)) { + // skip workspace attachments + return; } else { widget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels); } diff --git a/src/vs/workbench/contrib/chat/browser/chatContextService.ts b/src/vs/workbench/contrib/chat/browser/chatContextService.ts index 7222696e1d8..0d858bf3180 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContextService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContextService.ts @@ -9,7 +9,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { IChatContextPicker, IChatContextPickerItem, IChatContextPickService } from './chatContextPickService.js'; import { IChatContextItem, IChatContextProvider } from '../common/chatContext.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { IGenericChatRequestVariableEntry, StringChatContextValue } from '../common/chatVariableEntries.js'; +import { IChatRequestWorkspaceVariableEntry, IGenericChatRequestVariableEntry, StringChatContextValue } from '../common/chatVariableEntries.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { Disposable, DisposableMap, IDisposable } from '../../../../base/common/lifecycle.js'; @@ -22,7 +22,7 @@ export interface IChatContextService extends ChatContextService { } interface IChatContextProviderEntry { picker?: { title: string; icon: ThemeIcon }; chatContextProvider?: { - selector: LanguageSelector; + selector: LanguageSelector | undefined; provider: IChatContextProvider; }; } @@ -31,6 +31,7 @@ export class ChatContextService extends Disposable { _serviceBrand: undefined; private readonly _providers = new Map(); + private readonly _workspaceContext = new Map(); private readonly _registeredPickers = this._register(new DisposableMap()); private _lastResourceContext: Map = new Map(); @@ -56,7 +57,7 @@ export class ChatContextService extends Disposable { this._registeredPickers.set(id, this._contextPickService.registerChatContextItem(this._asPicker(providerEntry.picker.title, providerEntry.picker.icon, id))); } - registerChatContextProvider(id: string, selector: LanguageSelector, provider: IChatContextProvider): void { + registerChatContextProvider(id: string, selector: LanguageSelector | undefined, provider: IChatContextProvider): void { const providerEntry = this._providers.get(id) ?? { picker: undefined }; providerEntry.chatContextProvider = { selector, provider }; this._providers.set(id, providerEntry); @@ -68,6 +69,29 @@ export class ChatContextService extends Disposable { this._registeredPickers.deleteAndDispose(id); } + updateWorkspaceContextItems(id: string, items: IChatContextItem[]): void { + this._workspaceContext.set(id, items); + } + + getWorkspaceContextItems(): IChatRequestWorkspaceVariableEntry[] { + const items: IChatRequestWorkspaceVariableEntry[] = []; + for (const workspaceContexts of this._workspaceContext.values()) { + for (const item of workspaceContexts) { + if (!item.value) { + continue; + } + items.push({ + value: item.value, + name: item.label, + modelDescription: item.modelDescription, + id: item.label, + kind: 'workspace' + }); + } + } + return items; + } + async contextForResource(uri: URI): Promise { return this._contextForResource(uri, false); } @@ -75,7 +99,7 @@ export class ChatContextService extends Disposable { private async _contextForResource(uri: URI, withValue: boolean): Promise { const scoredProviders: Array<{ score: number; provider: IChatContextProvider }> = []; for (const providerEntry of this._providers.values()) { - if (!providerEntry.chatContextProvider?.provider.provideChatContextForResource) { + if (!providerEntry.chatContextProvider?.provider.provideChatContextForResource || (providerEntry.chatContextProvider.selector === undefined)) { continue; } const matchScore = score(providerEntry.chatContextProvider.selector, uri, '', true, undefined, undefined); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 5f7211875e5..2724953d42b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -105,6 +105,7 @@ import { ChatRelatedFiles } from './contrib/chatInputRelatedFilesContrib.js'; import { resizeImage } from './imageUtils.js'; import { IModelPickerDelegate, ModelPickerActionItem } from './modelPicker/modelPickerActionItem.js'; import { IModePickerDelegate, ModePickerActionItem } from './modelPicker/modePickerActionItem.js'; +import { IChatContextService } from './chatContextService.js'; const $ = dom.$; @@ -179,7 +180,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public getAttachedContext(sessionResource: URI) { const contextArr = new ChatRequestVariableSet(); - contextArr.add(...this.attachmentModel.attachments); + contextArr.add(...this.attachmentModel.attachments, ...this.chatContextService.getWorkspaceContextItems()); return contextArr; } @@ -411,6 +412,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @ILanguageModelToolsService private readonly toolService: ILanguageModelToolsService, @IChatService private readonly chatService: IChatService, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @IChatContextService private readonly chatContextService: IChatContextService, ) { super(); this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts index 8ed7665793a..6b02ef9c999 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts @@ -69,13 +69,22 @@ export function isVendorEntry(entry: IModelItemEntry | IVendorItemEntry): entry return entry.type === 'vendor'; } +export type IViewModelEntry = IModelItemEntry | IVendorItemEntry; + +export interface IViewModelChangeEvent { + at: number; + removed: number; + added: IViewModelEntry[]; +} + export class ChatModelsViewModel extends EditorModel { - private readonly _onDidChangeModelEntries = this._register(new Emitter()); - readonly onDidChangeModelEntries = this._onDidChangeModelEntries.event; + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; private modelEntries: IModelEntry[]; private readonly collapsedVendors = new Set(); + private searchValue: string = ''; constructor( @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @@ -83,14 +92,21 @@ export class ChatModelsViewModel extends EditorModel { ) { super(); this.modelEntries = []; - - this._register(this.chatEntitlementService.onDidChangeEntitlement(async () => { - await this.resolve(); - this._onDidChangeModelEntries.fire(); - })); + this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.refresh())); } - fetch(searchValue: string): (IModelItemEntry | IVendorItemEntry)[] { + private readonly _viewModelEntries: IViewModelEntry[] = []; + get viewModelEntries(): readonly IViewModelEntry[] { + return this._viewModelEntries; + } + private splice(at: number, removed: number, added: IViewModelEntry[]): void { + this._viewModelEntries.splice(at, removed, ...added); + this._onDidChange.fire({ at, removed, added }); + } + + filter(searchValue: string): readonly IViewModelEntry[] { + this.searchValue = searchValue; + let modelEntries = this.modelEntries; const capabilityMatchesMap = new Map(); @@ -135,11 +151,10 @@ export class ChatModelsViewModel extends EditorModel { } searchValue = searchValue.trim(); - if (!searchValue) { - return this.toEntries(modelEntries, capabilityMatchesMap); - } + const filtered = searchValue ? this.filterByText(modelEntries, searchValue, capabilityMatchesMap) : this.toEntries(modelEntries, capabilityMatchesMap); - return this.filterByText(modelEntries, searchValue, capabilityMatchesMap); + this.splice(0, this._viewModelEntries.length, filtered); + return this.viewModelEntries; } private filterByProviders(modelEntries: IModelEntry[], providers: string[]): IModelEntry[] { @@ -264,8 +279,12 @@ export class ChatModelsViewModel extends EditorModel { } override async resolve(): Promise { - this.modelEntries = []; + await this.refresh(); + return super.resolve(); + } + private async refresh(): Promise { + this.modelEntries = []; for (const vendor of this.getVendors()) { const modelIdentifiers = await this.languageModelsService.selectLanguageModels({ vendor: vendor.vendor }, vendor.vendor === 'copilot'); const models = coalesce(modelIdentifiers.map(identifier => { @@ -288,12 +307,24 @@ export class ChatModelsViewModel extends EditorModel { } this.modelEntries = distinct(this.modelEntries, modelEntry => ChatModelsViewModel.getId(modelEntry)); + this.filter(this.searchValue); + } - return super.resolve(); + toggleVisibility(model: IModelItemEntry): void { + const isVisible = model.modelEntry.metadata.isUserSelectable ?? false; + const newVisibility = !isVisible; + this.languageModelsService.updateModelPickerPreference(model.modelEntry.identifier, newVisibility); + const metadata = this.languageModelsService.lookupLanguageModel(model.modelEntry.identifier); + const index = this.viewModelEntries.indexOf(model); + if (metadata) { + model.id = ChatModelsViewModel.getId(model.modelEntry); + model.modelEntry.metadata = metadata; + this.splice(index, 1, [model]); + } } private static getId(modelEntry: IModelEntry): string { - return modelEntry.identifier + modelEntry.vendor + (modelEntry.metadata.version || ''); + return `${modelEntry.identifier}.${modelEntry.metadata.version}-visible:${modelEntry.metadata.isUserSelectable}`; } toggleVendorCollapsed(vendorId: string): void { @@ -302,7 +333,7 @@ export class ChatModelsViewModel extends EditorModel { } else { this.collapsedVendors.add(vendorId); } - this._onDidChangeModelEntries.fire(); + this.filter(this.searchValue); } getConfiguredVendors(): IVendorItemEntry[] { diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index 1e5a3d86f36..db6069540f4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -288,11 +288,8 @@ class GutterColumnRenderer extends ModelsTableColumnRenderer(); readonly onDidToggleCollapse = this._onDidToggleCollapse.event; - private readonly _onDidChange = new Emitter(); - readonly onDidChange = this._onDidChange.event; - constructor( - @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService + private readonly viewModel: ChatModelsViewModel, ) { super(); } @@ -348,11 +345,7 @@ class GutterColumnRenderer extends ModelsTableColumnRenderer { - const newVisibility = !isVisible; - this.languageModelsService.updateModelPickerPreference(modelEntry.identifier, newVisibility); - this._onDidChange.fire(); - } + run: async () => this.viewModel.toggleVisibility(entry) }); templateData.actionBar.push(toggleVisibilityAction, { icon: true, label: false }); } @@ -712,20 +705,13 @@ export class ChatModelsWidget extends Disposable { super(); this.searchFocusContextKey = CONTEXT_MODELS_SEARCH_FOCUS.bindTo(contextKeyService); - this.delayedFiltering = new Delayer(300); + this.delayedFiltering = new Delayer(200); this.viewModel = this._register(this.instantiationService.createInstance(ChatModelsViewModel)); this.element = DOM.$('.models-widget'); this.create(this.element); - const loadingPromise = this.extensionService.whenInstalledExtensionsRegistered().then(async () => { - await this.viewModel.resolve(); - this.refreshTable(); - }); - - // Show progress indicator while loading models + const loadingPromise = this.extensionService.whenInstalledExtensionsRegistered().then(async () => this.viewModel.resolve()); this.editorProgressService.showWhile(loadingPromise, 300); - - this._register(this.viewModel.onDidChangeModelEntries(() => this.refreshTable())); } private create(container: HTMLElement): void { @@ -765,7 +751,6 @@ export class ChatModelsWidget extends Disposable { focusContextKey: this.searchFocusContextKey, }, )); - this._register(this.searchWidget.onInputDidChange(() => this.filterModels())); const filterAction = this._register(new ModelsFilterAction()); const clearSearchAction = this._register(new Action( @@ -781,6 +766,7 @@ export class ChatModelsWidget extends Disposable { this._register(this.searchWidget.onInputDidChange(() => { clearSearchAction.enabled = !!this.searchWidget.getValue(); + this.filterModels(); })); this.searchActionsContainer = DOM.append(searchContainer, $('.models-search-actions')); @@ -821,7 +807,7 @@ export class ChatModelsWidget extends Disposable { this.tableContainer = DOM.append(container, $('.models-table-container')); // Create table - const gutterColumnRenderer = this.instantiationService.createInstance(GutterColumnRenderer); + const gutterColumnRenderer = this.instantiationService.createInstance(GutterColumnRenderer, this.viewModel); const modelNameColumnRenderer = this.instantiationService.createInstance(ModelNameColumnRenderer); const costColumnRenderer = this.instantiationService.createInstance(MultiplierColumnRenderer); const tokenLimitsColumnRenderer = this.instantiationService.createInstance(TokenLimitsColumnRenderer); @@ -832,12 +818,6 @@ export class ChatModelsWidget extends Disposable { this.viewModel.toggleVendorCollapsed(vendorId); })); - this._register(gutterColumnRenderer.onDidChange(e => { - this.viewModel.resolve().then(() => { - this.refreshTable(); - }); - })); - this._register(actionsColumnRenderer.onDidChange(e => { this.viewModel.resolve().then(() => { this.refreshTable(); @@ -960,6 +940,8 @@ export class ChatModelsWidget extends Disposable { } })); + this.table.splice(0, this.table.length, this.viewModel.viewModelEntries); + this._register(this.viewModel.onDidChange(({ at, removed, added }) => this.table.splice(at, removed, added))); } private filterModels(): void { @@ -968,7 +950,7 @@ export class ChatModelsWidget extends Disposable { private async refreshTable(): Promise { const searchValue = this.searchWidget.getValue(); - const modelItems = this.viewModel.fetch(searchValue); + const modelItems = this.viewModel.filter(searchValue); const vendors = this.viewModel.getVendors(); const configuredVendors = new Set(this.viewModel.getConfiguredVendors().map(cv => cv.vendorEntry.vendor)); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts index f40f4aee4da..fceb1312c0b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts @@ -662,7 +662,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ }); } - async hasChatSessionItemProvider(chatViewType: string): Promise { + async activateChatSessionItemProvider(chatViewType: string): Promise { await this._extensionService.whenInstalledExtensionsRegistered(); const resolvedType = this._resolveToPrimaryType(chatViewType); if (resolvedType) { @@ -671,16 +671,16 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ const contribution = this._contributions.get(chatViewType)?.contribution; if (contribution && !this._isContributionAvailable(contribution)) { - return false; + return undefined; } if (this._itemsProviders.has(chatViewType)) { - return true; + return this._itemsProviders.get(chatViewType); } await this._extensionService.activateByEvent(`onChatSession:${chatViewType}`); - return this._itemsProviders.has(chatViewType); + return this._itemsProviders.get(chatViewType); } async canResolveChatSession(chatSessionResource: URI) { @@ -709,7 +709,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } private async getChatSessionItems(chatSessionType: string, token: CancellationToken): Promise { - if (!(await this.hasChatSessionItemProvider(chatSessionType))) { + if (!(await this.activateChatSessionItemProvider(chatSessionType))) { return []; } @@ -790,7 +790,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ request: IChatAgentRequest; metadata?: any; }, token: CancellationToken): Promise { - if (!(await this.hasChatSessionItemProvider(chatSessionType))) { + if (!(await this.activateChatSessionItemProvider(chatSessionType))) { throw Error(`Cannot find provider for ${chatSessionType}`); } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts index c58a714d68b..fe2072b0aab 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts @@ -7,22 +7,19 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; -import { Schemas } from '../../../../../base/common/network.js'; import { IObservable } from '../../../../../base/common/observable.js'; -import { URI } from '../../../../../base/common/uri.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { IChatModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; import { ChatAgentLocation } from '../../common/constants.js'; -import { IChatWidget, IChatWidgetService } from '../chat.js'; +import { IChatWidget, IChatWidgetService, isIChatViewViewContext } from '../chat.js'; import { ChatSessionItemWithProvider } from './common.js'; export class LocalChatSessionsProvider extends Disposable implements IChatSessionItemProvider, IWorkbenchContribution { static readonly ID = 'workbench.contrib.localChatSessionsProvider'; static readonly CHAT_WIDGET_VIEW_ID = 'workbench.panel.chat.view.copilot'; - static readonly CHAT_WIDGET_VIEW_RESOURCE = URI.parse(`${Schemas.vscodeLocalChatSession}://widget`); readonly chatSessionType = localChatSessionType; private readonly _onDidChange = this._register(new Emitter()); @@ -59,8 +56,7 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio this._register(this.chatWidgetService.onDidAddWidget(widget => { // Only fire for chat view instance if (widget.location === ChatAgentLocation.Chat && - typeof widget.viewContext === 'object' && - 'viewId' in widget.viewContext && + isIChatViewViewContext(widget.viewContext) && widget.viewContext.viewId === LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID) { this._onDidChange.fire(); this._registerWidgetModelListeners(widget); @@ -69,7 +65,7 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio // Check for existing chat widgets and register listeners const existingWidgets = this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat) - .filter(widget => typeof widget.viewContext === 'object' && 'viewId' in widget.viewContext && widget.viewContext.viewId === LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID); + .filter(widget => isIChatViewViewContext(widget.viewContext) && widget.viewContext.viewId === LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID); existingWidgets.forEach(widget => { this._registerWidgetModelListeners(widget); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/view/chatSessionsView.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/view/chatSessionsView.ts index 20633da2090..d584f92babe 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/view/chatSessionsView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/view/chatSessionsView.ts @@ -86,7 +86,7 @@ export class ChatSessionsViewContrib extends Disposable implements IWorkbenchCon private async updateViewRegistration(): Promise { // prepare all chat session providers const contributions = this.chatSessionsService.getAllChatSessionContributions(); - await Promise.all(contributions.map(contrib => this.chatSessionsService.hasChatSessionItemProvider(contrib.type))); + await Promise.all(contributions.map(contrib => this.chatSessionsService.activateChatSessionItemProvider(contrib.type))); const currentProviders = this.getAllChatSessionItemProviders(); const currentProviderIds = new Set(currentProviders.map(p => p.chatSessionType)); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts index df76c123334..f4576d6e2e0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts @@ -10,11 +10,9 @@ import { IActionViewItem } from '../../../../../../base/browser/ui/actionbar/act import { IBaseActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { ITreeContextMenuEvent } from '../../../../../../base/browser/ui/tree/tree.js'; import { IAction, toAction } from '../../../../../../base/common/actions.js'; -import { coalesce } from '../../../../../../base/common/arrays.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { FuzzyScore } from '../../../../../../base/common/filters.js'; import { MarshalledId } from '../../../../../../base/common/marshallingIds.js'; -import { isEqual } from '../../../../../../base/common/resources.js'; import { truncate } from '../../../../../../base/common/strings.js'; import { URI } from '../../../../../../base/common/uri.js'; import * as nls from '../../../../../../nls.js'; @@ -304,11 +302,7 @@ export class SessionsViewPane extends ViewPane { const renderer = this.instantiationService.createInstance(SessionsRenderer, this.viewDescriptorService.getViewLocationById(this.viewId)); this._register(renderer); - const getResourceForElement = (element: ChatSessionItemWithProvider): URI | null => { - if (isEqual(element.resource, LocalChatSessionsProvider.CHAT_WIDGET_VIEW_RESOURCE)) { - return null; - } - + const getResourceForElement = (element: ChatSessionItemWithProvider): URI => { return element.resource; }; @@ -324,14 +318,14 @@ export class SessionsViewPane extends ViewPane { onDragStart: (data, originalEvent) => { try { const elements = data.getData() as ChatSessionItemWithProvider[]; - const uris = coalesce(elements.map(getResourceForElement)); + const uris = elements.map(getResourceForElement); this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, uris, originalEvent)); } catch { // noop } }, getDragURI: (element: ChatSessionItemWithProvider) => { - return getResourceForElement(element)?.toString() ?? null; + return getResourceForElement(element).toString(); }, getDragLabel: (elements: ChatSessionItemWithProvider[]) => { if (elements.length === 1) { diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 5ea583316e3..70682b5bfc8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -7,7 +7,7 @@ import { $, getWindow } from '../../../../base/browser/dom.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { MarshalledId } from '../../../../base/common/marshallingIds.js'; -import { autorun } from '../../../../base/common/observable.js'; +import { autorun, IReader } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -229,14 +229,14 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { })); this._widget.render(parent); - const updateWidgetVisibility = () => { - this._widget.setVisible(this.isBodyVisible() && !welcomeController.isShowingWelcome.get()); + const updateWidgetVisibility = (r?: IReader) => { + this._widget.setVisible(this.isBodyVisible() && !welcomeController.isShowingWelcome.read(r)); }; this._register(this.onDidChangeBodyVisibility(() => { updateWidgetVisibility(); })); this._register(autorun(r => { - updateWidgetVisibility(); + updateWidgetVisibility(r); })); const info = this.getTransferredOrPersistedSessionInfo(); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidgetService.ts b/src/vs/workbench/contrib/chat/browser/chatWidgetService.ts index 45b4a7ee5dc..604c9c1b102 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidgetService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidgetService.ts @@ -17,7 +17,6 @@ import { ChatAgentLocation } from '../common/constants.js'; import { ChatViewId, ChatViewPaneTarget, IChatWidget, IChatWidgetService, IQuickChatService, isIChatViewViewContext } from './chat.js'; import { ChatEditor, IChatEditorOptions } from './chatEditor.js'; import { findExistingChatEditorByUri } from './chatSessions/common.js'; -import { LocalChatSessionsProvider } from './chatSessions/localChatSessionsProvider.js'; import { ChatViewPane } from './chatViewPane.js'; export class ChatWidgetService extends Disposable implements IChatWidgetService { @@ -95,15 +94,6 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService openSession(sessionResource: URI, target?: typeof ChatViewPaneTarget): Promise; openSession(sessionResource: URI, target?: PreferredGroup, options?: IChatEditorOptions): Promise; async openSession(sessionResource: URI, target?: typeof ChatViewPaneTarget | PreferredGroup, options?: IChatEditorOptions): Promise { - // TODO remove this, open the real resource - if (isEqual(sessionResource, LocalChatSessionsProvider.CHAT_WIDGET_VIEW_RESOURCE)) { - const chatViewPane = await this.viewsService.openView(ChatViewId, true); - if (chatViewPane) { - chatViewPane.focusInput(); - } - return chatViewPane?.widget; - } - const alreadyOpenWidget = await this.revealSessionIfAlreadyOpen(sessionResource); if (alreadyOpenWidget) { return alreadyOpenWidget; diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 05476177d90..a5a3692b3fe 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -78,7 +78,6 @@ font-size: var(--vscode-chat-font-size-body-s); color: var(--vscode-descriptionForeground); overflow: hidden; - margin-left: 4px; } .interactive-item-container .detail-container .detail .agentOrSlashCommandDetected A { @@ -2494,6 +2493,10 @@ have to be updated for changes to the rules above, or to support more deeply nes display: none; } + .interactive-request .header.partially-disabled .detail-container { + margin-left: 4px; + } + .interactive-item-container .header .detail .codicon-check { margin-right: 7px; vertical-align: middle; diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 56d78746776..44d1bca7014 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -153,13 +153,6 @@ export interface IChatAgentRequest { editedFileEvents?: IChatAgentEditedFileEvent[]; isSubagent?: boolean; - /** - * Summary data for chat sessions context - */ - chatSummary?: { - prompt?: string; - history?: string; - }; } export interface IChatQuestion { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 95befa3816b..a567027daca 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -932,13 +932,6 @@ export interface IChatSendRequestOptions { */ confirmation?: string; - /** - * Summary data for chat sessions context - */ - chatSummary?: { - prompt?: string; - history?: string; - }; } export const IChatService = createDecorator('IChatService'); diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index feb807fed99..9adc2c0bb13 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -404,7 +404,7 @@ export class ChatService extends Disposable implements IChatService { }); } - shouldBeInHistory(entry: Partial) { + private shouldBeInHistory(entry: Partial) { if (entry.sessionResource) { return !entry.isImported && LocalChatSessionUri.parseLocalSessionId(entry.sessionResource) && entry.initialLocation !== ChatAgentLocation.EditorInline; } @@ -908,7 +908,6 @@ export class ChatService extends Disposable implements IChatService { userSelectedTools: options?.userSelectedTools?.get(), modeInstructions: options?.modeInfo?.modeInstructions, editedFileEvents: request.editedFileEvents, - chatSummary: options?.chatSummary }; let isInitialTools = true; @@ -937,6 +936,7 @@ export class ChatService extends Disposable implements IChatService { !commandPart && !agentSlashCommandPart && enableCommandDetection && + location !== ChatAgentLocation.EditorInline && options?.modeInfo?.kind !== ChatModeKind.Agent && options?.modeInfo?.kind !== ChatModeKind.Edit && !options?.agentIdSilent diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index d2bd31b0229..5a13c99a58e 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -155,7 +155,7 @@ export interface IChatSessionsService { readonly onDidChangeInProgress: Event; registerChatSessionItemProvider(provider: IChatSessionItemProvider): IDisposable; - hasChatSessionItemProvider(chatSessionType: string): Promise; + activateChatSessionItemProvider(chatSessionType: string): Promise; getAllChatSessionItemProviders(): IChatSessionItemProvider[]; getAllChatSessionContributions(): IChatSessionsExtensionPoint[]; diff --git a/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts index 95170b9f6d7..68f198d1158 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts @@ -90,6 +90,13 @@ export interface IChatRequestStringVariableEntry extends IBaseChatRequestVariabl readonly uri: URI; } +export interface IChatRequestWorkspaceVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'workspace'; + readonly value: string; + readonly modelDescription?: string; +} + + export interface IChatRequestPasteVariableEntry extends IBaseChatRequestVariableEntry { readonly kind: 'paste'; readonly code: string; @@ -260,7 +267,7 @@ export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChat | IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry | IPromptFileVariableEntry | IPromptTextVariableEntry | ISCMHistoryItemVariableEntry | ISCMHistoryItemChangeVariableEntry | ISCMHistoryItemChangeRangeVariableEntry | ITerminalVariableEntry - | IChatRequestStringVariableEntry; + | IChatRequestStringVariableEntry | IChatRequestWorkspaceVariableEntry; export namespace IChatRequestVariableEntry { @@ -293,6 +300,10 @@ export function isPasteVariableEntry(obj: IChatRequestVariableEntry): obj is ICh return obj.kind === 'paste'; } +export function isWorkspaceVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestWorkspaceVariableEntry { + return obj.kind === 'workspace'; +} + export function isImageVariableEntry(obj: IChatRequestVariableEntry): obj is IImageVariableEntry { return obj.kind === 'image'; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 3dcdf132686..361e887a529 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -16,7 +16,7 @@ import { ChatModeKind } from '../../constants.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; import { ILanguageModelToolsService } from '../../languageModelToolsService.js'; import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; -import { GithubPromptHeaderAttributes, IArrayValue, IHeaderAttribute, IStringValue, ParsedPromptFile, PROMPT_NAME_REGEXP, PromptHeaderAttributes, Target } from '../promptFileParser.js'; +import { GithubPromptHeaderAttributes, IArrayValue, IHeaderAttribute, IStringValue, ParsedPromptFile, PromptHeaderAttributes, Target } from '../promptFileParser.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; @@ -197,9 +197,6 @@ export class PromptValidator { report(toMarker(localize('promptValidator.nameShouldNotBeEmpty', "The 'name' attribute must not be empty."), nameAttribute.value.range, MarkerSeverity.Error)); return; } - if (!PROMPT_NAME_REGEXP.test(nameAttribute.value.value)) { - report(toMarker(localize('promptValidator.nameInvalidCharacters', "The 'name' attribute can only consist of letters, digits, underscores, hyphens, and periods."), nameAttribute.value.range, MarkerSeverity.Error)); - } } private validateDescription(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): void { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index a4ed0889775..d42e2ca1824 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -10,8 +10,6 @@ import { URI } from '../../../../../base/common/uri.js'; import { parse, YamlNode, YamlParseError, Position as YamlPosition } from '../../../../../base/common/yaml.js'; import { Range } from '../../../../../editor/common/core/range.js'; -export const PROMPT_NAME_REGEXP = /^[\p{L}\d_\-\.]+$/u; - export class PromptFileParser { constructor() { } @@ -162,11 +160,7 @@ export class PromptHeader { } public get name(): string | undefined { - const name = this.getStringAttribute(PromptHeaderAttributes.name); - if (name && PROMPT_NAME_REGEXP.test(name)) { - return name; - } - return undefined; + return this.getStringAttribute(PromptHeaderAttributes.name); } public get description(): string | undefined { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index dfef81722c3..6db1a4593ab 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -246,8 +246,10 @@ export class PromptsService extends Disposable implements IPromptsService { } private asChatPromptSlashCommand(parsedPromptFile: ParsedPromptFile, promptPath: IPromptPath): IChatPromptSlashCommand { + let name = parsedPromptFile?.header?.name ?? promptPath.name ?? getCleanPromptName(promptPath.uri); + name = name.replace(/[^\p{L}\d_\-\.]+/gu, '-'); // replace spaces with dashes return { - name: parsedPromptFile?.header?.name ?? promptPath.name ?? getCleanPromptName(promptPath.uri), + name: name, description: parsedPromptFile?.header?.description ?? promptPath.description, argumentHint: parsedPromptFile?.header?.argumentHint, parsedPromptFile, @@ -359,7 +361,7 @@ export class PromptsService extends Disposable implements IPromptsService { bucket.set(uri, entryPromise); const flushCachesIfRequired = () => { - this.cachedFileLocations[PromptsType.agent] = undefined; + this.cachedFileLocations[type] = undefined; switch (type) { case PromptsType.agent: this.cachedCustomAgents.refresh(); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts index 546036f8209..f89cc801097 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts @@ -419,8 +419,7 @@ suite('AgentSessionsViewModel', () => { await viewModel.resolve(undefined); assert.strictEqual(viewModel.sessions.length, 1); - assert.strictEqual(viewModel.sessions[0].provider, provider); - assert.strictEqual(viewModel.sessions[0].provider.chatSessionType, 'test-type'); + assert.strictEqual(viewModel.sessions[0].providerType, 'test-type'); }); }); @@ -536,7 +535,7 @@ suite('AgentSessionsViewModel', () => { await viewModel.resolve(undefined); assert.strictEqual(viewModel.sessions.length, 1); - assert.strictEqual(viewModel.sessions[0].provider.chatSessionType, localChatSessionType); + assert.strictEqual(viewModel.sessions[0].providerType, localChatSessionType); }); }); @@ -725,11 +724,7 @@ suite('AgentSessionsViewModel - Helper Functions', () => { test('isLocalAgentSessionItem should identify local sessions', () => { const localSession: IAgentSessionViewModel = { - provider: { - chatSessionType: localChatSessionType, - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [] - }, + providerType: localChatSessionType, providerLabel: 'Local', icon: Codicon.chatSparkle, resource: URI.parse('test://local-1'), @@ -741,12 +736,8 @@ suite('AgentSessionsViewModel - Helper Functions', () => { }; const remoteSession: IAgentSessionViewModel = { - provider: { - chatSessionType: 'remote', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [] - }, - providerLabel: 'Local', + providerType: 'remote', + providerLabel: 'Remote', icon: Codicon.chatSparkle, resource: URI.parse('test://remote-1'), label: 'Remote', @@ -762,11 +753,7 @@ suite('AgentSessionsViewModel - Helper Functions', () => { test('isAgentSession should identify session view models', () => { const session: IAgentSessionViewModel = { - provider: { - chatSessionType: 'test', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [] - }, + providerType: 'test', providerLabel: 'Local', icon: Codicon.chatSparkle, resource: URI.parse('test://test-1'), @@ -787,11 +774,7 @@ suite('AgentSessionsViewModel - Helper Functions', () => { test('isAgentSessionsViewModel should identify sessions view models', () => { const session: IAgentSessionViewModel = { - provider: { - chatSessionType: 'test', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [] - }, + providerType: 'test', providerLabel: 'Local', icon: Codicon.chatSparkle, resource: URI.parse('test://test-1'), @@ -855,7 +838,7 @@ suite('AgentSessionsViewFilter', () => { }; const session1: IAgentSessionViewModel = { - provider: provider1, + providerType: provider1.chatSessionType, providerLabel: 'Provider 1', icon: Codicon.chatSparkle, resource: URI.parse('test://session-1'), @@ -866,7 +849,7 @@ suite('AgentSessionsViewFilter', () => { }; const session2: IAgentSessionViewModel = { - provider: provider2, + providerType: provider2.chatSessionType, providerLabel: 'Provider 2', icon: Codicon.chatSparkle, resource: URI.parse('test://session-2'), @@ -907,7 +890,7 @@ suite('AgentSessionsViewFilter', () => { }; const archivedSession: IAgentSessionViewModel = { - provider, + providerType: provider.chatSessionType, providerLabel: 'Test Provider', icon: Codicon.chatSparkle, resource: URI.parse('test://archived-session'), @@ -918,7 +901,7 @@ suite('AgentSessionsViewFilter', () => { }; const activeSession: IAgentSessionViewModel = { - provider, + providerType: provider.chatSessionType, providerLabel: 'Test Provider', icon: Codicon.chatSparkle, resource: URI.parse('test://active-session'), @@ -959,7 +942,7 @@ suite('AgentSessionsViewFilter', () => { }; const failedSession: IAgentSessionViewModel = { - provider, + providerType: provider.chatSessionType, providerLabel: 'Test Provider', icon: Codicon.chatSparkle, resource: URI.parse('test://failed-session'), @@ -970,7 +953,7 @@ suite('AgentSessionsViewFilter', () => { }; const completedSession: IAgentSessionViewModel = { - provider, + providerType: provider.chatSessionType, providerLabel: 'Test Provider', icon: Codicon.chatSparkle, resource: URI.parse('test://completed-session'), @@ -981,7 +964,7 @@ suite('AgentSessionsViewFilter', () => { }; const inProgressSession: IAgentSessionViewModel = { - provider, + providerType: provider.chatSessionType, providerLabel: 'Test Provider', icon: Codicon.chatSparkle, resource: URI.parse('test://inprogress-session'), diff --git a/src/vs/workbench/contrib/chat/test/browser/chatModelsViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatModelsViewModel.test.ts index 0cbc89277a0..5fd56458d59 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatModelsViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatModelsViewModel.test.ts @@ -250,7 +250,7 @@ suite('ChatModelsViewModel', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('should fetch all models without filters', () => { - const results = viewModel.fetch(''); + const results = viewModel.filter(''); // Should have 2 vendor entries and 4 model entries (grouped by vendor) assert.strictEqual(results.length, 6); @@ -263,7 +263,7 @@ suite('ChatModelsViewModel', () => { }); test('should filter by provider name', () => { - const results = viewModel.fetch('@provider:copilot'); + const results = viewModel.filter('@provider:copilot'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 2); @@ -271,7 +271,7 @@ suite('ChatModelsViewModel', () => { }); test('should filter by provider display name', () => { - const results = viewModel.fetch('@provider:OpenAI'); + const results = viewModel.filter('@provider:OpenAI'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 2); @@ -279,14 +279,14 @@ suite('ChatModelsViewModel', () => { }); test('should filter by multiple providers with OR logic', () => { - const results = viewModel.fetch('@provider:copilot @provider:openai'); + const results = viewModel.filter('@provider:copilot @provider:openai'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 4); }); test('should filter by single capability - tools', () => { - const results = viewModel.fetch('@capability:tools'); + const results = viewModel.filter('@capability:tools'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 3); @@ -294,7 +294,7 @@ suite('ChatModelsViewModel', () => { }); test('should filter by single capability - vision', () => { - const results = viewModel.fetch('@capability:vision'); + const results = viewModel.filter('@capability:vision'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 3); @@ -302,7 +302,7 @@ suite('ChatModelsViewModel', () => { }); test('should filter by single capability - agent', () => { - const results = viewModel.fetch('@capability:agent'); + const results = viewModel.filter('@capability:agent'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 1); @@ -310,7 +310,7 @@ suite('ChatModelsViewModel', () => { }); test('should filter by multiple capabilities with AND logic', () => { - const results = viewModel.fetch('@capability:tools @capability:vision'); + const results = viewModel.filter('@capability:tools @capability:vision'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; // Should only return models that have BOTH tools and vision @@ -322,7 +322,7 @@ suite('ChatModelsViewModel', () => { }); test('should filter by three capabilities with AND logic', () => { - const results = viewModel.fetch('@capability:tools @capability:vision @capability:agent'); + const results = viewModel.filter('@capability:tools @capability:vision @capability:agent'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; // Should only return gpt-4o which has all three @@ -331,7 +331,7 @@ suite('ChatModelsViewModel', () => { }); test('should return no results when filtering by incompatible capabilities', () => { - const results = viewModel.fetch('@capability:vision @capability:agent'); + const results = viewModel.filter('@capability:vision @capability:agent'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; // Only gpt-4o has both vision and agent, but gpt-4-vision doesn't have agent @@ -340,7 +340,7 @@ suite('ChatModelsViewModel', () => { }); test('should filter by visibility - visible:true', () => { - const results = viewModel.fetch('@visible:true'); + const results = viewModel.filter('@visible:true'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 3); @@ -348,7 +348,7 @@ suite('ChatModelsViewModel', () => { }); test('should filter by visibility - visible:false', () => { - const results = viewModel.fetch('@visible:false'); + const results = viewModel.filter('@visible:false'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 1); @@ -356,7 +356,7 @@ suite('ChatModelsViewModel', () => { }); test('should combine provider and capability filters', () => { - const results = viewModel.fetch('@provider:copilot @capability:vision'); + const results = viewModel.filter('@provider:copilot @capability:vision'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 2); @@ -367,7 +367,7 @@ suite('ChatModelsViewModel', () => { }); test('should combine provider, capability, and visibility filters', () => { - const results = viewModel.fetch('@provider:openai @capability:vision @visible:false'); + const results = viewModel.filter('@provider:openai @capability:vision @visible:false'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 1); @@ -375,7 +375,7 @@ suite('ChatModelsViewModel', () => { }); test('should filter by text matching model name', () => { - const results = viewModel.fetch('GPT-4o'); + const results = viewModel.filter('GPT-4o'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 1); @@ -384,7 +384,7 @@ suite('ChatModelsViewModel', () => { }); test('should filter by text matching vendor name', () => { - const results = viewModel.fetch('GitHub'); + const results = viewModel.filter('GitHub'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.strictEqual(models.length, 2); @@ -392,7 +392,7 @@ suite('ChatModelsViewModel', () => { }); test('should combine text search with capability filter', () => { - const results = viewModel.fetch('@capability:tools GPT'); + const results = viewModel.filter('@capability:tools GPT'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; // Should match all models with tools capability and 'GPT' in name @@ -401,21 +401,21 @@ suite('ChatModelsViewModel', () => { }); test('should handle empty search value', () => { - const results = viewModel.fetch(''); + const results = viewModel.filter(''); // Should return all models grouped by vendor assert.ok(results.length > 0); }); test('should handle search value with only whitespace', () => { - const results = viewModel.fetch(' '); + const results = viewModel.filter(' '); // Should return all models grouped by vendor assert.ok(results.length > 0); }); test('should match capability text in free text search', () => { - const results = viewModel.fetch('vision'); + const results = viewModel.filter('vision'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; // Should match models that have vision capability or "vision" in their name @@ -429,7 +429,7 @@ suite('ChatModelsViewModel', () => { test('should toggle vendor collapsed state', () => { viewModel.toggleVendorCollapsed('copilot'); - const results = viewModel.fetch(''); + const results = viewModel.filter(''); const copilotVendor = results.find(r => isVendorEntry(r) && (r as IVendorItemEntry).vendorEntry.vendor === 'copilot') as IVendorItemEntry; assert.ok(copilotVendor); @@ -443,7 +443,7 @@ suite('ChatModelsViewModel', () => { // Toggle back viewModel.toggleVendorCollapsed('copilot'); - const resultsAfterExpand = viewModel.fetch(''); + const resultsAfterExpand = viewModel.filter(''); const copilotModelsAfterExpand = resultsAfterExpand.filter(r => !isVendorEntry(r) && (r as IModelItemEntry).modelEntry.vendor === 'copilot' ); @@ -452,7 +452,7 @@ suite('ChatModelsViewModel', () => { test('should fire onDidChangeModelEntries when entitlement changes', async () => { let fired = false; - store.add(viewModel.onDidChangeModelEntries(() => { + store.add(viewModel.onDidChange(() => { fired = true; })); @@ -468,7 +468,7 @@ suite('ChatModelsViewModel', () => { // When a search string is fully quoted (starts and ends with quotes), // the completeMatch flag is set to true, which currently skips all matching // This test verifies the quotes are processed without errors - const results = viewModel.fetch('"GPT"'); + const results = viewModel.filter('"GPT"'); // The function should complete without error // Note: complete match logic (both quotes) currently doesn't perform matching @@ -476,7 +476,7 @@ suite('ChatModelsViewModel', () => { }); test('should remove filter keywords from text search', () => { - const results = viewModel.fetch('@provider:copilot @capability:vision GPT'); + const results = viewModel.filter('@provider:copilot @capability:vision GPT'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; // Should only search 'GPT' in model names, not the filter keywords @@ -485,9 +485,9 @@ suite('ChatModelsViewModel', () => { }); test('should handle case-insensitive capability matching', () => { - const results1 = viewModel.fetch('@capability:TOOLS'); - const results2 = viewModel.fetch('@capability:tools'); - const results3 = viewModel.fetch('@capability:Tools'); + const results1 = viewModel.filter('@capability:TOOLS'); + const results2 = viewModel.filter('@capability:tools'); + const results3 = viewModel.filter('@capability:Tools'); const models1 = results1.filter(r => !isVendorEntry(r)); const models2 = results2.filter(r => !isVendorEntry(r)); @@ -498,8 +498,8 @@ suite('ChatModelsViewModel', () => { }); test('should support toolcalling alias for tools capability', () => { - const resultsTools = viewModel.fetch('@capability:tools'); - const resultsToolCalling = viewModel.fetch('@capability:toolcalling'); + const resultsTools = viewModel.filter('@capability:tools'); + const resultsToolCalling = viewModel.filter('@capability:toolcalling'); const modelsTools = resultsTools.filter(r => !isVendorEntry(r)); const modelsToolCalling = resultsToolCalling.filter(r => !isVendorEntry(r)); @@ -508,8 +508,8 @@ suite('ChatModelsViewModel', () => { }); test('should support agentmode alias for agent capability', () => { - const resultsAgent = viewModel.fetch('@capability:agent'); - const resultsAgentMode = viewModel.fetch('@capability:agentmode'); + const resultsAgent = viewModel.filter('@capability:agent'); + const resultsAgentMode = viewModel.filter('@capability:agentmode'); const modelsAgent = resultsAgent.filter(r => !isVendorEntry(r)); const modelsAgentMode = resultsAgentMode.filter(r => !isVendorEntry(r)); @@ -518,7 +518,7 @@ suite('ChatModelsViewModel', () => { }); test('should include matched capabilities in results', () => { - const results = viewModel.fetch('@capability:tools @capability:vision'); + const results = viewModel.filter('@capability:tools @capability:vision'); const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; assert.ok(models.length > 0); @@ -587,7 +587,7 @@ suite('ChatModelsViewModel', () => { const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(store, chatEntitlementService); await singleVendorViewModel.resolve(); - const results = singleVendorViewModel.fetch(''); + const results = singleVendorViewModel.filter(''); // Should have only model entries, no vendor entry const vendors = results.filter(isVendorEntry); @@ -600,7 +600,7 @@ suite('ChatModelsViewModel', () => { test('should show vendor headers when multiple vendors exist', () => { // This is the existing behavior test - const results = viewModel.fetch(''); + const results = viewModel.filter(''); // Should have 2 vendor entries and 4 model entries (grouped by vendor) const vendors = results.filter(isVendorEntry); @@ -617,7 +617,7 @@ suite('ChatModelsViewModel', () => { // Try to collapse the single vendor singleVendorViewModel.toggleVendorCollapsed('copilot'); - const results = singleVendorViewModel.fetch(''); + const results = singleVendorViewModel.filter(''); // Should still show models even though vendor is "collapsed" // because there's no vendor header to collapse @@ -632,7 +632,7 @@ suite('ChatModelsViewModel', () => { const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(store, chatEntitlementService); await singleVendorViewModel.resolve(); - const results = singleVendorViewModel.fetch('@capability:agent'); + const results = singleVendorViewModel.filter('@capability:agent'); // Should not show vendor header const vendors = results.filter(isVendorEntry); @@ -645,7 +645,7 @@ suite('ChatModelsViewModel', () => { }); test('should always place copilot vendor at the top', () => { - const results = viewModel.fetch(''); + const results = viewModel.filter(''); const vendors = results.filter(isVendorEntry) as IVendorItemEntry[]; assert.ok(vendors.length >= 2); @@ -708,7 +708,7 @@ suite('ChatModelsViewModel', () => { await viewModel.resolve(); - const results = viewModel.fetch(''); + const results = viewModel.filter(''); const vendors = results.filter(isVendorEntry) as IVendorItemEntry[]; // Should have 4 vendors: copilot, openai, anthropic, azure @@ -725,7 +725,7 @@ suite('ChatModelsViewModel', () => { test('should keep copilot at top even with text search', () => { // Even when searching, if results include multiple vendors, copilot should be first - const results = viewModel.fetch('GPT'); + const results = viewModel.filter('GPT'); const vendors = results.filter(isVendorEntry) as IVendorItemEntry[]; @@ -739,7 +739,7 @@ suite('ChatModelsViewModel', () => { }); test('should keep copilot at top when filtering by capability', () => { - const results = viewModel.fetch('@capability:tools'); + const results = viewModel.filter('@capability:tools'); const vendors = results.filter(isVendorEntry) as IVendorItemEntry[]; diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts index f26c1634353..6f3d3f3ac1d 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts @@ -472,27 +472,11 @@ suite('PromptValidator', () => { assert.strictEqual(markers[0].message, `The 'name' attribute must be a string.`); } - // Invalid characters in name - { - const content = [ - '---', - 'name: "My@Agent!"', - 'description: "Test agent"', - 'target: vscode', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Error); - assert.strictEqual(markers[0].message, `The 'name' attribute can only consist of letters, digits, underscores, hyphens, and periods.`); - } - // Valid name with allowed characters { const content = [ '---', - 'name: "My_Agent-2.0"', + 'name: "My_Agent-2.0 with spaces"', 'description: "Test agent"', 'target: vscode', '---', @@ -632,22 +616,6 @@ suite('PromptValidator', () => { assert.strictEqual(markers[0].severity, MarkerSeverity.Error); assert.strictEqual(markers[0].message, `The 'name' attribute must not be empty.`); } - - // Invalid characters in name - { - const content = [ - '---', - 'name: "My Instructions#"', - 'description: "Test instructions"', - 'applyTo: "**/*.ts"', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.instructions); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Error); - assert.strictEqual(markers[0].message, `The 'name' attribute can only consist of letters, digits, underscores, hyphens, and periods.`); - } }); }); @@ -786,21 +754,6 @@ suite('PromptValidator', () => { assert.strictEqual(markers[0].severity, MarkerSeverity.Error); assert.strictEqual(markers[0].message, `The 'name' attribute must not be empty.`); } - - // Invalid characters in name - { - const content = [ - '---', - 'name: "My Prompt!"', - 'description: "Test prompt"', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.prompt); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Error); - assert.strictEqual(markers[0].message, `The 'name' attribute can only consist of letters, digits, underscores, hyphens, and periods.`); - } }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index fa3ad78161a..d63564825b1 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -73,8 +73,8 @@ export class MockChatSessionsService implements IChatSessionsService { this.contributions = contributions; } - async hasChatSessionItemProvider(chatSessionType: string): Promise { - return this.sessionItemProviders.has(chatSessionType); + async activateChatSessionItemProvider(chatSessionType: string): Promise { + return this.sessionItemProviders.get(chatSessionType); } getAllChatSessionItemProviders(): IChatSessionItemProvider[] { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 3d69c2043b7..43dc08a7a52 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -543,15 +543,15 @@ export class InlineChatEscapeToolContribution extends Disposable { let result: { confirmed: boolean; checkboxChecked?: boolean }; if (dontAskAgain !== undefined) { - // Use previously stored user preference: true = 'Continue in Chat', false = 'Rephrase' (Cancel) + // Use previously stored user preference: true = 'Continue in Chat view', false = 'Rephrase' (Cancel) result = { confirmed: dontAskAgain, checkboxChecked: false }; } else { result = await dialogService.confirm({ type: 'question', - title: localize('confirm.title', "Continue in Panel Chat?"), - message: localize('confirm', "Do you want to continue in panel chat or rephrase your prompt?"), - detail: localize('confirm.detail', "Inline Chat is designed for single file code changes. This task is either too complex or requires a text response. You can rephrase your prompt or continue in panel chat."), - primaryButton: localize('confirm.yes', "Continue in Chat"), + title: localize('confirm.title', "Do you want to continue in Chat view?"), + message: localize('confirm', "Do you want to continue in Chat view?"), + detail: localize('confirm.detail', "Inline chat is designed for making single-file code changes. Continue your request in the Chat view or rephrase it for inline chat."), + primaryButton: localize('confirm.yes', "Continue in Chat view"), cancelButton: localize('confirm.cancel', "Cancel"), checkbox: { label: localize('chat.remove.confirmation.checkbox', "Don't ask again"), checked: false }, }); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 85dc306faf5..af92bb099a8 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -85,6 +85,7 @@ import { CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatConfigKeys, InlineChatResponse import { TestWorkerService } from './testWorkerService.js'; import { URI } from '../../../../../base/common/uri.js'; import { ChatWidgetService } from '../../../chat/browser/chatWidgetService.js'; +import { ChatContextService, IChatContextService } from '../../../chat/browser/chatContextService.js'; suite('InlineChatController', function () { @@ -256,6 +257,8 @@ suite('InlineChatController', function () { model.setEOL(EndOfLineSequence.LF); editor = store.add(instantiateTestCodeEditor(instaService, model)); + instaService.set(IChatContextService, store.add(instaService.createInstance(ChatContextService))); + store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, { async invoke(request, progress, history, token) { progress([{ diff --git a/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts b/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts index 80676ac2b56..9c750e37600 100644 --- a/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts +++ b/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts @@ -7,6 +7,7 @@ import { inputLatency } from '../../../../base/browser/performance.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { Event } from '../../../../base/common/event.js'; import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -16,6 +17,7 @@ export class InputLatencyContrib extends Disposable implements IWorkbenchContrib private readonly _scheduler: RunOnceScheduler; constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, @IEditorService private readonly _editorService: IEditorService, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { @@ -64,16 +66,20 @@ export class InputLatencyContrib extends Disposable implements IWorkbenchContrib render: InputLatencyStatisticFragment; total: InputLatencyStatisticFragment; sampleCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The number of samples measured.' }; + gpuAcceleration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether GPU acceleration was enabled at the time the event was reported.' }; }; - type PerformanceInputLatencyEvent = inputLatency.IInputLatencyMeasurements; + type PerformanceInputLatencyEvent = inputLatency.IInputLatencyMeasurements & { + gpuAcceleration: boolean; + }; this._telemetryService.publicLog2('performance.inputLatency', { keydown: measurements.keydown, input: measurements.input, render: measurements.render, total: measurements.total, - sampleCount: measurements.sampleCount + sampleCount: measurements.sampleCount, + gpuAcceleration: this._configurationService.getValue('editor.experimentalGpuAcceleration') === 'on' }); } } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 57949338fff..a67ececff33 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -344,7 +344,18 @@ registerAction2(class extends Action2 { } else { title = getHistoryItemEditorTitle(historyItem); historyItemId = historyItem.id; - historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : undefined; + + if (historyItem.parentIds.length > 0) { + // History item right above the incoming changes history item + if (historyItem.parentIds[0] === SCMIncomingHistoryItemId && historyItemRemoteRef) { + historyItemParentId = await historyProvider.resolveHistoryItemRefsCommonAncestor([ + historyItemRef.name, + historyItemRemoteRef.name + ]); + } else { + historyItemParentId = historyItem.parentIds[0]; + } + } } if (!title || !historyItemId || !historyItemParentId) { @@ -938,7 +949,24 @@ class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource 0 ? historyItem.parentIds[0] : undefined; + + if (historyItem.parentIds.length > 0) { + // History item right above the incoming changes history item + if (historyItem.parentIds[0] === SCMIncomingHistoryItemId) { + const historyItemRef = historyProvider?.historyItemRef.get(); + const historyItemRemoteRef = historyProvider?.historyItemRemoteRef.get(); + + if (!historyProvider || !historyItemRef || !historyItemRemoteRef) { + return []; + } + + historyItemParentId = await historyProvider.resolveHistoryItemRefsCommonAncestor([ + historyItemRef.name, + historyItemRemoteRef.name]); + } else { + historyItemParentId = historyItem.parentIds[0]; + } + } } const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItemId, historyItemParentId) ?? []; diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts index 7fead618873..d168d958d50 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -531,9 +531,6 @@ export class SCMRepositoriesViewPane extends ViewPane { getWidgetAriaLabel() { return localize('scm', "Source Control Repositories"); } - }, - twistieAdditionalCssClass: (e: unknown) => { - return isSCMRepository(e) ? 'force-twistie' : undefined; } } ) as WorkbenchCompressibleAsyncDataTree; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 9c755c94fb2..f78b971edad 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2405,9 +2405,7 @@ export class SCMViewPane extends ViewPane { }, accessibilityProvider: this.instantiationService.createInstance(SCMAccessibilityProvider), twistieAdditionalCssClass: (e: unknown) => { - if (isSCMRepository(e)) { - return 'force-twistie'; - } else if (isSCMActionButton(e) || isSCMInput(e)) { + if (isSCMActionButton(e) || isSCMInput(e)) { return 'force-no-twistie'; } diff --git a/src/vs/workbench/services/accounts/common/defaultAccount.ts b/src/vs/workbench/services/accounts/common/defaultAccount.ts index 91f17367c07..d5c6f16f5c1 100644 --- a/src/vs/workbench/services/accounts/common/defaultAccount.ts +++ b/src/vs/workbench/services/accounts/common/defaultAccount.ts @@ -163,7 +163,7 @@ export class DefaultAccountManagementContribution extends Disposable implements return; } - this.registerSignInAction(defaultAccountProviderId, this.productService.defaultAccount.authenticationProvider.scopes); + this.registerSignInAction(defaultAccountProviderId, this.productService.defaultAccount.authenticationProvider.scopes[0]); this.setDefaultAccount(await this.getDefaultAccountFromAuthenticatedSessions(defaultAccountProviderId, this.productService.defaultAccount.authenticationProvider.scopes)); this._register(this.authenticationService.onDidChangeSessions(async e => { @@ -205,11 +205,10 @@ export class DefaultAccountManagementContribution extends Disposable implements return result; } - private async getDefaultAccountFromAuthenticatedSessions(authProviderId: string, scopes: string[]): Promise { + private async getDefaultAccountFromAuthenticatedSessions(authProviderId: string, scopes: string[][]): Promise { try { this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authProviderId); - const sessions = await this.authenticationService.getSessions(authProviderId, undefined, undefined, true); - const session = sessions.find(s => this.scopesMatch(s.scopes, scopes)); + const session = await this.findMatchingProviderSession(authProviderId, scopes); if (!session) { this.logService.debug('[DefaultAccount] No matching session found for provider:', authProviderId); @@ -239,6 +238,18 @@ export class DefaultAccountManagementContribution extends Disposable implements } } + private async findMatchingProviderSession(authProviderId: string, allScopes: string[][]): Promise { + const sessions = await this.authenticationService.getSessions(authProviderId, undefined, undefined, true); + for (const session of sessions) { + for (const scopes of allScopes) { + if (this.scopesMatch(session.scopes, scopes)) { + return session; + } + } + } + return undefined; + } + private scopesMatch(scopes: ReadonlyArray, expectedScopes: string[]): boolean { return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope)); } diff --git a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts index 38ea26573a3..173c5dd11f8 100644 --- a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts @@ -10,28 +10,61 @@ declare module 'vscode' { export namespace chat { - // TODO@alexr00 API: - // selector is confusing - export function registerChatContextProvider(selector: DocumentSelector, id: string, provider: ChatContextProvider): Disposable; + /** + * Register a chat context provider. Chat context can be provided: + * - For a resource. Make sure to pass a selector that matches the resource you want to provide context for. + * Providers registered without a selector will not be called for resource-based context. + * - Explicitly. These context items are shown as options when the user explicitly attaches context. + * + * To ensure your extension is activated when chat context is requested, make sure to include the `onChatContextProvider:` activation event in your `package.json`. + * + * @param selector Optional document selector to filter which resources the provider is called for. If omitted, the provider will only be called for explicit context requests. + * @param id Unique identifier for the provider. + * @param provider The chat context provider. + */ + export function registerChatContextProvider(selector: DocumentSelector | undefined, id: string, provider: ChatContextProvider): Disposable; } export interface ChatContextItem { + /** + * Icon for the context item. + */ icon: ThemeIcon; + /** + * Human readable label for the context item. + */ label: string; + /** + * An optional description of the context item, e.g. to describe the item to the language model. + */ modelDescription?: string; + /** + * The value of the context item. Can be omitted when returned from one of the `provide` methods if the provider supports `resolveChatContext`. + */ value?: string; } export interface ChatContextProvider { + /** + * An optional event that should be fired when the workspace chat context has changed. + */ + onDidChangeWorkspaceChatContext?: Event; + + /** + * Provide a list of chat context items to be included as workspace context for all chat sessions. + * + * @param token A cancellation token. + */ + provideWorkspaceChatContext?(token: CancellationToken): ProviderResult; + /** * Provide a list of chat context items that a user can choose from. These context items are shown as options when the user explicitly attaches context. * Chat context items can be provided without a `value`, as the `value` can be resolved later using `resolveChatContext`. * `resolveChatContext` is only called for items that do not have a `value`. * - * @param options - * @param token + * @param token A cancellation token. */ provideChatContextExplicit?(token: CancellationToken): ProviderResult; @@ -40,17 +73,16 @@ declare module 'vscode' { * Chat context items can be provided without a `value`, as the `value` can be resolved later using `resolveChatContext`. * `resolveChatContext` is only called for items that do not have a `value`. * - * @param resource - * @param options - * @param token + * @param options Options include the resource for which to provide context. + * @param token A cancellation token. */ provideChatContextForResource?(options: { resource: Uri }, token: CancellationToken): ProviderResult; /** * If a chat context item is provided without a `value`, from either of the `provide` methods, this method is called to resolve the `value` for the item. * - * @param context - * @param token + * @param context The context item to resolve. + * @param token A cancellation token. */ resolveChatContext(context: T, token: CancellationToken): ProviderResult; } diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 489e5f952c8..e47cdbb1fe0 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -245,10 +245,6 @@ declare module 'vscode' { export interface ChatContext { readonly chatSessionContext?: ChatSessionContext; - readonly chatSummary?: { - readonly prompt?: string; - readonly history?: string; - }; } export interface ChatSessionContext {