diff --git a/.github/workflows/component-fixture-tests.yml b/.github/workflows/component-fixture-tests.yml index cdd00061ead80..325b550ebda00 100644 --- a/.github/workflows/component-fixture-tests.yml +++ b/.github/workflows/component-fixture-tests.yml @@ -28,7 +28,22 @@ jobs: with: node-version-file: .nvmrc + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts compile $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v5 + with: + path: .build/node_modules_cache + key: "node_modules-compile-${{ hashFiles('.build/packagelockhash') }}" + + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci --ignore-scripts env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 @@ -36,13 +51,23 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install build dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci working-directory: build - name: Install rspack dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci working-directory: build/rspack + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + - name: Transpile source run: npm run transpile-client diff --git a/.github/workflows/screenshot-test.yml b/.github/workflows/screenshot-test.yml index 8c372e1342889..099a6397728f9 100644 --- a/.github/workflows/screenshot-test.yml +++ b/.github/workflows/screenshot-test.yml @@ -36,7 +36,22 @@ jobs: with: node-version-file: .nvmrc + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts compile $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v5 + with: + path: .build/node_modules_cache + key: "node_modules-compile-${{ hashFiles('.build/packagelockhash') }}" + + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci --ignore-scripts env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 @@ -44,13 +59,23 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install build dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci working-directory: build - name: Install rspack dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci working-directory: build/rspack + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.ts .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + - name: Copy codicons run: cp node_modules/@vscode/codicons/dist/codicon.ttf src/vs/base/browser/ui/codicons/codicon/codicon.ttf @@ -328,11 +353,11 @@ jobs: diff -u test/componentFixtures/blocks-ci-screenshots.md /tmp/blocks-ci-updated.md || true exit 1 - # - name: Fail if fixtures had errors - # if: always() && steps.fixture_errors.outputs.has_errors == 'true' - # run: | - # echo "::error::One or more component fixtures failed to render. See the 'Check fixture errors' step for details." - # exit 1 + - name: Fail if fixtures had errors + if: always() && steps.fixture_errors.outputs.has_errors == 'true' + run: | + echo "::error::One or more component fixtures failed to render. See the 'Check fixture errors' step for details." + exit 1 # - name: Prepare explorer artifact diff --git a/.vscode/extensions/vscode-pr-pinger/package-lock.json b/.vscode/extensions/vscode-pr-pinger/package-lock.json index ad347b7bd78be..73cc0b7100c40 100644 --- a/.vscode/extensions/vscode-pr-pinger/package-lock.json +++ b/.vscode/extensions/vscode-pr-pinger/package-lock.json @@ -29,31 +29,48 @@ } }, "node_modules/@octokit/graphql": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.1.tgz", - "integrity": "sha512-sxmnewSwAixkP1TrLdE6yRG53eEhHhDTYUykUwdV9x8f91WcbhunIHk9x1PZLALdBZKRPUO2HRcm4kezZ79HoA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz", + "integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==", + "license": "MIT", "dependencies": { "@octokit/request": "^6.0.0", - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "universal-user-agent": "^6.0.0" }, "engines": { "node": ">= 14" } }, + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==", + "license": "MIT" + }, + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, "node_modules/@octokit/openapi-types": { "version": "13.10.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.10.0.tgz", "integrity": "sha512-wPQDpTyy35D6VS/lekXDaKcxy6LI2hzcbmXBnP180Pdgz3dXRzoHdav0w09yZzzWX8HHLGuqwAeyMqEPtWY2XA==" }, "node_modules/@octokit/request": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.1.tgz", - "integrity": "sha512-gYKRCia3cpajRzDSU+3pt1q2OcuC6PK8PmFIyxZDWCzRXRSIBH8jXjFJ8ZceoygBIm0KsEUg4x1+XcYBz7dHPQ==", + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", + "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", + "license": "MIT", "dependencies": { "@octokit/endpoint": "^7.0.0", "@octokit/request-error": "^3.0.0", - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "is-plain-object": "^5.0.0", "node-fetch": "^2.6.7", "universal-user-agent": "^6.0.0" @@ -63,11 +80,12 @@ } }, "node_modules/@octokit/request-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.1.tgz", - "integrity": "sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", + "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", + "license": "MIT", "dependencies": { - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "deprecation": "^2.0.0", "once": "^1.4.0" }, @@ -75,6 +93,36 @@ "node": ">= 14" } }, + "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==", + "license": "MIT" + }, + "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, + "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==", + "license": "MIT" + }, + "node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, "node_modules/@octokit/types": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.4.0.tgz", @@ -165,13 +213,28 @@ } }, "@octokit/graphql": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.1.tgz", - "integrity": "sha512-sxmnewSwAixkP1TrLdE6yRG53eEhHhDTYUykUwdV9x8f91WcbhunIHk9x1PZLALdBZKRPUO2HRcm4kezZ79HoA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz", + "integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==", "requires": { "@octokit/request": "^6.0.0", - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" + }, + "@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "requires": { + "@octokit/openapi-types": "^18.0.0" + } + } } }, "@octokit/openapi-types": { @@ -180,26 +243,56 @@ "integrity": "sha512-wPQDpTyy35D6VS/lekXDaKcxy6LI2hzcbmXBnP180Pdgz3dXRzoHdav0w09yZzzWX8HHLGuqwAeyMqEPtWY2XA==" }, "@octokit/request": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.1.tgz", - "integrity": "sha512-gYKRCia3cpajRzDSU+3pt1q2OcuC6PK8PmFIyxZDWCzRXRSIBH8jXjFJ8ZceoygBIm0KsEUg4x1+XcYBz7dHPQ==", + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", + "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", "requires": { "@octokit/endpoint": "^7.0.0", "@octokit/request-error": "^3.0.0", - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "is-plain-object": "^5.0.0", "node-fetch": "^2.6.7", "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" + }, + "@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "requires": { + "@octokit/openapi-types": "^18.0.0" + } + } } }, "@octokit/request-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.1.tgz", - "integrity": "sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", + "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", "requires": { - "@octokit/types": "^7.0.0", + "@octokit/types": "^9.0.0", "deprecation": "^2.0.0", "once": "^1.4.0" + }, + "dependencies": { + "@octokit/openapi-types": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" + }, + "@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "requires": { + "@octokit/openapi-types": "^18.0.0" + } + } } }, "@octokit/types": { diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index ed12a46473ace..aa8fc806f2e94 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -76,9 +76,6 @@ async function main(buildDir?: string): Promise { const appRoot = path.join(buildDir, `VSCode-darwin-${arch}`); const appName = product.nameLong + '.app'; const infoPlistPath = path.resolve(appRoot, appName, 'Contents', 'Info.plist'); - const embeddedInfoPlistPath = product.embedded - ? path.resolve(appRoot, appName, 'Contents', 'Applications', `${product.embedded.nameLong}.app`, 'Contents', 'Info.plist') - : undefined; const appOpts: SignOptions = { app: path.join(appRoot, appName), @@ -132,44 +129,6 @@ async function main(buildDir?: string): Promise { 'The app uses your local network for DNS resolution and to connect to locally running services.', `${infoPlistPath}` ]); - - if (embeddedInfoPlistPath && fs.existsSync(embeddedInfoPlistPath)) { - await spawn('plutil', [ - '-insert', - 'NSAppleEventsUsageDescription', - '-string', - `An application in ${product.embedded.nameLong} wants to use AppleScript.`, - `${embeddedInfoPlistPath}` - ]); - await spawn('plutil', [ - '-replace', - 'NSMicrophoneUsageDescription', - '-string', - `An application in ${product.embedded.nameLong} wants to use the Microphone.`, - `${embeddedInfoPlistPath}` - ]); - await spawn('plutil', [ - '-replace', - 'NSCameraUsageDescription', - '-string', - `An application in ${product.embedded.nameLong} wants to use the Camera.`, - `${embeddedInfoPlistPath}` - ]); - await spawn('plutil', [ - '-replace', - 'NSAudioCaptureUsageDescription', - '-string', - `An application in ${product.embedded.nameLong} wants to use Audio Capture.`, - `${embeddedInfoPlistPath}` - ]); - await spawn('plutil', [ - '-insert', - 'NSLocalNetworkUsageDescription', - '-string', - `The app uses your local network for DNS resolution and to connect to locally running services.`, - `${embeddedInfoPlistPath}` - ]); - } } await retrySignOnKeychainError(() => sign(appOpts)); diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 6205bfe48ed2e..a636b48cd5f07 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -32,7 +32,6 @@ import { compileBuildWithoutManglingTask, compileBuildWithManglingTask } from '. import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask, compileCopilotExtensionBuildTask } from './gulpfile.extensions.ts'; import { copyCodiconsTask } from './lib/compilation.ts'; import { getCopilotExcludeFilter, prepareBuiltInCopilotRipgrepShim } from './lib/copilot.ts'; -import type { EmbeddedProductInfo } from './lib/embeddedType.ts'; import { useEsbuildTranspile } from './buildConfig.ts'; import { promisify } from 'util'; import globCallback from 'glob'; @@ -288,10 +287,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const name = product.nameShort; const packageJsonUpdates: Record = { name, version }; - const isInsiderOrExploration = quality === 'insider' || quality === 'exploration'; - const embedded = isInsiderOrExploration - ? (product as typeof product & { embedded?: EmbeddedProductInfo }).embedded - : undefined; if (platform === 'linux') { packageJsonUpdates.desktopName = `${product.applicationName}.desktop`; @@ -312,11 +307,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d json.date = readISODate(out); json.checksums = checksums; json.version = version; - if (embedded) { - json['darwinSiblingBundleIdentifier'] = embedded.darwinBundleIdentifier; - const embeddedObj = json['embedded'] as EmbeddedProductInfo; - embeddedObj['darwinSiblingBundleIdentifier'] = json['darwinBundleIdentifier'] as string; - } return json; })) .pipe(es.through(function (file) { @@ -324,32 +314,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d this.emit('data', file); })); - const packageSubJsonStream = embedded - ? gulp.src(['package.json'], { base: '.' }) - .pipe(jsonEditor((json: Record) => { - json.name = embedded.nameShort; - return json; - })) - .pipe(rename('package.sub.json')) - : undefined; - - const productSubJsonStream = embedded - ? gulp.src(['product.json'], { base: '.' }) - .pipe(jsonEditor((json: Record) => { - // Preserve the host's mutex name before overlaying embedded properties, - // so the embedded app can poll for the correct InnoSetup -ready mutex. - const hostMutexName = json['win32MutexName']; - Object.keys(embedded).forEach(key => { - json[key] = embedded[key as keyof EmbeddedProductInfo]; - }); - if (hostMutexName) { - json['win32SetupMutexName'] = hostMutexName; - } - return json; - })) - .pipe(rename('product.sub.json')) - : undefined; - const license = gulp.src([product.licenseFileName, 'ThirdPartyNotices.txt', 'licenses/**'], { base: '.', allowEmpty: true }); // TODO the API should be copied to `out` during compile, not here @@ -401,12 +365,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d sources, deps ]; - if (packageSubJsonStream) { - mergeStreams.push(packageSubJsonStream); - } - if (productSubJsonStream) { - mergeStreams.push(productSubJsonStream); - } let all = es.merge(...mergeStreams); if (platform === 'win32') { @@ -442,9 +400,6 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d 'resources/win32/code_70x70.png', 'resources/win32/code_150x150.png' ], { base: '.' })); - if (embedded) { - all = es.merge(all, gulp.src('resources/win32/sessions.ico', { base: '.' })); - } } else if (platform === 'linux') { const policyDest = gulp.src('.build/policies/linux/**', { base: '.build/policies/linux' }) .pipe(rename(f => f.dirname = `policies/${f.dirname}`)); @@ -463,21 +418,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d ...config, platform, arch: arch === 'armhf' ? 'arm' : arch, - ffmpegChromium: false, - ...(embedded ? { - darwinMiniAppName: embedded.nameShort, - darwinMiniAppDisplayName: embedded.nameLong, - darwinMiniAppBundleIdentifier: embedded.darwinBundleIdentifier, - darwinMiniAppIcon: 'resources/darwin/agents.icns', - darwinMiniAppAssetsCar: 'resources/darwin/agents.car', - darwinMiniAppBundleURLTypes: [{ - role: 'Viewer', - name: embedded.nameLong, - urlSchemes: [embedded.urlProtocol] - }], - win32ProxyAppName: embedded.nameShort, - win32ProxyIcon: 'resources/win32/sessions.ico', - } : {}) + ffmpegChromium: false }; let result: NodeJS.ReadWriteStream = all @@ -489,8 +430,8 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d '**', '!LICENSE', '!version', - ...(platform === 'darwin' && !isInsiderOrExploration ? ['!**/Contents/Applications', '!**/Contents/Applications/**'] : []), - ...(platform === 'win32' && !isInsiderOrExploration ? ['!**/electron_proxy.exe'] : []), + ...(platform === 'darwin' ? ['!**/Contents/Applications', '!**/Contents/Applications/**'] : []), + ...(platform === 'win32' ? ['!**/electron_proxy.exe'] : []), ], { dot: true })); if (platform === 'linux') { diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts index 01070c9503c05..667057fcb09bf 100644 --- a/build/gulpfile.vscode.win32.ts +++ b/build/gulpfile.vscode.win32.ts @@ -14,7 +14,6 @@ import product from '../product.json' with { type: 'json' }; import { getVersion } from './lib/getVersion.ts'; import * as task from './lib/task.ts'; import * as util from './lib/util.ts'; -import type { EmbeddedProductInfo } from './lib/embeddedType.ts'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); @@ -112,25 +111,6 @@ function buildWin32Setup(arch: string, target: string): task.CallbackTask { Quality: quality }; - const isInsiderOrExploration = quality === 'insider' || quality === 'exploration'; - const embedded = isInsiderOrExploration - ? (product as typeof product & { embedded?: EmbeddedProductInfo }).embedded - : undefined; - - if (embedded) { - // VS Code's sibling is the embedded app. - productJson['win32SiblingExeBasename'] = embedded.nameShort; - // The embedded app's sibling is VS Code. - if (productJson['embedded']) { - productJson['embedded']['win32SiblingExeBasename'] = product.nameShort; - } - definitions['ProxyExeBasename'] = embedded.nameShort; - definitions['ProxyAppUserId'] = embedded.win32AppUserModelId; - definitions['ProxyNameLong'] = embedded.nameLong; - definitions['ProxyExeUrlProtocol'] = embedded.urlProtocol; - definitions['ProxyMutex'] = embedded.win32MutexName; - } - if (quality === 'stable' || quality === 'insider') { definitions['AppxPackage'] = `${quality === 'stable' ? 'code' : 'code_insider'}_${arch}.appx`; definitions['AppxPackageDll'] = `${quality === 'stable' ? 'code' : 'code_insider'}_explorer_command_${arch}.dll`; diff --git a/build/lib/embeddedType.ts b/build/lib/embeddedType.ts deleted file mode 100644 index b0b3ad7d833ec..0000000000000 --- a/build/lib/embeddedType.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export type EmbeddedProductInfo = { - nameShort: string; - nameLong: string; - applicationName: string; - dataFolderName: string; - darwinBundleIdentifier: string; - darwinSiblingBundleIdentifier?: string; - urlProtocol: string; - win32AppUserModelId: string; - win32MutexName: string; - win32RegValueName: string; - win32NameVersion: string; - win32VersionedUpdate: boolean; - win32SiblingExeBasename?: string; -}; diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 951401e17612c..a9bc64352d706 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -290,6 +290,10 @@ "name": "vs/workbench/contrib/externalUriOpener", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/agentsAppMergedBanner", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/welcomeGettingStarted", "project": "vscode-workbench" diff --git a/build/package-lock.json b/build/package-lock.json index 92f3b6a4a3e70..4e26bb1882781 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -1092,6 +1092,19 @@ "node": ">= 12.13.0" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2299,9 +2312,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", - "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "dev": true, "license": "MIT", "engines": { @@ -3495,9 +3508,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.9.tgz", + "integrity": "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==", "dev": true, "funding": [ { @@ -3511,9 +3524,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.7", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.7.tgz", - "integrity": "sha512-LteOsISQ2GEiDHZch6L9hB0+MLoYVLToR7xotrzU0opCICBkxOPgHAy1HxAvtxfJNXDJpgAsQN30mkrfpO2Prg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", "dev": true, "funding": [ { @@ -3523,9 +3536,10 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.1.3", - "strnum": "^2.2.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -5106,9 +5120,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", - "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "dev": true, "funding": [ { @@ -6166,9 +6180,9 @@ } }, "node_modules/strnum": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.1.tgz", - "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", "dev": true, "funding": [ { diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index c932ead6277d7..28febbbb9998a 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -1081,9 +1081,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { diff --git a/build/win32/code.iss b/build/win32/code.iss index 594453ed7b989..3e9657e65da69 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -74,10 +74,15 @@ Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\nod Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules.asar.unpacked"; Check: IsNotBackgroundUpdate Type: files; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules.asar"; Check: IsNotBackgroundUpdate Type: files; Name: "{app}\{#VersionedResourcesFolder}\resources\app\Credits_45.0.2454.85.html"; Check: IsNotBackgroundUpdate -#ifdef ProxyExeBasename -; Clean up legacy Start Menu shortcut that used ProxyExeBasename instead of ProxyNameLong -Type: files; Name: "{group}\{#ProxyExeBasename}.lnk" -#endif + +; Remove leftover shortcuts and pinned entries from the previous Agents sub-application. +Type: files; Name: "{group}\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{userprograms}\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{commonprograms}\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{autodesktop}\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Agents - Insiders.lnk"; Check: QualityIsInsiders +Type: files; Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\User Pinned\StartMenu\Agents - Insiders.lnk"; Check: QualityIsInsiders [UninstallDelete] Type: filesandordirs; Name: "{app}\_" @@ -99,12 +104,9 @@ Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{ Name: "{app}"; AfterInstall: DisableAppDirInheritance [Files] -Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\policies,\policies\*,\appx,\appx\*,\resources\app\product.json,\{#ExeBasename}.exe,{#ifdef ProxyExeBasename}\{#ProxyExeBasename}.exe,{#endif}\{#ExeBasename}.VisualElementsManifest.xml,\bin,\bin\*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\policies,\policies\*,\appx,\appx\*,\resources\app\product.json,\{#ExeBasename}.exe,\{#ExeBasename}.VisualElementsManifest.xml,\bin,\bin\*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#ExeBasename}.exe"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetExeBasename}"; Flags: ignoreversion Source: "{#ExeBasename}.VisualElementsManifest.xml"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetVisualElementsManifest}"; Flags: ignoreversion -#ifdef ProxyExeBasename -Source: "{#ProxyExeBasename}.exe"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetProxyExeBasename}"; Flags: ignoreversion -#endif Source: "tools\*"; DestDir: "{app}\{#VersionedResourcesFolder}\tools"; Flags: ignoreversion Source: "policies\*"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\policies"; Flags: ignoreversion skipifsourcedoesntexist Source: "bin\{#TunnelApplicationName}.exe"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirTunnelApplicationFilename}"; Flags: ignoreversion skipifsourcedoesntexist @@ -120,18 +122,10 @@ Source: "appx\{#AppxPackageDll}"; DestDir: "{code:GetDestDir}\{#VersionedResourc Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{group}\{#NameLong}.lnk')) Name: "{autodesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{autodesktop}\{#NameLong}.lnk')) Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#AppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}.lnk')) -#ifdef ProxyExeBasename -Name: "{group}\{#ProxyNameLong}"; Filename: "{app}\{#ProxyExeBasename}.exe"; AppUserModelID: "{#ProxyAppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{group}\{#ProxyNameLong}.lnk')) -Name: "{autodesktop}\{#ProxyNameLong}"; Filename: "{app}\{#ProxyExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#ProxyAppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{autodesktop}\{#ProxyNameLong}.lnk')) -Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#ProxyNameLong}"; Filename: "{app}\{#ProxyExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#ProxyAppUserId}"; Check: ShouldUpdateShortcut(ExpandConstant('{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#ProxyNameLong}.lnk')) -#endif [Run] Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunAfterUpdate Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Flags: nowait postinstall; Check: WizardNotSilent -#ifdef ProxyExeBasename -Filename: "{app}\{#ProxyExeBasename}.exe"; Description: "{cm:LaunchProgram,{#ProxyNameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunProxyAfterUpdate -#endif [Registry] #if "user" == InstallTarget @@ -1301,15 +1295,6 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValu Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Check: ShouldInstallLegacyFolderContextMenu Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Check: ShouldInstallLegacyFolderContextMenu -; URL Protocol handler for proxy executable -#ifdef ProxyExeBasename -#ifdef ProxyExeUrlProtocol -Root: HKCU; Subkey: "Software\Classes\{#ProxyExeUrlProtocol}"; ValueType: string; ValueName: ""; ValueData: "URL:{#ProxyExeUrlProtocol}"; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\{#ProxyExeUrlProtocol}"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\{#ProxyExeUrlProtocol}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ProxyExeBasename}.exe"" --open-url -- ""%1"""; Flags: uninsdeletekey -#endif -#endif - ; Environment #if "user" == InstallTarget #define EnvironmentRootKey "HKCU" @@ -1421,10 +1406,6 @@ end; var ShouldRestartTunnelService: Boolean; -#ifdef ProxyMutex - ProxyWasRunning: Boolean; - AppWasRunning: Boolean; -#endif function StopTunnelOtherProcesses(): Boolean; var @@ -1520,27 +1501,11 @@ end; function ShouldRunAfterUpdate(): Boolean; begin if IsBackgroundUpdate() then -#ifdef ProxyMutex - Result := (not LockFileExists()) and AppWasRunning -#else Result := not LockFileExists() -#endif else Result := True; end; -#ifdef ProxyMutex -function ShouldRunProxyAfterUpdate(): Boolean; -begin - // Relaunch the proxy app after a background update if it was - // running when the update started (detected via its mutex). - if IsBackgroundUpdate() then - Result := (not LockFileExists()) and ProxyWasRunning - else - Result := False; -end; -#endif - function IsWindows11OrLater(): Boolean; begin Result := (GetWindowsVersion >= $0A0055F0); @@ -1630,11 +1595,7 @@ begin if IsBackgroundUpdate() then Result := '' else -#ifdef ProxyMutex - Result := '{#AppMutex},{#ProxyMutex}'; -#else Result := '{#AppMutex}'; -#endif end; function GetSetupMutex(Value: string): string; @@ -1643,11 +1604,7 @@ begin // During background updates, also create a -updating mutex that VS Code checks // to avoid launching while an update is in progress. if IsBackgroundUpdate() then -#ifdef ProxyMutex - Result := '{#AppMutex}setup,{#AppMutex}-updating,{#ProxyMutex}-updating' -#else Result := '{#AppMutex}setup,{#AppMutex}-updating' -#endif else Result := '{#AppMutex}setup'; end; @@ -1676,16 +1633,6 @@ begin Result := ExpandConstant('{#ExeBasename}.exe'); end; -#ifdef ProxyExeBasename -function GetProxyExeBasename(Value: string): string; -begin - if IsBackgroundUpdate() and IsVersionedUpdate() then - Result := ExpandConstant('new_{#ProxyExeBasename}.exe') - else - Result := ExpandConstant('{#ProxyExeBasename}.exe'); -end; -#endif - function GetBinDirTunnelApplicationFilename(Value: string): string; begin if IsBackgroundUpdate() and IsVersionedUpdate() then @@ -1850,24 +1797,12 @@ begin if IsBackgroundUpdate() then begin -#ifdef ProxyMutex - // Snapshot whether each app is running before we wait for them to exit - ProxyWasRunning := CheckForMutexes('{#ProxyMutex}'); - AppWasRunning := CheckForMutexes('{#AppMutex}'); - Log('App was running: ' + BoolToStr(AppWasRunning)); - Log('Proxy app was running: ' + BoolToStr(ProxyWasRunning)); -#endif - SaveStringToFile(ExpandConstant('{app}\updating_version'), '{#Commit}', False); CreateMutex('{#AppMutex}-ready'); DeleteFile(GetUpdateProgressFilePath()); Log('Checking whether application is still running...'); -#ifdef ProxyMutex - while (CheckForMutexes('{#AppMutex},{#ProxyMutex}')) do -#else while (CheckForMutexes('{#AppMutex}')) do -#endif begin if CancelFileExists() then begin @@ -1882,14 +1817,14 @@ begin if not SessionEndFileExists() and not CancelFileExists() then begin StopTunnelServiceIfNeeded(); Log('Invoking inno_updater for background update'); - Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists()) + ' "{cm:UpdatingVisualStudioCode}"' {#ifdef ProxyExeBasename} + ' "{#ProxyExeBasename}.exe"' {#endif}), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists()) + ' "{cm:UpdatingVisualStudioCode}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); DeleteFile(ExpandConstant('{app}\updating_version')); Log('inno_updater completed successfully'); #if "system" == InstallTarget if IsVersionedUpdate() then begin KillContextMenuComSurrogate(); Log('Invoking inno_updater to remove previous installation folder'); - Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}" "{#ExeBasename}.exe"' {#ifdef ProxyExeBasename} + ' "{#ProxyExeBasename}.exe"' {#endif}), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}" "{#ExeBasename}.exe"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); Log('inno_updater completed gc successfully'); end; #endif @@ -1900,7 +1835,7 @@ begin if IsVersionedUpdate() then begin KillContextMenuComSurrogate(); Log('Invoking inno_updater to remove previous installation folder'); - Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}" "{#ExeBasename}.exe"' {#ifdef ProxyExeBasename} + ' "{#ProxyExeBasename}.exe"' {#endif}), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}" "{#ExeBasename}.exe"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); Log('inno_updater completed gc successfully'); end; end; diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index ba939ae5bccf9..7ed70c12cf1e8 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -10115,12 +10115,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", - "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -11286,9 +11286,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 17f42237d7a7e..a9d929015b719 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -543,6 +543,7 @@ "icon": "$(diff)", "userDescription": "%copilot.tools.changes.description%", "modelDescription": "Get git diffs of current file changes in a git repository. Don't forget that you can use run_in_terminal to run git commands in a terminal as well.", + "when": "config.github.copilot.chat.getChangedFilesTool.enabled", "tags": [ "vscode_codesearch" ], @@ -4445,6 +4446,16 @@ "experimental" ] }, + "github.copilot.chat.getChangedFilesTool.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%github.copilot.config.getChangedFilesTool.enabled%", + "tags": [ + "advanced", + "experimental", + "onExp" + ] + }, "github.copilot.chat.executionSubagent.enabled": { "type": "boolean", "default": false, @@ -4679,7 +4690,7 @@ }, "github.copilot.chat.cli.autoModel.enabled": { "type": "boolean", - "default": true, + "default": false, "markdownDescription": "%github.copilot.config.cli.autoModel.enabled%", "tags": [ "advanced" diff --git a/extensions/copilot/package.nls.json b/extensions/copilot/package.nls.json index bccc10f19cfbc..d1a98742c666e 100644 --- a/extensions/copilot/package.nls.json +++ b/extensions/copilot/package.nls.json @@ -491,6 +491,7 @@ "copilot.tools.skill.name": "Skill", "copilot.tools.skill.description": "Execute a skill by name. Skills provide specialized capabilities, domain knowledge, and refined workflows.", "github.copilot.config.skill.enabled": "Enable the skill tool in Copilot Chat. When enabled, skills are invoked via a dedicated skill tool instead of readFile.", + "github.copilot.config.getChangedFilesTool.enabled": "Enable the Get Changed Files tool in Copilot Chat. When enabled, the agent can retrieve git diffs of current changes via a dedicated tool.", "github.copilot.config.searchSubagent.enabled": "Enable the search subagent tool for iterative code exploration in the workspace.", "github.copilot.config.searchSubagent.useAgenticProxy": "Use the agentic proxy for the search subagent tool.", "github.copilot.config.searchSubagent.model": "Model to use for the search subagent. When useAgenticProxy is enabled, defaults to 'vscode-agentic-search-router-a'. Otherwise defaults to the main agent model.", diff --git a/extensions/copilot/src/extension/byok/vscode-node/anthropicProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/anthropicProvider.ts index 5a24520c0ce33..ca7d3b9e802a1 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/anthropicProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/anthropicProvider.ts @@ -34,6 +34,7 @@ import { IBYOKStorageService } from './byokStorageService'; export class AnthropicLMProvider extends AbstractLanguageModelChatProvider { public static readonly providerName = 'Anthropic'; + public static readonly providerId = this.providerName.toLowerCase(); constructor( knownModels: BYOKKnownModels | undefined, @@ -46,7 +47,7 @@ export class AnthropicLMProvider extends AbstractLanguageModelChatProvider { @IOTelService private readonly _otelService: IOTelService, @IToolDeferralService private readonly _toolDeferralService: IToolDeferralService, ) { - super(AnthropicLMProvider.providerName.toLowerCase(), AnthropicLMProvider.providerName, knownModels, byokStorageService, logService); + super(AnthropicLMProvider.providerId, AnthropicLMProvider.providerName, knownModels, byokStorageService, logService); } @@ -356,7 +357,7 @@ export class AnthropicLMProvider extends AbstractLanguageModelChatProvider { // Record OTel metrics for this Anthropic LLM call if (result.usage) { const durationSec = (Date.now() - issuedTime) / 1000; - const metricAttrs = { operationName: GenAiOperationName.CHAT, providerName: 'anthropic', requestModel: model.id, responseModel: model.id }; + const metricAttrs = { operationName: GenAiOperationName.CHAT, providerName: AnthropicLMProvider.providerId, requestModel: model.id, responseModel: model.id }; GenAiMetrics.recordOperationDuration(this._otelService, durationSec, metricAttrs); if (result.usage.prompt_tokens) { GenAiMetrics.recordTokenUsage(this._otelService, result.usage.prompt_tokens, 'input', metricAttrs); } if (result.usage.completion_tokens) { GenAiMetrics.recordTokenUsage(this._otelService, result.usage.completion_tokens, 'output', metricAttrs); } diff --git a/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts index 56f5bb2d617d6..6e32e34a46050 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/azureProvider.ts @@ -47,7 +47,8 @@ export function resolveAzureUrl(modelId: string, url: string): string { export class AzureBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider { - static readonly providerName = 'Azure'; + public static readonly providerName = 'Azure'; + public static readonly providerId = this.providerName.toLowerCase(); constructor( byokStorageService: IBYOKStorageService, @@ -59,7 +60,7 @@ export class AzureBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider { @IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext ) { super( - AzureBYOKModelProvider.providerName.toLowerCase(), + AzureBYOKModelProvider.providerId, AzureBYOKModelProvider.providerName, byokStorageService, logService, diff --git a/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts b/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts index 9ccea9caa4aa2..3bb2b4407f2a6 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/byokContribution.ts @@ -65,17 +65,17 @@ export class BYOKContrib extends Disposable implements IExtensionContribution { if (this._store.isDisposed) { return; } - this._providers.set(OllamaLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OllamaLMProvider, this._byokStorageService)); - this._providers.set(AnthropicLMProvider.providerName.toLowerCase(), instantiationService.createInstance(AnthropicLMProvider, knownModels[AnthropicLMProvider.providerName], this._byokStorageService)); - this._providers.set(GeminiNativeBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(GeminiNativeBYOKLMProvider, knownModels[GeminiNativeBYOKLMProvider.providerName], this._byokStorageService)); - this._providers.set(XAIBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(XAIBYOKLMProvider, knownModels[XAIBYOKLMProvider.providerName], this._byokStorageService)); - this._providers.set(OAIBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OAIBYOKLMProvider, knownModels[OAIBYOKLMProvider.providerName], this._byokStorageService)); - this._providers.set(OpenRouterLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OpenRouterLMProvider, this._byokStorageService)); - this._providers.set(AzureBYOKModelProvider.providerName.toLowerCase(), instantiationService.createInstance(AzureBYOKModelProvider, this._byokStorageService)); - this._providers.set(CustomOAIBYOKModelProvider.providerName.toLowerCase(), instantiationService.createInstance(CustomOAIBYOKModelProvider, this._byokStorageService)); + this._providers.set(OllamaLMProvider.providerId, instantiationService.createInstance(OllamaLMProvider, this._byokStorageService)); + this._providers.set(AnthropicLMProvider.providerId, instantiationService.createInstance(AnthropicLMProvider, knownModels[AnthropicLMProvider.providerName], this._byokStorageService)); + this._providers.set(GeminiNativeBYOKLMProvider.providerId, instantiationService.createInstance(GeminiNativeBYOKLMProvider, knownModels[GeminiNativeBYOKLMProvider.providerName], this._byokStorageService)); + this._providers.set(XAIBYOKLMProvider.providerId, instantiationService.createInstance(XAIBYOKLMProvider, knownModels[XAIBYOKLMProvider.providerName], this._byokStorageService)); + this._providers.set(OAIBYOKLMProvider.providerId, instantiationService.createInstance(OAIBYOKLMProvider, knownModels[OAIBYOKLMProvider.providerName], this._byokStorageService)); + this._providers.set(OpenRouterLMProvider.providerId, instantiationService.createInstance(OpenRouterLMProvider, this._byokStorageService)); + this._providers.set(AzureBYOKModelProvider.providerId, instantiationService.createInstance(AzureBYOKModelProvider, this._byokStorageService)); + this._providers.set(CustomOAIBYOKModelProvider.providerId, instantiationService.createInstance(CustomOAIBYOKModelProvider, this._byokStorageService)); - for (const [providerName, provider] of this._providers) { - this._byokRegistrations.add(lm.registerLanguageModelChatProvider(providerName, provider)); + for (const [providerId, provider] of this._providers) { + this._byokRegistrations.add(lm.registerLanguageModelChatProvider(providerId, provider)); } this._logService.info(`BYOK: registered ${this._providers.size} provider(s): ${Array.from(this._providers.keys()).join(', ')}`); } else if (!this._byokProvidersRegistered) { diff --git a/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts index 772e2cf446069..1c6e8c52589f4 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/customOAIProvider.ts @@ -163,8 +163,8 @@ export abstract class AbstractCustomOAIBYOKModelProvider extends AbstractOpenAIC export class CustomOAIBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider { - static readonly providerName: string = 'CustomOAI'; - private providerName: string = CustomOAIBYOKModelProvider.providerName; + public static readonly providerName = 'CustomOAI'; + public static readonly providerId = this.providerName.toLowerCase(); constructor( _byokStorageService: IBYOKStorageService, @@ -175,16 +175,16 @@ export class CustomOAIBYOKModelProvider extends AbstractCustomOAIBYOKModelProvid @IExperimentationService expService: IExperimentationService, @IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext ) { - super(CustomOAIBYOKModelProvider.providerName.toLowerCase(), CustomOAIBYOKModelProvider.providerName, _byokStorageService, logService, fetcherService, instantiationService, configurationService, expService, extensionContext); + super(CustomOAIBYOKModelProvider.providerId, CustomOAIBYOKModelProvider.providerName, _byokStorageService, logService, fetcherService, instantiationService, configurationService, expService, extensionContext); this.migrateExistingConfigs(); } // TODO: Remove this after 6 months private async migrateExistingConfigs(): Promise { - await this.migrateConfig(ConfigKey.Deprecated.CustomOAIModels, this.providerName, this.providerName); + await this.migrateConfig(ConfigKey.Deprecated.CustomOAIModels, CustomOAIBYOKModelProvider.providerName, CustomOAIBYOKModelProvider.providerName); } protected resolveUrl(modelId: string, url: string): string { return resolveCustomOAIUrl(modelId, url); } -} \ No newline at end of file +} diff --git a/extensions/copilot/src/extension/byok/vscode-node/geminiNativeProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/geminiNativeProvider.ts index c8fb3efb9e261..fee2a98702692 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/geminiNativeProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/geminiNativeProvider.ts @@ -26,6 +26,7 @@ import { IBYOKStorageService } from './byokStorageService'; export class GeminiNativeBYOKLMProvider extends AbstractLanguageModelChatProvider { public static readonly providerName = 'Gemini'; + public static readonly providerId = this.providerName.toLowerCase(); constructor( knownModels: BYOKKnownModels | undefined, @@ -35,7 +36,7 @@ export class GeminiNativeBYOKLMProvider extends AbstractLanguageModelChatProvide @ITelemetryService private readonly _telemetryService: ITelemetryService, @IOTelService private readonly _otelService: IOTelService, ) { - super(GeminiNativeBYOKLMProvider.providerName.toLowerCase(), GeminiNativeBYOKLMProvider.providerName, knownModels, byokStorageService, logService); + super(GeminiNativeBYOKLMProvider.providerId, GeminiNativeBYOKLMProvider.providerName, knownModels, byokStorageService, logService); } protected async getAllModels(silent: boolean, apiKey: string | undefined): Promise[]> { diff --git a/extensions/copilot/src/extension/byok/vscode-node/ollamaProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/ollamaProvider.ts index 43598527d80cb..de48e3f775a60 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/ollamaProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/ollamaProvider.ts @@ -38,7 +38,10 @@ export interface OllamaConfig extends LanguageModelChatConfiguration { } export class OllamaLMProvider extends AbstractOpenAICompatibleLMProvider { + public static readonly providerName = 'Ollama'; + public static readonly providerId = this.providerName.toLowerCase(); + private _modelCache = new Map(); constructor( @@ -50,7 +53,7 @@ export class OllamaLMProvider extends AbstractOpenAICompatibleLMProvider { }); it('resolves "auto" without querying SDK models', async () => { - const { models } = createModels({ hasSession: false }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: false, configService }); // Even without a session, 'auto' resolves to itself expect(await models.resolveModel('auto')).toBe('auto'); @@ -366,7 +368,9 @@ describe('CopilotCLIModels', () => { } it('always includes auto model in results', async () => { - const { models } = createModels({ hasSession: true }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: true, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); @@ -380,7 +384,9 @@ describe('CopilotCLIModels', () => { }); it('returns only auto when not authenticated', async () => { - const { models } = createModels({ hasSession: false }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: false, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); @@ -403,7 +409,9 @@ describe('CopilotCLIModels', () => { getRequestId: vi.fn(() => undefined), } as unknown as ICopilotCLISDK; - const { models } = createModels({ hasSession: true, sdk }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: true, sdk, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); @@ -430,7 +438,9 @@ describe('CopilotCLIModels', () => { }); it('returns full model list with auto prepended after fetch completes', async () => { - const { models } = createModels({ hasSession: true }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: true, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); @@ -444,7 +454,9 @@ describe('CopilotCLIModels', () => { }); it('resets to auto-only after auth change, then recovers', async () => { - const { models, auth } = createModels({ hasSession: true }); + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models, auth } = createModels({ hasSession: true, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); @@ -559,8 +571,10 @@ describe('CopilotCLIModels', () => { expect(await models.resolveModel('auto')).toBeUndefined(); }); - it('includes auto model when setting is enabled (default)', async () => { - const { models } = createModels({ hasSession: true }); + it('includes auto model when setting is enabled', async () => { + const configService = new MockConfigurationService(); + await configService.setConfig(ConfigKey.Advanced.CLIAutoModelEnabled, true); + const { models } = createModels({ hasSession: true, configService }); const lm = createLmMock(); models.registerLanguageModelChatProvider(lm.mock as any); diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts index cecd1b8eb400d..55e99b723369e 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts @@ -439,6 +439,9 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib } private async _getToken(): Promise { + if (!this._authenticationService.anyGitHubSession) { + return undefined; + } try { const copilotToken = await this._authenticationService.getCopilotToken(); return copilotToken; @@ -654,7 +657,7 @@ export class CopilotLanguageModelWrapper extends Disposable { throw vscode.LanguageModelError.Blocked(blockedExtensionMessage); } else if (result.type === ChatFetchResponseType.QuotaExceeded) { const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const details = getErrorDetailsFromChatFetchError(result, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const details = getErrorDetailsFromChatFetchError(result, this._authenticationService.copilotToken?.copilotPlan, outageStatus); const err = new vscode.LanguageModelError(details.message); err.name = 'ChatQuotaExceeded'; throw err; diff --git a/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts b/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts index 56c1ec62522de..caa0530a80746 100644 --- a/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts +++ b/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts @@ -171,7 +171,7 @@ export class InlineChatIntent implements IIntent { if (result.lastResponse.type !== ChatFetchResponseType.Success) { const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const details = getErrorDetailsFromChatFetchError(result.lastResponse, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const details = getErrorDetailsFromChatFetchError(result.lastResponse, this._authenticationService.copilotToken?.copilotPlan, outageStatus); return { errorDetails: { message: details.message, diff --git a/extensions/copilot/src/extension/intents/node/agentIntent.ts b/extensions/copilot/src/extension/intents/node/agentIntent.ts index 013d28faead7f..32fdf1f74aba0 100644 --- a/extensions/copilot/src/extension/intents/node/agentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/agentIntent.ts @@ -154,6 +154,9 @@ export const getAgentTools = async (accessor: ServicesAccessor, request: vscode. const skillToolEnabled = configurationService.getExperimentBasedConfig(ConfigKey.Advanced.SkillToolEnabled, experimentationService); allowTools[ToolName.Skill] = skillToolEnabled; + const getSCMChangesEnabled = configurationService.getExperimentBasedConfig(ConfigKey.Advanced.GetChangedFilesToolEnabled, experimentationService); + allowTools[ToolName.GetScmChanges] = getSCMChangesEnabled; + allowTools[ToolName.SessionStoreSql] = true; allowTools[CUSTOM_TOOL_SEARCH_NAME] = !!model.supportsToolSearch; diff --git a/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts b/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts index 4c5660ec76eca..6915d619b9bc4 100644 --- a/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts +++ b/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.ts @@ -9,7 +9,7 @@ import type { ChatRequest, ChatResponseReferencePart, ChatResponseStream, ChatRe import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; import { IAuthenticationChatUpgradeService } from '../../../platform/authentication/common/authenticationUpgrade'; import { IChatHookService, UserPromptSubmitHookInput, UserPromptSubmitHookOutput } from '../../../platform/chat/common/chatHookService'; -import { CanceledResult, ChatFetchResponseType, ChatLocation, ChatResponse, getErrorDetailsFromChatFetchError } from '../../../platform/chat/common/commonTypes'; +import { CanceledResult, ChatFetchError, ChatFetchResponseType, ChatLocation, ChatResponse, getErrorDetailsFromChatFetchError } from '../../../platform/chat/common/commonTypes'; import { IConversationOptions } from '../../../platform/chat/common/conversationOptions'; import { ISessionTranscriptService } from '../../../platform/chat/common/sessionTranscriptService'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; @@ -484,6 +484,11 @@ export class DefaultIntentRequestHandler { return {}; } + private async getErrorDetails(error: ChatFetchError) { + const status = await this._octoKitService.getGitHubOutageStatus(); + return getErrorDetailsFromChatFetchError(error, this._authenticationService.copilotToken?.copilotPlan, status); + } + private async processResult(fetchResult: ChatResponse, responseMessage: string, chatResult: ChatResult | void, metadataFragment: Partial, baseModelTelemetry: ConversationalBaseTelemetryData, rounds: IToolCallRound[]): Promise { switch (fetchResult.type) { case ChatFetchResponseType.Success: @@ -491,16 +496,14 @@ export class DefaultIntentRequestHandler { case ChatFetchResponseType.OffTopic: return this.processOffTopicFetchResult(baseModelTelemetry); case ChatFetchResponseType.Canceled: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; this.turn.setResponse(TurnStatus.Cancelled, { message: errorDetails.message, type: 'user' }, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.QuotaExceeded: case ChatFetchResponseType.RateLimited: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); if (fetchResult.type === ChatFetchResponseType.RateLimited && fetchResult.capiError?.code?.startsWith('user_model_rate_limited') && !fetchResult.isAuto) { @@ -520,22 +523,19 @@ export class DefaultIntentRequestHandler { case ChatFetchResponseType.BadRequest: case ChatFetchResponseType.NetworkError: case ChatFetchResponseType.Failed: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; this.turn.setResponse(TurnStatus.Error, { message: errorDetails.message, type: 'server' }, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.Filtered: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: { ...metadataFragment, filterReason: fetchResult.category } }; this.turn.setResponse(TurnStatus.Filtered, undefined, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.PromptFiltered: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: { ...metadataFragment, filterReason: FilterReason.Prompt } }; this.turn.setResponse(TurnStatus.PromptFiltered, undefined, baseModelTelemetry.properties.messageId, chatResult); return chatResult; @@ -546,30 +546,26 @@ export class DefaultIntentRequestHandler { return chatResult; } case ChatFetchResponseType.AgentFailedDependency: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.Length: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.NotFound: // before we had `NotFound`, it would fall into Unknown, so behavior should be consistent case ChatFetchResponseType.Unknown: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult); return chatResult; } case ChatFetchResponseType.ExtensionBlocked: { - const outageStatus = await this._octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = await this.getErrorDetails(fetchResult); const chatResult = { errorDetails, metadata: metadataFragment }; // This shouldn't happen, only 3rd party extensions should be blocked this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult); diff --git a/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapper.ts b/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapper.ts index 10e110c361d61..a104bd3925052 100644 --- a/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapper.ts +++ b/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapper.ts @@ -391,7 +391,7 @@ export class CodeMapper { return undefined; } const outageStatus = await this.octoKitService.getGitHubOutageStatus(); - const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this.authenticationService.getCopilotToken()).copilotPlan, outageStatus); + const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, this.authenticationService.copilotToken?.copilotPlan, outageStatus); result = createOutcome([{ label: errorDetails.message, message: `request ${fetchResult.type}`, severity: 'error' }], errorDetails); } if (result.annotations.length || result.errorDetails) { diff --git a/extensions/copilot/src/platform/chat/common/commonTypes.ts b/extensions/copilot/src/platform/chat/common/commonTypes.ts index 6598662913436..37984eb637dfd 100644 --- a/extensions/copilot/src/platform/chat/common/commonTypes.ts +++ b/extensions/copilot/src/platform/chat/common/commonTypes.ts @@ -358,11 +358,11 @@ function getQuotaHitMessage(fetchResult: ChatFetchError, copilotPlan: string | u } } -export function getErrorDetailsFromChatFetchError(fetchResult: ChatFetchError, copilotPlan: string, gitHubOutageStatus: GitHubOutageStatus): ChatErrorDetails { +export function getErrorDetailsFromChatFetchError(fetchResult: ChatFetchError, copilotPlan: string | undefined, gitHubOutageStatus: GitHubOutageStatus): ChatErrorDetails { return { code: fetchResult.type, ...getErrorDetailsFromChatFetchErrorInner(fetchResult, copilotPlan, gitHubOutageStatus) }; } -function getErrorDetailsFromChatFetchErrorInner(fetchResult: ChatFetchError, copilotPlan: string, gitHubOutageStatus: GitHubOutageStatus): ChatErrorDetails { +function getErrorDetailsFromChatFetchErrorInner(fetchResult: ChatFetchError, copilotPlan: string | undefined, gitHubOutageStatus: GitHubOutageStatus): ChatErrorDetails { let details: ChatErrorDetails; switch (fetchResult.type) { case ChatFetchResponseType.OffTopic: diff --git a/extensions/copilot/src/platform/chat/test/chatQuotaServiceImpl.spec.ts b/extensions/copilot/src/platform/chat/test/common/chatQuotaServiceImpl.spec.ts similarity index 98% rename from extensions/copilot/src/platform/chat/test/chatQuotaServiceImpl.spec.ts rename to extensions/copilot/src/platform/chat/test/common/chatQuotaServiceImpl.spec.ts index 1dac4cd8ffdbc..ca06e0d01fba0 100644 --- a/extensions/copilot/src/platform/chat/test/chatQuotaServiceImpl.spec.ts +++ b/extensions/copilot/src/platform/chat/test/common/chatQuotaServiceImpl.spec.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { describe, expect, test } from 'vitest'; -import { Emitter } from '../../../util/vs/base/common/event'; -import { IAuthenticationService } from '../../authentication/common/authentication'; -import { ChatQuotaService } from '../common/chatQuotaServiceImpl'; +import { Emitter } from '../../../../util/vs/base/common/event'; +import { IAuthenticationService } from '../../../authentication/common/authentication'; +import { ChatQuotaService } from '../../common/chatQuotaServiceImpl'; function createMockAuthService(): IAuthenticationService { return { diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index 2b4ba49838325..be27557cc1112 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -611,7 +611,7 @@ export namespace ConfigKey { export const OmitBaseAgentInstructions = defineAndMigrateSetting('chat.advanced.omitBaseAgentInstructions', 'chat.omitBaseAgentInstructions', false); export const CLIShowExternalSessions = defineSetting('chat.cli.showExternalSessions', ConfigType.Simple, true); export const CLIPlanExitModeEnabled = defineSetting('chat.cli.planExitMode.enabled', ConfigType.Simple, true); - export const CLIAutoModelEnabled = defineSetting('chat.cli.autoModel.enabled', ConfigType.Simple, true); + export const CLIAutoModelEnabled = defineSetting('chat.cli.autoModel.enabled', ConfigType.Simple, false); export const CLIModelDetailsEnabled = defineSetting('chat.agent.modelDetails.enabled', ConfigType.Simple, true); export const CLIPlanCommandEnabled = defineSetting('chat.cli.planCommand.enabled', ConfigType.Simple, true); export const CLIChatLazyLoadSessionItem = defineSetting('chat.cli.lazyLoadSessionItem.enabled', ConfigType.Simple, true); @@ -662,6 +662,8 @@ export namespace ConfigKey { export const ExecutionSubagentToolEnabled = defineSetting('chat.executionSubagent.enabled', ConfigType.ExperimentBased, false); export const SkillToolEnabled = defineSetting('chat.skillTool.enabled', ConfigType.ExperimentBased, false); + /** When enabled, the get_changed_files tool is available to the agent. */ + export const GetChangedFilesToolEnabled = defineSetting('chat.getChangedFilesTool.enabled', ConfigType.ExperimentBased, false); /** Model to use for the execution subagent */ /** Use the agentic proxy for the execution subagent */ export const ExecutionSubagentUseAgenticProxy = defineSetting('chat.executionSubagent.useAgenticProxy', ConfigType.ExperimentBased, false); diff --git a/extensions/esbuild-common.mts b/extensions/esbuild-common.mts new file mode 100644 index 0000000000000..8747583ef5e8a --- /dev/null +++ b/extensions/esbuild-common.mts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import path from 'node:path'; +import esbuild from 'esbuild'; + +export interface RunConfig { + readonly srcDir: string; + readonly outdir: string; + readonly entryPoints: esbuild.BuildOptions['entryPoints']; + readonly additionalOptions?: Partial; +} + +/** + * Shared build/watch runner for extension esbuild scripts. + */ +export async function runBuild( + config: RunConfig, + baseOptions: esbuild.BuildOptions, + args: string[], + didBuild?: (outDir: string) => unknown, +): Promise { + let outdir = config.outdir; + const outputRootIndex = args.indexOf('--outputRoot'); + if (outputRootIndex >= 0) { + const outputRoot = args[outputRootIndex + 1]; + const outputDirName = path.basename(outdir); + outdir = path.join(outputRoot, outputDirName); + } + + const resolvedOptions: esbuild.BuildOptions = { + ...baseOptions, + entryPoints: config.entryPoints, + outdir, + ...(config.additionalOptions || {}), + }; + + const isWatch = args.indexOf('--watch') >= 0; + if (isWatch) { + const ctx = await esbuild.context(resolvedOptions); + await watchWithParcel(ctx, config.srcDir, () => didBuild?.(outdir)); + } else { + try { + await esbuild.build(resolvedOptions); + await didBuild?.(outdir); + } catch { + process.exit(1); + } + } +} + +// We use @parcel/watcher as it has much lower cpu usage when idle compared to esbuild's watch mode +async function watchWithParcel(ctx: esbuild.BuildContext, srcDir: string, didBuild?: () => Promise | unknown): Promise { + let debounce: ReturnType | undefined; + const rebuild = () => { + if (debounce) { + clearTimeout(debounce); + } + debounce = setTimeout(async () => { + try { + await ctx.cancel(); + const result = await ctx.rebuild(); + if (result.errors.length === 0) { + await didBuild?.(); + } + } catch (error) { + console.error('[watch] build error:', error); + } + }, 100); + }; + + const watcher = await import('@parcel/watcher'); + await watcher.subscribe(srcDir, (_err, _events) => { + rebuild(); + }, { + ignore: ['**/node_modules/**', '**/dist/**', '**/out/**'] + }); + rebuild(); +} diff --git a/extensions/esbuild-extension-common.mts b/extensions/esbuild-extension-common.mts index cfbce204b66b0..5b967a9140e74 100644 --- a/extensions/esbuild-extension-common.mts +++ b/extensions/esbuild-extension-common.mts @@ -5,24 +5,16 @@ /** * @fileoverview Common build script for extensions. */ -import path from 'node:path'; import esbuild from 'esbuild'; +import { runBuild, type RunConfig } from './esbuild-common.mts'; -type BuildOptions = Partial & { - outdir: string; -}; - -interface RunConfig { +interface ExtensionRunConfig extends RunConfig { readonly platform: 'node' | 'browser'; readonly format?: 'cjs' | 'esm'; - readonly srcDir: string; - readonly outdir: string; - readonly entryPoints: string[] | Record | { in: string; out: string }[]; - readonly additionalOptions?: Partial; } -function resolveOptions(config: RunConfig, outdir: string): BuildOptions { - const options: BuildOptions = { +function resolveBaseOptions(config: ExtensionRunConfig): esbuild.BuildOptions { + const options: esbuild.BuildOptions = { platform: config.platform, bundle: true, minify: true, @@ -31,12 +23,9 @@ function resolveOptions(config: RunConfig, outdir: string): BuildOptions { target: ['es2024'], external: ['vscode'], format: config.format ?? 'cjs', - entryPoints: config.entryPoints, - outdir, logOverride: { 'import-is-undefined': 'error', }, - ...(config.additionalOptions || {}), }; if (config.platform === 'node') { @@ -56,47 +45,6 @@ function resolveOptions(config: RunConfig, outdir: string): BuildOptions { return options; } -export async function run(config: RunConfig, args: string[], didBuild?: (outDir: string) => unknown): Promise { - let outdir = config.outdir; - const outputRootIndex = args.indexOf('--outputRoot'); - if (outputRootIndex >= 0) { - const outputRoot = args[outputRootIndex + 1]; - const outputDirName = path.basename(outdir); - outdir = path.join(outputRoot, outputDirName); - } - - const resolvedOptions = resolveOptions(config, outdir); - - const isWatch = args.indexOf('--watch') >= 0; - if (isWatch) { - if (didBuild) { - resolvedOptions.plugins = [ - ...(resolvedOptions.plugins || []), - { - name: 'did-build', setup(pluginBuild) { - pluginBuild.onEnd(async result => { - if (result.errors.length > 0) { - return; - } - - try { - await didBuild(outdir); - } catch (error) { - console.error('didBuild failed:', error); - } - }); - }, - } - ]; - } - const ctx = await esbuild.context(resolvedOptions); - await ctx.watch(); - } else { - try { - await esbuild.build(resolvedOptions); - await didBuild?.(outdir); - } catch { - process.exit(1); - } - } +export async function run(config: ExtensionRunConfig, args: string[], didBuild?: (outDir: string) => unknown): Promise { + return runBuild(config, resolveBaseOptions(config), args, didBuild); } diff --git a/extensions/esbuild-webview-common.mts b/extensions/esbuild-webview-common.mts index 7e7bbe60ec412..294f9f6bd4128 100644 --- a/extensions/esbuild-webview-common.mts +++ b/extensions/esbuild-webview-common.mts @@ -6,77 +6,24 @@ /** * Common build script for extension scripts used in in webviews. */ -import path from 'node:path'; -import esbuild from 'esbuild'; +import { runBuild, type RunConfig } from './esbuild-common.mts'; -export type BuildOptions = Partial & { - readonly entryPoints: esbuild.BuildOptions['entryPoints']; - readonly outdir: string; +const baseOptions = { + bundle: true, + minify: true, + sourcemap: false, + format: 'esm' as const, + platform: 'browser' as const, + target: ['es2024'], + logOverride: { + 'import-is-undefined': 'error', + }, }; export async function run( - config: { - srcDir: string; - outdir: string; - entryPoints: BuildOptions['entryPoints']; - additionalOptions?: Partial; - }, + config: RunConfig, args: string[], didBuild?: (outDir: string) => unknown ): Promise { - let outdir = config.outdir; - const outputRootIndex = args.indexOf('--outputRoot'); - if (outputRootIndex >= 0) { - const outputRoot = args[outputRootIndex + 1]; - const outputDirName = path.basename(outdir); - outdir = path.join(outputRoot, outputDirName); - } - - const resolvedOptions: BuildOptions = { - bundle: true, - minify: true, - sourcemap: false, - format: 'esm', - platform: 'browser', - target: ['es2024'], - entryPoints: config.entryPoints, - outdir, - logOverride: { - 'import-is-undefined': 'error', - }, - ...(config.additionalOptions || {}), - }; - - const isWatch = args.indexOf('--watch') >= 0; - if (isWatch) { - if (didBuild) { - resolvedOptions.plugins = [ - ...(resolvedOptions.plugins || []), - { - name: 'did-build', setup(pluginBuild) { - pluginBuild.onEnd(async result => { - if (result.errors.length > 0) { - return; - } - - try { - await didBuild(outdir); - } catch (error) { - console.error('didBuild failed:', error); - } - }); - }, - } - ]; - } - const ctx = await esbuild.context(resolvedOptions); - await ctx.watch(); - } else { - try { - await esbuild.build(resolvedOptions); - await didBuild?.(outdir); - } catch { - process.exit(1); - } - } + return runBuild(config, baseOptions, args, didBuild); } diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 1e2a3253a1b58..5736e5fbdf37a 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -8,7 +8,8 @@ "license": "MIT", "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "enabledApiProposals": [ - "customEditorDiffs" + "customEditorDiffs", + "documentDiff" ], "engines": { "vscode": "^1.70.0" diff --git a/extensions/markdown-language-features/src/preview/lineDiff.ts b/extensions/markdown-language-features/src/preview/lineDiff.ts index 3a9eda944bd69..a842b4ff64aa5 100644 --- a/extensions/markdown-language-features/src/preview/lineDiff.ts +++ b/extensions/markdown-language-features/src/preview/lineDiff.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import type { API as GitAPI, GitExtension, Repository as GitRepository } from '../../../git/src/api/git'; import type { MarkdownPreviewLineChanges } from '../../types/previewMessaging'; interface LineChanges { @@ -19,17 +18,6 @@ interface LineMappings { readonly modifiedToOriginal: number[]; } -interface GitUriParams { - readonly path: string; - readonly ref: string; - readonly submoduleOf?: string; -} - -interface GitPatch { - readonly patch: string; - readonly isFullRepositoryDiff: boolean; -} - export class MarkdownPreviewLineDiffProvider { readonly #originalDocument: vscode.TextDocument; @@ -77,348 +65,50 @@ export class MarkdownPreviewLineDiffProvider { } async function computeLineChanges(originalDocument: vscode.TextDocument, modifiedDocument: vscode.TextDocument): Promise { - return await computeGitLineChanges(originalDocument, modifiedDocument) - ?? computeContentLineChanges(getDocumentLines(originalDocument), getDocumentLines(modifiedDocument)); -} - -async function computeGitLineChanges(originalDocument: vscode.TextDocument, modifiedDocument: vscode.TextDocument): Promise { - const gitApi = await getGitApi(); - if (!gitApi) { - return undefined; - } - - const originalUri = originalDocument.uri; - const modifiedUri = modifiedDocument.uri; - const originalGitUri = fromGitUri(originalUri); - const modifiedGitUri = fromGitUri(modifiedUri); - const filePath = originalGitUri?.path ?? modifiedGitUri?.path ?? (modifiedUri.scheme === 'file' ? modifiedUri.fsPath : undefined); - if (!filePath || originalGitUri?.submoduleOf || modifiedGitUri?.submoduleOf) { - return undefined; - } - - const repository = gitApi.getRepository(vscode.Uri.file(filePath)); - if (!repository) { - return undefined; - } - - const diff = await getGitPatch(repository, filePath, originalUri, originalGitUri, modifiedUri, modifiedGitUri); - if (!diff) { - return undefined; - } - - const relativePath = diff.isFullRepositoryDiff ? getRepositoryRelativePath(repository.rootUri, filePath) : undefined; - return diff.isFullRepositoryDiff && relativePath === undefined ? undefined : parseGitPatchLineChanges(diff.patch, relativePath, originalDocument.lineCount, modifiedDocument.lineCount); -} - -async function getGitApi(): Promise { - const gitExtension = vscode.extensions.getExtension('vscode.git'); - if (!gitExtension) { - return undefined; - } - - try { - return (gitExtension.isActive ? gitExtension.exports : await gitExtension.activate()).getAPI(1); - } catch { - return undefined; - } -} - -async function getGitPatch( - repository: GitRepository, - filePath: string, - originalUri: vscode.Uri, - originalGitUri: GitUriParams | undefined, - modifiedUri: vscode.Uri, - modifiedGitUri: GitUriParams | undefined, -): Promise { - try { - if (originalGitUri && !modifiedGitUri && modifiedUri.scheme === 'file' && samePath(originalGitUri.path, modifiedUri.fsPath)) { - if (originalGitUri.ref === '~') { - return { patch: await repository.diff(false), isFullRepositoryDiff: true }; - } - if (originalGitUri.ref === 'HEAD') { - return { patch: await repository.diffWithHEAD(filePath), isFullRepositoryDiff: false }; - } - return { patch: await repository.diffWith(originalGitUri.ref, filePath), isFullRepositoryDiff: false }; - } - - if (originalGitUri && modifiedGitUri && samePath(originalGitUri.path, modifiedGitUri.path)) { - if (modifiedGitUri.ref === '') { - return { - patch: originalGitUri.ref === 'HEAD' - ? await repository.diffIndexWithHEAD(filePath) - : await repository.diffIndexWith(originalGitUri.ref, filePath), - isFullRepositoryDiff: false - }; - } - - return { patch: await repository.diffBetween(originalGitUri.ref, modifiedGitUri.ref, filePath), isFullRepositoryDiff: false }; - } + const diff = vscode.workspace.getTextDiff(originalDocument, modifiedDocument, { + ignoreTrimWhitespace: false, + maxComputationTimeMs: 5000, + }); - if (!originalGitUri && modifiedGitUri && originalUri.scheme === 'file' && samePath(originalUri.fsPath, modifiedGitUri.path)) { - return { - patch: modifiedGitUri.ref === 'HEAD' - ? await repository.diffWithHEAD(filePath) - : await repository.diffWith(modifiedGitUri.ref, filePath), - isFullRepositoryDiff: false - }; - } - } catch { - return undefined; - } - - return undefined; -} - -function fromGitUri(uri: vscode.Uri): GitUriParams | undefined { - if (uri.scheme !== 'git') { - return undefined; - } - - try { - const value = JSON.parse(uri.query) as GitUriParams; - return typeof value.path === 'string' && typeof value.ref === 'string' ? value : undefined; - } catch { - return undefined; - } -} - -function getRepositoryRelativePath(rootUri: vscode.Uri, filePath: string): string | undefined { - const root = normalizePath(rootUri.fsPath).replace(/\/+$/, ''); - const file = normalizePath(filePath); - if (file === root) { - return ''; - } - - return file.toLowerCase().startsWith(`${root.toLowerCase()}/`) ? file.slice(root.length + 1) : undefined; -} - -function samePath(a: string, b: string): boolean { - return normalizePath(a).toLowerCase() === normalizePath(b).toLowerCase(); -} - -function normalizePath(value: string): string { - return value.replace(/\\/g, '/'); -} - -function getDocumentLines(document: vscode.TextDocument): string[] { - const lines: string[] = []; - for (let i = 0; i < document.lineCount; ++i) { - lines.push(document.lineAt(i).text); - } - return lines; -} - -function computeContentLineChanges(originalLines: readonly string[], modifiedLines: readonly string[]): LineChanges { - let start = 0; - while (start < originalLines.length && start < modifiedLines.length && originalLines[start] === modifiedLines[start]) { - ++start; - } - - let originalEnd = originalLines.length; - let modifiedEnd = modifiedLines.length; - while (originalEnd > start && modifiedEnd > start && originalLines[originalEnd - 1] === modifiedLines[modifiedEnd - 1]) { - --originalEnd; - --modifiedEnd; - } - - const originalCount = originalEnd - start; - const modifiedCount = modifiedEnd - start; - if (!originalCount && !modifiedCount) { - return createIdentityLineChanges(originalLines.length, modifiedLines.length); - } - - if (originalCount * modifiedCount > 500_000) { - return computeFallbackLineChanges(originalLines, modifiedLines, start, originalEnd, modifiedEnd); - } - - return computeLcsLineChanges(originalLines, modifiedLines, start, originalEnd, modifiedEnd); -} - -function parseGitPatchLineChanges(patch: string, relativePath: string | undefined, originalLineCount: number, modifiedLineCount: number): LineChanges { + const originalLineCount = originalDocument.lineCount; + const modifiedLineCount = modifiedDocument.lineCount; const added: number[] = []; const deleted: number[] = []; const mappings = createEmptyLineMappings(originalLineCount, modifiedLineCount); - const lines = patch.split(/\r?\n/); - let originalLine = 0; - let modifiedLine = 0; - let inHunk = false; - let fileMatches = !relativePath; - let matchedFile = !relativePath; - let oldPath: string | undefined; - let deletedBlockStart: number | undefined; - - const finishFile = () => { - if (fileMatches && matchedFile) { - fillUnchangedLineMappings(mappings, originalLine, originalLineCount, modifiedLine, modifiedLineCount); - } - }; - - for (const line of lines) { - if (line.startsWith('diff --git ')) { - finishFile(); - inHunk = false; - fileMatches = !relativePath; - matchedFile = !relativePath; - originalLine = 0; - modifiedLine = 0; - oldPath = undefined; - deletedBlockStart = undefined; - continue; - } - - if (!inHunk && line.startsWith('--- ')) { - oldPath = parseGitDiffPath(line.slice(4)); - continue; - } - - if (!inHunk && line.startsWith('+++ ')) { - const newPath = parseGitDiffPath(line.slice(4)); - fileMatches = !relativePath || oldPath === relativePath || newPath === relativePath; - matchedFile = matchedFile || fileMatches; - continue; - } - - const hunkMatch = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line); - if (hunkMatch) { - inHunk = true; - const nextOriginalLine = Math.max(0, Number(hunkMatch[1]) - 1); - const nextModifiedLine = Math.max(0, Number(hunkMatch[2]) - 1); - if (fileMatches) { - fillUnchangedLineMappings(mappings, originalLine, nextOriginalLine, modifiedLine, nextModifiedLine); - } - originalLine = nextOriginalLine; - modifiedLine = nextModifiedLine; - deletedBlockStart = undefined; - continue; - } - - if (!inHunk || !fileMatches || !line) { - continue; - } - - switch (line[0]) { - case ' ': - deletedBlockStart = undefined; - mappings.originalToModified[originalLine] = clampLine(modifiedLine, modifiedLineCount); - mappings.modifiedToOriginal[modifiedLine] = clampLine(originalLine, originalLineCount); - ++originalLine; - ++modifiedLine; - break; - case '-': - deletedBlockStart ??= originalLine; - mappings.originalToModified[originalLine] = clampLine(modifiedLine, modifiedLineCount); - deleted.push(originalLine++); - break; - case '+': - mappings.modifiedToOriginal[modifiedLine] = clampLine(deletedBlockStart ?? originalLine, originalLineCount); - added.push(modifiedLine++); - break; - case '\\': - break; - } - } - finishFile(); - fillMissingLineMappings(mappings); - return { added, deleted, ...mappings }; -} - -function parseGitDiffPath(rawPath: string): string | undefined { - if (rawPath === '/dev/null') { - return undefined; - } + let lastOriginalEnd = 0; + let lastModifiedEnd = 0; - const path = rawPath.startsWith('"') && rawPath.endsWith('"') ? rawPath.slice(1, -1) : rawPath; - return path.startsWith('a/') || path.startsWith('b/') ? path.slice(2) : path; -} + for await (const change of diff.changes) { + const origStart = change.originalRange.start.line; + const origEnd = change.originalRange.end.line; + const modStart = change.modifiedRange.start.line; + const modEnd = change.modifiedRange.end.line; -function computeLcsLineChanges(originalLines: readonly string[], modifiedLines: readonly string[], start: number, originalEnd: number, modifiedEnd: number): LineChanges { - const originalCount = originalEnd - start; - const modifiedCount = modifiedEnd - start; - const mappings = createEmptyLineMappings(originalLines.length, modifiedLines.length); - fillUnchangedLineMappings(mappings, 0, start, 0, start); - fillUnchangedLineMappings(mappings, originalEnd, originalLines.length, modifiedEnd, modifiedLines.length); - const lcsLengths: Uint32Array[] = []; - for (let i = 0; i <= originalCount; ++i) { - lcsLengths.push(new Uint32Array(modifiedCount + 1)); - } + // Map unchanged lines before this change + fillUnchangedLineMappings(mappings, lastOriginalEnd, origStart, lastModifiedEnd, modStart); - for (let i = originalCount - 1; i >= 0; --i) { - for (let j = modifiedCount - 1; j >= 0; --j) { - lcsLengths[i][j] = originalLines[start + i] === modifiedLines[start + j] - ? lcsLengths[i + 1][j + 1] + 1 - : Math.max(lcsLengths[i + 1][j], lcsLengths[i][j + 1]); + // Mark deleted and added lines within this change + for (let i = origStart; i < origEnd; ++i) { + deleted.push(i); + mappings.originalToModified[i] = clampLine(modStart, modifiedLineCount); } - } - - const added: number[] = []; - const deleted: number[] = []; - let originalIndex = 0; - let modifiedIndex = 0; - let deletedBlockStart: number | undefined; - let addedBlockStart: number | undefined; - while (originalIndex < originalCount || modifiedIndex < modifiedCount) { - if (originalIndex < originalCount && modifiedIndex < modifiedCount && originalLines[start + originalIndex] === modifiedLines[start + modifiedIndex]) { - deletedBlockStart = undefined; - addedBlockStart = undefined; - mappings.originalToModified[start + originalIndex] = clampLine(start + modifiedIndex, modifiedLines.length); - mappings.modifiedToOriginal[start + modifiedIndex] = clampLine(start + originalIndex, originalLines.length); - ++originalIndex; - ++modifiedIndex; - } else if (modifiedIndex < modifiedCount && (originalIndex === originalCount || lcsLengths[originalIndex][modifiedIndex + 1] >= lcsLengths[originalIndex + 1][modifiedIndex])) { - added.push(start + modifiedIndex); - addedBlockStart ??= start + modifiedIndex; - mappings.modifiedToOriginal[start + modifiedIndex] = clampLine(deletedBlockStart ?? start + originalIndex, originalLines.length); - ++modifiedIndex; - } else { - deleted.push(start + originalIndex); - deletedBlockStart ??= start + originalIndex; - mappings.originalToModified[start + originalIndex] = clampLine(addedBlockStart ?? start + modifiedIndex, modifiedLines.length); - ++originalIndex; + for (let i = modStart; i < modEnd; ++i) { + added.push(i); + mappings.modifiedToOriginal[i] = clampLine(origStart, originalLineCount); } - } - fillMissingLineMappings(mappings); - return { added, deleted, ...mappings }; -} - -function computeFallbackLineChanges(originalLines: readonly string[], modifiedLines: readonly string[], start: number, originalEnd: number, modifiedEnd: number): LineChanges { - const added: number[] = []; - const deleted: number[] = []; - const mappings = createEmptyLineMappings(originalLines.length, modifiedLines.length); - fillUnchangedLineMappings(mappings, 0, start, 0, start); - fillUnchangedLineMappings(mappings, originalEnd, originalLines.length, modifiedEnd, modifiedLines.length); - const sharedCount = Math.min(originalEnd - start, modifiedEnd - start); - for (let i = 0; i < sharedCount; ++i) { - mappings.originalToModified[start + i] = clampLine(start + i, modifiedLines.length); - mappings.modifiedToOriginal[start + i] = clampLine(start + i, originalLines.length); - if (originalLines[start + i] !== modifiedLines[start + i]) { - deleted.push(start + i); - added.push(start + i); - } + lastOriginalEnd = origEnd; + lastModifiedEnd = modEnd; } - for (let i = start + sharedCount; i < originalEnd; ++i) { - deleted.push(i); - mappings.originalToModified[i] = clampLine(modifiedEnd, modifiedLines.length); - } - for (let i = start + sharedCount; i < modifiedEnd; ++i) { - added.push(i); - mappings.modifiedToOriginal[i] = clampLine(originalEnd, originalLines.length); - } + // Map unchanged lines after the last change + fillUnchangedLineMappings(mappings, lastOriginalEnd, originalLineCount, lastModifiedEnd, modifiedLineCount); fillMissingLineMappings(mappings); return { added, deleted, ...mappings }; } -function createIdentityLineChanges(originalLineCount: number, modifiedLineCount: number): LineChanges { - const mappings = createEmptyLineMappings(originalLineCount, modifiedLineCount); - fillUnchangedLineMappings(mappings, 0, originalLineCount, 0, modifiedLineCount); - fillMissingLineMappings(mappings); - return { added: [], deleted: [], ...mappings }; -} - function createEmptyLineMappings(originalLineCount: number, modifiedLineCount: number): LineMappings { return { originalToModified: new Array(originalLineCount), diff --git a/extensions/markdown-language-features/tsconfig.json b/extensions/markdown-language-features/tsconfig.json index b1b21973b6073..dc420d653c812 100644 --- a/extensions/markdown-language-features/tsconfig.json +++ b/extensions/markdown-language-features/tsconfig.json @@ -11,6 +11,7 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.customEditorDiffs.d.ts" + "../../src/vscode-dts/vscode.proposed.customEditorDiffs.d.ts", + "../../src/vscode-dts/vscode.proposed.documentDiff.d.ts" ] } diff --git a/extensions/mermaid-chat-features/package-lock.json b/extensions/mermaid-chat-features/package-lock.json index 718056e9037e0..68890279e7d81 100644 --- a/extensions/mermaid-chat-features/package-lock.json +++ b/extensions/mermaid-chat-features/package-lock.json @@ -1234,9 +1234,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/extensions/theme-defaults/themes/2026-light.json b/extensions/theme-defaults/themes/2026-light.json index 76b2e09f3176b..40ee59eb2bc8f 100644 --- a/extensions/theme-defaults/themes/2026-light.json +++ b/extensions/theme-defaults/themes/2026-light.json @@ -143,6 +143,8 @@ "editorSuggestWidget.foreground": "#202020", "editorSuggestWidget.highlightForeground": "#0069CC", "editorSuggestWidget.selectedBackground": "#0069CC26", + "editorSuggestWidget.selectedForeground": "#202020", + "editorSuggestWidget.selectedIconForeground": "#202020", "editorHoverWidget.background": "#FAFAFD", "editorHoverWidget.border": "#E4E5E6FF", "peekView.border": "#0069CC", diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index 3fdbbead3d0d6..8941e8588c666 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -34,7 +34,9 @@ "terminal.inactiveSelectionBackground": "#E5EBF1", "widget.border": "#d4d4d4", "actionBar.toggledBackground": "#dddddd", - "diffEditor.unchangedRegionBackground": "#f8f8f8" + "diffEditor.unchangedRegionBackground": "#f8f8f8", + "agentsNewSessionButton.border": "#D8D8D8", + "agentsChatInput.border": "#D8D8D8" }, "tokenColors": [ { diff --git a/package-lock.json b/package-lock.json index 4261db48d6239..028214dc95322 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3424,9 +3424,9 @@ } }, "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-SGKnvs5EA+V1spnraYJqum/lEajE0IQ2bVVPC72hFfWjoCfQ6N7iVYxLUGreiE3VFyQWWQBPgXZrRUFnawVvpQ==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-UcEslgHBaHYPAisVQcyARDfps7nKyugmUyXcsfE1HiHcVuvZ4tBJ5C93sG1FDeHWJ9skGQ68ec+Xsx086geAfg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3436,19 +3436,19 @@ "node": ">=16.20.0" }, "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260429.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260429.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20260429.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260429.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20260429.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260429.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20260429.1" + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260506.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260506.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20260506.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260506.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20260506.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260506.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20260506.1" } }, "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-+Rl8iPf+vYKq0fnb8euEOJxxvE/abEOWmhdllQIe+Shd8xhS7UVi+2WunsP1GyH2Ofc+N8rGYz0/dMnhrRYEZA==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-dAd7qG2J508+4CRSuoEA0EUxViIedQ0D+8xKoZiM0EQHCwww8glWYCo72UTjcRZctS3QbJY3PtGSvo3nzL4oVw==", "cpu": [ "arm64" ], @@ -3463,9 +3463,9 @@ } }, "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-be6Y7VVJz+usdI1ifCHy5mcldpxf8KXGYoyIp8w5Rd54zUtvtkYEJJWKzV5/bJt4bsQLLcp1i0vD4KJSr06Tmg==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-1Q7Elncpuiozvx3HCTgFbSxNz2m2FIkO1QW5f15igcZDG3vMW4QglNflmXosc69bzYI7KfYZuaGX3yGzJkGbfg==", "cpu": [ "x64" ], @@ -3480,9 +3480,9 @@ } }, "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-ngN6+qt5bPdp2zzasShoT4UONGXr+tvzHdz4NjuitwhiAF/d70CseXunb4syaudl1a+lJyTHro/ALTC0hRf6vA==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-MfYn1p+aOorZ2Y+7sqLvSoAXPEz/RfKgHfeYO240Udco30B4oapm7Hsq2PsS9Z2Oth/RorGjY0jLP2OhnkY2Ig==", "cpu": [ "arm" ], @@ -3497,9 +3497,9 @@ } }, "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-44amAEH/VxG6K/hrAmhiyOTnwoTzm7bj0ja7d8sV8Iuocv37oUiSB/8OgJLytLqfIh+Q6kipfTwY6Do3jh6THQ==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-Q1W4DHplR2urmtPwoz9tw6XUGWRNXF+CIXJQ8ZpIZFj/OHgvTw8vkYkKFuaEao3lSjTsR4lQe/wL2Xr5K0hxuA==", "cpu": [ "arm64" ], @@ -3514,9 +3514,9 @@ } }, "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-haAOqc0fJCZkt4RDi0/ZQGBdDfpDzr2N+mEcR+FbiYQD3Y00kOK34hXSrjZafO2kq56ZDWunvCaUTCev0fJDbA==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-b+sbLBCIchbrGQNbjIvVN2qd+ieqqp/nghi0n2zOAKGPsfd5wG6ceqxWJKADdBDCohsCCGt//rZccUwFugIsyA==", "cpu": [ "x64" ], @@ -3531,9 +3531,9 @@ } }, "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-J5O0tGVGqOZHbqm9ijRnZ5ADfPqYTjFIwZtYKpQL1yj1dZnUzMszO8P3bnOSfYD//DJhZINQyJzpPJxu29uiwQ==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-l59d8pZjFT7GoWpgCOy6aBcxLSALphA91X4Z/2XHo5HnM0bQ/yJjB7XMeUQZBdk5DZCdZL+sWTfmXLRggm7sFg==", "cpu": [ "arm64" ], @@ -3548,9 +3548,9 @@ } }, "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20260429.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260429.1.tgz", - "integrity": "sha512-/OZ99Hi/32huvZQ5fdqTwqLvZtKC3QrCXmLuKfMyVuBisV/TSd6LhlFQLolvIpr7/E530mnFZ4sXjgDEzVFqAw==", + "version": "7.0.0-dev.20260506.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260506.1.tgz", + "integrity": "sha512-dJDLSzaz2xjRYYmTSfcCepZUi3ITjQSJ6Gk5YGplMF57UmZCAGI+ns4Te/V74IJiQigXqTnyEIGorwsOqhW8gQ==", "cpu": [ "x64" ], @@ -4374,9 +4374,9 @@ "license": "BSD-3-Clause" }, "node_modules/@xmldom/xmldom": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", - "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "dev": true, "license": "MIT", "engines": { @@ -5189,12 +5189,12 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -8222,13 +8222,13 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -8240,16 +8240,6 @@ "express": ">= 4.11" } }, - "node_modules/express-rate-limit/node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/express/node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -11669,13 +11659,10 @@ } }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", "engines": { "node": ">= 12" } @@ -12641,11 +12628,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" - }, "node_modules/jschardet": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz", @@ -17376,11 +17358,12 @@ } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", + "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", + "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -17508,7 +17491,9 @@ "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "optional": true }, "node_modules/ssh2": { "version": "1.17.0", diff --git a/remote/package-lock.json b/remote/package-lock.json index 83eabd2b0bfcd..9a4258aae75c8 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -1119,13 +1119,10 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", "engines": { "node": ">= 12" } @@ -1154,11 +1151,6 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" - }, "node_modules/jschardet": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz", @@ -1531,11 +1523,12 @@ } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", + "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", + "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -1556,11 +1549,6 @@ "node": ">= 14" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" - }, "node_modules/ssh2": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", diff --git a/src/bootstrap-meta.ts b/src/bootstrap-meta.ts index b1d8213d89342..1e5affb0a9754 100644 --- a/src/bootstrap-meta.ts +++ b/src/bootstrap-meta.ts @@ -5,7 +5,6 @@ import { createRequire } from 'node:module'; import type { IProductConfiguration } from './vs/base/common/product.js'; -import type { INodeProcess } from './vs/base/common/platform.js'; const require = createRequire(import.meta.url); @@ -19,30 +18,6 @@ if (pkgObj['BUILD_INSERT_PACKAGE_CONFIGURATION']) { pkgObj = require('../package.json'); // Running out of sources } -// Load sub files -if ((process as INodeProcess).isEmbeddedApp) { - // Preserve the parent VS Code's policy identity before the - // embedded app overrides win32RegValueName / darwinBundleIdentifier. - productObj.parentPolicyConfig = { - win32RegValueName: productObj.win32RegValueName, - darwinBundleIdentifier: productObj.darwinBundleIdentifier, - urlProtocol: productObj.urlProtocol, - }; - - try { - const productSubObj = require('../product.sub.json'); - if (productObj.embedded && productSubObj.embedded) { - Object.assign(productObj.embedded, productSubObj.embedded); - delete productSubObj.embedded; - } - Object.assign(productObj, productSubObj); - } catch (error) { /* ignore */ } - try { - const pkgSubObj = require('../package.sub.json'); - pkgObj = Object.assign(pkgObj, pkgSubObj); - } catch (error) { /* ignore */ } -} - let productOverridesObj = {}; if (process.env['VSCODE_DEV']) { try { diff --git a/src/typings/electron-cross-app-ipc.d.ts b/src/typings/electron-cross-app-ipc.d.ts deleted file mode 100644 index 4a184909159bb..0000000000000 --- a/src/typings/electron-cross-app-ipc.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * Type definitions for Electron's crossAppIPC module (custom build). - * - * This module provides secure IPC between an Electron host app and an - * embedded Electron app (MiniApp) within nested bundles. Communication - * is authenticated via code-signature verification (macOS: Mach ports, - * Windows: named pipes). - */ - -declare namespace Electron { - - interface CrossAppIPCMessageEvent { - /** The deserialized message data sent by the peer app. */ - data: any; - /** Array of transferred MessagePortMain objects (if any). */ - ports: Electron.MessagePortMain[]; - } - - type CrossAppIPCDisconnectReason = - | 'peer-disconnected' - | 'handshake-failed' - | 'connection-failed' - | 'connection-timeout'; - - interface CrossAppIPC extends NodeJS.EventEmitter { - on(event: 'connected', listener: () => void): this; - once(event: 'connected', listener: () => void): this; - removeListener(event: 'connected', listener: () => void): this; - - on(event: 'message', listener: (messageEvent: CrossAppIPCMessageEvent) => void): this; - once(event: 'message', listener: (messageEvent: CrossAppIPCMessageEvent) => void): this; - removeListener(event: 'message', listener: (messageEvent: CrossAppIPCMessageEvent) => void): this; - - on(event: 'disconnected', listener: (reason: CrossAppIPCDisconnectReason) => void): this; - once(event: 'disconnected', listener: (reason: CrossAppIPCDisconnectReason) => void): this; - removeListener(event: 'disconnected', listener: (reason: CrossAppIPCDisconnectReason) => void): this; - - connect(): void; - close(): void; - postMessage(message: any, transferables?: Electron.MessagePortMain[]): void; - readonly connected: boolean; - readonly isServer: boolean; - } - - interface CrossAppIPCModule { - createCrossAppIPC(): CrossAppIPC; - } - - namespace Main { - const crossAppIPC: CrossAppIPCModule | undefined; - } - - namespace CrossProcessExports { - const crossAppIPC: CrossAppIPCModule | undefined; - } -} diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 80da7321ed2fd..0b10da5ce44d3 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, CancellationTokenSource } from './cancellation.js'; -import { BugIndicatingError, CancellationError } from './errors.js'; +import { BugIndicatingError, CancellationError, isCancellationError } from './errors.js'; import { Emitter, Event } from './event.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, isDisposable, MutableDisposable, toDisposable } from './lifecycle.js'; import { extUri as defaultExtUri, IExtUri } from './resources.js'; @@ -116,6 +116,13 @@ export function raceCancellationError(promise: Promise, token: Cancellatio }); } +export function rejectIfNotCanceled(err: unknown): undefined { + if (isCancellationError(err)) { + return undefined; + } + return Promise.reject(err) as never; +} + /** * Wraps a cancellable promise such that it is no cancellable. Can be used to * avoid issues with shared promises that would normally be returned as diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index a924e98352669..0115e59d4180e 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -6,7 +6,7 @@ // This is a facade for the observable implementation. Only import from here! export { observableValueOpts } from './observables/observableValueOpts.js'; -export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges, autorunIterableDelta, autorunPerKeyedItem, autorunSelfDisposable } from './reactions/autorun.js'; +export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges, autorunIterableDelta, autorunPerKeyedItem, autorunSelfDisposable, registerAutorunSelfDisposable } from './reactions/autorun.js'; export { type IObservable, type IObservableWithChange, type IObserver, type IReader, type ISettable, type IReaderWithStore, type ISettableObservable, type ITransaction } from './base.js'; export { disposableObservableValue } from './observables/observableValue.js'; export { derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore } from './observables/derived.js'; diff --git a/src/vs/base/common/observableInternal/reactions/autorun.ts b/src/vs/base/common/observableInternal/reactions/autorun.ts index dad3cbdd20329..4df3da54541f7 100644 --- a/src/vs/base/common/observableInternal/reactions/autorun.ts +++ b/src/vs/base/common/observableInternal/reactions/autorun.ts @@ -232,6 +232,7 @@ export interface IReaderWithDispose extends IReaderWithStore, IDisposable { } /** * An autorun with a `dispose()` method on its `reader` which cancels the autorun. * It it safe to call `dispose()` synchronously. + * @deprecated Use autorunSelfDisposable2 */ export function autorunSelfDisposable(fn: (reader: IReaderWithDispose) => void, debugLocation = DebugLocation.ofCaller()): IDisposable { let ar: IDisposable | undefined; @@ -256,3 +257,38 @@ export function autorunSelfDisposable(fn: (reader: IReaderWithDispose) => void, return ar; } + + +/** + * An autorun with a `dispose()` method on its `reader` which cancels the autorun. + * It it safe to call `dispose()` synchronously. + * TODO@hediet/copilot: rename to delete autorunSelfDisposable, and rename autorunSelfDisposable2 to autorunSelfDisposable. + */ +export function registerAutorunSelfDisposable(store: DisposableStore, fn: (reader: IReaderWithDispose) => void, debugLocation = DebugLocation.ofCaller()): void { + let ar: IDisposable | undefined; + let disposeSync = false; + + // eslint-disable-next-line prefer-const + ar = autorun(reader => { + fn({ + delayedStore: reader.delayedStore, + store: reader.store, + readObservable: reader.readObservable.bind(reader), + dispose: () => { + if (!ar) { + // dispose on first run, ar is not initialized yet. + disposeSync = true; + } else { + // dispose on reaction, ar is already registered. + store.delete(ar); + } + } + }); + }, debugLocation); + + if (disposeSync) { + ar.dispose(); + } else { + store.add(ar); + } +} diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index 91831fa4b78b1..3013e09489b22 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -44,7 +44,6 @@ export interface INodeProcess { chrome?: string; }; type?: string; - isEmbeddedApp?: boolean; cwd: () => string; } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index f594d9451aefa..cc90fc6f0694d 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -79,7 +79,6 @@ export interface IProductConfiguration { readonly win32RegValueName?: string; readonly win32NameVersion?: string; readonly win32VersionedUpdate?: boolean; - readonly win32SiblingExeBasename?: string; readonly win32ContextMenu?: { readonly [arch: string]: { readonly clsid: string } }; readonly applicationName: string; readonly embedderIdentifier?: string; @@ -224,7 +223,6 @@ export interface IProductConfiguration { readonly 'editSessions.store'?: Omit; readonly darwinUniversalAssetId?: string; readonly darwinBundleIdentifier?: string; - readonly darwinSiblingBundleIdentifier?: string; readonly profileTemplatesUrl?: string; readonly commonlyUsedSettings?: string[]; @@ -242,8 +240,6 @@ export interface IProductConfiguration { readonly onboardingKeymaps?: readonly IProductOnboardingKeymap[]; readonly onboardingThemes?: readonly IProductOnboardingTheme[]; - readonly embedded?: IEmbeddedProductConfiguration; - /** * When running as an embedded app, the parent VS Code's policy * identity (win32RegValueName / darwinBundleIdentifier) so that @@ -270,22 +266,6 @@ export interface IProductOnboardingTheme { readonly type: 'dark' | 'light' | 'hcDark' | 'hcLight'; } -export type IEmbeddedProductConfiguration = Pick; - export interface ITunnelApplicationConfig { authenticationProviders: IStringDictionary<{ scopes: string[] }>; editorWebUrl: string; diff --git a/src/vs/code/electron-main/agentsLastRunningTracker.ts b/src/vs/code/electron-main/agentsLastRunningTracker.ts index d62f05767a71b..42abc6f391bd8 100644 --- a/src/vs/code/electron-main/agentsLastRunningTracker.ts +++ b/src/vs/code/electron-main/agentsLastRunningTracker.ts @@ -3,20 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from '../../base/common/buffer.js'; -import { Disposable } from '../../base/common/lifecycle.js'; import { Schemas } from '../../base/common/network.js'; import { joinPath } from '../../base/common/resources.js'; import { URI } from '../../base/common/uri.js'; -import { ICrossAppIPCService } from '../../platform/crossAppIpc/electron-main/crossAppIpcService.js'; -import { IFileService } from '../../platform/files/common/files.js'; +import { FileOperationResult, IFileService, toFileOperationResult } from '../../platform/files/common/files.js'; import { ILogService } from '../../platform/log/common/log.js'; /** * Marker file written by the Agents sub-application into the host VS Code's - * user-data directory while the Agents app is running. After a future update - * removes the sub-application, the host VS Code can detect this marker on - * first launch and restore the user's last-known windows state. + * user-data directory while it was running. After the update which removes + * the sub-application, the host VS Code detects this marker on first launch, + * restores the appropriate windows and removes the marker. */ export const AGENTS_LAST_RUNNING_MARKER_FILE_NAME = 'agentsLastRunning.json'; @@ -27,38 +24,35 @@ export interface IAgentsLastRunningMarker { readonly writtenAt: number; } -export class AgentsLastRunningTracker extends Disposable { - - private readonly markerResource: URI; - - constructor( - hostUserRoamingDataHome: URI, - @ICrossAppIPCService private readonly crossAppIPCService: ICrossAppIPCService, - @IFileService private readonly fileService: IFileService, - @ILogService private readonly logService: ILogService, - ) { - super(); - - this.markerResource = joinPath(hostUserRoamingDataHome.with({ scheme: Schemas.file }), AGENTS_LAST_RUNNING_MARKER_FILE_NAME); - - // Write marker now and refresh whenever the host's liveness changes - // so the recorded `vscodeRunning` snapshot reflects the latest state. - this.writeMarker(); - this._register(this.crossAppIPCService.onDidConnect(() => this.writeMarker())); - this._register(this.crossAppIPCService.onDidDisconnect(() => this.writeMarker())); +export async function tryConsumeAgentsLastRunningMarker(userRoamingDataHome: URI, fileService: IFileService, logService: ILogService): Promise { + const markerResource = joinPath(userRoamingDataHome.with({ scheme: Schemas.file }), AGENTS_LAST_RUNNING_MARKER_FILE_NAME); + + let parsed: IAgentsLastRunningMarker | undefined; + try { + const contents = await fileService.readFile(markerResource); + const json = JSON.parse(contents.value.toString()) as Partial; + if (typeof json.agentsRunning === 'boolean' && typeof json.vscodeRunning === 'boolean') { + parsed = { + agentsRunning: json.agentsRunning, + vscodeRunning: json.vscodeRunning, + writtenAt: typeof json.writtenAt === 'number' ? json.writtenAt : 0 + }; + } + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + logService.warn(`[agents] failed to read last-running marker at ${markerResource.fsPath}: ${error}`); + } + return undefined; } - private async writeMarker(): Promise { - const payload: IAgentsLastRunningMarker = { - agentsRunning: true, - vscodeRunning: this.crossAppIPCService.connected, - writtenAt: Date.now() - }; - try { - await this.fileService.writeFile(this.markerResource, VSBuffer.fromString(JSON.stringify(payload))); - this.logService.trace(`[agents] wrote last-running marker at ${this.markerResource.fsPath} (vscodeRunning=${payload.vscodeRunning})`); - } catch (error) { - this.logService.warn(`[agents] failed to write last-running marker at ${this.markerResource.fsPath}: ${error}`); - } + try { + await fileService.del(markerResource); + } catch (error) { + logService.warn(`[agents] failed to delete last-running marker at ${markerResource.fsPath}: ${error}`); } + + logService.info(`[agents] consumed last-running marker at ${markerResource.fsPath} (agentsRunning=${parsed?.agentsRunning}, vscodeRunning=${parsed?.vscodeRunning})`); + + return parsed; } + diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 75c1a577f6041..48eeec065d58a 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -6,8 +6,8 @@ import { app, Details, GPUFeatureStatus, powerMonitor, protocol, session, Session, systemPreferences, WebFrameMain } from 'electron'; import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from '../../base/node/unc.js'; import { validatedIpcMain } from '../../base/parts/ipc/electron-main/ipcMain.js'; -import { execFile } from 'child_process'; import { hostname, release } from 'os'; +import { exec } from 'child_process'; import { initWindowsVersionInfo } from '../../base/node/windowsVersion.js'; import { VSBuffer } from '../../base/common/buffer.js'; import { toErrorMessage } from '../../base/common/errorMessage.js'; @@ -17,7 +17,7 @@ import { getPathLabel } from '../../base/common/labels.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../base/common/lifecycle.js'; import { Schemas, VSCODE_AUTHORITY } from '../../base/common/network.js'; import { join, posix } from '../../base/common/path.js'; -import { INodeProcess, IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows, OS } from '../../base/common/platform.js'; +import { IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows, OS } from '../../base/common/platform.js'; import { assertType } from '../../base/common/types.js'; import { URI } from '../../base/common/uri.js'; import { generateUuid } from '../../base/common/uuid.js'; @@ -83,10 +83,7 @@ import { ITelemetryServiceConfig, TelemetryService } from '../../platform/teleme import { getPiiPathsFromEnvironment, getTelemetryLevel, isInternalTelemetry, NullTelemetryService, supportsTelemetry } from '../../platform/telemetry/common/telemetryUtils.js'; import { IUpdateService } from '../../platform/update/common/update.js'; import { UpdateChannel } from '../../platform/update/common/updateIpc.js'; -import { AbstractUpdateService } from '../../platform/update/electron-main/abstractUpdateService.js'; -import { CrossAppUpdateCoordinator } from '../../platform/update/electron-main/crossAppUpdateIpc.js'; import { NotAvailableUpdateDialog } from '../../platform/update/electron-main/notAvailableUpdateDialog.js'; -import { MacOSCrossAppSecretSharing } from '../../platform/secrets/electron-main/macOSCrossAppSecretSharing.js'; import { DarwinUpdateService } from '../../platform/update/electron-main/updateService.darwin.js'; import { LinuxUpdateService } from '../../platform/update/electron-main/updateService.linux.js'; import { SnapUpdateService } from '../../platform/update/electron-main/updateService.snap.js'; @@ -145,8 +142,7 @@ import { IWebContentExtractorService } from '../../platform/webContentExtractor/ import { NativeWebContentExtractorService } from '../../platform/webContentExtractor/electron-main/webContentExtractorService.js'; import { AgentNetworkFilterService, IAgentNetworkFilterService } from '../../platform/networkFilter/common/networkFilterService.js'; import { ITerminalSandboxService, NullTerminalSandboxService } from '../../platform/sandbox/common/terminalSandboxService.js'; -import { CrossAppIPCService, ICrossAppIPCService } from '../../platform/crossAppIpc/electron-main/crossAppIpcService.js'; -import { AgentsLastRunningTracker } from './agentsLastRunningTracker.js'; +import { tryConsumeAgentsLastRunningMarker } from './agentsLastRunningTracker.js'; import ErrorTelemetry from '../../platform/telemetry/electron-main/errorTelemetry.js'; /** @@ -581,6 +577,13 @@ export class CodeApplication extends Disposable { this.logService.error(error); } + // One-time cleanup of the previous Agents sub-application on macOS (Insiders only). + // The agents experience now ships as a window of VS Code itself, so any leftover + // Dock pinned entry and Launch Services registration of the old sub-app should be removed. + if (isMacintosh && this.productService.quality === 'insider') { + this.cleanupAgentsApplication(); + } + // Main process server (electron IPC based) const mainProcessElectronServer = new ElectronIPCServer(); Event.once(this.lifecycleMainService.onWillShutdown)(e => { @@ -757,11 +760,6 @@ export class CodeApplication extends Disposable { const windowOpenable = this.getWindowOpenableFromProtocolUrl(protocolUrl.uri); if (windowOpenable) { - if ((process as INodeProcess).isEmbeddedApp) { - this.logService.trace('app#resolveInitialProtocolUrls() agents app skipping window openable:', protocolUrl.uri.toString(true)); - continue; // Agents app: skip all window openables (file/folder/workspace) - } - if (await this.shouldBlockOpenable(windowOpenable, windowsMainService, dialogMainService)) { this.logService.trace('app#resolveInitialProtocolUrls() protocol url was blocked:', protocolUrl.uri.toString(true)); @@ -907,27 +905,6 @@ export class CodeApplication extends Disposable { private async handleProtocolUrl(windowsMainService: IWindowsMainService, dialogMainService: IDialogMainService, urlService: IURLService, uri: URI, options?: IOpenURLOptions): Promise { this.logService.trace('app#handleProtocolUrl():', uri.toString(true), options); - // Agents app: ensure the agents window is open, then let other handlers process the URL. - if ((process as INodeProcess).isEmbeddedApp) { - this.logService.trace('app#handleProtocolUrl() agents app handling protocol URL:', uri.toString(true)); - - // Skip window openables (file/folder/workspace) for security - const windowOpenable = this.getWindowOpenableFromProtocolUrl(uri); - if (windowOpenable) { - this.logService.trace('app#handleProtocolUrl() agents app skipping window openable:', uri.toString(true)); - return true; - } - - // Ensure agents window is open to receive the URL - const windows = await windowsMainService.openAgentsWindow({ context: OpenContext.LINK, cli: this.environmentMainService.args }); - const window = windows.at(0); - window?.focus(); - await window?.ready(); - - // Return false to let subsequent handlers (e.g., URLHandlerChannelClient) forward the URL - return false; - } - // Support 'workspace' URLs (https://github.com/microsoft/vscode/issues/124263) if (uri.scheme === this.productService.urlProtocol && uri.path === 'workspace') { uri = uri.with({ @@ -1094,9 +1071,6 @@ export class CodeApplication extends Disposable { // Encryption services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService)); - // Cross-app IPC - services.set(ICrossAppIPCService, new SyncDescriptor(CrossAppIPCService)); - // Browser View services.set(IBrowserViewMainService, new SyncDescriptor(BrowserViewMainService, undefined, false /* proxied to other processes */)); services.set(IBrowserViewGroupMainService, new SyncDescriptor(BrowserViewGroupMainService, undefined, false /* proxied to other processes */)); @@ -1244,46 +1218,14 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('userDataProfiles', userDataProfilesService); sharedProcessClient.then(client => client.registerChannel('userDataProfiles', userDataProfilesService)); - // Initialize cross-app IPC on supported platforms so all consumers - // (update coordination, secret sharing, etc.) share one connection. - const crossAppIPCService = accessor.get(ICrossAppIPCService); - if (isMacintosh || isWindows) { - crossAppIPCService.initialize(); - } - - // Update (with cross-app coordination on macOS/Windows where crossAppIPC is available) - const localUpdateService = accessor.get(IUpdateService); - let effectiveUpdateService: IUpdateService = localUpdateService; - const isInsiderOrExploration = this.productService.quality === 'insider' || this.productService.quality === 'exploration'; - if ((isMacintosh || isWindows) && isInsiderOrExploration) { - const updateCoordinator = this._register(new CrossAppUpdateCoordinator( - localUpdateService as AbstractUpdateService, - this.logService, - this.lifecycleMainService, - crossAppIPCService, - )); - effectiveUpdateService = updateCoordinator; - } - const updateChannel = new UpdateChannel(effectiveUpdateService); + // Update + const updateService = accessor.get(IUpdateService); + const updateChannel = new UpdateChannel(updateService); mainProcessElectronServer.registerChannel('update', updateChannel); // Show a native "no updates available" dialog from the focused app's main // process to avoid double dialogs across apps and ensure a native dialog. - this._register(new NotAvailableUpdateDialog(effectiveUpdateService, accessor.get(IDialogMainService))); - - // Cross-app secret sharing (macOS only, demand-driven) - if (isMacintosh) { - this._register(new MacOSCrossAppSecretSharing( - accessor.get(IStorageMainService), - accessor.get(IEncryptionMainService), - accessor.get(IStateService), - this.logService, - this.environmentMainService, - accessor.get(ILaunchMainService), - this.lifecycleMainService, - crossAppIPCService, - )); - } + this._register(new NotAvailableUpdateDialog(updateService, accessor.get(IDialogMainService))); // Metered Connection const meteredConnectionChannel = new MeteredConnectionChannel(accessor.get(IMeteredConnectionService) as MeteredConnectionMainService); @@ -1394,16 +1336,8 @@ export class CodeApplication extends Disposable { const context = isLaunchedFromCli(process.env) ? OpenContext.CLI : OpenContext.DESKTOP; const args = this.environmentMainService.args; - // If launched solely for cross-app secret sharing, don't open any windows - if (args['share-secrets-with-agents-app']) { - const hasOtherArgs = args._.length > 0 || args['folder-uri'] || args['file-uri']; - if (!hasOtherArgs) { - return []; - } - } - // Handle agents window first based on context - if ((process as INodeProcess).isEmbeddedApp || (args['agents'] && this.productService.quality !== 'stable')) { + if ((args['agents'] && this.productService.quality !== 'stable')) { return windowsMainService.openAgentsWindow({ context, cli: args, @@ -1411,6 +1345,21 @@ export class CodeApplication extends Disposable { }); } + const agentsLastRunning = this.productService.quality !== 'stable' + ? await tryConsumeAgentsLastRunningMarker(this.environmentMainService.userRoamingDataHome, this.fileService, this.logService) + : undefined; + if (agentsLastRunning?.agentsRunning) { + const agentsWindows = await windowsMainService.openAgentsWindow({ + context, + cli: args, + initialStartup: true + }); + if (!agentsLastRunning.vscodeRunning) { + return agentsWindows; + } + // Otherwise also restore the editor windows below. + } + // Then check for windows from protocol links to open if (initialProtocolUrls) { @@ -1699,16 +1648,6 @@ export class CodeApplication extends Disposable { }); } - // Agents app: write a marker into the host VS Code's user-data dir so - // that, after a future update which removes the sub-application, the - // host VS Code can detect that the Agents app was running and restore - // the appropriate windows on next launch. - if ((process as INodeProcess).isEmbeddedApp) { - const hostUserRoamingDataHome = this.environmentMainService.parentAppUserRoamingDataHome; - if (hostUserRoamingDataHome) { - this._register(instantiationService.createInstance(AgentsLastRunningTracker, hostUserRoamingDataHome)); - } - } } private async installMutex(): Promise { @@ -1790,33 +1729,40 @@ export class CodeApplication extends Disposable { // Validate Device ID is up to date (delay this as it has shown significant perf impact) // Refs: https://github.com/microsoft/vscode/issues/234064 validateDevDeviceId(this.stateService, this.logService); - - // macOS: eagerly register the embedded app with Launch Services - this.registerEmbeddedAppWithLaunchServices(); } - private registerEmbeddedAppWithLaunchServices(): void { - if (!isMacintosh || (process as INodeProcess).isEmbeddedApp || !this.productService.embedded?.nameShort || this.productService.quality === 'stable') { + private cleanupAgentsApplication(): void { + const cleanupKey = 'macAgentsSubAppCleanup.v1'; + if (this.stateService.getItem(cleanupKey, false)) { return; } - const stateKey = 'launchServices.registeredEmbeddedApp'; - const currentVersion = this.productService.version; - if (this.stateService.getItem(stateKey) === currentVersion) { - this.logService.trace('Embedded app already registered with Launch Services for this version, skipping.'); - return; - } - - // appRoot points to Contents/Resources/app on macOS - const embeddedAppPath = join(this.environmentMainService.appRoot, '..', '..', 'Applications', `${this.productService.embedded.nameLong}.app`); - const lsregister = '/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister'; - this.logService.trace('Registering embedded app with Launch Services:', embeddedAppPath); - const child = execFile(lsregister, ['-f', embeddedAppPath], { timeout: 30_000 }, (error) => { + const bundleId = 'com.microsoft.VSCodeAgentsInsiders'; + const script = [ + `plist="$HOME/Library/Preferences/com.apple.dock.plist"`, + `[ -f "$plist" ] || exit 0`, + `n=$(/usr/libexec/PlistBuddy -c "Print :persistent-apps" "$plist" 2>/dev/null | grep -c "^ Dict {")`, + `changed=0`, + `i=$((n-1))`, + `while [ $i -ge 0 ]; do`, + ` bid=$(/usr/libexec/PlistBuddy -c "Print :persistent-apps:$i:tile-data:bundle-identifier" "$plist" 2>/dev/null)`, + ` if [ "$bid" = "${bundleId}" ]; then`, + ` /usr/libexec/PlistBuddy -c "Delete :persistent-apps:$i" "$plist" 2>/dev/null`, + ` changed=1`, + ` fi`, + ` i=$((i-1))`, + `done`, + `[ "$changed" = "1" ] && /usr/bin/killall -HUP Dock`, + `exit 0` + ].join('\n'); + + const child = exec(script, { timeout: 10_000, killSignal: 'SIGKILL' }, (error, _stdout, stderr) => { if (error) { - this.logService.error('Failed to register embedded app with Launch Services:', error.message); - } else { - this.stateService.setItem(stateKey, currentVersion); + this.logService.warn(`[agents] legacy sub-app cleanup failed: ${error.message}${stderr ? ` (${stderr.trim()})` : ''}`); + return; } + this.stateService.setItem(cleanupKey, true); + this.logService.info('[agents] legacy sub-app cleanup completed'); }); child.unref(); } diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index d12791a2f5d80..e390b34f4f6f5 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -20,7 +20,6 @@ import { addArg, parseCLIProcessArgv } from '../../platform/environment/node/arg import { getStdinFilePath, hasStdinWithoutTty, readFromStdin, stdinDataListener } from '../../platform/environment/node/stdin.js'; import { createWaitMarkerFileSync } from '../../platform/environment/node/wait.js'; import product from '../../platform/product/common/product.js'; -import { resolveSiblingWindowsExePath } from '../../platform/native/node/siblingApp.js'; import { CancellationTokenSource } from '../../base/common/cancellation.js'; import { isUNC, randomPath } from '../../base/common/extpath.js'; import { Utils } from '../../platform/profiling/common/profiling.js'; @@ -493,18 +492,8 @@ export async function main(argv: string[]): Promise { options['stdio'] = ['ignore', 'pipe', 'ignore']; // restore ability to see output when --status is used } - // Figure out the app to launch: with --agents we try to launch the embedded app on Windows - let execToLaunch = process.execPath; - if (isWindows && args.agents) { - const siblingExe = resolveSiblingWindowsExePath(product); - if (siblingExe) { - execToLaunch = siblingExe; - argv = argv.filter(arg => arg !== '--agents'); - } - } - // We spawn the resolved executable directly - child = spawn(execToLaunch, argv.slice(2), options); + child = spawn(process.execPath, argv.slice(2), options); } else { // On macOS, we spawn using the open command to obtain behavior // similar to if the app was launched from the dock @@ -518,14 +507,7 @@ export async function main(argv: string[]): Promise { // This way, Mac does not automatically try to foreground the new instance, which causes // focusing issues when the new instance only sends data to a previous instance and then closes. const spawnArgs = ['-n', '-g']; - - // Figure out the app to launch: with --agents we try to launch the embedded app - if (args.agents && product.darwinSiblingBundleIdentifier) { - spawnArgs.push('-b', product.darwinSiblingBundleIdentifier); - argv = argv.filter(arg => arg !== '--agents'); - } else { - spawnArgs.push('-a', process.execPath); // -a opens the given application. - } + spawnArgs.push('-a', process.execPath); // -a opens the given application. if (args.verbose || args.status) { spawnArgs.push('--wait-apps'); // `open --wait-apps`: blocks until the launched app is closed (even if they were already running) diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts index 2b233fc81c335..9d9dfc2e7c12c 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RunOnceScheduler } from '../../../../base/common/async.js'; +import { rejectIfNotCanceled, RunOnceScheduler } from '../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, IReader, ISettableObservable, ITransaction, autorun, autorunWithStore, derived, observableSignal, observableSignalFromEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js'; +import { IObservable, IReader, ISettableObservable, ITransaction, autorun, derived, observableSignal, observableSignalFromEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js'; import { IDiffProviderFactoryService } from './diffProviderFactoryService.js'; import { filterWithPrevious } from './utils.js'; import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; @@ -261,8 +261,9 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo debouncer.schedule(); })); - this._register(autorunWithStore(async (reader, store) => { + this._register(autorun(async (reader) => { /** @description compute diff */ + const store = reader.store; // So that they get recomputed when these settings change this._options.hideUnchangedRegionsMinimumLineCount.read(reader); @@ -294,9 +295,9 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo ignoreTrimWhitespace: this._options.ignoreTrimWhitespace.read(reader), maxComputationTimeMs: this._options.maxComputationTimeMs.read(reader), computeMoves: this._options.showMoves.read(reader), - }, this._cancellationTokenSource.token); + }, this._cancellationTokenSource.token).catch(rejectIfNotCanceled); - if (this._cancellationTokenSource.token.isCancellationRequested) { + if (!result || this._cancellationTokenSource.token.isCancellationRequested) { return; } if (model.original.isDisposed() || model.modified.isDisposed()) { @@ -348,7 +349,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo } public async waitForDiff(): Promise { - await waitForState(this.isDiffUpToDate, s => s); + await waitForState(this.isDiffUpToDate, s => s, undefined, this._cancellationTokenSource.token).catch(rejectIfNotCanceled); } public serializeState(): SerializedState { diff --git a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts index d7192151b3460..bd6af43721efe 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts @@ -129,7 +129,7 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject< this._lastScrollTop = -1; this._isSettingScrollTop = false; - const btn = new Button(this._elements.collapseButton, {}); + const btn = this._register(new Button(this._elements.collapseButton, {})); this._register(autorun(reader => { btn.element.className = ''; diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts index 8d0acd345b8b1..01cf119741502 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable, ITransaction, ObservablePromise, ObservableResolvedPromise, constObservable, derived, derivedObservableWithWritableCache, mapObservableArrayCached, observableFromValueWithChangeEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js'; -import { timeout } from '../../../../base/common/async.js'; +import { rejectIfNotCanceled, timeout } from '../../../../base/common/async.js'; import { URI } from '../../../../base/common/uri.js'; import { ContextKeyValue } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -17,6 +17,7 @@ import { DiffEditorOptions } from '../diffEditor/diffEditorOptions.js'; import { DiffEditorViewModel } from '../diffEditor/diffEditorViewModel.js'; import { RefCounted } from '../diffEditor/utils.js'; import { IDocumentDiffItem, IMultiDiffEditorModel } from './model.js'; +import { cancelOnDispose } from '../../../../base/common/cancellation.js'; export class MultiDiffEditorViewModel extends Disposable { private readonly _documents: IObservable[] | 'loading'>; @@ -186,8 +187,8 @@ export class DocumentDiffItemViewModel extends Disposable { this.waitForInitialDiffOr1s = new ObservablePromise( Promise.race([ - this.diffEditorViewModel.waitForDiff(), - timeout(1000), + this.diffEditorViewModel.waitForDiff().catch(rejectIfNotCanceled), + timeout(1000, cancelOnDispose(this._store)).catch(rejectIfNotCanceled), ]) ); } diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 9a3dfa6590812..8c645030f8d6a 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -483,6 +483,12 @@ export interface IActionListOptions { */ readonly inlineDescription?: boolean; + /** + * Height (in px) used for action items that have a `detail` line. + * Defaults to 48. + */ + readonly detailItemHeight?: number; + /** * When true, the group title is shown on the first item of each group * in the description area (aligned to the right). @@ -999,7 +1005,7 @@ export class ActionListWidget extends Disposable { case ActionListItemKind.Separator: return this._separatorLineHeight; default: - return item.detail ? 48 : this._actionLineHeight; + return item.detail ? (this._options?.detailItemHeight ?? 48) : this._actionLineHeight; } } diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 7b79a90b33700..8f49370d8497f 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -55,7 +55,7 @@ /** Styles for each row in the list element **/ .action-widget .monaco-list .monaco-list-row { - padding: 0 12px 0 8px; + padding: 0 12px 0 6px; white-space: nowrap; cursor: pointer; touch-action: none; @@ -151,7 +151,7 @@ .action-widget .monaco-list-row.action .detail { order: 99; width: 100%; - padding-left: 20px; + padding-left: 18px; font-size: 11px; line-height: 14px; color: var(--vscode-descriptionForeground); @@ -242,8 +242,7 @@ &:has(.detail:not([style*="display: none"])) { flex-wrap: wrap; align-content: center; - padding-top: 6px; - padding-right: 2px; + padding-right: 6px; .title { line-height: 14px; diff --git a/src/vs/platform/actionWidget/browser/tabbedActionListWidget.css b/src/vs/platform/actionWidget/browser/tabbedActionListWidget.css index 7dfc41c2384b5..2c0141dd6394c 100644 --- a/src/vs/platform/actionWidget/browser/tabbedActionListWidget.css +++ b/src/vs/platform/actionWidget/browser/tabbedActionListWidget.css @@ -15,7 +15,7 @@ .action-widget .tabbed-action-list-tabbar .monaco-custom-radio { width: 100%; - gap: 8px; + gap: 4px; } .action-widget .tabbed-action-list-tabbar .monaco-custom-radio > .monaco-button { diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index f5b7046bbb967..f62922ec71093 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -5,7 +5,7 @@ import { RunOnceScheduler } from '../../../base/common/async.js'; import { DebounceEmitter, Emitter, Event } from '../../../base/common/event.js'; -import { DisposableStore } from '../../../base/common/lifecycle.js'; +import { DisposableStore, Disposable, IDisposable } from '../../../base/common/lifecycle.js'; import { IMenu, IMenuActionOptions, IMenuChangeEvent, IMenuCreateOptions, IMenuItem, IMenuItemHide, IMenuService, isIMenuItem, isISubmenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from './actions.js'; import { ICommandAction, ILocalizedString } from '../../action/common/action.js'; import { ICommandService } from '../../commands/common/commands.js'; @@ -16,7 +16,7 @@ import { removeFastWithoutKeepingOrder } from '../../../base/common/arrays.js'; import { localize } from '../../../nls.js'; import { IKeybindingService } from '../../keybinding/common/keybinding.js'; -export class MenuService implements IMenuService { +export class MenuService extends Disposable implements IMenuService { declare readonly _serviceBrand: undefined; @@ -27,7 +27,8 @@ export class MenuService implements IMenuService { @IKeybindingService private readonly _keybindingService: IKeybindingService, @IStorageService storageService: IStorageService, ) { - this._hiddenStates = new PersistedMenuHideState(storageService); + super(); + this._hiddenStates = this._register(new PersistedMenuHideState(storageService)); } createMenu(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuCreateOptions): IMenu { @@ -51,7 +52,7 @@ export class MenuService implements IMenuService { } } -class PersistedMenuHideState { +class PersistedMenuHideState implements IDisposable { private static readonly _key = 'menu.hiddenCommands'; diff --git a/src/vs/platform/crossAppIpc/electron-main/crossAppIpcService.ts b/src/vs/platform/crossAppIpc/electron-main/crossAppIpcService.ts deleted file mode 100644 index 3c5173607cf03..0000000000000 --- a/src/vs/platform/crossAppIpc/electron-main/crossAppIpcService.ts +++ /dev/null @@ -1,140 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as electron from 'electron'; -import { TimeoutTimer } from '../../../base/common/async.js'; -import { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable } from '../../../base/common/lifecycle.js'; -import { createDecorator } from '../../instantiation/common/instantiation.js'; -import { ILogService } from '../../log/common/log.js'; - -export const ICrossAppIPCService = createDecorator('crossAppIPCService'); - -export interface ICrossAppIPCMessage { - readonly type: string; - readonly data?: unknown; -} - -export interface ICrossAppIPCService { - readonly _serviceBrand: undefined; - - /** Whether the Electron crossAppIPC API is supported in this build. */ - readonly isSupported: boolean; - - /** Whether initialize() has been called and successfully set up the IPC. */ - readonly initialized: boolean; - - /** Whether the IPC connection is active. */ - readonly connected: boolean; - - /** Whether this app is the IPC server (`true`) or client (`false`). Only meaningful when connected. */ - readonly isServer: boolean; - - /** Fires when the peer connects. The boolean indicates whether this app is the server. */ - readonly onDidConnect: Event; - - /** Fires when the peer disconnects. The string is the disconnect reason. */ - readonly onDidDisconnect: Event; - - /** Fires when a message is received from the peer. */ - readonly onDidReceiveMessage: Event; - - /** Send a message to the peer. No-op if not connected. */ - sendMessage(msg: ICrossAppIPCMessage): void; - - /** Initialize the IPC connection. Call once during startup. */ - initialize(): void; -} - -/** - * Manages the single crossAppIPC connection for the entire application. - */ -export class CrossAppIPCService extends Disposable implements ICrossAppIPCService { - - declare readonly _serviceBrand: undefined; - - private ipc: Electron.CrossAppIPC | undefined; - private _connected = false; - private _isServer = false; - private readonly reconnectTimer = this._register(new TimeoutTimer()); - - private readonly _onDidConnect = this._register(new Emitter()); - readonly onDidConnect: Event = this._onDidConnect.event; - - private readonly _onDidDisconnect = this._register(new Emitter()); - readonly onDidDisconnect: Event = this._onDidDisconnect.event; - - private readonly _onDidReceiveMessage = this._register(new Emitter()); - readonly onDidReceiveMessage: Event = this._onDidReceiveMessage.event; - - get isSupported(): boolean { - const crossAppIPC: Electron.CrossAppIPCModule | undefined = (electron as typeof electron & { crossAppIPC?: Electron.CrossAppIPCModule }).crossAppIPC; - return crossAppIPC !== undefined; - } - get initialized(): boolean { return this.ipc !== undefined; } - get connected(): boolean { return this._connected; } - get isServer(): boolean { return this._isServer; } - - constructor( - @ILogService private readonly logService: ILogService, - ) { - super(); - } - - initialize(): void { - if (this.ipc) { - return; // Already initialized - } - - const crossAppIPC: Electron.CrossAppIPCModule | undefined = (electron as typeof electron & { crossAppIPC?: Electron.CrossAppIPCModule }).crossAppIPC; - - if (!crossAppIPC) { - this.logService.info('CrossAppIPCService: crossAppIPC not available'); - return; - } - - const ipc = crossAppIPC.createCrossAppIPC(); - this.ipc = ipc; - - ipc.on('connected', () => { - this._connected = true; - this._isServer = ipc.isServer; - this.logService.info(`CrossAppIPCService: connected (isServer=${ipc.isServer})`); - this._onDidConnect.fire(ipc.isServer); - }); - - ipc.on('message', (messageEvent) => { - this._onDidReceiveMessage.fire(messageEvent.data as ICrossAppIPCMessage); - }); - - ipc.on('disconnected', (reason) => { - this.logService.info(`CrossAppIPCService: disconnected (${reason})`); - this._connected = false; - this._isServer = false; - this._onDidDisconnect.fire(reason); - - // Reconnect to wait for the peer's next launch. - // Delay briefly to allow the old Mach bootstrap service to be - // deregistered before re-creating the server endpoint (macOS). - if (reason === 'peer-disconnected') { - this.reconnectTimer.cancelAndSet(() => ipc.connect(), 1000); - } - }); - - ipc.connect(); - this.logService.info('CrossAppIPCService: connecting to peer'); - } - - sendMessage(msg: ICrossAppIPCMessage): void { - if (this.ipc?.connected) { - this.ipc.postMessage(msg); - } - } - - override dispose(): void { - this.ipc?.close(); - super.dispose(); - } -} diff --git a/src/vs/platform/encryption/electron-main/encryptionMainService.ts b/src/vs/platform/encryption/electron-main/encryptionMainService.ts index 93987884ac9f4..1b4b1e05f28c7 100644 --- a/src/vs/platform/encryption/electron-main/encryptionMainService.ts +++ b/src/vs/platform/encryption/electron-main/encryptionMainService.ts @@ -4,19 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { safeStorage as safeStorageElectron, app } from 'electron'; -import { join } from '../../../base/common/path.js'; -import { INodeProcess, isMacintosh, isWindows } from '../../../base/common/platform.js'; +import { isMacintosh, isWindows } from '../../../base/common/platform.js'; import { KnownStorageProvider, IEncryptionMainService, PasswordStoreCLIOption } from '../common/encryptionService.js'; -import { getDefaultUserDataPath } from '../../environment/node/userDataPath.js'; import { ILogService } from '../../log/common/log.js'; -import { IProductService } from '../../product/common/productService.js'; // These APIs are currently only supported in our custom build of electron so // we need to guard against them not being available. interface ISafeStorageAdditionalAPIs { setUsePlainTextEncryption(usePlainText: boolean): void; getSelectedStorageBackend(): string; - initWithExistingKey(localStatePath: string): boolean; } const safeStorage: typeof import('electron').safeStorage & Partial = safeStorageElectron; @@ -25,8 +21,7 @@ export class EncryptionMainService implements IEncryptionMainService { _serviceBrand: undefined; constructor( - @ILogService private readonly logService: ILogService, - @IProductService private readonly productService: IProductService + @ILogService private readonly logService: ILogService ) { // if this commandLine switch is set, the user has opted in to using basic text encryption if (app.commandLine.getSwitchValue('password-store') === PasswordStoreCLIOption.basic) { @@ -34,40 +29,6 @@ export class EncryptionMainService implements IEncryptionMainService { safeStorage.setUsePlainTextEncryption?.(true); this.logService.trace('[EncryptionMainService] set usePlainTextEncryption to true'); } - - if (isWindows && (process as INodeProcess).isEmbeddedApp) { - this.initializeWithHostEncryptionKey(); - } - } - - private initializeWithHostEncryptionKey(): void { - if (!safeStorage.initWithExistingKey) { - this.logService.trace('[EncryptionMainService] initWithExistingKey API is not available'); - return; - } - - // embedded.win32SiblingExeBasename is derived from the host app's product.nameShort - // at build time, which is also the folder name used for the host's user data path. - const hostProductName = this.productService.embedded?.win32SiblingExeBasename; - if (!hostProductName) { - this.logService.warn('[EncryptionMainService] Host product name not available in embedded product config'); - return; - } - - const hostUserDataPath = getDefaultUserDataPath(hostProductName); - const localStatePath = join(hostUserDataPath, 'Local State'); - - this.logService.info(`[EncryptionMainService] Initializing encryption with host app key from: ${localStatePath}`); - try { - const result = safeStorage.initWithExistingKey(localStatePath); - if (result) { - this.logService.info('[EncryptionMainService] Successfully initialized encryption with host app key'); - } else { - this.logService.error('[EncryptionMainService] Failed to initialize encryption with host app key'); - } - } catch (e) { - this.logService.error('[EncryptionMainService] Error initializing encryption with host app key:', e); - } } async encrypt(value: string): Promise { diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index fbec7dfe5c394..5c0fa0cc32041 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -109,7 +109,6 @@ export interface NativeParsedArgs { 'locate-extension'?: string[]; // undefined or array of 1 or more 'enable-proposed-api'?: string[]; // undefined or array of 1 or more 'open-url'?: boolean; - 'open-chat-session'?: string; 'skip-release-notes'?: boolean; 'skip-welcome'?: boolean; 'disable-telemetry'?: boolean; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index d36711390f280..4fdf722707eb4 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -84,7 +84,6 @@ export interface IEnvironmentService { extensionLogLevel?: [string, string][]; verbose: boolean; isBuilt: boolean; - isEmbeddedApp?: boolean; // --- telemetry/exp disableTelemetry: boolean; @@ -93,43 +92,6 @@ export interface IEnvironmentService { // --- agent sessions workspace agentSessionsWorkspace?: URI; - - /** - * When running as the embedded app, the user roaming data home of - * the host VS Code application (i.e. the default profile's settings/User - * directory). `undefined` when not running as embedded. - */ - readonly parentAppUserRoamingDataHome?: URI; - - /** - * When running as the embedded app, the data home of the host - * VS Code application (e.g. `~/.vscode-insiders`). This identifies the - * host application's home/data directory and is used alongside other - * host-specific paths such as `hostUserRoamingDataHome` and - * `hostExtensionsHome`. `undefined` when not running as embedded. - */ - readonly parentAppUserHome?: URI; - - /** - * When running as the embedded app, the extensions directory of - * the host VS Code application. `undefined` when not running as embedded. - */ - readonly parentAppExtensionsHome?: URI; - - /** - * When running as the embedded app, the short display name of the - * parent VS Code application (e.g. "VS Code Insiders"). - * `undefined` when not running as embedded. - */ - readonly parentAppNameShort?: string; - - /** - * When running as the embedded app, the long display name of the - * parent VS Code application (e.g. "Visual Studio Code Insiders"). - * `undefined` when not running as embedded. - */ - readonly parentAppNameLong?: string; - // --- Policy policyFile?: URI; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 35ebd864985d7..004d0614c938a 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -37,20 +37,6 @@ export interface INativeEnvironmentPaths { * OS tmp dir. */ tmpDir: string; - - /** - * The parent application user data directory, if the current instance is running as an embedded application. - * This can be used to access data from the parent application that is not shared with the embedded application. - * This is only set when running as an embedded application and is `undefined` otherwise. - */ - parentAppUserDataDir: string | undefined; - - /** - * The parent application home directory, if the current instance is running as an embedded application. - * This can be used to access data from the parent application that is not shared with the embedded application. - * This is only set when running as an embedded application and is `undefined` otherwise. - */ - parentAppUserHomeDir: string | undefined; } export abstract class AbstractNativeEnvironmentService implements INativeEnvironmentService { @@ -313,41 +299,12 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron this.args['continueOn'] = value; } - @memoize - get parentAppUserRoamingDataHome(): URI | undefined { - return this.paths.parentAppUserDataDir ? URI.file(this.paths.parentAppUserDataDir).with({ scheme: Schemas.vscodeUserData }) : undefined; - } - - @memoize - get parentAppUserHome(): URI | undefined { - return this.paths.parentAppUserHomeDir ? URI.file(this.paths.parentAppUserHomeDir) : undefined; - } - - @memoize - get parentAppExtensionsHome(): URI | undefined { - if (!this.parentAppUserHome) { - return undefined; - } - return joinPath(this.parentAppUserHome, 'extensions'); - } - - @memoize - get parentAppNameShort(): string | undefined { - return getParentAppName(this.productService, this.isEmbeddedApp, 'short'); - } - - @memoize - get parentAppNameLong(): string | undefined { - return getParentAppName(this.productService, this.isEmbeddedApp, 'long'); - } - get args(): NativeParsedArgs { return this._args; } constructor( private readonly _args: NativeParsedArgs, private readonly paths: INativeEnvironmentPaths, - protected readonly productService: IProductService, - readonly isEmbeddedApp: boolean = false + protected readonly productService: IProductService ) { } } @@ -370,18 +327,3 @@ export function parseDebugParams(debugArg: string | undefined, debugBrkArg: stri return { port, break: brk, debugId, env }; } - -function getParentAppName(productService: IProductService, isEmbeddedApp: boolean, variant: 'short' | 'long'): string | undefined { - if (!isEmbeddedApp) { - return undefined; - } - const quality = productService.quality; - if (quality === 'stable') { - return variant === 'short' ? 'VS Code' : 'Visual Studio Code'; - } else if (quality === 'insider') { - return variant === 'short' ? 'VS Code Insiders' : 'Visual Studio Code Insiders'; - } else if (quality === 'exploration') { - return variant === 'short' ? 'VS Code Exploration' : 'Visual Studio Code Exploration'; - } - return undefined; -} diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 95216df31a498..86800f721cf1d 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -198,7 +198,6 @@ export const OPTIONS: OptionDescriptions> = { 'crash-reporter-id': { type: 'string' }, 'skip-add-to-recently-opened': { type: 'boolean' }, 'open-url': { type: 'boolean' }, - 'open-chat-session': { type: 'string' }, 'file-write': { type: 'boolean' }, 'file-chmod': { type: 'boolean' }, 'install-builtin-extension': { type: 'string[]' }, diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 28d299dd8e367..8652144b5634c 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -9,9 +9,6 @@ import { IDebugParams } from '../common/environment.js'; import { AbstractNativeEnvironmentService, parseDebugParams } from '../common/environmentService.js'; import { getUserDataPath } from './userDataPath.js'; import { IProductService } from '../../product/common/productService.js'; -import { INodeProcess } from '../../../base/common/platform.js'; -import { join } from '../../../base/common/path.js'; -import { env } from '../../../base/common/process.js'; export class NativeEnvironmentService extends AbstractNativeEnvironmentService { @@ -21,9 +18,7 @@ export class NativeEnvironmentService extends AbstractNativeEnvironmentService { homeDir, tmpDir: tmpdir(), userDataDir: getUserDataPath(args, productService.nameShort), - parentAppUserDataDir: getParentAppUserDataDir(args, productService), - parentAppUserHomeDir: getParentAppUserHomeDir(homeDir, productService) - }, productService, isEmbeddedApp()); + }, productService); } } @@ -38,55 +33,3 @@ export function parseAgentHostDebugPort(args: NativeParsedArgs, isBuilt: boolean export function parseSharedProcessDebugPort(args: NativeParsedArgs, isBuilt: boolean): IDebugParams { return parseDebugParams(args['inspect-sharedprocess'], args['inspect-brk-sharedprocess'], 5879, isBuilt, args.extensionEnvironment); } - - -function getParentAppUserDataDir(args: NativeParsedArgs, productService: IProductService): string | undefined { - if (!(process as INodeProcess).isEmbeddedApp) { - return undefined; - } - if (env['VSCODE_DEV']) { - return undefined; - } - const quality = productService.quality; - let hostProductName: string; - if (quality === 'stable') { - hostProductName = 'Code'; - } else if (quality === 'insider') { - hostProductName = 'Code - Insiders'; - } else if (quality === 'exploration') { - hostProductName = 'Code - Exploration'; - } else { - return undefined; - } - - // Honor the same env-var overrides that the host VS Code itself uses - // (portable mode and VSCODE_APPDATA), but intentionally skip --user-data-dir - // because that CLI arg belongs to the Agents app, not the host. - const hostUserDataPath = getUserDataPath(args, hostProductName); - return join(hostUserDataPath, 'User'); -} - -function getParentAppUserHomeDir(homeDir: string, productService: IProductService): string | undefined { - if (!(process as INodeProcess).isEmbeddedApp) { - return undefined; - } - if (env['VSCODE_DEV']) { - return undefined; - } - const quality = productService.quality; - let hostDataFolderName: string; - if (quality === 'stable') { - hostDataFolderName = '.vscode'; - } else if (quality === 'insider') { - hostDataFolderName = '.vscode-insiders'; - } else if (quality === 'exploration') { - hostDataFolderName = '.vscode-exploration'; - } else { - return undefined; - } - return join(homeDir, hostDataFolderName); -} - -function isEmbeddedApp(): boolean { - return !!(process as INodeProcess).isEmbeddedApp; -} diff --git a/src/vs/platform/environment/node/userDataPath.ts b/src/vs/platform/environment/node/userDataPath.ts index be9d71b601bdd..98b4411b4217f 100644 --- a/src/vs/platform/environment/node/userDataPath.ts +++ b/src/vs/platform/environment/node/userDataPath.ts @@ -5,7 +5,6 @@ import { homedir } from 'os'; import { NativeParsedArgs } from '../common/argv.js'; -import { INodeProcess } from '../../../base/common/platform.js'; // This file used to be a pure JS file and was always // importing `path` from node.js even though we ship // our own version of the library and prefer to use @@ -46,11 +45,7 @@ function doGetUserDataPath(cliArgs: NativeParsedArgs, productName: string): stri // 0. Running out of sources has a fixed productName if (process.env['VSCODE_DEV']) { - if ((process as INodeProcess).isEmbeddedApp) { - productName = 'agents-oss-dev'; - } else { - productName = 'code-oss-dev'; - } + productName = 'code-oss-dev'; } // 1. Support portable mode diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index ff9440d907a9b..1953604537dca 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -234,6 +234,9 @@ const _allApiProposals = { diffContentOptions: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffContentOptions.d.ts', }, + documentDiff: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentDiff.d.ts', + }, documentFiltersExclusive: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', }, diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 7815fdff84402..b45f24b688911 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -5,7 +5,6 @@ import { app } from 'electron'; import { coalesce } from '../../../base/common/arrays.js'; -import { Emitter, Event } from '../../../base/common/event.js'; import { IProcessEnvironment, isMacintosh } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { whenDeleted } from '../../../base/node/pfs.js'; @@ -36,21 +35,12 @@ export interface ILaunchMainService { start(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise; getMainProcessId(): Promise; - - /** - * Fires when a second instance sends `--share-secrets-with-agents-app`. - * Used for cross-app secret migration. - */ - readonly onDidRequestShareSecrets: Event; } export class LaunchMainService implements ILaunchMainService { declare readonly _serviceBrand: undefined; - private readonly _onDidRequestShareSecrets = new Emitter(); - readonly onDidRequestShareSecrets: Event = this._onDidRequestShareSecrets.event; - constructor( @ILogService private readonly logService: ILogService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @@ -62,14 +52,6 @@ export class LaunchMainService implements ILaunchMainService { async start(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { this.logService.trace('Received data from other instance: ', args, userEnv); - // Handle --share-secrets-with-agents-app from a second instance: - // trigger the secret sharing handshake without opening any window. - if (args['share-secrets-with-agents-app']) { - this.logService.info('Received --share-secrets-with-agents-app from second instance'); - this._onDidRequestShareSecrets.fire(); - return; - } - // macOS: Electron > 7.x changed its behaviour to not // bring the application to the foreground when a window // is focused programmatically. Only via `app.focus` and diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index d255fb14f3aad..968960dc58b4a 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -131,14 +131,6 @@ export interface ICommonNativeHostService { openAgentsWindow(options?: { folderUri?: UriComponents }): Promise; - /** - * Launches the sibling application (host ↔ embedded). - * The launched process is detached with its own process group. - * - * @param args CLI arguments to pass to the sibling application. - */ - launchSiblingApp(args?: string[]): Promise; - isFullScreen(options?: INativeHostOptions): Promise; toggleFullScreen(options?: INativeHostOptions): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 1cfa2a170cb2c..a809171d570db 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -18,7 +18,6 @@ import { AddFirstParameterToFunctions } from '../../../base/common/types.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { virtualMachineHint } from '../../../base/node/id.js'; import { Promises, SymlinkSupport } from '../../../base/node/pfs.js'; -import { launchSiblingApp } from '../node/siblingApp.js'; import { findFreePort, isPortFree } from '../../../base/node/ports.js'; import { localize } from '../../../nls.js'; import { ISerializableCommandAction } from '../../action/common/action.js'; @@ -316,35 +315,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } - async launchSiblingApp(_windowId: number | undefined, args?: string[]): Promise { - const finalArgs = [...(args ?? [])]; - - // Forward transient dirs to the sibling app so it runs fully isolated - const agentsUserDataDir = this.environmentMainService.args['agents-user-data-dir']; - if (agentsUserDataDir) { - finalArgs.push('--user-data-dir', agentsUserDataDir); - } - const agentsExtensionsDir = this.environmentMainService.args['agents-extensions-dir']; - if (agentsExtensionsDir) { - finalArgs.push('--extensions-dir', agentsExtensionsDir); - } - const sharedDataDir = this.environmentMainService.args['shared-data-dir']; - if (sharedDataDir) { - finalArgs.push('--shared-data-dir', sharedDataDir); - } - const agentPluginsDir = this.environmentMainService.args['agent-plugins-dir']; - if (agentPluginsDir) { - finalArgs.push('--agent-plugins-dir', agentPluginsDir); - } - - const result = launchSiblingApp(this.productService, finalArgs, err => { - this.logService.error('[launchSiblingApp] Failed to spawn sibling app:', err.message); - }); - if (!result) { - this.logService.warn('[launchSiblingApp] Could not resolve sibling app on this platform'); - } - } - async isFullScreen(windowId: number | undefined, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); return window?.isFullScreen ?? false; diff --git a/src/vs/platform/native/node/siblingApp.ts b/src/vs/platform/native/node/siblingApp.ts deleted file mode 100644 index 78228bf9f3e92..0000000000000 --- a/src/vs/platform/native/node/siblingApp.ts +++ /dev/null @@ -1,95 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ChildProcess, spawn } from 'child_process'; -import { statSync } from 'fs'; -import { dirname, join } from '../../../base/common/path.js'; -import { isMacintosh, isWindows, INodeProcess } from '../../../base/common/platform.js'; -import { IProductConfiguration } from '../../../base/common/product.js'; - -export interface ISiblingAppLaunchResult { - readonly child: ChildProcess; -} - -/** - * Launches the sibling application (host ↔ embedded) using a detached - * child process with its own process group. - * - * @param product The product configuration of the **current** process. - * @param args CLI arguments to forward to the sibling app. - * @param onError Optional callback invoked when the spawned process emits an error. - * @returns The spawned detached child process, or `undefined` if the - * sibling could not be resolved on the current platform. - */ -export function launchSiblingApp(product: IProductConfiguration, args: string[] = [], onError?: (err: Error) => void): ISiblingAppLaunchResult | undefined { - if (isMacintosh) { - const bundleId = resolveSiblingDarwinBundleIdentifier(product); - if (!bundleId) { - return undefined; - } - const spawnArgs = ['-n', '-g', '-b', bundleId]; - if (args.length > 0) { - spawnArgs.push('--args', ...args); - } - const child = spawn('open', spawnArgs, { - detached: true, - stdio: 'ignore', - }); - child.on('error', err => onError?.(err)); - child.unref(); - return { child }; - } - - if (isWindows) { - const exePath = resolveSiblingWindowsExePath(product); - if (!exePath) { - return undefined; - } - const child = spawn(exePath, args, { - detached: true, - stdio: 'ignore', - }); - child.on('error', err => onError?.(err)); - child.unref(); - return { child }; - } - - return undefined; -} - -/** - * Returns the macOS bundle identifier for the sibling app. - */ -function resolveSiblingDarwinBundleIdentifier(product: IProductConfiguration): string | undefined { - const isEmbedded = !!(process as INodeProcess).isEmbeddedApp; - return isEmbedded - ? product.embedded?.darwinSiblingBundleIdentifier - : product.darwinSiblingBundleIdentifier; -} - -/** - * Resolves the sibling app's Windows executable path. - */ -export function resolveSiblingWindowsExePath(product: IProductConfiguration): string | undefined { - const isEmbedded = !!(process as INodeProcess).isEmbeddedApp; - const siblingBasename = isEmbedded - ? product.embedded?.win32SiblingExeBasename - : product.win32SiblingExeBasename; - - if (!siblingBasename) { - return undefined; - } - - const siblingExe = join(dirname(process.execPath), `${siblingBasename}.exe`); - try { - if (statSync(siblingExe).isFile()) { - return siblingExe; - } - } catch { - // may not exist on disk - } - - return undefined; -} diff --git a/src/vs/platform/secrets/electron-main/macOSCrossAppSecretSharing.ts b/src/vs/platform/secrets/electron-main/macOSCrossAppSecretSharing.ts deleted file mode 100644 index 6dfea9bc15a96..0000000000000 --- a/src/vs/platform/secrets/electron-main/macOSCrossAppSecretSharing.ts +++ /dev/null @@ -1,315 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { execFile } from 'child_process'; -import { dirname } from '../../../base/common/path.js'; -import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; -import { ILogService } from '../../log/common/log.js'; -import { IEncryptionMainService } from '../../encryption/common/encryptionService.js'; -import { IStorageMainService } from '../../storage/electron-main/storageMainService.js'; -import { CROSS_APP_SHARED_SECRET_KEYS, secretStorageKey, readEncryptedSecret, writeEncryptedSecret } from '../common/secrets.js'; -import { IStateService } from '../../state/node/state.js'; -import { INodeProcess, isMacintosh } from '../../../base/common/platform.js'; -import { IStorageMain } from '../../storage/electron-main/storageMain.js'; -import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; -import { ILaunchMainService } from '../../launch/electron-main/launchMainService.js'; -import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.js'; -import { ICrossAppIPCService } from '../../crossAppIpc/electron-main/crossAppIpcService.js'; - -const MIGRATION_STATE_KEY = 'crossAppSecretSharing.migrationDone'; - -/** - * Message types exchanged between apps over crossAppIPC for secret sharing. - */ -const enum CrossAppSecretMessageType { - /** Agents → Host: Request secrets */ - SecretRequest = 'secrets/request', - /** Host → Agents: Response with secrets */ - SecretResponse = 'secrets/response', - /** Agents → Host: Confirms secrets were stored, both sides mark migration done */ - SecretAck = 'secrets/ack', -} - -interface CrossAppSecretMessage { - type: CrossAppSecretMessageType; - data?: Record; -} - -/** - * Coordinates one-time secret migration between the VS Code app and the - * agents app using Electron's crossAppIPC (macOS only). - * - * **Demand-driven**: Only the agents app initiates migration. If it - * detects that migration hasn't been done yet, it: - * 1. Waits for the crossAppIPC connection (managed by ICrossAppIPCService). - * 2. Spawns Code.app with `--share-secrets-with-agents-app`, which - * either starts Code.app fresh or (if already running) forwards - * the arg to the existing instance via the node IPC socket. - * 3. Code.app creates its own crossAppIPC connection when it sees - * the arg, and the two connect. - * 4. Agents app sends `SecretRequest` → Code.app responds with - * `SecretResponse` → Agents app sends `SecretAck`. - * 5. Both sides mark migration as done. Code.app quits if it was - * launched solely for this purpose. - * - * Security: crossAppIPC uses code-signature verification (Mach ports - * on macOS) — the kernel authenticates both endpoints. No secrets are - * ever in process args, files, or network. - */ -export class MacOSCrossAppSecretSharing extends Disposable { - - private readonly isEmbeddedApp: boolean; - private readonly applicationStorage: IStorageMain; - private _onHostMigrationComplete: (() => void) | undefined; - private readonly hostHandshakeListeners = this._register(new DisposableStore()); - - constructor( - storageMainService: IStorageMainService, - private readonly encryptionMainService: IEncryptionMainService, - private readonly stateService: IStateService, - private readonly logService: ILogService, - environmentMainService: IEnvironmentMainService, - launchMainService: ILaunchMainService, - lifecycleMainService: ILifecycleMainService, - private readonly crossAppIPCService: ICrossAppIPCService, - ) { - super(); - this.isEmbeddedApp = !!(process as INodeProcess).isEmbeddedApp; - this.applicationStorage = storageMainService.applicationStorage; - this.initialize(environmentMainService, launchMainService, lifecycleMainService); - } - - private initialize( - environmentMainService: IEnvironmentMainService, - launchMainService: ILaunchMainService, - lifecycleMainService: ILifecycleMainService, - ): void { - if (this.isEmbeddedApp) { - // Agents app: initiate migration if needed - this.initializeAsAgentsApp(); - } else if (environmentMainService.args['share-secrets-with-agents-app']) { - // Code.app launched fresh with --share-secrets-with-agents-app: - // respond to the agents app's request, then quit if no other reason to stay - const hasOtherArgs = environmentMainService.args._.length > 0 || environmentMainService.args['folder-uri'] || environmentMainService.args['file-uri']; - this.initializeAsHostApp(hasOtherArgs ? undefined : () => { - this.logService.info('[CrossAppSecretSharing] Host app was launched for migration only, quitting'); - lifecycleMainService.quit(); - }); - } else { - // Code.app already running: listen for --share-secrets-with-agents-app - // forwarded from a second instance via the launch service - this._register(launchMainService.onDidRequestShareSecrets(() => { - this.initializeAsHostApp(); - })); - } - } - - private async initializeAsAgentsApp(): Promise { - if (!isMacintosh || !this.isEmbeddedApp) { - return; - } - - if (this.isMigrationDone()) { - this.logService.trace('[CrossAppSecretSharing] Migration already done, skipping'); - return; - } - - // Wait for storage to be ready before we start — handleSecretResponse - // will write secrets into applicationStorage. - await this.applicationStorage.whenInit; - - if (!this.crossAppIPCService.initialized) { - this.logService.info('[CrossAppSecretSharing] crossAppIPC not initialized, skipping migration'); - return; - } - - this.logService.info('[CrossAppSecretSharing] Migration needed, starting...'); - - // Listen for connection — when connected, request secrets - this._register(this.crossAppIPCService.onDidConnect(isServer => { - this.logService.info(`[CrossAppSecretSharing] Connected (isServer=${isServer}), requesting secrets from host app`); - this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretRequest }); - })); - - // Listen for messages - this._register(this.crossAppIPCService.onDidReceiveMessage(msg => { - const secretMsg = msg as CrossAppSecretMessage; - if (secretMsg?.type === CrossAppSecretMessageType.SecretResponse) { - this.handleSecretResponse(secretMsg.data ?? {}); - } - })); - - // If already connected (e.g. service was initialized before storage was ready), - // send the request immediately. - if (this.crossAppIPCService.connected) { - this.logService.info(`[CrossAppSecretSharing] Already connected (isServer=${this.crossAppIPCService.isServer}), requesting secrets from host app`); - this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretRequest }); - } - - // Spawn Code.app with --share-secrets-with-agents-app - this.spawnHostApp(); - - // Timeout: if migration doesn't complete within 30s, give up - setTimeout(() => { - if (!this.isMigrationDone()) { - this.logService.warn('[CrossAppSecretSharing] Migration timed out'); - } - }, 30_000); - } - - private async initializeAsHostApp(onComplete?: () => void): Promise { - if (!isMacintosh || this.isEmbeddedApp) { - onComplete?.(); - return; - } - - if (this.isMigrationDone()) { - this.logService.trace('[CrossAppSecretSharing] Migration already done, skipping'); - onComplete?.(); - return; - } - - // Wait for application storage to be fully initialized before - // checking for secrets — storage may still be in-memory at this - // point during early startup. - await this.applicationStorage.whenInit; - - if (!this.hasAnySharedSecrets()) { - this.logService.trace('[CrossAppSecretSharing] No shared secrets to share, skipping'); - onComplete?.(); - return; - } - - if (!this.crossAppIPCService.initialized) { - this.logService.info('[CrossAppSecretSharing] crossAppIPC not initialized'); - onComplete?.(); - return; - } - - this._onHostMigrationComplete = onComplete; - - this.logService.info('[CrossAppSecretSharing] Host app responding to secret sharing request'); - - // Dispose previous listeners if initializeAsHostApp is called again - // (e.g. via repeated onDidRequestShareSecrets events). - this.hostHandshakeListeners.clear(); - - // Listen for messages from the agents app - this.hostHandshakeListeners.add(this.crossAppIPCService.onDidReceiveMessage(msg => { - const secretMsg = msg as CrossAppSecretMessage; - if (secretMsg?.type === CrossAppSecretMessageType.SecretRequest) { - this.handleSecretRequest(); - } else if (secretMsg?.type === CrossAppSecretMessageType.SecretAck) { - this.handleSecretAck(); - } - })); - - // If disconnected before ack, still allow the host to quit - this.hostHandshakeListeners.add(this.crossAppIPCService.onDidDisconnect(() => { - this._onHostMigrationComplete?.(); - this._onHostMigrationComplete = undefined; - })); - } - - private isMigrationDone(): boolean { - return this.stateService.getItem(MIGRATION_STATE_KEY, false); - } - - private hasAnySharedSecrets(): boolean { - for (const key of CROSS_APP_SHARED_SECRET_KEYS) { - if (this.applicationStorage.get(secretStorageKey(key)) !== undefined) { - return true; - } - } - return false; - } - - private spawnHostApp(): void { - // Agents app's process.execPath: - // /Contents/Applications//Contents/MacOS/Electron - // Code.app bundle is 6 directories up: - // MacOS → Contents → → Applications → Contents → - const codeAppBundle = dirname(dirname(dirname(dirname(dirname(dirname(process.execPath)))))); - - this.logService.info('[CrossAppSecretSharing] Spawning host app:', codeAppBundle); - - const child = execFile('open', [ - '-a', codeAppBundle, - '-n', // new instance (so args are passed even if already running) - '-g', // don't bring to front - '--args', '--share-secrets-with-agents-app', - ], (error) => { - if (error) { - this.logService.error('[CrossAppSecretSharing] Failed to spawn host app:', error.message); - } - }); - child.unref(); - } - - private async handleSecretRequest(): Promise { - this.logService.info('[CrossAppSecretSharing] Host app handling secret request'); - - const secrets: Record = {}; - - for (const key of CROSS_APP_SHARED_SECRET_KEYS) { - try { - const decrypted = await readEncryptedSecret( - key, - (fullKey) => this.applicationStorage.get(fullKey), - (value) => this.encryptionMainService.decrypt(value), - this.logService, - ); - if (decrypted !== undefined) { - secrets[key] = decrypted; - } - } catch (err) { - this.logService.error('[CrossAppSecretSharing] Failed to read secret for key:', key, err); - } - } - - this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretResponse, data: secrets }); - this.logService.info('[CrossAppSecretSharing] Sent secrets response with', Object.keys(secrets).length, 'keys'); - } - - private async handleSecretResponse(secrets: Record): Promise { - this.logService.info('[CrossAppSecretSharing] Agents app received', Object.keys(secrets).length, 'secrets'); - - for (const [key, value] of Object.entries(secrets)) { - if (!CROSS_APP_SHARED_SECRET_KEYS.includes(key)) { - this.logService.warn('[CrossAppSecretSharing] Ignoring unexpected key:', key); - continue; - } - - try { - await writeEncryptedSecret( - key, - value, - (fullKey, encrypted) => this.applicationStorage.set(fullKey, encrypted), - (v) => this.encryptionMainService.encrypt(v), - this.logService, - ); - } catch (err) { - this.logService.error('[CrossAppSecretSharing] Failed to store secret for key:', key, err); - } - } - - this.stateService.setItem(MIGRATION_STATE_KEY, true); - this.logService.info('[CrossAppSecretSharing] Migration complete'); - - // Tell the host app migration is done so it can also record it. - // Don't close here — let the host close first after receiving the ack. - this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretAck }); - } - - private handleSecretAck(): void { - this.stateService.setItem(MIGRATION_STATE_KEY, true); - this.logService.info('[CrossAppSecretSharing] Host app received ack, migration complete on both sides'); - - const onComplete = this._onHostMigrationComplete; - this._onHostMigrationComplete = undefined; - - onComplete?.(); - } -} diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts index 90c8db613ddd2..a56091d0ff79a 100644 --- a/src/vs/platform/storage/electron-main/storageMain.ts +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -12,8 +12,8 @@ import { join } from '../../../base/common/path.js'; import { StopWatch } from '../../../base/common/stopwatch.js'; import { URI } from '../../../base/common/uri.js'; import { Promises } from '../../../base/node/pfs.js'; -import { InMemoryStorageDatabase, IStorage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage, StorageHint, StorageState, MigratingStorage } from '../../../base/parts/storage/common/storage.js'; -import { ISQLiteStorageDatabaseLoggingOptions, ISQLiteStorageDatabaseOptions, SQLiteStorageDatabase } from '../../../base/parts/storage/node/storage.js'; +import { InMemoryStorageDatabase, IStorage, Storage, StorageHint, StorageState, MigratingStorage } from '../../../base/parts/storage/common/storage.js'; +import { ISQLiteStorageDatabaseLoggingOptions, SQLiteStorageDatabase } from '../../../base/parts/storage/node/storage.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { IFileService } from '../../files/common/files.js'; import { ILogService, LogLevel } from '../../log/common/log.js'; @@ -22,7 +22,6 @@ import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfil import { currentSessionDateStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey } from '../../telemetry/common/telemetry.js'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IAnyWorkspaceIdentifier } from '../../workspace/common/workspace.js'; import { Schemas } from '../../../base/common/network.js'; -import { ICrossAppIPCMessage, ICrossAppIPCService } from '../../crossAppIpc/electron-main/crossAppIpcService.js'; export interface IStorageMainOptions { @@ -361,15 +360,12 @@ export class ApplicationSharedStorageMain extends BaseStorageMain { return undefined; } - private sharedDatabase: SharedSQLiteStorageDatabase | undefined; - constructor( private readonly options: IStorageMainOptions, private readonly storageFolderPath: string, private readonly applicationStorage: IStorageMain, logService: ILogService, fileService: IFileService, - private readonly crossAppIPCService: ICrossAppIPCService, ) { super(logService, fileService); } @@ -379,32 +375,19 @@ export class ApplicationSharedStorageMain extends BaseStorageMain { this.logService.info(`[shared storage] Creating shared storage database at '${storageFilePath}' (wasCreated: ${wasCreated})`); - this.sharedDatabase = new SharedSQLiteStorageDatabase(storageFilePath, { - logging: this.createLoggingOptions(), - useWAL: true, - busyTimeout: 2000 - }, this.crossAppIPCService, this.logService); - this._register(this.sharedDatabase); + const database = new SQLiteStorageDatabase(storageFilePath, { + logging: this.createLoggingOptions() + }); - this.logService.info(`[shared storage] Initializing fallback application storage (type: ${this.applicationStorage instanceof HostApplicationStorageMain ? 'host' : 'local'}, path: ${this.applicationStorage.path ?? 'in-memory'})`); + this.logService.info(`[shared storage] Initializing fallback application storage (path: ${this.applicationStorage.path ?? 'in-memory'})`); await this.applicationStorage.init(); this.logService.info(`[shared storage] Fallback application storage initialized with ${this.applicationStorage.items.size} items`); - const migratingStorage = this._register(new MigratingStorage(this.sharedDatabase, { hint: wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined })); - migratingStorage.setFallbackStorage(this.applicationStorage.storage, this.applicationStorage instanceof HostApplicationStorageMain); + const migratingStorage = this._register(new MigratingStorage(database, { hint: wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined })); + migratingStorage.setFallbackStorage(this.applicationStorage.storage, false); return migratingStorage; } - protected override async doInit(storage: IStorage): Promise { - await super.doInit(storage); - - // Mark the shared database as initialized so that - // cross-app IPC messages are processed from now on. - // This must happen after Storage.init() completes to - // avoid processing stale queued messages. - this.sharedDatabase?.setInitialized(); - } - get applicationStorageItems(): Map { return this.applicationStorage.items; } @@ -427,29 +410,6 @@ export class ApplicationSharedStorageMain extends BaseStorageMain { } } -export class HostApplicationStorageMain extends BaseStorageMain { - - constructor( - readonly path: string, - logService: ILogService, - fileService: IFileService - ) { - super(logService, fileService); - } - - protected async doCreate(): Promise { - this.logService.info(`[shared storage] Opening host application storage at '${this.path}'`); - try { - const storage = new Storage(new SQLiteStorageDatabase(this.path, { logging: this.createLoggingOptions() })); - return storage; - } catch (error) { - this.logService.error(`[shared storage] Failed to open host application storage at '${this.path}': ${error}`); - throw error; - } - } - -} - export class WorkspaceStorageMain extends BaseStorageMain { private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb'; @@ -528,102 +488,6 @@ export class WorkspaceStorageMain extends BaseStorageMain { } } -const enum SharedStorageMessageType { - Changed = 'sharedStorage:changed' -} - -interface ISharedStorageChangedMessage extends ICrossAppIPCMessage { - readonly type: SharedStorageMessageType.Changed; - readonly data: { - readonly changed?: [string, string][]; - readonly deleted?: string[]; - }; -} - -/** - * A SQLite storage database wrapper that detects external changes - * via CrossAppIPC. When the sibling app (VS Code or Sessions app) - * writes to the shared storage, it sends an IPC message with the - * changed keys for instant notification. - */ -class SharedSQLiteStorageDatabase extends Disposable implements IStorageDatabase { - - private readonly _onDidChangeItemsExternal = this._register(new Emitter()); - readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; - - private readonly database: SQLiteStorageDatabase; - private initialized = false; - - constructor( - path: string, - options: ISQLiteStorageDatabaseOptions | undefined, - private readonly crossAppIPCService: ICrossAppIPCService, - private readonly logService: ILogService - ) { - super(); - - this.database = new SQLiteStorageDatabase(path, options); - - this.registerListeners(); - } - - private registerListeners(): void { - this._register(this.crossAppIPCService.onDidReceiveMessage(msg => { - if (msg.type !== SharedStorageMessageType.Changed) { - return; - } - - if (!this.initialized) { - this.logService.trace('[shared storage] Ignoring cross-app IPC message received before initialization'); - return; - } - - const { changed, deleted } = (msg as ISharedStorageChangedMessage).data; - this.logService.trace(`[shared storage] Received cross-app IPC change: ${changed?.length ?? 0} changed, ${deleted?.length ?? 0} deleted`); - - this._onDidChangeItemsExternal.fire({ - changed: changed ? new Map(changed) : undefined, - deleted: deleted ? new Set(deleted) : undefined - }); - })); - } - - setInitialized(): void { - this.initialized = true; - } - - async getItems(): Promise> { - const items = await this.database.getItems(); - this.logService.trace(`[shared storage] Initialized with ${items.size} items`); - return items; - } - - async updateItems(request: IUpdateRequest): Promise { - await this.database.updateItems(request); - - const changedCount = request.insert?.size ?? 0; - const deletedCount = request.delete?.size ?? 0; - this.logService.trace(`[shared storage] Sending cross-app IPC change: ${changedCount} changed, ${deletedCount} deleted`); - - // Notify the sibling app via IPC - this.crossAppIPCService.sendMessage({ - type: SharedStorageMessageType.Changed, - data: { - changed: request.insert ? Array.from(request.insert.entries()) : undefined, - deleted: request.delete ? Array.from(request.delete.values()) : undefined - } - }); - } - - async optimize(): Promise { - return this.database.optimize(); - } - - async close(recovery?: () => Map): Promise { - return this.database.close(recovery); - } -} - export class InMemoryStorageMain extends BaseStorageMain { get path(): string | undefined { diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index 35eb51cc274d5..d31880cc9b1d9 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -14,13 +14,12 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js'; import { ILogService } from '../../log/common/log.js'; import { AbstractStorageService, isProfileUsingDefaultStorage, IStorageService, StorageScope, StorageTarget } from '../common/storage.js'; -import { ApplicationStorageMain, ApplicationSharedStorageMain, ProfileStorageMain, InMemoryStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain, IStorageChangeEvent, HostApplicationStorageMain } from './storageMain.js'; +import { ApplicationStorageMain, ApplicationSharedStorageMain, ProfileStorageMain, InMemoryStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain, IStorageChangeEvent } from './storageMain.js'; import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; import { IUserDataProfilesMainService } from '../../userDataProfile/electron-main/userDataProfile.js'; import { IAnyWorkspaceIdentifier } from '../../workspace/common/workspace.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { Schemas } from '../../../base/common/network.js'; -import { ICrossAppIPCService } from '../../crossAppIpc/electron-main/crossAppIpcService.js'; //#region Storage Main Service (intent: make application, profile and workspace storage accessible to windows from main process) @@ -96,7 +95,6 @@ export class StorageMainService extends Disposable implements IStorageMainServic @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IFileService private readonly fileService: IFileService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @ICrossAppIPCService private readonly crossAppIPCService: ICrossAppIPCService, ) { super(); @@ -204,29 +202,11 @@ export class StorageMainService extends Disposable implements IStorageMainServic const sharedStorageFolderPath = join(this.environmentService.appSharedDataHome.with({ scheme: Schemas.file }).fsPath, 'sharedStorage'); - // Determine the fallback storage for transparent migration of keys - // from APPLICATION to APPLICATION_SHARED scope: - // In VS Code: reuse the own application storage (keys are local) - let fallbackStorage: IStorageMain = this.applicationStorage; - const hostUserRoamingDataHome = this.environmentService.parentAppUserRoamingDataHome; - if (hostUserRoamingDataHome) { - // - In the Agents App: create a storage backed by the host (VS Code) - // app's application DB so keys are found even if VS Code hasn't - // migrated them to the shared DB yet. - // We use ProfileStorageMain (not ApplicationStorageMain) to avoid - // writing telemetry state into the host app's DB — this is read-only. - const hostApplicationStoragePath = join(hostUserRoamingDataHome.with({ scheme: Schemas.file }).fsPath, 'globalStorage', 'state.vscdb'); - this.logService.info(`StorageMainService: creating application shared storage with host app fallback at '${hostApplicationStoragePath}'`); - fallbackStorage = this._register(new HostApplicationStorageMain( - hostApplicationStoragePath, - this.logService, - this.fileService - )); - } else { - this.logService.info(`StorageMainService: creating application shared storage with local application storage fallback`); - } - - const applicationSharedStorage = new ApplicationSharedStorageMain(this.getStorageOptions(), sharedStorageFolderPath, fallbackStorage, this.logService, this.fileService, this.crossAppIPCService); + // Use the local application storage as fallback for transparent migration + // of keys from APPLICATION to APPLICATION_SHARED scope. The agents window is + // now part of the same VS Code app, so there is no separate "host" app DB to + // fall back to. + const applicationSharedStorage = new ApplicationSharedStorageMain(this.getStorageOptions(), sharedStorageFolderPath, this.applicationStorage, this.logService, this.fileService); this._register(Event.once(applicationSharedStorage.onDidCloseStorage)(() => { this.logService.trace(`StorageMainService: closed application shared storage`); diff --git a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts index eef84a5d3517a..1ed6be2376c15 100644 --- a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts +++ b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { notStrictEqual, ok, strictEqual } from 'assert'; +import { notStrictEqual, strictEqual } from 'assert'; import { Schemas } from '../../../../base/common/network.js'; import { joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; @@ -26,8 +26,6 @@ import { UserDataProfilesMainService } from '../../../userDataProfile/electron-m import { TestLifecycleMainService } from '../../../test/electron-main/workbenchTestServices.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { ICrossAppIPCMessage, ICrossAppIPCService } from '../../../crossAppIpc/electron-main/crossAppIpcService.js'; suite('StorageMainService', function () { @@ -35,19 +33,6 @@ suite('StorageMainService', function () { const productService: IProductService = { _serviceBrand: undefined, ...product }; - const nullCrossAppIPCService: ICrossAppIPCService = { - _serviceBrand: undefined, - isSupported: false, - initialized: false, - connected: false, - isServer: false, - onDidConnect: Event.None, - onDidDisconnect: Event.None, - onDidReceiveMessage: Event.None, - sendMessage: () => { }, - initialize: () => { } - }; - const inMemoryProfileRoot = URI.file('/location').with({ scheme: Schemas.inMemory }); const inMemoryProfile: IUserDataProfile = { id: 'id', @@ -132,7 +117,7 @@ suite('StorageMainService', function () { const environmentService = new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService); const fileService = disposables.add(new FileService(new NullLogService())); const uriIdentityService = disposables.add(new UriIdentityService(fileService)); - const testStorageService = disposables.add(new TestStorageMainService(new NullLogService(), environmentService, disposables.add(new UserDataProfilesMainService(disposables.add(new StateService(SaveStrategy.DELAYED, environmentService, new NullLogService(), fileService)), disposables.add(uriIdentityService), environmentService, fileService, new NullLogService(), productService)), lifecycleMainService, fileService, uriIdentityService, nullCrossAppIPCService)); + const testStorageService = disposables.add(new TestStorageMainService(new NullLogService(), environmentService, disposables.add(new UserDataProfilesMainService(disposables.add(new StateService(SaveStrategy.DELAYED, environmentService, new NullLogService(), fileService)), disposables.add(uriIdentityService), environmentService, fileService, new NullLogService(), productService)), lifecycleMainService, fileService, uriIdentityService)); disposables.add(testStorageService.applicationStorage); @@ -282,90 +267,6 @@ suite('StorageMainService', function () { strictEqual(didCloseWorkspaceStorage, true); }); - test('application shared storage receives cross-app IPC changes', async function () { - const onDidReceiveMessage = disposables.add(new Emitter()); - const sentMessages: ICrossAppIPCMessage[] = []; - const crossAppIPCService: ICrossAppIPCService = { - _serviceBrand: undefined, - isSupported: true, - initialized: true, - connected: true, - isServer: false, - onDidConnect: Event.None, - onDidDisconnect: Event.None, - onDidReceiveMessage: onDidReceiveMessage.event, - sendMessage: (msg) => sentMessages.push(msg), - initialize: () => { } - }; - - const environmentService = new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService); - const fileService = disposables.add(new FileService(new NullLogService())); - const uriIdentityService = disposables.add(new UriIdentityService(fileService)); - const storageMainService = disposables.add(new TestStorageMainService(new NullLogService(), environmentService, disposables.add(new UserDataProfilesMainService(disposables.add(new StateService(SaveStrategy.DELAYED, environmentService, new NullLogService(), fileService)), disposables.add(uriIdentityService), environmentService, fileService, new NullLogService(), productService)), new TestLifecycleMainService(), fileService, uriIdentityService, crossAppIPCService)); - - const storage = storageMainService.applicationSharedStorage; - disposables.add(storage); - await storage.init(); - - // Verify that receiving a cross-app IPC message triggers a change event - let changeEvent: IStorageChangeEvent | undefined; - disposables.add(storage.onDidChangeStorage(e => { changeEvent = e; })); - - onDidReceiveMessage.fire({ - type: 'sharedStorage:changed', - data: { - changed: [['externalKey', 'externalValue']], - deleted: undefined - } - }); - - strictEqual(changeEvent?.key, 'externalKey'); - strictEqual(storage.get('externalKey'), 'externalValue'); - - // Verify that storing a value sends a cross-app IPC message - // (close flushes pending writes which triggers sendMessage) - const messagesBefore = sentMessages.length; - storage.set('testKey', 'testValue'); - await storage.close(); - ok(sentMessages.length > messagesBefore); - strictEqual(sentMessages[sentMessages.length - 1].type, 'sharedStorage:changed'); - - // Verify that messages received before init are ignored - const onDidReceiveMessage2 = disposables.add(new Emitter()); - const crossAppIPCService2: ICrossAppIPCService = { - ...crossAppIPCService, - onDidReceiveMessage: onDidReceiveMessage2.event, - }; - - const storageMainService2 = disposables.add(new TestStorageMainService(new NullLogService(), environmentService, disposables.add(new UserDataProfilesMainService(disposables.add(new StateService(SaveStrategy.DELAYED, environmentService, new NullLogService(), fileService)), disposables.add(new UriIdentityService(fileService)), environmentService, fileService, new NullLogService(), productService)), new TestLifecycleMainService(), fileService, disposables.add(new UriIdentityService(fileService)), crossAppIPCService2)); - - const storage2 = storageMainService2.applicationSharedStorage; - disposables.add(storage2); - - let preInitChangeReceived = false; - disposables.add(storage2.onDidChangeStorage(() => { preInitChangeReceived = true; })); - - // Fire message before init - onDidReceiveMessage2.fire({ - type: 'sharedStorage:changed', - data: { changed: [['preInitKey', 'preInitValue']] } - }); - - strictEqual(preInitChangeReceived, false); - - // Now init and verify subsequent messages work - await storage2.init(); - - onDidReceiveMessage2.fire({ - type: 'sharedStorage:changed', - data: { changed: [['postInitKey', 'postInitValue']] } - }); - - strictEqual(storage2.get('postInitKey'), 'postInitValue'); - - await storage2.close(); - }); - test('application shared storage closed onWillShutdown', async function () { const lifecycleMainService = new TestLifecycleMainService(); const storageMainService = createStorageService(lifecycleMainService); diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 1652c62e690c9..8cf5dceb0635d 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -86,7 +86,6 @@ export abstract class AbstractUpdateService implements IUpdateService { private _hasCheckedForOverwriteOnQuit: boolean = false; private readonly overwriteUpdatesCheckInterval = new IntervalTimer(); private _internalOrg: string | undefined = undefined; - protected _suspended = false; private readonly _onStateChange = new Emitter(); readonly onStateChange: Event = this._onStateChange.event; @@ -289,11 +288,6 @@ export abstract class AbstractUpdateService implements IUpdateService { async checkForUpdates(explicit: boolean): Promise { this.logService.trace('update#checkForUpdates, state = ', this.state.type); - if (this._suspended) { - this.logService.trace('update#checkForUpdates - suspended, skipping'); - return; - } - if (this.state.type !== StateType.Idle) { return; } @@ -301,19 +295,6 @@ export abstract class AbstractUpdateService implements IUpdateService { this.doCheckForUpdates(explicit); } - /** - * Prevents all update checks (automatic and manual) from running. - * Used by the cross-app update coordinator when another app owns - * the update client. - */ - suspend(): void { - this._suspended = true; - } - - resume(): void { - this._suspended = false; - } - async downloadUpdate(explicit: boolean): Promise { this.logService.trace('update#downloadUpdate, state = ', this.state.type); diff --git a/src/vs/platform/update/electron-main/crossAppUpdateIpc.ts b/src/vs/platform/update/electron-main/crossAppUpdateIpc.ts deleted file mode 100644 index 37582c9610ba5..0000000000000 --- a/src/vs/platform/update/electron-main/crossAppUpdateIpc.ts +++ /dev/null @@ -1,310 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; -import { ICrossAppIPCService } from '../../crossAppIpc/electron-main/crossAppIpcService.js'; -import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.js'; -import { ILogService } from '../../log/common/log.js'; -import { IUpdateService, State } from '../common/update.js'; -import { AbstractUpdateService } from './abstractUpdateService.js'; - -/** - * Message types exchanged between apps over crossAppIPC. - */ -const enum CrossAppUpdateMessageType { - /** Server → Client: Update state changed */ - StateChange = 'update/stateChange', - /** Client → Server: Request to check for updates */ - CheckForUpdates = 'update/checkForUpdates', - /** Client → Server: Request to download an available update */ - DownloadUpdate = 'update/downloadUpdate', - /** Client → Server: Request to apply a downloaded update */ - ApplyUpdate = 'update/applyUpdate', - /** Client → Server: Request to quit and install */ - QuitAndInstall = 'update/quitAndInstall', - /** Server → Client: Initial state sync after connection */ - InitialState = 'update/initialState', - /** Client → Server: Request initial state */ - RequestInitialState = 'update/requestInitialState', - /** Server → Client: Ask client to quit for an upcoming update */ - PrepareForQuit = 'update/prepareForQuit', - /** Client → Server: Client confirms it will quit */ - QuitConfirmed = 'update/quitConfirmed', - /** Client → Server: Client's quit was vetoed by the user */ - QuitVetoed = 'update/quitVetoed', -} - -interface CrossAppUpdateMessage { - type: CrossAppUpdateMessageType; - data?: State | boolean; -} - -/** - * Coordinates update ownership between host and embedded Electron apps - * using crossAppIPC. Whichever app starts first becomes the IPC server - * and owns the update client. The second app becomes the client and - * proxies update operations to the server. - * - * When only one app is running, it uses its local update service directly. - * When both apps are running, the IPC server owns the update client and - * the IPC client's local service is suspended to prevent duplicate - * checks and downloads. - * - * This class implements {@link IUpdateService} so it can be used directly - * as the update channel source for renderer processes while transparently - * handling the coordination. - */ -export class CrossAppUpdateCoordinator extends Disposable implements IUpdateService { - - declare readonly _serviceBrand: undefined; - - private mode: 'standalone' | 'server' | 'client' = 'standalone'; - - private _state: State; - - private readonly _onStateChange = this._register(new Emitter()); - readonly onStateChange: Event = this._onStateChange.event; - - /** Disposed when entering client mode, re-registered on disconnect. */ - private localStateListener: IDisposable | undefined; - - /** True when the server has sent PrepareForQuit and is waiting for a response. */ - private pendingQuitAndInstall = false; - - get state(): State { return this._state; } - - constructor( - private readonly localUpdateService: AbstractUpdateService, - private readonly logService: ILogService, - private readonly lifecycleMainService: ILifecycleMainService, - private readonly crossAppIPCService: ICrossAppIPCService, - ) { - super(); - - // Start with the local service's current state - this._state = this.localUpdateService.state; - - // Track local service state changes (used in standalone/server mode) - this.registerLocalStateListener(); - - // Subscribe to cross-app IPC events - this._register(this.crossAppIPCService.onDidConnect(isServer => { - this.handleConnect(isServer); - })); - - // If the service is already connected (e.g. another consumer initialized - // it earlier), run the connect logic immediately. - if (this.crossAppIPCService.connected) { - this.handleConnect(this.crossAppIPCService.isServer); - } - - this._register(this.crossAppIPCService.onDidReceiveMessage(msg => { - this.handleMessage(msg as CrossAppUpdateMessage); - })); - - this._register(this.crossAppIPCService.onDidDisconnect(reason => { - this.logService.info(`CrossAppUpdateCoordinator: disconnected (${reason}), was ${this.mode}`); - - if (this.mode === 'client') { - this.localUpdateService.resume(); - this.registerLocalStateListener(); - this.updateState(this.localUpdateService.state); - } - - if (this.mode === 'server' && this.pendingQuitAndInstall) { - this.logService.info('CrossAppUpdateCoordinator: client disconnected during pending quit, treating as confirmed'); - this.pendingQuitAndInstall = false; - this.mode = 'standalone'; - this.localUpdateService.quitAndInstall(); - return; - } - - this.mode = 'standalone'; - })); - } - - private handleConnect(isServer: boolean): void { - this.logService.info(`CrossAppUpdateCoordinator: connected (isServer=${isServer})`); - - if (isServer) { - this.mode = 'server'; - this.broadcastState(this.localUpdateService.state); - } else { - this.mode = 'client'; - this.localUpdateService.suspend(); - this.localStateListener?.dispose(); - this.localStateListener = undefined; - this.sendMessage({ type: CrossAppUpdateMessageType.RequestInitialState }); - } - } - - private registerLocalStateListener(): void { - this.localStateListener = this.localUpdateService.onStateChange(state => { - this.updateState(state); - this.broadcastState(state); - }); - } - - private handleMessage(msg: CrossAppUpdateMessage): void { - this.logService.trace(`CrossAppUpdateCoordinator: received ${msg.type} (mode=${this.mode})`); - - switch (msg.type) { - // --- Messages handled by the client --- - case CrossAppUpdateMessageType.StateChange: - case CrossAppUpdateMessageType.InitialState: - if (this.mode === 'client') { - this.updateState(msg.data as State); - } - break; - - case CrossAppUpdateMessageType.PrepareForQuit: - if (this.mode === 'client') { - this.logService.info('CrossAppUpdateCoordinator: server requested quit for update'); - this.lifecycleMainService.quit().then(veto => { - if (veto) { - this.logService.info('CrossAppUpdateCoordinator: client quit was vetoed'); - this.sendMessage({ type: CrossAppUpdateMessageType.QuitVetoed }); - } else { - this.sendMessage({ type: CrossAppUpdateMessageType.QuitConfirmed }); - } - }); - } - break; - - // --- Messages handled by the server --- - case CrossAppUpdateMessageType.RequestInitialState: - if (this.mode === 'server') { - this.sendMessage({ type: CrossAppUpdateMessageType.InitialState, data: this.localUpdateService.state }); - } - break; - - case CrossAppUpdateMessageType.CheckForUpdates: - if (this.mode === 'server') { - this.localUpdateService.checkForUpdates(typeof msg.data === 'boolean' ? msg.data : true); - } - break; - - case CrossAppUpdateMessageType.DownloadUpdate: - if (this.mode === 'server') { - this.localUpdateService.downloadUpdate(typeof msg.data === 'boolean' ? msg.data : true); - } - break; - - case CrossAppUpdateMessageType.ApplyUpdate: - if (this.mode === 'server') { - this.localUpdateService.applyUpdate(); - } - break; - - case CrossAppUpdateMessageType.QuitAndInstall: - if (this.mode === 'server') { - this.doCoordinatedQuitAndInstall(); - } - break; - - case CrossAppUpdateMessageType.QuitConfirmed: - if (this.mode === 'server') { - this.logService.info('CrossAppUpdateCoordinator: client confirmed quit, proceeding with quitAndInstall'); - this.pendingQuitAndInstall = false; - this.localUpdateService.quitAndInstall(); - } - break; - - case CrossAppUpdateMessageType.QuitVetoed: - if (this.mode === 'server') { - this.logService.info('CrossAppUpdateCoordinator: client vetoed quit, aborting quitAndInstall'); - this.pendingQuitAndInstall = false; - } - break; - } - } - - private updateState(state: State): void { - this._state = state; - this._onStateChange.fire(state); - } - - private broadcastState(state: State): void { - if (this.mode === 'server') { - this.sendMessage({ type: CrossAppUpdateMessageType.StateChange, data: state }); - } - } - - private sendMessage(msg: CrossAppUpdateMessage): void { - this.crossAppIPCService.sendMessage(msg); - } - - // --- IUpdateService implementation --- - - async checkForUpdates(explicit: boolean): Promise { - if (this.mode === 'client') { - this.sendMessage({ type: CrossAppUpdateMessageType.CheckForUpdates, data: explicit }); - } else { - await this.localUpdateService.checkForUpdates(explicit); - } - } - - async downloadUpdate(explicit: boolean): Promise { - if (this.mode === 'client') { - this.sendMessage({ type: CrossAppUpdateMessageType.DownloadUpdate, data: explicit }); - } else { - await this.localUpdateService.downloadUpdate(explicit); - } - } - - async applyUpdate(): Promise { - if (this.mode === 'client') { - this.sendMessage({ type: CrossAppUpdateMessageType.ApplyUpdate }); - } else { - await this.localUpdateService.applyUpdate(); - } - } - - /** - * Coordinates quit-and-install when a peer is connected. - * Asks the client to quit first; only proceeds with the server's - * quitAndInstall if the client confirms. If the client's quit is - * vetoed (e.g. unsaved editors), the whole operation is aborted. - * - * If no peer is connected (standalone), proceeds directly. - */ - private doCoordinatedQuitAndInstall(): void { - if (this.crossAppIPCService.connected) { - // Ask the client to quit; it will respond with QuitConfirmed/QuitVetoed, - // or disconnect (treated as implicit confirmation). - this.pendingQuitAndInstall = true; - this.sendMessage({ type: CrossAppUpdateMessageType.PrepareForQuit }); - } else { - this.localUpdateService.quitAndInstall(); - } - } - - async quitAndInstall(): Promise { - if (this.mode === 'client') { - // Ask the server to start the coordinated quit flow - this.sendMessage({ type: CrossAppUpdateMessageType.QuitAndInstall }); - } else { - this.doCoordinatedQuitAndInstall(); - } - } - - async isLatestVersion(): Promise { - return this.localUpdateService.isLatestVersion(); - } - - async _applySpecificUpdate(packagePath: string): Promise { - return this.localUpdateService._applySpecificUpdate(packagePath); - } - - async setInternalOrg(internalOrg: string | undefined): Promise { - return this.localUpdateService.setInternalOrg(internalOrg); - } - - override dispose(): void { - this.localStateListener?.dispose(); - super.dispose(); - } -} diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 0c5efaf80b817..e92c54e2fe8c2 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -165,11 +165,6 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau private onUpdateAvailable(): void { this.logService.trace('update#onUpdateAvailable - Electron autoUpdater reported update available'); - if (this._suspended) { - this.logService.trace('update#onUpdateAvailable - suspended, ignoring'); - return; - } - if (this.state.type !== StateType.CheckingForUpdates && this.state.type !== StateType.Overwriting) { return; } @@ -178,7 +173,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau } private onUpdateDownloaded(update: IUpdate): void { - if (this._suspended || this.state.type !== StateType.Downloading) { + if (this.state.type !== StateType.Downloading) { return; } @@ -191,11 +186,6 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau private onUpdateNotAvailable(): void { this.logService.trace('update#onUpdateNotAvailable - Electron autoUpdater reported no update available'); - if (this._suspended) { - this.logService.trace('update#onUpdateNotAvailable - suspended, ignoring'); - return; - } - if (this.state.type !== StateType.CheckingForUpdates) { return; } diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 6b97e209fb7be..a7851933a4b68 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -15,7 +15,6 @@ import { memoize } from '../../../base/common/decorators.js'; import { hash } from '../../../base/common/hash.js'; import * as path from '../../../base/common/path.js'; import { basename } from '../../../base/common/path.js'; -import { INodeProcess } from '../../../base/common/platform.js'; import { transform } from '../../../base/common/stream.js'; import { URI } from '../../../base/common/uri.js'; import { checksum } from '../../../base/node/crypto.js'; @@ -163,12 +162,11 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun const versionedResourcesFolder = this.productService.commit.substring(0, 10); const innoUpdater = path.join(exeDir, versionedResourcesFolder, 'tools', 'inno_updater.exe'); const exeName = basename(exePath); - const siblingExeName = this.productService.win32SiblingExeBasename ? `${this.productService.win32SiblingExeBasename}.exe` : ''; // Unblock inno_updater --gc when our context-menu COM surrogate keeps a // handle on the orphan commit folder. See https://github.com/microsoft/vscode/issues/294546. await this.killContextMenuComSurrogate(); await new Promise(resolve => { - const child = spawn(innoUpdater, ['--gc', exePath, versionedResourcesFolder, exeName, siblingExeName], { + const child = spawn(innoUpdater, ['--gc', exePath, versionedResourcesFolder, exeName], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: true, timeout: 2 * 60 * 1000 @@ -421,13 +419,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun this.setState(State.Idle(getUpdateType())); }); - // The InnoSetup installer creates the -ready mutex using the host app's - // mutex name ({#AppMutex}). When running as the embedded app, use - // win32SetupMutexName (the host's mutex) to find the correct signal. - const setupMutexName = (process as INodeProcess).isEmbeddedApp - ? this.productService.win32SetupMutexName - : this.productService.win32MutexName; - const readyMutexName = `${setupMutexName}-ready`; + const readyMutexName = `${this.productService.win32MutexName}-ready`; const mutex = await import('@vscode/windows-mutex'); this.updateCancellationTokenSource?.dispose(true); diff --git a/src/vs/platform/url/electron-main/electronUrlListener.ts b/src/vs/platform/url/electron-main/electronUrlListener.ts index fe9ce0b757d5f..77ac5fa75c704 100644 --- a/src/vs/platform/url/electron-main/electronUrlListener.ts +++ b/src/vs/platform/url/electron-main/electronUrlListener.ts @@ -7,7 +7,7 @@ import { app, Event as ElectronEvent } from 'electron'; import { disposableTimeout } from '../../../base/common/async.js'; import { Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; -import { INodeProcess, isWindows } from '../../../base/common/platform.js'; +import { isWindows } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; import { ILogService } from '../../log/common/log.js'; @@ -51,8 +51,7 @@ export class ElectronURLListener extends Disposable { // Windows: install as protocol handler // Skip in portable mode: the registered command wouldn't preserve // portable mode settings, causing issues with OAuth flows. - // Skip for embedded apps: protocol handler is registered at install time. - if (isWindows && !environmentMainService.isPortable && !(process as INodeProcess).isEmbeddedApp) { + if (isWindows && !environmentMainService.isPortable) { const windowsParameters = environmentMainService.isBuilt ? [] : [`"${environmentMainService.appRoot}"`]; windowsParameters.push('--open-url', '--'); app.setAsDefaultProtocolClient(productService.urlProtocol, process.execPath, windowsParameters); diff --git a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts index db523ea45d1c5..433fc3c46e071 100644 --- a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../base/common/event.js'; -import { INodeProcess } from '../../../base/common/platform.js'; import { joinPath } from '../../../base/common/resources.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; import { IFileService } from '../../files/common/files.js'; @@ -48,25 +47,10 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme } protected override createDefaultProfile(): IUserDataProfile { - const defaultProfile = { + return { ...super.createDefaultProfile(), agentPluginsHome: this.agentPluginsHome }; - if (!(process as INodeProcess).isEmbeddedApp) { - return defaultProfile; - } - const hostUserRoamingDataHome = this.environmentService.parentAppUserRoamingDataHome; - if (!hostUserRoamingDataHome) { - return defaultProfile; - } - const hostAgentPluginsHome = getParentAppAgentPluginsPath(this.nativeEnvironmentService); - return { - ...defaultProfile, - keybindingsResource: joinPath(hostUserRoamingDataHome, 'keybindings.json'), - promptsHome: joinPath(hostUserRoamingDataHome, 'prompts'), - mcpResource: joinPath(hostUserRoamingDataHome, 'mcp.json'), - agentPluginsHome: hostAgentPluginsHome ? URI.file(hostAgentPluginsHome) : this.agentPluginsHome - }; } async createAgentsWindowProfile(): Promise { @@ -87,14 +71,6 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme } } -function getParentAppAgentPluginsPath(environmentService: INativeEnvironmentService): string | undefined { - const hostUserHome = environmentService.parentAppUserHome; - if (!hostUserHome) { - return undefined; - } - return getAgentPluginsPath(environmentService.args, hostUserHome); -} - function getAgentPluginsPath(args: NativeParsedArgs, userHome: URI): string { const cliAgentPluginsDir = args['agent-plugins-dir']; if (cliAgentPluginsDir) { diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts index 319ff8fb6c458..37cd505a38bf8 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts @@ -26,8 +26,6 @@ class TestEnvironmentService extends AbstractNativeEnvironmentService { userDataDir, homeDir: userDataDir, tmpDir: userDataDir, - parentAppUserDataDir: undefined, - parentAppUserHomeDir: undefined }; super(Object.create(null), paths, { _serviceBrand: undefined, ...product }); } diff --git a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts index 53399df186a0b..c8ec2ae6af101 100644 --- a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts +++ b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts @@ -27,8 +27,6 @@ class TestEnvironmentService extends AbstractNativeEnvironmentService { userDataDir, homeDir: userDataDir, tmpDir: userDataDir, - parentAppUserDataDir: undefined, - parentAppUserHomeDir: undefined }; super(Object.create(null), paths, { _serviceBrand: undefined, ...product }); } diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 36511b63be60e..291648bca96e3 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -442,9 +442,6 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native homeDir: string; tmpDir: string; userDataDir: string; - isEmbeddedApp?: boolean; - parentAppUserDataDir?: string; - parentAppUserHomeDir?: string; partsSplash?: IPartsSplash; diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 07551319546ce..7aeeb120987f9 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -11,7 +11,7 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { FileAccess, Schemas } from '../../../base/common/network.js'; import { getMarks, mark } from '../../../base/common/performance.js'; -import { isTahoeOrNewer, isLinux, isMacintosh, isWindows, INodeProcess } from '../../../base/common/platform.js'; +import { isTahoeOrNewer, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { release } from 'os'; @@ -52,6 +52,7 @@ export interface IWindowCreationOptions { readonly state: IWindowState; readonly extensionDevelopmentPath?: string[]; readonly isExtensionTestHost?: boolean; + readonly isSessionsWindow?: boolean; } interface ITouchBarSegment extends electron.SegmentedControlSegment { @@ -707,8 +708,8 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { additionalArguments: [`--vscode-window-config=${this.configObjectUrl.resource.toString()}`], v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none' }; - if ((process as INodeProcess).isEmbeddedApp) { - webPreferences.backgroundThrottling = false; // disable for sub-app + if (config.isSessionsWindow) { + webPreferences.backgroundThrottling = false; // keep agents window responsive when in background } const options = instantiationService.invokeFunction(defaultBrowserWindowOptions, this.windowState, undefined, webPreferences); diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 3f6af0feb38f7..3899541ec3b7c 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -7,7 +7,7 @@ import electron, { Display, Rectangle } from 'electron'; import { Color } from '../../../base/common/color.js'; import { Event } from '../../../base/common/event.js'; import { join } from '../../../base/common/path.js'; -import { INodeProcess, IProcessEnvironment, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; +import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; @@ -180,11 +180,6 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt } else if (isWindows) { if (!environmentMainService.isBuilt) { options.icon = join(environmentMainService.appRoot, 'resources/win32/code_150x150.png'); // only when running out of sources on Windows - } else if ((process as INodeProcess).isEmbeddedApp) { - // For sub app the proxy executable acts as a launcher to the main executable whose - // icon will be used when creating windows if the following override is not set. - // This avoids sharing icon with the main application. - options.icon = join(environmentMainService.appRoot, 'resources/win32/sessions.ico'); } } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index e9072892638d7..8f56afc29ec23 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -17,7 +17,7 @@ import { Disposable, DisposableStore, IDisposable } from '../../../base/common/l import { Schemas } from '../../../base/common/network.js'; import { basename, join, normalize, posix } from '../../../base/common/path.js'; import { getMarks, mark } from '../../../base/common/performance.js'; -import { INodeProcess, IProcessEnvironment, isMacintosh, isWindows, OS } from '../../../base/common/platform.js'; +import { IProcessEnvironment, isMacintosh, isWindows, OS } from '../../../base/common/platform.js'; import { cwd } from '../../../base/common/process.js'; import { extUriBiasedIgnorePathCase, isEqual, isEqualAuthority, normalizePath, originalFSPath, removeTrailingPathSeparator } from '../../../base/common/resources.js'; import { assertReturnsDefined } from '../../../base/common/types.js'; @@ -334,12 +334,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic async open(openConfig: IOpenConfiguration): Promise { this.logService.trace('windowsManager#open'); - // Take care of agents app specially - const isAgentsApp = (process as INodeProcess).isEmbeddedApp; - if (isAgentsApp) { - openConfig = await this.ensureAgentsWindow(openConfig); - } - // Make sure addMode/removeMode is only enabled if we have an active window if ((openConfig.addMode || openConfig.removeMode) && (openConfig.initialStartup || !this.getLastActiveWindow())) { openConfig.addMode = false; @@ -408,7 +402,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // These are windows to restore because of hot-exit or from previous session (only performed once on startup!) - if (openConfig.initialStartup && !isAgentsApp /* skipped for agents app */) { + if (openConfig.initialStartup) { // Untitled workspaces are always restored untitledWorkspacesToRestore.push(...this.workspacesManagementMainService.getUntitledWorkspaces()); @@ -498,9 +492,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Handle ` chat` this.handleChatRequest(openConfig, usedWindows); - // Handle ` --open-chat-session` - this.handleOpenChatSession(openConfig, usedWindows); - return usedWindows; } @@ -544,17 +535,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } - private handleOpenChatSession(openConfig: IOpenConfiguration, usedWindows: ICodeWindow[]): void { - const sessionUri = openConfig.cli['open-chat-session']; - if (!sessionUri || usedWindows.length === 0) { - return; - } - - const window = usedWindows[0]; - window.sendWhenReady('vscode:openChatSession', CancellationToken.None, sessionUri); - window.focus(); - } - private async doOpen( openConfig: IOpenConfiguration, workspacesToOpen: IWorkspacePathToOpen[], @@ -1570,9 +1550,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic homeDir: this.environmentMainService.userHome.with({ scheme: Schemas.file }).fsPath, tmpDir: this.environmentMainService.tmpDir.with({ scheme: Schemas.file }).fsPath, userDataDir: this.environmentMainService.userDataPath, - isEmbeddedApp: this.environmentMainService.isEmbeddedApp, - parentAppUserDataDir: this.environmentMainService.parentAppUserRoamingDataHome?.with({ scheme: Schemas.file }).fsPath, - parentAppUserHomeDir: this.environmentMainService.parentAppUserHome?.with({ scheme: Schemas.file }).fsPath, remoteAuthority: options.remoteAuthority, workspace: options.workspace, @@ -1618,7 +1595,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const createdWindow = window = this.instantiationService.createInstance(CodeWindow, { state, extensionDevelopmentPath: configuration.extensionDevelopmentPath, - isExtensionTestHost: !!configuration.extensionTestsPath + isExtensionTestHost: !!configuration.extensionTestsPath, + isSessionsWindow: configuration.isSessionsWindow }); mark('code/didCreateCodeWindow'); diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index 68f71f1cf2f4d..560f5ca44af8b 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -10,7 +10,7 @@ import { Emitter, Event as CommonEvent } from '../../../base/common/event.js'; import { normalizeDriveLetter, splitRecentLabel } from '../../../base/common/labels.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { Schemas } from '../../../base/common/network.js'; -import { isMacintosh, INodeProcess, isWindows } from '../../../base/common/platform.js'; +import { isMacintosh, isWindows } from '../../../base/common/platform.js'; import { basename, extUriBiasedIgnorePathCase, originalFSPath } from '../../../base/common/resources.js'; import { URI } from '../../../base/common/uri.js'; import { Promises } from '../../../base/node/pfs.js'; @@ -108,7 +108,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Add to recent documents (Windows only, macOS later) // Skip in portable mode to avoid leaving traces on the machine // Skip in the sessions app to avoid polluting the jump list - if (isWindows && recent.fileUri.scheme === Schemas.file && !this.environmentMainService.isPortable && !(process as INodeProcess).isEmbeddedApp) { + if (isWindows && recent.fileUri.scheme === Schemas.file && !this.environmentMainService.isPortable) { app.addRecentDocument(recent.fileUri.fsPath); } } @@ -326,11 +326,6 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa return; } - // Skip in the sessions app to avoid polluting the jump list - if ((process as INodeProcess).isEmbeddedApp) { - return; - } - await this.updateWindowsJumpList(); this._register(this.onDidChangeRecentlyOpened(() => this.updateWindowsJumpList())); } @@ -461,11 +456,6 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa return; } - // Skip in the sessions app to avoid polluting the dock - if ((process as INodeProcess).isEmbeddedApp) { - return; - } - // We clear all documents first to ensure an up-to-date view on the set. Since entries // can get deleted on disk, this ensures that the list is always valid app.clearRecentDocuments(); diff --git a/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts index 70e37d5fcf15c..be602b61d4993 100644 --- a/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -1401,12 +1401,18 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement if (this._newSession === newSession) { this._newSession = undefined; } + // Clear the pending session before firing the replace event so + // that any synchronous listener calling getSessions() sees only + // the committed session and not both. + this._pendingSession = undefined; this._onDidReplaceSession.fire({ from: skeleton, to: committedSession }); return committedSession; } } catch { // Connection lost or timeout — fall through to the failure cleanup. } finally { + // Defensive clear: covers the failure path where the try block + // never reached the explicit clear above. this._pendingSession = undefined; } diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 8b8254034daaa..35f78a8ec5e05 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -1352,7 +1352,7 @@ class ChangesPickerActionItem extends ActionWidgetDropdownActionViewItem { }, }; - super(action, { actionProvider, listOptions: {} }, actionWidgetService, keybindingService, contextKeyService, telemetryService); + super(action, { actionProvider, listOptions: { detailItemHeight: 44 } }, actionWidgetService, keybindingService, contextKeyService, telemetryService); this._register(autorun(reader => { viewModel.versionModeObs.read(reader); diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css index 17899477bde53..91396a31fc18c 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css @@ -370,44 +370,41 @@ } } -/* Shimmer effect for in-progress session titles when not selected */ - -.monaco-list-row:not(.selected) .session-item.in-progress .session-title { - background: linear-gradient( - 90deg, - var(--vscode-strongForeground) 0%, - var(--vscode-strongForeground) 30%, - var(--vscode-chat-thinkingShimmer) 50%, - var(--vscode-strongForeground) 70%, - var(--vscode-strongForeground) 100% - ); - background-size: 400% 100%; - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - animation: session-title-shimmer 3s linear infinite; -} - -.vs-dark .monaco-list-row:not(.selected) .session-item.in-progress .session-title, -.hc-black .monaco-list-row:not(.selected) .session-item.in-progress .session-title { - background-image: linear-gradient( - 90deg, - var(--vscode-strongForeground) 0%, - var(--vscode-strongForeground) 30%, - var(--vscode-descriptionForeground) 50%, - var(--vscode-strongForeground) 70%, - var(--vscode-strongForeground) 100% - ); -} +/* Shimmer effect for in-progress session titles when not selected. */ +/* Gated behind @supports because some environments apply */ +/* `-webkit-text-fill-color: transparent` without honoring */ +/* `background-clip: text`, which renders gradient-filled blocks */ +/* instead of clipped text. Also disabled under reduced motion. */ + +@supports ((background-clip: text) or (-webkit-background-clip: text)) { + @media not (prefers-reduced-motion: reduce) { + .monaco-list-row:not(.selected) .session-item.in-progress .session-title { + background: linear-gradient( + 90deg, + var(--vscode-strongForeground) 0%, + var(--vscode-strongForeground) 30%, + var(--vscode-chat-thinkingShimmer) 50%, + var(--vscode-strongForeground) 70%, + var(--vscode-strongForeground) 100% + ); + background-size: 400% 100%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: session-title-shimmer 3s linear infinite; + } -@media (prefers-reduced-motion: reduce) { - .monaco-list-row:not(.selected) .session-item.in-progress .session-title { - animation: none; - background: none; - background-clip: border-box; - -webkit-background-clip: border-box; - color: var(--vscode-strongForeground); - -webkit-text-fill-color: var(--vscode-strongForeground); + .vs-dark .monaco-list-row:not(.selected) .session-item.in-progress .session-title, + .hc-black .monaco-list-row:not(.selected) .session-item.in-progress .session-title { + background-image: linear-gradient( + 90deg, + var(--vscode-strongForeground) 0%, + var(--vscode-strongForeground) 30%, + var(--vscode-descriptionForeground) 50%, + var(--vscode-strongForeground) 70%, + var(--vscode-strongForeground) 100% + ); + } } } diff --git a/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts index 95b679003a455..065515701a963 100644 --- a/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts @@ -127,12 +127,15 @@ class SessionsManagementService extends Disposable implements ISessionsManagemen private onDidReplaceSession(from: ISession, to: ISession): void { if (this._activeSession.get()?.sessionId === from.sessionId) { this.setActiveSession(to); - this._onDidChangeSessions.fire({ - added: [], - removed: from.sessionId === to.sessionId ? [] : [from], - changed: [to], - }); } + // Always fire the change event so the SessionsList refreshes even when + // the user navigated to a different session while the new one was + // being created (which is how duplicate rows appeared in the list). + this._onDidChangeSessions.fire({ + added: [], + removed: from.sessionId === to.sessionId ? [] : [from], + changed: [to], + }); } private onDidChangeSessionsFromSessionsProviders(e: ISessionChangeEvent): void { diff --git a/src/vs/sessions/services/vscode/browser/themeImporterService.ts b/src/vs/sessions/services/vscode/browser/themeImporterService.ts deleted file mode 100644 index 63bf1f07b6e14..0000000000000 --- a/src/vs/sessions/services/vscode/browser/themeImporterService.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IThemeImporterService, IThemePreviewResult } from '../common/themeImporter.js'; - -/** - * Browser/web no-op implementation of {@link IThemeImporterService}. The web - * variant of the Agents app does not have access to a parent VS Code - * installation, so theme importing is unavailable. - */ -class BrowserThemeImporterService implements IThemeImporterService { - - declare readonly _serviceBrand: undefined; - - async getVSCodeTheme(): Promise { - return undefined; - } - - async previewVSCodeTheme(): Promise { - return undefined; - } -} - -registerSingleton(IThemeImporterService, BrowserThemeImporterService, InstantiationType.Delayed); diff --git a/src/vs/sessions/services/vscode/common/themeImporter.ts b/src/vs/sessions/services/vscode/common/themeImporter.ts deleted file mode 100644 index 9b1df8a9fe22f..0000000000000 --- a/src/vs/sessions/services/vscode/common/themeImporter.ts +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; - -/** The VS Code configuration key for the active color theme. */ -export const COLOR_THEME_SETTINGS_ID = 'workbench.colorTheme'; - -export const IThemeImporterService = createDecorator('IThemeImporterService'); - -/** - * Result of previewing the parent VS Code's color theme. - */ -export interface IThemePreviewResult extends IDisposable { - /** - * Permanently imports the previewed theme into the Agents app by - * copying the providing extension and installing it from there. - */ - apply(): Promise; -} - -/** - * Service that reads the parent VS Code installation's active color theme - * and can import it into the Agents app — using the providing extension - * from the parent VS Code installation if necessary. - */ -export interface IThemeImporterService { - - readonly _serviceBrand: undefined; - - /** - * Resolves the parent VS Code's active color theme. Returns `undefined` - * when the parent settings cannot be read or the theme is already one of - * the onboarding themes displayed in the theme picker. - */ - getVSCodeTheme(): Promise; - - /** - * Temporarily installs the providing extension from the host's extensions - * directory and applies the VS Code theme. Returns a cached - * {@link IThemePreviewResult} to apply or dispose the preview. Returns - * `undefined` if the theme is already available or cannot be resolved. - */ - previewVSCodeTheme(): Promise; -} diff --git a/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts b/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts deleted file mode 100644 index 92342b992cbcc..0000000000000 --- a/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts +++ /dev/null @@ -1,257 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { parse as parseJSONC } from '../../../../base/common/jsonc.js'; -import { getErrorMessage } from '../../../../base/common/errors.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { joinPath } from '../../../../base/common/resources.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; -import { IExtensionManagementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { IExtensionsScannerService } from '../../../../platform/extensionManagement/common/extensionsScannerService.js'; -import { ExtensionType, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IWorkbenchThemeService } from '../../../../workbench/services/themes/common/workbenchThemeService.js'; -import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js'; -import { IThemeImporterService, IThemePreviewResult, COLOR_THEME_SETTINGS_ID } from '../common/themeImporter.js'; -import { INativeWorkbenchEnvironmentService } from '../../../../workbench/services/environment/electron-browser/environmentService.js'; - -/** - * Describes a color theme from the parent VS Code installation. - */ -interface IParentThemeInfo { - /** The settingsId of the theme (e.g. "Dark Modern", "Monokai"). */ - readonly settingsId: string; - /** - * The location of the extension that provides this theme. - * `undefined` when the theme is already available (built-in or installed). - */ - readonly extensionLocation: URI | undefined; -} - -class ThemeImporterService extends Disposable implements IThemeImporterService { - - declare readonly _serviceBrand: undefined; - - private _parentThemePromise: Promise | undefined; - private _previewPromise: Promise | undefined; - - constructor( - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, - @IFileService private readonly fileService: IFileService, - @ILogService private readonly logService: ILogService, - @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, - ) { - super(); - } - - async getVSCodeTheme(): Promise { - if (!this._parentThemePromise) { - this._parentThemePromise = this._resolveVSCodeTheme(); - } - const themeInfo = await this._parentThemePromise; - return themeInfo?.settingsId; - } - - async previewVSCodeTheme(): Promise { - if (!this._previewPromise) { - this._previewPromise = this._doPreview(); - // Clear cache if preview resolved to undefined so callers can retry - this._previewPromise.then(result => { - if (!result) { - this._previewPromise = undefined; - } - }); - } - return this._previewPromise; - } - - private async _doPreview(): Promise { - try { - const theme = await this._getVSCodeTheme(); - if (!theme) { - return undefined; - } - - const installed = await this._installFromHostLocation(theme); - await this._setTheme(theme.settingsId); - let applied = false; - - return { - apply: async () => { - applied = true; - this._previewPromise = undefined; - await this._apply(theme); - }, - dispose: () => { - this._previewPromise = undefined; - if (applied) { - return; - } - void this._disposePreview(installed); - }, - }; - } catch (err) { - this.logService.error('[VSCodeThemeImporter] Failed to preview theme:', err); - return undefined; - } - } - - private async _apply(theme: IParentThemeInfo): Promise { - try { - if (!theme.extensionLocation) { - return; - } - - // Copy extension to Agents app's own extensions directory - const extensionsHome = URI.file(this.environmentService.extensionsPath); - const folderName = theme.extensionLocation.path.split('/').pop()!; - const targetLocation = joinPath(extensionsHome, folderName); - - this.logService.info(`[VSCodeThemeImporter] Copying extension to ${targetLocation.toString()}`); - await this.fileService.copy(theme.extensionLocation, targetLocation, true); - - // Replace install from the copied location - const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; - await this.extensionManagementService.installFromLocation(targetLocation, profileLocation); - } catch (err) { - this.logService.error('[VSCodeThemeImporter] Failed to apply theme:', err); - } - } - - private async _disposePreview(installed: ILocalExtension | undefined): Promise { - if (!installed) { - return; - } - try { - const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; - await this.extensionManagementService.uninstall(installed, { profileLocation }); - } catch (err) { - this.logService.warn('[VSCodeThemeImporter] Failed to uninstall preview extension:', err); - } - } - - private async _getVSCodeTheme(): Promise { - if (!this._parentThemePromise) { - this._parentThemePromise = this._resolveVSCodeTheme(); - } - return this._parentThemePromise; - } - - /** - * Installs the extension from the host's extensions directory if needed. - * Returns the installed extension, or `undefined` if no install was needed. - */ - private async _installFromHostLocation(theme: IParentThemeInfo): Promise { - if (!theme.extensionLocation) { - return undefined; - } - this.logService.info(`[VSCodeThemeImporter] Installing extension from ${theme.extensionLocation.toString()}`); - const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; - return this.extensionManagementService.installFromLocation(theme.extensionLocation, profileLocation); - } - - private async _setTheme(themeSettingsId: string): Promise { - const allThemes = await this.themeService.getColorThemes(); - const match = allThemes.find(t => t.settingsId === themeSettingsId); - if (match) { - await this.themeService.setColorTheme(match.id, ConfigurationTarget.USER); - } else { - this.logService.warn(`[VSCodeThemeImporter] Theme ${themeSettingsId} not found after install`); - } - } - - private async _resolveVSCodeTheme(): Promise { - try { - const settingsId = await this._readVSCodeThemeId(); - if (!settingsId) { - return undefined; - } - - // Find the extension providing this theme by scanning the host's extensions - const extensionLocation = await this._findThemeExtension(settingsId); - - return { settingsId, extensionLocation }; - } catch (err) { - this.logService.warn('[VSCodeThemeImporter] Failed to resolve VS Code theme:', err); - return undefined; - } - } - - /** - * Scans the host VS Code's extensions directory to find which extension - * provides the given theme. Returns the extension location URI, or - * `undefined` if the theme is already available (built-in or installed). - */ - private async _findThemeExtension(themeSettingsId: string): Promise { - const allThemes = await this.themeService.getColorThemes(); - if (allThemes.find(t => t.settingsId === themeSettingsId)) { - return undefined; - } - - const hostExtensionsHome = this.environmentService.parentAppExtensionsHome; - if (!hostExtensionsHome) { - return undefined; - } - - try { - const scanned = await this.extensionsScannerService.scanOneOrMultipleExtensions( - hostExtensionsHome, - ExtensionType.User, - {}, - ); - for (const ext of scanned) { - if (this._extensionProvidesTheme(ext.manifest, themeSettingsId)) { - return ext.location; - } - } - } catch (err) { - this.logService.warn('[VSCodeThemeImporter] Failed to scan host extensions:', err); - } - - return undefined; - } - - private _extensionProvidesTheme(manifest: IExtensionManifest, themeSettingsId: string): boolean { - const themes = manifest.contributes?.themes; - if (!Array.isArray(themes)) { - return false; - } - return themes.some(t => { - const theme = t as { id?: string; label?: string }; - return theme.id === themeSettingsId || theme.label === themeSettingsId; - }); - } - - private async _readVSCodeThemeId(): Promise { - const hostDataHome = this.environmentService.parentAppUserRoamingDataHome; - if (!hostDataHome) { - this.logService.warn('[VSCodeThemeImporter] Host user data home is not available'); - return undefined; - } - - try { - const settingsUri = joinPath(hostDataHome, 'settings.json'); - const content = await this.fileService.readFile(settingsUri); - const settings = parseJSONC>(content.value.toString()); - const themeId = settings[COLOR_THEME_SETTINGS_ID]; - if (typeof themeId === 'string') { - return themeId; - } - this.logService.warn('[VSCodeThemeImporter] workbench.colorTheme is not set in host settings.json', themeId, settingsUri.toString()); - return undefined; - } catch (e) { - this.logService.warn('[VSCodeThemeImporter] Failed to read host settings.json, falling back to default theme', getErrorMessage(e)); - return undefined; - } - } -} - -registerSingleton(IThemeImporterService, ThemeImporterService, InstantiationType.Delayed); diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 9c88bb5abcaa2..8c28636d6b61a 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -200,7 +200,6 @@ import '../workbench/contrib/policyExport/electron-browser/policyExport.contribu //#region --- sessions contributions import './electron-browser/sessions.desktop.contribution.js'; -import './services/vscode/electron-browser/themeImporterService.js'; // Remote Agent Host import '../platform/agentHost/electron-browser/agentHostService.js'; diff --git a/src/vs/sessions/sessions.web.main.ts b/src/vs/sessions/sessions.web.main.ts index cf54fb54dc4a8..7d1421489769b 100644 --- a/src/vs/sessions/sessions.web.main.ts +++ b/src/vs/sessions/sessions.web.main.ts @@ -71,7 +71,6 @@ import '../platform/extensionResourceLoader/browser/extensionResourceLoaderServi import '../workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.js'; import '../workbench/services/power/browser/powerService.js'; import '../platform/sandbox/browser/sandboxHelperService.js'; -import './services/vscode/browser/themeImporterService.js'; import { InstantiationType, registerSingleton } from '../platform/instantiation/common/extensions.js'; import { IAccessibilityService } from '../platform/accessibility/common/accessibility.js'; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 6a5528bdf1337..c5bc15deaf9e1 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -51,6 +51,7 @@ import './mainThreadManagedSockets.js'; import './mainThreadOutputService.js'; import './mainThreadProgress.js'; import './mainThreadQuickDiff.js'; +import './mainThreadDocumentDiff.js'; import './mainThreadQuickOpen.js'; import './mainThreadRemoteConnectionData.js'; import './mainThreadSaveParticipant.js'; diff --git a/src/vs/workbench/api/browser/mainThreadDocumentDiff.ts b/src/vs/workbench/api/browser/mainThreadDocumentDiff.ts new file mode 100644 index 0000000000000..5ec1bb5f6c70f --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadDocumentDiff.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRange } from '../../../editor/common/core/range.js'; +import { URI, UriComponents } from '../../../base/common/uri.js'; +import { IEditorWorkerService } from '../../../editor/common/services/editorWorker.js'; +import { IDocumentDiffLineChangeDto, IDocumentDiffResultDto, MainContext, MainThreadDocumentDiffShape } from '../common/extHost.protocol.js'; +import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; + +@extHostNamedCustomer(MainContext.MainThreadDocumentDiff) +export class MainThreadDocumentDiff implements MainThreadDocumentDiffShape { + + constructor( + _extHostContext: IExtHostContext, + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + ) { + } + + async $computeDocumentDiff(originalUri: UriComponents, modifiedUri: UriComponents, ignoreTrimWhitespace: boolean, maxComputationTimeMs: number, computeMoves: boolean): Promise { + const original = URI.revive(originalUri); + const modified = URI.revive(modifiedUri); + const result = await this._editorWorkerService.computeDiff(original, modified, { + ignoreTrimWhitespace, + maxComputationTimeMs, + computeMoves, + }, 'advanced'); + if (!result) { + return null; + } + const toLineRange = (r: { startLineNumber: number; endLineNumberExclusive: number }): IRange => ({ + startLineNumber: r.startLineNumber, + startColumn: 1, + endLineNumber: r.endLineNumberExclusive, + endColumn: 1, + }); + + const mapChange = (c: typeof result.changes[0]): IDocumentDiffLineChangeDto => ({ + originalRange: toLineRange(c.original), + modifiedRange: toLineRange(c.modified), + innerChanges: c.innerChanges?.map(ic => ({ + originalRange: ic.originalRange, + modifiedRange: ic.modifiedRange, + })), + }); + + return { + identical: result.identical, + quitEarly: result.quitEarly, + changes: result.changes.map(mapChange), + moves: result.moves.map(m => ({ + originalRange: toLineRange(m.lineRangeMapping.original), + modifiedRange: toLineRange(m.lineRangeMapping.modified), + changes: m.changes.map(mapChange), + })), + }; + } + + dispose(): void { + // nothing to dispose + } +} diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index f641d14eac276..4601f067dc70a 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -292,12 +292,12 @@ class TreeViewDataProvider implements ITreeViewDataProvider { this.hasResolve = this._proxy.$hasResolve(this.treeViewId); } - async getChildren(treeItem?: ITreeItem): Promise { + async getChildren(treeItem?: ITreeItem): Promise { const batches = await this.getChildrenBatch(treeItem ? [treeItem] : undefined); return batches?.[0]; } - getChildrenBatch(treeItems?: ITreeItem[]): Promise { + getChildrenBatch(treeItems?: ITreeItem[]): Promise<(readonly ITreeItem[])[] | undefined> { if (!treeItems) { this.itemsMap.clear(); } @@ -317,12 +317,12 @@ class TreeViewDataProvider implements ITreeViewDataProvider { }); } - private convertTransferChildren(parents: ITreeItem[], children: (number | ITreeItem)[][] | undefined) { - const convertedChildren: (ITreeItem[] | undefined)[] = Array(parents.length); + private convertTransferChildren(parents: ITreeItem[], children: (readonly (number | ITreeItem)[])[] | undefined) { + const convertedChildren: (readonly ITreeItem[] | undefined)[] = Array(parents.length); if (children) { for (const childGroup of children) { const childGroupIndex = childGroup[0] as number; - convertedChildren[childGroupIndex] = childGroup.slice(1) as ITreeItem[]; + convertedChildren[childGroupIndex] = childGroup.slice(1) as readonly ITreeItem[]; } } return convertedChildren; @@ -366,7 +366,7 @@ class TreeViewDataProvider implements ITreeViewDataProvider { return this.itemsMap.size === 0; } - private async postGetChildren(elementGroups: (ITreeItem[] | undefined)[] | undefined): Promise { + private async postGetChildren(elementGroups: (readonly ITreeItem[] | undefined)[] | undefined): Promise { if (elementGroups === undefined) { return undefined; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ebe8580ca5d3a..7420337083012 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -5,6 +5,7 @@ import type * as vscode from 'vscode'; import { CancellationTokenSource } from '../../../base/common/cancellation.js'; +import { AsyncIterableObject, raceCancellationError } from '../../../base/common/async.js'; import * as errors from '../../../base/common/errors.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { combinedDisposable } from '../../../base/common/lifecycle.js'; @@ -29,7 +30,7 @@ import { UIKind } from '../../services/extensions/common/extensionHostProtocol.j import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { ProxyIdentifier } from '../../services/extensions/common/proxyIdentifier.js'; import { AISearchKeyword, ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContext2, TextSearchMatch2 } from '../../services/search/common/searchExtTypes.js'; -import { CandidatePortSource, ExtHostContext, ExtHostLogLevelServiceShape, MainContext } from './extHost.protocol.js'; +import { CandidatePortSource, ExtHostContext, ExtHostLogLevelServiceShape, IDocumentDiffLineChangeDto, MainContext } from './extHost.protocol.js'; import { ExtHostRelatedInformation } from './extHostAiRelatedInformation.js'; import { ExtHostAiSettingsSearch } from './extHostAiSettingsSearch.js'; import { ExtHostApiCommands } from './extHostApiCommands.js'; @@ -1164,6 +1165,59 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'textSearchProvider2'); return extHostWorkspace.findTextInFiles2(query, options, extension.identifier, token); }, + getTextDiff(originalDocument: vscode.TextDocument, modifiedDocument: vscode.TextDocument, options?: vscode.TextDiffOptions, token?: vscode.CancellationToken): vscode.TextDiffResponse { + checkProposedApiEnabled(extension, 'documentDiff'); + const proxy = rpcProtocol.getProxy(MainContext.MainThreadDocumentDiff); + if (token?.isCancellationRequested) { + const error = new errors.CancellationError(); + return { + changes: AsyncIterableObject.EMPTY, + complete: Promise.reject(error), + }; + } + const resultPromise = proxy.$computeDocumentDiff( + originalDocument.uri, + modifiedDocument.uri, + options?.ignoreTrimWhitespace ?? false, + options?.maxComputationTimeMs ?? 5000, + options?.computeMoves ?? false, + ); + const diffPromise = token ? raceCancellationError(resultPromise, token) : resultPromise; + const mappedPromise = diffPromise.then(result => { + if (!result) { + throw new Error('Could not compute diff. Make sure both documents are available.'); + } + return result; + }); + + const mapChange = (c: IDocumentDiffLineChangeDto) => ({ + originalRange: typeConverters.Range.to(c.originalRange), + modifiedRange: typeConverters.Range.to(c.modifiedRange), + innerChanges: c.innerChanges?.map(ic => ({ + originalRange: typeConverters.Range.to(ic.originalRange), + modifiedRange: typeConverters.Range.to(ic.modifiedRange), + })), + }); + + // TODO@API currently the diff is computed in one shot and all changes are emitted at once. + // In the future, we may want to stream changes incrementally as they are computed + // (e.g. by having the worker yield partial results). + return { + changes: new AsyncIterableObject(async emitter => { + const result = await mappedPromise; + emitter.emitMany(result.changes.map(mapChange)); + }), + complete: mappedPromise.then(result => ({ + identical: result.identical, + mayBeIncomplete: result.quitEarly, + moves: result.moves.map(m => ({ + originalRange: typeConverters.Range.to(m.originalRange), + modifiedRange: typeConverters.Range.to(m.modifiedRange), + changes: m.changes.map(mapChange), + })), + })), + }; + }, save: (uri) => { return extHostWorkspace.save(uri); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f9514d62fea43..e6dcc34086047 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2149,6 +2149,29 @@ export interface MainThreadQuickDiffShape extends IDisposable { $unregisterQuickDiffProvider(handle: number): Promise; } +export interface IDocumentDiffLineChangeDto { + originalRange: IRange; + modifiedRange: IRange; + innerChanges: { originalRange: IRange; modifiedRange: IRange }[] | undefined; +} + +export interface IDocumentDiffMoveDto { + originalRange: IRange; + modifiedRange: IRange; + changes: IDocumentDiffLineChangeDto[]; +} + +export interface IDocumentDiffResultDto { + identical: boolean; + quitEarly: boolean; + changes: IDocumentDiffLineChangeDto[]; + moves: IDocumentDiffMoveDto[]; +} + +export interface MainThreadDocumentDiffShape extends IDisposable { + $computeDocumentDiff(originalUri: UriComponents, modifiedUri: UriComponents, ignoreTrimWhitespace: boolean, maxComputationTimeMs: number, computeMoves: boolean): Promise; +} + export type DebugSessionUUID = string; export interface IDebugConfiguration { @@ -2398,7 +2421,7 @@ export interface ExtHostTreeViewsShape { * for [x,y] returns * [[1,z]], where the inner array is [original index, ...children] */ - $getChildren(treeViewId: string, treeItemHandles?: string[]): Promise<(number | ITreeItem)[][] | undefined>; + $getChildren(treeViewId: string, treeItemHandles?: string[]): Promise<(readonly (number | ITreeItem)[])[] | undefined>; $handleDrop(destinationViewId: string, requestId: number, treeDataTransfer: DataTransferDTO, targetHandle: string | undefined, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise; $handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise; $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void; @@ -3920,6 +3943,7 @@ export const MainContext = { MainThreadOutputService: createProxyIdentifier('MainThreadOutputService'), MainThreadProgress: createProxyIdentifier('MainThreadProgress'), MainThreadQuickDiff: createProxyIdentifier('MainThreadQuickDiff'), + MainThreadDocumentDiff: createProxyIdentifier('MainThreadDocumentDiff'), MainThreadQuickOpen: createProxyIdentifier('MainThreadQuickOpen'), MainThreadStatusBar: createProxyIdentifier('MainThreadStatusBar'), MainThreadSecretState: createProxyIdentifier('MainThreadSecretState'), diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index c0d05cbb48633..3424a8f62b2b8 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -164,7 +164,7 @@ export class ExtHostTreeViews extends Disposable implements ExtHostTreeViewsShap return view as vscode.TreeView; } - async $getChildren(treeViewId: string, treeItemHandles?: string[]): Promise<(number | ITreeItem)[][] | undefined> { + async $getChildren(treeViewId: string, treeItemHandles?: string[]): Promise<(readonly (number | ITreeItem)[])[] | undefined> { const treeView = this._treeViews.get(treeViewId); if (!treeView) { return Promise.reject(new NoTreeViewError(treeViewId)); @@ -488,7 +488,7 @@ class ExtHostTreeView extends Disposable { } } - async getChildren(parentHandle: TreeItemHandle | Root): Promise { + async getChildren(parentHandle: TreeItemHandle | Root): Promise { const parentElement = parentHandle ? this.getExtensionElement(parentHandle) : undefined; if (parentHandle && !parentElement) { this._logService.error(`No tree item with id \'${parentHandle}\' found.`); diff --git a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts index 2ca97fac8a80a..59187e1d3c2a0 100644 --- a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts @@ -20,14 +20,14 @@ import { runWithFakedTimers } from '../../../../base/test/common/timeTravelSched import { IExtHostTelemetry } from '../../common/extHostTelemetry.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -function unBatchChildren(result: (number | ITreeItem)[][] | undefined): ITreeItem[] | undefined { +function unBatchChildren(result: (readonly (number | ITreeItem)[])[] | undefined): readonly ITreeItem[] | undefined { if (!result || result.length === 0) { return undefined; } if (result.length > 1) { throw new Error('Unexpected result length, all tests are unbatched.'); } - return result[0].slice(1) as ITreeItem[]; + return result[0].slice(1) as readonly ITreeItem[]; } suite('ExtHostTreeView', function () { diff --git a/src/vs/workbench/browser/actions/quickAccessActions.ts b/src/vs/workbench/browser/actions/quickAccessActions.ts index fe18d7a076f70..734412e75b3ad 100644 --- a/src/vs/workbench/browser/actions/quickAccessActions.ts +++ b/src/vs/workbench/browser/actions/quickAccessActions.ts @@ -179,7 +179,8 @@ registerAction2(class QuickAccessAction extends Action2 { const configurationService = accessor.get(IConfigurationService); const commandService = accessor.get(ICommandService); - const useUnifiedQuickAccess = configurationService.getValue(UNIFIED_AGENTS_BAR_SETTING) === true; + const aiFeaturesDisabled = configurationService.getValue('chat.disableAIFeatures') === true; + const useUnifiedQuickAccess = !aiFeaturesDisabled && configurationService.getValue(UNIFIED_AGENTS_BAR_SETTING) === true; if (useUnifiedQuickAccess) { try { await commandService.executeCommand('workbench.action.unifiedQuickAccess'); diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 1c9305bd780ba..487cf07936e3f 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -366,12 +366,12 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { return this._isEmpty; } - async getChildren(element?: ITreeItem): Promise { + async getChildren(element?: ITreeItem): Promise { const batches = await this.getChildrenBatch(element ? [element] : undefined); return batches?.[0]; } - private updateEmptyState(nodes: ITreeItem[], childrenGroups: ITreeItem[][]): void { + private updateEmptyState(nodes: ITreeItem[], childrenGroups: (readonly ITreeItem[])[]): void { if ((nodes.length === 1) && (nodes[0] instanceof Root)) { const oldEmpty = this._isEmpty; this._isEmpty = (childrenGroups.length === 0) || (childrenGroups[0].length === 0); @@ -381,7 +381,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } } - private findCheckboxesUpdated(nodes: ITreeItem[], childrenGroups: ITreeItem[][]): ITreeItem[] { + private findCheckboxesUpdated(nodes: ITreeItem[], childrenGroups: (readonly ITreeItem[])[]): ITreeItem[] { if (childrenGroups.length === 0) { return []; } @@ -401,8 +401,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { return checkboxesUpdated; } - async getChildrenBatch(nodes?: ITreeItem[]): Promise { - let childrenGroups: ITreeItem[][]; + async getChildrenBatch(nodes?: ITreeItem[]): Promise<(readonly ITreeItem[])[]> { + let childrenGroups: (readonly ITreeItem[])[]; let checkboxesUpdated: ITreeItem[] = []; if (nodes?.every((node): node is Required => !!node.children)) { childrenGroups = nodes.map(node => node.children); @@ -1172,7 +1172,7 @@ class TreeViewDelegate implements IListVirtualDelegate { } } -async function doGetChildrenOrBatch(dataProvider: ITreeViewDataProvider, nodes: ITreeItem[] | undefined): Promise { +async function doGetChildrenOrBatch(dataProvider: ITreeViewDataProvider, nodes: ITreeItem[] | undefined): Promise<(readonly ITreeItem[])[] | undefined> { if (dataProvider.getChildrenBatch) { return dataProvider.getChildrenBatch(nodes); } else { @@ -1197,8 +1197,8 @@ class TreeDataSource implements IAsyncDataSource { } private batch: ITreeItem[] | undefined; - private batchPromise: Promise | undefined; - async getChildren(element: ITreeItem): Promise { + private batchPromise: Promise<(readonly ITreeItem[])[] | undefined> | undefined; + async getChildren(element: ITreeItem): Promise { const dataProvider = this.treeView.dataProvider; if (!dataProvider) { return []; @@ -1210,7 +1210,7 @@ class TreeDataSource implements IAsyncDataSource { this.batch.push(element); } const indexInBatch = this.batch.length - 1; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { setTimeout(async () => { const batch = this.batch; this.batch = undefined; diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 29b8e0445288a..da69831713055 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -793,7 +793,7 @@ export interface ITreeItem { command?: TreeCommand; - children?: ITreeItem[]; + children?: readonly ITreeItem[]; parent?: ITreeItem; @@ -876,8 +876,8 @@ export class NoTreeViewError extends Error { export interface ITreeViewDataProvider { readonly isTreeEmpty?: boolean; readonly onDidChangeEmpty?: Event; - getChildren(element?: ITreeItem): Promise; - getChildrenBatch?(element?: ITreeItem[]): Promise; + getChildren(element?: ITreeItem): Promise; + getChildrenBatch?(element?: ITreeItem[]): Promise<(readonly ITreeItem[])[] | undefined>; } export interface ITreeViewDragAndDropController { diff --git a/src/vs/workbench/contrib/agentsAppMergedBanner/browser/agentsAppMergedBanner.contribution.ts b/src/vs/workbench/contrib/agentsAppMergedBanner/browser/agentsAppMergedBanner.contribution.ts new file mode 100644 index 0000000000000..99601910656e1 --- /dev/null +++ b/src/vs/workbench/contrib/agentsAppMergedBanner/browser/agentsAppMergedBanner.contribution.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { localize } from '../../../../nls.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js'; +import { IBannerService } from '../../../services/banner/browser/bannerService.js'; +import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; + +/** + * Tracks whether we have already shown the one-time banner explaining that + * the formerly separate Agents application is now a window inside VS Code. + * Stored in `StorageScope.APPLICATION` so the banner is shown at most once + * per installation, regardless of profile. + */ +const AGENTS_APP_MERGED_BANNER_SHOWN_KEY = 'workbench.banner.agentsAppMerged.shown'; + +class AgentsAppMergedBannerContribution { + + constructor( + @IBannerService bannerService: IBannerService, + @IStorageService storageService: IStorageService, + @IProductService productService: IProductService + ) { + if (productService.quality === 'stable') { + return; + } + + if (storageService.getBoolean(AGENTS_APP_MERGED_BANNER_SHOWN_KEY, StorageScope.APPLICATION, false)) { + return; + } + + storageService.store(AGENTS_APP_MERGED_BANNER_SHOWN_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE); + + const message = localize('agentsAppMerged.message', "The Agents app is deprecated. Starting with this version, it is integrated into {0}.", productService.nameLong); + const openAction = { + href: 'command:workbench.action.openAgentsWindow', + label: localize('agentsAppMerged.open', "Open Agents Window") + }; + + bannerService.show({ + id: 'workbench.banner.agentsAppMerged', + icon: ThemeIcon.fromId('info'), + message, + ariaLabel: message, + actions: [openAction] + }); + } +} + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(AgentsAppMergedBannerContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 0a0fc3726cd38..5c3da450d93bf 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -1085,6 +1085,19 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return !!widget && isAutoApproveLevel(widget.input.currentModeInfo.permissionLevel); } + /** + * True if the session is in an auto-approve level (Auto-Approve / Autopilot), + * via either the last request's stamped level or the live picker level. + */ + private _isSessionInAutoApproveLevel(chatSessionResource: URI | undefined): boolean { + if (!chatSessionResource) { + return false; + } + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + return isAutoApproveLevel(request?.modeInfo?.permissionLevel) || this._isSessionLiveAutoApproveLevel(chatSessionResource); + } + private getEligibleForAutoApprovalSpecialCase(toolData: IToolData): string | undefined { if (toolData.id === 'vscode_fetchWebPage_internal') { return 'fetch'; @@ -1135,19 +1148,12 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return undefined; } - // Auto-Approve All permission level bypasses all tool confirmations, - // unless enterprise policy has explicitly disabled global auto-approve. - // Check both the request-stamped level AND the live picker level so that - // switching to Autopilot mid-session takes effect immediately. - if (chatSessionResource && !this._isAutoApprovePolicyRestricted()) { - const model = this._chatService.getSession(chatSessionResource); - const request = model?.getRequests().at(-1); - if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) || this._isSessionLiveAutoApproveLevel(chatSessionResource)) { - // CLI sessions must always show their multi-option confirmation dialogs - // (e.g. uncommitted-changes prompt) even under Bypass Approvals - if (!(toolIdsThatCannotBeAutoApproved.has(tool.data.id) && getChatSessionType(chatSessionResource) !== localChatSessionType)) { - return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; - } + // Bypass confirmation under Auto-Approve / Autopilot, unless enterprise + // policy disables global auto-approve. + if (chatSessionResource && !this._isAutoApprovePolicyRestricted() && this._isSessionInAutoApproveLevel(chatSessionResource)) { + // CLI sessions still need their multi-option dialogs (e.g. uncommitted changes). + if (!(toolIdsThatCannotBeAutoApproved.has(tool.data.id) && getChatSessionType(chatSessionResource) !== localChatSessionType)) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } } @@ -1183,20 +1189,18 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined, chatRequestId: string | undefined): Promise { - // Auto-Approve All permission level bypasses all post-execution confirmations, - // unless enterprise policy has explicitly disabled global auto-approve. - // Check both the request-stamped level AND the live picker level. - if (chatSessionResource && !this._isAutoApprovePolicyRestricted()) { - const model = this._chatService.getSession(chatSessionResource); - const request = model?.getRequests().at(-1); - if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) || this._isSessionLiveAutoApproveLevel(chatSessionResource)) { - if (!(toolIdsThatCannotBeAutoApproved.has(toolId) && getChatSessionType(chatSessionResource) !== localChatSessionType)) { - return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; - } + // Bypass post-execution confirmation under Auto-Approve / Autopilot, + // unless enterprise policy disables global auto-approve. + const sessionAutoApprove = chatSessionResource && !this._isAutoApprovePolicyRestricted() && this._isSessionInAutoApproveLevel(chatSessionResource); + if (sessionAutoApprove) { + if (!(toolIdsThatCannotBeAutoApproved.has(toolId) && getChatSessionType(chatSessionResource!) !== localChatSessionType)) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } } - if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) && await this._checkGlobalAutoApprove()) { + // Don't show the YOLO opt-in dialog under autopilot: this runs after the + // tool result is already back in the agent loop, so it can't block anything. + if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) && !sessionAutoApprove && await this._checkGlobalAutoApprove()) { return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove }; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts index 849393873dd5a..6893c58f15c9d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts @@ -215,7 +215,7 @@ export class CollapsibleListPool extends Disposable { const container = $('.chat-used-context-list'); store.add(createFileIconThemableTreeContainerScope(container, this.themeService)); - const list = this.instantiationService.createInstance( + const list = store.add(this.instantiationService.createInstance( WorkbenchList, 'ChatListRenderer', container, @@ -268,7 +268,7 @@ export class CollapsibleListPool extends Disposable { } }, }, - }); + })); return { list, diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index bf5b6c9bf31ea..1bb83a8fd5019 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2644,7 +2644,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge let inputModel = this.modelService.getModel(this.inputUri); if (!inputModel) { - inputModel = this.modelService.createModel('', null, this.inputUri, true); + inputModel = this._register(this.modelService.createModel('', null, this.inputUri, true)); } this.textModelResolverService.createModelReference(this.inputUri).then(ref => { @@ -3164,13 +3164,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.updateToolConfirmationCarouselMaxHeight(); const capturedKey = key; - Event.once(part.onDidEmpty)(() => { + this._register(Event.once(part.onDidEmpty)(() => { this._chatToolConfirmationCarousels.deleteAndDispose(capturedKey); if (this._currentSessionKey === capturedKey) { dom.clearNode(this.chatToolConfirmationCarouselContainer); dom.hide(this.chatToolConfirmationCarouselContainer); } - }); + })); return part; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index 9a7c758271ecd..07baa0272047b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -190,7 +190,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { } }], reporter: { id: 'ChatPermissionPicker', name: 'ChatPermissionPicker', includeOptions: true }, - listOptions: { minWidth: 255 }, + listOptions: { minWidth: 255, detailItemHeight: 44 }, }, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 56a6e8feeb081..aa624dfbe5286 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -9,12 +9,12 @@ import { VSBuffer, decodeHex, encodeHex } from '../../../../../base/common/buffe import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { revive } from '../../../../../base/common/marshalling.js'; import { Schemas } from '../../../../../base/common/network.js'; import { equals } from '../../../../../base/common/objects.js'; -import { IObservable, autorun, autorunSelfDisposable, constObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue, observableValueOpts } from '../../../../../base/common/observable.js'; +import { IObservable, autorun, constObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue, observableValueOpts, registerAutorunSelfDisposable } from '../../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../../base/common/resources.js'; import { hasKey, WithDefinedProps } from '../../../../../base/common/types.js'; import { URI, UriDto } from '../../../../../base/common/uri.js'; @@ -761,7 +761,8 @@ class ResponseView extends AbstractResponse { } export class Response extends AbstractResponse implements IDisposable { - private _onDidChangeValue = new Emitter(); + private readonly _store = new DisposableStore(); + private _onDidChangeValue = this._store.add(new Emitter()); public get onDidChangeValue() { return this._onDidChangeValue.event; } @@ -778,7 +779,7 @@ export class Response extends AbstractResponse implements IDisposable { } dispose(): void { - this._onDidChangeValue.dispose(); + this._store.dispose(); } @@ -902,7 +903,7 @@ export class Response extends AbstractResponse implements IDisposable { }); } else if (progress.kind === 'toolInvocation') { - autorunSelfDisposable(reader => { + registerAutorunSelfDisposable(this._store, reader => { progress.state.read(reader); // update repr when state changes this._contentChanged(false); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index b60f49efcf557..60db6589ab111 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -56,6 +56,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { EnablementState, IExtensionManagementServerService, IPublisherInfo, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkspaceExtensionsConfigService } from '../../../services/extensionRecommendations/common/workspaceExtensionsConfig.js'; +import { EXTENSIONS_SUPPORT_SESSIONS_WINDOW } from '../../../services/extensions/common/extensionManifestPropertiesService.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; @@ -214,6 +215,24 @@ Registry.as(ConfigurationExtensions.Configuration) } }] }, + [EXTENSIONS_SUPPORT_SESSIONS_WINDOW]: { + type: 'object', + scope: ConfigurationScope.APPLICATION, + markdownDescription: localize('extensions.supportSessionsWindow', "Override the Agents window support of an extension. Extensions using `true` will be enabled in the Agents window even when they would otherwise be disabled."), + patternProperties: { + '([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$': { + type: 'boolean', + default: false + } + }, + additionalProperties: false, + default: {}, + defaultSnippets: [{ + 'body': { + 'pub.name': true + } + }] + }, 'extensions.experimental.affinity': { type: 'object', markdownDescription: localize('extensions.affinity', "Configure an extension to execute in a different extension host process."), diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index a14f0adcfe9e9..1499006feaac8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -816,14 +816,23 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { () => this._isVisible, () => xterm, async (cols, rows) => { + if (this.isDisposed) { + return; + } xterm.resize(cols, rows); await this._updatePtyDimensions(xterm.raw); }, async (cols) => { + if (this.isDisposed) { + return; + } xterm.resize(cols, xterm.raw.rows); await this._updatePtyDimensions(xterm.raw); }, async (rows) => { + if (this.isDisposed) { + return; + } xterm.resize(xterm.raw.cols, rows); await this._updatePtyDimensions(xterm.raw); } @@ -2047,8 +2056,22 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private async _updatePtyDimensions(rawXterm: XTermTerminal): Promise { - const pixelWidth = rawXterm.dimensions?.css.canvas.width; - const pixelHeight = rawXterm.dimensions?.css.canvas.height; + if (this.isDisposed) { + return; + } + // `rawXterm.dimensions` proxies to xterm.js' RenderService which throws + // `Cannot read properties of undefined (reading 'dimensions')` if the + // renderer was disposed between scheduling and invocation (debounced or + // idle resize callbacks racing with terminal teardown). Guard against + // that here so the optional chaining short-circuits to undefined. + let pixelWidth: number | undefined; + let pixelHeight: number | undefined; + try { + pixelWidth = rawXterm.dimensions?.css.canvas.width; + pixelHeight = rawXterm.dimensions?.css.canvas.height; + } catch { + // Renderer disposed mid-flight; fall through with undefined dimensions. + } const roundedPixelWidth = pixelWidth ? Math.round(pixelWidth) : undefined; const roundedPixelHeight = pixelHeight ? Math.round(pixelHeight) : undefined; await this._processManager.setDimensions(rawXterm.cols, rawXterm.rows, undefined, roundedPixelWidth, roundedPixelHeight); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalResizeDebouncer.ts b/src/vs/workbench/contrib/terminal/browser/terminalResizeDebouncer.ts index 8afe11348cc2b..88062ff24fb7f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalResizeDebouncer.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalResizeDebouncer.ts @@ -33,6 +33,9 @@ export class TerminalResizeDebouncer extends Disposable { } async resize(cols: number, rows: number, immediate: boolean): Promise { + if (this._store.isDisposed) { + return; + } this._latestX = cols; this._latestY = rows; @@ -49,12 +52,18 @@ export class TerminalResizeDebouncer extends Disposable { if (win && !this._isVisible()) { if (!this._resizeXJob.value) { this._resizeXJob.value = runWhenWindowIdle(win, async () => { + if (this._store.isDisposed) { + return; + } this._resizeXCallback(this._latestX); this._resizeXJob.clear(); }); } if (!this._resizeYJob.value) { this._resizeYJob.value = runWhenWindowIdle(win, async () => { + if (this._store.isDisposed) { + return; + } this._resizeYCallback(this._latestY); this._resizeYJob.clear(); }); @@ -70,6 +79,9 @@ export class TerminalResizeDebouncer extends Disposable { } flush(): void { + if (this._store.isDisposed) { + return; + } if (this._resizeXJob.value || this._resizeYJob.value) { this._resizeXJob.clear(); this._resizeYJob.clear(); @@ -79,6 +91,12 @@ export class TerminalResizeDebouncer extends Disposable { @debounce(100) private _debounceResizeX(cols: number) { + // The @debounce decorator schedules a setTimeout that is not tied to the + // disposable store, so this can fire after the terminal/xterm renderer is + // disposed. Bail out to avoid throwing from xterm.js dimension getters. + if (this._store.isDisposed) { + return; + } this._resizeXCallback(cols); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts index b26d97bf66a64..5543fa53d6fe3 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts @@ -64,7 +64,7 @@ export class UserDataSyncConflictsViewPane extends TreeViewPane implements IUser this.treeView.dataProvider = { getChildren() { return that.getTreeItems(); } }; } - private async getTreeItems(): Promise { + private async getTreeItems(): Promise { const roots: ITreeItem[] = []; const conflictResources: UserDataSyncConflictResource[] = this.userDataSyncService.conflicts diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 1d831a025a592..64b8586af2959 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -63,17 +63,10 @@ function getSessionsWindowTrustNote(environmentService: IWorkbenchEnvironmentSer if (!environmentService.isSessionsWindow) { return undefined; } - const parentAppName = productService.quality === 'stable' - ? 'Visual Studio Code' - : productService.quality === 'insider' - ? 'Visual Studio Code Insiders' - : productService.quality === 'exploration' - ? 'Visual Studio Code Exploration' - : productService.nameLong; if (isWorkspace) { - return localize('sessionsWindowWorkspaceTrustNote', "Trusting this workspace will also mark it as trusted in {0}.", parentAppName); + return localize('sessionsWindowWorkspaceTrustNote', "Trusting this workspace will also mark it as trusted in {0}.", productService.nameLong); } - return localize('sessionsWindowFolderTrustNote', "Trusting this folder will also mark it as trusted in {0}.", parentAppName); + return localize('sessionsWindowFolderTrustNote', "Trusting this folder will also mark it as trusted in {0}.", productService.nameLong); } export class WorkspaceTrustContextKeys extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/services/environment/electron-browser/environmentService.ts b/src/vs/workbench/services/environment/electron-browser/environmentService.ts index b267fefcbea14..1abd21a9d7ef7 100644 --- a/src/vs/workbench/services/environment/electron-browser/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-browser/environmentService.ts @@ -164,10 +164,7 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment homeDir: configuration.homeDir, tmpDir: configuration.tmpDir, userDataDir: configuration.userDataDir, - parentAppUserDataDir: configuration.parentAppUserDataDir, - parentAppUserHomeDir: configuration.parentAppUserHomeDir }, - productService, - !!configuration.isEmbeddedApp); + productService); } } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 86cb30c390819..ccff0f3760ed4 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -638,8 +638,17 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } - // Built-in extensions are always enabled in the sessions window. + // Built-in extensions are enabled in sessions window except the chat extension and extensions that contribute not supported features. if (extension.isBuiltin) { + if (extension.identifier.id.toLowerCase() === this._chatExtensionId) { + return false; + } + + const contributes = extension.manifest.contributes; + if (contributes?.debuggers || contributes?.views || contributes?.viewsContainers || contributes?.walkthroughs) { + return true; + } + return false; } diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 4c3d2f745c308..4a23062fc708c 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -30,7 +30,7 @@ import { IHostService } from '../../../host/browser/host.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { IExtensionBisectService } from '../../browser/extensionBisect.js'; import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, WorkspaceTrustRequestOptions } from '../../../../../platform/workspace/common/workspaceTrust.js'; -import { ExtensionManifestPropertiesService, IExtensionManifestPropertiesService } from '../../../extensions/common/extensionManifestPropertiesService.js'; +import { EXTENSIONS_SUPPORT_SESSIONS_WINDOW, ExtensionManifestPropertiesService, IExtensionManifestPropertiesService } from '../../../extensions/common/extensionManifestPropertiesService.js'; import { TestChatEntitlementService, TestContextService, TestProductService, TestWorkspaceTrustEnablementService, TestWorkspaceTrustManagementService } from '../../../../test/common/workbenchTestServices.js'; import { TestWorkspace } from '../../../../../platform/workspace/test/common/testWorkspace.js'; import { ExtensionManagementService } from '../../common/extensionManagementService.js'; @@ -98,7 +98,7 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { instantiationService.stub(IAllowedExtensionsService, disposables.add(new AllowedExtensionsService(instantiationService.get(IProductService), instantiationService.get(IConfigurationService)))), workspaceTrustManagementService, new class extends mock() { override requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { return Promise.resolve(true); } }, - instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, disposables.add(new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService(), new TestWorkspaceTrustEnablementService(), new NullLogService()))), + instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, disposables.add(new ExtensionManifestPropertiesService(TestProductService, instantiationService.get(IConfigurationService), new TestWorkspaceTrustEnablementService(), new NullLogService()))), chatEntitlementService ?? new TestChatEntitlementService(), instantiationService, new NullLogService(), @@ -1266,6 +1266,22 @@ suite('ExtensionEnablementService Test', () => { ]); }); + test('test configured extensions are enabled in sessions window', async () => { + await (instantiationService.get(IConfigurationService) as TestConfigurationService).setUserConfiguration(EXTENSIONS_SUPPORT_SESSIONS_WINDOW, { 'pub.withMain': true, 'pub.nonThemeContrib': true }); + instantiationService.stub(IWorkbenchEnvironmentService, { isSessionsWindow: true }); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); + + const withMain = aLocalExtension2('pub.withMain', { main: 'main.js', contributes: aContributes('themes') }); + const nonThemeContrib = aLocalExtension2('pub.nonThemeContrib', { contributes: aContributes('commands') }); + const withBrowser = aLocalExtension2('pub.withBrowser', { browser: 'main.browser.js', contributes: aContributes('themes') }); + + assert.deepStrictEqual([withMain, nonThemeContrib, withBrowser].map(ext => testObject.getEnablementState(ext)), [ + EnablementState.EnabledGlobally, + EnablementState.EnabledGlobally, + EnablementState.DisabledByEnvironment, + ]); + }); + test('test extensions are not disabled in non-sessions window', () => { const withMain = aLocalExtension2('pub.withMain', { main: 'main.js' }); const withBrowser = aLocalExtension2('pub.withBrowser', { browser: 'main.browser.js' }); diff --git a/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts index d647721265701..0fe9ce70ee097 100644 --- a/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts +++ b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts @@ -22,6 +22,8 @@ import { isWeb } from '../../../../base/common/platform.js'; export const IExtensionManifestPropertiesService = createDecorator('extensionManifestPropertiesService'); +export const EXTENSIONS_SUPPORT_SESSIONS_WINDOW = 'extensions.supportSessionsWindow'; + const SESSIONS_WINDOW_ALLOWED_CONTRIBUTION_POINTS: ReadonlySet = new Set([ 'themes', 'iconThemes', @@ -62,6 +64,7 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE private _productVirtualWorkspaceSupportMap: ExtensionIdentifierMap<{ default?: boolean; override?: boolean }> | null = null; private _configuredVirtualWorkspaceSupportMap: ExtensionIdentifierMap | null = null; + private _configuredSessionsWindowSupportMap: ExtensionIdentifierMap | null = null; private readonly _configuredExtensionWorkspaceTrustRequestMap: ExtensionIdentifierMap<{ supported: ExtensionUntrustedWorkspaceSupportType; version?: string }>; private readonly _productExtensionWorkspaceTrustRequestMap: Map; @@ -91,6 +94,11 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE } canExecuteOnSessionsWindow(manifest: IExtensionManifest): boolean { + const configuredSessionsWindowSupport = this.getConfiguredSessionsWindowSupport(manifest); + if (configuredSessionsWindowSupport !== undefined) { + return configuredSessionsWindowSupport; + } + // In the sessions window only extensions that have no code are currently allowed to run if (manifest.main || manifest.browser) { return false; @@ -371,6 +379,22 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE return this._configuredVirtualWorkspaceSupportMap.get(extensionId); } + private getConfiguredSessionsWindowSupport(manifest: IExtensionManifest): boolean | undefined { + if (this._configuredSessionsWindowSupportMap === null) { + const configuredSessionsWindowSupportMap = new ExtensionIdentifierMap(); + const configuredSessionsWindowSupport = this.configurationService.getValue<{ [key: string]: boolean }>(EXTENSIONS_SUPPORT_SESSIONS_WINDOW) || {}; + for (const id of Object.keys(configuredSessionsWindowSupport)) { + if (configuredSessionsWindowSupport[id] !== undefined) { + configuredSessionsWindowSupportMap.set(id, configuredSessionsWindowSupport[id]); + } + } + this._configuredSessionsWindowSupportMap = configuredSessionsWindowSupportMap; + } + + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + return this._configuredSessionsWindowSupportMap.get(extensionId); + } + private getConfiguredExtensionWorkspaceTrustRequest(manifest: IExtensionManifest): ExtensionUntrustedWorkspaceSupportType | undefined { const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); const extensionWorkspaceTrustRequest = this._configuredExtensionWorkspaceTrustRequestMap.get(extensionId); diff --git a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts index f822c23a330af..6147ec33ac54d 100644 --- a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts +++ b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts @@ -14,7 +14,7 @@ import { TestInstantiationService } from '../../../../../platform/instantiation/ import { NullLogService } from '../../../../../platform/log/common/log.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IWorkspaceTrustEnablementService } from '../../../../../platform/workspace/common/workspaceTrust.js'; -import { ExtensionManifestPropertiesService } from '../../common/extensionManifestPropertiesService.js'; +import { EXTENSIONS_SUPPORT_SESSIONS_WINDOW, ExtensionManifestPropertiesService } from '../../common/extensionManifestPropertiesService.js'; import { TestProductService, TestWorkspaceTrustEnablementService } from '../../../../test/common/workbenchTestServices.js'; suite('ExtensionManifestPropertiesService - ExtensionKind', () => { @@ -109,6 +109,53 @@ suite('ExtensionManifestPropertiesService - ExtensionKind', () => { }); }); +suite('ExtensionManifestPropertiesService - SessionsWindowSupport', () => { + + let disposables: DisposableStore; + let testConfigurationService: TestConfigurationService; + let testObject: ExtensionManifestPropertiesService; + + setup(() => { + disposables = new DisposableStore(); + testConfigurationService = new TestConfigurationService(); + }); + + teardown(() => { + testObject.dispose(); + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function getExtensionManifest(properties: Partial = {}): IExtensionManifest { + return Object.create({ name: 'a', publisher: 'pub', version: '1.0.0', ...properties }) as IExtensionManifest; + } + + function createTestObject(): ExtensionManifestPropertiesService { + return disposables.add(new ExtensionManifestPropertiesService(TestProductService, testConfigurationService, new TestWorkspaceTrustEnablementService(), new NullLogService())); + } + + test('defaults to declarative extensions without executable code and supported contributions', () => { + testObject = createTestObject(); + + assert.deepStrictEqual([ + testObject.canExecuteOnSessionsWindow(getExtensionManifest({ contributes: { themes: [] } })), + testObject.canExecuteOnSessionsWindow(getExtensionManifest({ main: './out/extension.js', contributes: { themes: [] } })), + testObject.canExecuteOnSessionsWindow(getExtensionManifest({ contributes: { commands: [] } })), + ], [true, false, false]); + }); + + test('uses configured sessions window support override', async () => { + await testConfigurationService.setUserConfiguration(EXTENSIONS_SUPPORT_SESSIONS_WINDOW, { 'pub.a': true, 'pub.b': false }); + testObject = createTestObject(); + + assert.deepStrictEqual([ + testObject.canExecuteOnSessionsWindow(getExtensionManifest({ main: './out/extension.js', contributes: { commands: [] } })), + testObject.canExecuteOnSessionsWindow(getExtensionManifest({ name: 'b', contributes: { themes: [] } })), + ], [true, false]); + }); +}); + // Workspace Trust is disabled in web at the moment if (!isWeb) { diff --git a/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts index 2bc08dd758784..bd8b75e607b33 100644 --- a/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts @@ -330,7 +330,7 @@ function renderInputBoxes({ container, disposableStore }: ComponentFixtureContex // Count Badges // ============================================================================ -function renderCountBadges({ container }: ComponentFixtureContext): void { +function renderCountBadges({ container, disposableStore }: ComponentFixtureContext): void { container.style.padding = '16px'; container.style.display = 'flex'; container.style.gap = '12px'; @@ -350,7 +350,7 @@ function renderCountBadges({ container }: ComponentFixtureContext): void { label.style.color = 'var(--vscode-foreground)'; badgeContainer.appendChild(label); - new CountBadge(badgeContainer, { count }, themedBadgeStyles); + disposableStore.add(new CountBadge(badgeContainer, { count }, themedBadgeStyles)); container.appendChild(badgeContainer); } } @@ -381,12 +381,12 @@ function renderActionBar({ container, disposableStore }: ComponentFixtureContext })); horizontalBar.push([ - new Action('editor.action.save', 'Save', ThemeIcon.asClassName(Codicon.save), true, async () => console.log('Save')), - new Action('editor.action.undo', 'Undo', ThemeIcon.asClassName(Codicon.discard), true, async () => console.log('Undo')), - new Action('editor.action.redo', 'Redo', ThemeIcon.asClassName(Codicon.redo), true, async () => console.log('Redo')), + disposableStore.add(new Action('editor.action.save', 'Save', ThemeIcon.asClassName(Codicon.save), true, async () => console.log('Save'))), + disposableStore.add(new Action('editor.action.undo', 'Undo', ThemeIcon.asClassName(Codicon.discard), true, async () => console.log('Undo'))), + disposableStore.add(new Action('editor.action.redo', 'Redo', ThemeIcon.asClassName(Codicon.redo), true, async () => console.log('Redo'))), new Separator(), - new Action('editor.action.find', 'Find', ThemeIcon.asClassName(Codicon.search), true, async () => console.log('Find')), - new Action('editor.action.replace', 'Replace', ThemeIcon.asClassName(Codicon.replaceAll), true, async () => console.log('Replace')), + disposableStore.add(new Action('editor.action.find', 'Find', ThemeIcon.asClassName(Codicon.search), true, async () => console.log('Find'))), + disposableStore.add(new Action('editor.action.replace', 'Replace', ThemeIcon.asClassName(Codicon.replaceAll), true, async () => console.log('Replace'))), ]); // Action bar with disabled items @@ -404,9 +404,9 @@ function renderActionBar({ container, disposableStore }: ComponentFixtureContext })); mixedBar.push([ - new Action('action.enabled', 'Enabled', ThemeIcon.asClassName(Codicon.play), true, async () => { }), - new Action('action.disabled', 'Disabled', ThemeIcon.asClassName(Codicon.debugPause), false, async () => { }), - new Action('action.enabled2', 'Enabled', ThemeIcon.asClassName(Codicon.debugStop), true, async () => { }), + disposableStore.add(new Action('action.enabled', 'Enabled', ThemeIcon.asClassName(Codicon.play), true, async () => { })), + disposableStore.add(new Action('action.disabled', 'Disabled', ThemeIcon.asClassName(Codicon.debugPause), false, async () => { })), + disposableStore.add(new Action('action.enabled2', 'Enabled', ThemeIcon.asClassName(Codicon.debugStop), true, async () => { })), ]); } @@ -473,7 +473,7 @@ function renderProgressBars({ container, disposableStore }: ComponentFixtureCont // Highlighted Label // ============================================================================ -function renderHighlightedLabels({ container }: ComponentFixtureContext): void { +function renderHighlightedLabels({ container, disposableStore }: ComponentFixtureContext): void { container.style.padding = '16px'; container.style.display = 'flex'; container.style.flexDirection = 'column'; @@ -487,7 +487,7 @@ function renderHighlightedLabels({ container }: ComponentFixtureContext): void { row.style.gap = '8px'; const labelContainer = $('div'); - const label = new HighlightedLabel(labelContainer); + const label = disposableStore.add(new HighlightedLabel(labelContainer)); label.set(text, highlights); row.appendChild(labelContainer); diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts index cec1f50ae4bb7..5e29f3a87949e 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatFixtureUtils.ts @@ -176,7 +176,7 @@ export function registerChatFixtureServices(reg: ServiceRegistration, options: I reg.defineInstance(IChatSessionsService, new class extends mock() { override getAllChatSessionContributions() { return []; } override readonly onDidChangeSessionOptions = Event.None; override readonly onDidChangeOptionGroups = Event.None; override readonly onDidChangeAvailability = Event.None; override getCustomAgentTargetForSessionType() { return Target.Undefined; } override requiresCustomModelsForSessionType() { return false; } override getOptionGroupsForSessionType() { return []; } }()); reg.defineInstance(IChatEntitlementService, new class extends mock() { }()); reg.defineInstance(IChatModeService, new MockChatModeService()); - reg.defineInstance(ILanguageModelsService, new class extends mock() { override onDidChangeLanguageModels = Event.None; override getLanguageModelIds() { return []; } }()); + reg.defineInstance(ILanguageModelsService, new class extends mock() { override onDidChangeLanguageModels = Event.None; override getLanguageModelIds() { return []; } override getVendors() { return []; } override hasResolvedVendor() { return false; } }()); reg.defineInstance(ILanguageModelToolsService, new class extends mock() { override onDidChangeTools = Event.None; override onDidPrepareToolCallBecomeUnresponsive = Event.None; override getTools() { return []; } }()); reg.defineInstance(IChatToolRiskAssessmentService, new class extends mock() { override isEnabled() { return false; } diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatToolRiskBadge.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatToolRiskBadge.fixture.ts index ecaa2e213b5ae..d3e042d3c2cc4 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatToolRiskBadge.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatToolRiskBadge.fixture.ts @@ -7,6 +7,7 @@ import * as dom from '../../../../../base/browser/dom.js'; import { ToolRiskBadgeWidget } from '../../../../contrib/chat/browser/widget/chatContentParts/toolInvocationParts/toolRiskBadgeWidget.js'; import { IToolRiskAssessment, ToolRiskLevel } from '../../../../contrib/chat/browser/tools/chatToolRiskAssessmentService.js'; import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; +import { IFixtureMessage, renderChatWidget } from './chatWidget.fixture.js'; import '../../../../contrib/chat/browser/widget/media/chat.css'; @@ -42,6 +43,21 @@ function renderBadge(context: ComponentFixtureContext, state: RenderState): void container.appendChild(itemContainer); } +function makeInContextMessage(assessment?: IToolRiskAssessment): IFixtureMessage[] { + return [{ + user: '', + assistant: [{ + kind: 'terminalConfirmation', + command: 'git init', + riskAssessment: assessment, + riskLoading: !assessment, + }], + responseComplete: false, + }]; +} + +const inContextOptions = { width: 720, height: 400 }; + const greenAssessment: IToolRiskAssessment = { risk: ToolRiskLevel.Green, explanation: 'Reads workspace files and returns matches; no side effects.', @@ -77,4 +93,24 @@ export default defineThemedFixtureGroup({ path: 'chat/' }, { labels: { kind: 'screenshot' }, render: (ctx) => renderBadge(ctx, { kind: 'assessment', assessment: redAssessment }), }), + + GreenInContext: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: (ctx) => renderChatWidget(ctx, { messages: makeInContextMessage(greenAssessment), ...inContextOptions }), + }), + + OrangeInContext: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: (ctx) => renderChatWidget(ctx, { messages: makeInContextMessage(orangeAssessment), ...inContextOptions }), + }), + + RedInContext: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: (ctx) => renderChatWidget(ctx, { messages: makeInContextMessage(redAssessment), ...inContextOptions }), + }), + + LoadingInContext: defineComponentFixture({ + labels: { kind: 'animated' }, + render: (ctx) => renderChatWidget(ctx, { messages: makeInContextMessage(), ...inContextOptions }), + }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts index 8a1200ae8f554..f9bb34e7f5956 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts @@ -20,7 +20,8 @@ import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from '../../.. import { IChatWidget, IChatWidgetService } from '../../../../contrib/chat/browser/chat.js'; import { IChatService } from '../../../../contrib/chat/common/chatService/chatService.js'; import { ChatToolInvocation } from '../../../../contrib/chat/common/model/chatProgressTypes/chatToolInvocation.js'; -import { IToolData, ToolDataSource } from '../../../../contrib/chat/common/tools/languageModelToolsService.js'; +import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../contrib/chat/common/tools/languageModelToolsService.js'; +import { IChatToolRiskAssessmentService, IToolRiskAssessment, ToolRiskLevel } from '../../../../contrib/chat/browser/tools/chatToolRiskAssessmentService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../../contrib/chat/common/constants.js'; @@ -30,18 +31,20 @@ import { FixtureMenuService, registerChatFixtureServices } from './chatFixtureUt import '../../../../contrib/chat/browser/widget/media/chat.css'; -interface IFixtureMessage { +export interface IFixtureMessage { readonly user: string; // user prompt text readonly assistant?: ReadonlyArray< | { kind: 'markdown'; text: string } | { kind: 'progress'; text: string } - | { kind: 'terminalConfirmation'; command: string; title?: string } + | { kind: 'terminalConfirmation'; command: string; title?: string; riskAssessment?: { risk: ToolRiskLevel; explanation: string }; riskLoading?: boolean } >; readonly responseComplete?: boolean; } -interface IChatWidgetFixtureOptions { +export interface IChatWidgetFixtureOptions { readonly messages: ReadonlyArray; + readonly width?: number; + readonly height?: number; } function makeUserMessage(text: string) { @@ -51,11 +54,24 @@ function makeUserMessage(text: string) { }; } -async function renderChatWidget(context: ComponentFixtureContext, options: IChatWidgetFixtureOptions): Promise { +export async function renderChatWidget(context: ComponentFixtureContext, options: IChatWidgetFixtureOptions): Promise { const { container, disposableStore } = context; const widgetHolder: { current: IChatWidget | undefined } = { current: undefined }; + const fixtureToolData: IToolData = { + id: 'fixture.terminalTool', + displayName: 'Terminal', + modelDescription: 'Run a command in the terminal', + source: ToolDataSource.Internal, + }; + + // Collect risk assessments from messages so the risk badge service can + // return them synchronously via getCached(). + const hasRiskAssessment = options.messages.some(m => m.assistant?.some(p => p.kind === 'terminalConfirmation' && p.riskAssessment)); + const hasRiskLoading = options.messages.some(m => m.assistant?.some(p => p.kind === 'terminalConfirmation' && p.riskLoading)); + const needsRiskService = hasRiskAssessment || hasRiskLoading; + const instantiationService = createEditorServices(disposableStore, { colorTheme: context.theme, additionalServices: (reg) => { @@ -74,14 +90,39 @@ async function renderChatWidget(context: ComponentFixtureContext, options: IChat override getWidgetsByLocations() { return []; } override register() { return { dispose() { } }; } }()); + + if (needsRiskService) { + reg.defineInstance(ILanguageModelToolsService, new class extends mock() { + override onDidChangeTools = Event.None; + override onDidPrepareToolCallBecomeUnresponsive = Event.None; + override getTools() { return [fixtureToolData]; } + override getTool(id: string) { return id === fixtureToolData.id ? fixtureToolData : undefined; } + }()); + reg.defineInstance(IChatToolRiskAssessmentService, new class extends mock() { + override isEnabled() { return true; } + override getCached() { + // Return the first risk assessment found in the fixture messages. + for (const m of options.messages) { + for (const p of m.assistant ?? []) { + if (p.kind === 'terminalConfirmation' && p.riskAssessment) { + return p.riskAssessment; + } + } + } + return undefined; + } + // For riskLoading: assess() never resolves, keeping the badge in loading state. + override async assess(): Promise { return new Promise(() => { }); } + }()); + } }, }); const configService = instantiationService.get(IConfigurationService) as TestConfigurationService; - await configService.setUserConfiguration('chat', { + configService.setUserConfiguration('chat', { editor: { fontSize: 13, fontFamily: 'default', fontWeight: 'default', lineHeight: 0, wordWrap: 'off' }, }); - await configService.setUserConfiguration('editor', { fontFamily: 'monospace', fontLigatures: false }); + configService.setUserConfiguration('editor', { fontFamily: 'monospace', fontLigatures: false }); configService.setUserConfiguration(ChatConfiguration.ToolConfirmationCarousel, true); // Build a real ChatModel populated with hand-crafted requests/responses, then drive a @@ -94,13 +135,6 @@ async function renderChatWidget(context: ComponentFixtureContext, options: IChat )); chatService.addSession(model); - const fixtureToolData: IToolData = { - id: 'fixture.terminalTool', - displayName: 'Terminal', - modelDescription: 'Run a command in the terminal', - source: ToolDataSource.Internal, - }; - for (const message of options.messages) { const request = model.addRequest(makeUserMessage(message.user), { variables: [] }, 0); const response = request.response!; @@ -137,8 +171,10 @@ async function renderChatWidget(context: ComponentFixtureContext, options: IChat const viewModel = disposableStore.add(instantiationService.createInstance(ChatViewModel, model, undefined)); - container.style.width = '720px'; - container.style.height = '600px'; + const width = options.width ?? 720; + const height = options.height ?? 600; + container.style.width = `${width}px`; + container.style.height = `${height}px`; container.style.backgroundColor = 'var(--vscode-sideBar-background, var(--vscode-editor-background))'; container.classList.add('monaco-workbench'); @@ -198,9 +234,7 @@ async function renderChatWidget(context: ComponentFixtureContext, options: IChat widgetHolder.current = fixtureWidget; inputPart.render(session, '', fixtureWidget); - inputPart.layout(720); - await new Promise(r => setTimeout(r, 50)); - inputPart.layout(720); + inputPart.layout(width); const listContainer = dom.$('.interactive-list'); listContainer.style.flex = '1 1 auto'; @@ -231,11 +265,7 @@ async function renderChatWidget(context: ComponentFixtureContext, options: IChat listWidget.refresh(); const listHeight = 420; - listWidget.layout(listHeight, 720); - - // Allow the renderer to flush its async progressive rendering pass. - await new Promise(r => setTimeout(r, 100)); - listWidget.layout(listHeight, 720); + listWidget.layout(listHeight, width); listWidget.scrollTop = 0; } @@ -252,7 +282,14 @@ const PENDING_TOOL_APPROVAL: IFixtureMessage[] = [ { user: 'run git init', assistant: [ - { kind: 'terminalConfirmation', command: 'git init' }, + { + kind: 'terminalConfirmation', + command: 'git init', + riskAssessment: { + risk: ToolRiskLevel.Orange, + explanation: 'Initializes a new Git repository in the current directory. Reversible by removing the .git folder.', + }, + }, ], responseComplete: false, }, diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/promptFilePickers.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/promptFilePickers.fixture.ts index 8df42a30c25d1..83ea3499dafe1 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/promptFilePickers.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/promptFilePickers.fixture.ts @@ -41,13 +41,28 @@ interface RenderPromptPickerOptions extends ComponentFixtureContext { } class FixtureQuickInputService extends QuickInputService { + private readonly _activePicks = new Set>(); + override createQuickPick(options: { useSeparators: true }): IQuickPick; override createQuickPick(options?: { useSeparators: boolean }): IQuickPick; override createQuickPick(options: { useSeparators: boolean } = { useSeparators: false }): IQuickPick { const quickPick = super.createQuickPick(options) as IQuickPick; quickPick.ignoreFocusOut = true; + this._activePicks.add(quickPick); return quickPick; } + + override dispose(): void { + // Force-hide any open picks so PromptFilePickers' onDidHide handler + // disposes its internal DisposableStore (it skips disposal while + // `ignoreFocusOut` is true). + for (const pick of this._activePicks) { + pick.ignoreFocusOut = false; + pick.hide(); + } + this._activePicks.clear(); + super.dispose(); + } } export default defineThemedFixtureGroup({ path: 'chat/' }, { diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/inlineChatZoneWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/inlineChatZoneWidget.fixture.ts index 1a8478b3dd54f..eb4b9db4dd97f 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/inlineChatZoneWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/inlineChatZoneWidget.fixture.ts @@ -397,7 +397,7 @@ function renderInlineChatZoneWidget({ container, disposableStore, theme }: Compo zoneWidget.show(new Position(10, 1)); - const dummyModel = instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.EditorInline, canUseTools: false }); + const dummyModel = disposableStore.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.EditorInline, canUseTools: false })); zoneWidget.widget.chatWidget.setModel(dummyModel); zoneWidget.widget.chatWidget.setInputPlaceholder('Ask Copilot...'); diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/other.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/other.fixture.ts index 55c97a33069ca..55f7a785480fb 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/other.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/other.fixture.ts @@ -231,7 +231,7 @@ function createLongDistanceEditor(options: { clearSuggestWidgetInlineCompletions: () => { }, dispose: () => { }, fetch: async () => true, - inlineCompletions: constObservable(new InlineCompletionsState([ + inlineCompletions: constObservable(disposableStore.add(new InlineCompletionsState([ InlineEditItem.createForTest( TextModelValueReference.snapshot(textModel), new Range( @@ -242,11 +242,11 @@ function createLongDistanceEditor(options: { ), options.newText ) - ], undefined)), + ], undefined))), loading: constObservable(false), seedInlineCompletionsWithSuggestWidget: () => { }, seedWithCompletion: () => { }, - suggestWidgetInlineCompletions: constObservable(InlineCompletionsState.createEmpty()), + suggestWidgetInlineCompletions: constObservable(disposableStore.add(InlineCompletionsState.createEmpty())), }); const editorWidgetOptions: ICodeEditorWidgetOptions = { @@ -317,17 +317,17 @@ export function createApp(config: Config) { clearSuggestWidgetInlineCompletions: () => { }, dispose: () => { }, fetch: async () => true, - inlineCompletions: constObservable(new InlineCompletionsState([ + inlineCompletions: constObservable(disposableStore.add(new InlineCompletionsState([ InlineEditItem.createForTest( TextModelValueReference.snapshot(targetModel), new Range(1, 1, 3, 100), `export interface Config {\n\tport: number;\n\thost: string;\n\tdebug: boolean;\n}` ) - ], undefined)), + ], undefined))), loading: constObservable(false), seedInlineCompletionsWithSuggestWidget: () => { }, seedWithCompletion: () => { }, - suggestWidgetInlineCompletions: constObservable(InlineCompletionsState.createEmpty()), + suggestWidgetInlineCompletions: constObservable(disposableStore.add(InlineCompletionsState.createEmpty())), }); const editor = disposableStore.add(instantiationService.createInstance( diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/views.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/views.fixture.ts index 484cb369dbacf..cef9ca0e48bd7 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/views.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/inlineCompletions/views.fixture.ts @@ -56,7 +56,7 @@ function renderInlineEdit(options: InlineEditOptions): void { clearSuggestWidgetInlineCompletions: () => { }, dispose: () => { }, fetch: async () => true, - inlineCompletions: constObservable(new InlineCompletionsState([ + inlineCompletions: constObservable(disposableStore.add(new InlineCompletionsState([ InlineEditItem.createForTest( TextModelValueReference.snapshot(textModel), new Range( @@ -67,11 +67,11 @@ function renderInlineEdit(options: InlineEditOptions): void { ), options.newText ) - ], undefined)), + ], undefined))), loading: constObservable(false), seedInlineCompletionsWithSuggestWidget: () => { }, seedWithCompletion: () => { }, - suggestWidgetInlineCompletions: constObservable(InlineCompletionsState.createEmpty()), + suggestWidgetInlineCompletions: constObservable(disposableStore.add(InlineCompletionsState.createEmpty())), }); const editorWidgetOptions: ICodeEditorWidgetOptions = { diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/multiDiffEditor.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/multiDiffEditor.fixture.ts index cf8c005f0ac70..3d2758eb3275a 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/multiDiffEditor.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/multiDiffEditor.fixture.ts @@ -124,7 +124,7 @@ function renderMultiDiffEditor({ container, disposableStore, theme }: ComponentF documents: ValueWithChangeEvent.const([doc1, doc2, doc3]), }; - const viewModel = widget.createViewModel(model); + const viewModel = disposableStore.add(widget.createViewModel(model)); widget.setViewModel(viewModel); widget.layout(new Dimension(800, 600)); } @@ -141,9 +141,9 @@ class DelayedDocumentDiffProvider implements IDocumentDiffProvider { readonly onDidChange: Event = () => toDisposable(() => { }); constructor(private readonly _delayMs: number) { } - async computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions, _cancellationToken: CancellationToken): Promise { - await timeout(this._delayMs); - if (_cancellationToken.isCancellationRequested || original.isDisposed() || modified.isDisposed()) { + async computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions, cancellationToken: CancellationToken): Promise { + await timeout(this._delayMs, cancellationToken); + if (cancellationToken.isCancellationRequested || original.isDisposed() || modified.isDisposed()) { return ({ changes: [], quitEarly: true, @@ -223,7 +223,7 @@ function renderMultiDiffEditorIncrementalUpdate() { // Start with only doc1 — its diff resolves immediately (800ms virtual) const documents = new ValueWithChangeEvent[]>([doc1]); const model: IMultiDiffEditorModel = { documents }; - const viewModel = widget.createViewModel(model); + const viewModel = disposableStore.add(widget.createViewModel(model)); widget.setViewModel(viewModel); widget.layout(new Dimension(800, 600)); @@ -266,7 +266,7 @@ function renderMultiDiffEditorDocumentSwap() { // Start with A and B const documents = new ValueWithChangeEvent[]>([docA, docB]); const model: IMultiDiffEditorModel = { documents }; - const viewModel = widget.createViewModel(model); + const viewModel = disposableStore.add(widget.createViewModel(model)); widget.setViewModel(viewModel); widget.layout(new Dimension(800, 600)); diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/peekReference.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/peekReference.fixture.ts index 938e24504ba58..a66087769e686 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/peekReference.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/peekReference.fixture.ts @@ -106,7 +106,7 @@ function renderPeekReference({ container, disposableStore, theme }: ComponentFix contributions: [] }; - const editor = disposableStore.add(instantiationService.createInstance( + const editor = instantiationService.createInstance( CodeEditorWidget, container, { @@ -118,7 +118,7 @@ function renderPeekReference({ container, disposableStore, theme }: ComponentFix cursorBlinking: 'solid', }, editorWidgetOptions - )); + ); editor.setModel(textModel); editor.focus(); @@ -131,7 +131,11 @@ function renderPeekReference({ container, disposableStore, theme }: ComponentFix true, layoutData, ); + // Register widget BEFORE editor so widget.dispose() runs first; otherwise + // `ReferenceWidget.dispose()` calls `observableCodeEditor(disposed editor)` + // which creates a fresh untracked ObservableCodeEditor. disposableStore.add(referenceWidget); + disposableStore.add(editor); const range = { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 21 }; referenceWidget.setTitle('processFile'); diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/renameWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/renameWidget.fixture.ts index 37e16a7b6a5f8..364cea7621c90 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/renameWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/renameWidget.fixture.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { toDisposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; @@ -91,6 +92,7 @@ function renderRenameWidget(options: RenameFixtureOptions): void { undefined, cts ); + disposableStore.add(toDisposable(() => renameWidget.cancelInput(false, 'fixture-teardown'))); } export default defineThemedFixtureGroup({ path: 'editor/' }, { diff --git a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts index 9595e4c8fa8a5..113f198fcd0bf 100644 --- a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts @@ -8,6 +8,7 @@ import { defineFixture, defineFixtureGroup, defineFixtureVariants } from '@vscode/component-explorer'; import { DisposableStore, DisposableTracker, IDisposable, IReference, setDisposableTracker, toDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; +import { ModifierKeyEmitter } from '../../../../base/browser/dom.js'; // eslint-disable-next-line local/code-import-patterns import '../../../../../../build/vite/style.css'; import '../../../browser/media/style.css'; @@ -444,6 +445,23 @@ export class FixtureLogService extends NullLogService { } } +/** + * `ModelService` for fixtures that disposes all owned text models when the + * service itself is disposed. This is safe because `TestInstantiationService` + * is the first item added to the fixture's `DisposableStore`, so it disposes + * last (LIFO) — after all widgets have already torn down. + */ +export class FixtureModelService extends ModelService { + override dispose(): void { + for (const model of this.getModels()) { + if (!model.isDisposed()) { + model.dispose(); + } + } + super.dispose(); + } +} + /** * `ITextModelService` for fixtures that resolves URIs against `IModelService`. * Models created via `createTextModel` (which uses `IModelService.createModel`) @@ -524,7 +542,7 @@ export function createEditorServices(disposables: DisposableStore, options?: Cre define(IThemeService, TestThemeService); } define(ILogService, FixtureLogService); - define(IModelService, ModelService); + define(IModelService, FixtureModelService); define(ICodeEditorService, TestCodeEditorService); define(IContextKeyService, MockContextKeyService); define(ICommandService, TestCommandService); @@ -661,7 +679,16 @@ export function createEditorServices(disposables: DisposableStore, options?: Cre }, }); - const instantiationService = disposables.add(new TestInstantiationService(services, true)); + // Pass `_properDispose: true` so the underlying `InstantiationService`'s + // dispose runs, which disposes services it instantiated lazily from + // `SyncDescriptor`s (e.g. MenuService, ContextKeyService). Without this, + // production services with internal Disposables leak past the fixture. + // + // Don't add TestInstantiationService to disposables immediately — it must + // dispose runs, which disposes services it instantiated lazily from + // `SyncDescriptor`s (e.g. MenuService, ContextKeyService). Without this, + // production services with internal Disposables leak past the fixture. + const instantiationService = disposables.add(new TestInstantiationService(services, true, undefined, true)); disposables.add(toDisposable(() => { for (const id of serviceIdentifiers) { @@ -781,7 +808,7 @@ export interface ComponentFixtureContext { export interface ComponentFixtureOptions { render: (context: ComponentFixtureContext) => void | Promise; labels?: ThemedFixtureGroupLabels; - virtualTime?: { enabled?: boolean; durationMs?: number }; + virtualTime?: { enabled?: boolean; durationMs?: number; teardownDrainMs?: number }; } type ThemedFixtures = ReturnType; @@ -800,10 +827,6 @@ if (logOutsideTime) { let fixtureRenderCounter = 0; -// See TODO in defineComponentFixture: leak errors detected during teardown are -// stashed here and rethrown from the next fixture render. -let pendingLeakErrorToThrow: Error | undefined; - /** * Creates Dark and Light fixture variants from a single render function. * The render function receives a context with container and disposableStore. @@ -818,60 +841,98 @@ export function defineComponentFixture(options: ComponentFixtureOptions): Themed displayMode: { type: 'component' }, background: theme === darkTheme ? 'dark' : 'light', render: async (container: HTMLElement, context) => { - // TODO: component-explorer currently ignores errors thrown from the - // teardown disposable (where leak detection runs, after the screenshot). - // Until it surfaces those, we stash the leak error and rethrow it from - // the next fixture render so the failure still becomes visible. - const pendingLeakError = pendingLeakErrorToThrow; - pendingLeakErrorToThrow = undefined; - if (pendingLeakError) { - throw pendingLeakError; - } - const disposableStore = new DisposableStore(); // Do not enable virtual time in explorer ui, as multiple fixtures are rendered in parallel. const virtualTimeEnabled = (options.virtualTime?.enabled ?? true) && context.host.kind !== 'explorer-ui'; - // Detect disposable leaks the same way unit tests do (`ensureNoDisposablesAreLeakedInTestSuite`). // The tracker is global and therefore unsafe when fixtures render in parallel, // so it is only enabled outside the explorer UI (e.g. in screenshot/CI mode). - const leakDetectionEnabled = false && context.host.kind !== 'explorer-ui'; + const leakDetectionEnabled = true && context.host.kind !== 'explorer-ui'; + // Warm up the `ModifierKeyEmitter` singleton before the leak tracker + // starts so its long-lived `DisposableStore` (created on first + // `MenuEntryActionViewItem.render`) doesn't show up as a leak in + // the first fixture that uses a menu toolbar. + if (leakDetectionEnabled) { + ModifierKeyEmitter.getInstance(); + } const tracker = leakDetectionEnabled ? new DisposableTracker() : undefined; if (tracker) { setDisposableTracker(tracker); } - const leakLabel = `${(options.labels ? resolveLabels(options.labels).join('/') : '')}/${theme === darkTheme ? 'Dark' : 'Light'} (render#${fixtureRenderCounter + 1})`; + // Virtual time infrastructure lives across the whole fixture + // lifetime (render + dispose). This lets us advance virtual time + // during dispose to drain async cleanup work (e.g. `Promise.race` + // guards behind `timeout(1000)` that hold references until they + // settle) before the leak tracker checks for undisposed objects. + const clock = new VirtualClock(Date.now()); + const p = new VirtualTimeProcessor( + clock, + drainMicrotasksEmbedding(realTimeApi), + realTimeApi, + { defaultMaxEvents: 100 }, + ); + const virtualTimeApi = createVirtualTimeApi(clock, { fakeRequestAnimationFrame: true }); + const teardownDrainMs = options.virtualTime?.teardownDrainMs ?? 1100; + + // Single async dispose orchestrates teardown order: + // 1. dispose user disposables (synchronous part) + // 2. drain virtual time (so timers scheduled during dispose + // — like `Promise.race([..., timeout(1000)])` — settle and + // release their captured references) + // 3. tear down virtual time (uninstall global API, dispose `p`) + // 4. stop tracker and check for leaks + // All on one disposable so the steps run in order. + context.addDisposable({ + dispose: async () => { + // Re-push virtual time so any `setTimeout`/`setInterval` + // calls made by `dispose()` of fixture-owned objects + // land in `p` and can be drained below. Render unpushes + // virtual time when it completes (so screenshot capture + // etc. can use real timers), so we have to push again. + let teardownTimeApi: IDisposable | undefined; + if (virtualTimeEnabled) { + teardownTimeApi = pushGlobalTimeApi(virtualTimeApi); + } - context.addDisposable(toDisposable(() => { - disposableStore.dispose(); - if (tracker) { - setDisposableTracker(null); - const result = tracker.computeLeakingDisposables(); - if (result) { - console.error(result.details); - pendingLeakErrorToThrow = new Error(`[leak detected in previous fixture: ${leakLabel}] There are ${result.leaks.length} undisposed disposables!${result.details}`); + try { + disposableStore.dispose(); + } catch (e) { + console.error(`[ComponentFixture] error disposing fixture: ${e instanceof Error ? e.stack : e}`); } - } - })); - async function actualRender() { - const schedulerStore = disposableStore.add(new DisposableStore()); - const clock = new VirtualClock(Date.now()); - const p = schedulerStore.add(new VirtualTimeProcessor( - clock, - drainMicrotasksEmbedding(realTimeApi), - realTimeApi, - { defaultMaxEvents: 100 }, - )); + if (virtualTimeEnabled) { + try { + await p.run({ + until: untilTime(clock.now + teardownDrainMs), + maxEvents: 1000, + maxTraceDepth: 5, + }); + } catch (e) { + console.error(`[ComponentFixture] error draining virtual time during teardown: ${e instanceof Error ? e.stack : e}`); + } + } - await setupTheme(container, theme); + teardownTimeApi?.dispose(); + p.dispose(); + + if (tracker) { + setDisposableTracker(null); + const result = tracker.computeLeakingDisposables(); + if (result) { + throw new Error(`There are ${result.leaks.length} undisposed disposables!${result.details}`); + } + } + }, + }); - const virtualTimeApi = createVirtualTimeApi(clock, { fakeRequestAnimationFrame: true }); + async function actualRender() { + await setupTheme(container, theme); + let renderTimeApi: IDisposable | undefined; if (virtualTimeEnabled) { - schedulerStore.add(pushGlobalTimeApi(virtualTimeApi)); + renderTimeApi = pushGlobalTimeApi(virtualTimeApi); disposableStore.add(installFakeRunWhenIdle((_targetWindow, callback, _timeout?) => { const stackTrace = new Error().stack; @@ -917,7 +978,9 @@ export function defineComponentFixture(options: ComponentFixtureOptions): Themed } throw e; } finally { - schedulerStore.dispose(); + // Unpush virtual time so the post-render flow (screenshot + // capture, stability checks, …) runs with real timers. + renderTimeApi?.dispose(); } } @@ -932,6 +995,14 @@ export function defineComponentFixture(options: ComponentFixtureOptions): Themed // Trace-reset escapes virtual time so it actually fires. afterMicrotaskClosure: cb => nextMacrotask(realTimeApi, cb), }); + + const wantsTimeTrace = !!context.input && typeof context.input === 'object' && !!(context.input as Record).outputTimeTrace; + if (wantsTimeTrace && virtualTimeEnabled && p.history.length > 0) { + const startTime = p.history[0].time; + const history = buildHistoryFromTasks(p.history, startTime); + return { output: renderSwimlanes(history) }; + } + return undefined; }, }); diff --git a/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts index 88dc3ac98f9c0..89a853231a5b6 100644 --- a/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts @@ -60,7 +60,7 @@ async function renderCarousel(context: ComponentFixtureContext, collection: IIma colorTheme: theme, additionalServices: ({ defineInstance }) => { const fileService = new FileService(new NullLogService()); - fileService.registerProvider(Schemas.file, new NullFileSystemProvider()); + disposableStore.add(fileService.registerProvider(Schemas.file, new NullFileSystemProvider())); disposableStore.add(fileService); defineInstance(IFileService, fileService); defineInstance(IWebviewService, new class extends mock() { }()); @@ -73,7 +73,7 @@ async function renderCarousel(context: ComponentFixtureContext, collection: IIma editor.create(container); editor.layout(new Dimension(600, 500)); - const input = new ImageCarouselEditorInput(collection, startIndex); + const input = disposableStore.add(new ImageCarouselEditorInput(collection, startIndex)); await editor.setInput(input, undefined, {}, CancellationToken.None); } diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/agentSessionsViewer.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/agentSessionsViewer.fixture.ts index 1c06a3540bc7d..45c4a252e5658 100644 --- a/src/vs/workbench/test/browser/componentFixtures/sessions/agentSessionsViewer.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/agentSessionsViewer.fixture.ts @@ -11,6 +11,7 @@ import { FuzzyScore } from '../../../../../base/common/filters.js'; import { ITreeNode } from '../../../../../base/browser/ui/tree/tree.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { Event } from '../../../../../base/common/event.js'; +import { toDisposable } from '../../../../../base/common/lifecycle.js'; import { IMarkdownRendererService, MarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -134,7 +135,12 @@ function renderSessionItem(ctx: ComponentFixtureContext, session: IAgentSession, container.appendChild(listRow); const template = renderer.renderTemplate(listRow); - renderer.renderElement(wrapAsTreeNode(session), 0, template); + const treeNode = wrapAsTreeNode(session); + renderer.renderElement(treeNode, 0, template); + disposableStore.add(toDisposable(() => { + renderer.disposeElement(treeNode, 0, template); + renderer.disposeTemplate(template); + })); } function renderSectionItem(ctx: ComponentFixtureContext, section: IAgentSessionSection): void { @@ -160,7 +166,12 @@ function renderSectionItem(ctx: ComponentFixtureContext, section: IAgentSessionS container.appendChild(listRow); const template = renderer.renderTemplate(listRow); - renderer.renderElement(wrapAsTreeNode(section), 0, template); + const treeNode = wrapAsTreeNode(section); + renderer.renderElement(treeNode, 0, template); + disposableStore.add(toDisposable(() => { + renderer.disposeElement(treeNode, 0, template); + renderer.disposeTemplate(template); + })); } // ============================================================================ diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts index b55ed9c1a8a9d..a1ec2496ce54b 100644 --- a/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/aiCustomizationManagementEditor.fixture.ts @@ -703,7 +703,8 @@ async function renderEditor(ctx: ComponentFixtureContext, options: IRenderEditor languageServiceRef.value = instantiationService.get(ILanguageService); for (const [uri, content] of fileContents) { if (!modelServiceRef.value.getModel(uri)) { - modelServiceRef.value.createModel(content, null, uri, false); + const model = modelServiceRef.value.createModel(content, null, uri, false); + ctx.disposableStore.add({ dispose: () => model.dispose() }); } } @@ -713,7 +714,8 @@ async function renderEditor(ctx: ComponentFixtureContext, options: IRenderEditor editor.create(ctx.container); editor.layout(new Dimension(width, height)); - await editor.setInput(AICustomizationManagementEditorInput.getOrCreate(), undefined, {}, CancellationToken.None); + const editorInput = ctx.disposableStore.add(AICustomizationManagementEditorInput.getOrCreate()); + await editor.setInput(editorInput, undefined, {}, CancellationToken.None); if (options.selectedSection) { editor.selectSectionById(options.selectedSection); diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index f1c685b03794f..c56e45f72c864 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -105,8 +105,6 @@ export class TestNativeHostService implements INativeHostService { async openAgentsWindow(_options?: { folderUri?: UriComponents }): Promise { } - async launchSiblingApp(_args?: string[]): Promise { } - async toggleFullScreen(): Promise { } async isMaximized(): Promise { return true; } async isFullScreen(): Promise { return true; } diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 4277093b10389..4530bb0ec7a9b 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -187,6 +187,9 @@ import './contrib/encryption/electron-browser/encryption.contribution.js'; // Emergency Alert import './contrib/emergencyAlert/electron-browser/emergencyAlert.contribution.js'; +// Agents App Merged Banner +import './contrib/agentsAppMergedBanner/browser/agentsAppMergedBanner.contribution.js'; + // MCP import './contrib/mcp/electron-browser/mcp.contribution.js'; diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index df78e006e8e0f..e5cff6649f79b 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -12249,6 +12249,8 @@ declare module 'vscode' { /** * Get the children of `element` or root if no element is passed. * + * *Note:* The result is not mutated by the API consumer; readonly arrays may be cast to `T[]`. + * * @param element The element from which the provider gets children. Can be `undefined`. * @returns Children of `element` or root if no element is passed. */ diff --git a/src/vscode-dts/vscode.proposed.documentDiff.d.ts b/src/vscode-dts/vscode.proposed.documentDiff.d.ts new file mode 100644 index 0000000000000..2e9c5320c0200 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.documentDiff.d.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export namespace workspace { + + /** + * Compute the diff between two text documents. + * + * This uses the same diff algorithm that powers the built-in diff editor, + * returning line-level and character-level change mappings. + * + * @param originalDocument The original (left-hand side) document. + * @param modifiedDocument The modified (right-hand side) document. + * @param options Options to control the diff computation. + * @param token A cancellation token. + * + * @returns A response object with streaming changes and a completion promise. + */ + export function getTextDiff(originalDocument: TextDocument, modifiedDocument: TextDocument, options?: TextDiffOptions, token?: CancellationToken): TextDiffResponse; + } + + /** + * Options for computing a text diff. + */ + export interface TextDiffOptions { + /** + * When `true`, the diff algorithm ignores changes in leading and trailing whitespace. + * Defaults to `false`. + */ + readonly ignoreTrimWhitespace?: boolean; + + /** + * Maximum time in milliseconds to spend computing the diff. + * `0` means no limit. Defaults to `5000`. + */ + readonly maxComputationTimeMs?: number; + + /** + * When `true`, the diff algorithm also computes moved text blocks. + * Defaults to `false`. + */ + readonly computeMoves?: boolean; + } + + /** + * The response from {@link workspace.getTextDiff}. + */ + export interface TextDiffResponse { + /** + * The line-level changes between the two documents, streamed as they are computed. + */ + readonly changes: AsyncIterable; + + /** + * Resolves when the diff computation is complete, with summary information. + */ + readonly complete: Thenable; + } + + /** + * Completion information for a text diff computation. + */ + export interface TextDiffComplete { + /** + * `true` if both documents are identical (byte-wise). + * + * A diff may return 0 changes but still have `identical` be `false`. This can happen if different diff options + * are passed in for example. + */ + readonly identical: boolean; + + /** + * `true` if the diff computation timed out and the result may be inaccurate. + */ + readonly mayBeIncomplete: boolean; + + /** + * Detected text moves (blocks of text that were moved from one location to another). + * Only populated when {@link DocumentDiffOptions.computeMoves} is `true`. + */ + readonly moves: readonly TextDiffMove[]; + } + + /** + * Represents a line-level change between two documents, optionally + * containing character-level (inner) changes. + */ + export interface TextDiffChange { + /** + * The line range in the original document. + */ + readonly originalRange: Range; + + /** + * The line range in the modified document. + */ + readonly modifiedRange: Range; + + /** + * Character-level changes within this line range change. + * May be `undefined` if inner changes were not computed. + */ + readonly innerChanges: readonly TextDiffInnerChange[] | undefined; + } + + /** + * Represents a character-level change within a {@link TextDiffChange}. + */ + export interface TextDiffInnerChange { + /** + * The range in the original document. + */ + readonly originalRange: Range; + + /** + * The range in the modified document. + */ + readonly modifiedRange: Range; + } + + /** + * Represents a detected text move between two documents. + */ + export interface TextDiffMove { + /** + * The line range in the original document that was moved. + */ + readonly originalRange: Range; + + /** + * The line range in the modified document where the text was moved to. + */ + readonly modifiedRange: Range; + + /** + * The changes within the moved text (differences between the original + * and the moved copy). + */ + readonly changes: readonly TextDiffChange[]; + } +} diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index bae3bfa5d9462..bd24534e9a0f6 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -489,12 +489,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", - "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -753,9 +753,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12"