Merge branch 'main' into dev/dmitriv/micro-perf

This commit is contained in:
Dmitriy Vasyura
2025-11-19 11:09:18 -08:00
committed by GitHub
78 changed files with 909 additions and 519 deletions
@@ -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: |
@@ -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
@@ -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'))
@@ -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') }}:
@@ -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
@@ -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
@@ -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 }}
@@ -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
@@ -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
@@ -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
-13
View File
@@ -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
@@ -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
@@ -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:
@@ -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
@@ -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
@@ -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"
+1 -3
View File
@@ -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))
+1 -8
View File
@@ -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,
-1
View File
@@ -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',
+36 -36
View File
@@ -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": {
+5 -5
View File
@@ -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"
}
}
}
+2 -1
View File
@@ -528,7 +528,8 @@ function configureCrashReporter(): void {
productName: process.env['VSCODE_DEV'] ? `${productName} Dev` : productName,
submitURL,
uploadToServer,
compress: true
compress: true,
ignoreSystemCrashHandler: true
});
}
Binary file not shown.
@@ -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 {
+1
View File
@@ -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;
+1 -1
View File
@@ -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;
+24 -27
View File
@@ -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<string, AmbiguousCharacters>((localesStr) => {
const locales = localesStr.split(',');
function arrayToMap(arr: number[]): Map<number, number> {
const result = new Map<number, number>();
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<string>): AmbiguousCharacters {
return AmbiguousCharacters.cache.get(Array.from(locales));
public static getInstance(locales: Iterable<string>): AmbiguousCharacters {
return AmbiguousCharacters.cache.get(Array.from(locales).join(','));
}
private static _locales = new Lazy<string[]>(() =>
+12
View File
@@ -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', () => {
@@ -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) {
@@ -39,6 +39,6 @@
}
.action-item .action-label.separator {
background-color: var(--vscode-menu-separatorBackground);
background-color: var(--vscode-button-separator);
}
}
@@ -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'));
@@ -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();
});
@@ -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);
@@ -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<number, { id: string; selector: IDocumentFilterDto[]; support: IChatContextSupport }>();
private readonly _providers = new Map<number, { id: string; selector: IDocumentFilterDto[] | undefined; support: IChatContextSupport }>();
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);
}
}
@@ -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);
},
};
@@ -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<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContextDto; chatSummary?: { prompt?: string; history?: string } }, token: CancellationToken): Promise<IChatAgentResult | undefined>;
$invokeAgent(handle: number, request: Dto<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContextDto }, token: CancellationToken): Promise<IChatAgentResult | undefined>;
$provideFollowups(request: Dto<IChatAgentRequest>, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise<IChatFollowup[]>;
$acceptFeedback(handle: number, result: IChatAgentResult, voteAction: IChatVoteAction): void;
$acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void;
@@ -559,7 +559,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
this._onDidChangeChatRequestTools.fire(request.extRequest);
}
async $invokeAgent(handle: number, requestDto: Dto<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContextDto; chatSummary?: { prompt?: string; history?: string } }, token: CancellationToken): Promise<IChatAgentResult | undefined> {
async $invokeAgent(handle: number, requestDto: Dto<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContextDto }, token: CancellationToken): Promise<IChatAgentResult | undefined> {
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,
@@ -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<number, vscode.ChatContextProvider> = new Map();
private _providers: Map<number, { provider: vscode.ChatContextProvider; disposables: DisposableStore }> = new Map();
private _itemPool: number = 0;
private _items: Map<number, Map<number, vscode.ChatContextItem>> = 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<IChatContextItem> {
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<IChatContextItem> {
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<IChatContextItem> {
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();
}
}
}
@@ -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);
@@ -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<void> {
@@ -169,19 +176,16 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions
mapSessionContributionToType.set(contribution.type, contribution);
}
const resolvedProviders = new Set<string>();
const sessions = new ResourceMap<IAgentSessionViewModel>();
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
@@ -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
@@ -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<IAgentSessionViewModel>): void {
private async showContextMenu({ element: session, anchor }: ITreeContextMenuEvent<IAgentSessionViewModel>): Promise<void> {
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
@@ -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;
}
@@ -258,7 +258,7 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
}
})()
}
}))
}), { groupId: 'agent.sessions' })
);
}
}
@@ -336,13 +336,13 @@ export class AgentSessionsCompressionDelegate implements ITreeCompressionDelegat
export class AgentSessionsSorter implements ITreeSorter<IAgentSessionViewModel> {
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)
}
@@ -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<IConfigurationRegistry>(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);
@@ -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);
}
@@ -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<string, IChatContextProviderEntry>();
private readonly _workspaceContext = new Map<string, IChatContextItem[]>();
private readonly _registeredPickers = this._register(new DisposableMap<string, IDisposable>());
private _lastResourceContext: Map<StringChatContextValue, { originalItem: IChatContextItem; provider: IChatContextProvider }> = 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<StringChatContextValue | undefined> {
return this._contextForResource(uri, false);
}
@@ -75,7 +99,7 @@ export class ChatContextService extends Disposable {
private async _contextForResource(uri: URI, withValue: boolean): Promise<StringChatContextValue | undefined> {
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);
@@ -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 }));
@@ -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<void>());
readonly onDidChangeModelEntries = this._onDidChangeModelEntries.event;
private readonly _onDidChange = this._register(new Emitter<IViewModelChangeEvent>());
readonly onDidChange = this._onDidChange.event;
private modelEntries: IModelEntry[];
private readonly collapsedVendors = new Set<string>();
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<string, string[]>();
@@ -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<void> {
this.modelEntries = [];
await this.refresh();
return super.resolve();
}
private async refresh(): Promise<void> {
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[] {
@@ -288,11 +288,8 @@ class GutterColumnRenderer extends ModelsTableColumnRenderer<IToggleCollapseColu
private readonly _onDidToggleCollapse = new Emitter<string>();
readonly onDidToggleCollapse = this._onDidToggleCollapse.event;
private readonly _onDidChange = new Emitter<void>();
readonly onDidChange = this._onDidChange.event;
constructor(
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService
private readonly viewModel: ChatModelsViewModel,
) {
super();
}
@@ -348,11 +345,7 @@ class GutterColumnRenderer extends ModelsTableColumnRenderer<IToggleCollapseColu
class: `model-visibility-toggle ${isVisible ? `${ThemeIcon.asClassName(Codicon.eyeClosed)} model-visible` : `${ThemeIcon.asClassName(Codicon.eye)} model-hidden`}`,
tooltip: isVisible ? localize('models.visible', 'Hide in the chat model picker') : localize('models.hidden', 'Show in the chat model picker'),
checked: !isVisible,
run: async () => {
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<void>(300);
this.delayedFiltering = new Delayer<void>(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<void> {
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));
@@ -662,7 +662,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
});
}
async hasChatSessionItemProvider(chatViewType: string): Promise<boolean> {
async activateChatSessionItemProvider(chatViewType: string): Promise<IChatSessionItemProvider | undefined> {
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<IChatSessionItem[]> {
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<IChatSessionItem> {
if (!(await this.hasChatSessionItemProvider(chatSessionType))) {
if (!(await this.activateChatSessionItemProvider(chatSessionType))) {
throw Error(`Cannot find provider for ${chatSessionType}`);
}
@@ -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<void>());
@@ -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);
@@ -86,7 +86,7 @@ export class ChatSessionsViewContrib extends Disposable implements IWorkbenchCon
private async updateViewRegistration(): Promise<void> {
// 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));
@@ -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) {
@@ -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();
@@ -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<IChatWidget | undefined>;
openSession(sessionResource: URI, target?: PreferredGroup, options?: IChatEditorOptions): Promise<IChatWidget | undefined>;
async openSession(sessionResource: URI, target?: typeof ChatViewPaneTarget | PreferredGroup, options?: IChatEditorOptions): Promise<IChatWidget | undefined> {
// TODO remove this, open the real resource
if (isEqual(sessionResource, LocalChatSessionsProvider.CHAT_WIDGET_VIEW_RESOURCE)) {
const chatViewPane = await this.viewsService.openView<ChatViewPane>(ChatViewId, true);
if (chatViewPane) {
chatViewPane.focusInput();
}
return chatViewPane?.widget;
}
const alreadyOpenWidget = await this.revealSessionIfAlreadyOpen(sessionResource);
if (alreadyOpenWidget) {
return alreadyOpenWidget;
@@ -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;
@@ -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 {
@@ -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>('IChatService');
@@ -404,7 +404,7 @@ export class ChatService extends Disposable implements IChatService {
});
}
shouldBeInHistory(entry: Partial<ChatModel>) {
private shouldBeInHistory(entry: Partial<ChatModel>) {
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
@@ -155,7 +155,7 @@ export interface IChatSessionsService {
readonly onDidChangeInProgress: Event<void>;
registerChatSessionItemProvider(provider: IChatSessionItemProvider): IDisposable;
hasChatSessionItemProvider(chatSessionType: string): Promise<boolean>;
activateChatSessionItemProvider(chatSessionType: string): Promise<IChatSessionItemProvider | undefined>;
getAllChatSessionItemProviders(): IChatSessionItemProvider[];
getAllChatSessionContributions(): IChatSessionsExtensionPoint[];
@@ -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';
}
@@ -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 {
@@ -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 {
@@ -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();
@@ -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'),
@@ -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[];
@@ -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.`);
}
});
});
@@ -73,8 +73,8 @@ export class MockChatSessionsService implements IChatSessionsService {
this.contributions = contributions;
}
async hasChatSessionItemProvider(chatSessionType: string): Promise<boolean> {
return this.sessionItemProviders.has(chatSessionType);
async activateChatSessionItemProvider(chatSessionType: string): Promise<IChatSessionItemProvider | undefined> {
return this.sessionItemProviders.get(chatSessionType);
}
getAllChatSessionItemProviders(): IChatSessionItemProvider[] {
@@ -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 },
});
@@ -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([{
@@ -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<PerformanceInputLatencyEvent, PerformanceInputLatencyClassification>('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'
});
}
}
@@ -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<SC
} else {
// History item
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) {
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) ?? [];
@@ -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<ISCMViewService, TreeElement>;
@@ -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';
}
@@ -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<IDefaultAccount | null> {
private async getDefaultAccountFromAuthenticatedSessions(authProviderId: string, scopes: string[][]): Promise<IDefaultAccount | null> {
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<AuthenticationSession | undefined> {
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<string>, expectedScopes: string[]): boolean {
return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope));
}
+42 -10
View File
@@ -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:<id>` 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<T extends ChatContextItem = ChatContextItem> {
/**
* An optional event that should be fired when the workspace chat context has changed.
*/
onDidChangeWorkspaceChatContext?: Event<void>;
/**
* 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<T[]>;
/**
* 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<T[]>;
@@ -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<T | undefined>;
/**
* 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<ChatContextItem>;
}
@@ -245,10 +245,6 @@ declare module 'vscode' {
export interface ChatContext {
readonly chatSessionContext?: ChatSessionContext;
readonly chatSummary?: {
readonly prompt?: string;
readonly history?: string;
};
}
export interface ChatSessionContext {