From 493874c390d147f9da7bde871e381bf594b784bf Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 16 Apr 2025 16:04:12 +0200 Subject: [PATCH 001/114] migrate github-extension to ESM --- build/lib/extensions.ts | 9 +- ...config.js => extension.webpack.config.cjs} | 7 + extensions/github/package-lock.json | 358 +++++++----------- extensions/github/package.json | 5 +- extensions/github/src/auth.ts | 4 +- extensions/github/src/branchProtection.ts | 10 +- extensions/github/src/canonicalUriProvider.ts | 2 +- extensions/github/src/commands.ts | 8 +- extensions/github/src/credentialProvider.ts | 4 +- extensions/github/src/extension.ts | 30 +- .../github/src/historyItemDetailsProvider.ts | 8 +- extensions/github/src/links.ts | 4 +- extensions/github/src/publish.ts | 6 +- extensions/github/src/pushErrorHandler.ts | 10 +- extensions/github/src/remoteSourceProvider.ts | 8 +- .../github/src/remoteSourcePublisher.ts | 4 +- extensions/github/src/shareProviders.ts | 6 +- extensions/github/src/test/github.test.ts | 2 +- extensions/github/src/test/index.ts | 4 +- extensions/github/src/util.ts | 2 +- extensions/github/tsconfig.json | 4 + 21 files changed, 217 insertions(+), 278 deletions(-) rename extensions/github/{extension.webpack.config.js => extension.webpack.config.cjs} (83%) diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 908480b6077..b900802ed6a 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -14,7 +14,6 @@ import { Stream } from 'stream'; import File from 'vinyl'; import { createStatsStream } from './stats'; import * as util2 from './util'; -const vzip = require('gulp-vinyl-zip'); import filter from 'gulp-filter'; import rename from 'gulp-rename'; import fancyLog from 'fancy-log'; @@ -26,6 +25,7 @@ import { getProductionDependencies } from './dependencies'; import { IExtensionDefinition, getExtensionStream } from './builtInExtensions'; import { getVersion } from './getVersion'; import { fetchUrls, fetchGithub } from './fetch'; +const vzip = require('gulp-vinyl-zip'); const root = path.dirname(path.dirname(__dirname)); const commit = getVersion(root); @@ -62,7 +62,12 @@ function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): } function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolean): Stream { - const webpackConfigFileName = forWeb ? 'extension-browser.webpack.config.js' : 'extension.webpack.config.js'; + + const esm = JSON.parse(fs.readFileSync(path.join(extensionPath, 'package.json'), 'utf8')).type === 'module'; + + const webpackConfigFileName = forWeb + ? `extension-browser.webpack.config.${!esm ? 'js' : 'cjs'}` + : `extension.webpack.config.${!esm ? 'js' : 'cjs'}`; const isWebPacked = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); let input = isWebPacked diff --git a/extensions/github/extension.webpack.config.js b/extensions/github/extension.webpack.config.cjs similarity index 83% rename from extensions/github/extension.webpack.config.js rename to extensions/github/extension.webpack.config.cjs index 45600607fc5..e0ac991de2e 100644 --- a/extensions/github/extension.webpack.config.js +++ b/extensions/github/extension.webpack.config.cjs @@ -13,5 +13,12 @@ module.exports = withDefaults({ context: __dirname, entry: { extension: './src/extension.ts' + }, + output: { + libraryTarget: 'module', + chunkFormat: 'module', + }, + experiments: { + outputModule: true } }); diff --git a/extensions/github/package-lock.json b/extensions/github/package-lock.json index 1b7dc727a92..109aba370d1 100644 --- a/extensions/github/package-lock.json +++ b/extensions/github/package-lock.json @@ -9,9 +9,9 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@octokit/graphql": "5.0.5", + "@octokit/graphql": "8.2.0", "@octokit/graphql-schema": "14.4.0", - "@octokit/rest": "19.0.4", + "@octokit/rest": "21.1.0", "@vscode/extension-telemetry": "^0.9.8", "tunnel": "^0.0.6" }, @@ -147,96 +147,92 @@ "license": "MIT" }, "node_modules/@octokit/auth-token": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.1.tgz", - "integrity": "sha512-/USkK4cioY209wXRpund6HZzHo9GmjakpV9ycOkpMcMxMk7QVcVFVyCMtzvXYiHsB2crgDgrtNYSELYFBXhhaA==", - "dependencies": { - "@octokit/types": "^7.0.0" - }, + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", + "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-token/node_modules/@octokit/openapi-types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", - "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" - }, - "node_modules/@octokit/auth-token/node_modules/@octokit/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", - "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", - "dependencies": { - "@octokit/openapi-types": "^13.6.0" + "node": ">= 18" } }, "node_modules/@octokit/core": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.0.5.tgz", - "integrity": "sha512-4R3HeHTYVHCfzSAi0C6pbGXV8UDI5Rk+k3G7kLVNckswN9mvpOzW9oENfjfH3nEmzg8y3AmKmzs8Sg6pLCeOCA==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz", + "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==", "dependencies": { - "@octokit/auth-token": "^3.0.0", - "@octokit/graphql": "^5.0.0", - "@octokit/request": "^6.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^7.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.2.2", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/graphql": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", + "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", + "dependencies": { + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", - "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==" }, "node_modules/@octokit/core/node_modules/@octokit/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", - "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", + "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", "dependencies": { - "@octokit/openapi-types": "^13.6.0" + "@octokit/openapi-types": "^25.0.0" } }, "node_modules/@octokit/endpoint": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.1.tgz", - "integrity": "sha512-/wTXAJwt0HzJ2IeE4kQXO+mBScfzyCkI0hMtkIaqyXd9zg76OpOfNQfHL9FlaxAV2RsNiOXZibVWloy8EexENg==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", "dependencies": { - "@octokit/types": "^7.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", - "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==" }, "node_modules/@octokit/endpoint/node_modules/@octokit/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", - "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", + "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", "dependencies": { - "@octokit/openapi-types": "^13.6.0" + "@octokit/openapi-types": "^25.0.0" } }, "node_modules/@octokit/graphql": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz", - "integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.0.tgz", + "integrity": "sha512-gejfDywEml/45SqbWTWrhfwvLBrcGYhOn50sPOjIeVvH6i7D16/9xcFA8dAJNp2HMcd+g4vru41g4E2RBiZvfQ==", "dependencies": { - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/request": "^9.1.4", + "@octokit/types": "^13.8.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/graphql-schema": { @@ -249,148 +245,121 @@ } }, "node_modules/@octokit/openapi-types": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-17.1.0.tgz", - "integrity": "sha512-rnI26BAITDZTo5vqFOmA7oX4xRd18rO+gcK4MiTpJmsRMxAw0JmevNjPsjpry1bb9SVNo56P/0kbiyXXa4QluA==" + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-4.2.0.tgz", - "integrity": "sha512-8otLCIK9esfmOCY14CBnG/xPqv0paf14rc+s9tHpbOpeFwrv5CnECKW1qdqMAT60ngAa9eB1bKQ+l2YCpi0HPQ==", + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz", + "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==", "dependencies": { - "@octokit/types": "^7.2.0" + "@octokit/types": "^13.10.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=4" - } - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", - "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", - "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", - "dependencies": { - "@octokit/openapi-types": "^13.6.0" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-request-log": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", - "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", + "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", + "engines": { + "node": ">= 18" + }, "peerDependencies": { - "@octokit/core": ">=3" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.4.0.tgz", - "integrity": "sha512-YP4eUqZ6vORy/eZOTdil1ZSrMt0kv7i/CVw+HhC2C0yJN+IqTc/rot957JQ7JfyeJD6HZOjLg6Jp1o9cPhI9KA==", + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz", + "integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==", "dependencies": { - "@octokit/types": "^7.2.0", - "deprecation": "^2.3.1" + "@octokit/types": "^13.10.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=3" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", - "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", - "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", - "dependencies": { - "@octokit/openapi-types": "^13.6.0" + "@octokit/core": ">=6" } }, "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": "9.2.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", + "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^7.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "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": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", "dependencies": { - "@octokit/types": "^7.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/types": "^14.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", - "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==" }, "node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", - "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", + "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", "dependencies": { - "@octokit/openapi-types": "^13.6.0" + "@octokit/openapi-types": "^25.0.0" } }, "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", - "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==" }, "node_modules/@octokit/request/node_modules/@octokit/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", - "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", + "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", "dependencies": { - "@octokit/openapi-types": "^13.6.0" + "@octokit/openapi-types": "^25.0.0" } }, "node_modules/@octokit/rest": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.4.tgz", - "integrity": "sha512-LwG668+6lE8zlSYOfwPj4FxWdv/qFXYBpv79TWIQEpBLKA9D/IMcWsF/U9RGpA3YqMVDiTxpgVpEW3zTFfPFTA==", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.0.tgz", + "integrity": "sha512-93iLxcKDJboUpmnUyeJ6cRIi7z7cqTZT1K7kRK4LobGxwTwpsa+2tQQbRQNGy7IFDEAmrtkf4F4wBj3D5rVlJQ==", "dependencies": { - "@octokit/core": "^4.0.0", - "@octokit/plugin-paginate-rest": "^4.0.0", - "@octokit/plugin-request-log": "^1.0.4", - "@octokit/plugin-rest-endpoint-methods": "^6.0.0" + "@octokit/core": "^6.1.3", + "@octokit/plugin-paginate-rest": "^11.4.0", + "@octokit/plugin-request-log": "^5.3.1", + "@octokit/plugin-rest-endpoint-methods": "^13.3.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/types": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.2.0.tgz", - "integrity": "sha512-xySzJG4noWrIBFyMu4lg4tu9vAgNg9S0aoLRONhAEz6ueyi1evBzb40HitIosaYS4XOexphG305IVcLrIX/30g==", + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", "dependencies": { - "@octokit/openapi-types": "^17.1.0" + "@octokit/openapi-types": "^24.2.0" } }, "node_modules/@types/node": { @@ -417,14 +386,24 @@ } }, "node_modules/before-after-hook": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", - "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==" }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + "node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] }, "node_modules/graphql": { "version": "16.8.1", @@ -448,46 +427,6 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E= sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -508,28 +447,9 @@ "dev": true }, "node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" } } } diff --git a/extensions/github/package.json b/extensions/github/package.json index 524cee5bbea..86b54f3303d 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -20,6 +20,7 @@ "vscode.git-base" ], "main": "./out/extension.js", + "type": "module", "capabilities": { "virtualWorkspaces": false, "untrustedWorkspaces": { @@ -227,9 +228,9 @@ "watch": "gulp watch-extension:github" }, "dependencies": { - "@octokit/graphql": "5.0.5", + "@octokit/graphql": "8.2.0", "@octokit/graphql-schema": "14.4.0", - "@octokit/rest": "19.0.4", + "@octokit/rest": "21.1.0", "tunnel": "^0.0.6", "@vscode/extension-telemetry": "^0.9.8" }, diff --git a/extensions/github/src/auth.ts b/extensions/github/src/auth.ts index e7be2637da0..f27967d22f7 100644 --- a/extensions/github/src/auth.ts +++ b/extensions/github/src/auth.ts @@ -5,7 +5,7 @@ import { AuthenticationSession, authentication, window } from 'vscode'; import { Agent, globalAgent } from 'https'; -import { graphql } from '@octokit/graphql/dist-types/types'; +import { graphql } from '@octokit/graphql/types'; import { Octokit } from '@octokit/rest'; import { httpsOverHttp } from 'tunnel'; import { URL } from 'url'; @@ -71,7 +71,7 @@ export async function getOctokitGraphql(): Promise { const token = session.accessToken; const { graphql } = await import('@octokit/graphql'); - return graphql.defaults({ + return graphql({ headers: { authorization: `token ${token}` }, diff --git a/extensions/github/src/branchProtection.ts b/extensions/github/src/branchProtection.ts index 52b4fbfae22..b381320962e 100644 --- a/extensions/github/src/branchProtection.ts +++ b/extensions/github/src/branchProtection.ts @@ -5,9 +5,9 @@ import { authentication, EventEmitter, LogOutputChannel, Memento, Uri, workspace } from 'vscode'; import { Repository as GitHubRepository, RepositoryRuleset } from '@octokit/graphql-schema'; -import { AuthenticationError, getOctokitGraphql } from './auth'; -import { API, BranchProtection, BranchProtectionProvider, BranchProtectionRule, Repository } from './typings/git'; -import { DisposableStore, getRepositoryFromUrl } from './util'; +import { AuthenticationError, getOctokitGraphql } from './auth.js'; +import { API, BranchProtection, BranchProtectionProvider, BranchProtectionRule, Repository } from './typings/git.js'; +import { DisposableStore, getRepositoryFromUrl } from './util.js'; import TelemetryReporter from '@vscode/extension-telemetry'; const REPOSITORY_QUERY = ` @@ -74,7 +74,7 @@ export class GitHubBranchProtectionProviderManager { private readonly gitAPI: API, private readonly globalState: Memento, private readonly logger: LogOutputChannel, - private readonly telemetryReporter: TelemetryReporter) { + private readonly telemetryReporter: TelemetryReporter.default) { this.disposables.add(this.gitAPI.onDidOpenRepository(repository => { if (this._enabled) { this.providerDisposables.add(gitAPI.registerBranchProtectionProvider(repository.rootUri, new GitHubBranchProtectionProvider(repository, this.globalState, this.logger, this.telemetryReporter))); @@ -113,7 +113,7 @@ export class GitHubBranchProtectionProvider implements BranchProtectionProvider private readonly repository: Repository, private readonly globalState: Memento, private readonly logger: LogOutputChannel, - private readonly telemetryReporter: TelemetryReporter) { + private readonly telemetryReporter: TelemetryReporter.default) { // Restore branch protection from global state this.branchProtection = this.globalState.get(this.globalStateKey, []); diff --git a/extensions/github/src/canonicalUriProvider.ts b/extensions/github/src/canonicalUriProvider.ts index 09f5e243bc1..0838c7377dd 100644 --- a/extensions/github/src/canonicalUriProvider.ts +++ b/extensions/github/src/canonicalUriProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, CanonicalUriProvider, CanonicalUriRequestOptions, Disposable, ProviderResult, Uri, workspace } from 'vscode'; -import { API } from './typings/git'; +import { API } from './typings/git.js'; const SUPPORTED_SCHEMES = ['ssh', 'https', 'file']; diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 4e5587c09b5..48e9574c708 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API as GitAPI, RefType, Repository } from './typings/git'; -import { publishRepository } from './publish'; -import { DisposableStore, getRepositoryFromUrl } from './util'; -import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links'; +import { API as GitAPI, RefType, Repository } from './typings/git.js'; +import { publishRepository } from './publish.js'; +import { DisposableStore, getRepositoryFromUrl } from './util.js'; +import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links.js'; async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) { try { diff --git a/extensions/github/src/credentialProvider.ts b/extensions/github/src/credentialProvider.ts index 14c7e6a2c73..d184960c23b 100644 --- a/extensions/github/src/credentialProvider.ts +++ b/extensions/github/src/credentialProvider.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CredentialsProvider, Credentials, API as GitAPI } from './typings/git'; +import { CredentialsProvider, Credentials, API as GitAPI } from './typings/git.js'; import { workspace, Uri, Disposable } from 'vscode'; -import { getSession } from './auth'; +import { getSession } from './auth.js'; const EmptyDisposable: Disposable = { dispose() { } }; diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index de0349ba9d3..6c0b848737a 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -5,18 +5,18 @@ import { commands, Disposable, ExtensionContext, extensions, l10n, LogLevel, LogOutputChannel, window } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; -import { GithubRemoteSourceProvider } from './remoteSourceProvider'; -import { API, GitExtension } from './typings/git'; -import { registerCommands } from './commands'; -import { GithubCredentialProviderManager } from './credentialProvider'; -import { DisposableStore, repositoryHasGitHubRemote } from './util'; -import { GithubPushErrorHandler } from './pushErrorHandler'; -import { GitBaseExtension } from './typings/git-base'; -import { GithubRemoteSourcePublisher } from './remoteSourcePublisher'; -import { GitHubBranchProtectionProviderManager } from './branchProtection'; -import { GitHubCanonicalUriProvider } from './canonicalUriProvider'; -import { VscodeDevShareProvider } from './shareProviders'; -import { GitHubSourceControlHistoryItemDetailsProvider } from './historyItemDetailsProvider'; +import { GithubRemoteSourceProvider } from './remoteSourceProvider.js'; +import { API, GitExtension } from './typings/git.js'; +import { registerCommands } from './commands.js'; +import { GithubCredentialProviderManager } from './credentialProvider.js'; +import { DisposableStore, repositoryHasGitHubRemote } from './util.js'; +import { GithubPushErrorHandler } from './pushErrorHandler.js'; +import { GitBaseExtension } from './typings/git-base.js'; +import { GithubRemoteSourcePublisher } from './remoteSourcePublisher.js'; +import { GitHubBranchProtectionProviderManager } from './branchProtection.js'; +import { GitHubCanonicalUriProvider } from './canonicalUriProvider.js'; +import { VscodeDevShareProvider } from './shareProviders.js'; +import { GitHubSourceControlHistoryItemDetailsProvider } from './historyItemDetailsProvider.js'; export function activate(context: ExtensionContext): void { const disposables: Disposable[] = []; @@ -31,8 +31,8 @@ export function activate(context: ExtensionContext): void { disposables.push(logger.onDidChangeLogLevel(onDidChangeLogLevel)); onDidChangeLogLevel(logger.logLevel); - const { aiKey } = require('../package.json') as { aiKey: string }; - const telemetryReporter = new TelemetryReporter(aiKey); + const { aiKey } = context.extension.packageJSON as { aiKey: string }; + const telemetryReporter = new TelemetryReporter.default(aiKey); disposables.push(telemetryReporter); disposables.push(initializeGitBaseExtension()); @@ -84,7 +84,7 @@ function setGitHubContext(gitAPI: API, disposables: DisposableStore) { } } -function initializeGitExtension(context: ExtensionContext, telemetryReporter: TelemetryReporter, logger: LogOutputChannel): Disposable { +function initializeGitExtension(context: ExtensionContext, telemetryReporter: TelemetryReporter.default, logger: LogOutputChannel): Disposable { const disposables = new DisposableStore(); let gitExtension = extensions.getExtension('vscode.git'); diff --git a/extensions/github/src/historyItemDetailsProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts index b31126e2ada..add55e6f403 100644 --- a/extensions/github/src/historyItemDetailsProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -5,10 +5,10 @@ import { authentication, Command, l10n, LogOutputChannel, workspace } from 'vscode'; import { Commit, Repository as GitHubRepository, Maybe } from '@octokit/graphql-schema'; -import { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git'; -import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, groupBy, sequentialize } from './util'; -import { AuthenticationError, getOctokitGraphql } from './auth'; -import { getAvatarLink } from './links'; +import { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git.js'; +import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, groupBy, sequentialize } from './util.js'; +import { AuthenticationError, getOctokitGraphql } from './auth.js'; +import { getAvatarLink } from './links.js'; const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/g; diff --git a/extensions/github/src/links.ts b/extensions/github/src/links.ts index fdcac0c5cfd..8eb0f6b23f6 100644 --- a/extensions/github/src/links.ts +++ b/extensions/github/src/links.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API as GitAPI, RefType, Repository } from './typings/git'; -import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util'; +import { API as GitAPI, RefType, Repository } from './typings/git.js'; +import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util.js'; export function isFileInRepo(repository: Repository, file: vscode.Uri): boolean { return file.path.toLowerCase() === repository.rootUri.path.toLowerCase() || diff --git a/extensions/github/src/publish.ts b/extensions/github/src/publish.ts index dee8898d348..587b2652d61 100644 --- a/extensions/github/src/publish.ts +++ b/extensions/github/src/publish.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API as GitAPI, Repository } from './typings/git'; -import { getOctokit } from './auth'; +import { API as GitAPI, Repository } from './typings/git.js'; +import { getOctokit } from './auth.js'; import { TextEncoder } from 'util'; import { basename } from 'path'; import { Octokit } from '@octokit/rest'; -import { isInCodespaces } from './pushErrorHandler'; +import { isInCodespaces } from './pushErrorHandler.js'; function sanitizeRepositoryName(value: string): string { return value.trim().replace(/[^a-z0-9_.]/ig, '-'); diff --git a/extensions/github/src/pushErrorHandler.ts b/extensions/github/src/pushErrorHandler.ts index f1702bf15dd..d76addbe65f 100644 --- a/extensions/github/src/pushErrorHandler.ts +++ b/extensions/github/src/pushErrorHandler.ts @@ -5,10 +5,12 @@ import { TextDecoder } from 'util'; import { commands, env, ProgressLocation, Uri, window, workspace, QuickPickOptions, FileType, l10n, Disposable, TextDocumentContentProvider } from 'vscode'; -import TelemetryReporter from '@vscode/extension-telemetry'; -import { getOctokit } from './auth'; -import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git'; +import { getOctokit } from './auth.js'; +import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git.js'; import * as path from 'path'; +import TelemetryReporter from '@vscode/extension-telemetry'; + + type Awaited = T extends PromiseLike ? Awaited : T; @@ -100,7 +102,7 @@ export class GithubPushErrorHandler implements PushErrorHandler { private disposables: Disposable[] = []; private commandErrors = new CommandErrorOutputTextDocumentContentProvider(); - constructor(private readonly telemetryReporter: TelemetryReporter) { + constructor(private readonly telemetryReporter: TelemetryReporter.default) { this.disposables.push(workspace.registerTextDocumentContentProvider('github-output', this.commandErrors)); } diff --git a/extensions/github/src/remoteSourceProvider.ts b/extensions/github/src/remoteSourceProvider.ts index 0d8b9340695..291a3f1a6ba 100644 --- a/extensions/github/src/remoteSourceProvider.ts +++ b/extensions/github/src/remoteSourceProvider.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Uri, env, l10n, workspace } from 'vscode'; -import { RemoteSourceProvider, RemoteSource, RemoteSourceAction } from './typings/git-base'; -import { getOctokit } from './auth'; +import { RemoteSourceProvider, RemoteSource, RemoteSourceAction } from './typings/git-base.js'; +import { getOctokit } from './auth.js'; import { Octokit } from '@octokit/rest'; -import { getRepositoryFromQuery, getRepositoryFromUrl } from './util'; -import { getBranchLink, getVscodeDevHost } from './links'; +import { getRepositoryFromQuery, getRepositoryFromUrl } from './util.js'; +import { getBranchLink, getVscodeDevHost } from './links.js'; function asRemoteSource(raw: any): RemoteSource { const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol'); diff --git a/extensions/github/src/remoteSourcePublisher.ts b/extensions/github/src/remoteSourcePublisher.ts index 2e6a5d88ead..97ce05a835c 100644 --- a/extensions/github/src/remoteSourcePublisher.ts +++ b/extensions/github/src/remoteSourcePublisher.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { publishRepository } from './publish'; -import { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git'; +import { publishRepository } from './publish.js'; +import { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git.js'; export class GithubRemoteSourcePublisher implements RemoteSourcePublisher { readonly name = 'GitHub'; diff --git a/extensions/github/src/shareProviders.ts b/extensions/github/src/shareProviders.ts index 7aea9c27b24..d2e94a47147 100644 --- a/extensions/github/src/shareProviders.ts +++ b/extensions/github/src/shareProviders.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API } from './typings/git'; -import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util'; -import { encodeURIComponentExceptSlashes, ensurePublished, getRepositoryForFile, notebookCellRangeString, rangeString } from './links'; +import { API } from './typings/git.js'; +import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util.js'; +import { encodeURIComponentExceptSlashes, ensurePublished, getRepositoryForFile, notebookCellRangeString, rangeString } from './links.js'; export class VscodeDevShareProvider implements vscode.ShareProvider, vscode.Disposable { readonly id: string = 'copyVscodeDevLink'; diff --git a/extensions/github/src/test/github.test.ts b/extensions/github/src/test/github.test.ts index 2fc5fbd23a5..db0eba515cb 100644 --- a/extensions/github/src/test/github.test.ts +++ b/extensions/github/src/test/github.test.ts @@ -6,7 +6,7 @@ import 'mocha'; import * as assert from 'assert'; import { workspace, extensions, Uri, commands } from 'vscode'; -import { findPullRequestTemplates, pickPullRequestTemplate } from '../pushErrorHandler'; +import { findPullRequestTemplates, pickPullRequestTemplate } from '../pushErrorHandler.js'; suite('github smoke test', function () { const cwd = workspace.workspaceFolders![0].uri; diff --git a/extensions/github/src/test/index.ts b/extensions/github/src/test/index.ts index 52c5acf885f..6573ab1daa4 100644 --- a/extensions/github/src/test/index.ts +++ b/extensions/github/src/test/index.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import * as testRunner from '../../../../test/integration/electron/testrunner'; +import * as testRunner from '../../../../test/integration/electron/testrunner.js'; const suite = 'Github Tests'; @@ -27,4 +27,4 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { testRunner.configure(options); -export = testRunner; +export default testRunner; diff --git a/extensions/github/src/util.ts b/extensions/github/src/util.ts index 4c8a032405d..1841ba0d032 100644 --- a/extensions/github/src/util.ts +++ b/extensions/github/src/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { Repository } from './typings/git'; +import { Repository } from './typings/git.js'; export class DisposableStore { diff --git a/extensions/github/tsconfig.json b/extensions/github/tsconfig.json index 8435c0d09e8..9d04625950c 100644 --- a/extensions/github/tsconfig.json +++ b/extensions/github/tsconfig.json @@ -1,8 +1,12 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "./out", + "skipLibCheck": true, "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, "typeRoots": [ "./node_modules/@types" ] From dfc4d128fa5f1103b78a431123fe154152389519 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 16 Apr 2025 17:04:27 +0200 Subject: [PATCH 002/114] add missing JS file --- build/lib/extensions.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 79a9507f66c..1d2f95299c8 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -61,7 +61,6 @@ const crypto_1 = __importDefault(require("crypto")); const vinyl_1 = __importDefault(require("vinyl")); const stats_1 = require("./stats"); const util2 = __importStar(require("./util")); -const vzip = require('gulp-vinyl-zip'); const gulp_filter_1 = __importDefault(require("gulp-filter")); const gulp_rename_1 = __importDefault(require("gulp-rename")); const fancy_log_1 = __importDefault(require("fancy-log")); @@ -72,6 +71,7 @@ const dependencies_1 = require("./dependencies"); const builtInExtensions_1 = require("./builtInExtensions"); const getVersion_1 = require("./getVersion"); const fetch_1 = require("./fetch"); +const vzip = require('gulp-vinyl-zip'); const root = path_1.default.dirname(path_1.default.dirname(__dirname)); const commit = (0, getVersion_1.getVersion)(root); const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; @@ -104,7 +104,10 @@ function updateExtensionPackageJSON(input, update) { .pipe(packageJsonFilter.restore); } function fromLocal(extensionPath, forWeb, disableMangle) { - const webpackConfigFileName = forWeb ? 'extension-browser.webpack.config.js' : 'extension.webpack.config.js'; + const esm = JSON.parse(fs_1.default.readFileSync(path_1.default.join(extensionPath, 'package.json'), 'utf8')).type === 'module'; + const webpackConfigFileName = forWeb + ? `extension-browser.webpack.config.${!esm ? 'js' : 'cjs'}` + : `extension.webpack.config.${!esm ? 'js' : 'cjs'}`; const isWebPacked = fs_1.default.existsSync(path_1.default.join(extensionPath, webpackConfigFileName)); let input = isWebPacked ? fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) From 38768712a381ebf9bb3857e9f5bc3ab171acf1dd Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 16 Apr 2025 14:57:53 -0700 Subject: [PATCH 003/114] chat: allow tools to report progress This adds a `toolProgress` proposed API that allows extensions to report progress keyed on the `toolInvocationToken`. Internally, when given to a tool, this now includes the call ID of the tool. We can use that both from extensions and internally to report progress for tools, and I hooked this up for MCP servers. Currently only the text is updated. Involved some changes in the progress service internally: - Previously `viewId` was a naked string, I wrapped it in an object to make it more identifiable. - The progress service is in `workbench/services` and directly calls into other services to effect progress. In leui of going for a full 'contribution' model, I made a small `ILanguageModelToolProgressService` that it writes state into and that can be read back out. The state (an observable) is kept as long as progress is ongoing or this is is an observer. --- .../common/extensionsApiProposals.ts | 3 + src/vs/platform/progress/common/progress.ts | 8 +- .../workbench/api/common/extHost.api.impl.ts | 2 +- .../api/common/extHostLanguageModelTools.ts | 2 +- .../workbench/api/common/extHostProgress.ts | 9 +- .../api/common/extHostTypeConverters.ts | 12 ++- .../workbench/browser/parts/views/treeView.ts | 4 +- .../workbench/browser/parts/views/viewPane.ts | 4 +- .../chatToolInvocationPart.ts | 33 ++++++-- .../chat/common/languageModelToolsService.ts | 1 + .../browser/languageModelToolsService.test.ts | 3 +- .../contrib/debug/browser/debugProgress.ts | 2 +- .../contrib/debug/browser/debugViewlet.ts | 2 +- .../browser/extensionsWorkbenchService.ts | 2 +- .../contrib/files/browser/fileImportExport.ts | 6 +- .../workbench/contrib/mcp/common/mcpServer.ts | 28 ++++++- .../mcp/common/mcpServerRequestHandler.ts | 2 +- .../contrib/mcp/common/mcpService.ts | 9 +- .../workbench/contrib/mcp/common/mcpTypes.ts | 6 ++ .../contrib/scm/browser/scmHistoryViewPane.ts | 6 +- .../testing/browser/testExplorerActions.ts | 2 +- .../testResultsView/testResultsTree.ts | 2 +- .../contrib/timeline/browser/timelinePane.ts | 2 +- .../webviewView/browser/webviewViewPane.ts | 2 +- .../languageModelToolProgressService.ts | 83 +++++++++++++++++++ .../progress/browser/progressService.ts | 26 ++++-- .../userDataProfileImportExportService.ts | 2 +- .../vscode.proposed.toolProgress.d.ts | 34 ++++++++ 28 files changed, 248 insertions(+), 49 deletions(-) create mode 100644 src/vs/workbench/services/progress/browser/languageModelToolProgressService.ts create mode 100644 src/vscode-dts/vscode.proposed.toolProgress.d.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index e21a27208f3..536e3dd8a45 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -394,6 +394,9 @@ const _allApiProposals = { tokenInformation: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', }, + toolProgress: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.toolProgress.d.ts', + }, treeViewActiveItem: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts', }, diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 312f01a17d9..3a0a6ca7f31 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -50,8 +50,12 @@ export const enum ProgressLocation { Dialog = 20 } +export type ProgressLocationDetailed = { viewId: string } | { toolInvocationCallId: string | undefined }; + +export type ProgressLocationDescriptor = ProgressLocation | ProgressLocationDetailed; + export interface IProgressOptions { - readonly location: ProgressLocation | string; + readonly location: ProgressLocationDescriptor; readonly title?: string; readonly source?: string | INotificationSource; readonly total?: number; @@ -81,7 +85,7 @@ export interface IProgressWindowOptions extends IProgressOptions { } export interface IProgressCompositeOptions extends IProgressOptions { - readonly location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | string; + readonly location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | { viewId: string }; readonly delay?: number; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 0db23694cd0..494f426ba16 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -815,7 +815,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostProgress.withProgress(extension, { location: extHostTypes.ProgressLocation.SourceControl }, (progress, token) => task({ report(n: number) { /*noop*/ } })); }, - withProgress(options: vscode.ProgressOptions, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable) { + withProgress(options: vscode.ProgressOptions | vscode.ProgressOptions2, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable) { return extHostProgress.withProgress(extension, options, task); }, createOutputChannel(name: string, options: string | { log: true } | undefined): any { diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index fcbd6c80691..594c870540e 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -73,7 +73,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape callId, parameters: options.input, tokenBudget: options.tokenizationOptions?.tokenBudget, - context: options.toolInvocationToken as IToolInvocationContext | undefined, + context: options.toolInvocationToken ? { ...(options.toolInvocationToken as IToolInvocationContext), callId } : undefined, chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined, chatInteractionId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatInteractionId : undefined, }, token); diff --git a/src/vs/workbench/api/common/extHostProgress.ts b/src/vs/workbench/api/common/extHostProgress.ts index 0914c2e7916..15f94246b25 100644 --- a/src/vs/workbench/api/common/extHostProgress.ts +++ b/src/vs/workbench/api/common/extHostProgress.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ProgressOptions } from 'vscode'; +import { ProgressOptions, ProgressOptions2 } from 'vscode'; import { MainThreadProgressShape, ExtHostProgressShape } from './extHost.protocol.js'; import { ProgressLocation } from './extHostTypeConverters.js'; import { Progress, IProgressStep } from '../../../platform/progress/common/progress.js'; @@ -11,6 +11,7 @@ import { CancellationTokenSource, CancellationToken } from '../../../base/common import { throttle } from '../../../base/common/decorators.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { onUnexpectedExternalError } from '../../../base/common/errors.js'; +import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; export class ExtHostProgress implements ExtHostProgressShape { @@ -22,11 +23,15 @@ export class ExtHostProgress implements ExtHostProgressShape { this._proxy = proxy; } - async withProgress(extension: IExtensionDescription, options: ProgressOptions, task: (progress: Progress, token: CancellationToken) => Thenable): Promise { + async withProgress(extension: IExtensionDescription, options: ProgressOptions | ProgressOptions2, task: (progress: Progress, token: CancellationToken) => Thenable): Promise { const handle = this._handles++; const { title, location, cancellable } = options; const source = { label: extension.displayName || extension.name, id: extension.identifier.value }; + if (typeof location === 'object' && location.hasOwnProperty('toolInvocationToken')) { + checkProposedApiEnabled(extension, 'toolProgress'); + } + this._proxy.$startProgress(handle, { location: ProgressLocation.from(location), title, source, cancellable }, !extension.isUnderDevelopment ? extension.identifier.value : undefined).catch(onUnexpectedExternalError); return this._withProgress(handle, task, !!cancellable); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 75c7f54d600..cb208b29f5e 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -36,14 +36,14 @@ import { EndOfLineSequence, TrackedRangeStickiness } from '../../../editor/commo import { ITextEditorOptions } from '../../../platform/editor/common/editor.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { IMarkerData, IRelatedInformation, MarkerSeverity, MarkerTag } from '../../../platform/markers/common/markers.js'; -import { ProgressLocation as MainProgressLocation } from '../../../platform/progress/common/progress.js'; +import { ProgressLocation as MainProgressLocation, ProgressLocationDescriptor as MainProgressLocationDescriptor } from '../../../platform/progress/common/progress.js'; import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; import { IViewBadge } from '../../common/views.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; import { IChatRequestVariableEntry, isImageVariableEntry } from '../../contrib/chat/common/chatModel.js'; import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; -import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { IToolData, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js'; @@ -1458,9 +1458,13 @@ export namespace EndOfLine { } export namespace ProgressLocation { - export function from(loc: vscode.ProgressLocation | { viewId: string }): MainProgressLocation | string { + export function from(loc: vscode.ProgressLocation | { viewId: string } | { toolInvocationToken: object | undefined }): MainProgressLocationDescriptor { if (typeof loc === 'object') { - return loc.viewId; + if ('toolInvocationToken' in loc) { + return { toolInvocationCallId: (loc.toolInvocationToken as IToolInvocationContext).callId }; + } else { + return { viewId: loc.viewId }; + } } switch (loc) { diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index bc660c6f7a1..e5562575d4c 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -692,7 +692,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService); const treeMenus = this.treeDisposables.add(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this.treeDisposables.add(this.instantiationService.createInstance(ResourceLabels, this)); - const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); + const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: { viewId: this.id } }, () => task)); const aligner = this.treeDisposables.add(new Aligner(this.themeService)); const checkboxStateHandler = this.treeDisposables.add(new CheckboxStateHandler()); const renderer = this.treeDisposables.add(this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler, () => this.manuallyManageCheckboxes)); @@ -1791,7 +1791,7 @@ export class CustomTreeView extends AbstractTreeView { id: this.id, }); this.createTree(); - this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) + this.progressService.withProgress({ location: { viewId: this.id } }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) .then(() => timeout(2000)) .then(() => { this.updateMessage(); diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index e8dedb875d8..58200d5fc40 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -651,8 +651,8 @@ export abstract class ViewPane extends Pane implements IView { return this.progressIndicator; } - protected getProgressLocation(): string { - return this.viewDescriptorService.getViewContainerByViewId(this.id)!.id; + protected getProgressLocation(): { viewId: string } { + return { viewId: this.viewDescriptorService.getViewContainerByViewId(this.id)!.id }; } protected getLocationBasedColors(): IViewPaneLocationColors { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts index 86a5d9e5991..a1489fc90b1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts @@ -9,6 +9,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable, thenIfNotDisposed, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorunWithStore } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; @@ -22,6 +23,8 @@ import { IContextKeyService } from '../../../../../platform/contextkey/common/co import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; +import { IProgressService } from '../../../../../platform/progress/common/progress.js'; +import { ILanguageModelToolProgressService } from '../../../../services/progress/browser/languageModelToolProgressService.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatMarkdownContent, IChatProgressMessage, IChatTerminalToolInvocationData, IChatToolInvocation, IChatToolInvocationSerialized } from '../../common/chatService.js'; import { IChatRendererContent } from '../../common/chatViewModel.js'; @@ -64,6 +67,7 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa codeBlockModelCollection: CodeBlockModelCollection, codeBlockStartIndex: number, @IInstantiationService instantiationService: IInstantiationService, + @IProgressService progressService: IProgressService, ) { super(); @@ -140,6 +144,7 @@ class ChatToolInvocationSubPart extends Disposable { @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @ICommandService private readonly commandService: ICommandService, @IMarkerService private readonly markerService: IMarkerService, + @ILanguageModelToolProgressService private readonly languageModelToolProgressService: ILanguageModelToolProgressService, ) { super(); @@ -454,27 +459,37 @@ class ChatToolInvocationSubPart extends Disposable { } private createProgressPart(): HTMLElement { - let content: IMarkdownString; if (this.toolInvocation.isComplete && this.toolInvocation.isConfirmed !== false && this.toolInvocation.pastTenseMessage) { - content = typeof this.toolInvocation.pastTenseMessage === 'string' ? - new MarkdownString().appendText(this.toolInvocation.pastTenseMessage) : - this.toolInvocation.pastTenseMessage; + const part = this.renderProgressContent(this.toolInvocation.pastTenseMessage); + this._register(part); + return part.domNode; } else { - content = typeof this.toolInvocation.invocationMessage === 'string' ? - new MarkdownString().appendText(this.toolInvocation.invocationMessage + '…') : - MarkdownString.lift(this.toolInvocation.invocationMessage).appendText('…'); + const container = document.createElement('div'); + const progressObservable = this._register(this.languageModelToolProgressService.listenForProgress(this.toolInvocation.toolCallId)); + this._register(autorunWithStore((reader, store) => { + const progress = progressObservable.object.read(reader); + const part = store.add(this.renderProgressContent(progress?.message || this.toolInvocation.invocationMessage)); + dom.reset(container, part.domNode); + })); + return container; + } + } + + private renderProgressContent(content: IMarkdownString | string) { + if (typeof content === 'string') { + content = new MarkdownString().appendText(content); } const progressMessage: IChatProgressMessage = { kind: 'progressMessage', content }; + const iconOverride = !this.toolInvocation.isConfirmed ? Codicon.error : this.toolInvocation.isComplete ? Codicon.check : undefined; - const progressPart = this._register(this.instantiationService.createInstance(ChatProgressContentPart, progressMessage, this.renderer, this.context, undefined, true, iconOverride)); - return progressPart.domNode; + return this.instantiationService.createInstance(ChatProgressContentPart, progressMessage, this.renderer, this.context, undefined, true, iconOverride); } private createTerminalMarkdownProgressPart(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, terminalData: IChatTerminalToolInvocationData): HTMLElement { diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index c123f290cb8..7b37b56bf2f 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -83,6 +83,7 @@ export interface IToolInvocation { export interface IToolInvocationContext { sessionId: string; + callId: string | undefined; } export function isToolInvocationContext(obj: any): obj is IToolInvocationContext { diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 855c89aefed..eb47ea1ac00 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -174,7 +174,8 @@ suite('LanguageModelToolsService', () => { a: 1 }, context: { - sessionId + sessionId, + callId: '1', }, }; chatService.addSession({ diff --git a/src/vs/workbench/contrib/debug/browser/debugProgress.ts b/src/vs/workbench/contrib/debug/browser/debugProgress.ts index 0cca619bf96..5ad4c9c995a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugProgress.ts +++ b/src/vs/workbench/contrib/debug/browser/debugProgress.ts @@ -37,7 +37,7 @@ export class DebugProgressContribution implements IWorkbenchContribution { }); if (viewsService.isViewContainerVisible(VIEWLET_ID)) { - progressService.withProgress({ location: VIEWLET_ID }, () => promise); + progressService.withProgress({ location: { viewId: VIEWLET_ID } }, () => promise); } const source = debugService.getAdapterManager().getDebuggerLabel(session.configuration.type); progressService.withProgress({ diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index 10b8d244b2e..af6ef42997c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -131,7 +131,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } if (state === State.Initializing) { - this.progressService.withProgress({ location: VIEWLET_ID, }, _progress => { + this.progressService.withProgress({ location: { viewId: VIEWLET_ID }, }, _progress => { return new Promise(resolve => this.progressResolve = resolve); }); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index ce3cab343b4..8533863a48c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -2737,7 +2737,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private doInstall(extension: IExtension | undefined, installTask: () => Promise, progressLocation?: ProgressLocation | string): Promise { const title = extension ? nls.localize('installing named extension', "Installing '{0}' extension....", extension.displayName) : nls.localize('installing extension', 'Installing extension....'); return this.withProgress({ - location: progressLocation ?? ProgressLocation.Extensions, + location: typeof progressLocation === 'string' ? { viewId: progressLocation } : (progressLocation ?? ProgressLocation.Extensions), title }, async () => { try { diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 44ecb6d65e5..8337c2a9750 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -98,7 +98,7 @@ export class BrowserFileUpload { ); // Also indicate progress in the files view - this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => uploadPromise); + this.progressService.withProgress({ location: { viewId: VIEW_ID }, delay: 500 }, () => uploadPromise); return uploadPromise; } @@ -417,7 +417,7 @@ export class ExternalFileImport { ); // Also indicate progress in the files view - this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => importPromise); + this.progressService.withProgress({ location: { viewId: VIEW_ID }, delay: 500 }, () => importPromise); return importPromise; } @@ -619,7 +619,7 @@ export class FileDownload { ); // Also indicate progress in the files view - this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => downloadPromise); + this.progressService.withProgress({ location: { viewId: VIEW_ID }, delay: 500 }, () => downloadPromise); return downloadPromise; } diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 6545737fb4a..e6a54cbac63 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -27,6 +27,8 @@ import { INotificationService, Severity } from '../../../../platform/notificatio import { localize } from '../../../../nls.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; type ServerBootData = { supportsLogging: boolean; @@ -481,7 +483,31 @@ export class McpTool implements IMcpTool { call(params: Record, token?: CancellationToken): Promise { // serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it. const name = this._definition.serverToolName ?? this._definition.name; - return this._server.callOn(h => h.callTool({ name, arguments: params }), token); + return this._server.callOn(h => h.callTool({ name, arguments: params }, token), token); + } + + callWithProgress(params: Record, progress: IProgress, token?: CancellationToken): Promise { + // serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it. + const name = this._definition.serverToolName ?? this._definition.name; + const progressToken = generateUuid(); + + return this._server.callOn(h => { + let lastProgressN = 0; + const listener = h.onDidReceiveProgressNotification((e) => { + if (e.params.progressToken === progressToken) { + progress.report({ + message: e.params.message, + increment: e.params.progress - lastProgressN, + total: e.params.total, + }); + lastProgressN = e.params.progress; + } + }); + + return h.callTool({ name, arguments: params, _meta: { progressToken } }, token).finally(() => { + listener.dispose(); + }); + }, token); } compare(other: IMcpTool): number { diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts index 8c0fa08cae0..011f50f026d 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts @@ -466,7 +466,7 @@ export class McpServerRequestHandler extends Disposable { /** * Call a specific tool */ - callTool(params: MCP.CallToolRequest['params'], token?: CancellationToken): Promise { + callTool(params: MCP.CallToolRequest['params'] & MCP.Request['params'], token?: CancellationToken): Promise { return this.sendRequest({ method: 'tools/call', params }, token); } diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts index 98a9bddf4e2..fe8660b7625 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -14,6 +14,7 @@ import { localize } from '../../../../nls.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IProgressService } from '../../../../platform/progress/common/progress.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; @@ -214,6 +215,7 @@ class McpToolImplementation implements IToolImpl { private readonly _tool: IMcpTool, private readonly _server: IMcpServer, @IProductService private readonly _productService: IProductService, + @IProgressService private readonly _progressService: IProgressService, ) { } async prepareToolInvocation(parameters: any): Promise { @@ -254,7 +256,12 @@ class McpToolImplementation implements IToolImpl { const outputParts: string[] = []; - const callResult = await this._tool.call(invocation.parameters as Record, token); + + const params = invocation.parameters as Record; + const callResult = invocation.context + ? await this._progressService.withProgress({ location: { toolInvocationCallId: invocation.callId } }, p => this._tool.callWithProgress(params, p, token)) + : await this._tool.call(params, token); + for (const item of callResult.content) { if (item.type === 'text') { result.content.push({ diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 7d06c30764e..9450894dffb 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -19,6 +19,7 @@ import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; import { MCP } from './modelContextProtocol.js'; +import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; export const extensionMcpCollectionPrefix = 'ext.'; @@ -248,6 +249,11 @@ export interface IMcpTool { * @throws {@link McpConnectionFailedError} if the connection to the server fails */ call(params: Record, token?: CancellationToken): Promise; + + /** + * Identical to {@link call}, but reports progress. + */ + callWithProgress(params: Record, progress: IProgress, token?: CancellationToken): Promise; } export const enum McpServerTransportType { diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index fdde018b017..9d984d1abfc 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -651,7 +651,7 @@ class SCMHistoryViewPaneActionRunner extends ActionRunner { } protected override runAction(action: IAction, context?: unknown): Promise { - return this._progressService.withProgress({ location: HISTORY_VIEW_PANE_ID }, + return this._progressService.withProgress({ location: { viewId: HISTORY_VIEW_PANE_ID } }, async () => await super.runAction(action, context)); } } @@ -1318,7 +1318,7 @@ export class SCMHistoryViewPane extends ViewPane { await waitForState(firstRepositoryInitialized); // Initial rendering - await this._progressService.withProgress({ location: this.id }, async () => { + await this._progressService.withProgress({ location: { viewId: this.id } }, async () => { await this._treeOperationSequencer.queue(async () => { await this._tree.setInput(this._treeViewModel); this._tree.scrollTop = 0; @@ -1689,7 +1689,7 @@ export class SCMHistoryViewPane extends ViewPane { return this._updateChildrenThrottler.queue( () => this._treeOperationSequencer.queue( async () => { - await this._progressService.withProgress({ location: this.id }, + await this._progressService.withProgress({ location: { viewId: this.id } }, async () => { await this._tree.updateChildren(undefined, undefined, undefined, { // diffIdentityProvider: this._treeIdentityProvider diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 160800155fc..f23dae9b9a5 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -1684,7 +1684,7 @@ export class RefreshTestsAction extends Action2 { const progressService = accessor.get(IProgressService); const controllerIds = distinct(elements.filter(isDefined).map(e => e.test.controllerId)); - return progressService.withProgress({ location: Testing.ViewletId }, async () => { + return progressService.withProgress({ location: { viewId: Testing.ViewletId } }, async () => { if (controllerIds.length) { await Promise.all(controllerIds.map(id => testService.refreshTests(id))); } else { diff --git a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts index 1895434ddba..7bfe65c7eff 100644 --- a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts +++ b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts @@ -585,7 +585,7 @@ export class OutputPeekTree extends Disposable { return coverageService.closeCoverage(); } progressService.withProgress( - { location: options.locationForProgress }, + { location: { viewId: options.locationForProgress } }, () => coverageService.openCoverage(task, true) ); } diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index d6582ce54f8..f48f461e011 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -645,7 +645,7 @@ export class TimelinePane extends ViewPane { private async handleRequest(request: TimelineRequest) { let response: Timeline | undefined; try { - response = await this.progressService.withProgress({ location: this.id }, () => request.result); + response = await this.progressService.withProgress({ location: { viewId: this.id } }, () => request.result); } finally { this.pendingRequests.get(request.source)?.dispose(); diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index 6dae8155249..0de87305fc0 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -267,7 +267,7 @@ export class WebviewViewPane extends ViewPane { } private async withProgress(task: () => Promise): Promise { - return this.progressService.withProgress({ location: this.id, delay: 500 }, task); + return this.progressService.withProgress({ location: { viewId: this.id }, delay: 500 }, task); } override onDidScrollRoot() { diff --git a/src/vs/workbench/services/progress/browser/languageModelToolProgressService.ts b/src/vs/workbench/services/progress/browser/languageModelToolProgressService.ts new file mode 100644 index 00000000000..1e744fd1dc0 --- /dev/null +++ b/src/vs/workbench/services/progress/browser/languageModelToolProgressService.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { IReference } from '../../../../base/common/lifecycle.js'; +import { IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; + +export const ILanguageModelToolProgressService = createDecorator('languageModelToolProgressService'); + +export interface ILanguageModelToolProgressService { + _serviceBrand: undefined; + + listenForProgress(callId: string): IReference>; + + handleProgress

, R = unknown>(callId: string, callback: (progress: IProgress) => P): P; +} + +export interface ILanguageModelToolProgressDelegate { + onDidCancel: Event; + update(step: IProgressStep): void; + dispose?(): void; +} + +export class LanguageModelToolProgressService implements ILanguageModelToolProgressService { + declare readonly _serviceBrand: undefined; + + private readonly progress = new Map; + rc: number; + }>(); + + listenForProgress(callId: string): IReference> { + let rec = this.progress.get(callId); + if (!rec) { + rec = { value: observableValue(this, { message: undefined, total: 0, done: 0 }), rc: 0 }; + this.progress.set(callId, rec); + } + rec.rc++; + + return { + object: rec.value, + dispose: () => { + if (!--rec.rc) { + this.progress.delete(callId); + } + } + }; + } + + handleProgress

, R = unknown>(callId: string, callback: (progress: IProgress) => P): P { + let rec = this.progress.get(callId); + if (!rec) { + rec = { value: observableValue(this, { message: undefined, total: 0, done: 0 }), rc: 0 }; + this.progress.set(callId, rec); + } + + rec.rc++; + + let last = 0; + const promise = callback({ + report: update => { + if (update.increment) { + last += update.increment; + } + rec.value.set({ message: update.message, total: update.total || 100, done: last }, undefined); + }, + }); + promise.finally(() => { + if (!--rec.rc) { + this.progress.delete(callId); + } + }); + + return promise; + } +} + +registerSingleton(ILanguageModelToolProgressService, LanguageModelToolProgressService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index cb8831c7e77..96d04e24474 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -24,6 +24,7 @@ import { IPaneCompositePartService } from '../../panecomposite/browser/panecompo import { stripIcons } from '../../../../base/common/iconLabels.js'; import { IUserActivityService } from '../../userActivity/common/userActivityService.js'; import { createWorkbenchDialogOptions } from '../../../../platform/dialogs/browser/dialog.js'; +import { ILanguageModelToolProgressService } from './languageModelToolProgressService.js'; export class ProgressService extends Disposable implements IProgressService { @@ -39,6 +40,7 @@ export class ProgressService extends Disposable implements IProgressService { @ILayoutService private readonly layoutService: ILayoutService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IUserActivityService private readonly userActivityService: IUserActivityService, + @ILanguageModelToolProgressService private readonly languageModelToolProgressService: ILanguageModelToolProgressService, ) { super(); } @@ -55,24 +57,32 @@ export class ProgressService extends Disposable implements IProgressService { } }; - const handleStringLocation = (location: string) => { - const viewContainer = this.viewDescriptorService.getViewContainerById(location); + const handleViewLocation = (location: { viewId: string }) => { + const viewContainer = this.viewDescriptorService.getViewContainerById(location.viewId); if (viewContainer) { const viewContainerLocation = this.viewDescriptorService.getViewContainerLocation(viewContainer); if (viewContainerLocation !== null) { - return this.withPaneCompositeProgress(location, viewContainerLocation, task, { ...options, location }); + return this.withPaneCompositeProgress(location.viewId, viewContainerLocation, task, { ...options, location }); } } - if (this.viewDescriptorService.getViewDescriptorById(location) !== null) { - return this.withViewProgress(location, task, { ...options, location }); + if (this.viewDescriptorService.getViewDescriptorById(location.viewId) !== null) { + return this.withViewProgress(location.viewId, task, { ...options, location }); } throw new Error(`Bad progress location: ${location}`); }; - if (typeof location === 'string') { - return handleStringLocation(location); + if (typeof location === 'object') { + if ('viewId' in location) { + return handleViewLocation(location); + } else if ('toolInvocationCallId' in location) { + if (!location.toolInvocationCallId) { + return task({ report: () => { } }); + } + + return this.languageModelToolProgressService.handleProgress(location.toolInvocationCallId, task); + } } switch (location) { @@ -102,7 +112,7 @@ export class ProgressService extends Disposable implements IProgressService { case ProgressLocation.Explorer: return this.withPaneCompositeProgress('workbench.view.explorer', ViewContainerLocation.Sidebar, task, { ...options, location }); case ProgressLocation.Scm: - return handleStringLocation('workbench.scm'); + return handleViewLocation({ viewId: 'workbench.scm' }); case ProgressLocation.Extensions: return this.withPaneCompositeProgress('workbench.view.extensions', ViewContainerLocation.Sidebar, task, { ...options, location }); case ProgressLocation.Dialog: diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts index df2b9fa8763..b309fe618e8 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -262,7 +262,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU try { await this.progressService.withProgress({ - location, + location: typeof location === 'string' ? { viewId: location } : location, title: localize('profiles.exporting', "{0}: Exporting...", PROFILES_CATEGORY.value), }, async progress => { const id = await this.pickProfileContentHandler(profile.name); diff --git a/src/vscode-dts/vscode.proposed.toolProgress.d.ts b/src/vscode-dts/vscode.proposed.toolProgress.d.ts new file mode 100644 index 00000000000..6969c1f928d --- /dev/null +++ b/src/vscode-dts/vscode.proposed.toolProgress.d.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface ProgressOptions2 extends Omit { + location: ProgressLocation | { + /** + * The identifier of a view for which progress should be shown. + */ + viewId: string; + } | { + /** + * An invocation token for progress shown while a {@link LanguageModelTool} is running. + */ + toolInvocationToken: ChatParticipantToolToken | undefined; + }; + } + + export namespace window { + export function withProgress(options: ProgressOptions2, task: (progress: Progress<{ + /** + * A progress message that represents a chunk of work + */ + message?: string; + /** + * An increment for discrete progress. Increments will be summed up until 100% is reached + */ + increment?: number; + }>, token: CancellationToken) => Thenable): Thenable; + } +} From edc8644253dc87ee322ace9dd9e94d3111d2afe4 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 17 Apr 2025 10:51:04 -0700 Subject: [PATCH 004/114] be less creative ;) --- src/vs/platform/progress/common/progress.ts | 8 +- .../browser/mainThreadLanguageModelTools.ts | 7 +- .../workbench/api/common/extHost.api.impl.ts | 2 +- .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostLanguageModelTools.ts | 15 +++- .../workbench/api/common/extHostProgress.ts | 9 +- .../api/common/extHostTypeConverters.ts | 12 +-- .../workbench/browser/parts/views/treeView.ts | 4 +- .../workbench/browser/parts/views/viewPane.ts | 4 +- .../chatToolInvocationPart.ts | 6 +- .../chat/browser/languageModelToolsService.ts | 37 +++++++-- .../chatProgressTypes/chatToolInvocation.ts | 12 +++ .../contrib/chat/common/chatService.ts | 2 + .../chat/common/languageModelToolsService.ts | 9 +- .../contrib/chat/common/tools/editFileTool.ts | 3 +- .../electron-sandbox/tools/fetchPageTool.ts | 3 +- .../browser/languageModelToolsService.test.ts | 5 +- .../common/mockLanguageModelToolsService.ts | 5 ++ .../contrib/debug/browser/debugProgress.ts | 2 +- .../contrib/debug/browser/debugViewlet.ts | 2 +- .../browser/extensionsWorkbenchService.ts | 2 +- .../extensions/common/searchExtensionsTool.ts | 3 +- .../contrib/files/browser/fileImportExport.ts | 6 +- .../contrib/mcp/common/mcpConfiguration.ts | 20 ++--- .../contrib/mcp/common/mcpService.ts | 12 +-- .../contrib/scm/browser/scmHistoryViewPane.ts | 6 +- .../testing/browser/testExplorerActions.ts | 2 +- .../testResultsView/testResultsTree.ts | 2 +- .../contrib/timeline/browser/timelinePane.ts | 2 +- .../webviewView/browser/webviewViewPane.ts | 2 +- .../languageModelToolProgressService.ts | 83 ------------------- .../progress/browser/progressService.ts | 26 ++---- .../userDataProfileImportExportService.ts | 2 +- .../vscode.proposed.toolProgress.d.ts | 37 ++++----- 34 files changed, 144 insertions(+), 209 deletions(-) delete mode 100644 src/vs/workbench/services/progress/browser/languageModelToolProgressService.ts diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 3a0a6ca7f31..312f01a17d9 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -50,12 +50,8 @@ export const enum ProgressLocation { Dialog = 20 } -export type ProgressLocationDetailed = { viewId: string } | { toolInvocationCallId: string | undefined }; - -export type ProgressLocationDescriptor = ProgressLocation | ProgressLocationDetailed; - export interface IProgressOptions { - readonly location: ProgressLocationDescriptor; + readonly location: ProgressLocation | string; readonly title?: string; readonly source?: string | INotificationSource; readonly total?: number; @@ -85,7 +81,7 @@ export interface IProgressWindowOptions extends IProgressOptions { } export interface IProgressCompositeOptions extends IProgressOptions { - readonly location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | { viewId: string }; + readonly location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | string; readonly delay?: number; } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 67502d9db2c..88ab6e0e598 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; +import { IProgressStep } from '../../../platform/progress/common/progress.js'; import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; @@ -45,6 +46,10 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre }; } + $acceptToolProgress(requestId: string | undefined, callId: string, progress: IProgressStep): void { + this._languageModelToolsService.acceptProgress(requestId, callId, progress); + } + $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise { const fn = this._countTokenCallbacks.get(callId); if (!fn) { @@ -58,7 +63,7 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre const disposable = this._languageModelToolsService.registerToolImplementation( id, { - invoke: async (dto, countTokens, token) => { + invoke: async (dto, countTokens, _progress, token) => { try { this._countTokenCallbacks.set(dto.callId, countTokens); const resultDto = await this._proxy.$invokeTool(dto, token); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 494f426ba16..0db23694cd0 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -815,7 +815,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostProgress.withProgress(extension, { location: extHostTypes.ProgressLocation.SourceControl }, (progress, token) => task({ report(n: number) { /*noop*/ } })); }, - withProgress(options: vscode.ProgressOptions | vscode.ProgressOptions2, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable) { + withProgress(options: vscode.ProgressOptions, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable) { return extHostProgress.withProgress(extension, options, task); }, createOutputChannel(name: string, options: string | { log: true } | undefined): any { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 23b2026b543..b74dc18c946 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1380,6 +1380,7 @@ export type IToolDataDto = Omit; export interface MainThreadLanguageModelToolsShape extends IDisposable { $getTools(): Promise[]>; + $acceptToolProgress(requestId: string | undefined, callId: string, progress: IProgressStep): void; $invokeTool(dto: IToolInvocation, token?: CancellationToken): Promise>; $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise; $registerTool(id: string): void; diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 594c870540e..82de8a39f02 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -73,7 +73,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape callId, parameters: options.input, tokenBudget: options.tokenizationOptions?.tokenBudget, - context: options.toolInvocationToken ? { ...(options.toolInvocationToken as IToolInvocationContext), callId } : undefined, + context: options.toolInvocationToken as IToolInvocationContext | undefined, chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined, chatInteractionId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatInteractionId : undefined, }, token); @@ -138,7 +138,18 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } - const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token)), token); + const progress: vscode.Progress<{ message?: string; increment?: number }> = { + report: value => { + checkProposedApiEnabled(item.extension, 'toolProgress'); + this._proxy.$acceptToolProgress(dto.chatRequestId, dto.callId, { + message: value.message, + increment: value.increment, + total: 100, + }); + } + }; + + const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token, progress)), token); if (!extensionResult) { throw new CancellationError(); } diff --git a/src/vs/workbench/api/common/extHostProgress.ts b/src/vs/workbench/api/common/extHostProgress.ts index 15f94246b25..0914c2e7916 100644 --- a/src/vs/workbench/api/common/extHostProgress.ts +++ b/src/vs/workbench/api/common/extHostProgress.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ProgressOptions, ProgressOptions2 } from 'vscode'; +import { ProgressOptions } from 'vscode'; import { MainThreadProgressShape, ExtHostProgressShape } from './extHost.protocol.js'; import { ProgressLocation } from './extHostTypeConverters.js'; import { Progress, IProgressStep } from '../../../platform/progress/common/progress.js'; @@ -11,7 +11,6 @@ import { CancellationTokenSource, CancellationToken } from '../../../base/common import { throttle } from '../../../base/common/decorators.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { onUnexpectedExternalError } from '../../../base/common/errors.js'; -import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; export class ExtHostProgress implements ExtHostProgressShape { @@ -23,15 +22,11 @@ export class ExtHostProgress implements ExtHostProgressShape { this._proxy = proxy; } - async withProgress(extension: IExtensionDescription, options: ProgressOptions | ProgressOptions2, task: (progress: Progress, token: CancellationToken) => Thenable): Promise { + async withProgress(extension: IExtensionDescription, options: ProgressOptions, task: (progress: Progress, token: CancellationToken) => Thenable): Promise { const handle = this._handles++; const { title, location, cancellable } = options; const source = { label: extension.displayName || extension.name, id: extension.identifier.value }; - if (typeof location === 'object' && location.hasOwnProperty('toolInvocationToken')) { - checkProposedApiEnabled(extension, 'toolProgress'); - } - this._proxy.$startProgress(handle, { location: ProgressLocation.from(location), title, source, cancellable }, !extension.isUnderDevelopment ? extension.identifier.value : undefined).catch(onUnexpectedExternalError); return this._withProgress(handle, task, !!cancellable); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index cb208b29f5e..75c7f54d600 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -36,14 +36,14 @@ import { EndOfLineSequence, TrackedRangeStickiness } from '../../../editor/commo import { ITextEditorOptions } from '../../../platform/editor/common/editor.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { IMarkerData, IRelatedInformation, MarkerSeverity, MarkerTag } from '../../../platform/markers/common/markers.js'; -import { ProgressLocation as MainProgressLocation, ProgressLocationDescriptor as MainProgressLocationDescriptor } from '../../../platform/progress/common/progress.js'; +import { ProgressLocation as MainProgressLocation } from '../../../platform/progress/common/progress.js'; import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; import { IViewBadge } from '../../common/views.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; import { IChatRequestVariableEntry, isImageVariableEntry } from '../../contrib/chat/common/chatModel.js'; import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; -import { IToolData, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js'; @@ -1458,13 +1458,9 @@ export namespace EndOfLine { } export namespace ProgressLocation { - export function from(loc: vscode.ProgressLocation | { viewId: string } | { toolInvocationToken: object | undefined }): MainProgressLocationDescriptor { + export function from(loc: vscode.ProgressLocation | { viewId: string }): MainProgressLocation | string { if (typeof loc === 'object') { - if ('toolInvocationToken' in loc) { - return { toolInvocationCallId: (loc.toolInvocationToken as IToolInvocationContext).callId }; - } else { - return { viewId: loc.viewId }; - } + return loc.viewId; } switch (loc) { diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index e5562575d4c..bc660c6f7a1 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -692,7 +692,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService); const treeMenus = this.treeDisposables.add(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this.treeDisposables.add(this.instantiationService.createInstance(ResourceLabels, this)); - const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: { viewId: this.id } }, () => task)); + const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); const aligner = this.treeDisposables.add(new Aligner(this.themeService)); const checkboxStateHandler = this.treeDisposables.add(new CheckboxStateHandler()); const renderer = this.treeDisposables.add(this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler, () => this.manuallyManageCheckboxes)); @@ -1791,7 +1791,7 @@ export class CustomTreeView extends AbstractTreeView { id: this.id, }); this.createTree(); - this.progressService.withProgress({ location: { viewId: this.id } }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) + this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) .then(() => timeout(2000)) .then(() => { this.updateMessage(); diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index 58200d5fc40..e8dedb875d8 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -651,8 +651,8 @@ export abstract class ViewPane extends Pane implements IView { return this.progressIndicator; } - protected getProgressLocation(): { viewId: string } { - return { viewId: this.viewDescriptorService.getViewContainerByViewId(this.id)!.id }; + protected getProgressLocation(): string { + return this.viewDescriptorService.getViewContainerByViewId(this.id)!.id; } protected getLocationBasedColors(): IViewPaneLocationColors { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts index a1489fc90b1..20ca1f46cdb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts @@ -24,7 +24,6 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; import { IProgressService } from '../../../../../platform/progress/common/progress.js'; -import { ILanguageModelToolProgressService } from '../../../../services/progress/browser/languageModelToolProgressService.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatMarkdownContent, IChatProgressMessage, IChatTerminalToolInvocationData, IChatToolInvocation, IChatToolInvocationSerialized } from '../../common/chatService.js'; import { IChatRendererContent } from '../../common/chatViewModel.js'; @@ -144,7 +143,6 @@ class ChatToolInvocationSubPart extends Disposable { @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @ICommandService private readonly commandService: ICommandService, @IMarkerService private readonly markerService: IMarkerService, - @ILanguageModelToolProgressService private readonly languageModelToolProgressService: ILanguageModelToolProgressService, ) { super(); @@ -465,9 +463,9 @@ class ChatToolInvocationSubPart extends Disposable { return part.domNode; } else { const container = document.createElement('div'); - const progressObservable = this._register(this.languageModelToolProgressService.listenForProgress(this.toolInvocation.toolCallId)); + const progressObservable = this.toolInvocation.kind === 'toolInvocation' ? this.toolInvocation.progress : undefined; this._register(autorunWithStore((reader, store) => { - const progress = progressObservable.object.read(reader); + const progress = progressObservable?.read(reader); const part = store.add(this.renderProgressContent(progress?.message || this.toolInvocation.invocationMessage)); dom.reset(container, part.domNode); })); diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index fa680a553d1..4a6b02c24b8 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -12,7 +12,7 @@ import { Emitter } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { Lazy } from '../../../../base/common/lazy.js'; -import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { LRUCache } from '../../../../base/common/map.js'; import { localize } from '../../../../nls.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; @@ -22,6 +22,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import * as JSONContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IProgressStep } from '../../../../platform/progress/common/progress.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -40,6 +41,11 @@ interface IToolEntry { impl?: IToolImpl; } +interface TrackedCall { + invocation?: ChatToolInvocation; + store: IDisposable; +} + export class LanguageModelToolsService extends Disposable implements ILanguageModelToolsService { _serviceBrand: undefined; @@ -53,7 +59,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private _toolContextKeys = new Set(); private readonly _ctxToolsCount: IContextKey; - private _callsByRequestId = new Map(); + private _callsByRequestId = new Map(); private _workspaceToolConfirmStore: Lazy; private _profileToolConfirmStore: Lazy; @@ -91,6 +97,17 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo this._ctxToolsCount = ChatContextKeys.Tools.toolsCount.bindTo(_contextKeyService); } + acceptProgress(sessionId: string | undefined, callId: string, progress: IProgressStep): void { + if (!sessionId) { + return; // not supported, yet + } + + this._callsByRequestId.get(sessionId) + ?.find(call => call.invocation?.toolCallId === callId) + ?.invocation + ?.acceptProgress(progress); + } + registerToolData(toolData: IToolData): IDisposable { if (this._tools.has(toolData.id)) { throw new Error(`Tool "${toolData.id}" is already registered.`); @@ -235,7 +252,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo if (!this._callsByRequestId.has(requestId)) { this._callsByRequestId.set(requestId, []); } - this._callsByRequestId.get(requestId)!.push(store); + const trackedCall: TrackedCall = { store }; + this._callsByRequestId.get(requestId)!.push(trackedCall); const source = new CancellationTokenSource(); store.add(toDisposable(() => { @@ -252,6 +270,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const prepared = await this.prepareToolInvocation(tool, dto, token); toolInvocation = new ChatToolInvocation(prepared, tool.data, dto.callId); + trackedCall.invocation = toolInvocation; if (this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace)) { toolInvocation.confirmed.complete(true); } @@ -285,7 +304,11 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo throw new CancellationError(); } - toolResult = await tool.impl.invoke(dto, countTokens, token); + toolResult = await tool.impl.invoke(dto, countTokens, { + report: step => { + toolInvocation?.acceptProgress(step); + } + }, token); this.ensureToolDetails(dto, toolResult, tool.data); this._telemetryService.publicLog2( @@ -403,7 +426,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private cleanupCallDisposables(requestId: string, store: DisposableStore): void { const disposables = this._callsByRequestId.get(requestId); if (disposables) { - const index = disposables.indexOf(store); + const index = disposables.findIndex(d => d.store === store); if (index > -1) { disposables.splice(index, 1); } @@ -417,7 +440,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo cancelToolCallsForRequest(requestId: string): void { const calls = this._callsByRequestId.get(requestId); if (calls) { - calls.forEach(call => call.dispose()); + calls.forEach(call => call.store.dispose()); this._callsByRequestId.delete(requestId); } } @@ -425,7 +448,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo public override dispose(): void { super.dispose(); - this._callsByRequestId.forEach(calls => dispose(calls)); + this._callsByRequestId.forEach(calls => calls.forEach(call => call.store.dispose())); this._ctxToolsCount.reset(); } } diff --git a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts index 9c3570e9f68..e27f5219ea4 100644 --- a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts @@ -5,7 +5,9 @@ import { DeferredPromise } from '../../../../../base/common/async.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { observableValue } from '../../../../../base/common/observable.js'; import { localize } from '../../../../../nls.js'; +import { IProgressStep } from '../../../../../platform/progress/common/progress.js'; import { IChatTerminalToolInvocationData, IChatToolInputInvocationData, IChatToolInvocation, IChatToolInvocationSerialized } from '../chatService.js'; import { IPreparedToolInvocation, IToolConfirmationMessages, IToolData, IToolResult } from '../languageModelToolsService.js'; @@ -45,6 +47,8 @@ export class ChatToolInvocation implements IChatToolInvocation { public readonly toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData; + public readonly progress = observableValue<{ message?: string; progress: number }>(this, { progress: 0 }); + constructor(preparedInvocation: IPreparedToolInvocation | undefined, toolData: IToolData, public readonly toolCallId: string) { const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${toolData.displayName}"`); const invocationMessage = preparedInvocation?.invocationMessage ?? defaultMessage; @@ -84,6 +88,14 @@ export class ChatToolInvocation implements IChatToolInvocation { return this._confirmationMessages; } + public acceptProgress(step: IProgressStep) { + const prev = this.progress.get(); + this.progress.set({ + progress: step.increment ? (prev.progress + step.increment) : prev.progress, + message: step.message, + }, undefined); + } + public toJSON(): IChatToolInvocationSerialized { return { kind: 'toolInvocationSerialized', diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 3891650fa10..84f6f3309dc 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -7,6 +7,7 @@ import { DeferredPromise } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; +import { IObservable } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; @@ -234,6 +235,7 @@ export interface IChatToolInvocation { invocationMessage: string | IMarkdownString; pastTenseMessage: string | IMarkdownString | undefined; resultDetails: IToolResult['toolResultDetails']; + progress: IObservable<{ message?: string; progress: number }>; readonly toolId: string; readonly toolCallId: string; diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index 7b37b56bf2f..4f675dee458 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -8,14 +8,15 @@ import { Event } from '../../../../base/common/event.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../base/common/network.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; +import { Location } from '../../../../editor/common/languages.js'; import { ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { Location } from '../../../../editor/common/languages.js'; +import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; import { IChatTerminalToolInvocationData, IChatToolInputInvocationData } from './chatService.js'; -import { Schemas } from '../../../../base/common/network.js'; import { PromptElementJSON, stringifyPromptElementJSON } from './tools/promptTsxTypes.js'; export interface IToolData { @@ -83,7 +84,6 @@ export interface IToolInvocation { export interface IToolInvocationContext { sessionId: string; - callId: string | undefined; } export function isToolInvocationContext(obj: any): obj is IToolInvocationContext { @@ -134,7 +134,7 @@ export interface IPreparedToolInvocation { } export interface IToolImpl { - invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise; + invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: IProgress, token: CancellationToken): Promise; prepareToolInvocation?(parameters: any, token: CancellationToken): Promise; } @@ -151,6 +151,7 @@ export interface ILanguageModelToolsService { getTool(id: string): IToolData | undefined; getToolByName(name: string): IToolData | undefined; invokeTool(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise; + acceptProgress(sessionId: string | undefined, callId: string, progress: IProgressStep): void; setToolAutoConfirmation(toolId: string, scope: 'workspace' | 'profile' | 'memory', autoConfirm?: boolean): void; resetToolAutoConfirmation(): void; cancelToolCallsForRequest(requestId: string): void; diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index 4dbe039d0e5..95a77534bd4 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -9,6 +9,7 @@ import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; +import { IProgress, IProgressStep } from '../../../../../platform/progress/common/progress.js'; import { SaveReason } from '../../../../common/editor.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { CellUri } from '../../../notebook/common/notebookCommon.js'; @@ -42,7 +43,7 @@ export class EditTool implements IToolImpl { @INotebookService private readonly notebookService: INotebookService, ) { } - async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { + async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, _progress: IProgress, token: CancellationToken): Promise { if (!invocation.context) { throw new Error('toolInvocationToken is required for this tool'); } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts b/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts index b81143b9467..7d361f97bb8 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts @@ -11,6 +11,7 @@ import { ITrustedDomainService } from '../../../url/browser/trustedDomainService import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultTextPart } from '../../common/languageModelToolsService.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { InternalFetchWebPageToolId } from '../../common/tools/tools.js'; +import { IProgress, IProgressStep } from '../../../../../platform/progress/common/progress.js'; export const FetchWebPageToolData: IToolData = { id: InternalFetchWebPageToolId, @@ -41,7 +42,7 @@ export class FetchWebPageTool implements IToolImpl { @ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService, ) { } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _token: CancellationToken): Promise { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: IProgress, _token: CancellationToken): Promise { const parsedUriResults = this._parseUris((invocation.parameters as { urls?: string[] }).urls); const validUris = Array.from(parsedUriResults.values()).filter((uri): uri is URI => !!uri); if (!validUris.length) { diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index eb47ea1ac00..5454f7f7ca3 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -149,7 +149,7 @@ suite('LanguageModelToolsService', () => { const toolBarrier = new Barrier(); const toolImpl: IToolImpl = { - invoke: async (invocation, countTokens, cancelToken) => { + invoke: async (invocation, countTokens, progress, cancelToken) => { assert.strictEqual(invocation.callId, '1'); assert.strictEqual(invocation.toolId, 'testTool'); assert.deepStrictEqual(invocation.parameters, { a: 1 }); @@ -174,8 +174,7 @@ suite('LanguageModelToolsService', () => { a: 1 }, context: { - sessionId, - callId: '1', + sessionId }, }; chatService.addSession({ diff --git a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts index 87a60b7fc0e..2cf7956aa5a 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { IProgressStep } from '../../../../../platform/progress/common/progress.js'; import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; export class MockLanguageModelToolsService implements ILanguageModelToolsService { @@ -46,6 +47,10 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService return undefined; } + acceptProgress(sessionId: string | undefined, callId: string, progress: IProgressStep): void { + + } + async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { return { content: [{ kind: 'text', value: 'result' }] diff --git a/src/vs/workbench/contrib/debug/browser/debugProgress.ts b/src/vs/workbench/contrib/debug/browser/debugProgress.ts index 5ad4c9c995a..0cca619bf96 100644 --- a/src/vs/workbench/contrib/debug/browser/debugProgress.ts +++ b/src/vs/workbench/contrib/debug/browser/debugProgress.ts @@ -37,7 +37,7 @@ export class DebugProgressContribution implements IWorkbenchContribution { }); if (viewsService.isViewContainerVisible(VIEWLET_ID)) { - progressService.withProgress({ location: { viewId: VIEWLET_ID } }, () => promise); + progressService.withProgress({ location: VIEWLET_ID }, () => promise); } const source = debugService.getAdapterManager().getDebuggerLabel(session.configuration.type); progressService.withProgress({ diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index af6ef42997c..10b8d244b2e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -131,7 +131,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } if (state === State.Initializing) { - this.progressService.withProgress({ location: { viewId: VIEWLET_ID }, }, _progress => { + this.progressService.withProgress({ location: VIEWLET_ID, }, _progress => { return new Promise(resolve => this.progressResolve = resolve); }); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 8533863a48c..ce3cab343b4 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -2737,7 +2737,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private doInstall(extension: IExtension | undefined, installTask: () => Promise, progressLocation?: ProgressLocation | string): Promise { const title = extension ? nls.localize('installing named extension', "Installing '{0}' extension....", extension.displayName) : nls.localize('installing extension', 'Installing extension....'); return this.withProgress({ - location: typeof progressLocation === 'string' ? { viewId: progressLocation } : (progressLocation ?? ProgressLocation.Extensions), + location: progressLocation ?? ProgressLocation.Extensions, title }, async () => { try { diff --git a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts index 3d587cef983..9bf3486ad36 100644 --- a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -9,6 +9,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { SortBy } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js'; +import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; import { ExtensionState, IExtensionsWorkbenchService } from '../common/extensions.js'; @@ -64,7 +65,7 @@ export class SearchExtensionsTool implements IToolImpl { @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, ) { } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, token: CancellationToken): Promise { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: IProgress, token: CancellationToken): Promise { const params = invocation.parameters as InputParams; if (!params.keywords?.length && !params.category) { return { diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 8337c2a9750..44ecb6d65e5 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -98,7 +98,7 @@ export class BrowserFileUpload { ); // Also indicate progress in the files view - this.progressService.withProgress({ location: { viewId: VIEW_ID }, delay: 500 }, () => uploadPromise); + this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => uploadPromise); return uploadPromise; } @@ -417,7 +417,7 @@ export class ExternalFileImport { ); // Also indicate progress in the files view - this.progressService.withProgress({ location: { viewId: VIEW_ID }, delay: 500 }, () => importPromise); + this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => importPromise); return importPromise; } @@ -619,7 +619,7 @@ export class FileDownload { ); // Also indicate progress in the files view - this.progressService.withProgress({ location: { viewId: VIEW_ID }, delay: 500 }, () => downloadPromise); + this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => downloadPromise); return downloadPromise; } diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts index 51c78d8dd7a..2d3e09b6a4d 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -16,12 +16,6 @@ const mcpActivationEventPrefix = 'onMcpCollection:'; export const mcpActivationEvent = (collectionId: string) => mcpActivationEventPrefix + collectionId; -const mcpSchemaExampleServer = { - command: 'node', - args: ['my-mcp-server.js'], - env: {}, -}; - export const enum DiscoverySource { ClaudeDesktop = 'claude-desktop', Windsurf = 'windsurf', @@ -55,15 +49,17 @@ export const mcpSchemaExampleServers = { } }; -const httpSchemaExample = { - url: 'http://localhost:3001/mcp', - headers: {}, +const httpSchemaExamples = { + 'my-mcp-server': { + url: 'http://localhost:3001/mcp', + headers: {}, + } }; export const mcpStdioServerSchema: IJSONSchema = { type: 'object', additionalProperties: false, - examples: [mcpSchemaExampleServer], + examples: [mcpSchemaExampleServers['mcp-server-time']], properties: { type: { type: 'string', @@ -110,14 +106,14 @@ export const mcpServerSchema: IJSONSchema = { servers: { examples: [ mcpSchemaExampleServers, - httpSchemaExample, + httpSchemaExamples, ], additionalProperties: { oneOf: [mcpStdioServerSchema, { type: 'object', additionalProperties: false, required: ['url'], - examples: [httpSchemaExample], + examples: [httpSchemaExamples['my-mcp-server']], properties: { type: { type: 'string', diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts index fe8660b7625..bf4c7407011 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -14,7 +14,7 @@ import { localize } from '../../../../nls.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; -import { IProgressService } from '../../../../platform/progress/common/progress.js'; +import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; @@ -215,7 +215,6 @@ class McpToolImplementation implements IToolImpl { private readonly _tool: IMcpTool, private readonly _server: IMcpServer, @IProductService private readonly _productService: IProductService, - @IProgressService private readonly _progressService: IProgressService, ) { } async prepareToolInvocation(parameters: any): Promise { @@ -248,7 +247,7 @@ class McpToolImplementation implements IToolImpl { }; } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, token: CancellationToken) { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, progress: IProgress, token: CancellationToken) { const result: IToolResult = { content: [] @@ -256,12 +255,7 @@ class McpToolImplementation implements IToolImpl { const outputParts: string[] = []; - - const params = invocation.parameters as Record; - const callResult = invocation.context - ? await this._progressService.withProgress({ location: { toolInvocationCallId: invocation.callId } }, p => this._tool.callWithProgress(params, p, token)) - : await this._tool.call(params, token); - + const callResult = await this._tool.callWithProgress(invocation.parameters as Record, progress, token); for (const item of callResult.content) { if (item.type === 'text') { result.content.push({ diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 9d984d1abfc..fdde018b017 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -651,7 +651,7 @@ class SCMHistoryViewPaneActionRunner extends ActionRunner { } protected override runAction(action: IAction, context?: unknown): Promise { - return this._progressService.withProgress({ location: { viewId: HISTORY_VIEW_PANE_ID } }, + return this._progressService.withProgress({ location: HISTORY_VIEW_PANE_ID }, async () => await super.runAction(action, context)); } } @@ -1318,7 +1318,7 @@ export class SCMHistoryViewPane extends ViewPane { await waitForState(firstRepositoryInitialized); // Initial rendering - await this._progressService.withProgress({ location: { viewId: this.id } }, async () => { + await this._progressService.withProgress({ location: this.id }, async () => { await this._treeOperationSequencer.queue(async () => { await this._tree.setInput(this._treeViewModel); this._tree.scrollTop = 0; @@ -1689,7 +1689,7 @@ export class SCMHistoryViewPane extends ViewPane { return this._updateChildrenThrottler.queue( () => this._treeOperationSequencer.queue( async () => { - await this._progressService.withProgress({ location: { viewId: this.id } }, + await this._progressService.withProgress({ location: this.id }, async () => { await this._tree.updateChildren(undefined, undefined, undefined, { // diffIdentityProvider: this._treeIdentityProvider diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index f23dae9b9a5..160800155fc 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -1684,7 +1684,7 @@ export class RefreshTestsAction extends Action2 { const progressService = accessor.get(IProgressService); const controllerIds = distinct(elements.filter(isDefined).map(e => e.test.controllerId)); - return progressService.withProgress({ location: { viewId: Testing.ViewletId } }, async () => { + return progressService.withProgress({ location: Testing.ViewletId }, async () => { if (controllerIds.length) { await Promise.all(controllerIds.map(id => testService.refreshTests(id))); } else { diff --git a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts index 7bfe65c7eff..1895434ddba 100644 --- a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts +++ b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts @@ -585,7 +585,7 @@ export class OutputPeekTree extends Disposable { return coverageService.closeCoverage(); } progressService.withProgress( - { location: { viewId: options.locationForProgress } }, + { location: options.locationForProgress }, () => coverageService.openCoverage(task, true) ); } diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index f48f461e011..d6582ce54f8 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -645,7 +645,7 @@ export class TimelinePane extends ViewPane { private async handleRequest(request: TimelineRequest) { let response: Timeline | undefined; try { - response = await this.progressService.withProgress({ location: { viewId: this.id } }, () => request.result); + response = await this.progressService.withProgress({ location: this.id }, () => request.result); } finally { this.pendingRequests.get(request.source)?.dispose(); diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index 0de87305fc0..6dae8155249 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -267,7 +267,7 @@ export class WebviewViewPane extends ViewPane { } private async withProgress(task: () => Promise): Promise { - return this.progressService.withProgress({ location: { viewId: this.id }, delay: 500 }, task); + return this.progressService.withProgress({ location: this.id, delay: 500 }, task); } override onDidScrollRoot() { diff --git a/src/vs/workbench/services/progress/browser/languageModelToolProgressService.ts b/src/vs/workbench/services/progress/browser/languageModelToolProgressService.ts deleted file mode 100644 index 1e744fd1dc0..00000000000 --- a/src/vs/workbench/services/progress/browser/languageModelToolProgressService.ts +++ /dev/null @@ -1,83 +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 { Event } from '../../../../base/common/event.js'; -import { IReference } from '../../../../base/common/lifecycle.js'; -import { IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; -import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; - -export const ILanguageModelToolProgressService = createDecorator('languageModelToolProgressService'); - -export interface ILanguageModelToolProgressService { - _serviceBrand: undefined; - - listenForProgress(callId: string): IReference>; - - handleProgress

, R = unknown>(callId: string, callback: (progress: IProgress) => P): P; -} - -export interface ILanguageModelToolProgressDelegate { - onDidCancel: Event; - update(step: IProgressStep): void; - dispose?(): void; -} - -export class LanguageModelToolProgressService implements ILanguageModelToolProgressService { - declare readonly _serviceBrand: undefined; - - private readonly progress = new Map; - rc: number; - }>(); - - listenForProgress(callId: string): IReference> { - let rec = this.progress.get(callId); - if (!rec) { - rec = { value: observableValue(this, { message: undefined, total: 0, done: 0 }), rc: 0 }; - this.progress.set(callId, rec); - } - rec.rc++; - - return { - object: rec.value, - dispose: () => { - if (!--rec.rc) { - this.progress.delete(callId); - } - } - }; - } - - handleProgress

, R = unknown>(callId: string, callback: (progress: IProgress) => P): P { - let rec = this.progress.get(callId); - if (!rec) { - rec = { value: observableValue(this, { message: undefined, total: 0, done: 0 }), rc: 0 }; - this.progress.set(callId, rec); - } - - rec.rc++; - - let last = 0; - const promise = callback({ - report: update => { - if (update.increment) { - last += update.increment; - } - rec.value.set({ message: update.message, total: update.total || 100, done: last }, undefined); - }, - }); - promise.finally(() => { - if (!--rec.rc) { - this.progress.delete(callId); - } - }); - - return promise; - } -} - -registerSingleton(ILanguageModelToolProgressService, LanguageModelToolProgressService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 96d04e24474..cb8831c7e77 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -24,7 +24,6 @@ import { IPaneCompositePartService } from '../../panecomposite/browser/panecompo import { stripIcons } from '../../../../base/common/iconLabels.js'; import { IUserActivityService } from '../../userActivity/common/userActivityService.js'; import { createWorkbenchDialogOptions } from '../../../../platform/dialogs/browser/dialog.js'; -import { ILanguageModelToolProgressService } from './languageModelToolProgressService.js'; export class ProgressService extends Disposable implements IProgressService { @@ -40,7 +39,6 @@ export class ProgressService extends Disposable implements IProgressService { @ILayoutService private readonly layoutService: ILayoutService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IUserActivityService private readonly userActivityService: IUserActivityService, - @ILanguageModelToolProgressService private readonly languageModelToolProgressService: ILanguageModelToolProgressService, ) { super(); } @@ -57,32 +55,24 @@ export class ProgressService extends Disposable implements IProgressService { } }; - const handleViewLocation = (location: { viewId: string }) => { - const viewContainer = this.viewDescriptorService.getViewContainerById(location.viewId); + const handleStringLocation = (location: string) => { + const viewContainer = this.viewDescriptorService.getViewContainerById(location); if (viewContainer) { const viewContainerLocation = this.viewDescriptorService.getViewContainerLocation(viewContainer); if (viewContainerLocation !== null) { - return this.withPaneCompositeProgress(location.viewId, viewContainerLocation, task, { ...options, location }); + return this.withPaneCompositeProgress(location, viewContainerLocation, task, { ...options, location }); } } - if (this.viewDescriptorService.getViewDescriptorById(location.viewId) !== null) { - return this.withViewProgress(location.viewId, task, { ...options, location }); + if (this.viewDescriptorService.getViewDescriptorById(location) !== null) { + return this.withViewProgress(location, task, { ...options, location }); } throw new Error(`Bad progress location: ${location}`); }; - if (typeof location === 'object') { - if ('viewId' in location) { - return handleViewLocation(location); - } else if ('toolInvocationCallId' in location) { - if (!location.toolInvocationCallId) { - return task({ report: () => { } }); - } - - return this.languageModelToolProgressService.handleProgress(location.toolInvocationCallId, task); - } + if (typeof location === 'string') { + return handleStringLocation(location); } switch (location) { @@ -112,7 +102,7 @@ export class ProgressService extends Disposable implements IProgressService { case ProgressLocation.Explorer: return this.withPaneCompositeProgress('workbench.view.explorer', ViewContainerLocation.Sidebar, task, { ...options, location }); case ProgressLocation.Scm: - return handleViewLocation({ viewId: 'workbench.scm' }); + return handleStringLocation('workbench.scm'); case ProgressLocation.Extensions: return this.withPaneCompositeProgress('workbench.view.extensions', ViewContainerLocation.Sidebar, task, { ...options, location }); case ProgressLocation.Dialog: diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts index b309fe618e8..df2b9fa8763 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -262,7 +262,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU try { await this.progressService.withProgress({ - location: typeof location === 'string' ? { viewId: location } : location, + location, title: localize('profiles.exporting', "{0}: Exporting...", PROFILES_CATEGORY.value), }, async progress => { const id = await this.pickProfileContentHandler(profile.name); diff --git a/src/vscode-dts/vscode.proposed.toolProgress.d.ts b/src/vscode-dts/vscode.proposed.toolProgress.d.ts index 6969c1f928d..8deea607747 100644 --- a/src/vscode-dts/vscode.proposed.toolProgress.d.ts +++ b/src/vscode-dts/vscode.proposed.toolProgress.d.ts @@ -5,30 +5,21 @@ declare module 'vscode' { - export interface ProgressOptions2 extends Omit { - location: ProgressLocation | { - /** - * The identifier of a view for which progress should be shown. - */ - viewId: string; - } | { - /** - * An invocation token for progress shown while a {@link LanguageModelTool} is running. - */ - toolInvocationToken: ChatParticipantToolToken | undefined; - }; + /** + * todo@connor4312: `vscode.window.withProgres` can take this interface as well. + */ + export interface ProgressStep { + /** + * A progress message that represents a chunk of work + */ + message?: string; + /** + * An increment for discrete progress. Increments will be summed up until 100 (100%) is reached + */ + increment?: number; } - export namespace window { - export function withProgress(options: ProgressOptions2, task: (progress: Progress<{ - /** - * A progress message that represents a chunk of work - */ - message?: string; - /** - * An increment for discrete progress. Increments will be summed up until 100% is reached - */ - increment?: number; - }>, token: CancellationToken) => Thenable): Thenable; + export interface LanguageModelTool { + invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken, progress: Progress): ProviderResult; } } From df61dc46bde3f785c1bd07fbe4ba9f5a4bd78eff Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 17 Apr 2025 13:16:08 -0700 Subject: [PATCH 005/114] Revert "Merge pull request #240011 from gabritto/reverthover" This reverts commit ef3a39fffd573e6f9ac8bb98d08134221f12948c, reversing changes made to e38edc4e3b49bb6ce030c4038001e32ee9bbd22a. --- .../typescript-language-features/package.json | 12 ++++- .../package.nls.json | 3 +- .../src/languageFeatures/hover.ts | 44 ++++++++++++++++--- .../tsconfig.json | 3 +- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 072cca645ff..a555ded542a 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -11,7 +11,8 @@ "workspaceTrust", "multiDocumentHighlightProvider", "codeActionAI", - "codeActionRanges" + "codeActionRanges", + "editorHoverVerbosityLevel" ], "capabilities": { "virtualWorkspaces": { @@ -571,6 +572,15 @@ "type": "boolean", "default": true, "markdownDescription": "%configuration.updateImportsOnPaste%" + }, + "typescript.experimental.expandableHover": { + "type": "boolean", + "default": false, + "description": "%configuration.expandableHover%", + "scope": "window", + "tags": [ + "experimental" + ] } } }, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index abbc9896ce3..2e964736837 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -230,7 +230,8 @@ "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors on web even when project wide IntelliSense is enabled. This is always on when project wide IntelliSense is not enabled or available. See `#typescript.tsserver.web.projectWideIntellisense.enabled#`", "configuration.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web. This enables IntelliSense for imported packages. Requires `#typescript.tsserver.web.projectWideIntellisense.enabled#`. Currently not supported for Safari.", "configuration.tsserver.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.", - "configuration.updateImportsOnPaste": "Automatically update imports when pasting code. Requires TypeScript 5.6+.", + "configuration.updateImportsOnPaste": "Enable updating imports when pasting code. Requires TypeScript 5.7+.\n\nBy default this shows a option to update imports after pasting. You can use the `#editor.pasteAs.preferences#` setting to update imports automatically when pasting: `\"editor.pasteAs.preferences\": [ \"text.updateImports.jsts\" ]`.", + "configuration.expandableHover": "Enable expanding/contracting the hover to reveal more/less information from the TS server.", "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", "walkthroughs.nodejsWelcome.downloadNode.forMacOrWindows.title": "Install Node.js", diff --git a/extensions/typescript-language-features/src/languageFeatures/hover.ts b/extensions/typescript-language-features/src/languageFeatures/hover.ts index 3012658036f..edb08d029fa 100644 --- a/extensions/typescript-language-features/src/languageFeatures/hover.ts +++ b/extensions/typescript-language-features/src/languageFeatures/hover.ts @@ -11,10 +11,11 @@ import { DocumentSelector } from '../configuration/documentSelector'; import { documentationToMarkdown } from './util/textRendering'; import * as typeConverters from '../typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; - +import { API } from '../tsServer/api'; class TypeScriptHoverProvider implements vscode.HoverProvider { + private lastHoverAndLevel: [vscode.Hover, number] | undefined; public constructor( private readonly client: ITypeScriptServiceClient, @@ -24,17 +25,24 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { public async provideHover( document: vscode.TextDocument, position: vscode.Position, - token: vscode.CancellationToken - ): Promise { + token: vscode.CancellationToken, + context?: vscode.HoverContext, + ): Promise { const filepath = this.client.toOpenTsFilePath(document); if (!filepath) { return undefined; } + const enableExpandableHover = vscode.workspace.getConfiguration('typescript').get('experimental.expandableHover'); + let verbosityLevel: number | undefined; + if (enableExpandableHover && this.client.apiVersion.gte(API.v570)) { + verbosityLevel = Math.max(0, this.getPreviousLevel(context?.previousHover) + (context?.verbosityDelta ?? 0)); + } + const args = { ...typeConverters.Position.toFileLocationRequestArgs(filepath, position), verbosityLevel }; + const response = await this.client.interruptGetErr(async () => { await this.fileConfigurationManager.ensureConfigurationForDocument(document, token); - const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position); return this.client.execute('quickinfo', args, token); }); @@ -42,9 +50,24 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { return undefined; } - return new vscode.Hover( - this.getContents(document.uri, response.body, response._serverType), - typeConverters.Range.fromTextSpan(response.body)); + const contents = this.getContents(document.uri, response.body, response._serverType); + const range = typeConverters.Range.fromTextSpan(response.body); + const hover = verbosityLevel !== undefined ? + new vscode.VerboseHover( + contents, + range, + // @ts-expect-error + /*canIncreaseVerbosity*/ response.body.canIncreaseVerbosityLevel, + /*canDecreaseVerbosity*/ verbosityLevel !== 0 + ) : new vscode.Hover( + contents, + range + ); + + if (verbosityLevel !== undefined) { + this.lastHoverAndLevel = [hover, verbosityLevel]; + } + return hover; } private getContents( @@ -72,6 +95,13 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { parts.push(md); return parts; } + + private getPreviousLevel(previousHover: vscode.Hover | undefined): number { + if (previousHover && this.lastHoverAndLevel && this.lastHoverAndLevel[0] === previousHover) { + return this.lastHoverAndLevel[1]; + } + return 0; + } } export function register( diff --git a/extensions/typescript-language-features/tsconfig.json b/extensions/typescript-language-features/tsconfig.json index f604952abd2..776a71efaf8 100644 --- a/extensions/typescript-language-features/tsconfig.json +++ b/extensions/typescript-language-features/tsconfig.json @@ -14,6 +14,7 @@ "../../src/vscode-dts/vscode.proposed.codeActionAI.d.ts", "../../src/vscode-dts/vscode.proposed.codeActionRanges.d.ts", "../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts", - "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts" + "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts", + "../../src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts", ] } From e9766a8d3856208937a4268314ac795c000f4ea6 Mon Sep 17 00:00:00 2001 From: Wolfgang Faust Date: Thu, 17 Apr 2025 18:08:30 -0700 Subject: [PATCH 006/114] feat: markdown-basics snippets: quote all lines Previously, this snippet just inserted "> " before the selected text, which is not very useful; particularly, when multiple paragraphs are selected, this will only quote the first one. Instead, we now prefix every line in the selection with "> ", so that it will all be quoted regardless of how many paragraphs are taken. This does also change the behavior in the case of no selected text slightly; previously it would insert "> []" (where "[]" indicates the selection; that is, it would put the insertion point after the quote marker). It now inserts "[> ]" instead (that is, the same text but it's all selected) so if you start typing you'll wind up removing the quote marker. Since using this snippet *not* on a selection seems like a roundabout way of writing "> " this seems like an acceptable tradeoff. --- extensions/markdown-basics/snippets/markdown.code-snippets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/markdown-basics/snippets/markdown.code-snippets b/extensions/markdown-basics/snippets/markdown.code-snippets index 6eb55374e2e..08e4021e951 100644 --- a/extensions/markdown-basics/snippets/markdown.code-snippets +++ b/extensions/markdown-basics/snippets/markdown.code-snippets @@ -11,7 +11,7 @@ }, "Insert quoted text": { "prefix": "quote", - "body": "> ${1:${TM_SELECTED_TEXT}}", + "body": "${1:${TM_SELECTED_TEXT/^/> /gm}}", "description": "Insert quoted text" }, "Insert inline code": { From 55fe0332111f447f189b8a602a00489be38c616a Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 18 Apr 2025 08:29:33 -0700 Subject: [PATCH 007/114] update description and api check --- extensions/typescript-language-features/package.nls.json | 4 ++-- .../src/languageFeatures/hover.ts | 2 +- extensions/typescript-language-features/src/tsServer/api.ts | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 2e964736837..2547b30e88c 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -230,8 +230,8 @@ "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors on web even when project wide IntelliSense is enabled. This is always on when project wide IntelliSense is not enabled or available. See `#typescript.tsserver.web.projectWideIntellisense.enabled#`", "configuration.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web. This enables IntelliSense for imported packages. Requires `#typescript.tsserver.web.projectWideIntellisense.enabled#`. Currently not supported for Safari.", "configuration.tsserver.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.", - "configuration.updateImportsOnPaste": "Enable updating imports when pasting code. Requires TypeScript 5.7+.\n\nBy default this shows a option to update imports after pasting. You can use the `#editor.pasteAs.preferences#` setting to update imports automatically when pasting: `\"editor.pasteAs.preferences\": [ \"text.updateImports.jsts\" ]`.", - "configuration.expandableHover": "Enable expanding/contracting the hover to reveal more/less information from the TS server.", + "configuration.updateImportsOnPaste": "Automatically update imports when pasting code. Requires TypeScript 5.6+.", + "configuration.expandableHover": "Enable expanding/contracting the hover to reveal more/less information from the TS server. Requires TypeScript 5.9+.", "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", "walkthroughs.nodejsWelcome.downloadNode.forMacOrWindows.title": "Install Node.js", diff --git a/extensions/typescript-language-features/src/languageFeatures/hover.ts b/extensions/typescript-language-features/src/languageFeatures/hover.ts index edb08d029fa..6c42d642f0f 100644 --- a/extensions/typescript-language-features/src/languageFeatures/hover.ts +++ b/extensions/typescript-language-features/src/languageFeatures/hover.ts @@ -35,7 +35,7 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { const enableExpandableHover = vscode.workspace.getConfiguration('typescript').get('experimental.expandableHover'); let verbosityLevel: number | undefined; - if (enableExpandableHover && this.client.apiVersion.gte(API.v570)) { + if (enableExpandableHover && this.client.apiVersion.gte(API.v590)) { verbosityLevel = Math.max(0, this.getPreviousLevel(context?.previousHover) + (context?.verbosityDelta ?? 0)); } const args = { ...typeConverters.Position.toFileLocationRequestArgs(filepath, position), verbosityLevel }; diff --git a/extensions/typescript-language-features/src/tsServer/api.ts b/extensions/typescript-language-features/src/tsServer/api.ts index 4ddc29944f0..ef3ce4c933d 100644 --- a/extensions/typescript-language-features/src/tsServer/api.ts +++ b/extensions/typescript-language-features/src/tsServer/api.ts @@ -30,6 +30,7 @@ export class API { public static readonly v540 = API.fromSimpleString('5.4.0'); public static readonly v560 = API.fromSimpleString('5.6.0'); public static readonly v570 = API.fromSimpleString('5.7.0'); + public static readonly v590 = API.fromSimpleString('5.9.0'); public static fromVersionString(versionString: string): API { let version = semver.valid(versionString); From b063ab5e2db592e14758cf6fdb91de6a623005b5 Mon Sep 17 00:00:00 2001 From: RedCMD <33529441+RedCMD@users.noreply.github.com> Date: Sat, 19 Apr 2025 11:43:08 +1200 Subject: [PATCH 008/114] addYAMLstringsSuggestionsDefaultSetting --- extensions/yaml/package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/yaml/package.json b/extensions/yaml/package.json index 486458e8ad6..c7e4faed8fd 100644 --- a/extensions/yaml/package.json +++ b/extensions/yaml/package.json @@ -95,7 +95,10 @@ "editor.tabSize": 2, "editor.autoIndent": "advanced", "diffEditor.ignoreTrimWhitespace": false, - "editor.defaultColorDecorators": "never" + "editor.defaultColorDecorators": "never", + "editor.quickSuggestions": { + "strings": true + }, }, "[dockercompose]": { "editor.insertSpaces": true, From 4558fa13ebc27a10de64af621cbb67907fef0c22 Mon Sep 17 00:00:00 2001 From: RedCMD <33529441+RedCMD@users.noreply.github.com> Date: Sun, 20 Apr 2025 08:44:19 +1200 Subject: [PATCH 009/114] Change to `"on"`. Remove comma --- extensions/yaml/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/yaml/package.json b/extensions/yaml/package.json index c7e4faed8fd..66f0a200d08 100644 --- a/extensions/yaml/package.json +++ b/extensions/yaml/package.json @@ -97,8 +97,8 @@ "diffEditor.ignoreTrimWhitespace": false, "editor.defaultColorDecorators": "never", "editor.quickSuggestions": { - "strings": true - }, + "strings": "on" + } }, "[dockercompose]": { "editor.insertSpaces": true, From bd55d40bddbc15788262d2b0be7d4b40609d35c7 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 09:32:08 -0700 Subject: [PATCH 010/114] add `PromptModeMetadata` class --- .../parsers/promptHeader/metadata/mode.ts | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts new file mode 100644 index 00000000000..06a23c90580 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptMetadataRecord } from './record.js'; +import { ChatMode } from '../../../../constants.js'; +import { localize } from '../../../../../../../../nls.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { PromptMetadataDiagnostic, PromptMetadataError } from '../diagnostics.js'; +import { FrontMatterRecord, FrontMatterString, FrontMatterToken } from '../../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; + +/** + * Name of the metadata record in the prompt header. + */ +const RECORD_NAME = 'mode'; + +/** + * Valid chat mode values. + */ +const VALID_MODES = Object.freeze([ + ChatMode.Ask, + ChatMode.Edit, + ChatMode.Agent, +]); + +/** + * Prompt `mode` metadata record inside the prompt header. + */ +export class PromptModeMetadata extends PromptMetadataRecord { + /** + * Private field for tracking all diagnostic issues + * related to this metadata record. + */ + private readonly issues: PromptMetadataDiagnostic[]; + + /** + * List of all diagnostic issues related to this metadata record. + */ + public get diagnostics(): readonly PromptMetadataDiagnostic[] { + return this.issues; + } + + /** + * Private field for tracking the chat mode value. + */ + private modeValue: ChatMode | undefined; + /** + * Chat mode value of the metadata record. + */ + public get mode(): ChatMode | undefined { + return this.modeValue; + } + + constructor( + private readonly recordToken: FrontMatterRecord, + ) { + // sanity check on the name of the record + assert( + PromptModeMetadata.isModeRecord(recordToken), + `Record token must be 'mode', got '${recordToken.nameToken.text}'.`, + ); + + super(recordToken.range); + + this.issues = []; + this.collectDiagnostics(); + } + + /** + * Validate the metadata record and collect all issues + * related to its content. + */ + private collectDiagnostics(): void { + const { valueToken } = this.recordToken; + + // validate that the record value is a string + if ((valueToken instanceof FrontMatterString) === false) { + this.issues.push( + new PromptMetadataError( + valueToken.range, + localize( + 'prompt.header.metadata.mode.diagnostics.invalid-value-type', + "Value of the '{0}' metadata must be '{1}', got '{2}.", + RECORD_NAME, + 'string', + valueToken.valueTypeName, + ), + ), + ); + + return; + } + + const { cleanText } = valueToken; + + // validate that text value is one of the valid modes + const validModes: string[] = [...VALID_MODES]; + const index = validModes.indexOf(cleanText); + if (index !== -1) { + this.modeValue = VALID_MODES[index]; + return; + } + + // if not valid mode value, add an appropriate diagnostic + this.issues.push( + new PromptMetadataError( + valueToken.range, + localize( + 'prompt.header.metadata.mode.diagnostics.invalid-value', + "Value of the '{0}' metadata must be one of ({1}), got '{2}'.", + RECORD_NAME, + VALID_MODES + .map((modeName) => { + return `'${modeName}'`; + }).join(', '), + cleanText, + ), + ), + ); + } + + /** + * Check if a provided front matter token is a metadata record + * with name equal to `mode`. + */ + public static isModeRecord( + token: FrontMatterToken, + ): boolean { + if ((token instanceof FrontMatterRecord) === false) { + return false; + } + + if (token.nameToken.text === RECORD_NAME) { + return true; + } + + return false; + } +} From 4a3a1ee7dad8bf6ea3e3b60121a08734cbacccb1 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 09:34:36 -0700 Subject: [PATCH 011/114] minor refactor --- .../common/promptSyntax/parsers/promptHeader/header.ts | 8 ++++++-- .../promptSyntax/parsers/promptHeader/metadata/index.ts | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/index.ts diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts index 5e6f4829681..376af2ea7ca 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../../../../nls.js'; -import { PromptToolsMetadata } from './metadata/tools.js'; -import { PromptDescriptionMetadata } from './metadata/description.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { Text } from '../../../../../../../editor/common/codecs/baseToken.js'; import { PromptMetadataError, PromptMetadataWarning, TDiagnostic } from './diagnostics.js'; import { TokenStream } from '../../../../../../../editor/common/codecs/utils/tokenStream.js'; import { SimpleToken } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/index.js'; +import { PromptToolsMetadata, PromptModeMetadata, PromptDescriptionMetadata } from './metadata/index.js'; import { FrontMatterRecord } from '../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; import { FrontMatterDecoder, TFrontMatterToken } from '../../../../../../../editor/common/codecs/frontMatterCodec/frontMatterDecoder.js'; @@ -27,6 +26,11 @@ export interface IHeaderMetadata { * Description metadata in the prompt header. */ description?: PromptDescriptionMetadata; + + /** + * Chat mode metadata in the prompt header. + */ + mode?: PromptModeMetadata; } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/index.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/index.ts new file mode 100644 index 00000000000..7c7b1e44115 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/index.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export { PromptModeMetadata } from './mode.js'; +export { PromptToolsMetadata } from './tools.js'; +export { PromptDescriptionMetadata } from './description.js'; From d1baa875eb2d45f7a6630a3aaea80c89d8bd7727 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 09:36:42 -0700 Subject: [PATCH 012/114] use mode metadata parsing in the prompt header class --- .../promptSyntax/parsers/promptHeader/header.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts index 376af2ea7ca..625b5c67df8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts @@ -161,6 +161,18 @@ export class PromptHeader extends Disposable { return; } + // if the record might be a "mode" metadata + // add it to the list of parsed metadata records + if (PromptModeMetadata.isModeRecord(token)) { + const modeMetadata = new PromptModeMetadata(token); + const { diagnostics } = modeMetadata; + + this.issues.push(...diagnostics); + this.meta.mode = modeMetadata; + this.recordNames.add(recordName); + return; + } + // all other records are currently not supported this.issues.push( new PromptMetadataWarning( From ad2116380ef06d8f886622953697373ed62d28fc Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 09:58:02 -0700 Subject: [PATCH 013/114] add logic to validate compatibility of the `tools` and `mode` metadata records --- .../parsers/promptHeader/header.ts | 93 ++++++++++++++++--- .../promptHeader/metadata/description.ts | 6 +- .../parsers/promptHeader/metadata/mode.ts | 12 ++- .../parsers/promptHeader/metadata/record.ts | 5 + .../parsers/promptHeader/metadata/tools.ts | 12 ++- 5 files changed, 105 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts index 625b5c67df8..0a3d8deceb3 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts @@ -12,6 +12,9 @@ import { SimpleToken } from '../../../../../../../editor/common/codecs/simpleCod import { PromptToolsMetadata, PromptModeMetadata, PromptDescriptionMetadata } from './metadata/index.js'; import { FrontMatterRecord } from '../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; import { FrontMatterDecoder, TFrontMatterToken } from '../../../../../../../editor/common/codecs/frontMatterCodec/frontMatterDecoder.js'; +import { ChatMode } from '../../../constants.js'; +import { assertDefined } from '../../../../../../../base/common/types.js'; +import { assert } from '../../../../../../../base/common/assert.js'; /** * Metadata defined in the prompt header. @@ -136,19 +139,6 @@ export class PromptHeader extends Disposable { return; } - // if the record might be a "tools" metadata - // add it to the list of parsed metadata records - if (PromptToolsMetadata.isToolsRecord(token)) { - const toolsMetadata = new PromptToolsMetadata(token); - const { diagnostics } = toolsMetadata; - - this.issues.push(...diagnostics); - this.meta.tools = toolsMetadata; - this.recordNames.add(recordName); - - return; - } - // if the record might be a "description" metadata // add it to the list of parsed metadata records if (PromptDescriptionMetadata.isDescriptionRecord(token)) { @@ -161,6 +151,19 @@ export class PromptHeader extends Disposable { return; } + // if the record might be a "tools" metadata + // add it to the list of parsed metadata records + if (PromptToolsMetadata.isToolsRecord(token)) { + const toolsMetadata = new PromptToolsMetadata(token); + const { diagnostics } = toolsMetadata; + + this.issues.push(...diagnostics); + this.meta.tools = toolsMetadata; + this.recordNames.add(recordName); + + return this.checkToolsAndModeCompatibility(); + } + // if the record might be a "mode" metadata // add it to the list of parsed metadata records if (PromptModeMetadata.isModeRecord(token)) { @@ -170,7 +173,8 @@ export class PromptHeader extends Disposable { this.issues.push(...diagnostics); this.meta.mode = modeMetadata; this.recordNames.add(recordName); - return; + + return this.checkToolsAndModeCompatibility(); } // all other records are currently not supported @@ -186,6 +190,67 @@ export class PromptHeader extends Disposable { ); } + /** + * TODO: @legomushroom + */ + private toolsAndModeCompatible(): boolean { + const { tools, mode } = this.meta; + + // if `mode` is not set or equal to `agent` mode, + // then the tools metadata can have any value so noop + if ((mode === undefined) || (mode.chatMode === ChatMode.Agent)) { + return true; + } + + // if `tools` is not set, then the mode metadata + // can have any value so skip the validation + if (tools === undefined) { + return true; + } + + // in the other cases when `tools` are defined and `mode` is not + // equal to `agent`, then the `tools` and `mode` are incompatible + return false; + } + + /** + * TODO: @legomushroom + */ + private checkToolsAndModeCompatibility(): void { + if (this.toolsAndModeCompatible() === true) { + return; + } + + const { tools, mode } = this.meta; + + // sanity checks on the behavior of the `toolsAndModeCompatible` method + assertDefined( + tools, + 'Tools metadata must have been present.', + ); + assertDefined( + mode, + 'Mode metadata must have been present.', + ); + assert( + mode.chatMode !== ChatMode.Agent, + 'Mode metadata must not be agent mode.', + ); + + this.issues.push( + new PromptMetadataWarning( + mode.range, + localize( + 'prompt.header.metadata.mode.diagnostics.incompatible-with-tools', + "Unknown metadata record '{0}' can only have the '{1}' value if '{2}' record is present so it will be ignored.", + mode.recordName, + ChatMode.Agent, + tools.recordName, + ), + ), + ); + } + /** * Process errors from the underlying front matter decoder. */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts index 7181acacbe9..29ad6c8e48a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts @@ -10,7 +10,7 @@ import { PromptMetadataDiagnostic, PromptMetadataError } from '../diagnostics.js import { FrontMatterRecord, FrontMatterString, FrontMatterToken } from '../../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; /** - * Name of the `description` metadata record in the prompt header. + * Name of the metadata record in the prompt header. */ const RECORD_NAME = 'description'; @@ -18,6 +18,10 @@ const RECORD_NAME = 'description'; * Prompt `description` metadata record inside the prompt header. */ export class PromptDescriptionMetadata extends PromptMetadataRecord { + public override get recordName(): string { + return RECORD_NAME; + } + /** * Private field for tracking all diagnostic issues * related to this metadata record. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts index 06a23c90580..f1f26f130de 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts @@ -28,6 +28,10 @@ const VALID_MODES = Object.freeze([ * Prompt `mode` metadata record inside the prompt header. */ export class PromptModeMetadata extends PromptMetadataRecord { + public override get recordName(): string { + return RECORD_NAME; + } + /** * Private field for tracking all diagnostic issues * related to this metadata record. @@ -44,12 +48,12 @@ export class PromptModeMetadata extends PromptMetadataRecord { /** * Private field for tracking the chat mode value. */ - private modeValue: ChatMode | undefined; + private value: ChatMode | undefined; /** * Chat mode value of the metadata record. */ - public get mode(): ChatMode | undefined { - return this.modeValue; + public get chatMode(): ChatMode | undefined { + return this.value; } constructor( @@ -98,7 +102,7 @@ export class PromptModeMetadata extends PromptMetadataRecord { const validModes: string[] = [...VALID_MODES]; const index = validModes.indexOf(cleanText); if (index !== -1) { - this.modeValue = VALID_MODES[index]; + this.value = VALID_MODES[index]; return; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts index 4bce7f20e02..ab3a8f482a8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts @@ -22,6 +22,11 @@ export abstract class PromptMetadataRecord { public readonly range: Range, ) { } + /** + * TODO: @legomushroom + */ + public abstract get recordName(): string; + /** * List of all `error` issue diagnostics. */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts index ac75d971047..37fa26d3d00 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts @@ -10,14 +10,18 @@ import { PromptMetadataDiagnostic, PromptMetadataError, PromptMetadataWarning } import { FrontMatterArray, FrontMatterRecord, FrontMatterString, FrontMatterToken, FrontMatterValueToken } from '../../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; /** - * Name of the `tools` metadata record in the prompt header. + * Name of the metadata record in the prompt header. */ -const TOOLS_NAME = 'tools'; +const RECORD_NAME = 'tools'; /** * Prompt `tools` metadata record inside the prompt header. */ export class PromptToolsMetadata extends PromptMetadataRecord { + public override get recordName(): string { + return RECORD_NAME; + } + /** * Private field for tracking all diagnostic issues * related to this metadata record. @@ -76,7 +80,7 @@ export class PromptToolsMetadata extends PromptMetadataRecord { localize( 'prompt.header.metadata.tools.diagnostics.invalid-value-type', "Value of the '{0}' metadata must be '{1}', got '{2}.", - TOOLS_NAME, + RECORD_NAME, 'array', valueToken.valueTypeName, ), @@ -165,7 +169,7 @@ export class PromptToolsMetadata extends PromptMetadataRecord { return false; } - if (token.nameToken.text === TOOLS_NAME) { + if (token.nameToken.text === RECORD_NAME) { return true; } From 59268814647fe68ce17312ed42ce5ea74015b7cb Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 10:08:52 -0700 Subject: [PATCH 014/114] return only valid metadata header records from the prompt parsers --- .../common/promptSyntax/parsers/basePromptParser.ts | 13 ++++++++++--- .../promptSyntax/parsers/promptHeader/header.ts | 4 ++-- .../chat/common/promptSyntax/parsers/types.d.ts | 13 ++++++++++--- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index dac5234b67f..3534d9ce1b8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -492,23 +492,30 @@ export class BasePromptParser } /** - * Metadata defined in the prompt header. + * Valid metadata records defined in the prompt header. */ public get metadata(): IPromptMetadata { if (this.header === undefined) { return {}; } - const { metadata } = this.header; + const { metadata, toolsAndModeCompatible } = this.header; if (metadata === undefined) { return {}; } const result: IPromptMetadata = {}; - const { tools, description } = metadata; + const { tools, mode, description } = metadata; if (tools !== undefined) { result.tools = tools.toolNames; + + // copy over the `mode` metadata value only if + // the `tools` and `mode` metadata are compatible, + // otherwise ignore the `mode` metadata completely + if (toolsAndModeCompatible === true) { + result.mode = mode?.chatMode; + } } if (description !== undefined) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts index 0a3d8deceb3..8f8f25668cf 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts @@ -193,7 +193,7 @@ export class PromptHeader extends Disposable { /** * TODO: @legomushroom */ - private toolsAndModeCompatible(): boolean { + public get toolsAndModeCompatible(): boolean { const { tools, mode } = this.meta; // if `mode` is not set or equal to `agent` mode, @@ -217,7 +217,7 @@ export class PromptHeader extends Disposable { * TODO: @legomushroom */ private checkToolsAndModeCompatibility(): void { - if (this.toolsAndModeCompatible() === true) { + if (this.toolsAndModeCompatible === true) { return; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts index b9e8d297a7e..22422e1b04a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ChatMode } from '../../constants.ts'; import { URI } from '../../../../../../base/common/uri.js'; import { ResolveError } from '../../promptFileReferenceErrors.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { IRange, Range } from '../../../../../../editor/common/core/range.js'; -import { IHeaderMetadata } from './promptHeader/header.ts'; /** * A resolve error with a parent prompt URI, if any. @@ -49,19 +49,26 @@ export interface ITopError extends IResolveError { readonly localizedMessage: string; } +// TODO: @legomushroom - remove the `.d.ts` of the file name? + /** * Metadata defined in the prompt header. */ export interface IPromptMetadata { + /** + * Description metadata in the prompt header. + */ + description?: string; + /** * Tools metadata in the prompt header. */ tools?: readonly string[]; /** - * Description metadata in the prompt header. + * Chat mode metadata in the prompt header. */ - description?: string; + mode?: ChatMode; } /** From ac9f91ad7c94d16b7d19d1830d9eb5429ad2be0b Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 10:13:23 -0700 Subject: [PATCH 015/114] refactor to encapsulate the tools/mode metadata compatibility check logic in the prompt header class --- .../promptSyntax/parsers/basePromptParser.ts | 11 ++++------- .../promptSyntax/parsers/promptHeader/header.ts | 14 +++++++++++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index 3534d9ce1b8..dd9acd080d7 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -499,7 +499,7 @@ export class BasePromptParser return {}; } - const { metadata, toolsAndModeCompatible } = this.header; + const { metadata } = this.header; if (metadata === undefined) { return {}; } @@ -509,13 +509,10 @@ export class BasePromptParser const { tools, mode, description } = metadata; if (tools !== undefined) { result.tools = tools.toolNames; + } - // copy over the `mode` metadata value only if - // the `tools` and `mode` metadata are compatible, - // otherwise ignore the `mode` metadata completely - if (toolsAndModeCompatible === true) { - result.mode = mode?.chatMode; - } + if (mode !== undefined) { + result.mode = mode.chatMode; } if (description !== undefined) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts index 8f8f25668cf..322b339f117 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts @@ -53,7 +53,15 @@ export class PromptHeader extends Disposable { * Metadata records. */ public get metadata(): Readonly { - return this.meta; + const result = { ...this.meta }; + + // if the `tools` and `mode` metadata are compatible, + // then drop the `mode` metadata from the result + if (this.toolsAndModeCompatible === false) { + delete result.mode; + } + + return result; } /** @@ -193,7 +201,7 @@ export class PromptHeader extends Disposable { /** * TODO: @legomushroom */ - public get toolsAndModeCompatible(): boolean { + private get toolsAndModeCompatible(): boolean { const { tools, mode } = this.meta; // if `mode` is not set or equal to `agent` mode, @@ -223,7 +231,7 @@ export class PromptHeader extends Disposable { const { tools, mode } = this.meta; - // sanity checks on the behavior of the `toolsAndModeCompatible` method + // sanity checks on the behavior of the `toolsAndModeCompatible` getter assertDefined( tools, 'Tools metadata must have been present.', From 986490ccb67de6bb04b415a507bdd71982412d93 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 10:24:33 -0700 Subject: [PATCH 016/114] add documentation comments --- .../promptSyntax/parsers/promptHeader/header.ts | 14 ++++++++------ .../parsers/promptHeader/metadata/record.ts | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts index 322b339f117..bbe7d0bd166 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts @@ -169,7 +169,7 @@ export class PromptHeader extends Disposable { this.meta.tools = toolsMetadata; this.recordNames.add(recordName); - return this.checkToolsAndModeCompatibility(); + return this.validateToolsAndModeCompatibility(); } // if the record might be a "mode" metadata @@ -182,7 +182,7 @@ export class PromptHeader extends Disposable { this.meta.mode = modeMetadata; this.recordNames.add(recordName); - return this.checkToolsAndModeCompatibility(); + return this.validateToolsAndModeCompatibility(); } // all other records are currently not supported @@ -199,7 +199,8 @@ export class PromptHeader extends Disposable { } /** - * TODO: @legomushroom + * Check if value of `tools` and `mode` metadata + * are compatible with each other. */ private get toolsAndModeCompatible(): boolean { const { tools, mode } = this.meta; @@ -222,9 +223,10 @@ export class PromptHeader extends Disposable { } /** - * TODO: @legomushroom + * Validate that the `tools` and `mode` metadata are compatible + * with each other. If not, add a warning diagnostic. */ - private checkToolsAndModeCompatibility(): void { + private validateToolsAndModeCompatibility(): void { if (this.toolsAndModeCompatible === true) { return; } @@ -250,7 +252,7 @@ export class PromptHeader extends Disposable { mode.range, localize( 'prompt.header.metadata.mode.diagnostics.incompatible-with-tools', - "Unknown metadata record '{0}' can only have the '{1}' value if '{2}' record is present so it will be ignored.", + "Record '{0}' is implied to have the '{1}' value if '{2}' record is present so the specified value will be ignored.", mode.recordName, ChatMode.Agent, tools.recordName, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts index ab3a8f482a8..362031b3628 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts @@ -23,7 +23,7 @@ export abstract class PromptMetadataRecord { ) { } /** - * TODO: @legomushroom + * Name of the metadata record. */ public abstract get recordName(): string; From 17d69fbdc7e0fe534eb6ca59a018d4bb3f7a3eff Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 10:25:42 -0700 Subject: [PATCH 017/114] remove the `.d.ts` prefix on types filename to prevent compilation issues --- .../chat/common/promptSyntax/parsers/{types.d.ts => types.ts} | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) rename src/vs/workbench/contrib/chat/common/promptSyntax/parsers/{types.d.ts => types.ts} (98%) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts similarity index 98% rename from src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts index 22422e1b04a..8b5a2abf9f0 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ChatMode } from '../../constants.ts'; +import { ChatMode } from '../../constants.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ResolveError } from '../../promptFileReferenceErrors.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; @@ -49,8 +49,6 @@ export interface ITopError extends IResolveError { readonly localizedMessage: string; } -// TODO: @legomushroom - remove the `.d.ts` of the file name? - /** * Metadata defined in the prompt header. */ From e33da907db84c4863deadcac490c48db7a40e12c Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 10:50:40 -0700 Subject: [PATCH 018/114] fix header metadata records diagnostic message typos --- .../promptSyntax/parsers/promptHeader/metadata/description.ts | 2 +- .../common/promptSyntax/parsers/promptHeader/metadata/mode.ts | 2 +- .../common/promptSyntax/parsers/promptHeader/metadata/tools.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts index 29ad6c8e48a..c14d94d7430 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts @@ -82,7 +82,7 @@ export class PromptDescriptionMetadata extends PromptMetadataRecord { valueToken.range, localize( 'prompt.header.metadata.description.diagnostics.invalid-value-type', - "Value of the '{0}' metadata must be '{1}', got '{2}.", + "Value of the '{0}' metadata must be '{1}', got '{2}'.", RECORD_NAME, 'string', valueToken.valueTypeName, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts index f1f26f130de..f2f66370d62 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts @@ -85,7 +85,7 @@ export class PromptModeMetadata extends PromptMetadataRecord { valueToken.range, localize( 'prompt.header.metadata.mode.diagnostics.invalid-value-type', - "Value of the '{0}' metadata must be '{1}', got '{2}.", + "Value of the '{0}' metadata must be '{1}', got '{2}'.", RECORD_NAME, 'string', valueToken.valueTypeName, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts index 37fa26d3d00..4fcd8524ae1 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts @@ -79,7 +79,7 @@ export class PromptToolsMetadata extends PromptMetadataRecord { valueToken.range, localize( 'prompt.header.metadata.tools.diagnostics.invalid-value-type', - "Value of the '{0}' metadata must be '{1}', got '{2}.", + "Value of the '{0}' metadata must be '{1}', got '{2}'.", RECORD_NAME, 'array', valueToken.valueTypeName, From 9c6f9e7d5a9ee8219855c3ef00ed1715cd797f93 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 10:56:15 -0700 Subject: [PATCH 019/114] add unit tests for the new `mode` prompt header metadata --- .../parsers/textModelPromptParser.test.ts | 107 +++++++++++------- .../promptSyntax/promptFileReference.test.ts | 15 +-- 2 files changed, 71 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index 32f0d97749e..26616d489f4 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -18,7 +18,7 @@ import { randomBoolean } from '../../../../../../../base/test/common/testUtils.j import { FileService } from '../../../../../../../platform/files/common/fileService.js'; import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; -import { ExpectedDiagnosticWarning, TExpectedDiagnostic } from '../testUtils/expectedDiagnostic.js'; +import { ExpectedDiagnosticError, ExpectedDiagnosticWarning, TExpectedDiagnostic } from '../testUtils/expectedDiagnostic.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { TextModelPromptParser } from '../../../../common/promptSyntax/parsers/textModelPromptParser.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; @@ -121,7 +121,13 @@ class TextModelPromptParserTest extends Disposable { `Expected diagnostic #${i} be ${expectedDiagnostics[i]}, got 'undefined'.`, ); - expectedDiagnostics[i].validateEqual(diagnostic); + try { + expectedDiagnostics[i].validateEqual(diagnostic); + } catch (_error) { + throw new Error( + `Expected diagnostic #${i} to be ${expectedDiagnostics[i]}, got '${diagnostic}'.`, + ); + } } assert.strictEqual( @@ -277,16 +283,18 @@ suite('TextModelPromptParser', () => { createURI('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", - /* 02 */" something: true", /* unknown metadata record */ - /* 03 */" tools: [ 'tool_name1', \"tool_name2\", 'tool_name1', true, false, '', 'tool_name2' ]\t\t", - /* 04 */" tools: [ 'tool_name3', \"tool_name4\" ]", /* duplicate `tools` record is ignored */ - /* 05 */" tools: 'tool_name5'", /* duplicate `tools` record with invalid value is ignored */ - /* 06 */"---", - /* 07 */"The cactus on my desk has a thriving Instagram account.", - /* 08 */"Midnight snacks are the secret to eternal [text](./foo-bar-baz/another-file.ts) happiness.", - /* 09 */"In an alternate universe, pigeons deliver sushi by drone.", - /* 10 */"Lunar rainbows only appear when you sing in falsetto.", - /* 11 */"Carrots have secret telepathic abilities, but only on Tuesdays.", + /* 02 */"description: 'My prompt.'\t\t", + /* 03 */" something: true", /* unknown metadata record */ + /* 04 */" tools: [ 'tool_name1', \"tool_name2\", 'tool_name1', true, false, '', 'tool_name2' ]\t\t", + /* 05 */" tools: [ 'tool_name3', \"tool_name4\" ]", /* duplicate `tools` record is ignored */ + /* 06 */" tools: 'tool_name5'", /* duplicate `tools` record with invalid value is ignored */ + /* 07 */" mode: 'agent'", + /* 08 */"---", + /* 09 */"The cactus on my desk has a thriving Instagram account.", + /* 10 */"Midnight snacks are the secret to eternal [text](./foo-bar-baz/another-file.ts) happiness.", + /* 11 */"In an alternate universe, pigeons deliver sushi by drone.", + /* 12 */"Lunar rainbows only appear when you sing in falsetto.", + /* 13 */"Carrots have secret telepathic abilities, but only on Tuesdays.", ], ); @@ -295,7 +303,7 @@ suite('TextModelPromptParser', () => { uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), text: '[text](./foo-bar-baz/another-file.ts)', path: './foo-bar-baz/another-file.ts', - startLine: 8, + startLine: 10, startColumn: 43, pathStartColumn: 50, childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), @@ -308,23 +316,24 @@ suite('TextModelPromptParser', () => { 'Prompt header must be defined.', ); - const { tools } = metadata; - assertDefined( - tools, - 'Tools metadata must be present.', - ); - - assert.strictEqual( - tools.length, - 2, - `Prompt header tools metadata must have 2 tool names, got '[${tools.join(', ')}]'.`, - ); - + const { tools, mode, description } = metadata; assert.deepStrictEqual( tools, ['tool_name1', 'tool_name2'], `Prompt header must have correct tools metadata.`, ); + + assert.strictEqual( + mode, + 'agent', + `Prompt header must have correct mode metadata.`, + ); + + assert.strictEqual( + description, + 'My prompt.', + `Prompt header must have correct description metadata.`, + ); }); test('• has correct diagnostics', async function () { @@ -332,16 +341,18 @@ suite('TextModelPromptParser', () => { createURI('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", - /* 02 */" something: true", /* unknown metadata record */ - /* 03 */"tools: [ 'tool_name1', \"tool_name2\", 'tool_name1', true, false, '', ,'tool_name2' ] ", - /* 04 */" tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ - /* 05 */"tools: 'tool_name5'", /* duplicate `tools` record with invalid value is ignored */ - /* 06 */"---", - /* 07 */"The cactus on my desk has a thriving Instagram account.", - /* 08 */"Midnight snacks are the secret to eternal [text](./foo-bar-baz/another-file.ts) happiness.", - /* 09 */"In an alternate universe, pigeons deliver sushi by drone.", - /* 10 */"Lunar rainbows only appear when you sing in falsetto.", - /* 11 */"Carrots have secret telepathic abilities, but only on Tuesdays.", + /* 02 */" description: true \t ", + /* 03 */" mode: \"ask\"", + /* 04 */" something: true", /* unknown metadata record */ + /* 05 */"tools: [ 'tool_name1', \"tool_name2\", 'tool_name1', true, false, '', ,'tool_name2' ] ", + /* 06 */" tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ + /* 07 */"tools: 'tool_name5'", /* duplicate `tools` record with invalid value is ignored */ + /* 08 */"---", + /* 09 */"The cactus on my desk has a thriving Instagram account.", + /* 10 */"Midnight snacks are the secret to eternal [text](./foo-bar-baz/another-file.ts) happiness.", + /* 11 */"In an alternate universe, pigeons deliver sushi by drone.", + /* 12 */"Lunar rainbows only appear when you sing in falsetto.", + /* 13 */"Carrots have secret telepathic abilities, but only on Tuesdays.", ], ); @@ -350,7 +361,7 @@ suite('TextModelPromptParser', () => { uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), text: '[text](./foo-bar-baz/another-file.ts)', path: './foo-bar-baz/another-file.ts', - startLine: 8, + startLine: 10, startColumn: 43, pathStartColumn: 50, childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), @@ -370,36 +381,44 @@ suite('TextModelPromptParser', () => { ); await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticError( + new Range(2, 15, 2, 15 + 4), + 'Value of the \'description\' metadata must be \'string\', got \'boolean\'.', + ), new ExpectedDiagnosticWarning( - new Range(2, 2, 2, 2 + 15), + new Range(4, 2, 4, 2 + 15), 'Unknown metadata record \'something\' will be ignored.', ), new ExpectedDiagnosticWarning( - new Range(3, 38, 3, 38 + 12), + new Range(5, 38, 5, 38 + 12), 'Duplicate tool name \'tool_name1\'.', ), new ExpectedDiagnosticWarning( - new Range(3, 52, 3, 52 + 4), + new Range(5, 52, 5, 52 + 4), 'Expected a tool name (string), got \'true\'.', ), new ExpectedDiagnosticWarning( - new Range(3, 58, 3, 58 + 5), + new Range(5, 58, 5, 58 + 5), 'Expected a tool name (string), got \'false\'.', ), new ExpectedDiagnosticWarning( - new Range(3, 65, 3, 65 + 2), + new Range(5, 65, 5, 65 + 2), 'Tool name cannot be empty.', ), new ExpectedDiagnosticWarning( - new Range(3, 70, 3, 70 + 12), + new Range(5, 70, 5, 70 + 12), 'Duplicate tool name \'tool_name2\'.', ), new ExpectedDiagnosticWarning( - new Range(4, 3, 4, 3 + 37), + new Range(3, 2, 3, 2 + 11), + 'Record \'mode\' is implied to have the \'agent\' value if \'tools\' record is present so the specified value will be ignored.', + ), + new ExpectedDiagnosticWarning( + new Range(6, 3, 6, 3 + 37), 'Duplicate metadata record \'tools\' will be ignored.', ), new ExpectedDiagnosticWarning( - new Range(5, 1, 5, 1 + 19), + new Range(7, 1, 7, 1 + 19), 'Duplicate metadata record \'tools\' will be ignored.', ), ]); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index 7b9a8781803..4b45d4d06ae 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -732,6 +732,7 @@ suite('PromptFileReference (Unix)', function () { '---', 'description: \'Root prompt description.\'', 'tools: [\'my-tool1\']', + 'mode: "agent" ', '---', '## Files', '\t- this file #file:folder1/file3.prompt.md ', @@ -763,6 +764,7 @@ suite('PromptFileReference (Unix)', function () { '---', 'tools: [\'my-tool1\', "my-tool2", true, , ]', 'something: true', + 'mode: \'ask\'\t', '---', 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', '', @@ -810,7 +812,7 @@ suite('PromptFileReference (Unix)', function () { [ new ExpectedReference( rootUri, - createTestFileReference('folder1/file3.prompt.md', 6, 14), + createTestFileReference('folder1/file3.prompt.md', 7, 14), ), new ExpectedReference( URI.joinPath(rootUri, './folder1'), @@ -827,8 +829,7 @@ suite('PromptFileReference (Unix)', function () { URI.joinPath(rootUri, './folder1'), createTestFileReference( `/${rootFolderName}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md`, - 6, - 26, + 6, 26, ), ), new ExpectedReference( @@ -856,13 +857,13 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( rootUri, new MarkdownLink( - 7, 14, + 8, 14, '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', ), ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), - createTestFileReference('./some-non-existing/file.prompt.md', 5, 30), + createTestFileReference('./some-non-existing/file.prompt.md', 6, 30), new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), 'Failed to open non-existing prompt snippets file', @@ -870,7 +871,7 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), - createTestFileReference('./some-non-prompt-file.md', 9, 13), + createTestFileReference('./some-non-prompt-file.md', 10, 13), new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.md'), 'Oh no!', @@ -879,7 +880,7 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( URI.joinPath(rootUri, './some-other-folder/folder1'), new MarkdownLink( - 9, 48, + 10, 48, '[]', '(../../folder1/)', ), new FolderReference( From 647a61606802eed58a5948116014e21d1d8bd410 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 11:20:15 -0700 Subject: [PATCH 020/114] add unit tests for the compatibility checks of tools and mode metadata --- .../parsers/textModelPromptParser.test.ts | 471 +++++++++++++++--- 1 file changed, 400 insertions(+), 71 deletions(-) diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index 26616d489f4..d0f550dbd7e 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import { createURI } from '../testUtils/createUri.js'; +import { ChatMode } from '../../../../common/constants.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import { ExpectedReference } from '../testUtils/expectedReference.js'; @@ -18,11 +19,11 @@ import { randomBoolean } from '../../../../../../../base/test/common/testUtils.j import { FileService } from '../../../../../../../platform/files/common/fileService.js'; import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; -import { ExpectedDiagnosticError, ExpectedDiagnosticWarning, TExpectedDiagnostic } from '../testUtils/expectedDiagnostic.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { TextModelPromptParser } from '../../../../common/promptSyntax/parsers/textModelPromptParser.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { InMemoryFileSystemProvider } from '../../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { ExpectedDiagnosticError, ExpectedDiagnosticWarning, TExpectedDiagnostic } from '../testUtils/expectedDiagnostic.js'; import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; /** @@ -71,6 +72,13 @@ class TextModelPromptParserTest extends Disposable { ).start(); } + /** + * Wait for the prompt parsing/resolve process to finish. + */ + public allSettled(): Promise { + return this.parser.allSettled(); + } + /** * Validate the current state of the parser. */ @@ -278,7 +286,7 @@ suite('TextModelPromptParser', () => { }); suite('• header', () => { - test('• has correct metadata', async function () { + test('• has correct metadata', async () => { const test = createTest( createURI('/absolute/folder/and/a/filename.txt'), [ @@ -336,10 +344,11 @@ suite('TextModelPromptParser', () => { ); }); - test('• has correct diagnostics', async function () { - const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), - [ + suite('• diagnostics', () => { + test('• core logic', async () => { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ /* 01 */"---", /* 02 */" description: true \t ", /* 03 */" mode: \"ask\"", @@ -353,75 +362,395 @@ suite('TextModelPromptParser', () => { /* 11 */"In an alternate universe, pigeons deliver sushi by drone.", /* 12 */"Lunar rainbows only appear when you sing in falsetto.", /* 13 */"Carrots have secret telepathic abilities, but only on Tuesdays.", - ], - ); + ], + ); - await test.validateReferences([ - new ExpectedReference({ - uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), - text: '[text](./foo-bar-baz/another-file.ts)', - path: './foo-bar-baz/another-file.ts', - startLine: 10, - startColumn: 43, - pathStartColumn: 50, - childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), - }), - ]); + await test.validateReferences([ + new ExpectedReference({ + uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + text: '[text](./foo-bar-baz/another-file.ts)', + path: './foo-bar-baz/another-file.ts', + startLine: 10, + startColumn: 43, + pathStartColumn: 50, + childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + }), + ]); - const { header, metadata } = test.parser; - assertDefined( - header, - 'Prompt header must be defined.', - ); + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); - const { tools } = metadata; - assertDefined( - tools, - 'Tools metadata must be defined.', - ); + const { tools } = metadata; + assertDefined( + tools, + 'Tools metadata must be defined.', + ); - await test.validateHeaderDiagnostics([ - new ExpectedDiagnosticError( - new Range(2, 15, 2, 15 + 4), - 'Value of the \'description\' metadata must be \'string\', got \'boolean\'.', - ), - new ExpectedDiagnosticWarning( - new Range(4, 2, 4, 2 + 15), - 'Unknown metadata record \'something\' will be ignored.', - ), - new ExpectedDiagnosticWarning( - new Range(5, 38, 5, 38 + 12), - 'Duplicate tool name \'tool_name1\'.', - ), - new ExpectedDiagnosticWarning( - new Range(5, 52, 5, 52 + 4), - 'Expected a tool name (string), got \'true\'.', - ), - new ExpectedDiagnosticWarning( - new Range(5, 58, 5, 58 + 5), - 'Expected a tool name (string), got \'false\'.', - ), - new ExpectedDiagnosticWarning( - new Range(5, 65, 5, 65 + 2), - 'Tool name cannot be empty.', - ), - new ExpectedDiagnosticWarning( - new Range(5, 70, 5, 70 + 12), - 'Duplicate tool name \'tool_name2\'.', - ), - new ExpectedDiagnosticWarning( - new Range(3, 2, 3, 2 + 11), - 'Record \'mode\' is implied to have the \'agent\' value if \'tools\' record is present so the specified value will be ignored.', - ), - new ExpectedDiagnosticWarning( - new Range(6, 3, 6, 3 + 37), - 'Duplicate metadata record \'tools\' will be ignored.', - ), - new ExpectedDiagnosticWarning( - new Range(7, 1, 7, 1 + 19), - 'Duplicate metadata record \'tools\' will be ignored.', - ), - ]); + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticError( + new Range(2, 15, 2, 15 + 4), + 'Value of the \'description\' metadata must be \'string\', got \'boolean\'.', + ), + new ExpectedDiagnosticWarning( + new Range(4, 2, 4, 2 + 15), + 'Unknown metadata record \'something\' will be ignored.', + ), + new ExpectedDiagnosticWarning( + new Range(5, 38, 5, 38 + 12), + 'Duplicate tool name \'tool_name1\'.', + ), + new ExpectedDiagnosticWarning( + new Range(5, 52, 5, 52 + 4), + 'Expected a tool name (string), got \'true\'.', + ), + new ExpectedDiagnosticWarning( + new Range(5, 58, 5, 58 + 5), + 'Expected a tool name (string), got \'false\'.', + ), + new ExpectedDiagnosticWarning( + new Range(5, 65, 5, 65 + 2), + 'Tool name cannot be empty.', + ), + new ExpectedDiagnosticWarning( + new Range(5, 70, 5, 70 + 12), + 'Duplicate tool name \'tool_name2\'.', + ), + new ExpectedDiagnosticWarning( + new Range(3, 2, 3, 2 + 11), + 'Record \'mode\' is implied to have the \'agent\' value if \'tools\' record is present so the specified value will be ignored.', + ), + new ExpectedDiagnosticWarning( + new Range(6, 3, 6, 3 + 37), + 'Duplicate metadata record \'tools\' will be ignored.', + ), + new ExpectedDiagnosticWarning( + new Range(7, 1, 7, 1 + 19), + 'Duplicate metadata record \'tools\' will be ignored.', + ), + ]); + }); + + suite('• tools and mode compatibility', () => { + suite('• tools is set', () => { + test('• ask mode', async () => { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ + /* 03 */"mode: \"ask\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assertDefined( + tools, + 'Tools metadata must be defined.', + ); + + assert.strictEqual( + mode, + undefined, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticWarning( + new Range(3, 1, 3, 1 + 11), + 'Record \'mode\' is implied to have the \'agent\' value if \'tools\' record is present so the specified value will be ignored.', + ), + ]); + }); + + test('• edit mode', async () => { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ + /* 03 */"mode: \"edit\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assertDefined( + tools, + 'Tools metadata must be defined.', + ); + + assert.strictEqual( + mode, + undefined, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticWarning( + new Range(3, 1, 3, 1 + 12), + 'Record \'mode\' is implied to have the \'agent\' value if \'tools\' record is present so the specified value will be ignored.', + ), + ]); + }); + + test('• agent mode', async () => { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ + /* 03 */"mode: \"agent\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assertDefined( + tools, + 'Tools metadata must be defined.', + ); + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([]); + }); + + test('• no mode', async () => { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ + /* 03 */"---", + /* 04 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assertDefined( + tools, + 'Tools metadata must be defined.', + ); + + assert.strictEqual( + mode, + undefined, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([]); + }); + }); + + suite('• tools is not set', () => { + test('• ask mode', async () => { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"description: ['my prompt', 'description.']", + /* 03 */"mode: \"ask\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assert( + tools === undefined, + 'Tools metadata must not be defined.', + ); + + assert.strictEqual( + mode, + ChatMode.Ask, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticError( + new Range(2, 14, 2, 14 + 29), + 'Value of the \'description\' metadata must be \'string\', got \'array\'.', + ), + ]); + }); + + test('• edit mode', async () => { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"description: my prompt description. \t\t \t\t ", + /* 03 */"mode: \"edit\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assert( + tools === undefined, + 'Tools metadata must not be defined.', + ); + + assert.strictEqual( + mode, + ChatMode.Edit, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticError( + new Range(2, 1, 2, 1 + 11), + 'Unexpected token \'description\'.', + ), + new ExpectedDiagnosticError( + new Range(2, 12, 2, 12 + 2), + 'Unexpected token \': \'.', + ), + new ExpectedDiagnosticError( + new Range(2, 14, 2, 14 + 2), + 'Unexpected token \'my\'.', + ), + new ExpectedDiagnosticError( + new Range(2, 17, 2, 17 + 6), + 'Unexpected token \'prompt\'.', + ), + new ExpectedDiagnosticError( + new Range(2, 24, 2, 24 + 12), + 'Unexpected token \'description.\'.', + ), + ]); + }); + + test('• agent mode', async () => { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"mode: \"agent\"", + /* 03 */"---", + /* 04 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assert( + tools === undefined, + 'Tools metadata must not be defined.', + ); + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([]); + }); + + test('• no mode', async () => { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"description: 'My prompt.'", + /* 03 */"---", + /* 04 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assert( + tools === undefined, + 'Tools metadata must not be defined.', + ); + + assert.strictEqual( + mode, + undefined, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([]); + }); + }); + }); }); }); From d87794b47eca90eff32249ac2aa12e21ae0cdb28 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 11:44:46 -0700 Subject: [PATCH 021/114] add logic to make a root prompt to define resulting mode of the entire nested references tree --- .../promptSyntax/parsers/basePromptParser.ts | 13 +- .../promptSyntax/promptFileReference.test.ts | 513 +++++++++++++++++- 2 files changed, 521 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index dd9acd080d7..c5125e167a1 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -30,6 +30,7 @@ import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCod import { MarkdownToken } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownToken.js'; import { FrontMatterHeader } from '../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; import { OpenFailed, NotPromptFile, RecursiveReference, FolderReference, ResolveError } from '../../promptFileReferenceErrors.js'; +import { ChatMode } from '../../constants.js'; /** * Error conditions that may happen during the file reference resolution. @@ -530,13 +531,23 @@ export class BasePromptParser let hasTools = false; const result: string[] = []; - const { tools } = this.metadata; + const { tools, mode } = this.metadata; if (tools !== undefined) { result.push(...tools); hasTools = true; } + const isRootInAgentMode = ((hasTools === true) || (mode === ChatMode.Agent)); + + // the top-level mode defines the overall mode for all + // nested prompt references, therefore if mode of + // the top-level prompt is not equal to `agent`, then + // ignore all `tools` metadata of the nested references + if (isRootInAgentMode === false) { + return null; + } + for (const reference of this.references) { const { allToolsMetadata } = reference; diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index 4b45d4d06ae..f72cec2e951 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { ChatMode } from '../../../common/constants.js'; import { URI } from '../../../../../../base/common/uri.js'; import { Schemas } from '../../../../../../base/common/network.js'; import { extUri } from '../../../../../../base/common/resources.js'; import { isWindows } from '../../../../../../base/common/platform.js'; import { Range } from '../../../../../../editor/common/core/range.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { IMockFolder, MockFilesystem } from './testUtils/mockFilesystem.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; @@ -29,7 +31,6 @@ import { InMemoryFileSystemProvider } from '../../../../../../platform/files/com import { IFileContentsProviderOptions } from '../../../common/promptSyntax/contentProviders/filePromptContentsProvider.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { NotPromptFile, RecursiveReference, OpenFailed, FolderReference } from '../../../common/promptFileReferenceErrors.js'; -import { assertDefined } from '../../../../../../base/common/types.js'; /** * Represents a file reference with an expected @@ -899,13 +900,13 @@ suite('PromptFileReference (Unix)', function () { assert.deepStrictEqual( tools, ['my-tool1'], - 'Must have correct tools metadata', + 'Must have correct tools metadata.', ); assert.deepStrictEqual( description, 'Root prompt description.', - 'Must have correct description metadata', + 'Must have correct description metadata.', ); assertDefined( @@ -915,8 +916,512 @@ suite('PromptFileReference (Unix)', function () { assert.deepStrictEqual( allToolsMetadata, ['my-tool1', 'my-tool3', 'my-tool2'], - 'Must have correct all tools metadata', + 'Must have correct all tools metadata.', ); }); + + suite('• tools and mode compatibility', () => { + test('• tools are ignored if root prompt in the ask mode', async function () { + if (isWindows) { + this.skip(); + } + + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.prompt.md', + contents: [ + '---', + 'description: \'Description of my prompt.\'', + 'mode: "ask" ', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + 'mode: \'agent\'\t', + '---', + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , ]', + 'something: true', + 'mode: \'ask\'\t', + '---', + '', + '', + 'and some more content', + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.prompt.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 6, 14), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 7, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, mode, description } = metadata; + + assert.deepStrictEqual( + tools, + undefined, + 'Must have correct tools metadata.', + ); + + assert.deepStrictEqual( + mode, + ChatMode.Ask, + 'Must have correct tools metadata.', + ); + + assert.deepStrictEqual( + description, + 'Description of my prompt.', + 'Must have correct description metadata.', + ); + + assert.deepStrictEqual( + allToolsMetadata, + null, + 'Must have correct all tools metadata.', + ); + }); + + test('• tools are ignored if root prompt in the edit mode', async function () { + if (isWindows) { + this.skip(); + } + + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.prompt.md', + contents: [ + '---', + 'description: \'Description of my prompt.\'', + 'mode:\t\t"edit"\t\t', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , ]', + 'something: true', + 'mode: \'agent\'\t', + '---', + '', + '', + 'and some more content', + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.prompt.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 6, 14), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 7, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, mode, description } = metadata; + + assert.deepStrictEqual( + tools, + undefined, + 'Must have correct tools metadata.', + ); + + assert.deepStrictEqual( + mode, + ChatMode.Edit, + 'Must have correct tools metadata.', + ); + + assert.deepStrictEqual( + description, + 'Description of my prompt.', + 'Must have correct description metadata.', + ); + + assert.deepStrictEqual( + allToolsMetadata, + null, + 'Must have correct all tools metadata.', + ); + }); + + test('• tools are not ignored if root prompt in the agent mode', async function () { + if (isWindows) { + this.skip(); + } + + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.prompt.md', + contents: [ + '---', + 'description: \'Description of my prompt.\'', + 'mode: \t\t "agent" \t\t ', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , \'my-tool3\' , ]', + 'something: true', + 'mode: \'agent\'\t', + '---', + '', + '', + 'and some more content', + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.prompt.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 6, 14), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 7, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, mode, description } = metadata; + + assert.deepStrictEqual( + tools, + undefined, + 'Must have correct tools metadata.', + ); + + assert.deepStrictEqual( + mode, + ChatMode.Agent, + 'Must have correct tools metadata.', + ); + + assert.deepStrictEqual( + description, + 'Description of my prompt.', + 'Must have correct description metadata.', + ); + + assert.deepStrictEqual( + allToolsMetadata, + [ + 'my-tool1', + 'my-tool2', + 'my-tool3', + ], + 'Must have correct all tools metadata.', + ); + }); + + test('• tools are not ignored if root prompt implicitly in the agent mode', async function () { + if (isWindows) { + this.skip(); + } + + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool12\' , ]', + 'description: \'Description of my prompt.\'', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , \'my-tool3\' , ]', + 'something: true', + 'mode: \'agent\'\t', + '---', + '', + '', + 'and some more content', + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.prompt.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 6, 14), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 7, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, mode, description } = metadata; + + assert.deepStrictEqual( + tools, + ['my-tool12'], + 'Must have correct tools metadata.', + ); + + assert.deepStrictEqual( + mode, + undefined, + 'Must have correct tools metadata.', + ); + + assert.deepStrictEqual( + description, + 'Description of my prompt.', + 'Must have correct description metadata.', + ); + + assert.deepStrictEqual( + allToolsMetadata, + [ + 'my-tool12', + 'my-tool1', + 'my-tool2', + 'my-tool3', + ], + 'Must have correct all tools metadata.', + ); + }); + }); }); }); From 56003255441227df8370cc4742477ecc11fcdf24 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 12:28:13 -0700 Subject: [PATCH 022/114] add more unit tests, make the resulting `mode` metadata to be always present and be conditioned on the `tools` metadata --- .../promptSyntax/parsers/basePromptParser.ts | 29 +++++++++++++------ .../parsers/promptHeader/header.ts | 18 ++++-------- .../chat/common/promptSyntax/parsers/types.ts | 2 +- .../parsers/textModelPromptParser.test.ts | 8 ++--- .../promptSyntax/promptFileReference.test.ts | 2 +- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index c5125e167a1..f87b2d79e7c 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -497,29 +497,40 @@ export class BasePromptParser */ public get metadata(): IPromptMetadata { if (this.header === undefined) { - return {}; + return { + mode: ChatMode.Ask, + }; } const { metadata } = this.header; if (metadata === undefined) { - return {}; + return { + mode: ChatMode.Ask, + }; } - const result: IPromptMetadata = {}; const { tools, mode, description } = metadata; - if (tools !== undefined) { - result.tools = tools.toolNames; - } - if (mode !== undefined) { - result.mode = mode.chatMode; - } + // compute resulting mode based on presence + // of `tools` metadata in the prompt header + const resultingMode = (tools !== undefined) + ? ChatMode.Agent + : mode?.chatMode; + + // fallback to `ask` mode if no mode is defined + const result: IPromptMetadata = { + mode: resultingMode ?? ChatMode.Ask, + }; if (description !== undefined) { result.description = description.text ?? undefined; } + if (tools !== undefined) { + result.tools = tools.toolNames; + } + return result; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts index bbe7d0bd166..711253709ce 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts @@ -3,7 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ChatMode } from '../../../constants.js'; import { localize } from '../../../../../../../nls.js'; +import { assert } from '../../../../../../../base/common/assert.js'; +import { assertDefined } from '../../../../../../../base/common/types.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { Text } from '../../../../../../../editor/common/codecs/baseToken.js'; import { PromptMetadataError, PromptMetadataWarning, TDiagnostic } from './diagnostics.js'; @@ -12,9 +15,6 @@ import { SimpleToken } from '../../../../../../../editor/common/codecs/simpleCod import { PromptToolsMetadata, PromptModeMetadata, PromptDescriptionMetadata } from './metadata/index.js'; import { FrontMatterRecord } from '../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; import { FrontMatterDecoder, TFrontMatterToken } from '../../../../../../../editor/common/codecs/frontMatterCodec/frontMatterDecoder.js'; -import { ChatMode } from '../../../constants.js'; -import { assertDefined } from '../../../../../../../base/common/types.js'; -import { assert } from '../../../../../../../base/common/assert.js'; /** * Metadata defined in the prompt header. @@ -53,15 +53,9 @@ export class PromptHeader extends Disposable { * Metadata records. */ public get metadata(): Readonly { - const result = { ...this.meta }; - - // if the `tools` and `mode` metadata are compatible, - // then drop the `mode` metadata from the result - if (this.toolsAndModeCompatible === false) { - delete result.mode; - } - - return result; + return Object.freeze({ + ...this.meta, + }); } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts index 8b5a2abf9f0..0d10f67183c 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts @@ -66,7 +66,7 @@ export interface IPromptMetadata { /** * Chat mode metadata in the prompt header. */ - mode?: ChatMode; + mode: ChatMode; } /** diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index d0f550dbd7e..b59cf01c2ce 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -463,7 +463,7 @@ suite('TextModelPromptParser', () => { assert.strictEqual( mode, - undefined, + ChatMode.Agent, 'Mode metadata must have correct value.', ); @@ -503,7 +503,7 @@ suite('TextModelPromptParser', () => { assert.strictEqual( mode, - undefined, + ChatMode.Agent, 'Mode metadata must have correct value.', ); @@ -577,7 +577,7 @@ suite('TextModelPromptParser', () => { assert.strictEqual( mode, - undefined, + ChatMode.Agent, 'Mode metadata must have correct value.', ); @@ -743,7 +743,7 @@ suite('TextModelPromptParser', () => { assert.strictEqual( mode, - undefined, + ChatMode.Ask, 'Mode metadata must have correct value.', ); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index f72cec2e951..14c1a7b7ab4 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -1401,7 +1401,7 @@ suite('PromptFileReference (Unix)', function () { assert.deepStrictEqual( mode, - undefined, + ChatMode.Agent, 'Must have correct tools metadata.', ); From 8143099f6d0c5b602612f8b0271603aceae3d1b6 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 13:27:54 -0700 Subject: [PATCH 023/114] fix run command for implicit prompt file context --- .../chat/browser/contrib/chatImplicitContext.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index 20da77d613a..e6f72768b7d 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -20,7 +20,7 @@ import { EditorsOrder } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { getNotebookEditorFromEditorPane, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; -import { IChatRequestFileEntry, IChatRequestImplicitVariableEntry } from '../../common/chatModel.js'; +import { IChatRequestFileEntry, IChatRequestImplicitVariableEntry, IPromptVariableEntry } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; @@ -340,13 +340,22 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli this._onDidChangeValue.fire(); } - toBaseEntry(): IChatRequestFileEntry { - return { + toBaseEntry(): IChatRequestFileEntry | IPromptVariableEntry { + const result: IChatRequestFileEntry = { kind: 'file', id: this.id, name: this.name, value: this.value, - modelDescription: this.modelDescription + modelDescription: this.modelDescription, + }; + + if (this.isPromptFile === false) { + return result; + } + + return { + ...result, + isRoot: true, }; } } From 52592e3ca8f6c18d612907245e809ddd24f76291 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Mon, 21 Apr 2025 13:49:22 -0700 Subject: [PATCH 024/114] compute and use chat mode inside chat widget --- .../contrib/chat/browser/chatInputPart.ts | 8 + .../contrib/chat/browser/chatWidget.ts | 157 ++++++++++++------ 2 files changed, 116 insertions(+), 49 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index d33d5a96808..ce2afe94957 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -182,9 +182,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ); } + // prompt files may have nested child references to other prompt + // files that are resolved asynchronously, hence we need to wait + // for the entire prompt instruction tree to be processed + const instructionsStarted = performance.now(); + // wait for all prompt files resolve precesses to settle await this.promptInstructionsAttachmentsPart.allSettled(); + // allow-any-unicode-next-line + this.logService.trace(`[⏱] instructions tree resolved in ${performance.now() - instructionsStarted}ms`); + contextArr .push(...this.promptInstructionsAttachmentsPart.chatAttachments); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 40d3f741b27..c0ad2d6e050 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1178,19 +1178,10 @@ export class ChatWidget extends Disposable implements IChatWidget { let input = !query ? editorValue : query.query; const isUserQuery = !query; + let attachedContext = await this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); + const { promptInstructions } = this.inputPart.attachmentModel; const instructionsEnabled = promptInstructions.featureEnabled; - if (instructionsEnabled) { - // prompt files may have nested child references to other prompt - // files that are resolved asynchronously, hence we need to wait - // for the entire prompt instruction tree to be processed - const instructionsStarted = performance.now(); - await promptInstructions.allSettled(); - // allow-any-unicode-next-line - this.logService.trace(`[⏱] instructions tree resolved in ${performance.now() - instructionsStarted}ms`); - } - - let attachedContext = await this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); if (instructionsEnabled) { const result = await this._handlePromptSlashCommand(); if (result.input !== undefined) { @@ -1201,29 +1192,9 @@ export class ChatWidget extends Disposable implements IChatWidget { attachedContext.push(result.attachment); } - const promptFileVariables = attachedContext.filter(isPromptFileChatVariable); - if (promptFileVariables.length > 0) { - input = `Follow the prompt instructions from ${promptFileVariables.map(v => v.name).join(', ')}\n${input}`; - - const allToolsMetadata = await this.getPromptFileToolsMetadata(promptFileVariables); - - // if there are some tools defined in the prompt files, switch to - // the agent mode and select only the specified tools - if ((allToolsMetadata !== null) && (allToolsMetadata.length > 0)) { - const options: IToggleChatModeArgs = { mode: ChatMode.Agent }; - - // tools are currently only available in agent mode hence - // switch to the mode before updating the selected tools - await this.commandService.executeCommand( - ToggleAgentModeActionId, options, - ); - - // update the selected tools - this.inputPart - .selectedToolsModel - .selectOnly(allToolsMetadata); - } - } + await this.setupChatModeAndTools( + attachedContext.filter(isPromptFileChatVariable), + ); } if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentMode === ChatMode.Edit && !this.chatService.edits2Enabled) { @@ -1484,11 +1455,64 @@ export class ChatWidget extends Disposable implements IChatWidget { } /** - * Gets a list of all tools specified in the provided prompt files. + * Set's up the `chat mode` and selects required `tools` based on + * the metadata defined in headers of attached prompt files. */ - private async getPromptFileToolsMetadata( + private async setupChatModeAndTools( + promptFileVariables: readonly IPromptVariableEntry[], + ): Promise { + if (promptFileVariables.length === 0) { + return; + } + + const metadata = await this.getPromptFilesMetadata(promptFileVariables); + if (metadata !== null) { + const { allToolsMetadata, resultingChatMode } = metadata; + + // switch to appropriate chat mode if needed + if (resultingChatMode !== this.inputPart.currentMode) { + const toggleModeOptions: IToggleChatModeArgs = { + mode: resultingChatMode, + }; + + await this.commandService.executeCommand( + ToggleAgentModeActionId, toggleModeOptions, + ); + } + + // if there are some tools defined in the prompt files, switch to + // the agent mode and select only the specified tools + if ((allToolsMetadata !== undefined) && (allToolsMetadata.length > 0)) { + // sanity check on the logic of the `getPromptFilesMetadata` method + // and the code above in case this block is moved around somewhere else + assert( + this.inputPart.currentMode === ChatMode.Agent, + `Chat mode must be 'agent' when there are 'tools' defined, got ${this.inputPart.currentMode}.`, + ); + + // update the selected tools + this.inputPart + .selectedToolsModel + .selectOnly(allToolsMetadata); + } + } + } + + /** + * Collect metadata from all headers of all attached prompt files, + * and computes resulting `chat mode` and `tools` metadata. + * + * The `tools` metadata is combined into a single list across all prompt files. + * On the other hand, the `chat mode` is computed as the single safest mode + * that will satisfy all prompt file attachments. For instance: + * + * - `Ask`, `Ask`, `Ask` -> `Ask` + * - `Ask`, `Ask`, `Edit` -> `Edit` + * - `Agent`, `Ask`, `Edit` -> `Agent` + */ + private async getPromptFilesMetadata( variables: readonly IPromptVariableEntry[], - ): Promise { + ): Promise { // process starting from the 'root' prompt files const rootVariables = variables .filter(pick('isRoot')); @@ -1523,7 +1547,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return prompt; }); - const allToolsMetadata = (await Promise.allSettled(toolMetadataPromises)) + const allMetadata = (await Promise.allSettled(toolMetadataPromises)) .filter((result): result is PromiseFulfilledResult => { const { status } = result; const isFulfilled = (status === 'fulfilled'); @@ -1539,34 +1563,69 @@ export class ChatWidget extends Disposable implements IChatWidget { return isFulfilled; }) .map(({ value: prompt }) => { - return prompt.allToolsMetadata; + return { + allToolsMetadata: prompt.allToolsMetadata, + mode: prompt.metadata.mode, + }; }); // flag to track whether any of the prompt files // contained any tools metadata we can use let hasMetadata = false; const result: string[] = []; + let resultingMode: ChatMode | undefined; // copy over all the tools metadata into single array // keep tracking if any of them contained any metadata - for (const maybeMetadata of allToolsMetadata) { - if (maybeMetadata === null) { + for (const { allToolsMetadata, mode } of allMetadata) { + if (allToolsMetadata !== null) { + hasMetadata = true; + result.push(...allToolsMetadata); + } + + /** + * TODO: @legomushroom + */ + + if (resultingMode === undefined) { + resultingMode = mode; + continue; } - hasMetadata = true; - result.push(...maybeMetadata); + if (resultingMode === ChatMode.Agent) { + continue; + } + + if (mode === ChatMode.Agent) { + resultingMode = mode; + continue; + } + + if (resultingMode === ChatMode.Edit) { + continue; + } + + resultingMode = mode; } - // if no prompt files contained tools metadata, return null - if (hasMetadata === false) { - return null; - } - - return result; + return { + allToolsMetadata: (hasMetadata === false) + ? undefined + : result, + resultingChatMode: resultingMode ?? ChatMode.Ask, + }; } } +/** + * Return value of the {@link ChatWidget.getPromptFilesMetadata} method. + */ +interface IPromptFilesMetadata { + readonly allToolsMetadata?: readonly string[]; + readonly resultingChatMode: ChatMode; +} + export class ChatWidgetService extends Disposable implements IChatWidgetService { declare readonly _serviceBrand: undefined; From e0dc1d710ac5878334bff7929785aca82388665a Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Mon, 21 Apr 2025 17:10:46 -0700 Subject: [PATCH 025/114] Support categories in model picker (#247084) * Support categories in model picker * Fix missing contribution registration --- .../actionWidget/browser/actionList.ts | 2 +- .../api/common/extHostLanguageModels.ts | 2 + .../contrib/chat/browser/chat.contribution.ts | 4 + .../browser/modelPicker/modelPickerWidget.ts | 71 ++++++++++-- .../contrib/chat/common/languageModels.ts | 1 + .../modelPicker/chatModelCategoryService.ts | 100 +++++++++++++++++ .../chatModelPickerCategory.contribution.ts | 101 ++++++++++++++++++ .../chat/test/common/languageModels.test.ts | 4 + .../vscode.proposed.chatProvider.d.ts | 7 ++ 9 files changed, 281 insertions(+), 11 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/modelPicker/chatModelCategoryService.ts create mode 100644 src/vs/workbench/contrib/chat/common/modelPicker/chatModelPickerCategory.contribution.ts diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 3f6a3760f8c..4205ba69d1e 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -75,7 +75,7 @@ class HeaderRenderer implements IListRenderer, IHeaderTemp } renderElement(element: IActionListItem, _index: number, templateData: IHeaderTemplateData): void { - templateData.text.textContent = element.group?.title ?? ''; + templateData.text.textContent = element.group?.title ?? element.label ?? ''; } disposeTemplate(_templateData: IHeaderTemplateData): void { diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index c7ddb2133d5..cab1d649948 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -25,6 +25,7 @@ import { IExtHostRpcService } from './extHostRpcService.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../contrib/chat/common/modelPicker/chatModelCategoryService.js'; export interface IExtHostLanguageModels extends ExtHostLanguageModels { } @@ -181,6 +182,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { targetExtensions: metadata.extensions, isDefault: metadata.isDefault, isUserSelectable: metadata.isUserSelectable, + modelPickerCategory: metadata.categoryId ?? DEFAULT_MODEL_PICKER_CATEGORY, capabilities: metadata.capabilities, }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index b79f4b81c5f..bafb0610b0d 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -48,6 +48,7 @@ import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } f import { ILanguageModelsService, LanguageModelsService } from '../common/languageModels.js'; import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; +import { ChatModelCategoryService, IChatModelCategoryService } from '../common/modelPicker/chatModelCategoryService.js'; import { PROMPT_DOCUMENTATION_URL } from '../common/promptSyntax/constants.js'; import { registerReusablePromptLanguageFeatures } from '../common/promptSyntax/languageFeatures/providers/index.js'; import { PromptsService } from '../common/promptSyntax/service/promptsService.js'; @@ -105,6 +106,7 @@ import './promptSyntax/contributions/attachInstructionsCommand.js'; import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler.js'; import { runSaveToPromptAction, SAVE_TO_PROMPT_SLASH_COMMAND_NAME } from './actions/promptActions/chatSaveToPromptAction.js'; import { assertDefined } from '../../../../base/common/types.js'; +import { ChatModelPickerCategoryHandler } from '../common/modelPicker/chatModelPickerCategory.contribution.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -613,6 +615,7 @@ registerWorkbenchContribution2(ChatEditingEditorAccessibility.ID, ChatEditingEdi registerWorkbenchContribution2(ChatEditingEditorOverlay.ID, ChatEditingEditorOverlay, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingEditorContextKeys.ID, ChatEditingEditorContextKeys, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribution, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(ChatModelPickerCategoryHandler.ID, ChatModelPickerCategoryHandler, WorkbenchPhase.Eventually); registerChatActions(); registerChatCopyActions(); @@ -654,6 +657,7 @@ registerSingleton(IChatMarkdownAnchorService, ChatMarkdownAnchorService, Instant registerSingleton(ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService, InstantiationType.Delayed); registerSingleton(IChatEntitlementService, ChatEntitlementService, InstantiationType.Delayed); registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed); +registerSingleton(IChatModelCategoryService, ChatModelCategoryService, InstantiationType.Delayed); registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup); diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts index 3bec3dc0419..a99b3712b97 100644 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts @@ -19,6 +19,7 @@ import { ActionListItemKind, IActionListItem } from '../../../../../platform/act import { ThemeIcon } from '../../../../../base/common/themables.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY, IChatModelCategoryService } from '../../common/modelPicker/chatModelCategoryService.js'; interface IModelPickerActionItem { model: ILanguageModelChatMetadataAndIdentifier; @@ -41,6 +42,7 @@ export class ModelPickerWidget extends Disposable { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, @ICommandService private readonly commandService: ICommandService, + @IChatModelCategoryService private readonly modelCategoryService: IChatModelCategoryService, ) { super(); } @@ -101,16 +103,65 @@ export class ModelPickerWidget extends Disposable { * Shows the picker at the specified anchor */ showAt(anchor: HTMLElement | StandardMouseEvent | IAnchor, container?: HTMLElement): void { - const items: IActionListItem[] = this.getActionItems().map(item => ({ - item: item.model, - description: item.model.metadata.description, - kind: ActionListItemKind.Action, - canPreview: false, - group: { title: '', icon: ThemeIcon.fromId(item.isCurrent ? Codicon.check.id : Codicon.blank.id) }, - disabled: false, - hideIcon: false, - label: item.model.metadata.name, - } satisfies IActionListItem)); + const actionItems = this.getActionItems(); + const items: IActionListItem[] = []; + + // Group models by categories + const modelsByCategory = new Map(); + + // First, group models by their categories + for (const item of actionItems) { + const category = this.modelCategoryService.getCategoryById(item.model.metadata.modelPickerCategory); + if (category) { + if (!modelsByCategory.has(category.id)) { + modelsByCategory.set(category.id, []); + } + modelsByCategory.get(category.id)!.push(item); + continue; + } + + // If no category or category not found, add to default category + if (!modelsByCategory.has(DEFAULT_MODEL_PICKER_CATEGORY)) { + modelsByCategory.set(DEFAULT_MODEL_PICKER_CATEGORY, []); + } + modelsByCategory.get(DEFAULT_MODEL_PICKER_CATEGORY)!.push(item); + } + + const categories = this.modelCategoryService.getCategories(); + for (const category of categories) { + const modelsInCategory = modelsByCategory.get(category.id) || []; + + // Skip empty categories + if (modelsInCategory.length === 0) { + continue; + } + + // Add category header + items.push({ + label: category.name, + kind: ActionListItemKind.Header, + canPreview: false, + disabled: false, + hideIcon: true, + } satisfies IActionListItem); + + // Add models in this category + for (const item of modelsInCategory) { + items.push({ + item: item.model, + description: item.model.metadata.description, + kind: ActionListItemKind.Action, + canPreview: false, + group: { title: '', icon: ThemeIcon.fromId(item.isCurrent ? Codicon.check.id : Codicon.blank.id) }, + disabled: false, + hideIcon: false, + label: item.model.metadata.name, + } satisfies IActionListItem); + } + + // Remove this category from the map so we don't process it again + modelsByCategory.delete(category.id); + } const delegate = { onSelect: (item: ILanguageModelChatMetadataAndIdentifier) => { diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 018238b7b4c..7ae29dfac5c 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -127,6 +127,7 @@ export interface ILanguageModelChatMetadata { readonly isDefault?: boolean; readonly isUserSelectable?: boolean; + readonly modelPickerCategory: string; readonly auth?: { readonly providerLabel: string; readonly accountLabel?: string; diff --git a/src/vs/workbench/contrib/chat/common/modelPicker/chatModelCategoryService.ts b/src/vs/workbench/contrib/chat/common/modelPicker/chatModelCategoryService.ts new file mode 100644 index 00000000000..be1c6b393be --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/modelPicker/chatModelCategoryService.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { localize } from '../../../../../nls.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; + +export interface IChatModelCategory { + /** + * The unique identifier for this category + */ + id: string; + + /** + * The user-facing name of this category + */ + name: string; +} + +export interface IChatModelCategoryService { + readonly _serviceBrand: undefined; + + /** + * An event that fires when the registered categories change + */ + readonly onDidChangeCategories: Event; + + /** + * Register a model category + * @param category The category to register + * @returns A disposable that will unregister the category + */ + registerCategory(category: IChatModelCategory): IDisposable; + + /** + * Get all registered categories + * @returns An array of registered categories + */ + getCategories(): IChatModelCategory[]; + + /** + * Get a category by its ID + * @param id The ID of the category to retrieve + * @returns The category with the given ID, or undefined if not found + */ + getCategoryById(id: string): IChatModelCategory | undefined; +} + +export const IChatModelCategoryService = createDecorator('chatModelCategoryService'); + +export const DEFAULT_MODEL_PICKER_CATEGORY = 'other'; + +export class ChatModelCategoryService extends Disposable implements IChatModelCategoryService { + _serviceBrand: undefined; + + private readonly _onDidChangeCategories = this._register(new Emitter()); + readonly onDidChangeCategories = this._onDidChangeCategories.event; + + private readonly _categories = new Map(); + private readonly _modelCategories = new Map(); + + constructor() { + super(); + this._register(this.registerCategory({ id: DEFAULT_MODEL_PICKER_CATEGORY, name: localize('chat.modelPicker.other', "Other Models") })); + } + + registerCategory(category: IChatModelCategory) { + if (this._categories.has(category.id)) { + throw new Error(`Model category with ID '${category.id}' is already registered`); + } + + this._categories.set(category.id, category); + this._onDidChangeCategories.fire(); + + return { + dispose: () => { + if (this._categories.delete(category.id)) { + this._onDidChangeCategories.fire(); + } + } + }; + } + + getCategories(): IChatModelCategory[] { + return Array.from(this._categories.values()).sort((a, b) => { + return a.name.localeCompare(b.name); + }); + } + + getCategoryById(id: string): IChatModelCategory | undefined { + return this._categories.get(id); + } + + getCategoryForModel(modelId: string): string | undefined { + return this._modelCategories.get(modelId); + } +} diff --git a/src/vs/workbench/contrib/chat/common/modelPicker/chatModelPickerCategory.contribution.ts b/src/vs/workbench/contrib/chat/common/modelPicker/chatModelPickerCategory.contribution.ts new file mode 100644 index 00000000000..32fdbd180d3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/modelPicker/chatModelPickerCategory.contribution.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableMap, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../nls.js'; +import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; +import { IChatModelCategoryService } from './chatModelCategoryService.js'; + +export interface IRawChatModelPickerCategoryContribution { + /** + * The id of the category + */ + id: string; + + /** + * User-facing name of the category + */ + name: string; +} + +const chatModelPickerCategoriesExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'chatModelPickerCategories', + jsonSchema: { + description: localize('vscode.extension.contributes.chatModelPickerCategories', 'Contributes categories for organizing models in the chat model picker'), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { id: '', name: '' } }], + required: ['id', 'name'], + properties: { + id: { + description: localize('chatModelPickerCategoryId', "A unique identifier for this category."), + type: 'string' + }, + name: { + description: localize('chatModelPickerCategoryName', "User-facing name for this category."), + type: 'string' + } + } + } + }, + activationEventsGenerator: (contributions: IRawChatModelPickerCategoryContribution[], result: { push(item: string): void }) => { + for (const contrib of contributions) { + result.push(`onChatModelPickerCategory:${contrib.id}`); + } + }, +}); + +export class ChatModelPickerCategoryHandler implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.chatModelPickerCategoryHandler'; + + private _categoryRegistrationDisposables = new DisposableMap(); + + constructor( + @IChatModelCategoryService private readonly modelCategoryService: IChatModelCategoryService, + @ILogService private readonly logService: ILogService + ) { + this.handleAndRegisterModelCategories(); + } + + private handleAndRegisterModelCategories(): void { + chatModelPickerCategoriesExtensionPoint.setHandler((extensions, delta) => { + for (const extension of delta.added) { + for (const categoryDescriptor of extension.value) { + try { + const store = new DisposableStore(); + store.add(this.modelCategoryService.registerCategory({ + id: categoryDescriptor.id, + name: categoryDescriptor.name + })); + + // Store the registration under a key that combines extension ID and category ID + this._categoryRegistrationDisposables.set( + getCategoryKey(extension.description.identifier, categoryDescriptor.id), + store + ); + } catch (e) { + this.logService.error(`Failed to register model picker category ${categoryDescriptor.id}: ${e}`); + } + } + } + + for (const extension of delta.removed) { + for (const categoryDescriptor of extension.value) { + this._categoryRegistrationDisposables.deleteAndDispose(getCategoryKey(extension.description.identifier, categoryDescriptor.id)); + } + } + }); + } +} + +function getCategoryKey(extensionId: ExtensionIdentifier, categoryId: string): string { + return `${extensionId.value}_${categoryId}`; +} diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index 06560d694d6..3b96b7a7232 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -14,6 +14,7 @@ import { ChatMessageRole, IChatResponseFragment, languageModelExtensionPoint, La import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; import { ExtensionsRegistry } from '../../../../services/extensions/common/extensionsRegistry.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/modelPicker/chatModelCategoryService.js'; suite('LanguageModels', function () { @@ -50,6 +51,7 @@ suite('LanguageModels', function () { name: 'Pretty Name', vendor: 'test-vendor', family: 'test-family', + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, version: 'test-version', id: 'test-id', maxInputTokens: 100, @@ -70,6 +72,7 @@ suite('LanguageModels', function () { vendor: 'test-vendor', family: 'test2-family', version: 'test2-version', + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, id: 'test-id', maxInputTokens: 100, maxOutputTokens: 100, @@ -119,6 +122,7 @@ suite('LanguageModels', function () { id: 'actual-lm', maxInputTokens: 100, maxOutputTokens: 100, + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, }, sendChatRequest: async (messages, _from, _options, token) => { // const message = messages.at(-1); diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index f3edb530291..39924d43e6a 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -70,6 +70,13 @@ declare module 'vscode' { readonly toolCalling?: boolean; readonly agentMode?: boolean; }; + + /** + * Optional ID of the category this model belongs to. + * Categories are contributed via the `chatModelPickerCategories` extension point. + * If not specified, the model will appear in the "Other Models" category. + */ + readonly categoryId?: string; } export interface ChatResponseProviderMetadata { From 72deaf12c62554a903231454abdb550988d24501 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Mon, 21 Apr 2025 18:17:03 -0600 Subject: [PATCH 026/114] Update to AI keywords to show 3 at the same time --- .../search/browser/media/searchview.css | 12 +++++ .../contrib/search/browser/searchView.ts | 47 +++++++------------ 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index deccc655fb0..000d9a09f49 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -169,6 +169,18 @@ overflow-wrap: break-word; } +.search-view .message.ai-keywords { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + margin: 0 22px 8px; + padding: 0px; +} + .search-view .message p:first-child { margin-top: 0px; margin-bottom: 0px; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 102e8a447d6..79f9a4c7dfc 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -1958,43 +1958,30 @@ export class SearchView extends ViewPane { } private updateKeywordSuggestion(keywords: AISearchKeyword[]) { - let currentKeyword = keywords.shift()?.keyword || ''; const messageEl = this.clearMessage(); + messageEl.classList.add('ai-keywords'); - // Refresh icon - if (keywords.length !== 0) { - const icon = dom.append(messageEl, dom.$('')); - icon.ariaLabel = nls.localize('search.refresh', "Get new suggestion"); - icon.role = 'button'; - icon.tabIndex = 0; - icon.classList.add('codicon', 'codicon-refresh', 'keyword-refresh'); - icon.onclick = () => { - // change the keyword to the next one - const nextKeyword = keywords.shift(); - if (nextKeyword) { - currentKeyword = nextKeyword.keyword; - textButton.element.textContent = currentKeyword; - } - if (keywords.length === 0) { - icon.remove(); - } - }; + if (keywords.length === 0) { + // Do not display anything if there are no keywords + return; } - // Unclickable message + // Add unclickable message const resultMsg = nls.localize('keywordSuggestion.message', "Search instead for: "); - this.tree.ariaLabel = resultMsg + nls.localize('aiSearchForTerm', " - Search: {0}", currentKeyword); dom.append(messageEl, resultMsg); - const textButton = this.messageDisposables.add(new SearchLinkButton( - currentKeyword, - () => this.handleKeywordClick(currentKeyword), - this.hoverService, - )); - - dom.append(messageEl, textButton.element); - - + const topKeywords = keywords.slice(0, 3); + topKeywords.forEach((keyword, index) => { + if (index > 0 && index < topKeywords.length) { + dom.append(messageEl, ', '); + } + const button = this.messageDisposables.add(new SearchLinkButton( + keyword.keyword, + () => this.handleKeywordClick(keyword.keyword), + this.hoverService + )); + dom.append(messageEl, button.element); + }); } private addMessage(message: TextSearchCompleteMessage) { From 29c0ca5e1e78587b2717801be222cdeeb0b9b038 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Mon, 21 Apr 2025 20:00:58 -0700 Subject: [PATCH 027/114] Revert "Support categories in model picker (#247084)" (#247090) This reverts commit e0dc1d710ac5878334bff7929785aca82388665a. --- .../actionWidget/browser/actionList.ts | 2 +- .../api/common/extHostLanguageModels.ts | 2 - .../contrib/chat/browser/chat.contribution.ts | 4 - .../browser/modelPicker/modelPickerWidget.ts | 71 ++---------- .../contrib/chat/common/languageModels.ts | 1 - .../modelPicker/chatModelCategoryService.ts | 100 ----------------- .../chatModelPickerCategory.contribution.ts | 101 ------------------ .../chat/test/common/languageModels.test.ts | 4 - .../vscode.proposed.chatProvider.d.ts | 7 -- 9 files changed, 11 insertions(+), 281 deletions(-) delete mode 100644 src/vs/workbench/contrib/chat/common/modelPicker/chatModelCategoryService.ts delete mode 100644 src/vs/workbench/contrib/chat/common/modelPicker/chatModelPickerCategory.contribution.ts diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 4205ba69d1e..3f6a3760f8c 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -75,7 +75,7 @@ class HeaderRenderer implements IListRenderer, IHeaderTemp } renderElement(element: IActionListItem, _index: number, templateData: IHeaderTemplateData): void { - templateData.text.textContent = element.group?.title ?? element.label ?? ''; + templateData.text.textContent = element.group?.title ?? ''; } disposeTemplate(_templateData: IHeaderTemplateData): void { diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index cab1d649948..c7ddb2133d5 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -25,7 +25,6 @@ import { IExtHostRpcService } from './extHostRpcService.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; -import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../contrib/chat/common/modelPicker/chatModelCategoryService.js'; export interface IExtHostLanguageModels extends ExtHostLanguageModels { } @@ -182,7 +181,6 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { targetExtensions: metadata.extensions, isDefault: metadata.isDefault, isUserSelectable: metadata.isUserSelectable, - modelPickerCategory: metadata.categoryId ?? DEFAULT_MODEL_PICKER_CATEGORY, capabilities: metadata.capabilities, }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index bafb0610b0d..b79f4b81c5f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -48,7 +48,6 @@ import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } f import { ILanguageModelsService, LanguageModelsService } from '../common/languageModels.js'; import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; -import { ChatModelCategoryService, IChatModelCategoryService } from '../common/modelPicker/chatModelCategoryService.js'; import { PROMPT_DOCUMENTATION_URL } from '../common/promptSyntax/constants.js'; import { registerReusablePromptLanguageFeatures } from '../common/promptSyntax/languageFeatures/providers/index.js'; import { PromptsService } from '../common/promptSyntax/service/promptsService.js'; @@ -106,7 +105,6 @@ import './promptSyntax/contributions/attachInstructionsCommand.js'; import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler.js'; import { runSaveToPromptAction, SAVE_TO_PROMPT_SLASH_COMMAND_NAME } from './actions/promptActions/chatSaveToPromptAction.js'; import { assertDefined } from '../../../../base/common/types.js'; -import { ChatModelPickerCategoryHandler } from '../common/modelPicker/chatModelPickerCategory.contribution.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -615,7 +613,6 @@ registerWorkbenchContribution2(ChatEditingEditorAccessibility.ID, ChatEditingEdi registerWorkbenchContribution2(ChatEditingEditorOverlay.ID, ChatEditingEditorOverlay, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingEditorContextKeys.ID, ChatEditingEditorContextKeys, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribution, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(ChatModelPickerCategoryHandler.ID, ChatModelPickerCategoryHandler, WorkbenchPhase.Eventually); registerChatActions(); registerChatCopyActions(); @@ -657,7 +654,6 @@ registerSingleton(IChatMarkdownAnchorService, ChatMarkdownAnchorService, Instant registerSingleton(ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService, InstantiationType.Delayed); registerSingleton(IChatEntitlementService, ChatEntitlementService, InstantiationType.Delayed); registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed); -registerSingleton(IChatModelCategoryService, ChatModelCategoryService, InstantiationType.Delayed); registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup); diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts index a99b3712b97..3bec3dc0419 100644 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts @@ -19,7 +19,6 @@ import { ActionListItemKind, IActionListItem } from '../../../../../platform/act import { ThemeIcon } from '../../../../../base/common/themables.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; -import { DEFAULT_MODEL_PICKER_CATEGORY, IChatModelCategoryService } from '../../common/modelPicker/chatModelCategoryService.js'; interface IModelPickerActionItem { model: ILanguageModelChatMetadataAndIdentifier; @@ -42,7 +41,6 @@ export class ModelPickerWidget extends Disposable { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, @ICommandService private readonly commandService: ICommandService, - @IChatModelCategoryService private readonly modelCategoryService: IChatModelCategoryService, ) { super(); } @@ -103,65 +101,16 @@ export class ModelPickerWidget extends Disposable { * Shows the picker at the specified anchor */ showAt(anchor: HTMLElement | StandardMouseEvent | IAnchor, container?: HTMLElement): void { - const actionItems = this.getActionItems(); - const items: IActionListItem[] = []; - - // Group models by categories - const modelsByCategory = new Map(); - - // First, group models by their categories - for (const item of actionItems) { - const category = this.modelCategoryService.getCategoryById(item.model.metadata.modelPickerCategory); - if (category) { - if (!modelsByCategory.has(category.id)) { - modelsByCategory.set(category.id, []); - } - modelsByCategory.get(category.id)!.push(item); - continue; - } - - // If no category or category not found, add to default category - if (!modelsByCategory.has(DEFAULT_MODEL_PICKER_CATEGORY)) { - modelsByCategory.set(DEFAULT_MODEL_PICKER_CATEGORY, []); - } - modelsByCategory.get(DEFAULT_MODEL_PICKER_CATEGORY)!.push(item); - } - - const categories = this.modelCategoryService.getCategories(); - for (const category of categories) { - const modelsInCategory = modelsByCategory.get(category.id) || []; - - // Skip empty categories - if (modelsInCategory.length === 0) { - continue; - } - - // Add category header - items.push({ - label: category.name, - kind: ActionListItemKind.Header, - canPreview: false, - disabled: false, - hideIcon: true, - } satisfies IActionListItem); - - // Add models in this category - for (const item of modelsInCategory) { - items.push({ - item: item.model, - description: item.model.metadata.description, - kind: ActionListItemKind.Action, - canPreview: false, - group: { title: '', icon: ThemeIcon.fromId(item.isCurrent ? Codicon.check.id : Codicon.blank.id) }, - disabled: false, - hideIcon: false, - label: item.model.metadata.name, - } satisfies IActionListItem); - } - - // Remove this category from the map so we don't process it again - modelsByCategory.delete(category.id); - } + const items: IActionListItem[] = this.getActionItems().map(item => ({ + item: item.model, + description: item.model.metadata.description, + kind: ActionListItemKind.Action, + canPreview: false, + group: { title: '', icon: ThemeIcon.fromId(item.isCurrent ? Codicon.check.id : Codicon.blank.id) }, + disabled: false, + hideIcon: false, + label: item.model.metadata.name, + } satisfies IActionListItem)); const delegate = { onSelect: (item: ILanguageModelChatMetadataAndIdentifier) => { diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 7ae29dfac5c..018238b7b4c 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -127,7 +127,6 @@ export interface ILanguageModelChatMetadata { readonly isDefault?: boolean; readonly isUserSelectable?: boolean; - readonly modelPickerCategory: string; readonly auth?: { readonly providerLabel: string; readonly accountLabel?: string; diff --git a/src/vs/workbench/contrib/chat/common/modelPicker/chatModelCategoryService.ts b/src/vs/workbench/contrib/chat/common/modelPicker/chatModelCategoryService.ts deleted file mode 100644 index be1c6b393be..00000000000 --- a/src/vs/workbench/contrib/chat/common/modelPicker/chatModelCategoryService.ts +++ /dev/null @@ -1,100 +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 { localize } from '../../../../../nls.js'; -import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; - -export interface IChatModelCategory { - /** - * The unique identifier for this category - */ - id: string; - - /** - * The user-facing name of this category - */ - name: string; -} - -export interface IChatModelCategoryService { - readonly _serviceBrand: undefined; - - /** - * An event that fires when the registered categories change - */ - readonly onDidChangeCategories: Event; - - /** - * Register a model category - * @param category The category to register - * @returns A disposable that will unregister the category - */ - registerCategory(category: IChatModelCategory): IDisposable; - - /** - * Get all registered categories - * @returns An array of registered categories - */ - getCategories(): IChatModelCategory[]; - - /** - * Get a category by its ID - * @param id The ID of the category to retrieve - * @returns The category with the given ID, or undefined if not found - */ - getCategoryById(id: string): IChatModelCategory | undefined; -} - -export const IChatModelCategoryService = createDecorator('chatModelCategoryService'); - -export const DEFAULT_MODEL_PICKER_CATEGORY = 'other'; - -export class ChatModelCategoryService extends Disposable implements IChatModelCategoryService { - _serviceBrand: undefined; - - private readonly _onDidChangeCategories = this._register(new Emitter()); - readonly onDidChangeCategories = this._onDidChangeCategories.event; - - private readonly _categories = new Map(); - private readonly _modelCategories = new Map(); - - constructor() { - super(); - this._register(this.registerCategory({ id: DEFAULT_MODEL_PICKER_CATEGORY, name: localize('chat.modelPicker.other', "Other Models") })); - } - - registerCategory(category: IChatModelCategory) { - if (this._categories.has(category.id)) { - throw new Error(`Model category with ID '${category.id}' is already registered`); - } - - this._categories.set(category.id, category); - this._onDidChangeCategories.fire(); - - return { - dispose: () => { - if (this._categories.delete(category.id)) { - this._onDidChangeCategories.fire(); - } - } - }; - } - - getCategories(): IChatModelCategory[] { - return Array.from(this._categories.values()).sort((a, b) => { - return a.name.localeCompare(b.name); - }); - } - - getCategoryById(id: string): IChatModelCategory | undefined { - return this._categories.get(id); - } - - getCategoryForModel(modelId: string): string | undefined { - return this._modelCategories.get(modelId); - } -} diff --git a/src/vs/workbench/contrib/chat/common/modelPicker/chatModelPickerCategory.contribution.ts b/src/vs/workbench/contrib/chat/common/modelPicker/chatModelPickerCategory.contribution.ts deleted file mode 100644 index 32fdbd180d3..00000000000 --- a/src/vs/workbench/contrib/chat/common/modelPicker/chatModelPickerCategory.contribution.ts +++ /dev/null @@ -1,101 +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 { DisposableMap, DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { localize } from '../../../../../nls.js'; -import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; -import { IChatModelCategoryService } from './chatModelCategoryService.js'; - -export interface IRawChatModelPickerCategoryContribution { - /** - * The id of the category - */ - id: string; - - /** - * User-facing name of the category - */ - name: string; -} - -const chatModelPickerCategoriesExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'chatModelPickerCategories', - jsonSchema: { - description: localize('vscode.extension.contributes.chatModelPickerCategories', 'Contributes categories for organizing models in the chat model picker'), - type: 'array', - items: { - additionalProperties: false, - type: 'object', - defaultSnippets: [{ body: { id: '', name: '' } }], - required: ['id', 'name'], - properties: { - id: { - description: localize('chatModelPickerCategoryId', "A unique identifier for this category."), - type: 'string' - }, - name: { - description: localize('chatModelPickerCategoryName', "User-facing name for this category."), - type: 'string' - } - } - } - }, - activationEventsGenerator: (contributions: IRawChatModelPickerCategoryContribution[], result: { push(item: string): void }) => { - for (const contrib of contributions) { - result.push(`onChatModelPickerCategory:${contrib.id}`); - } - }, -}); - -export class ChatModelPickerCategoryHandler implements IWorkbenchContribution { - - static readonly ID = 'workbench.contrib.chatModelPickerCategoryHandler'; - - private _categoryRegistrationDisposables = new DisposableMap(); - - constructor( - @IChatModelCategoryService private readonly modelCategoryService: IChatModelCategoryService, - @ILogService private readonly logService: ILogService - ) { - this.handleAndRegisterModelCategories(); - } - - private handleAndRegisterModelCategories(): void { - chatModelPickerCategoriesExtensionPoint.setHandler((extensions, delta) => { - for (const extension of delta.added) { - for (const categoryDescriptor of extension.value) { - try { - const store = new DisposableStore(); - store.add(this.modelCategoryService.registerCategory({ - id: categoryDescriptor.id, - name: categoryDescriptor.name - })); - - // Store the registration under a key that combines extension ID and category ID - this._categoryRegistrationDisposables.set( - getCategoryKey(extension.description.identifier, categoryDescriptor.id), - store - ); - } catch (e) { - this.logService.error(`Failed to register model picker category ${categoryDescriptor.id}: ${e}`); - } - } - } - - for (const extension of delta.removed) { - for (const categoryDescriptor of extension.value) { - this._categoryRegistrationDisposables.deleteAndDispose(getCategoryKey(extension.description.identifier, categoryDescriptor.id)); - } - } - }); - } -} - -function getCategoryKey(extensionId: ExtensionIdentifier, categoryId: string): string { - return `${extensionId.value}_${categoryId}`; -} diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index 3b96b7a7232..06560d694d6 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -14,7 +14,6 @@ import { ChatMessageRole, IChatResponseFragment, languageModelExtensionPoint, La import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; import { ExtensionsRegistry } from '../../../../services/extensions/common/extensionsRegistry.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/modelPicker/chatModelCategoryService.js'; suite('LanguageModels', function () { @@ -51,7 +50,6 @@ suite('LanguageModels', function () { name: 'Pretty Name', vendor: 'test-vendor', family: 'test-family', - modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, version: 'test-version', id: 'test-id', maxInputTokens: 100, @@ -72,7 +70,6 @@ suite('LanguageModels', function () { vendor: 'test-vendor', family: 'test2-family', version: 'test2-version', - modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, id: 'test-id', maxInputTokens: 100, maxOutputTokens: 100, @@ -122,7 +119,6 @@ suite('LanguageModels', function () { id: 'actual-lm', maxInputTokens: 100, maxOutputTokens: 100, - modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, }, sendChatRequest: async (messages, _from, _options, token) => { // const message = messages.at(-1); diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 39924d43e6a..f3edb530291 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -70,13 +70,6 @@ declare module 'vscode' { readonly toolCalling?: boolean; readonly agentMode?: boolean; }; - - /** - * Optional ID of the category this model belongs to. - * Categories are contributed via the `chatModelPickerCategories` extension point. - * If not specified, the model will appear in the "Other Models" category. - */ - readonly categoryId?: string; } export interface ChatResponseProviderMetadata { From 00b721f0c4eef7a5ad54a9a6034d55909be03054 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 22 Apr 2025 17:16:01 +1000 Subject: [PATCH 028/114] Do not decorate new cells in empty notebook (#247097) --- .../chatEditingNotebookEditorIntegration.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts index 6fee0f48d05..3d20f5a1028 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts @@ -286,10 +286,18 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I const modifiedChanges = changes.filter(c => c.type === 'modified'); this.createDecorators(); - this.insertedCellDecorator?.apply(changes); - this.modifiedCellDecorator?.apply(modifiedChanges); - this.deletedCellDecorator?.apply(changes, originalModel); - this.overlayToolbarDecorator?.decorate(changes.filter(c => c.type === 'insert' || c.type === 'modified')); + // If all cells are just inserts, then no need to show any decorations. + if (changes.every(c => c.type === 'insert')) { + this.insertedCellDecorator?.apply([]); + this.modifiedCellDecorator?.apply([]); + this.deletedCellDecorator?.apply([], originalModel); + this.overlayToolbarDecorator?.decorate([]); + } else { + this.insertedCellDecorator?.apply(changes); + this.modifiedCellDecorator?.apply(modifiedChanges); + this.deletedCellDecorator?.apply(changes, originalModel); + this.overlayToolbarDecorator?.decorate(changes.filter(c => c.type === 'insert' || c.type === 'modified')); + } })); } From 8c27d61650e670ed907294810b33a92d775f812b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 22 Apr 2025 09:49:37 +0200 Subject: [PATCH 029/114] Enable searching by ids (#247102) --- .../extensions/common/searchExtensionsTool.ts | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts index 3d587cef983..d86c92c7664 100644 --- a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -10,7 +10,7 @@ import { localize } from '../../../../nls.js'; import { SortBy } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js'; import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; -import { ExtensionState, IExtensionsWorkbenchService } from '../common/extensions.js'; +import { ExtensionState, IExtensionsWorkbenchService, IExtension } from '../common/extensions.js'; export const SearchExtensionsToolId = 'vscode_searchExtensions_internal'; @@ -21,7 +21,7 @@ export const SearchExtensionsToolData: IToolData = { icon: ThemeIcon.fromId(Codicon.extensions.id), supportsToolPicker: true, displayName: localize('searchExtensionsTool.displayName', 'Search Extensions'), - modelDescription: localize('searchExtensionsTool.modelDescription', "This tool helps the model search for VS Code extensions from the Marketplace. The model should specify the category of extensions and keywords to search for. Note that the results may include false positives, so further filtering by reviewing the results is recommended."), + modelDescription: localize('searchExtensionsTool.modelDescription', "This is a tool for browsing Visual Studio Code Extensions Marketplace. It allows the model to search for extensions and retrieve detailed information about them. The model should use this tool whenever it needs to discover extensions or resolve information about known ones. To use the tool, the model has to provide the category of the extensions, relevant search keywords, or known extension IDs. Note that search results may include false positives, so reviewing and filtering is recommended."), source: { type: 'internal' }, inputSchema: { type: 'object', @@ -38,6 +38,13 @@ export const SearchExtensionsToolData: IToolData = { }, description: 'The keywords to search for', }, + ids: { + type: 'array', + items: { + type: 'string', + }, + description: 'The ids of the extensions to search for', + }, }, } }; @@ -45,6 +52,7 @@ export const SearchExtensionsToolData: IToolData = { type InputParams = { category?: string; keywords?: string; + ids?: string[]; }; type ExtensionData = { @@ -66,16 +74,35 @@ export class SearchExtensionsTool implements IToolImpl { async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, token: CancellationToken): Promise { const params = invocation.parameters as InputParams; - if (!params.keywords?.length && !params.category) { + if (!params.keywords?.length && !params.category && !params.ids?.length) { return { content: [{ kind: 'text', - value: localize('searchExtensionsTool.noInput', 'Please provide a category or keyword to search for.') + value: localize('searchExtensionsTool.noInput', 'Please provide a category or keywords or ids to search for.') }] }; } const extensionsMap = new Map(); + + const addExtension = (extensions: IExtension[]) => { + for (const extension of extensions) { + if (extension.deprecationInfo || extension.isMalicious) { + continue; + } + extensionsMap.set(extension.identifier.id.toLowerCase(), { + id: extension.identifier.id, + name: extension.displayName, + description: extension.description, + installed: extension.state === ExtensionState.Installed, + installCount: extension.installCount ?? 0, + rating: extension.rating ?? 0, + categories: extension.categories ?? [], + tags: extension.gallery?.tags ?? [] + }); + } + }; + const queryAndAddExtensions = async (text: string) => { const extensions = await this.extensionWorkbenchService.queryGallery({ text, @@ -83,24 +110,16 @@ export class SearchExtensionsTool implements IToolImpl { sortBy: SortBy.InstallCount }, token); if (extensions.firstPage.length) { - for (const extension of extensions.firstPage) { - if (extension.deprecationInfo || extension.isMalicious) { - continue; - } - extensionsMap.set(extension.identifier.id.toLowerCase(), { - id: extension.identifier.id, - name: extension.displayName, - description: extension.description, - installed: extension.state === ExtensionState.Installed, - installCount: extension.installCount ?? 0, - rating: extension.rating ?? 0, - categories: extension.categories ?? [], - tags: extension.gallery?.tags ?? [] - }); - } + addExtension(extensions.firstPage); } }; + // Search for extensions by their ids + if (params.ids?.length) { + const extensions = await this.extensionWorkbenchService.getExtensions(params.ids.map(id => ({ id })), token); + addExtension(extensions); + } + if (params.keywords?.length) { for (const keyword of params.keywords ?? []) { if (keyword === 'featured') { @@ -120,7 +139,7 @@ export class SearchExtensionsTool implements IToolImpl { return { content: [{ kind: 'text', - value: `Here are the list of extensions:\n${JSON.stringify(result)}\n. Use the following format to display extensions to the user because there is a renderer available to parse these extensions in this format and display them with all details. So, do not describe about the extensions to the user.\n\`\`\`vscode-extensions\nextensionId1,extensionId2\n\`\`\`\n.` + value: `Here are the list of extensions:\n${JSON.stringify(result)}\n. Important: Use the following format to display extensions to the user because there is a renderer available to parse these extensions in this format and display them with all details. So, do not describe about the extensions to the user.\n\`\`\`vscode-extensions\nextensionId1,extensionId2\n\`\`\`\n.` }], toolResultDetails: { input: JSON.stringify(params), From 0fc973dc47743e1a9edbafd94c9e696de9f5ecf2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 22 Apr 2025 08:57:50 +0100 Subject: [PATCH 030/114] SCM - add visibility toggle to context menu (#247071) * Add method to toggle visibility * SCM - add visibility toggle to context menu --- src/vs/platform/actions/common/actions.ts | 1 + .../contrib/scm/browser/quickDiffDecorator.ts | 42 ++++++++++++++++++- .../contrib/scm/browser/quickDiffModel.ts | 2 +- .../contrib/scm/browser/scm.contribution.ts | 6 +++ .../workbench/contrib/scm/common/quickDiff.ts | 2 + .../contrib/scm/common/quickDiffService.ts | 39 ++++++++++++----- 6 files changed, 79 insertions(+), 13 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 9356af1f8c0..2d588832880 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -128,6 +128,7 @@ export class MenuId { static readonly SCMHistoryItemContext = new MenuId('SCMHistoryItemContext'); static readonly SCMHistoryItemHover = new MenuId('SCMHistoryItemHover'); static readonly SCMHistoryItemRefContext = new MenuId('SCMHistoryItemRefContext'); + static readonly SCMQuickDiffDecorations = new MenuId('SCMQuickDiffDecorations'); static readonly SCMTitle = new MenuId('SCMTitle'); static readonly SearchContext = new MenuId('SearchContext'); static readonly SearchActionMenu = new MenuId('SearchActionContext'); diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts index a4d035a6230..e1abee3350d 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts @@ -16,14 +16,16 @@ import { IEditorDecorationsCollection } from '../../../../editor/common/editorCo import { OverviewRulerLane, IModelDecorationOptions, MinimapPosition, IModelDeltaDecoration } from '../../../../editor/common/model.js'; import * as domStylesheetsJs from '../../../../base/browser/domStylesheets.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { ChangeType, getChangeType, minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../common/quickDiff.js'; +import { ChangeType, getChangeType, IQuickDiffService, QuickDiffProvider, minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../common/quickDiff.js'; import { QuickDiffModel, IQuickDiffModelService } from './quickDiffModel.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; -import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyTrueExpr, ContextKeyFalseExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { autorun, autorunWithStore, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; +import { registerAction2, Action2, MenuId } from '../../../../platform/actions/common/actions.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; export const quickDiffDecorationCount = new RawContextKey('quickDiffDecorationCount', 0); @@ -218,6 +220,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben private readonly quickDiffDecorationCount: IContextKey; private readonly activeEditor: IObservable; + private readonly quickDiffProviders: IObservable; // Resource URI -> Code Editor Id -> Decoration (Disposable) private readonly decorators = new ResourceMap>(); @@ -229,6 +232,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IQuickDiffModelService private readonly quickDiffModelService: IQuickDiffModelService, + @IQuickDiffService private readonly quickDiffService: IQuickDiffService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IContextKeyService contextKeyService: IContextKeyService, ) { @@ -240,6 +244,9 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben this.activeEditor = observableFromEvent(this, this.editorService.onDidActiveEditorChange, () => this.editorService.activeEditor); + this.quickDiffProviders = observableFromEvent(this, + this.quickDiffService.onDidChangeQuickDiffProviders, () => this.quickDiffService.providers); + const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorations')); this._register(onDidChangeConfiguration(this.onDidChangeConfiguration, this)); this.onDidChangeConfiguration(); @@ -308,6 +315,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben this.onEditorsChanged(); this.onDidActiveEditorChange(); + this.onDidChangeQuickDiffProviders(); this.enabled = true; } @@ -359,6 +367,36 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben })); } + private onDidChangeQuickDiffProviders(): void { + this.transientDisposables.add(autorunWithStore((reader, store) => { + const providers = this.quickDiffProviders.read(reader); + + for (let index = 0; index < providers.length; index++) { + const provider = providers[index]; + store.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.scm.action.toggleQuickDiffVisibility.${provider.label}`, + title: provider.label, + toggled: provider.visible + ? ContextKeyTrueExpr.INSTANCE + : ContextKeyFalseExpr.INSTANCE, + menu: { + id: MenuId.SCMQuickDiffDecorations, + order: index + 1, + }, + f1: false + }); + } + override run(accessor: ServicesAccessor): void { + const quickDiffService = accessor.get(IQuickDiffService); + quickDiffService.toggleQuickDiffVisibility(provider.label); + } + })); + } + })); + } + private onEditorsChanged(): void { for (const editor of this.editorService.visibleTextEditorControls) { if (!isCodeEditor(editor)) { diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index b5b24d2f8bf..4f9f36f99a4 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -339,7 +339,7 @@ export class QuickDiffModel extends Disposable { return []; } - if (equals(this._quickDiffs, quickDiffs, (a, b) => a.originalResource.toString() === b.originalResource.toString() && a.label === b.label)) { + if (equals(this._quickDiffs, quickDiffs, (a, b) => a.originalResource.toString() === b.originalResource.toString() && a.label === b.label && a.visible === b.visible)) { return quickDiffs; } diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index a3ce54d97ca..cc397b3a99a 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -626,6 +626,12 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +MenuRegistry.appendMenuItem(MenuId.EditorLineNumberContext, { + title: localize('quickDiffDecoration', "Diff Decorations"), + submenu: MenuId.SCMQuickDiffDecorations, + group: '9_quickDiffDecorations' +}); + registerSingleton(ISCMService, SCMService, InstantiationType.Delayed); registerSingleton(ISCMViewService, SCMViewService, InstantiationType.Delayed); registerSingleton(IQuickDiffService, QuickDiffService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index a0f0d50e789..72280a72104 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -102,8 +102,10 @@ export interface IQuickDiffService { readonly _serviceBrand: undefined; readonly onDidChangeQuickDiffProviders: Event; + readonly providers: readonly QuickDiffProvider[]; addQuickDiffProvider(quickDiff: QuickDiffProvider): IDisposable; getQuickDiffs(uri: URI, language?: string, isSynchronized?: boolean): Promise; + toggleQuickDiffVisibility(label: string): void; } export enum ChangeType { diff --git a/src/vs/workbench/contrib/scm/common/quickDiffService.ts b/src/vs/workbench/contrib/scm/common/quickDiffService.ts index a4f211541b3..b4da8c5feb1 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiffService.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiffService.ts @@ -25,16 +25,7 @@ function createProviderComparer(uri: URI): (a: QuickDiffProvider, b: QuickDiffPr const bIsParent = isEqualOrParent(uri, b.rootUri!); if (aIsParent && bIsParent) { - if (a.kind === 'primary') { - return -1; - } else if (b.kind === 'primary') { - return 1; - } else if (a.kind === 'secondary') { - return -1; - } else if (b.kind === 'secondary') { - return 1; - } - return 0; + return providerComparer(a, b); } else if (aIsParent) { return -1; } else if (bIsParent) { @@ -45,10 +36,27 @@ function createProviderComparer(uri: URI): (a: QuickDiffProvider, b: QuickDiffPr }; } +function providerComparer(a: QuickDiffProvider, b: QuickDiffProvider): number { + if (a.kind === 'primary') { + return -1; + } else if (b.kind === 'primary') { + return 1; + } else if (a.kind === 'secondary') { + return -1; + } else if (b.kind === 'secondary') { + return 1; + } + return 0; +} + export class QuickDiffService extends Disposable implements IQuickDiffService { declare readonly _serviceBrand: undefined; private quickDiffProviders: Set = new Set(); + get providers(): readonly QuickDiffProvider[] { + return Array.from(this.quickDiffProviders).sort(providerComparer); + } + private readonly _onDidChangeQuickDiffProviders = this._register(new Emitter()); readonly onDidChangeQuickDiffProviders = this._onDidChangeQuickDiffProviders.event; @@ -90,6 +98,17 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { return quickDiffs.filter(this.isQuickDiff); } + + toggleQuickDiffVisibility(label: string): void { + const quickDiff = Array.from(this.quickDiffProviders) + .find(provider => provider.label === label); + if (!quickDiff) { + return; + } + + quickDiff.visible = !quickDiff.visible; + this._onDidChangeQuickDiffProviders.fire(); + } } export async function getOriginalResource(quickDiffService: IQuickDiffService, uri: URI, language: string | undefined, isSynchronized: boolean | undefined): Promise { From e810985ce29bb59a7fe5a22a16a1a76e3677c9bf Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 22 Apr 2025 08:57:59 +0100 Subject: [PATCH 031/114] SCM - tweak secondary diff decoration colors (#247073) --- src/vs/workbench/contrib/scm/common/quickDiff.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index 72280a72104..99920db6e37 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -27,7 +27,7 @@ const editorGutterModifiedBackground = registerColor('editorGutter.modifiedBackg }, nls.localize('editorGutterModifiedBackground', "Editor gutter background color for lines that are modified.")); registerColor('editorGutter.modifiedSecondaryBackground', - { dark: darken(editorGutterModifiedBackground, 0.5), light: lighten(editorGutterModifiedBackground, 0.9), hcDark: '#1B81A8', hcLight: '#2090D3' }, + { dark: darken(editorGutterModifiedBackground, 0.5), light: lighten(editorGutterModifiedBackground, 0.7), hcDark: '#1B81A8', hcLight: '#2090D3' }, nls.localize('editorGutterModifiedSecondaryBackground', "Editor gutter secondary background color for lines that are modified.")); const editorGutterAddedBackground = registerColor('editorGutter.addedBackground', { @@ -35,7 +35,7 @@ const editorGutterAddedBackground = registerColor('editorGutter.addedBackground' }, nls.localize('editorGutterAddedBackground', "Editor gutter background color for lines that are added.")); registerColor('editorGutter.addedSecondaryBackground', - { dark: darken(editorGutterAddedBackground, 0.5), light: lighten(editorGutterAddedBackground, 0.9), hcDark: '#487E02', hcLight: '#48985D' }, + { dark: darken(editorGutterAddedBackground, 0.5), light: lighten(editorGutterAddedBackground, 0.7), hcDark: '#487E02', hcLight: '#48985D' }, nls.localize('editorGutterAddedSecondaryBackground', "Editor gutter secondary background color for lines that are added.")); const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', From dffb8661aa64cd0d80e4d2dc2fcbd44aacf0fee0 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 22 Apr 2025 18:35:32 +1000 Subject: [PATCH 032/114] Render chat changes in editor if diff is available (#246440) --- .../browser/chatEditing/chatEditingCodeEditorIntegration.ts | 4 ++-- .../browser/chatEditing/chatEditingModifiedDocumentEntry.ts | 2 +- .../notebook/chatEditingNotebookEditorIntegration.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts index a5a53c49a68..f2c27df5ea5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts @@ -70,6 +70,7 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito private readonly _entry: IModifiedFileEntry, private readonly _editor: ICodeEditor, documentDiffInfo: IObservable, + renderDiffImmediately: boolean, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IEditorService private readonly _editorService: IEditorService, @IAccessibilitySignalService private readonly _accessibilitySignalsService: IAccessibilitySignalService, @@ -147,8 +148,7 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito } // done: render diff - if (!_entry.isCurrentlyBeingModifiedBy.read(r)) { - + if (!_entry.isCurrentlyBeingModifiedBy.read(r) || renderDiffImmediately) { const isDiffEditor = this._editor.getOption(EditorOption.inDiffEditor); codeEditorObs.getOption(EditorOption.fontInfo).read(r); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index e6d83e24c38..44cc67fded2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -507,6 +507,6 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie } satisfies IDocumentDiff2; }); - return this._instantiationService.createInstance(ChatEditingCodeEditorIntegration, this, codeEditor, diffInfo); + return this._instantiationService.createInstance(ChatEditingCodeEditorIntegration, this, codeEditor, diffInfo, false); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts index 3d20f5a1028..38f75783f3c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts @@ -250,7 +250,7 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I } } else { const diff2 = observableValue(`diff${cell.handle}`, diff); - const integration = this.instantiationService.createInstance(ChatEditingCodeEditorIntegration, _entry, editor, diff2); + const integration = this.instantiationService.createInstance(ChatEditingCodeEditorIntegration, _entry, editor, diff2, true); this.cellEditorIntegrations.set(cell, { integration, diff: diff2 }); this._register(integration); this._register(editor.onDidDispose(() => { From 069714629fb7f7b5f8b7fd870567c0fc9b87ef1d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:54:02 +0100 Subject: [PATCH 033/114] =?UTF-8?q?SCM=20-=20=F0=9F=92=84=20remove=20`visi?= =?UTF-8?q?ble`=20property=20from=20quickDiffProvider=20API=20proposal=20(?= =?UTF-8?q?#247103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vs/workbench/api/browser/mainThreadQuickDiff.ts | 4 ++-- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostQuickDiff.ts | 2 +- src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts index 1db282c0abe..f9606f61a85 100644 --- a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts +++ b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts @@ -23,12 +23,12 @@ export class MainThreadQuickDiff implements MainThreadQuickDiffShape { this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostQuickDiff); } - async $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined, visible: boolean): Promise { + async $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined): Promise { const provider: QuickDiffProvider = { label, rootUri: URI.revive(rootUri), selector, - visible, + visible: true, kind: 'contributed', getOriginalResource: async (uri: URI) => { return URI.revive(await this.proxy.$provideOriginalResource(handle, uri, CancellationToken.None)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 28953da827b..6a4906c6321 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1669,7 +1669,7 @@ export interface MainThreadSCMShape extends IDisposable { } export interface MainThreadQuickDiffShape extends IDisposable { - $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined, visible: boolean): Promise; + $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined): Promise; $unregisterQuickDiffProvider(handle: number): Promise; } diff --git a/src/vs/workbench/api/common/extHostQuickDiff.ts b/src/vs/workbench/api/common/extHostQuickDiff.ts index 2b04fcd6222..f959ebf9d92 100644 --- a/src/vs/workbench/api/common/extHostQuickDiff.ts +++ b/src/vs/workbench/api/common/extHostQuickDiff.ts @@ -39,7 +39,7 @@ export class ExtHostQuickDiff implements ExtHostQuickDiffShape { registerQuickDiffProvider(selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, label: string, rootUri?: vscode.Uri): vscode.Disposable { const handle = ExtHostQuickDiff.handlePool++; this.providers.set(handle, quickDiffProvider); - this.proxy.$registerQuickDiffProvider(handle, DocumentSelector.from(selector, this.uriTransformer), label, rootUri, quickDiffProvider.visible ?? true); + this.proxy.$registerQuickDiffProvider(handle, DocumentSelector.from(selector, this.uriTransformer), label, rootUri); return { dispose: () => { this.proxy.$unregisterQuickDiffProvider(handle); diff --git a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts index 5c83a2d3557..a90a9f7e9cb 100644 --- a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts @@ -16,7 +16,6 @@ declare module 'vscode' { } export interface QuickDiffProvider { - label?: string; - readonly visible?: boolean; + readonly label?: string; } } From 72069925aefb3ba83353cb634f7bb8f247d24941 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:57:32 +0200 Subject: [PATCH 034/114] Add .*.env for ini Fixes #247055 --- extensions/ini/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/ini/package.json b/extensions/ini/package.json index 3a594569546..8523df264c1 100644 --- a/extensions/ini/package.json +++ b/extensions/ini/package.json @@ -45,7 +45,8 @@ ], "filenamePatterns": [ "**/.config/git/config", - "**/.git/config" + "**/.git/config", + ".*.env" ], "aliases": [ "Properties", From 05a7abaa651ec5d0fe7de206ab3de8c7ff91bff8 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 22 Apr 2025 11:11:57 +0200 Subject: [PATCH 035/114] fix https://github.com/microsoft/vscode/issues/246875 (#247107) --- src/vs/workbench/api/common/extHostExtensionService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 03aabb46045..5b14b6d5a37 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -1088,7 +1088,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected _isESM(extensionDescription: IExtensionDescription | undefined, modulePath?: string): boolean { modulePath ??= extensionDescription?.main; - return modulePath?.endsWith('.mjs') || extensionDescription?.type === 'module'; + return modulePath?.endsWith('.mjs') || (extensionDescription?.type === 'module' && !modulePath?.endsWith('.cjs')); } protected abstract _beforeAlmostReadyToRunExtensions(): Promise; From 2dd16c3901c5f8b5e1ba3e29552b8cda7e4a2639 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 22 Apr 2025 11:40:52 +0200 Subject: [PATCH 036/114] # should suggest editors that are outside the current workspace (#247109) re https://github.com/microsoft/vscode-copilot-release/issues/7717#issuecomment-2820699517 --- .../contrib/chat/browser/contrib/chatInputCompletions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 41da2f77280..541edfb9e13 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -692,8 +692,8 @@ class BuiltinDynamicCompletions extends Disposable { // HISTORY // always take the last N items for (const item of this.historyService.getHistory()) { - if (!item.resource || !this.workspaceContextService.getWorkspaceFolder(item.resource)) { - // ignore "forgein" editors + if (!item.resource) { + // ignore editors without a resource continue; } From c60b25c67fb2c6e3990357a988921fa2088fced7 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 22 Apr 2025 12:36:15 +0200 Subject: [PATCH 037/114] clean up (#247112) --- .../ui/severityIcon/media/severityIcon.css | 4 +- .../preferences/browser/media/preferences.css | 242 ------------------ .../browser/media/settingsWidgets.css | 144 +++++++++++ .../browser/preferences.contribution.ts | 52 +++- .../preferences/browser/preferencesEditor.ts | 57 ----- .../preferences/browser/preferencesService.ts | 2 +- 6 files changed, 195 insertions(+), 306 deletions(-) delete mode 100644 src/vs/workbench/contrib/preferences/browser/media/preferences.css delete mode 100644 src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts diff --git a/src/vs/base/browser/ui/severityIcon/media/severityIcon.css b/src/vs/base/browser/ui/severityIcon/media/severityIcon.css index 62d99edf337..0e0a140a89c 100644 --- a/src/vs/base/browser/ui/severityIcon/media/severityIcon.css +++ b/src/vs/base/browser/ui/severityIcon/media/severityIcon.css @@ -8,7 +8,6 @@ .text-search-provider-messages .providerMessage .codicon.codicon-error, .extensions-viewlet > .extensions .codicon.codicon-error, .extension-editor .codicon.codicon-error, -.preferences-editor .codicon.codicon-error, .chat-attached-context-attachment .codicon.codicon-error { color: var(--vscode-problemsErrorIcon-foreground); } @@ -26,7 +25,6 @@ .markers-panel .marker-icon.info, .markers-panel .marker-icon .codicon.codicon-info, .text-search-provider-messages .providerMessage .codicon.codicon-info, .extensions-viewlet > .extensions .codicon.codicon-info, -.extension-editor .codicon.codicon-info, -.preferences-editor .codicon.codicon-info { +.extension-editor .codicon.codicon-info { color: var(--vscode-problemsInfoIcon-foreground); } diff --git a/src/vs/workbench/contrib/preferences/browser/media/preferences.css b/src/vs/workbench/contrib/preferences/browser/media/preferences.css deleted file mode 100644 index f1c876cc8f8..00000000000 --- a/src/vs/workbench/contrib/preferences/browser/media/preferences.css +++ /dev/null @@ -1,242 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.preferences-editor { - display: flex; - flex-direction: column; -} - -.preferences-editor > .preferences-header { - padding-left: 27px; - padding-right: 32px; - padding-bottom: 11px; - padding-top: 11px; -} - -.preferences-editor .deprecation-warning { - display: flex; - margin-top: 4px; -} - -.preferences-editor .deprecation-warning .icon { - margin-right: 3px; -} - -.preferences-editor .deprecation-warning .learnMore-button { - margin-left: 3px; - text-decoration: underline; -} - -.preferences-editor > .preferences-editors-container.side-by-side-preferences-editor { - flex: 1; -} - -.preferences-editor > .preferences-editors-container.side-by-side-preferences-editor .preferences-header-container { - line-height: 28px; -} - -.settings-tabs-widget > .monaco-action-bar .action-item.disabled { - display: none; -} - -.settings-tabs-widget > .monaco-action-bar .action-item { - max-width: 300px; - overflow: hidden; - text-overflow: ellipsis; -} - -.default-preferences-editor-container > .preferences-header-container > .default-preferences-header, -.settings-tabs-widget > .monaco-action-bar .action-item .action-label { - text-transform: uppercase; - font-size: 11px; - margin-right: 5px; - cursor: pointer; - display: flex; - overflow: hidden; - text-overflow: ellipsis; -} - -.default-preferences-editor-container > .preferences-header-container > .default-preferences-header, -.preferences-editor .settings-tabs-widget > .monaco-action-bar .action-item .action-label { - margin-left: 33px; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label { - display: block; - padding: 0px; - border-radius: initial; - background: none !important; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label.folder-settings { - display: flex; -} - -.default-preferences-editor-container > .preferences-header-container > .default-preferences-header, -.settings-tabs-widget > .monaco-action-bar .action-item { - padding: 3px 0px; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-title { - text-overflow: ellipsis; - overflow: hidden; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-details { - text-transform: none; - margin-left: 0.5em; - font-size: 10px; - opacity: 0.7; -} - -.settings-tabs-widget .monaco-action-bar .action-item .dropdown-icon { - padding-left: 0.3em; - padding-top: 8px; - font-size: 12px; -} - -.settings-tabs-widget .monaco-action-bar .action-item .dropdown-icon.hide { - display: none; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label { - color: var(--vscode-panelTitle-inactiveForeground); -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label.checked, -.settings-tabs-widget > .monaco-action-bar .action-item .action-label:hover { - color: var(--vscode-panelTitle-activeForeground); - border-bottom: 1px solid var(--vscode-panelTitle-activeBorder); - outline: 1px solid var(--vscode-contrastActiveBorder, transparent); - outline-offset: -1px; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label:focus { - border-bottom: 1px solid var(--vscode-focusBorder); - outline: 1px solid transparent; - outline-offset: -1px; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label:not(.checked):hover { - outline-style: dashed; -} - -.preferences-header > .settings-header-widget { - flex: 1; - display: flex; - position: relative; - align-self: stretch; -} - -.settings-header-widget > .settings-search-controls > .settings-count-widget { - margin: 6px 0px; - padding: 0px 8px; - border-radius: 2px; - float: left; -} - -.settings-header-widget > .settings-search-controls { - position: absolute; - right: 10px; -} - -.settings-header-widget > .settings-search-controls > .settings-count-widget.hide { - display: none; -} - -.settings-header-widget > .settings-search-container { - flex: 1; -} - -.settings-header-widget > .settings-search-container > .settings-search-input { - vertical-align: middle; -} - -.settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox { - height: 30px; -} - -.monaco-workbench.vs .settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox { - border: 1px solid #ddd; -} - -.settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox .input { - font-size: 14px; - padding-left:10px; -} - -.monaco-editor .view-zones > .settings-header-widget { - z-index: 1; -} - -.monaco-editor .settings-header-widget .title-container { - display: flex; - user-select: none; - -webkit-user-select: none; -} - -.monaco-editor .settings-header-widget .title-container .title { - font-weight: bold; - white-space: nowrap; - text-transform: uppercase; -} - -.monaco-editor .settings-header-widget .title-container .message { - white-space: nowrap; -} - -.monaco-editor .settings-group-title-widget { - z-index: 1; -} - -.monaco-editor .settings-group-title-widget .title-container { - width: 100%; - cursor: pointer; - font-weight: bold; - user-select: none; - -webkit-user-select: none; - display: flex; -} - - -.monaco-editor .settings-group-title-widget .title-container .title { - white-space: nowrap; - overflow: hidden; -} - -.monaco-editor.vs-dark .settings-group-title-widget .title-container.focused, -.monaco-editor.vs .settings-group-title-widget .title-container.focused { - outline: none !important; -} - -.monaco-editor .settings-group-title-widget .title-container.focused, -.monaco-editor .settings-group-title-widget .title-container:hover { - background-color: rgba(153, 153, 153, 0.2); -} - -.monaco-editor.hc-black .settings-group-title-widget .title-container.focused { - outline: 1px dotted #f38518; -} - -.monaco-editor.hc-light .settings-group-title-widget .title-container.focused { - outline: 1px dotted #0F4A85; -} - -.monaco-editor .settings-group-title-widget .title-container .codicon { - margin: 0 2px; - width: 16px; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.monaco-editor .dim-configuration { - color: #b1b1b1; -} - -.codicon-settings-edit:hover { - cursor: pointer; -} diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css index 088783e156c..3f7611dd40b 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css @@ -227,3 +227,147 @@ .settings-editor > .settings-body .settings-tree-container .setting-list-widget .setting-list-object-list-row.select-container > select { width: inherit; } + +.settings-tabs-widget > .monaco-action-bar .action-item.disabled { + display: none; +} + +.settings-tabs-widget > .monaco-action-bar .action-item { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label { + text-transform: uppercase; + font-size: 11px; + margin-right: 5px; + cursor: pointer; + display: flex; + overflow: hidden; + text-overflow: ellipsis; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label { + display: block; + padding: 0px; + border-radius: initial; + background: none !important; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label.folder-settings { + display: flex; +} + +.settings-tabs-widget > .monaco-action-bar .action-item { + padding: 3px 0px; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-title { + text-overflow: ellipsis; + overflow: hidden; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-details { + text-transform: none; + margin-left: 0.5em; + font-size: 10px; + opacity: 0.7; +} + +.settings-tabs-widget .monaco-action-bar .action-item .dropdown-icon { + padding-left: 0.3em; + padding-top: 8px; + font-size: 12px; +} + +.settings-tabs-widget .monaco-action-bar .action-item .dropdown-icon.hide { + display: none; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label { + color: var(--vscode-panelTitle-inactiveForeground); +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label.checked, +.settings-tabs-widget > .monaco-action-bar .action-item .action-label:hover { + color: var(--vscode-panelTitle-activeForeground); + border-bottom: 1px solid var(--vscode-panelTitle-activeBorder); + outline: 1px solid var(--vscode-contrastActiveBorder, transparent); + outline-offset: -1px; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label:focus { + border-bottom: 1px solid var(--vscode-focusBorder); + outline: 1px solid transparent; + outline-offset: -1px; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label:not(.checked):hover { + outline-style: dashed; +} + +.settings-header-widget > .settings-search-controls > .settings-count-widget { + margin: 6px 0px; + padding: 0px 8px; + border-radius: 2px; + float: left; +} + +.settings-header-widget > .settings-search-controls { + position: absolute; + right: 10px; +} + +.settings-header-widget > .settings-search-controls > .settings-count-widget.hide { + display: none; +} + +.settings-header-widget > .settings-search-container { + flex: 1; +} + +.settings-header-widget > .settings-search-container > .settings-search-input { + vertical-align: middle; +} + +.settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox { + height: 30px; +} + +.monaco-workbench.vs .settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox { + border: 1px solid #ddd; +} + +.settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox .input { + font-size: 14px; + padding-left:10px; +} + +.monaco-editor .view-zones > .settings-header-widget { + z-index: 1; +} + +.monaco-editor .settings-header-widget .title-container { + display: flex; + user-select: none; + -webkit-user-select: none; +} + +.monaco-editor .settings-header-widget .title-container .title { + font-weight: bold; + white-space: nowrap; + text-transform: uppercase; +} + +.monaco-editor .settings-header-widget .title-container .message { + white-space: nowrap; +} + +.monaco-editor .dim-configuration { + color: #b1b1b1; +} + +.codicon-settings-edit:hover { + cursor: pointer; +} diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index d69acc0a677..ae066a22889 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -8,7 +8,6 @@ import { Disposable, DisposableStore, MutableDisposable } from '../../../../base import { Schemas } from '../../../../base/common/network.js'; import { isBoolean, isObject, isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import './media/preferences.css'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { Context as SuggestContext } from '../../../../editor/contrib/suggest/browser/suggest.js'; import * as nls from '../../../../nls.js'; @@ -31,7 +30,6 @@ import { ResourceContextKey, RemoteNameContext, WorkbenchStateContext } from '.. import { ExplorerFolderContext, ExplorerRootContext } from '../../files/common/files.js'; import { KeybindingsEditor } from './keybindingsEditor.js'; import { ConfigureLanguageBasedSettingsAction } from './preferencesActions.js'; -import { SettingsEditorContribution } from './preferencesEditor.js'; import { preferencesOpenSettingsIcon } from './preferencesIcons.js'; import { SettingsEditor2, SettingsFocusContext } from './settingsEditor2.js'; import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, CONTEXT_WHEN_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ACCEPT_WHEN, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_HISTORY, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REJECT_WHEN, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from '../common/preferences.js'; @@ -44,11 +42,14 @@ import { DEFINE_KEYBINDING_EDITOR_CONTRIB_ID, IDefineKeybindingEditorContributio import { SettingsEditor2Input } from '../../../services/preferences/common/preferencesEditorInput.js'; import { IUserDataProfileService, CURRENT_PROFILE_CONTEXT } from '../../../services/userDataProfile/common/userDataProfile.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; -import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { resolveCommandsContext } from '../../../browser/parts/editor/editorCommandsContext.js'; import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IListService } from '../../../../platform/list/browser/listService.js'; +import { SettingsEditorModel } from '../../../services/preferences/common/preferencesModels.js'; +import { IPreferencesRenderer, WorkspaceSettingsRenderer, UserSettingsRenderer } from './preferencesRenderers.js'; +import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; @@ -1313,6 +1314,51 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo } } +class SettingsEditorContribution extends Disposable { + static readonly ID: string = 'editor.contrib.settings'; + + private currentRenderer: IPreferencesRenderer | undefined; + private readonly disposables = this._register(new DisposableStore()); + + constructor( + private readonly editor: ICodeEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IPreferencesService private readonly preferencesService: IPreferencesService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService + ) { + super(); + this._createPreferencesRenderer(); + this._register(this.editor.onDidChangeModel(e => this._createPreferencesRenderer())); + this._register(this.workspaceContextService.onDidChangeWorkbenchState(() => this._createPreferencesRenderer())); + } + + private async _createPreferencesRenderer(): Promise { + this.disposables.clear(); + this.currentRenderer = undefined; + + const model = this.editor.getModel(); + if (model && /\.(json|code-workspace)$/.test(model.uri.path)) { + // Fast check: the preferences renderer can only appear + // in settings files or workspace files + const settingsModel = await this.preferencesService.createPreferencesEditorModel(model.uri); + if (settingsModel instanceof SettingsEditorModel && this.editor.getModel()) { + this.disposables.add(settingsModel); + switch (settingsModel.configurationTarget) { + case ConfigurationTarget.WORKSPACE: + this.currentRenderer = this.disposables.add(this.instantiationService.createInstance(WorkspaceSettingsRenderer, this.editor, settingsModel)); + break; + default: + this.currentRenderer = this.disposables.add(this.instantiationService.createInstance(UserSettingsRenderer, this.editor, settingsModel)); + break; + } + } + + this.currentRenderer?.render(); + } + } +} + + function getEditorGroupFromArguments(accessor: ServicesAccessor, args: unknown[]): IEditorGroup | undefined { const context = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); return context.groupedEditors[0]?.group; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts deleted file mode 100644 index aed3a321c96..00000000000 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ /dev/null @@ -1,57 +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 { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { IPreferencesRenderer, UserSettingsRenderer, WorkspaceSettingsRenderer } from './preferencesRenderers.js'; -import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; -import { SettingsEditorModel } from '../../../services/preferences/common/preferencesModels.js'; - -export class SettingsEditorContribution extends Disposable { - static readonly ID: string = 'editor.contrib.settings'; - - private currentRenderer: IPreferencesRenderer | undefined; - private readonly disposables = this._register(new DisposableStore()); - - constructor( - private readonly editor: ICodeEditor, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IPreferencesService private readonly preferencesService: IPreferencesService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService - ) { - super(); - this._createPreferencesRenderer(); - this._register(this.editor.onDidChangeModel(e => this._createPreferencesRenderer())); - this._register(this.workspaceContextService.onDidChangeWorkbenchState(() => this._createPreferencesRenderer())); - } - - private async _createPreferencesRenderer(): Promise { - this.disposables.clear(); - this.currentRenderer = undefined; - - const model = this.editor.getModel(); - if (model && /\.(json|code-workspace)$/.test(model.uri.path)) { - // Fast check: the preferences renderer can only appear - // in settings files or workspace files - const settingsModel = await this.preferencesService.createPreferencesEditorModel(model.uri); - if (settingsModel instanceof SettingsEditorModel && this.editor.getModel()) { - this.disposables.add(settingsModel); - switch (settingsModel.configurationTarget) { - case ConfigurationTarget.WORKSPACE: - this.currentRenderer = this.disposables.add(this.instantiationService.createInstance(WorkspaceSettingsRenderer, this.editor, settingsModel)); - break; - default: - this.currentRenderer = this.disposables.add(this.instantiationService.createInstance(UserSettingsRenderer, this.editor, settingsModel)); - break; - } - } - - this.currentRenderer?.render(); - } - } -} diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index c657835f97f..565c4dd6d03 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -123,7 +123,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return workspace.configuration || workspace.folders[0].toResource(FOLDER_SETTINGS_PATH); } - createOrGetCachedSettingsEditor2Input(): SettingsEditor2Input { + private createOrGetCachedSettingsEditor2Input(): SettingsEditor2Input { if (!this._cachedSettingsEditor2Input || this._cachedSettingsEditor2Input.isDisposed()) { // Recreate the input if the user never opened the Settings editor, // or if they closed it and want to reopen it. From 5f86a213cc3d7ac78197c0619a1da269a1619007 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 22 Apr 2025 13:53:35 +0200 Subject: [PATCH 038/114] chat context - make it "Files & Folders..." instead of just folders (#247118) --- .../browser/actions/chatContextActions.ts | 6 +- .../browser/contrib/chatDynamicVariables.ts | 94 ++++++++++++------- .../browser/contrib/chatInputCompletions.ts | 10 +- 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 6be85d4c04b..4640b8c240a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -61,7 +61,7 @@ import { IToolData } from '../../common/languageModelToolsService.js'; import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from '../chat.js'; import { imageToHash, isImage } from '../chatPasteProviders.js'; import { isQuickChat } from '../chatWidget.js'; -import { createFolderQuickPick } from '../contrib/chatDynamicVariables.js'; +import { createFilesAndFolderQuickPick } from '../contrib/chatDynamicVariables.js'; import { convertBufferToScreenshotVariable, ScreenshotVariableId } from '../contrib/screenshot.js'; import { resizeImage } from '../imageUtils.js'; import { INSTRUCTIONS_COMMAND_ID } from '../promptSyntax/contributions/attachInstructionsCommand.js'; @@ -747,7 +747,7 @@ export class AttachContextAction extends Action2 { quickPickItems.push({ kind: 'folder', - label: localize('chatContext.folder', 'Folders...'), + label: localize('chatContext.folder', 'Files & Folders...'), iconClass: ThemeIcon.asClassName(Codicon.folder), id: 'folder', }); @@ -935,7 +935,7 @@ export class AttachContextAction extends Action2 { } private async _showFolders(instantiationService: IInstantiationService): Promise { - const folder = await instantiationService.invokeFunction(accessor => createFolderQuickPick(accessor)); + const folder = await instantiationService.invokeFunction(createFilesAndFolderQuickPick); if (!folder) { return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index f601d70bedb..042d6c5e4a4 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -11,21 +11,25 @@ import * as glob from '../../../../../base/common/glob.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, dispose, isDisposable } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; -import { basename, dirname, joinPath, relativePath } from '../../../../../base/common/resources.js'; +import { basename, dirname, extUri, joinPath, relativePath } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js'; import { Command, isLocation } from '../../../../../editor/common/languages.js'; +import { ILanguageService } from '../../../../../editor/common/languages/language.js'; +import { getIconClasses } from '../../../../../editor/common/services/getIconClasses.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { FileType, IFileService } from '../../../../../platform/files/common/files.js'; +import { FileKind, FileType, IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { PromptsConfig } from '../../../../../platform/prompts/common/config.js'; import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { IHistoryService } from '../../../../services/history/common/history.js'; import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType } from '../../../../services/search/common/search.js'; import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js'; import { IChatWidget } from '../chat.js'; @@ -235,20 +239,29 @@ function isDynamicVariable(obj: any): obj is IDynamicVariable { ChatWidget.CONTRIBS.push(ChatDynamicVariableModel); -export async function createFolderQuickPick(accessor: ServicesAccessor): Promise { +export async function createFilesAndFolderQuickPick(accessor: ServicesAccessor): Promise { const quickInputService = accessor.get(IQuickInputService); const searchService = accessor.get(ISearchService); const configurationService = accessor.get(IConfigurationService); const workspaceService = accessor.get(IWorkspaceContextService); const fileService = accessor.get(IFileService); const labelService = accessor.get(ILabelService); + const modelService = accessor.get(IModelService); + const languageService = accessor.get(ILanguageService); + const historyService = accessor.get(IHistoryService); + + type ResourcePick = IQuickPickItem & { resource: URI; kind: FileKind }; const workspaces = workspaceService.getWorkspace().folders.map(folder => folder.uri); - const topLevelFolderItems = (await getTopLevelFolders(workspaces, fileService)).map(createQuickPickItem); - const quickPick = quickInputService.createQuickPick(); + const defaultItems: ResourcePick[] = []; + (await getTopLevelFolders(workspaces, fileService)).forEach(uri => defaultItems.push(createQuickPickItem(uri, FileKind.FOLDER))); + historyService.getHistory().filter(a => a.resource).slice(0, 30).forEach(uri => defaultItems.push(createQuickPickItem(uri.resource!, FileKind.FILE))); + defaultItems.sort((a, b) => extUri.compare(a.resource, b.resource)); + + const quickPick = quickInputService.createQuickPick(); quickPick.placeholder = 'Search folder by name'; - quickPick.items = topLevelFolderItems; + quickPick.items = defaultItems; return await new Promise(_resolve => { @@ -261,24 +274,32 @@ export async function createFolderQuickPick(accessor: ServicesAccessor): Promise disposables.add(quickPick.onDidChangeValue(async value => { if (value === '') { - quickPick.items = topLevelFolderItems; + quickPick.items = defaultItems; return; } - const workspaceFolders = await Promise.all( - workspaces.map(workspace => - searchFolders( - workspace, - value, - true, - undefined, - undefined, - configurationService, - searchService - ) - )); + const picks: ResourcePick[] = []; - quickPick.items = workspaceFolders.flat().map(createQuickPickItem); + await Promise.all(workspaces.map(async workspace => { + const result = await searchFilesAndFolders( + workspace, + value, + true, + undefined, + undefined, + configurationService, + searchService + ); + + for (const folder of result.folders) { + picks.push(createQuickPickItem(folder, FileKind.FOLDER)); + } + for (const file of result.files) { + picks.push(createQuickPickItem(file, FileKind.FILE)); + } + })); + + quickPick.items = picks.sort((a, b) => extUri.compare(a.resource, b.resource)); })); disposables.add(quickPick.onDidAccept((e) => { @@ -293,15 +314,16 @@ export async function createFolderQuickPick(accessor: ServicesAccessor): Promise quickPick.show(); }); - function createQuickPickItem(folder: URI): IQuickPickItem & { resource: URI } { + function createQuickPickItem(resource: URI, kind: FileKind): ResourcePick { return { - type: 'item', - id: folder.toString(), - resource: folder, + resource, + kind, + id: resource.toString(), alwaysShow: true, - label: basename(folder), - description: labelService.getUriLabel(dirname(folder), { relative: true }), - iconClass: ThemeIcon.asClassName(Codicon.folder), + label: basename(resource), + description: labelService.getUriLabel(dirname(resource), { relative: true }), + iconClasses: kind === FileKind.FILE ? getIconClasses(modelService, languageService, resource, FileKind.FILE) : undefined, + iconClass: kind === FileKind.FOLDER ? ThemeIcon.asClassName(Codicon.folder) : undefined }; } } @@ -326,7 +348,7 @@ export async function getTopLevelFolders(workspaces: URI[], fileService: IFileSe return folders; } -export async function searchFolders( +export async function searchFilesAndFolders( workspace: URI, pattern: string, fuzzyMatch: boolean, @@ -334,7 +356,7 @@ export async function searchFolders( cacheKey: string | undefined, configurationService: IConfigurationService, searchService: ISearchService -): Promise { +): Promise<{ folders: URI[]; files: URI[] }> { const segmentMatchPattern = caseInsensitiveGlobPattern(fuzzyMatch ? fuzzyMatchingGlobPattern(pattern) : continousMatchingGlobPattern(pattern)); const searchExcludePattern = getExcludes(configurationService.getValue({ resource: workspace })) || {}; @@ -349,21 +371,23 @@ export async function searchFolders( excludePattern: searchExcludePattern, }; - let folderResults: ISearchComplete | undefined; + let searchResult: ISearchComplete | undefined; try { - folderResults = await searchService.fileSearch({ ...searchOptions, filePattern: `**/${segmentMatchPattern}/**` }, token); + searchResult = await searchService.fileSearch({ ...searchOptions, filePattern: `{**/${segmentMatchPattern}/**,${pattern}}` }, token); } catch (e) { if (!isCancellationError(e)) { throw e; } } - if (!folderResults || token?.isCancellationRequested) { - return []; + if (!searchResult || token?.isCancellationRequested) { + return { files: [], folders: [] }; } - const folderResources = getMatchingFoldersFromFiles(folderResults.results.map(result => result.resource), workspace, segmentMatchPattern); - return folderResources; + const fileResources = searchResult.results.map(result => result.resource); + const folderResources = getMatchingFoldersFromFiles(fileResources, workspace, segmentMatchPattern); + + return { folders: folderResources, files: fileResources }; } function fuzzyMatchingGlobPattern(pattern: string): string { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 541edfb9e13..41172b10850 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -45,7 +45,7 @@ import { IPromptsService } from '../../common/promptSyntax/service/types.js'; import { ChatSubmitAction } from '../actions/chatExecuteActions.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { ChatInputPart } from '../chatInputPart.js'; -import { ChatDynamicVariableModel, getTopLevelFolders, searchFolders } from './chatDynamicVariables.js'; +import { ChatDynamicVariableModel, getTopLevelFolders, searchFilesAndFolders } from './chatDynamicVariables.js'; class SlashCommandCompletions extends Disposable { constructor( @@ -792,7 +792,13 @@ class BuiltinDynamicCompletions extends Disposable { const cacheKey = this.updateCacheKey(); - const folders = await Promise.all(workspaces.map(workspace => searchFolders(workspace, pattern, true, token, cacheKey.key, this.configurationService, this.searchService))); + const folders: URI[][] = []; + + await Promise.all(workspaces.map(async workspace => { + const result = await searchFilesAndFolders(workspace, pattern, true, token, cacheKey.key, this.configurationService, this.searchService); + folders.push(result.folders); + })); + for (const resource of folders.flat()) { if (seen.has(resource)) { // already included via history From 9b4e21695e2b905d293544dcb583fae2ef8ec7c0 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 22 Apr 2025 13:54:07 +0200 Subject: [PATCH 039/114] make sure chat edits part use the canonical uris (#247120) fyi @roblourens re https://github.com/microsoft/vscode-copilot/issues/16050 --- .../api/browser/mainThreadChatAgents2.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 953499f889f..4560cc6026f 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -21,6 +21,7 @@ import { ILanguageFeaturesService } from '../../../editor/common/services/langua import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../platform/log/common/log.js'; +import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { IChatWidgetService } from '../../contrib/chat/browser/chat.js'; import { ChatInputPart } from '../../contrib/chat/browser/chatInputPart.js'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../contrib/chat/browser/contrib/chatDynamicVariables.js'; @@ -102,6 +103,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @IExtensionService private readonly _extensionService: IExtensionService, + @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); @@ -227,7 +229,19 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA } async $handleProgressChunk(requestId: string, progress: IChatProgressDto, responsePartHandle?: number): Promise { - const revivedProgress = progress.kind === 'notebookEdit' ? ChatNotebookEdit.fromChatEdit(revive(progress)) : revive(progress) as IChatProgress; + + const revivedProgress = progress.kind === 'notebookEdit' + ? ChatNotebookEdit.fromChatEdit(revive(progress)) + : revive(progress) as IChatProgress; + + if (revivedProgress.kind === 'notebookEdit' + || revivedProgress.kind === 'textEdit' + || revivedProgress.kind === 'codeblockUri' + ) { + // make sure to use the canonical uri + revivedProgress.uri = this._uriIdentityService.asCanonicalUri(revivedProgress.uri); + } + if (revivedProgress.kind === 'progressTask') { const handle = ++this._responsePartHandlePool; const responsePartId = `${requestId}_${handle}`; From ef5a49f391ed8e35be89eddec654ada6b21254bf Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 22 Apr 2025 15:25:22 +0200 Subject: [PATCH 040/114] move tools-picker (#247125) re https://github.com/microsoft/vscode/issues/246911 --- .../workbench/contrib/chat/browser/actions/chatToolActions.ts | 4 ++-- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts index b70f7e04a70..87f68f6406e 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts @@ -93,9 +93,9 @@ export class AttachToolsAction extends Action2 { precondition: ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent), menu: { when: ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent), - id: MenuId.ChatInputAttachmentToolbar, + id: MenuId.ChatInput, group: 'navigation', - order: 1 + order: 100 }, keybinding: { when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent)), diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index ce2afe94957..701c056a9ae 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -1129,6 +1129,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return viewItem; } if (action.id === AttachToolsAction.id) { + // TODO@jrieken let's remove this once the tools picker has its final place. return this.selectedToolsModel.toolsActionItemViewItemProvider(action, options); } return undefined; From b45cc51ad5e25d5652384fdc5079222eb9351e35 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 22 Apr 2025 06:26:00 -0700 Subject: [PATCH 041/114] if submenu, don't include kb (#247127) check if submenu --- src/vs/platform/actions/browser/buttonbar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/actions/browser/buttonbar.ts b/src/vs/platform/actions/browser/buttonbar.ts index 3200306b821..1e29718f04a 100644 --- a/src/vs/platform/actions/browser/buttonbar.ts +++ b/src/vs/platform/actions/browser/buttonbar.ts @@ -86,7 +86,7 @@ export class WorkbenchButtonBar extends ButtonBar { let action: IAction; let btn: IButton; let tooltip: string = ''; - const kb = this._keybindingService.lookupKeybinding(actionOrSubmenu.id); + const kb = actionOrSubmenu instanceof SubmenuAction ? '' : this._keybindingService.lookupKeybinding(actionOrSubmenu.id); if (kb) { tooltip = localize('labelWithKeybinding', "{0} ({1})", actionOrSubmenu.tooltip || actionOrSubmenu.label, kb.getLabel()); } else { From 6b3ff68dbf82f5705840440aa50e41c66539ab48 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 22 Apr 2025 06:26:23 -0700 Subject: [PATCH 042/114] rm verbose aria label (#247128) fix #247098 --- src/vs/workbench/contrib/chat/browser/codeBlockPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 399a2f9e36f..4974fd399a9 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -704,7 +704,7 @@ export class CodeCompareBlockPart extends Disposable { const toolbarElt = this.toolbar.getElement(); if (this.accessibilityService.isScreenReaderOptimized()) { toolbarElt.style.display = 'block'; - toolbarElt.ariaLabel = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat) ? localize('chat.codeBlock.toolbarVerbose', 'Toolbar for code block which can be reached via tab') : localize('chat.codeBlock.toolbar', 'Code block toolbar'); + toolbarElt.ariaLabel = localize('chat.codeBlock.toolbar', 'Code block toolbar'); } else { toolbarElt.style.display = ''; } From 970d4f34b989f6eb62a3acd0fd9c263b7eb8728e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:31:29 +0100 Subject: [PATCH 043/114] SCM - maintain quick diff provider visibility (#247129) Maintain selection --- .../api/browser/mainThreadQuickDiff.ts | 4 +- src/vs/workbench/api/browser/mainThreadSCM.ts | 4 +- .../workbench/api/common/extHost.api.impl.ts | 4 +- .../workbench/api/common/extHost.protocol.ts | 2 +- .../workbench/api/common/extHostQuickDiff.ts | 5 +- .../contrib/scm/browser/quickDiffDecorator.ts | 15 ++-- .../workbench/contrib/scm/common/quickDiff.ts | 18 ++--- .../contrib/scm/common/quickDiffService.ts | 72 ++++++++++++++----- .../vscode.proposed.quickDiffProvider.d.ts | 3 +- 9 files changed, 84 insertions(+), 43 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts index f9606f61a85..2d15ed9a843 100644 --- a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts +++ b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts @@ -23,12 +23,12 @@ export class MainThreadQuickDiff implements MainThreadQuickDiffShape { this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostQuickDiff); } - async $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined): Promise { + async $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string, rootUri: UriComponents | undefined): Promise { const provider: QuickDiffProvider = { + id, label, rootUri: URI.revive(rootUri), selector, - visible: true, kind: 'contributed', getOriginalResource: async (uri: URI) => { return URI.revive(await this.proxy.$provideOriginalResource(handle, uri, CancellationToken.None)); diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 86387c27651..13be9f16149 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -334,9 +334,9 @@ class MainThreadSCMProvider implements ISCMProvider { if (features.hasQuickDiffProvider && !this._quickDiff) { this._quickDiff = this._quickDiffService.addQuickDiffProvider({ + id: `${this._providerId}.quickDiffProvider`, label: features.quickDiffLabel ?? this.label, rootUri: this.rootUri, - visible: true, kind: 'primary', getOriginalResource: async (uri: URI) => { if (!this.features.hasQuickDiffProvider) { @@ -354,9 +354,9 @@ class MainThreadSCMProvider implements ISCMProvider { if (features.hasSecondaryQuickDiffProvider && !this._stagedQuickDiff) { this._stagedQuickDiff = this._quickDiffService.addQuickDiffProvider({ + id: `${this._providerId}.secondaryQuickDiffProvider`, label: features.secondaryQuickDiffLabel ?? this.label, rootUri: this.rootUri, - visible: true, kind: 'secondary', getOriginalResource: async (uri: URI) => { if (!this.features.hasSecondaryQuickDiffProvider) { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index f0d7e72251a..b5699214188 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -917,9 +917,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'profileContentHandlers'); return extHostProfileContentHandlers.registerProfileContentHandler(extension, id, handler); }, - registerQuickDiffProvider(selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, label: string, rootUri?: vscode.Uri): vscode.Disposable { + registerQuickDiffProvider(selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, id: string, label: string, rootUri?: vscode.Uri): vscode.Disposable { checkProposedApiEnabled(extension, 'quickDiffProvider'); - return extHostQuickDiff.registerQuickDiffProvider(checkSelector(selector), quickDiffProvider, label, rootUri); + return extHostQuickDiff.registerQuickDiffProvider(extension, checkSelector(selector), quickDiffProvider, id, label, rootUri); }, get tabGroups(): vscode.TabGroups { return extHostEditorTabs.tabGroups; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6a4906c6321..acfa2a7f3c9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1669,7 +1669,7 @@ export interface MainThreadSCMShape extends IDisposable { } export interface MainThreadQuickDiffShape extends IDisposable { - $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined): Promise; + $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string, rootUri: UriComponents | undefined): Promise; $unregisterQuickDiffProvider(handle: number): Promise; } diff --git a/src/vs/workbench/api/common/extHostQuickDiff.ts b/src/vs/workbench/api/common/extHostQuickDiff.ts index f959ebf9d92..5418497ce54 100644 --- a/src/vs/workbench/api/common/extHostQuickDiff.ts +++ b/src/vs/workbench/api/common/extHostQuickDiff.ts @@ -10,6 +10,7 @@ import { ExtHostQuickDiffShape, IMainContext, MainContext, MainThreadQuickDiffSh import { asPromise } from '../../../base/common/async.js'; import { DocumentSelector } from './extHostTypeConverters.js'; import { IURITransformer } from '../../../base/common/uriIpc.js'; +import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; export class ExtHostQuickDiff implements ExtHostQuickDiffShape { private static handlePool: number = 0; @@ -36,10 +37,10 @@ export class ExtHostQuickDiff implements ExtHostQuickDiffShape { .then(r => r || null); } - registerQuickDiffProvider(selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, label: string, rootUri?: vscode.Uri): vscode.Disposable { + registerQuickDiffProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, id: string, label: string, rootUri?: vscode.Uri): vscode.Disposable { const handle = ExtHostQuickDiff.handlePool++; this.providers.set(handle, quickDiffProvider); - this.proxy.$registerQuickDiffProvider(handle, DocumentSelector.from(selector, this.uriTransformer), label, rootUri); + this.proxy.$registerQuickDiffProvider(handle, DocumentSelector.from(selector, this.uriTransformer), `${extension.identifier.value}.${id}`, label, rootUri); return { dispose: () => { this.proxy.$unregisterQuickDiffProvider(handle); diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts index e1abee3350d..20d58302c89 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts @@ -373,24 +373,25 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben for (let index = 0; index < providers.length; index++) { const provider = providers[index]; + const visible = this.quickDiffService.isQuickDiffProviderVisible(provider.id); + const group = provider.kind !== 'contributed' ? '0_scm' : '1_contributed'; + const order = index + 1; + store.add(registerAction2(class extends Action2 { constructor() { super({ - id: `workbench.scm.action.toggleQuickDiffVisibility.${provider.label}`, + id: `workbench.scm.action.toggleQuickDiffVisibility.${provider.id}`, title: provider.label, - toggled: provider.visible - ? ContextKeyTrueExpr.INSTANCE - : ContextKeyFalseExpr.INSTANCE, + toggled: visible ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE, menu: { - id: MenuId.SCMQuickDiffDecorations, - order: index + 1, + id: MenuId.SCMQuickDiffDecorations, group, order }, f1: false }); } override run(accessor: ServicesAccessor): void { const quickDiffService = accessor.get(IQuickDiffService); - quickDiffService.toggleQuickDiffVisibility(provider.label); + quickDiffService.toggleQuickDiffProviderVisibility(provider.id); } })); } diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index 99920db6e37..f290f4dc0f2 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -67,18 +67,19 @@ export const editorGutterItemGlyphForeground = registerColor('editorGutter.itemG export const editorGutterItemBackground = registerColor('editorGutter.itemBackground', { dark: opaque(listInactiveSelectionBackground, editorBackground), light: darken(opaque(listInactiveSelectionBackground, editorBackground), .05), hcDark: Color.white, hcLight: Color.black }, nls.localize('editorGutterItemBackground', 'Editor gutter decoration color for gutter item background. This color should be opaque.')); export interface QuickDiffProvider { - label: string; - rootUri: URI | undefined; - selector?: LanguageSelector; - visible: boolean; + readonly id: string; + readonly label: string; + readonly rootUri: URI | undefined; + readonly selector?: LanguageSelector; readonly kind: 'primary' | 'secondary' | 'contributed'; getOriginalResource(uri: URI): Promise; } export interface QuickDiff { - label: string; - originalResource: URI; - visible: boolean; + readonly id: string; + readonly label: string; + readonly originalResource: URI; + readonly visible: boolean; readonly kind: 'primary' | 'secondary' | 'contributed'; } @@ -105,7 +106,8 @@ export interface IQuickDiffService { readonly providers: readonly QuickDiffProvider[]; addQuickDiffProvider(quickDiff: QuickDiffProvider): IDisposable; getQuickDiffs(uri: URI, language?: string, isSynchronized?: boolean): Promise; - toggleQuickDiffVisibility(label: string): void; + toggleQuickDiffProviderVisibility(id: string): void; + isQuickDiffProviderVisible(id: string): boolean; } export enum ChangeType { diff --git a/src/vs/workbench/contrib/scm/common/quickDiffService.ts b/src/vs/workbench/contrib/scm/common/quickDiffService.ts index b4da8c5feb1..3942c918364 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiffService.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiffService.ts @@ -10,6 +10,7 @@ import { isEqualOrParent } from '../../../../base/common/resources.js'; import { score } from '../../../../editor/common/languageSelector.js'; import { Emitter } from '../../../../base/common/event.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; function createProviderComparer(uri: URI): (a: QuickDiffProvider, b: QuickDiffProvider) => number { return (a, b) => { @@ -51,64 +52,99 @@ function providerComparer(a: QuickDiffProvider, b: QuickDiffProvider): number { export class QuickDiffService extends Disposable implements IQuickDiffService { declare readonly _serviceBrand: undefined; + private static readonly STORAGE_KEY = 'workbench.scm.quickDiffProviders.hidden'; - private quickDiffProviders: Set = new Set(); + private quickDiffProviders: Map = new Map(); get providers(): readonly QuickDiffProvider[] { - return Array.from(this.quickDiffProviders).sort(providerComparer); + return Array.from(this.quickDiffProviders.values()).sort(providerComparer); } private readonly _onDidChangeQuickDiffProviders = this._register(new Emitter()); readonly onDidChangeQuickDiffProviders = this._onDidChangeQuickDiffProviders.event; - constructor(@IUriIdentityService private readonly uriIdentityService: IUriIdentityService) { + private hiddenQuickDiffProviders = new Set(); + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + ) { super(); + + this.loadState(); } addQuickDiffProvider(quickDiff: QuickDiffProvider): IDisposable { - this.quickDiffProviders.add(quickDiff); + this.quickDiffProviders.set(quickDiff.id, quickDiff); this._onDidChangeQuickDiffProviders.fire(); return { dispose: () => { - this.quickDiffProviders.delete(quickDiff); + this.quickDiffProviders.delete(quickDiff.id); this._onDidChangeQuickDiffProviders.fire(); } }; } - private isQuickDiff(diff: { originalResource?: URI; label?: string }): diff is QuickDiff { - return !!diff.originalResource && (typeof diff.label === 'string'); - } - async getQuickDiffs(uri: URI, language: string = '', isSynchronized: boolean = false): Promise { - const providers = Array.from(this.quickDiffProviders) + const providers = Array.from(this.quickDiffProviders.values()) .filter(provider => !provider.rootUri || this.uriIdentityService.extUri.isEqualOrParent(uri, provider.rootUri)) .sort(createProviderComparer(uri)); const quickDiffs = await Promise.all(providers.map(async provider => { + const visible = this.isQuickDiffProviderVisible(provider.id); const scoreValue = provider.selector ? score(provider.selector, uri, language, isSynchronized, undefined, undefined) : 10; const originalResource = scoreValue > 0 ? await provider.getOriginalResource(uri) ?? undefined : undefined; return { - originalResource, + id: provider.id, label: provider.label, - visible: provider.visible, - kind: provider.kind + visible: visible, + kind: provider.kind, + originalResource } satisfies Partial; })); return quickDiffs.filter(this.isQuickDiff); } - toggleQuickDiffVisibility(label: string): void { - const quickDiff = Array.from(this.quickDiffProviders) - .find(provider => provider.label === label); - if (!quickDiff) { + toggleQuickDiffProviderVisibility(id: string): void { + if (!this.quickDiffProviders.has(id)) { return; } - quickDiff.visible = !quickDiff.visible; + if (this.isQuickDiffProviderVisible(id)) { + this.hiddenQuickDiffProviders.add(id); + } else { + this.hiddenQuickDiffProviders.delete(id); + } + + this.saveState(); this._onDidChangeQuickDiffProviders.fire(); } + + isQuickDiffProviderVisible(id: string): boolean { + return !this.hiddenQuickDiffProviders.has(id); + } + + private loadState(): void { + const raw = this.storageService.get(QuickDiffService.STORAGE_KEY, StorageScope.PROFILE); + if (raw) { + try { + this.hiddenQuickDiffProviders = new Set(JSON.parse(raw)); + } catch { } + } + } + + private saveState(): void { + if (this.hiddenQuickDiffProviders.size === 0) { + this.storageService.remove(QuickDiffService.STORAGE_KEY, StorageScope.PROFILE); + } else { + this.storageService.store(QuickDiffService.STORAGE_KEY, JSON.stringify(Array.from(this.hiddenQuickDiffProviders)), StorageScope.PROFILE, StorageTarget.USER); + } + } + + private isQuickDiff(diff: { id?: string; label?: string; originalResource?: URI }): diff is QuickDiff { + return typeof diff.id === 'string' && typeof diff.label === 'string' && !!diff.originalResource; + } } export async function getOriginalResource(quickDiffService: IQuickDiffService, uri: URI, language: string | undefined, isSynchronized: boolean | undefined): Promise { diff --git a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts index a90a9f7e9cb..43f4c935993 100644 --- a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts @@ -8,7 +8,7 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/169012 export namespace window { - export function registerQuickDiffProvider(selector: DocumentSelector, quickDiffProvider: QuickDiffProvider, label: string, rootUri?: Uri): Disposable; + export function registerQuickDiffProvider(selector: DocumentSelector, quickDiffProvider: QuickDiffProvider, id: string, label: string, rootUri?: Uri): Disposable; } export interface SourceControl { @@ -16,6 +16,7 @@ declare module 'vscode' { } export interface QuickDiffProvider { + readonly id?: string; readonly label?: string; } } From 544674f984e97b8aa9a444cb5487cfc4c27f2b7a Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 22 Apr 2025 06:45:51 -0700 Subject: [PATCH 044/114] play diff signals when navigating in accessible view (#247131) fix #246921 --- .../inlineCompletionsAccessibleView.ts | 3 +-- .../accessibility/browser/accessibleView.ts | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts index 959a99ea366..38a558a1e66 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts @@ -17,7 +17,6 @@ import { InlineCompletionsModel } from './model/inlineCompletionsModel.js'; import { TextEdit } from '../../../common/core/textEdit.js'; import { LineEdit } from '../../../common/core/lineEdit.js'; import { TextModelText } from '../../../common/model/textModelText.js'; -import { localize } from '../../../../nls.js'; export class InlineCompletionsAccessibleView implements IAccessibleViewImplementation { readonly type = AccessibleViewType.View; @@ -70,7 +69,7 @@ class InlineCompletionsAccessibleViewContentProvider extends Disposable implemen } else { const text = new TextModelText(this._model.textModel); const lineEdit = LineEdit.fromTextEdit(new TextEdit(state.edits), text); - return localize('inlineEditAvailable', 'There is an inline edit available:') + '\n' + lineEdit.humanReadablePatch(text.getLines()); + return lineEdit.humanReadablePatch(text.getLines()); } } public provideNextContent(): string | undefined { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 409a37c512c..f4aee6bce89 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -28,6 +28,7 @@ import { CodeActionController } from '../../../../editor/contrib/codeAction/brow import { localize } from '../../../../nls.js'; import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol, isIAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; import { ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX, IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; @@ -109,7 +110,8 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi @IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService, @IStorageService private readonly _storageService: IStorageService, @ITextModelService private readonly textModelResolverService: ITextModelService, - @IQuickInputService private readonly _quickInputService: IQuickInputService + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, ) { super(); @@ -185,15 +187,29 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this._register(this._editorWidget.onDidDispose(() => this._resetContextKeys())); this._register(this._editorWidget.onDidChangeCursorPosition(() => { this._onLastLine.set(this._editorWidget.getPosition()?.lineNumber === this._editorWidget.getModel()?.getLineCount()); - })); - this._register(this._editorWidget.onDidChangeCursorPosition(() => { const cursorPosition = this._editorWidget.getPosition()?.lineNumber; if (this._codeBlocks && cursorPosition !== undefined) { const inCodeBlock = this._codeBlocks.find(c => c.startLine <= cursorPosition && c.endLine >= cursorPosition) !== undefined; this._accessibleViewInCodeBlock.set(inCodeBlock); } + this._playDiffSignals(); })); } + + private _playDiffSignals(): void { + const position = this._editorWidget.getPosition(); + const model = this._editorWidget.getModel(); + if (!position || !model) { + return undefined; + } + const lineContent = model.getLineContent(position.lineNumber); + if (lineContent?.startsWith('+')) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted); + } else if (lineContent?.startsWith('-')) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted); + } + } + provideTextContent(resource: URI): Promise | null { return this._getTextModel(resource); } From 4ccec8917add2113b0aac4b129040a9f30065427 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 22 Apr 2025 08:18:23 -0700 Subject: [PATCH 045/114] mcp: finish wiring up the 'version' on ext MCP collections (#247096) --- src/vs/workbench/api/common/extHostMcp.ts | 1 + .../contrib/mcp/browser/mcpCommands.ts | 3 +- .../contrib/mcp/common/mcpContextKeys.ts | 2 +- .../workbench/contrib/mcp/common/mcpServer.ts | 64 ++++++++++++------- .../workbench/contrib/mcp/common/mcpTypes.ts | 6 ++ 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index e8ccfc48f4d..a163ff04aa0 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -112,6 +112,7 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService servers.push({ id: ExtensionIdentifier.toKey(extension.identifier), label: item.label, + cacheNonce: item.version, launch: Convert.McpServerDefinition.from(item) }); } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 6f536d3a7c1..519b08137ef 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -247,6 +247,7 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo let thisState = DisplayedState.None; switch (server.toolsState.read(reader)) { case McpServerToolsState.Unknown: + case McpServerToolsState.Outdated: if (server.trusted.read(reader) === false) { thisState = DisplayedState.None; } else { @@ -324,7 +325,7 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo const { state, servers } = displayedState.get(); if (state === DisplayedState.NewTools) { - servers.forEach(server => server.start()); + servers.forEach(server => server.stop().then(() => server.start())); mcpService.activateCollections(); } else if (state === DisplayedState.Refreshing) { servers.at(-1)?.showOutput(); diff --git a/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts b/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts index 0d0aff9aa6d..3cbb821058c 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts @@ -56,7 +56,7 @@ export class McpContextKeysController extends Disposable implements IWorkbenchCo } const toolState = s.toolsState.read(r); - return toolState === McpServerToolsState.Unknown || toolState === McpServerToolsState.RefreshingFromUnknown; + return toolState === McpServerToolsState.Unknown || toolState === McpServerToolsState.Outdated || toolState === McpServerToolsState.RefreshingFromUnknown; })); })); } diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 6545737fb4a..3bc03f06603 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -55,6 +55,7 @@ type ServerBootStateClassification = { }; interface IToolCacheEntry { + readonly nonce: string | undefined; /** Cached tools so we can show what's available before it's started */ readonly tools: readonly IValidatedMcpTool[]; } @@ -109,13 +110,13 @@ export class McpServerMetadataCache extends Disposable { } /** Gets cached tools for a server (used before a server is running) */ - getTools(definitionId: string): readonly IValidatedMcpTool[] | undefined { - return this.cache.get(definitionId)?.tools; + getTools(definitionId: string) { + return this.cache.get(definitionId); } /** Sets cached tools for a server */ - storeTools(definitionId: string, tools: readonly IValidatedMcpTool[]): void { - this.cache.set(definitionId, { ...this.cache.get(definitionId), tools }); + storeTools(definitionId: string, nonce: string | undefined, tools: readonly IValidatedMcpTool[]): void { + this.cache.set(definitionId, { ...this.cache.get(definitionId), nonce, tools }); this.didChange = true; } @@ -154,17 +155,33 @@ export class McpServer extends Disposable implements IMcpServer { private get toolsFromCache() { return this._toolCache.getTools(this.definition.id); } - private readonly toolsFromServerPromise = observableValue | undefined>(this, undefined); + private readonly toolsFromServerPromise = observableValue | undefined>(this, undefined); private readonly toolsFromServer = derived(reader => this.toolsFromServerPromise.read(reader)?.promiseResult.read(reader)?.data); public readonly tools: IObservable; public readonly toolsState = derived(reader => { + const currentNonce = () => this._mcpRegistry.collections.read(reader) + .find(c => c.id === this.collection.id) + ?.serverDefinitions.read(reader) + .find(d => d.id === this.definition.id) + ?.cacheNonce; + const stateWhenServingFromCache = () => { + if (!this.toolsFromCache) { + return McpServerToolsState.Unknown; + } + + return currentNonce() === this.toolsFromCache.nonce ? McpServerToolsState.Cached : McpServerToolsState.Outdated; + }; + const fromServer = this.toolsFromServerPromise.read(reader); const connectionState = this.connectionState.read(reader); const isIdle = McpConnectionState.canBeStarted(connectionState.state) && !fromServer; if (isIdle) { - return this.toolsFromCache ? McpServerToolsState.Cached : McpServerToolsState.Unknown; + return stateWhenServingFromCache(); } const fromServerResult = fromServer?.promiseResult.read(reader); @@ -172,7 +189,11 @@ export class McpServer extends Disposable implements IMcpServer { return this.toolsFromCache ? McpServerToolsState.RefreshingFromCached : McpServerToolsState.RefreshingFromUnknown; } - return fromServerResult.error ? (this.toolsFromCache ? McpServerToolsState.Cached : McpServerToolsState.Unknown) : McpServerToolsState.Live; + if (fromServerResult.error) { + return stateWhenServingFromCache(); + } + + return fromServerResult.data?.nonce === currentNonce() ? McpServerToolsState.Live : McpServerToolsState.Outdated; }); private readonly _loggerId: string; @@ -228,27 +249,20 @@ export class McpServer extends Disposable implements IMcpServer { // 2. Populate this.tools when we connect to a server. this._register(autorunWithStore((reader, store) => { - const cnx = this._connection.read(reader)?.handler.read(reader); - if (cnx) { - this.populateLiveData(cnx, store); + const cnx = this._connection.read(reader); + const handler = cnx?.handler.read(reader); + if (handler) { + this.populateLiveData(handler, cnx?.definition.cacheNonce, store); } else { this.resetLiveData(); } })); - // 3. Update the cache when tools update - this._register(autorun(reader => { - const tools = this.toolsFromServer.read(reader); - if (tools) { - this._toolCache.storeTools(definition.id, tools); - } - })); - - // 4. Publish tools + // 3. Publish tools const toolPrefix = this._mcpRegistry.collectionToolPrefix(this.collection); this.tools = derived(reader => { const serverTools = this.toolsFromServer.read(reader); - const definitions = serverTools ?? this.toolsFromCache ?? []; + const definitions = serverTools?.tools ?? this.toolsFromCache?.tools ?? []; const prefix = toolPrefix.read(reader); return definitions.map(def => new McpTool(this, prefix, def)).sort((a, b) => a.compare(b)); }); @@ -384,7 +398,7 @@ export class McpServer extends Disposable implements IMcpServer { return validated; } - private populateLiveData(handler: McpServerRequestHandler, store: DisposableStore) { + private populateLiveData(handler: McpServerRequestHandler, cacheNonce: string | undefined, store: DisposableStore) { const cts = new CancellationTokenSource(); store.add(toDisposable(() => cts.dispose(true))); @@ -394,11 +408,11 @@ export class McpServer extends Disposable implements IMcpServer { const toolPromise = handler.capabilities.tools ? handler.listTools({}, cts.token) : Promise.resolve([]); const toolPromiseSafe = toolPromise.then(async tools => { handler.logger.info(`Discovered ${tools.length} tools`); - return this._getValidatedTools(handler, tools); + return { tools: await this._getValidatedTools(handler, tools), nonce: cacheNonce }; }); this.toolsFromServerPromise.set(new ObservablePromise(toolPromiseSafe), tx); - return [toolPromise]; + return [toolPromiseSafe]; }; store.add(handler.onDidChangeToolList(() => { @@ -411,7 +425,9 @@ export class McpServer extends Disposable implements IMcpServer { promises = updateTools(tx); }); - Promise.all(promises!).then(([tools]) => { + Promise.all(promises!).then(([{ tools }]) => { + this._toolCache.storeTools(this.definition.id, cacheNonce, tools); + this._telemetryService.publicLog2('mcp/serverBoot', { supportsLogging: !!handler.capabilities.logging, supportsPrompts: !!handler.capabilities.prompts, diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index a590925c9a8..258040db89d 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -104,6 +104,8 @@ export interface McpServerDefinition { readonly roots?: URI[] | undefined; /** If set, allows configuration variables to be resolved in the {@link launch} with the given context */ readonly variableReplacement?: McpServerDefinitionVariableReplacement; + /** Nonce used for caching the server. Changing the nonce will indicate that tools need to be refreshed. */ + readonly cacheNonce?: string; readonly presentation?: { /** Sort order of the definition. */ @@ -117,6 +119,7 @@ export namespace McpServerDefinition { export interface Serialized { readonly id: string; readonly label: string; + readonly cacheNonce?: string; readonly launch: McpServerLaunch.Serialized; readonly variableReplacement?: McpServerDefinitionVariableReplacement.Serialized; } @@ -129,6 +132,7 @@ export namespace McpServerDefinition { return { id: def.id, label: def.label, + cacheNonce: def.cacheNonce, launch: McpServerLaunch.fromSerialized(def.launch), variableReplacement: def.variableReplacement ? McpServerDefinitionVariableReplacement.fromSerialized(def.variableReplacement) : undefined, }; @@ -233,6 +237,8 @@ export const enum McpServerToolsState { Unknown, /** Tools were read from the cache */ Cached, + /** Tools were read from the cache or live, but they may be outdated. */ + Outdated, /** Tools are refreshing for the first time */ RefreshingFromUnknown, /** Tools are refreshing and the current tools are cached */ From 9ea17f8a1c8d6c3d3b14daa74bf787c6c55cbace Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 22 Apr 2025 09:19:58 -0700 Subject: [PATCH 046/114] pr comments --- .../browser/mainThreadLanguageModelTools.ts | 21 +++++++++------- .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/common/extHostLanguageModelTools.ts | 24 ++++++++++--------- .../chatToolInvocationPart.ts | 2 -- .../contrib/chat/browser/chatSetup.ts | 4 ++-- .../chat/browser/languageModelToolsService.ts | 18 +++----------- .../chat/common/languageModelToolsService.ts | 1 - .../vscode.proposed.toolProgress.d.ts | 6 ++--- 8 files changed, 34 insertions(+), 44 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 88ab6e0e598..0a7db37edb5 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -6,7 +6,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; -import { IProgressStep } from '../../../platform/progress/common/progress.js'; +import { IProgress, IProgressStep } from '../../../platform/progress/common/progress.js'; import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; @@ -17,7 +17,10 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre private readonly _proxy: ExtHostLanguageModelToolsShape; private readonly _tools = this._register(new DisposableMap()); - private readonly _countTokenCallbacks = new Map(); + private readonly _runningToolCalls = new Map; + }>(); constructor( extHostContext: IExtHostContext, @@ -46,30 +49,30 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre }; } - $acceptToolProgress(requestId: string | undefined, callId: string, progress: IProgressStep): void { - this._languageModelToolsService.acceptProgress(requestId, callId, progress); + $acceptToolProgress(callId: string, progress: IProgressStep): void { + this._runningToolCalls.get(callId)?.progress.report(progress); } $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise { - const fn = this._countTokenCallbacks.get(callId); + const fn = this._runningToolCalls.get(callId); if (!fn) { throw new Error(`Tool invocation call ${callId} not found`); } - return fn(input, token); + return fn.countTokens(input, token); } $registerTool(id: string): void { const disposable = this._languageModelToolsService.registerToolImplementation( id, { - invoke: async (dto, countTokens, _progress, token) => { + invoke: async (dto, countTokens, progress, token) => { try { - this._countTokenCallbacks.set(dto.callId, countTokens); + this._runningToolCalls.set(dto.callId, { countTokens, progress }); const resultDto = await this._proxy.$invokeTool(dto, token); return revive(resultDto) as IToolResult; } finally { - this._countTokenCallbacks.delete(dto.callId); + this._runningToolCalls.delete(dto.callId); } }, prepareToolInvocation: (parameters, token) => this._proxy.$prepareToolInvocation(id, parameters, token), diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3e50b7d456e..9eeabd64abd 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1380,7 +1380,7 @@ export type IToolDataDto = Omit; export interface MainThreadLanguageModelToolsShape extends IDisposable { $getTools(): Promise[]>; - $acceptToolProgress(requestId: string | undefined, callId: string, progress: IProgressStep): void; + $acceptToolProgress(callId: string, progress: IProgressStep): void; $invokeTool(dto: IToolInvocation, token?: CancellationToken): Promise>; $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise; $registerTool(id: string): void; diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 82de8a39f02..faaa208029e 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -138,18 +138,20 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } - const progress: vscode.Progress<{ message?: string; increment?: number }> = { - report: value => { - checkProposedApiEnabled(item.extension, 'toolProgress'); - this._proxy.$acceptToolProgress(dto.chatRequestId, dto.callId, { - message: value.message, - increment: value.increment, - total: 100, - }); - } - }; + let progress: vscode.Progress<{ message?: string; increment?: number }> | undefined; + if (isProposedApiEnabled(item.extension, 'toolProgress')) { + progress = { + report: value => { + this._proxy.$acceptToolProgress(dto.callId, { + message: value.message, + increment: value.increment, + total: 100, + }); + } + }; + } - const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token, progress)), token); + const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token, progress!)), token); if (!extensionResult) { throw new CancellationError(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts index 20ca1f46cdb..24a8d787794 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts @@ -23,7 +23,6 @@ import { IContextKeyService } from '../../../../../platform/contextkey/common/co import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; -import { IProgressService } from '../../../../../platform/progress/common/progress.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatMarkdownContent, IChatProgressMessage, IChatTerminalToolInvocationData, IChatToolInvocation, IChatToolInvocationSerialized } from '../../common/chatService.js'; import { IChatRendererContent } from '../../common/chatViewModel.js'; @@ -66,7 +65,6 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa codeBlockModelCollection: CodeBlockModelCollection, codeBlockStartIndex: number, @IInstantiationService instantiationService: IInstantiationService, - @IProgressService progressService: IProgressService, ) { super(); diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 7b15fa89f3f..2fb469b94f2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -37,7 +37,7 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import product from '../../../../platform/product/common/product.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; -import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { IProgress, IProgressService, IProgressStep, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; @@ -518,7 +518,7 @@ class SetupTool extends Disposable implements IToolImpl { super(); } - invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { + invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: IProgress, token: CancellationToken): Promise { const result: IToolResult = { content: [ { diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 4a6b02c24b8..b21654e4d9e 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -22,7 +22,6 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import * as JSONContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { IProgressStep } from '../../../../platform/progress/common/progress.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -41,7 +40,7 @@ interface IToolEntry { impl?: IToolImpl; } -interface TrackedCall { +interface ITrackedCall { invocation?: ChatToolInvocation; store: IDisposable; } @@ -59,7 +58,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private _toolContextKeys = new Set(); private readonly _ctxToolsCount: IContextKey; - private _callsByRequestId = new Map(); + private _callsByRequestId = new Map(); private _workspaceToolConfirmStore: Lazy; private _profileToolConfirmStore: Lazy; @@ -97,17 +96,6 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo this._ctxToolsCount = ChatContextKeys.Tools.toolsCount.bindTo(_contextKeyService); } - acceptProgress(sessionId: string | undefined, callId: string, progress: IProgressStep): void { - if (!sessionId) { - return; // not supported, yet - } - - this._callsByRequestId.get(sessionId) - ?.find(call => call.invocation?.toolCallId === callId) - ?.invocation - ?.acceptProgress(progress); - } - registerToolData(toolData: IToolData): IDisposable { if (this._tools.has(toolData.id)) { throw new Error(`Tool "${toolData.id}" is already registered.`); @@ -252,7 +240,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo if (!this._callsByRequestId.has(requestId)) { this._callsByRequestId.set(requestId, []); } - const trackedCall: TrackedCall = { store }; + const trackedCall: ITrackedCall = { store }; this._callsByRequestId.get(requestId)!.push(trackedCall); const source = new CancellationTokenSource(); diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index f78950c320f..75332b21169 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -152,7 +152,6 @@ export interface ILanguageModelToolsService { getTool(id: string): IToolData | undefined; getToolByName(name: string): IToolData | undefined; invokeTool(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise; - acceptProgress(sessionId: string | undefined, callId: string, progress: IProgressStep): void; setToolAutoConfirmation(toolId: string, scope: 'workspace' | 'profile' | 'memory', autoConfirm?: boolean): void; resetToolAutoConfirmation(): void; cancelToolCallsForRequest(requestId: string): void; diff --git a/src/vscode-dts/vscode.proposed.toolProgress.d.ts b/src/vscode-dts/vscode.proposed.toolProgress.d.ts index 8deea607747..51562cd6ee3 100644 --- a/src/vscode-dts/vscode.proposed.toolProgress.d.ts +++ b/src/vscode-dts/vscode.proposed.toolProgress.d.ts @@ -6,9 +6,9 @@ declare module 'vscode' { /** - * todo@connor4312: `vscode.window.withProgres` can take this interface as well. + * A progress update during an {@link LanguageModelTool.invoke} call. */ - export interface ProgressStep { + export interface ToolProgressStep { /** * A progress message that represents a chunk of work */ @@ -20,6 +20,6 @@ declare module 'vscode' { } export interface LanguageModelTool { - invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken, progress: Progress): ProviderResult; + invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken, progress: Progress): ProviderResult; } } From 7097d6a43d0ae49bf0e4f8757145a59a0ae15543 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Tue, 22 Apr 2025 10:25:35 -0700 Subject: [PATCH 047/114] Register categories as part of LM provider (#247146) * Revert "Revert "Support categories in model picker (#247084)" (#247090)" This reverts commit 29c0ca5e1e78587b2717801be222cdeeb0b9b038. * Switch model categories to be part of registration --- .../actionWidget/browser/actionList.ts | 2 +- .../api/common/extHostLanguageModels.ts | 2 + .../browser/modelPicker/modelPickerWidget.ts | 57 +++++++++++++++---- .../contrib/chat/common/languageModels.ts | 1 + .../common/modelPicker/modelPickerWidget.ts | 8 +++ .../chat/test/common/languageModels.test.ts | 4 ++ .../vscode.proposed.chatProvider.d.ts | 7 +++ 7 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/modelPicker/modelPickerWidget.ts diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 3f6a3760f8c..4205ba69d1e 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -75,7 +75,7 @@ class HeaderRenderer implements IListRenderer, IHeaderTemp } renderElement(element: IActionListItem, _index: number, templateData: IHeaderTemplateData): void { - templateData.text.textContent = element.group?.title ?? ''; + templateData.text.textContent = element.group?.title ?? element.label ?? ''; } disposeTemplate(_templateData: IHeaderTemplateData): void { diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index c7ddb2133d5..d524ae8aef8 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -25,6 +25,7 @@ import { IExtHostRpcService } from './extHostRpcService.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../contrib/chat/common/modelPicker/modelPickerWidget.js'; export interface IExtHostLanguageModels extends ExtHostLanguageModels { } @@ -181,6 +182,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { targetExtensions: metadata.extensions, isDefault: metadata.isDefault, isUserSelectable: metadata.isUserSelectable, + modelPickerCategory: metadata.category ?? DEFAULT_MODEL_PICKER_CATEGORY, capabilities: metadata.capabilities, }); diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts index 3bec3dc0419..bab46807135 100644 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts @@ -101,16 +101,53 @@ export class ModelPickerWidget extends Disposable { * Shows the picker at the specified anchor */ showAt(anchor: HTMLElement | StandardMouseEvent | IAnchor, container?: HTMLElement): void { - const items: IActionListItem[] = this.getActionItems().map(item => ({ - item: item.model, - description: item.model.metadata.description, - kind: ActionListItemKind.Action, - canPreview: false, - group: { title: '', icon: ThemeIcon.fromId(item.isCurrent ? Codicon.check.id : Codicon.blank.id) }, - disabled: false, - hideIcon: false, - label: item.model.metadata.name, - } satisfies IActionListItem)); + const actionItems = this.getActionItems(); + const items: IActionListItem[] = []; + + // Group models by categories + const modelsByCategory = new Map(); + + // First, group models by their categories + for (const item of actionItems) { + const category = item.model.metadata.modelPickerCategory; + if (!modelsByCategory.has(category.label)) { + modelsByCategory.set(category.label, []); + } + modelsByCategory.get(category.label)!.push(item); + } + + for (const [categoryLabel, modelsInCategory] of modelsByCategory.entries()) { + // Skip empty categories + if (modelsInCategory.length === 0) { + continue; + } + + // Add category header + items.push({ + label: categoryLabel, + kind: ActionListItemKind.Header, + canPreview: false, + disabled: false, + hideIcon: true, + } satisfies IActionListItem); + + // Add models in this category + for (const item of modelsInCategory) { + items.push({ + item: item.model, + description: item.model.metadata.description, + kind: ActionListItemKind.Action, + canPreview: false, + group: { title: '', icon: ThemeIcon.fromId(item.isCurrent ? Codicon.check.id : Codicon.blank.id) }, + disabled: false, + hideIcon: false, + label: item.model.metadata.name, + } satisfies IActionListItem); + } + + // Remove this category from the map so we don't process it again + modelsByCategory.delete(categoryLabel); + } const delegate = { onSelect: (item: ILanguageModelChatMetadataAndIdentifier) => { diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 018238b7b4c..281592d3bab 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -127,6 +127,7 @@ export interface ILanguageModelChatMetadata { readonly isDefault?: boolean; readonly isUserSelectable?: boolean; + readonly modelPickerCategory: { label: string }; readonly auth?: { readonly providerLabel: string; readonly accountLabel?: string; diff --git a/src/vs/workbench/contrib/chat/common/modelPicker/modelPickerWidget.ts b/src/vs/workbench/contrib/chat/common/modelPicker/modelPickerWidget.ts new file mode 100644 index 00000000000..57e7eae127b --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/modelPicker/modelPickerWidget.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../nls.js'; + +export const DEFAULT_MODEL_PICKER_CATEGORY = { label: localize('chat.modelPicker.other', "Other Models") }; diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index 06560d694d6..138d176f95b 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -14,6 +14,7 @@ import { ChatMessageRole, IChatResponseFragment, languageModelExtensionPoint, La import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; import { ExtensionsRegistry } from '../../../../services/extensions/common/extensionsRegistry.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/modelPicker/modelPickerWidget.js'; suite('LanguageModels', function () { @@ -50,6 +51,7 @@ suite('LanguageModels', function () { name: 'Pretty Name', vendor: 'test-vendor', family: 'test-family', + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, version: 'test-version', id: 'test-id', maxInputTokens: 100, @@ -70,6 +72,7 @@ suite('LanguageModels', function () { vendor: 'test-vendor', family: 'test2-family', version: 'test2-version', + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, id: 'test-id', maxInputTokens: 100, maxOutputTokens: 100, @@ -119,6 +122,7 @@ suite('LanguageModels', function () { id: 'actual-lm', maxInputTokens: 100, maxOutputTokens: 100, + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, }, sendChatRequest: async (messages, _from, _options, token) => { // const message = messages.at(-1); diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index f3edb530291..b0faf2dd8fa 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -70,6 +70,13 @@ declare module 'vscode' { readonly toolCalling?: boolean; readonly agentMode?: boolean; }; + + /** + * Optional category to group models by in the model picker. + * Has no effect if `isUserSelectable` is `false`. + * If not specified, the model will appear in the "Other Models" category. + */ + readonly category?: { label: string }; } export interface ChatResponseProviderMetadata { From dcfa9b9992ad8d076e2d6c3f9bb88b15c140070f Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 22 Apr 2025 10:25:51 -0700 Subject: [PATCH 048/114] Remove commented out code (#247147) --- .../workbench/contrib/chat/common/chatModel.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 7aedf0ee4c6..006e48dce64 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -1449,22 +1449,6 @@ export class ChatModel extends Disposable implements IChatModel { this._editingSession = new ObservablePromise(editingSessionPromise); this._editingSession.promise.then(editingSession => { this._store.isDisposed ? editingSession.dispose() : this._register(editingSession); - - // const currentStates = new ResourceMap(); - // this._register(autorun(r => { - // editingSession.entries.read(r).forEach(entry => { - // const state = entry.state.read(r); - // if (state !== currentStates.get(entry.modifiedURI)) { - // currentStates.set(entry.modifiedURI, state); - // if (state === ModifiedFileEntryState.Rejected) { - // this.currentWorkingSetEntries.push({ - // uri: entry.modifiedURI, - // state: ChatAgentWorkingSetEntryState.Rejected - // }); - // } - // } - // }); - // })); }); } From c6c85b3e9d3c5b200ee446a209be5473fbcef00c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 22 Apr 2025 10:33:18 -0700 Subject: [PATCH 049/114] pass in an options object instead --- src/vs/workbench/api/common/extHostLanguageModelTools.ts | 8 ++++---- src/vscode-dts/vscode.proposed.toolProgress.d.ts | 9 ++++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index faaa208029e..2cbe65e5701 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -112,9 +112,10 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape throw new Error(`Unknown tool ${dto.toolId}`); } - const options: vscode.LanguageModelToolInvocationOptions = { + const options: vscode.LanguageModelToolInvocation = { input: dto.parameters, toolInvocationToken: dto.context as vscode.ChatParticipantToolToken | undefined, + progress: undefined!, }; if (isProposedApiEnabled(item.extension, 'chatParticipantPrivate')) { options.chatRequestId = dto.chatRequestId; @@ -138,9 +139,8 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } - let progress: vscode.Progress<{ message?: string; increment?: number }> | undefined; if (isProposedApiEnabled(item.extension, 'toolProgress')) { - progress = { + options.progress = { report: value => { this._proxy.$acceptToolProgress(dto.callId, { message: value.message, @@ -151,7 +151,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } - const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token, progress!)), token); + const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token)), token); if (!extensionResult) { throw new CancellationError(); } diff --git a/src/vscode-dts/vscode.proposed.toolProgress.d.ts b/src/vscode-dts/vscode.proposed.toolProgress.d.ts index 51562cd6ee3..6eed0305a81 100644 --- a/src/vscode-dts/vscode.proposed.toolProgress.d.ts +++ b/src/vscode-dts/vscode.proposed.toolProgress.d.ts @@ -19,7 +19,14 @@ declare module 'vscode' { increment?: number; } + export interface LanguageModelToolInvocation extends LanguageModelToolInvocationOptions { + /** + * Progress interface to report updates as the tool is running. + */ + progress: Progress; + } + export interface LanguageModelTool { - invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken, progress: Progress): ProviderResult; + invoke(invocation: LanguageModelToolInvocation, token: CancellationToken): ProviderResult; } } From 8e7731efd3d11d5cb71a6b41aa07911ab8bb6e06 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 22 Apr 2025 19:33:59 +0100 Subject: [PATCH 050/114] Engineering - update endgame notebooks (#247154) --- .vscode/notebooks/endgame.github-issues | 2 +- .vscode/notebooks/my-endgame.github-issues | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index d778179a15d..4911987e921 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\r\n\r\n$MILESTONE=milestone:\"March 2025\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"April 2025\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index c9890631658..f6e6cc35b4f 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"March 2025\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"April 2025\"\n\n$MINE=assignee:@me" }, { "kind": 1, From 44627bca579507d9da9d092135605b95c6ba989f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:56:41 +0100 Subject: [PATCH 051/114] SCM - fix issue related to deleted line decoration (#247161) --- .../contrib/scm/browser/media/dirtydiffDecorator.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css b/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css index 889f1b63fdd..d1490f77723 100644 --- a/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css +++ b/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css @@ -102,12 +102,14 @@ } .monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-added, -.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-modified { +.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-modified, +.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-deleted { transition: opacity 0.5s; } .monaco-editor .margin:hover .dirty-diff-added, -.monaco-editor .margin:hover .dirty-diff-modified { +.monaco-editor .margin:hover .dirty-diff-modified, +.monaco-editor .margin:hover .dirty-diff-deleted { opacity: 1; } From 8e191843f7acb2b41d6b0d0a0a221909b6fd697d Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 22 Apr 2025 13:01:49 -0700 Subject: [PATCH 052/114] mcp: fix tools being unavailable after change in cache (#247160) * mcp: fix tools being unavailable after change in cache Refs #245921. Not sure if this is all the cases, but it's one. * fix --- .../contrib/mcp/common/mcpService.ts | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts index 98a9bddf4e2..bcc7320493a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -7,7 +7,7 @@ import { RunOnceScheduler } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; import { equals } from '../../../../base/common/objects.js'; import { autorun, IObservable, observableValue, transaction } from '../../../../base/common/observable.js'; import { localize } from '../../../../nls.js'; @@ -22,8 +22,7 @@ import { IMcpServer, IMcpService, IMcpTool, McpCollectionDefinition, McpServerDe interface ISyncedToolData { toolData: IToolData; - toolDispose: IDisposable; - implDispose: IDisposable; + store: DisposableStore; } type IMcpServerRec = IReference; @@ -111,27 +110,32 @@ export class McpService extends Disposable implements IMcpService { tags: ['mcp'], }; + const registerTool = (store: DisposableStore) => { + store.add(this._toolsService.registerToolData(toolData)); + store.add(this._toolsService.registerToolImplementation(tool.id, this._instantiationService.createInstance(McpToolImplementation, tool, server))); + }; + + if (existing) { if (!equals(existing.toolData, toolData)) { existing.toolData = toolData; - existing.toolDispose.dispose(); - existing.toolDispose = this._toolsService.registerToolData(toolData); + existing.store.clear(); + // We need to re-register both the data and implementation, as the + // implementation is discarded when the data is removed (#245921) + registerTool(store); } toDelete.delete(tool.id); } else { - tools.set(tool.id, { - toolData, - toolDispose: this._toolsService.registerToolData(toolData), - implDispose: this._toolsService.registerToolImplementation(tool.id, this._instantiationService.createInstance(McpToolImplementation, tool, server)), - }); + const store = new DisposableStore(); + registerTool(store); + tools.set(tool.id, { toolData, store }); } } for (const id of toDelete) { const tool = tools.get(id); if (tool) { - tool.toolDispose.dispose(); - tool.implDispose.dispose(); + tool.store.dispose(); tools.delete(id); } } @@ -139,8 +143,7 @@ export class McpService extends Disposable implements IMcpService { store.add(toDisposable(() => { for (const tool of tools.values()) { - tool.toolDispose.dispose(); - tool.implDispose.dispose(); + tool.store.dispose(); } })); } From 2d6fa91f86acb32c8f8adb1c0ab66ad03d9be7e2 Mon Sep 17 00:00:00 2001 From: Joyce Er <30305945+joyceerhl@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:34:04 -0700 Subject: [PATCH 053/114] fix: emit chat edit signal rejection before doing rejection (#247165) --- .../chat/browser/chatEditing/chatEditingModifiedFileEntry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 67a0a3e60f9..536750cdce5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -184,10 +184,10 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im return; } + this._notifyAction('rejected'); await this._doReject(tx); this._stateObs.set(ModifiedFileEntryState.Rejected, tx); this._autoAcceptCtrl.set(undefined, tx); - this._notifyAction('rejected'); } protected abstract _doReject(tx: ITransaction | undefined): Promise; From bd5d449fe7c76de85073ab3be6eede819dd27944 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Tue, 22 Apr 2025 14:34:51 -0700 Subject: [PATCH 054/114] Adopt base dropdown menu for model picker widget (#247170) --- src/vs/base/browser/ui/dropdown/dropdown.ts | 2 +- .../ui/dropdown/dropdownActionViewItem.ts | 89 ++++++++++++++- .../modelPicker/modelPickerActionItem.ts | 105 ++++++------------ .../browser/modelPicker/modelPickerWidget.ts | 30 ++--- 4 files changed, 131 insertions(+), 95 deletions(-) diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index b4a7d38a6fb..67a53c3b0d4 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -27,7 +27,7 @@ interface IBaseDropdownOptions { labelRenderer?: ILabelRenderer; } -class BaseDropdown extends ActionRunner { +export class BaseDropdown extends ActionRunner { private _element: HTMLElement; private boxContainer?: HTMLElement; private _label?: HTMLElement; diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 89a3bd7b4f3..97ec7fef7cc 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -20,7 +20,7 @@ import { AnchorAlignment } from '../contextview/contextview.js'; import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js'; import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js'; import './dropdown.css'; -import { DropdownMenu, IActionProvider, IDropdownMenuOptions, ILabelRenderer } from './dropdown.js'; +import { BaseDropdown, DropdownMenu, IActionProvider, IDropdownMenuOptions, ILabelRenderer } from './dropdown.js'; export interface IKeybindingProvider { (action: IAction): ResolvedKeybinding | undefined; @@ -40,6 +40,93 @@ export interface IDropdownMenuActionViewItemOptions extends IBaseActionViewItemO readonly skipTelemetry?: boolean; } +export class BaseDropdownMenuActionViewItem extends BaseActionViewItem { + private dropdownMenu: BaseDropdown | undefined; + private actionItem: HTMLElement | null = null; + + private _onDidChangeVisibility = this._register(new Emitter()); + readonly onDidChangeVisibility = this._onDidChangeVisibility.event; + + protected override readonly options: IDropdownMenuActionViewItemOptions = Object.create(null); + + constructor( + action: IAction, + private readonly _dropdownFactory: (container: HTMLElement, labelRender: ILabelRenderer) => BaseDropdown, + ) { + super(null, action); + } + + override render(container: HTMLElement): void { + this.actionItem = container; + + const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => { + this.element = append(el, $('a.action-label')); + return this.renderLabel(this.element); + }; + + this.dropdownMenu = this._register(this._dropdownFactory(container, labelRenderer)); + this._register(this.dropdownMenu.onDidChangeVisibility(visible => { + this.element?.setAttribute('aria-expanded', `${visible}`); + this._onDidChangeVisibility.fire(visible); + })); + + this.updateTooltip(); + this.updateEnabled(); + } + + protected renderLabel(element: HTMLElement): IDisposable | null { + let classNames: string[] = []; + + if (typeof this.options.classNames === 'string') { + classNames = this.options.classNames.split(/\s+/g).filter(s => !!s); + } else if (this.options.classNames) { + classNames = this.options.classNames; + } + + // todo@aeschli: remove codicon, should come through `this.options.classNames` + if (!classNames.find(c => c === 'icon')) { + classNames.push('codicon'); + } + + element.classList.add(...classNames); + + if (this._action.label) { + this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), element, this._action.label)); + } + + return null; + } + + protected setAriaLabelAttributes(element: HTMLElement): void { + element.setAttribute('role', 'button'); + element.setAttribute('aria-haspopup', 'true'); + element.setAttribute('aria-expanded', 'false'); + element.ariaLabel = this._action.label || ''; + } + + protected override getTooltip(): string | undefined { + let title: string | null = null; + + if (this.action.tooltip) { + title = this.action.tooltip; + } else if (this.action.label) { + title = this.action.label; + } + + return title ?? undefined; + } + + show(): void { + this.dropdownMenu?.show(); + } + + protected override updateEnabled(): void { + const disabled = !this.action.enabled; + this.actionItem?.classList.toggle('disabled', disabled); + this.element?.classList.toggle('disabled', disabled); + } +} + export class DropdownMenuActionViewItem extends BaseActionViewItem { private menuActionsOrProvider: readonly IAction[] | IActionProvider; private dropdownMenu: DropdownMenu | undefined; diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts index 25b0c02279c..4a29ae68b39 100644 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts @@ -11,10 +11,8 @@ import { ModelPickerWidget } from './modelPickerWidget.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import * as dom from '../../../../../base/browser/dom.js'; import { renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { ActionViewItem } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; -import { IAnchor } from '../../../../../base/browser/ui/contextview/contextview.js'; +import { BaseDropdownMenuActionViewItem } from '../../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; export interface IModelPickerDelegate { readonly onDidChangeModel: Event; @@ -25,12 +23,10 @@ export interface IModelPickerDelegate { /** * Action view item for selecting a language model in the chat interface. */ -export class ModelPickerActionItem extends ActionViewItem { - private widget: ModelPickerWidget | undefined; - +export class ModelPickerActionItem extends BaseDropdownMenuActionViewItem { constructor( action: IAction, - private readonly currentModel: ILanguageModelChatMetadataAndIdentifier, + private currentModel: ILanguageModelChatMetadataAndIdentifier, private readonly delegate: IModelPickerDelegate, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { @@ -39,84 +35,45 @@ export class ModelPickerActionItem extends ActionViewItem { ...action, label: currentModel.metadata.name, tooltip: localize('chat.modelPicker.label', "Pick Model"), - run: () => { /* Will be overridden by our click handler */ } + run: () => { } }; - super(undefined, actionWithLabel, { label: true }); + super(actionWithLabel, (container, labelRenderer) => { + const widget = this.instantiationService.createInstance( + ModelPickerWidget, + container, + labelRenderer, + () => this.currentModel, + () => this.delegate.getModels(), + (model) => this.delegate.setModel(model) + ); + this._register(widget); + this._register(widget.onDidChangeModel(model => { + this.currentModel = model; + if (this.element) { + this.renderLabel(this.element); + } + })); + return widget; + }); // Listen for model changes from the delegate this._register(delegate.onDidChangeModel(model => { - this.action.label = model.metadata.name; - this.updateLabel(); + this.currentModel = model; + if (this.element) { + this.renderLabel(this.element); + } })); } - /** - * Override rendering of the label to include the dropdown indicator - */ - protected override updateLabel(): void { - if (this.label) { - // Reset the label element with the current model name and a dropdown indicator - dom.reset(this.label, - dom.$('span.chat-model-label', undefined, this.action.label), - ...renderLabelWithIcons(`$(${Codicon.chevronDown.id})`) - ); - } + protected override renderLabel(element: HTMLElement): IDisposable | null { + this.setAriaLabelAttributes(element); + dom.reset(element, dom.$('span.chat-model-label', undefined, this.currentModel.metadata.name), ...renderLabelWithIcons(`$(chevron-down)`)); + return null; } - /** - * Override rendering to add CSS classes and initialize the widget - */ override render(container: HTMLElement): void { super.render(container); - - // Add classes for styling this element container.classList.add('chat-modelPicker-item'); - - // Create the model picker widget that will be shown when clicked - this.widget = this.instantiationService.createInstance( - ModelPickerWidget, - this.currentModel, - () => this.delegate.getModels(), - (model) => this.delegate.setModel(model) - ); - - // Register event handlers - this._register(this.widget.onDidChangeModel(model => { - this.action.label = model.metadata.name; - this.updateLabel(); - })); - } - - /** - * Override the onClick to show our picker widget - */ - override onClick(event: MouseEvent): void { - if (!this.widget || !this.element) { - return; - } - - this.show(this.element); - - event.stopPropagation(); - event.preventDefault(); - } - - show(anchor?: HTMLElement | StandardMouseEvent | IAnchor): void { - if (!this.widget || !this.element) { - return; - } - - // Show the model picker at the current position - this.widget.showAt(anchor ?? this.element); - } - - /** - * Set aria label attributes on the element - */ - protected setAriaLabelAttributes(element: HTMLElement): void { - element.setAttribute('aria-label', localize('chatModelPicker', "Chat Model: {0}", this.action.label)); - element.setAttribute('aria-haspopup', 'true'); - element.setAttribute('role', 'button'); } } diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts index bab46807135..36b8a01be08 100644 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { IAction } from '../../../../../base/common/actions.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IAnchor } from '../../../../../base/browser/ui/contextview/contextview.js'; import { Emitter } from '../../../../../base/common/event.js'; import { ILanguageModelChatMetadataAndIdentifier } from '../../common/languageModels.js'; import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; @@ -18,7 +16,7 @@ import { getFlatActionBarActions } from '../../../../../platform/actions/browser import { ActionListItemKind, IActionListItem } from '../../../../../platform/actionWidget/browser/actionList.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; +import { BaseDropdown, ILabelRenderer } from '../../../../../base/browser/ui/dropdown/dropdown.js'; interface IModelPickerActionItem { model: ILanguageModelChatMetadataAndIdentifier; @@ -28,12 +26,14 @@ interface IModelPickerActionItem { /** * Widget for picking a language model for chat. */ -export class ModelPickerWidget extends Disposable { +export class ModelPickerWidget extends BaseDropdown { private readonly _onDidChangeModel = this._register(new Emitter()); readonly onDidChangeModel = this._onDidChangeModel.event; constructor( - private currentModel: ILanguageModelChatMetadataAndIdentifier, + container: HTMLElement, + labelRenderer: ILabelRenderer, + private readonly getCurrentModel: () => ILanguageModelChatMetadataAndIdentifier, private readonly getModels: () => ILanguageModelChatMetadataAndIdentifier[], private readonly setModel: (model: ILanguageModelChatMetadataAndIdentifier) => void, @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, @@ -42,14 +42,7 @@ export class ModelPickerWidget extends Disposable { @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, @ICommandService private readonly commandService: ICommandService, ) { - super(); - } - - /** - * Get the label to display in the button that shows the current model - */ - get buttonLabel(): string { - return this.currentModel.metadata.name; + super(container, { labelRenderer }); } /** @@ -58,7 +51,7 @@ export class ModelPickerWidget extends Disposable { private getActionItems(): IModelPickerActionItem[] { const items: IModelPickerActionItem[] = this.getModels().map(model => ({ model, - isCurrent: model.identifier === this.currentModel.identifier + isCurrent: model.identifier === this.getCurrentModel().identifier })); return items; @@ -100,7 +93,7 @@ export class ModelPickerWidget extends Disposable { /** * Shows the picker at the specified anchor */ - showAt(anchor: HTMLElement | StandardMouseEvent | IAnchor, container?: HTMLElement): void { + override show(): void { const actionItems = this.getActionItems(); const items: IActionListItem[] = []; @@ -151,9 +144,8 @@ export class ModelPickerWidget extends Disposable { const delegate = { onSelect: (item: ILanguageModelChatMetadataAndIdentifier) => { - if (item.identifier !== this.currentModel.identifier) { + if (item.identifier !== this.getCurrentModel().identifier) { this.setModel(item); - this.currentModel = item; this._onDidChangeModel.fire(item); } this.actionWidgetService.hide(false); @@ -177,8 +169,8 @@ export class ModelPickerWidget extends Disposable { false, items, delegate, - anchor, - container, + this.element, + undefined, buttonBar ); } From 3dd15b5da049a2c1caff5e67c6e067e17e27c1e8 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 22 Apr 2025 08:50:20 -0700 Subject: [PATCH 055/114] add prompt reference parsing logic to the implicit context class --- .../chatPromptAttachmentsCollection.ts | 2 +- .../contrib/chat/browser/chatInputPart.ts | 9 +- .../browser/contrib/chatImplicitContext.ts | 139 ++++++++++++------ .../contrib/chat/common/chatModel.ts | 1 + .../promptSyntax/service/promptsService.ts | 12 +- 5 files changed, 113 insertions(+), 50 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts index fcb37c09f12..fbad507f195 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts @@ -75,7 +75,7 @@ export const toChatVariable = ( const modelDescription = (isPromptFile) ? 'Prompt instructions file' - : undefined; + : 'File attachment'; return { id, diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 701c056a9ae..f737cdaacf0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -163,7 +163,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public async getAttachedAndImplicitContext(sessionId: string): Promise { const contextArr = [...this.attachmentModel.attachments]; if (this.implicitContext?.enabled && this.implicitContext.value) { - contextArr.push(this.implicitContext.toBaseEntry()); + + const implicitChatVariables = await this.implicitContext.toBaseEntries(); + contextArr.push(...implicitChatVariables); } // factor in nested file links of a prompt into the implicit context @@ -899,7 +901,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const attachmentToolbarContainer = elements.attachmentToolbar; this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; if (this.options.enableImplicitContext) { - this._implicitContext = this._register(new ChatImplicitContext()); + this._implicitContext = this._register( + this.instantiationService.createInstance(ChatImplicitContext, undefined), + ); + this._register(this._implicitContext.onDidChangeValue(() => this._handleAttachedContextChange())); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index e6f72768b7d..f0d05b32438 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -9,11 +9,11 @@ import { Disposable, DisposableStore, MutableDisposable } from '../../../../../b import { Schemas } from '../../../../../base/common/network.js'; import { autorun } from '../../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../../base/common/resources.js'; -import { assertDefined } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { Location } from '../../../../../editor/common/languages.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { EditorsOrder } from '../../../../common/editor.js'; @@ -25,8 +25,10 @@ import { IChatService } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/constants.js'; +import { TextModelPromptParser } from '../../common/promptSyntax/parsers/textModelPromptParser.js'; +import { IPromptsService } from '../../common/promptSyntax/service/types.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; -import { createPromptVariableId } from '../chatAttachmentModel/chatPromptAttachmentsCollection.js'; +import { toChatVariable } from '../chatAttachmentModel/chatPromptAttachmentsCollection.js'; export class ChatImplicitContextContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'chat.implicitContext'; @@ -235,20 +237,16 @@ export class ChatImplicitContextContribution extends Disposable implements IWork } export class ChatImplicitContext extends Disposable implements IChatRequestImplicitVariableEntry { + /** + * TODO: @legomushroom + */ + private prompt: TextModelPromptParser | undefined; + get id() { - // IDs for prompt files need to start with a special prefix - // that is used by the copilot extension to identify them - if (this.isPromptFile) { - assertDefined( - this.value, - 'Implicit prompt attachments must have a value.', - ); + if (this.prompt !== undefined) { + const variable = toChatVariable(this.prompt, true); - const uri = URI.isUri(this.value) - ? this.value - : this.value.uri; - - return createPromptVariableId(uri, true); + return variable.id; } if (URI.isUri(this.value)) { @@ -265,12 +263,16 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli } get name(): string { - const fileType = this.isPromptFile ? 'prompt' : 'file'; + if (this.prompt !== undefined) { + const variable = toChatVariable(this.prompt, true); + + return variable.name; + } if (URI.isUri(this.value)) { - return `${fileType}:${basename(this.value)}`; + return `file:${basename(this.value)}`; } else if (this.value) { - return `${fileType}:${basename(this.value.uri)}`; + return `file:${basename(this.value.uri)}`; } else { return 'implicit'; } @@ -279,14 +281,10 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli readonly kind = 'implicit'; get modelDescription(): string { - if (this.isPromptFile) { - if (URI.isUri(this.value)) { - return `User's active prompt file`; - } else if (this._isSelection) { - return `User's active selection inside prompt file`; - } else { - return `User's current visible prompt text`; - } + if (this.prompt !== undefined) { + const variable = toChatVariable(this.prompt, true); + + return variable.modelDescription; } if (URI.isUri(this.value)) { @@ -313,11 +311,6 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli return this._value; } - private _languageId: string | undefined; - get isPromptFile() { - return (this._languageId === PROMPT_LANGUAGE_ID); - } - private _enabled = true; get enabled() { return this._enabled; @@ -328,7 +321,12 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli this._onDidChangeValue.fire(); } - constructor(value?: Location | URI) { + constructor( + // TODO: @legomushroom - remove the constructor param? + value: Location | URI | undefined, + @IPromptsService private readonly promptsService: IPromptsService, + @IModelService private readonly modelService: IModelService, + ) { super(); this._value = value; } @@ -336,26 +334,85 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli setValue(value: Location | URI | undefined, isSelection: boolean, languageId?: string): void { this._value = value; this._isSelection = isSelection; - this._languageId = languageId; + + // remove and dispose existent prompt parser instance + this.removePrompt(); + // if language ID is a 'prompt' language, create a prompt parser instance + if (value && (languageId === PROMPT_LANGUAGE_ID)) { + this.addPrompt(value); + } + this._onDidChangeValue.fire(); } - toBaseEntry(): IChatRequestFileEntry | IPromptVariableEntry { - const result: IChatRequestFileEntry = { + // TODO: @legomushroom - update the return type + async toBaseEntries(): Promise { + if (this.prompt !== undefined) { + const result = [ + ...this.prompt.allValidReferences.map((link) => { + return toChatVariable(link, false); + }), + toChatVariable({ + uri: this.prompt.uri, + // TODO: @legomushroom - check the comment correctness + // the attached file must have been a prompt file therefore + // we force that assumption here; this makes sure that prompts + // in untitled documents can be also attached to the chat input + isPromptFile: true, + }, true), + ]; + + return result; + } + + return [{ kind: 'file', id: this.id, name: this.name, value: this.value, modelDescription: this.modelDescription, - }; + }]; + } - if (this.isPromptFile === false) { - return result; + /** + * TODO: @legomushroom + */ + public get isPromptFile() { + return (this.prompt !== undefined); + } + + /** + * TODO: @legomushroom + */ + private addPrompt( + value: URI | Location, + ): void { + const uri = URI.isUri(value) + ? value + : value.uri; + + const model = this.modelService.getModel(uri); + if ((model === null) || model.isDisposed()) { + // TODO: @legomushroom - log a warning + return; } - return { - ...result, - isRoot: true, - }; + this.prompt = this.promptsService + .getSyntaxParserFor(model) + .start(); + } + + /** + * TODO: @legomushroom + */ + private removePrompt(): void { + this.prompt?.dispose(); + delete this.prompt; + } + + public override dispose(): void { + this.removePrompt(); + + super.dispose(); } } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 006e48dce64..6da2e642d7a 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -135,6 +135,7 @@ export interface IPromptVariableEntry extends IBaseChatRequestVariableEntry { readonly kind: 'file'; readonly value: URI | Location; readonly isRoot: boolean; + readonly modelDescription: string; } export namespace IDiagnosticVariableEntryFilterData { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index c910ffaf4c5..3d562c76c2d 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -3,20 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from '../../../../../../nls.js'; import { URI } from '../../../../../../base/common/uri.js'; import { assert } from '../../../../../../base/common/assert.js'; +import { basename } from '../../../../../../base/common/path.js'; import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { ObjectCache } from '../../../../../../base/common/objectCache.js'; import { TextModelPromptParser } from '../parsers/textModelPromptParser.js'; -import { IChatPromptSlashCommand, IPromptPath, IPromptsService, TPromptsStorage, TPromptsType } from './types.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { PROMPT_FILE_EXTENSION } from '../../../../../../platform/prompts/common/constants.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; -import { PROMPT_FILE_EXTENSION } from '../../../../../../platform/prompts/common/constants.js'; -import { localize } from '../../../../../../nls.js'; -import { ILabelService } from '../../../../../../platform/label/common/label.js'; -import { basename } from '../../../../../../base/common/path.js'; +import { IChatPromptSlashCommand, IPromptPath, IPromptsService, TPromptsStorage, TPromptsType } from './types.js'; /** * Provides prompt services. @@ -81,7 +81,7 @@ export class PromptsService extends Disposable implements IPromptsService { model: ITextModel, ): TextModelPromptParser & { disposed: false } { assert( - !model.isDisposed(), + model.isDisposed() === false, 'Cannot create a prompt syntax parser for a disposed model.', ); From fca0efbf4684eecd3ec11eb367fa75d6aacdc22b Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 22 Apr 2025 08:51:15 -0700 Subject: [PATCH 056/114] remove unused constructor parameter --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 2 +- .../contrib/chat/browser/contrib/chatImplicitContext.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index f737cdaacf0..f93ad344435 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -902,7 +902,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; if (this.options.enableImplicitContext) { this._implicitContext = this._register( - this.instantiationService.createInstance(ChatImplicitContext, undefined), + this.instantiationService.createInstance(ChatImplicitContext), ); this._register(this._implicitContext.onDidChangeValue(() => this._handleAttachedContextChange())); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index f0d05b32438..1a70c23b8ec 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -322,13 +322,10 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli } constructor( - // TODO: @legomushroom - remove the constructor param? - value: Location | URI | undefined, @IPromptsService private readonly promptsService: IPromptsService, @IModelService private readonly modelService: IModelService, ) { super(); - this._value = value; } setValue(value: Location | URI | undefined, isSelection: boolean, languageId?: string): void { From 3fb471595b04120eb204c82eda16064a3fdb065d Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 22 Apr 2025 09:00:44 -0700 Subject: [PATCH 057/114] add documentation comments and cleanup --- .../browser/contrib/chatImplicitContext.ts | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index 1a70c23b8ec..84ade30533a 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -20,7 +20,7 @@ import { EditorsOrder } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { getNotebookEditorFromEditorPane, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; -import { IChatRequestFileEntry, IChatRequestImplicitVariableEntry, IPromptVariableEntry } from '../../common/chatModel.js'; +import { IChatRequestFileEntry, IChatRequestImplicitVariableEntry } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; @@ -238,7 +238,8 @@ export class ChatImplicitContextContribution extends Disposable implements IWork export class ChatImplicitContext extends Disposable implements IChatRequestImplicitVariableEntry { /** - * TODO: @legomushroom + * If the implicit context references a prompt file, this field + * holds a reference to an associated prompt parser instance. */ private prompt: TextModelPromptParser | undefined; @@ -342,44 +343,46 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli this._onDidChangeValue.fire(); } - // TODO: @legomushroom - update the return type - async toBaseEntries(): Promise { - if (this.prompt !== undefined) { - const result = [ - ...this.prompt.allValidReferences.map((link) => { - return toChatVariable(link, false); - }), - toChatVariable({ - uri: this.prompt.uri, - // TODO: @legomushroom - check the comment correctness - // the attached file must have been a prompt file therefore - // we force that assumption here; this makes sure that prompts - // in untitled documents can be also attached to the chat input - isPromptFile: true, - }, true), - ]; + public async toBaseEntries(): Promise { + // chat variable for non-prompt file attachment + if (this.prompt === undefined) { + return [{ + kind: 'file', + id: this.id, + name: this.name, + value: this.value, + modelDescription: this.modelDescription, + }]; - return result; } - return [{ - kind: 'file', - id: this.id, - name: this.name, - value: this.value, - modelDescription: this.modelDescription, - }]; + // prompt can have any number of nested references, hence + // collect all of valid ones and return the entire list + return [ + // add all valid child references in the prompt + ...this.prompt.allValidReferences.map((link) => { + return toChatVariable(link, false); + }), + // and then the root prompt reference itself + toChatVariable({ + uri: this.prompt.uri, + // the attached file must have been a prompt file therefore + // we force that assumption here; this makes sure that prompts + // in untitled documents can be also attached to the chat input + isPromptFile: true, + }, true), + ]; } /** - * TODO: @legomushroom + * Whether the implicit context references a prompt file. */ public get isPromptFile() { return (this.prompt !== undefined); } /** - * TODO: @legomushroom + * Add prompt parser instance for the provided value. */ private addPrompt( value: URI | Location, @@ -400,7 +403,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli } /** - * TODO: @legomushroom + * Remove and dispose prompt parser instance. */ private removePrompt(): void { this.prompt?.dispose(); @@ -409,7 +412,6 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli public override dispose(): void { this.removePrompt(); - super.dispose(); } } From 264a865447410b67a8275b383230d72e3689bf67 Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 22 Apr 2025 09:18:12 -0700 Subject: [PATCH 058/114] wait on prompt resolve/parse process to settle for implicit prompt context --- .../chat/browser/contrib/chatImplicitContext.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index 84ade30533a..e2b11812ff4 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -15,6 +15,7 @@ import { ICodeEditorService } from '../../../../../editor/browser/services/codeE import { Location } from '../../../../../editor/common/languages.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { EditorsOrder } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; @@ -325,6 +326,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli constructor( @IPromptsService private readonly promptsService: IPromptsService, @IModelService private readonly modelService: IModelService, + @ILogService private readonly logService: ILogService, ) { super(); } @@ -358,6 +360,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli // prompt can have any number of nested references, hence // collect all of valid ones and return the entire list + await this.prompt.allSettled(); return [ // add all valid child references in the prompt ...this.prompt.allValidReferences.map((link) => { @@ -392,14 +395,14 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli : value.uri; const model = this.modelService.getModel(uri); - if ((model === null) || model.isDisposed()) { - // TODO: @legomushroom - log a warning - return; + const modelExists = (model !== null); + if ((modelExists === false) || model.isDisposed()) { + return this.logService.warn( + `cannot create prompt parser instance for ${uri.path} (model exists: ${modelExists})`, + ); } - this.prompt = this.promptsService - .getSyntaxParserFor(model) - .start(); + this.prompt = this.promptsService.getSyntaxParserFor(model); } /** From 6619b5d4f6f82fe48f532d44e9d8e1ceb6e5f3cb Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 22 Apr 2025 09:31:51 -0700 Subject: [PATCH 059/114] fix prompt files related chat input message --- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index c0ad2d6e050..d6bae7b8045 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1192,9 +1192,12 @@ export class ChatWidget extends Disposable implements IChatWidget { attachedContext.push(result.attachment); } - await this.setupChatModeAndTools( - attachedContext.filter(isPromptFileChatVariable), - ); + const promptFileVariables = attachedContext.filter(isPromptFileChatVariable); + await this.setupChatModeAndTools(promptFileVariables); + + if (promptFileVariables.length > 0) { + input = `Follow the prompt instructions from ${promptFileVariables.map(v => v.name).join(', ')}\n${input}`; + } } if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentMode === ChatMode.Edit && !this.chatService.edits2Enabled) { From 894abdf1d6ae00e64ee1393cf49b764faf8f635d Mon Sep 17 00:00:00 2001 From: Oleg Solomko Date: Tue, 22 Apr 2025 14:31:31 -0700 Subject: [PATCH 060/114] improve prompts services to not allow for shared prompt parser instance disposal --- .../contrib/chat/browser/contrib/chatImplicitContext.ts | 6 ++---- .../languageFeatures/providers/providerInstanceBase.ts | 5 ++--- .../contrib/chat/common/promptSyntax/service/types.ts | 9 ++++++++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index e2b11812ff4..da0807b736d 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -26,8 +26,7 @@ import { IChatService } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/constants.js'; -import { TextModelPromptParser } from '../../common/promptSyntax/parsers/textModelPromptParser.js'; -import { IPromptsService } from '../../common/promptSyntax/service/types.js'; +import { IPromptsService, TSharedPrompt } from '../../common/promptSyntax/service/types.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { toChatVariable } from '../chatAttachmentModel/chatPromptAttachmentsCollection.js'; @@ -242,7 +241,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli * If the implicit context references a prompt file, this field * holds a reference to an associated prompt parser instance. */ - private prompt: TextModelPromptParser | undefined; + private prompt: TSharedPrompt | undefined; get id() { if (this.prompt !== undefined) { @@ -409,7 +408,6 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli * Remove and dispose prompt parser instance. */ private removePrompt(): void { - this.prompt?.dispose(); delete this.prompt; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceBase.ts index 5a81f665774..2cb0b49b7b1 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceBase.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IPromptsService } from '../../service/types.js'; +import { IPromptsService, TSharedPrompt } from '../../service/types.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; -import { TextModelPromptParser } from '../../parsers/textModelPromptParser.js'; import { ObservableDisposable } from '../../../../../../../base/common/observableDisposable.js'; /** @@ -25,7 +24,7 @@ export abstract class ProviderInstanceBase extends ObservableDisposable { /** * The prompt parser instance. */ - protected readonly parser: TextModelPromptParser; + protected readonly parser: TSharedPrompt; constructor( protected readonly model: ITextModel, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts index 0d92ac1991f..3ed92d63230 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts @@ -45,6 +45,13 @@ export interface IPromptPath { readonly type: TPromptsType; } +/** + * Type for a shared prompt parser instance returned by the {@link IPromptsService}. + * Because the parser is shared, we omit the `dispose` method from + * the original type so the caller cannot dispose it prematurely + */ +export type TSharedPrompt = Omit; + /** * Provides prompt services. */ @@ -57,7 +64,7 @@ export interface IPromptsService extends IDisposable { */ getSyntaxParserFor( model: ITextModel, - ): TextModelPromptParser & { disposed: false }; + ): TSharedPrompt & { disposed: false }; /** * List all available prompt files. From 0204cf08b05ca7425e1758510b673d248a908a5c Mon Sep 17 00:00:00 2001 From: Aaron Munger <2019016+amunger@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:19:10 -0700 Subject: [PATCH 061/114] dont edit added markdown cells on diff navigation (#247178) --- .../notebook/chatEditingNotebookEditorIntegration.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts index 38f75783f3c..fe22b0709d3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts @@ -15,6 +15,7 @@ import { localize } from '../../../../../../nls.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { MenuId } from '../../../../../../platform/actions/common/actions.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IEditorPane, IResourceDiffEditorInput } from '../../../../../common/editor.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; import { NotebookDeletedCellDecorator } from '../../../../notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.js'; @@ -120,6 +121,7 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I @IChatAgentService private readonly _chatAgentService: IChatAgentService, @INotebookEditorService notebookEditorService: INotebookEditorService, @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService, + @ILogService private readonly logService: ILogService, ) { super(); @@ -401,7 +403,8 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I const cellViewModel = this.getCellViewModel(change); if (cellViewModel) { this.updateCurrentIndex(change, indexInCell); - this.revealChangeInView(cellViewModel, textChange?.modified, change); + this.revealChangeInView(cellViewModel, textChange?.modified, change) + .catch(err => { this.logService.warn(`Error revealing change in view: ${err}`); }); return true; } break; @@ -429,11 +432,12 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I private async revealChangeInView(cell: ICellViewModel, lines: LineRange | undefined, change: ICellDiffInfo): Promise { const targetLines = lines ?? new LineRange(0, 0); - if (cell.cellKind === CellKind.Markup && cell.getEditState() === CellEditState.Preview) { + if (change.type === 'modified' && cell.cellKind === CellKind.Markup && cell.getEditState() === CellEditState.Preview) { cell.updateEditState(CellEditState.Editing, 'chatEditNavigation'); } - await this.notebookEditor.focusNotebookCell(cell, 'editor', { focusEditorLine: targetLines.startLineNumber }); + const focusTarget = cell.cellKind === CellKind.Code || change.type === 'modified' ? 'editor' : 'container'; + await this.notebookEditor.focusNotebookCell(cell, focusTarget, { focusEditorLine: targetLines.startLineNumber }); await this.notebookEditor.revealRangeInCenterAsync(cell, new Range(targetLines.startLineNumber, 0, targetLines.endLineNumberExclusive, 0)); } From bbaed6818cb68dce835b01de65816a95dd193ee2 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 22 Apr 2025 17:48:39 -0700 Subject: [PATCH 062/114] mcp: fix not using resolved connection from extension api (#247184) --- src/vs/workbench/api/common/extHostTypes.ts | 2 +- .../contrib/mcp/common/mcpRegistry.ts | 2 +- .../mcp/test/common/mcpRegistry.test.ts | 43 ++++++++++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index b8a8c9c2a8d..c7b7ea6ed80 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -5157,7 +5157,7 @@ export class McpStdioServerDefinition implements vscode.McpStdioServerDefinition public label: string, public command: string, public args: string[], - public env: Record, + public env: Record = {}, public version?: string, ) { } } diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts index 65ba4094e3a..01533b48a6d 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts @@ -370,7 +370,7 @@ export class McpRegistry extends Disposable implements IMcpRegistry { } try { - launch = await this._replaceVariablesInLaunch(definition, definition.launch); + launch = await this._replaceVariablesInLaunch(definition, launch); } catch (e) { this._notificationService.notify({ severity: Severity.Error, diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts index 04baff6b84d..8fc67844c70 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts @@ -25,7 +25,7 @@ import { TestLoggerService, TestStorageService } from '../../../../test/common/w import { McpRegistry } from '../../common/mcpRegistry.js'; import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js'; import { McpServerConnection } from '../../common/mcpServerConnection.js'; -import { LazyCollectionState, McpCollectionDefinition, McpCollectionReference, McpServerDefinition, McpServerTransportType } from '../../common/mcpTypes.js'; +import { LazyCollectionState, McpCollectionDefinition, McpCollectionReference, McpServerDefinition, McpServerTransportStdio, McpServerTransportType } from '../../common/mcpTypes.js'; import { TestMcpMessageTransport } from './mcpRegistryTypes.js'; import { ConfigurationResolverExpression } from '../../../../services/configurationResolver/common/configurationResolverExpression.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; @@ -259,6 +259,47 @@ suite('Workbench - MCP - Registry', () => { connection3.dispose(); }); + test('resolveConnection uses user-provided launch configuration', async () => { + // Create a collection with custom launch resolver + const customCollection: McpCollectionDefinition = { + ...testCollection, + resolveServerLanch: async (def) => { + return { + ...(def.launch as McpServerTransportStdio), + env: { CUSTOM_ENV: 'value' }, + }; + } + }; + + // Create a definition with variable replacement + const definition: McpServerDefinition = { + ...baseDefinition, + variableReplacement: { + section: 'mcp', + target: ConfigurationTarget.WORKSPACE, + } + }; + + const delegate = new TestMcpHostDelegate(); + store.add(registry.registerDelegate(delegate)); + testCollection.serverDefinitions.set([definition], undefined); + store.add(registry.registerCollection(customCollection)); + + // Resolve connection should use the custom launch configuration + const connection = await registry.resolveConnection({ + collectionRef: customCollection, + definitionRef: definition, + logger + }) as McpServerConnection; + + assert.ok(connection); + + // Verify the launch configuration passed to _replaceVariablesInLaunch was the custom one + assert.deepStrictEqual((connection.launchDefinition as McpServerTransportStdio).env, { CUSTOM_ENV: 'value' }); + + connection.dispose(); + }); + suite('Trust Management', () => { setup(() => { const delegate = new TestMcpHostDelegate(); From 38ec4828fff50218c5d3a9f485fb49dd0e7b7264 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 23 Apr 2025 11:04:13 +1000 Subject: [PATCH 063/114] Notify Notebook changes from user to chat (#247183) --- .../chatEditingModifiedNotebookEntry.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index cb5b7ea96e0..ac3ca516761 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -258,24 +258,33 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie // const didResetToOriginalContent = createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService) === this.initialContent; let didResetToOriginalContent = this.initialContentComparer.isEqual(this.modifiedModel); const currentState = this._stateObs.get(); - if (currentState === ModifiedFileEntryState.Rejected) { - return; - } if (currentState === ModifiedFileEntryState.Modified && didResetToOriginalContent) { this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); this.updateCellDiffInfo([], undefined); this.initializeModelsFromDiff(); + this._notifyAction('rejected'); return; } if (!e.rawEvents.length) { return; } + + // If user executes a cell, then mark this notebook as modified by the user. + if (e.rawEvents.some(e => e.kind === NotebookCellsChangeType.ChangeCellInternalMetadata && typeof e.internalMetadata.lastRunSuccess === 'boolean')) { + this._userEditScheduler.schedule(); + } + + if (currentState === ModifiedFileEntryState.Rejected) { + return; + } + if (isTransientIPyNbExtensionEvent(this.modifiedModel.notebookType, e)) { return; } this._allEditsAreFromUs = false; + this._userEditScheduler.schedule(); // Changes to cell text is sync'ed and handled separately. // See ChatEditingNotebookCellEntry._mirrorEdits @@ -504,6 +513,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); this.updateCellDiffInfo([], undefined); this.initializeModelsFromDiff(); + this._notifyAction('userModified'); }, redo: async () => { initial = createSnapshot(this.modifiedModel, transientOptions, outputSizeLimit); @@ -517,6 +527,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie this._stateObs.set(redoState, undefined); this.updateCellDiffInfo([], undefined); this.initializeModelsFromDiff(); + this._notifyAction('userModified'); } }; } @@ -679,6 +690,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie if (new SnapshotComparer(currentSnapshot).isEqual(this.originalModel)) { const state = accepted ? ModifiedFileEntryState.Accepted : ModifiedFileEntryState.Rejected; this._stateObs.set(state, undefined); + this._notifyAction(accepted ? 'accepted' : 'rejected'); } } From cb66f895f960afe46db5f13a622d9dd9aea9bc8e Mon Sep 17 00:00:00 2001 From: Joyce Er <30305945+joyceerhl@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:36:01 -0700 Subject: [PATCH 064/114] fix: chat session ID in `MappedEditsRequest` (#247167) * fix: chat session ID in `MappedEditsRequest` * Plumbing for chat session ID in code blocks --- .../api/browser/mainThreadChatCodeMapper.ts | 1 + src/vs/workbench/api/common/extHostCodeMapper.ts | 1 + .../accessibility/browser/accessibleView.ts | 5 +++-- .../chat/browser/actions/chatCodeblockActions.ts | 3 ++- .../chat/browser/actions/codeBlockOperations.ts | 13 +++++++------ src/vs/workbench/contrib/chat/browser/chat.ts | 1 + .../chatCollapsibleContentPart.ts | 3 ++- .../chatContentParts/chatMarkdownContentPart.ts | 4 +++- .../chatContentParts/chatToolInvocationPart.ts | 15 ++++++++++----- .../contrib/chat/browser/codeBlockPart.ts | 5 +++++ .../contrib/chat/common/chatCodeMapperService.ts | 1 + .../contrib/chat/common/tools/editFileTool.ts | 1 + .../chat/browser/terminalChatController.ts | 3 ++- .../vscode.proposed.mappedEditsProvider.d.ts | 1 + 14 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts index 152c23193e2..4f15773352b 100644 --- a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts +++ b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts @@ -38,6 +38,7 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo codeBlocks: uiRequest.codeBlocks, chatRequestId: uiRequest.chatRequestId, chatRequestModel: uiRequest.chatRequestModel, + chatSessionId: uiRequest.chatSessionId, location: uiRequest.location }; try { diff --git a/src/vs/workbench/api/common/extHostCodeMapper.ts b/src/vs/workbench/api/common/extHostCodeMapper.ts index 1039d66155a..5e22a066b8e 100644 --- a/src/vs/workbench/api/common/extHostCodeMapper.ts +++ b/src/vs/workbench/api/common/extHostCodeMapper.ts @@ -53,6 +53,7 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape location: internalRequest.location, chatRequestId: internalRequest.chatRequestId, chatRequestModel: internalRequest.chatRequestModel, + chatSessionId: internalRequest.chatSessionId, codeBlocks: internalRequest.codeBlocks.map(block => { return { code: block.code, diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index f4aee6bce89..21be375a627 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -62,6 +62,7 @@ interface ICodeBlock { endLine: number; code: string; languageId?: string; + chatSessionId: string | undefined; } export class AccessibleView extends Disposable implements ITextModelContentProvider { @@ -255,7 +256,7 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi if (!codeBlock || codeBlockIndex === undefined) { return; } - return { code: codeBlock.code, languageId: codeBlock.languageId, codeBlockIndex, element: undefined }; + return { code: codeBlock.code, languageId: codeBlock.languageId, codeBlockIndex, element: undefined, chatSessionId: codeBlock.chatSessionId }; } navigateToCodeBlock(type: 'next' | 'previous'): void { @@ -399,7 +400,7 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi inBlock = false; const endLine = i; const code = lines.slice(startLine, endLine).join('\n'); - this._codeBlocks?.push({ startLine, endLine, code, languageId }); + this._codeBlocks?.push({ startLine, endLine, code, languageId, chatSessionId: undefined }); } }); this._accessibleViewContainsCodeBlocks.set(this._codeBlocks.length > 0); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 7e6a74fba21..e6f04e259e8 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -538,7 +538,8 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): codeBlockIndex: codeBlockInfo.codeBlockIndex, code: editor.getValue(), languageId: editor.getModel()!.getLanguageId(), - codemapperUri: codeBlockInfo.codemapperUri + codemapperUri: codeBlockInfo.codemapperUri, + chatSessionId: codeBlockInfo.chatSessionId, }; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 6858766505a..91581f38c9a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -151,7 +151,7 @@ export class ApplyCodeBlockOperation { let result: IComputeEditsResult | undefined = undefined; if (activeEditorControl && !this.notebookService.hasSupportedNotebooks(codemapperUri)) { - result = await this.handleTextEditor(activeEditorControl, context.code); + result = await this.handleTextEditor(activeEditorControl, context.chatSessionId, context.code); } else { const activeNotebookEditor = getActiveNotebookEditor(this.editorService); if (activeNotebookEditor) { @@ -226,14 +226,14 @@ export class ApplyCodeBlockOperation { return undefined; } - private async handleTextEditor(codeEditor: IActiveCodeEditor, code: string): Promise { + private async handleTextEditor(codeEditor: IActiveCodeEditor, chatSessionId: string | undefined, code: string): Promise { const activeModel = codeEditor.getModel(); if (isReadOnly(activeModel, this.textFileService)) { this.notify(localize('applyCodeBlock.readonly', "Cannot apply code block to read-only file.")); return undefined; } - const codeBlock = { code, resource: activeModel.uri, markdownBeforeBlock: undefined }; + const codeBlock = { code, resource: activeModel.uri, chatSessionId, markdownBeforeBlock: undefined }; const codeMapper = this.codeMapperService.providers[0]?.displayName; if (!codeMapper) { @@ -247,7 +247,7 @@ export class ApplyCodeBlockOperation { { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, async progress => { progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) }); - const editsIterable = this.getEdits(codeBlock, cancellationTokenSource.token); + const editsIterable = this.getEdits(codeBlock, chatSessionId, cancellationTokenSource.token); return await this.waitForFirstElement(editsIterable); }, () => cancellationTokenSource.cancel() @@ -267,10 +267,11 @@ export class ApplyCodeBlockOperation { }; } - private getEdits(codeBlock: ICodeMapperCodeBlock, token: CancellationToken): AsyncIterable { + private getEdits(codeBlock: ICodeMapperCodeBlock, chatSessionId: string | undefined, token: CancellationToken): AsyncIterable { return new AsyncIterableObject(async executor => { const request: ICodeMapperRequest = { - codeBlocks: [codeBlock] + codeBlocks: [codeBlock], + chatSessionId }; const response: ICodeMapperResponse = { textEdit: (target: URI, edit: TextEdit[]) => { diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 3205aaf7a37..857fd1aaec7 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -101,6 +101,7 @@ export interface IChatCodeBlockInfo { readonly uriPromise: Promise; codemapperUri: URI | undefined; readonly isStreaming: boolean; + readonly chatSessionId: string; focus(): void; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollapsibleContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollapsibleContentPart.ts index b78f30375ed..492eb521655 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollapsibleContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollapsibleContentPart.ts @@ -156,7 +156,8 @@ export class ChatCollapsibleEditorContentPart extends ChatCollapsibleContentPart codeBlockPartIndex: 0, element: this.context.element, parentContextKeyService: this.contextKeyService, - renderOptions: this.options + renderOptions: this.options, + chatSessionId: this.codeBlockInfo.chatSessionId }; this._editorReference.object.render(data, this._currentWidth || 300); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 68aa1a45d3e..295e2a0223a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -141,7 +141,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP if (hideToolbar !== undefined) { renderOptions.hideToolbar = hideToolbar; } - const codeBlockInfo: ICodeBlockData = { languageId, textModel, codeBlockIndex: globalIndex, codeBlockPartIndex: thisPartIndex, element, range, parentContextKeyService: contextKeyService, vulns, codemapperUri: codeblockEntry?.codemapperUri, renderOptions }; + const codeBlockInfo: ICodeBlockData = { languageId, textModel, codeBlockIndex: globalIndex, codeBlockPartIndex: thisPartIndex, element, range, parentContextKeyService: contextKeyService, vulns, codemapperUri: codeblockEntry?.codemapperUri, renderOptions, chatSessionId: element.sessionId }; if (element.isCompleteAddedRequest || !codeblockEntry?.codemapperUri || !codeblockEntry.isEdit) { const ref = this.renderCodeBlock(codeBlockInfo, text, isCodeBlockComplete, currentWidth); @@ -157,6 +157,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP readonly codeBlockIndex = globalIndex; readonly elementId = element.id; readonly isStreaming = false; + readonly chatSessionId = element.sessionId; codemapperUri = undefined; // will be set async public get uri() { // here we must do a getter because the ref.object is rendered @@ -190,6 +191,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP readonly elementId = element.id; readonly isStreaming = !isCodeBlockComplete; readonly codemapperUri = codeblockEntry?.codemapperUri; + readonly chatSessionId = element.sessionId; public get uri() { return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts index 86a5d9e5991..16435e642ef 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts @@ -289,7 +289,8 @@ class ChatToolInvocationSubPart extends Disposable { element: this.context.element, languageId: langId ?? 'json', renderOptions: codeBlockRenderOptions, - textModel: Promise.resolve(model) + textModel: Promise.resolve(model), + chatSessionId: this.context.element.sessionId }, this.currentWidthDelegate()); this._codeblocks.push({ codeBlockIndex: this.codeBlockStartIndex, @@ -299,7 +300,8 @@ class ChatToolInvocationSubPart extends Disposable { isStreaming: false, ownerMarkdownPartId: this.codeblocksPartId, uri: model.uri, - uriPromise: Promise.resolve(model.uri) + uriPromise: Promise.resolve(model.uri), + chatSessionId: this.context.element.sessionId }); this._register(editor.object.onDidChangeContentHeight(() => { editor.object.layout(this.currentWidthDelegate()); @@ -410,7 +412,8 @@ class ChatToolInvocationSubPart extends Disposable { element: this.context.element, languageId: langId, renderOptions: codeBlockRenderOptions, - textModel: Promise.resolve(model) + textModel: Promise.resolve(model), + chatSessionId: this.context.element.sessionId }, this.currentWidthDelegate()); this._register(thenIfNotDisposed(renderPromise, () => this._onDidChangeHeight.fire())); this._codeblocks.push({ @@ -421,7 +424,8 @@ class ChatToolInvocationSubPart extends Disposable { isStreaming: false, ownerMarkdownPartId: this.codeblocksPartId, uri: model.uri, - uriPromise: Promise.resolve(model.uri) + uriPromise: Promise.resolve(model.uri), + chatSessionId: this.context.element.sessionId }); this._register(editor.object.onDidChangeContentHeight(() => { editor.object.layout(this.currentWidthDelegate()); @@ -533,7 +537,8 @@ class ChatToolInvocationSubPart extends Disposable { isStreaming: false, ownerMarkdownPartId: this.codeblocksPartId, uri: model.uri, - uriPromise: Promise.resolve(model.uri) + uriPromise: Promise.resolve(model.uri), + chatSessionId: this.context.element.sessionId } )); this._register(collapsibleListPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 4974fd399a9..9ef202f62c7 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -89,6 +89,8 @@ export interface ICodeBlockData { readonly parentContextKeyService?: IContextKeyService; readonly renderOptions?: ICodeBlockRenderOptions; + + readonly chatSessionId: string; } /** @@ -135,6 +137,8 @@ export interface ICodeBlockActionContext { languageId?: string; codeBlockIndex: number; element: unknown; + + chatSessionId: string | undefined; } export interface ICodeBlockRenderOptions { @@ -451,6 +455,7 @@ export class CodeBlockPart extends Disposable { element: data.element, languageId: textModel.getLanguageId(), codemapperUri: data.codemapperUri, + chatSessionId: data.chatSessionId } satisfies ICodeBlockActionContext; this.resourceContextKey.set(textModel.uri); } diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index efa8b7781de..749850a7aee 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -25,6 +25,7 @@ export interface ICodeMapperRequest { readonly codeBlocks: ICodeMapperCodeBlock[]; readonly chatRequestId?: string; readonly chatRequestModel?: string; + readonly chatSessionId?: string; readonly location?: string; } diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index ba9a79c3f1a..cdeff278af1 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -102,6 +102,7 @@ export class EditTool implements IToolImpl { location: 'tool', chatRequestId: invocation.chatRequestId, chatRequestModel: invocation.modelId, + chatSessionId: invocation.context.sessionId, }, { textEdit: (target, edits) => { model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 7e67399b2d6..fbf6c29bf3b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -68,7 +68,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr element: editor, code: editor.getValue(), codeBlockIndex: 0, - languageId: editor.getModel()!.getLanguageId() + languageId: editor.getModel()!.getLanguageId(), + chatSessionId: this._terminalChatWidget.value.inlineChatWidget.chatWidget.viewModel?.sessionId }; } }, 'terminal')); diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index b119fc57672..ae22bc707b2 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -76,6 +76,7 @@ declare module 'vscode' { readonly location?: string; readonly chatRequestId?: string; readonly chatRequestModel?: string; + readonly chatSessionId?: string; } export interface MappedEditsResponseStream { From 97d28fc6876acd23a3aee14c07a65a89bdc671cb Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 23 Apr 2025 09:37:35 +0200 Subject: [PATCH 065/114] Tweak file/folder suggestions in chat input (#247196) * use correct word-pattern when compute file/folder suggestions fixes https://github.com/microsoft/vscode-copilot/issues/16312 * merge folder and file suggestions into one --- .../browser/contrib/chatDynamicVariables.ts | 1 + .../browser/contrib/chatInputCompletions.ts | 151 +++--------------- 2 files changed, 25 insertions(+), 127 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 042d6c5e4a4..1e9f6118492 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -369,6 +369,7 @@ export async function searchFilesAndFolders( shouldGlobMatchFilePattern: true, cacheKey, excludePattern: searchExcludePattern, + sortByScore: true, }; let searchResult: ISearchComplete | undefined; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 41172b10850..6098f443e1d 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -9,7 +9,6 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { isPatternInWord } from '../../../../../base/common/filters.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; -import { dirname } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; @@ -24,8 +23,8 @@ import { localize } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { FileKind } from '../../../../../platform/files/common/files.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; @@ -33,7 +32,6 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IHistoryService } from '../../../../services/history/common/history.js'; import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; -import { QueryBuilder } from '../../../../services/search/common/queryBuilder.js'; import { ISearchService } from '../../../../services/search/common/search.js'; import { IChatAgentData, IChatAgentNameService, IChatAgentService, getFullyQualifiedId } from '../../common/chatAgents.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; @@ -45,7 +43,7 @@ import { IPromptsService } from '../../common/promptSyntax/service/types.js'; import { ChatSubmitAction } from '../actions/chatExecuteActions.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { ChatInputPart } from '../chatInputPart.js'; -import { ChatDynamicVariableModel, getTopLevelFolders, searchFilesAndFolders } from './chatDynamicVariables.js'; +import { ChatDynamicVariableModel, searchFilesAndFolders } from './chatDynamicVariables.js'; class SlashCommandCompletions extends Disposable { constructor( @@ -501,7 +499,6 @@ class BuiltinDynamicCompletions extends Disposable { private static readonly addReferenceCommand = '_addReferenceCmd'; private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}[\\w:-]*`, 'g'); // MUST be using `g`-flag - private readonly queryBuilder: QueryBuilder; constructor( @IHistoryService private readonly historyService: IHistoryService, @@ -511,43 +508,23 @@ class BuiltinDynamicCompletions extends Disposable { @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IOutlineModelService private readonly outlineService: IOutlineModelService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IFileService private readonly fileService: IFileService, ) { super(); - // File completions - this.registerVariableCompletions('file', async ({ widget, range, position, model }, token) => { + // File/Folder completions in one go and m + const fileWordPattern = new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'); + this.registerVariableCompletions('fileAndFolder', async ({ widget, range }, token) => { if (!widget.supportsFileReferences) { return; } - const result: CompletionList = { suggestions: [] }; - const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); - if (range2) { - await this.addFileEntries(widget, result, range2, token); - } - + await this.addFileAndFolderEntries(widget, result, range, token); return result; - }); - // Folder completions - this.registerVariableCompletions('folder', async ({ widget, range, position, model }, token) => { - if (!widget.supportsFileReferences) { - return; - } - - const result: CompletionList = { suggestions: [] }; - const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); - if (range2) { - await this.addFolderEntries(widget, result, range2, token); - } - - return result; - }); + }, fileWordPattern); // Selection completion this.registerVariableCompletions('selection', ({ widget, range }, token) => { @@ -611,11 +588,9 @@ class BuiltinDynamicCompletions extends Disposable { }); this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg))); - - this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); } - private registerVariableCompletions(debugName: string, provider: (details: IVariableCompletionsDetails, token: CancellationToken) => ProviderResult) { + private registerVariableCompletions(debugName: string, provider: (details: IVariableCompletionsDetails, token: CancellationToken) => ProviderResult, wordPattern: RegExp = BuiltinDynamicCompletions.VariableNameDef) { this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: `chatVarCompletions-${debugName}`, triggerCharacters: [chatVariableLeader], @@ -625,7 +600,7 @@ class BuiltinDynamicCompletions extends Disposable { return; } - const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef, true); + const range = computeCompletionRanges(model, position, wordPattern, true); if (range) { return provider({ model, position, widget, range, context }, token); } @@ -637,9 +612,9 @@ class BuiltinDynamicCompletions extends Disposable { private cacheKey?: { key: string; time: number }; - private async addFileEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) { + private async addFileAndFolderEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) { - const makeFileCompletionItem = (resource: URI, description?: string): CompletionItem => { + const makeCompletionItem = (resource: URI, kind: FileKind, description?: string): CompletionItem => { const basename = this.labelService.getUriBasenameLabel(resource); const text = `${chatVariableLeader}file:${basename}`; @@ -654,12 +629,13 @@ class BuiltinDynamicCompletions extends Disposable { filterText: `${chatVariableLeader}${basename}`, insertText: info.varWord?.endColumn === info.replace.endColumn ? `${text} ` : text, range: info, - kind: CompletionItemKind.File, + kind: kind === FileKind.FILE ? CompletionItemKind.File : CompletionItemKind.Folder, sortText, command: { id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { id: resource.toString(), - isFile: true, + isFile: kind === FileKind.FILE, + isDirectory: kind === FileKind.FOLDER, range: { startLineNumber: info.replace.startLineNumber, startColumn: info.replace.startColumn, endLineNumber: info.replace.endLineNumber, endColumn: info.replace.startColumn + text.length }, data: resource })] @@ -684,7 +660,7 @@ class BuiltinDynamicCompletions extends Disposable { continue; } seen.add(relatedFile.uri); - result.suggestions.push(makeFileCompletionItem(relatedFile.uri, relatedFile.description)); + result.suggestions.push(makeCompletionItem(relatedFile.uri, FileKind.FILE, relatedFile.description)); } } } @@ -706,7 +682,7 @@ class BuiltinDynamicCompletions extends Disposable { } seen.add(item.resource); - const newLen = result.suggestions.push(makeFileCompletionItem(item.resource)); + const newLen = result.suggestions.push(makeCompletionItem(item.resource, FileKind.FILE)); if (newLen - len >= 5) { break; } @@ -717,95 +693,16 @@ class BuiltinDynamicCompletions extends Disposable { if (pattern) { const cacheKey = this.updateCacheKey(); + const workspaces = this.workspaceContextService.getWorkspace().folders.map(folder => folder.uri); - const query = this.queryBuilder.file(this.workspaceContextService.getWorkspace().folders, { - filePattern: pattern, - sortByScore: true, - maxResults: 250, - cacheKey: cacheKey.key - }); - - const data = await this.searchService.fileSearch(query, token); - for (const match of data.results) { - if (seen.has(match.resource)) { - // already included via history - continue; + for (const workspace of workspaces) { + const { folders, files } = await searchFilesAndFolders(workspace, pattern, true, token, cacheKey.key, this.configurationService, this.searchService); + for (const file of files) { + result.suggestions.push(makeCompletionItem(file, FileKind.FILE)); } - result.suggestions.push(makeFileCompletionItem(match.resource)); - } - } - - // mark results as incomplete because further typing might yield - // in more search results - result.incomplete = true; - } - - private async addFolderEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) { - - const folderLeader = `${chatVariableLeader}folder:`; - - const makeFolderCompletionItem = (resource: URI, description?: string): CompletionItem => { - - const basename = this.labelService.getUriBasenameLabel(resource); - const text = `${folderLeader}${basename}`; - const uriLabel = this.labelService.getUriLabel(dirname(resource), { relative: true }); - const labelDescription = description - ? localize('folderEntryDescription', '{0} ({1})', uriLabel, description) - : uriLabel; - const sortText = description ? 'z' : '{'; // after `z` - - return { - label: { label: basename, description: labelDescription }, - filterText: `${folderLeader}${basename}`, - insertText: info.varWord?.endColumn === info.replace.endColumn ? `${text} ` : text, - range: info, - kind: CompletionItemKind.Folder, - sortText, - command: { - id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { - id: 'vscode.folder', - isFile: false, - isDirectory: true, - range: { startLineNumber: info.replace.startLineNumber, startColumn: info.replace.startColumn, endLineNumber: info.replace.endLineNumber, endColumn: info.replace.startColumn + text.length }, - data: resource - })] + for (const folder of folders) { + result.suggestions.push(makeCompletionItem(folder, FileKind.FOLDER)); } - }; - }; - - const seen = new ResourceSet(); - const workspaces = this.workspaceContextService.getWorkspace().folders.map(folder => folder.uri); - - let pattern: string | undefined; - if (info.varWord?.word && info.varWord.word.startsWith(folderLeader)) { - pattern = info.varWord.word.toLowerCase().slice(folderLeader.length); - - for (const folder of await getTopLevelFolders(workspaces, this.fileService)) { - result.suggestions.push(makeFolderCompletionItem(folder)); - seen.add(folder); - } - } - - // SEARCH - // use folder search when having a pattern - if (pattern) { - - const cacheKey = this.updateCacheKey(); - - const folders: URI[][] = []; - - await Promise.all(workspaces.map(async workspace => { - const result = await searchFilesAndFolders(workspace, pattern, true, token, cacheKey.key, this.configurationService, this.searchService); - folders.push(result.folders); - })); - - for (const resource of folders.flat()) { - if (seen.has(resource)) { - // already included via history - continue; - } - seen.add(resource); - result.suggestions.push(makeFolderCompletionItem(resource)); } } From 2c44e9cff858a252ca1aa94a11c6150941e96ba7 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:33:53 +0100 Subject: [PATCH 066/114] =?UTF-8?q?SCM=20-=20=F0=9F=92=84=20quick=20diff?= =?UTF-8?q?=20service=20cleanup=20(#247198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contrib/scm/common/quickDiffService.ts | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/scm/common/quickDiffService.ts b/src/vs/workbench/contrib/scm/common/quickDiffService.ts index 3942c918364..dd6740abbd6 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiffService.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiffService.ts @@ -89,21 +89,33 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { .filter(provider => !provider.rootUri || this.uriIdentityService.extUri.isEqualOrParent(uri, provider.rootUri)) .sort(createProviderComparer(uri)); - const quickDiffs = await Promise.all(providers.map(async provider => { - const visible = this.isQuickDiffProviderVisible(provider.id); + const quickDiffOriginalResources = await Promise.all(providers.map(async provider => { const scoreValue = provider.selector ? score(provider.selector, uri, language, isSynchronized, undefined, undefined) : 10; const originalResource = scoreValue > 0 ? await provider.getOriginalResource(uri) ?? undefined : undefined; - - return { - id: provider.id, - label: provider.label, - visible: visible, - kind: provider.kind, - originalResource - } satisfies Partial; + return { id: provider.id, originalResource }; })); - return quickDiffs.filter(this.isQuickDiff); + const quickDiffs: QuickDiff[] = []; + for (const { id, originalResource } of quickDiffOriginalResources) { + if (!originalResource) { + continue; + } + + const provider = this.quickDiffProviders.get(id); + if (!provider) { + continue; + } + + quickDiffs.push({ + id: provider.id, + label: provider.label, + kind: provider.kind, + visible: this.isQuickDiffProviderVisible(provider.id), + originalResource, + } satisfies QuickDiff); + } + + return quickDiffs; } toggleQuickDiffProviderVisibility(id: string): void { @@ -141,10 +153,6 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { this.storageService.store(QuickDiffService.STORAGE_KEY, JSON.stringify(Array.from(this.hiddenQuickDiffProviders)), StorageScope.PROFILE, StorageTarget.USER); } } - - private isQuickDiff(diff: { id?: string; label?: string; originalResource?: URI }): diff is QuickDiff { - return typeof diff.id === 'string' && typeof diff.label === 'string' && !!diff.originalResource; - } } export async function getOriginalResource(quickDiffService: IQuickDiffService, uri: URI, language: string | undefined, isSynchronized: boolean | undefined): Promise { From b790a4d15d2288f3978eb4ddae52befc014d285c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 23 Apr 2025 10:48:18 +0100 Subject: [PATCH 067/114] SCM - more quick diff cleanup (#247205) * SCM - more quick diff cleanup * Missed something --- src/vs/workbench/api/common/extHostQuickDiff.ts | 6 ++++-- src/vs/workbench/contrib/scm/browser/quickDiffModel.ts | 4 ++-- src/vs/workbench/contrib/scm/common/quickDiff.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/common/extHostQuickDiff.ts b/src/vs/workbench/api/common/extHostQuickDiff.ts index 5418497ce54..b17b67c6a3d 100644 --- a/src/vs/workbench/api/common/extHostQuickDiff.ts +++ b/src/vs/workbench/api/common/extHostQuickDiff.ts @@ -10,7 +10,7 @@ import { ExtHostQuickDiffShape, IMainContext, MainContext, MainThreadQuickDiffSh import { asPromise } from '../../../base/common/async.js'; import { DocumentSelector } from './extHostTypeConverters.js'; import { IURITransformer } from '../../../base/common/uriIpc.js'; -import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; export class ExtHostQuickDiff implements ExtHostQuickDiffShape { private static handlePool: number = 0; @@ -40,7 +40,9 @@ export class ExtHostQuickDiff implements ExtHostQuickDiffShape { registerQuickDiffProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, id: string, label: string, rootUri?: vscode.Uri): vscode.Disposable { const handle = ExtHostQuickDiff.handlePool++; this.providers.set(handle, quickDiffProvider); - this.proxy.$registerQuickDiffProvider(handle, DocumentSelector.from(selector, this.uriTransformer), `${extension.identifier.value}.${id}`, label, rootUri); + + const extensionId = ExtensionIdentifier.toKey(extension.identifier); + this.proxy.$registerQuickDiffProvider(handle, DocumentSelector.from(selector, this.uriTransformer), `${extensionId}.${id}`, label, rootUri); return { dispose: () => { this.proxy.$unregisterQuickDiffProvider(handle); diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index 4f9f36f99a4..5461cfda2d4 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -181,12 +181,11 @@ export class QuickDiffModel extends Disposable { .filter(change => change.label === quickDiff.label); return { - label: quickDiff.label, original: quickDiff.originalResource, modified: this._model.resource, changes: changes.map(change => change.change), changes2: changes.map(change => change.change2) - }; + } satisfies QuickDiffResult; }); } @@ -290,6 +289,7 @@ export class QuickDiffModel extends Disposable { } allDiffs.push({ + providerId: quickDiff.id, label: quickDiff.label, original: quickDiff.originalResource, modified: this._model.resource, diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index f290f4dc0f2..acd2db337eb 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -84,6 +84,7 @@ export interface QuickDiff { } export interface QuickDiffChange { + readonly providerId: string; readonly label: string; readonly original: URI; readonly modified: URI; @@ -92,7 +93,6 @@ export interface QuickDiffChange { } export interface QuickDiffResult { - readonly label: string; readonly original: URI; readonly modified: URI; readonly changes: IChange[]; From 85ae1182434b6542af250db1795cffc71b74d882 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:48:49 +0100 Subject: [PATCH 068/114] =?UTF-8?q?SCM=20-=20=F0=9F=92=84=20more=20usage?= =?UTF-8?q?=20of=20the=20quick=20diff=20provider=20id=20instead=20of=20lab?= =?UTF-8?q?el=20(#247212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vs/workbench/api/browser/mainThreadEditors.ts | 2 +- .../contrib/scm/browser/quickDiffDecorator.ts | 6 +++--- .../workbench/contrib/scm/browser/quickDiffModel.ts | 12 ++++++------ .../workbench/contrib/scm/browser/quickDiffWidget.ts | 5 +++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 1a7074347d4..5bf9b358673 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -398,7 +398,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { try { const primaryQuickDiff = quickDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.kind === 'primary'); - const primaryQuickDiffChanges = quickDiffModelRef.object.changes.filter(change => change.label === primaryQuickDiff?.label); + const primaryQuickDiffChanges = quickDiffModelRef.object.changes.filter(change => change.providerId === primaryQuickDiff?.id); return Promise.resolve(primaryQuickDiffChanges.map(change => change.change) ?? []); } finally { diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts index 20d58302c89..8d02e737e23 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts @@ -134,12 +134,12 @@ class QuickDiffDecorator extends Disposable { const pattern = this.configurationService.getValue<{ added: boolean; modified: boolean }>('scm.diffDecorationsGutterPattern'); const primaryQuickDiff = this.quickDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.kind === 'primary'); - const primaryQuickDiffChanges = this.quickDiffModelRef.object.changes.filter(labeledChange => labeledChange.label === primaryQuickDiff?.label); + const primaryQuickDiffChanges = this.quickDiffModelRef.object.changes.filter(change => change.providerId === primaryQuickDiff?.id); const decorations: IModelDeltaDecoration[] = []; for (const change of this.quickDiffModelRef.object.changes) { const quickDiff = this.quickDiffModelRef.object.quickDiffs - .find(quickDiff => quickDiff.label === change.label); + .find(quickDiff => quickDiff.id === change.providerId); if (!quickDiff?.visible) { // Not visible @@ -357,7 +357,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben const visibleDecorationCount = observableFromEvent(this, quickDiffModelRef.object.onDidChange, () => { const visibleQuickDiffs = quickDiffModelRef.object.quickDiffs.filter(quickDiff => quickDiff.visible); - return quickDiffModelRef.object.changes.filter(labeledChange => visibleQuickDiffs.some(quickDiff => quickDiff.label === labeledChange.label)).length; + return quickDiffModelRef.object.changes.filter(change => visibleQuickDiffs.some(quickDiff => quickDiff.id === change.providerId)).length; }); store.add(autorun(reader => { diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index 5461cfda2d4..06d6e885c8f 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -178,7 +178,7 @@ export class QuickDiffModel extends Disposable { public getQuickDiffResults(): QuickDiffResult[] { return this._quickDiffs.map(quickDiff => { const changes = this.changes - .filter(change => change.label === quickDiff.label); + .filter(change => change.providerId === quickDiff.id); return { original: quickDiff.originalResource, @@ -339,7 +339,7 @@ export class QuickDiffModel extends Disposable { return []; } - if (equals(this._quickDiffs, quickDiffs, (a, b) => a.originalResource.toString() === b.originalResource.toString() && a.label === b.label && a.visible === b.visible)) { + if (equals(this._quickDiffs, quickDiffs, (a, b) => a.originalResource.toString() === b.originalResource.toString() && a.id === b.id && a.visible === b.visible)) { return quickDiffs; } @@ -411,11 +411,11 @@ export class QuickDiffModel extends Disposable { return nextChange !== -1 ? nextChange : 0; } - const primaryQuickDiffLabel = this.quickDiffs - .find(quickDiff => quickDiff.kind === 'primary')?.label; + const primaryQuickDiffId = this.quickDiffs + .find(quickDiff => quickDiff.kind === 'primary')?.id; const primaryInclusiveChangeIndex = this.changes - .findIndex(change => change.label === primaryQuickDiffLabel && + .findIndex(change => change.providerId === primaryQuickDiffId && change.change.modifiedStartLineNumber <= lineNumber && getModifiedEndLineNumber(change.change) >= lineNumber); @@ -438,7 +438,7 @@ export class QuickDiffModel extends Disposable { } // Skip quick diffs that are not visible - if (!this.quickDiffs.find(quickDiff => quickDiff.label === this.changes[i].label)?.visible) { + if (!this.quickDiffs.find(quickDiff => quickDiff.id === this.changes[i].providerId)?.visible) { continue; } diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index c505713f34a..ba26aaea1d0 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -311,10 +311,11 @@ class QuickDiffWidget extends PeekViewWidget { const change = this.model.changes[this._index]; const quickDiffWithChange = this.model.changes .filter(c => change.change2.modified.overlapOrTouch(c.change2.modified)) - .map(c => c.label); + .map(c => c.providerId); const quickDiffs = this.model.quickDiffs - .filter(quickDiff => quickDiff.visible && quickDiffWithChange.includes(quickDiff.label)); + .filter(quickDiff => quickDiff.visible && + quickDiffWithChange.includes(quickDiff.id)); return quickDiffs.length > 1; } From 2023ee02fbf06bf5a3c10a453dca3041caece2b7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 23 Apr 2025 06:51:45 -0700 Subject: [PATCH 069/114] Provide parsed links for text after brackets Fixes #231998 --- .../links/browser/terminalLinkParsing.ts | 17 ++++++++ .../test/browser/terminalLinkParsing.test.ts | 42 +++++++++++++++++++ .../browser/terminalLocalLinkDetector.test.ts | 7 ++++ 3 files changed, 66 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts index d93b62aa938..ea69ddc1154 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts @@ -314,6 +314,23 @@ function detectLinksViaSuffix(line: string): IParsedLink[] { prefix, suffix }); + + // If the path contains an opening bracket, provide the path starting immediately after + // the opening bracket as an additional result + const openingBracketMatch = path.matchAll(/(?[\[\(])(?![\]\)])/g); + for (const match of openingBracketMatch) { + const bracket = match.groups?.bracket; + if (bracket) { + results.push({ + path: { + index: linkStartIndex + (prefix?.text.length || 0) + match.index + 1, + text: path.substring(match.index + bracket.length) + }, + prefix, + suffix + }); + } + } } } diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts index 9d4684a66fa..2109f3d52a3 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts @@ -322,6 +322,48 @@ suite('TerminalLinkParsing', () => { ); }); + test('should detect multiple links when opening brackets are in the text', () => { + deepStrictEqual( + detectLinks('notlink[foo:45]', OperatingSystem.Linux), + [ + { + path: { + index: 0, + text: 'notlink[foo' + }, + prefix: undefined, + suffix: { + col: undefined, + row: 45, + rowEnd: undefined, + colEnd: undefined, + suffix: { + index: 11, + text: ':45' + } + } + }, + { + path: { + index: 8, + text: 'foo' + }, + prefix: undefined, + suffix: { + col: undefined, + row: 45, + rowEnd: undefined, + colEnd: undefined, + suffix: { + index: 11, + text: ':45' + } + } + }, + ] as IParsedLink[] + ); + }); + test('should extract the link prefix', () => { deepStrictEqual( detectLinks('"foo", line 5, col 6', OperatingSystem.Linux), diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts index a2275a18a59..04d0dfa7415 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts @@ -251,6 +251,13 @@ suite('Workbench - TerminalLocalLinkDetector', () => { { range: [[1, 1], [16, 1]], uri: URI.file('/parent/cwd/foo') } ]); }); + + test('should support finding links after brackets', async () => { + validResources = [URI.file('/parent/cwd/foo')]; + await assertLinks(TerminalBuiltinLinkType.LocalFile, 'bar[foo:5', [ + { range: [[5, 1], [9, 1]], uri: URI.file('/parent/cwd/foo') } + ]); + }); }); suite('macOS/Linux', () => { From 477d26e314e38688d4d9fc0c735c3f6e3cf336f2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:52:25 +0100 Subject: [PATCH 070/114] SCM - more quick diff code cleanup (#247219) --- .../contrib/scm/browser/quickDiffDecorator.ts | 11 ++++++----- .../workbench/contrib/scm/browser/quickDiffModel.ts | 12 +++++++++--- .../workbench/contrib/scm/browser/quickDiffWidget.ts | 12 +++++++----- src/vs/workbench/contrib/scm/common/quickDiff.ts | 1 - .../workbench/contrib/scm/common/quickDiffService.ts | 1 - 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts index 8d02e737e23..5dd8d643aa9 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts @@ -74,7 +74,8 @@ class QuickDiffDecorator extends Disposable { constructor( private readonly codeEditor: ICodeEditor, private readonly quickDiffModelRef: IReference, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IQuickDiffService private readonly quickDiffService: IQuickDiffService ) { super(); @@ -141,8 +142,8 @@ class QuickDiffDecorator extends Disposable { const quickDiff = this.quickDiffModelRef.object.quickDiffs .find(quickDiff => quickDiff.id === change.providerId); - if (!quickDiff?.visible) { - // Not visible + // Skip quick diffs that are not visible + if (!quickDiff || !this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)) { continue; } @@ -356,7 +357,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben const visibleDecorationCount = observableFromEvent(this, quickDiffModelRef.object.onDidChange, () => { - const visibleQuickDiffs = quickDiffModelRef.object.quickDiffs.filter(quickDiff => quickDiff.visible); + const visibleQuickDiffs = quickDiffModelRef.object.quickDiffs.filter(quickDiff => this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)); return quickDiffModelRef.object.changes.filter(change => visibleQuickDiffs.some(quickDiff => quickDiff.id === change.providerId)).length; }); @@ -423,7 +424,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben this.decorators.set(textModel.uri, new DisposableMap()); } - this.decorators.get(textModel.uri)!.set(editorId, new QuickDiffDecorator(editor, quickDiffModelRef, this.configurationService)); + this.decorators.get(textModel.uri)!.set(editorId, new QuickDiffDecorator(editor, quickDiffModelRef, this.configurationService, this.quickDiffService)); } // Dispose decorators for editors that are no longer visible. diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index 06d6e885c8f..2a88b551251 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -339,7 +339,11 @@ export class QuickDiffModel extends Disposable { return []; } - if (equals(this._quickDiffs, quickDiffs, (a, b) => a.originalResource.toString() === b.originalResource.toString() && a.id === b.id && a.visible === b.visible)) { + if (equals(this._quickDiffs, quickDiffs, (a, b) => + a.id === b.id && + a.originalResource.toString() === b.originalResource.toString() && + this.quickDiffService.isQuickDiffProviderVisible(a.id) === this.quickDiffService.isQuickDiffProviderVisible(b.id)) + ) { return quickDiffs; } @@ -399,7 +403,8 @@ export class QuickDiffModel extends Disposable { findNextClosestChange(lineNumber: number, inclusive = true, provider?: string): number { const visibleQuickDiffLabels = this.quickDiffs - .filter(quickDiff => (!provider || quickDiff.label === provider) && quickDiff.visible) + .filter(quickDiff => (!provider || quickDiff.label === provider) && + this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)) .map(quickDiff => quickDiff.label); if (!inclusive) { @@ -438,7 +443,8 @@ export class QuickDiffModel extends Disposable { } // Skip quick diffs that are not visible - if (!this.quickDiffs.find(quickDiff => quickDiff.id === this.changes[i].providerId)?.visible) { + const quickDiff = this.quickDiffs.find(quickDiff => quickDiff.id === this.changes[i].providerId); + if (!quickDiff || !this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)) { continue; } diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index ba26aaea1d0..ab210d09841 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -28,7 +28,7 @@ import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from ' import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { rot } from '../../../../base/common/numbers.js'; import { ISplice } from '../../../../base/common/sequence.js'; -import { ChangeType, getChangeHeight, getChangeType, getChangeTypeColor, getModifiedEndLineNumber, lineIntersectsChange, QuickDiffChange } from '../common/quickDiff.js'; +import { ChangeType, getChangeHeight, getChangeType, getChangeTypeColor, getModifiedEndLineNumber, IQuickDiffService, lineIntersectsChange, QuickDiffChange } from '../common/quickDiff.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { TextCompareEditorActiveContext } from '../../../common/contextkeys.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; @@ -168,7 +168,8 @@ class QuickDiffWidget extends PeekViewWidget { @IThemeService private readonly themeService: IThemeService, @IInstantiationService instantiationService: IInstantiationService, @IMenuService private readonly menuService: IMenuService, - @IContextKeyService private contextKeyService: IContextKeyService + @IContextKeyService private contextKeyService: IContextKeyService, + @IQuickDiffService private readonly quickDiffService: IQuickDiffService ) { super(editor, { isResizeable: true, frameWidth: 1, keepEditorSelection: true, className: 'dirty-diff' }, instantiationService); @@ -314,8 +315,8 @@ class QuickDiffWidget extends PeekViewWidget { .map(c => c.providerId); const quickDiffs = this.model.quickDiffs - .filter(quickDiff => quickDiff.visible && - quickDiffWithChange.includes(quickDiff.id)); + .filter(quickDiff => quickDiffWithChange.includes(quickDiff.id) && + this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)); return quickDiffs.length > 1; } @@ -344,7 +345,8 @@ class QuickDiffWidget extends PeekViewWidget { protected override _fillHead(container: HTMLElement): void { super._fillHead(container, true); - const visibleQuickDiffs = this.model.quickDiffs.filter(quickDiff => quickDiff.visible); + const visibleQuickDiffs = this.model.quickDiffs + .filter(quickDiff => this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)); this.dropdownContainer = dom.prepend(this._titleElement!, dom.$('.dropdown')); this.dropdown = this.instantiationService.createInstance(QuickDiffPickerViewItem, diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index acd2db337eb..e1c0b0bf437 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -79,7 +79,6 @@ export interface QuickDiff { readonly id: string; readonly label: string; readonly originalResource: URI; - readonly visible: boolean; readonly kind: 'primary' | 'secondary' | 'contributed'; } diff --git a/src/vs/workbench/contrib/scm/common/quickDiffService.ts b/src/vs/workbench/contrib/scm/common/quickDiffService.ts index dd6740abbd6..97811516760 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiffService.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiffService.ts @@ -110,7 +110,6 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { id: provider.id, label: provider.label, kind: provider.kind, - visible: this.isQuickDiffProviderVisible(provider.id), originalResource, } satisfies QuickDiff); } From ca2881aa4f7e6e99c9e6906814a44bc2b0e13bee Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 23 Apr 2025 07:32:59 -0700 Subject: [PATCH 071/114] make merge editor more accessible (#247156) --- .../mergeEditor/browser/commands/commands.ts | 48 +++++++++++++++++-- .../browser/mergeEditor.contribution.ts | 4 +- .../browser/mergeEditorAccessibilityHelp.ts | 4 +- .../mergeEditor/browser/mergeEditorInput.ts | 28 +++++++++++ .../mergeEditor/browser/view/mergeEditor.ts | 12 +++++ .../mergeEditor/browser/view/viewModel.ts | 25 ++++++++++ 6 files changed, 115 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts index b39f4ca1b23..f0c63879e6b 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts @@ -24,6 +24,8 @@ import { ctxIsMergeEditor, ctxMergeEditorLayout, ctxMergeEditorShowBase, ctxMerg import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { transaction } from '../../../../../base/common/observable.js'; import { ModifiedBaseRangeStateKind } from '../model/modifiedBaseRange.js'; +import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; abstract class MergeEditorAction extends Action2 { constructor(desc: Readonly) { @@ -515,7 +517,7 @@ export class AcceptAllInput1 extends MergeEditorAction { super({ id: 'merge.acceptAllInput1', category: mergeEditorCategory, - title: localize2('merge.acceptAllInput1', "Accept All Changes from Left"), + title: localize2('merge.acceptAllInput1', "Accept All Incoming Changes from Left"), f1: true, precondition: ctxIsMergeEditor, menu: { id: MenuId.MergeInput1Toolbar, group: 'primary' }, @@ -533,7 +535,7 @@ export class AcceptAllInput2 extends MergeEditorAction { super({ id: 'merge.acceptAllInput2', category: mergeEditorCategory, - title: localize2('merge.acceptAllInput2', "Accept All Changes from Right"), + title: localize2('merge.acceptAllInput2', "Accept All Current Changes from Right"), f1: true, precondition: ctxIsMergeEditor, menu: { id: MenuId.MergeInput2Toolbar, group: 'primary' }, @@ -621,8 +623,15 @@ export class AcceptMerge extends MergeEditorAction2 { id: 'mergeEditor.acceptMerge', category: mergeEditorCategory, title: localize2('mergeEditor.acceptMerge', "Complete Merge"), - f1: false, - precondition: ctxIsMergeEditor + f1: true, + precondition: ctxIsMergeEditor, + keybinding: [ + { + primary: KeyMod.CtrlCmd | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib, + when: ctxIsMergeEditor, + } + ] }); } @@ -652,3 +661,34 @@ export class AcceptMerge extends MergeEditorAction2 { }; } } + +export class ToggleBetweenInputs extends MergeEditorAction2 { + constructor() { + super({ + id: 'mergeEditor.toggleBetweenInputs', + category: mergeEditorCategory, + title: localize2('mergeEditor.toggleBetweenInputs', "Toggle Between Merge Editor Inputs"), + f1: true, + precondition: ctxIsMergeEditor, + keybinding: [ + { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyT, + // Override reopen closed editor + weight: KeybindingWeight.WorkbenchContrib + 10, + when: ctxIsMergeEditor, + } + ] + }); + } + + override runWithMergeEditor({ viewModel }: MergeEditorAction2Args, accessor: ServicesAccessor) { + const input1IsFocused = viewModel.inputCodeEditorView1.editor.hasWidgetFocus(); + + // Toggle focus between inputs + if (input1IsFocused) { + viewModel.inputCodeEditorView2.editor.focus(); + } else { + viewModel.inputCodeEditorView1.editor.focus(); + } + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index 06e88212e2c..6399b030b16 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -16,7 +16,7 @@ import { CompareInput2WithBaseCommand, GoToNextUnhandledConflict, GoToPreviousUnhandledConflict, OpenBaseFile, OpenMergeEditor, OpenResultResource, ResetToBaseAndAutoMergeCommand, SetColumnLayout, SetMixedLayout, ShowHideTopBase, ShowHideCenterBase, ShowHideBase, ShowNonConflictingChanges, ToggleActiveConflictInput1, ToggleActiveConflictInput2, ResetCloseWithConflictsChoice, - AcceptAllCombination + AcceptAllCombination, ToggleBetweenInputs } from './commands/commands.js'; import { MergeEditorCopyContentsToJSON, MergeEditorLoadContentsFromFolder, MergeEditorSaveContentsToFolder } from './commands/devCommands.js'; import { MergeEditorInput } from './mergeEditorInput.js'; @@ -89,6 +89,8 @@ registerAction2(AcceptMerge); registerAction2(ResetCloseWithConflictsChoice); registerAction2(AcceptAllCombination); +registerAction2(ToggleBetweenInputs); + // Dev Commands registerAction2(MergeEditorCopyContentsToJSON); registerAction2(MergeEditorSaveContentsToFolder); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts index 2ead17bee71..cfa00747647 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts @@ -28,7 +28,9 @@ export class MergeEditorAccessibilityHelpProvider implements IAccessibleViewImpl const content = [ localize('msg1', "You are in a merge editor."), localize('msg2', "Navigate between merge conflicts using the commands Go to Next Unhandled Conflict{0} and Go to Previous Unhandled Conflict{1}.", '', ''), - localize('msg3', "Run the command Merge Editor: Accept All Changes from the Left{0} and Merge Editor: Accept All Changes from the Right{1}", '', ''), + localize('msg3', "Run the command Merge Editor: Accept All Incoming Changes from the Left{0} and Merge Editor: Accept All Current Changes from the Right{1}", '', ''), + localize('msg4', "Complete the Merge{0}.", ''), + localize('msg5', "Toggle between merge editor inputs, incoming and current changes {0}.", ''), ]; return new AccessibleContentProvider( diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index 28217f06aa7..2f1bfb218dd 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -23,6 +23,8 @@ import { MergeEditorTelemetry } from './telemetry.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; import { ILanguageSupport, ITextFileSaveOptions, ITextFileService } from '../../../services/textfile/common/textfiles.js'; +import { alert } from '../../../../base/browser/ui/aria/aria.js'; +import { MergeEditorType } from './view/viewModel.js'; export class MergeEditorInputData { constructor( @@ -38,6 +40,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements private _inputModel?: IMergeEditorInputModel; + private _focusedEditor: MergeEditorType = 'result'; + override closeHandler: IEditorCloseHandler = { showConfirm: () => this._inputModel?.shouldConfirmClose() ?? false, confirm: async (editors) => { @@ -178,5 +182,29 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements this._inputModel?.model.setLanguageId(languageId, source); } + /** + * Updates the focused editor and triggers a name change event + */ + public updateFocusedEditor(editor: MergeEditorType): void { + if (this._focusedEditor !== editor) { + this._focusedEditor = editor; + alertFocusedEditor(editor); + } + } + // implement get/set encoding } + +function alertFocusedEditor(editor: MergeEditorType) { + switch (editor) { + case 'input1': + alert(localize('mergeEditor.input1', "Incoming, Left Input")); + break; + case 'input2': + alert(localize('mergeEditor.input2', "Current, Right Input")); + break; + case 'result': + alert(localize('mergeEditor.result', "Merge Result")); + break; + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 57587b04cab..6c6e6b5eaf3 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -218,6 +218,18 @@ export class MergeEditor extends AbstractTextEditor { }); this._sessionDisposables.add(viewModel); + // Track focus changes to update the editor name + this._sessionDisposables.add(autorun(reader => { + /** @description Update focused editor name based on focus */ + const focusedType = viewModel.focusedEditorType.read(reader); + + if (!(input instanceof MergeEditorInput)) { + return; + } + + input.updateFocusedEditor(focusedType || 'result'); + })); + // Set/unset context keys based on input this._ctxResultUri.set(inputModel.resultUri.toString()); this._ctxBaseUri.set(model.base.uri.toString()); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index 74322dc65a4..b8158dc995a 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -116,6 +116,29 @@ export class MergeEditorViewModel extends Disposable { return undefined; }); + /** + * Returns an observable that tracks which editor type is currently focused + */ + public readonly focusedEditorType = derived(this, reader => { + const lastFocusedEditor = this.lastFocusedEditor.read(reader); + + if (!lastFocusedEditor.view) { + return undefined; + } + + if (lastFocusedEditor.view === this.inputCodeEditorView1) { + return 'input1'; + } else if (lastFocusedEditor.view === this.inputCodeEditorView2) { + return 'input2'; + } else if (lastFocusedEditor.view === this.resultCodeEditorView) { + return 'result'; + } else if (lastFocusedEditor.view === this.baseCodeEditorView.read(reader)) { + return 'base'; + } + + return undefined; + }); + public readonly selectionInBase = derived(this, reader => { const sourceEditor = this.lastFocusedEditor.read(reader).view; if (!sourceEditor) { @@ -343,3 +366,5 @@ interface IAttachedHistoryElement { undo(): void; redo(): void; } + +export type MergeEditorType = 'input1' | 'input2' | 'result' | 'base'; From 600aaf279413c13a3df3ff4c93336edaaa341a9f Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:35:04 +0200 Subject: [PATCH 072/114] Bump distro --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 30378836bb1..8d3443fc25e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.100.0", - "distro": "47d9c6291ce9549954bbf119a3874d724e55884f", + "distro": "ded402cf93975c2544e3a91d2163ebc3d97f152c", "author": { "name": "Microsoft Corporation" }, From 46bf82d9b161e64901e9e9e0a57a26cca79e258a Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 23 Apr 2025 16:56:49 +0200 Subject: [PATCH 073/114] Tweak chat overlay for replace and multipass edits (#247224) --- .../chat/browser/actions/chatActions.ts | 6 - .../chatEditing/chatEditingEditorActions.ts | 39 +-- .../chatEditingEditorContextKeys.ts | 5 +- .../chatEditing/chatEditingEditorOverlay.ts | 278 ++++++++---------- .../media/chatEditingEditorOverlay.css | 37 +-- .../chat/browser/media/chatEditorOverlay.css | 8 - .../inlineChat/browser/inlineChatActions.ts | 13 +- 7 files changed, 157 insertions(+), 229 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 3e1df623987..426eb94e0a9 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -54,7 +54,6 @@ import { ChatMode, validateChatMode } from '../../common/constants.js'; import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; import { ChatViewId, IChatWidget, IChatWidgetService, showChatView, showCopilotView } from '../chat.js'; -import { ctxHasRequestInProgress, ctxIsGlobalEditingSession } from '../chatEditing/chatEditingEditorContextKeys.js'; import { IChatEditorOptions } from '../chatEditor.js'; import { ChatEditorInput } from '../chatEditorInput.js'; import { ChatViewPane } from '../chatViewPane.js'; @@ -123,11 +122,6 @@ export function registerChatActions() { id: MenuId.ChatTitleBarMenu, group: 'a_open', order: 1 - }, { - id: MenuId.ChatEditingEditorContent, - when: ContextKeyExpr.and(ctxHasRequestInProgress, ctxIsGlobalEditingSession), - group: 'navigate', - order: 4, }] }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts index 8074b03af83..597bd21a716 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts @@ -9,7 +9,7 @@ import { Action2, IAction2Options, MenuId, MenuRegistry, registerAction2 } from import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; -import { ctxHasEditorModification, ctxHasRequestInProgress, ctxReviewModeEnabled } from './chatEditingEditorContextKeys.js'; +import { ctxHasEditorModification, ctxHasRequestInProgress, ctxIsGlobalEditingSession, ctxReviewModeEnabled } from './chatEditingEditorContextKeys.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; @@ -73,7 +73,7 @@ abstract class NavigateAction extends ChatEditingEditorAction { ? localize2('next', 'Go to Next Chat Edit') : localize2('prev', 'Go to Previous Chat Edit'), icon: next ? Codicon.arrowDown : Codicon.arrowUp, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ctxHasRequestInProgress.negate()), + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ctxHasEditorModification), keybinding: { primary: next ? KeyMod.Alt | KeyCode.F5 @@ -89,7 +89,7 @@ abstract class NavigateAction extends ChatEditingEditorAction { id: MenuId.ChatEditingEditorContent, group: 'navigate', order: !next ? 2 : 3, - when: ContextKeyExpr.and(ctxReviewModeEnabled, ctxHasRequestInProgress.negate()) + when: ContextKeyExpr.and(ctxReviewModeEnabled, ctxHasEditorModification) } }); } @@ -156,37 +156,40 @@ async function openNextOrPreviousChange(accessor: ServicesAccessor, session: ICh return true; } -abstract class AcceptDiscardAction extends ChatEditingEditorAction { +abstract class KeepOrUndoAction extends ChatEditingEditorAction { - constructor(id: string, readonly accept: boolean) { + constructor(id: string, private _keep: boolean) { super({ id, - title: accept + title: _keep ? localize2('accept', 'Keep Chat Edits') : localize2('discard', 'Undo Chat Edits'), - shortTitle: accept + shortTitle: _keep ? localize2('accept2', 'Keep') : localize2('discard2', 'Undo'), - tooltip: accept + tooltip: _keep ? localize2('accept3', 'Keep Chat Edits in this File') : localize2('discard3', 'Undo Chat Edits in this File'), precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxHasRequestInProgress.negate()), - icon: accept + icon: _keep ? Codicon.check : Codicon.discard, f1: true, keybinding: { when: EditorContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib, - primary: accept + primary: _keep ? KeyMod.CtrlCmd | KeyCode.Enter : KeyMod.CtrlCmd | KeyCode.Backspace }, menu: { id: MenuId.ChatEditingEditorContent, group: 'a_resolve', - order: accept ? 0 : 1, - when: ContextKeyExpr.and(!accept ? ctxReviewModeEnabled : undefined, ctxHasRequestInProgress.negate()) + order: _keep ? 0 : 1, + when: ContextKeyExpr.or( + ContextKeyExpr.and(ctxIsGlobalEditingSession.negate(), ctxHasRequestInProgress.negate()), // Inline chat + ContextKeyExpr.and(ctxIsGlobalEditingSession, !_keep ? ctxReviewModeEnabled : undefined), // Panel chat + ) } }); } @@ -195,7 +198,7 @@ abstract class AcceptDiscardAction extends ChatEditingEditorAction { const instaService = accessor.get(IInstantiationService); - if (this.accept) { + if (this._keep) { session.accept(entry.modifiedURI); } else { session.reject(entry.modifiedURI); @@ -205,7 +208,7 @@ abstract class AcceptDiscardAction extends ChatEditingEditorAction { } } -export class AcceptAction extends AcceptDiscardAction { +export class AcceptAction extends KeepOrUndoAction { static readonly ID = 'chatEditor.action.accept'; @@ -214,7 +217,7 @@ export class AcceptAction extends AcceptDiscardAction { } } -export class RejectAction extends AcceptDiscardAction { +export class RejectAction extends KeepOrUndoAction { static readonly ID = 'chatEditor.action.reject'; @@ -267,7 +270,7 @@ class ToggleDiffAction extends ChatEditingEditorAction { condition: ContextKeyExpr.or(EditorContextKeys.inDiffEditor, ActiveEditorContext.isEqualTo(TEXT_DIFF_EDITOR_ID))!, icon: Codicon.goToFile, }, - precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxHasRequestInProgress.negate()), + precondition: ContextKeyExpr.and(ctxHasEditorModification), icon: Codicon.diffSingle, keybinding: { when: EditorContextKeys.focus, @@ -281,7 +284,7 @@ class ToggleDiffAction extends ChatEditingEditorAction { id: MenuId.ChatEditingEditorContent, group: 'a_resolve', order: 2, - when: ContextKeyExpr.and(ctxReviewModeEnabled, ctxHasRequestInProgress.negate()) + when: ContextKeyExpr.and(ctxReviewModeEnabled) }] }); } @@ -401,7 +404,7 @@ export function registerChatEditorActions() { }, group: 'navigate', order: -1, - when: ContextKeyExpr.and(ctxReviewModeEnabled, ctxHasRequestInProgress.negate()), + when: ContextKeyExpr.and(ctxReviewModeEnabled, ctxHasEditorModification), }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts index 5755dadb175..7d7ff25a7a2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts @@ -120,10 +120,7 @@ class ContextKeyGroup { this._ctxHasEditorModification.set(isInlineChat || entry?.state.read(r) === ModifiedFileEntryState.Modified); this._ctxIsGlobalEditingSession.set(session.isGlobalEditingSession); this._ctxReviewModeEnabled.set(entry ? entry.reviewMode.read(r) : false); - this._ctxHasRequestInProgress.set( - Boolean(entry?.isCurrentlyBeingModifiedBy.read(r)) // any entry changing - || (isInlineChat && isRequestInProgress.read(r)) // inline chat request - ); + this._ctxHasRequestInProgress.set(isRequestInProgress.read(r)); // number of requests const requestCount = chatModel diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index 54e51b8bcef..8069c2810fc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -4,18 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import '../media/chatEditingEditorOverlay.css'; -import { combinedDisposable, DisposableMap, DisposableStore, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { combinedDisposable, Disposable, DisposableMap, DisposableStore, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun, derived, derivedOpts, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IChatEditingService, IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { ActionViewItem } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IActionRunner } from '../../../../../base/common/actions.js'; -import { addDisposableGenericMouseMoveListener, append, reset } from '../../../../../base/browser/dom.js'; -import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; +import { $, addDisposableGenericMouseMoveListener, append } from '../../../../../base/browser/dom.js'; import { assertType } from '../../../../../base/common/types.js'; import { localize } from '../../../../../nls.js'; import { AcceptAction, navigationBearingFakeActionId, RejectAction } from './chatEditingEditorActions.js'; @@ -30,39 +27,137 @@ import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/edi import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { ObservableEditorSession } from './chatEditingEditorContextKeys.js'; -import { rcut } from '../../../../../base/common/strings.js'; -import { CHAT_OPEN_ACTION_ID } from '../actions/chatActions.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; -class ChatEditorOverlayWidget { +class ChatEditorOverlayWidget extends Disposable { private readonly _domNode: HTMLElement; - private readonly _toolbar: WorkbenchToolBar; + private readonly _toolbarNode: HTMLElement; - private readonly _showStore = new DisposableStore(); + private readonly _showStore = this._store.add(new DisposableStore()); private readonly _session = observableValue(this, undefined); private readonly _entry = observableValue(this, undefined); + private readonly _isBusy: IObservable; private readonly _navigationBearings = observableValue<{ changeCount: number; activeIdx: number; entriesCount: number }>(this, { changeCount: -1, activeIdx: -1, entriesCount: -1 }); constructor( private readonly _editor: { focus(): void }, @IChatService private readonly _chatService: IChatService, - @IInstantiationService instaService: IInstantiationService, + @IInstantiationService private readonly _instaService: IInstantiationService, ) { + super(); this._domNode = document.createElement('div'); this._domNode.classList.add('chat-editor-overlay-widget'); + this._isBusy = derived(r => { + const session = this._session.read(r); + const chatModel = session && _chatService.getSession(session?.chatSessionId); + return chatModel && observableFromEvent(this, chatModel.onDidChange, () => chatModel.requestInProgress).read(r); + }); + + const requestMessage = derived(r => { + + const session = this._session.read(r); + const chatModel = this._chatService.getSession(session?.chatSessionId ?? ''); + if (!session || !chatModel) { + return undefined; + } + + const response = this._entry.read(r)?.isCurrentlyBeingModifiedBy.read(r); + + if (response) { + + if (response?.isPaused.read(r)) { + return { message: localize('paused', "Edits Paused"), paused: true }; + } + + const entry = this._entry.read(r); + if (entry) { + return { message: localize('working', "Working...") }; + } + } + + if (session.isGlobalEditingSession) { + return undefined; + } + + return { message: localize('working', "Working...") }; + }); + + const progressNode = document.createElement('div'); progressNode.classList.add('chat-editor-overlay-progress'); append(progressNode, renderIcon(ThemeIcon.modify(Codicon.loading, 'spin'))); + const textProgress = append(progressNode, $('span.progress-message')); this._domNode.appendChild(progressNode); - const toolbarNode = document.createElement('div'); - toolbarNode.classList.add('chat-editor-overlay-toolbar'); - this._domNode.appendChild(toolbarNode); + this._store.add(autorun(r => { + const value = requestMessage.read(r); + const busy = this._isBusy.read(r) && !value?.paused; - this._toolbar = instaService.createInstance(MenuWorkbenchToolBar, toolbarNode, MenuId.ChatEditingEditorContent, { + this._domNode.classList.toggle('busy', busy); + + textProgress.innerText = busy && value && !this._session.read(r)?.isGlobalEditingSession + ? value.message + : ''; + })); + + this._toolbarNode = document.createElement('div'); + this._toolbarNode.classList.add('chat-editor-overlay-toolbar'); + + } + + override dispose() { + this.hide(); + super.dispose(); + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + show(session: IChatEditingSession, entry: IModifiedFileEntry | undefined, indicies: { entryIndex: IObservable; changeIndex: IObservable }) { + + this._showStore.clear(); + + transaction(tx => { + this._session.set(session, tx); + this._entry.set(entry, tx); + }); + + this._showStore.add(autorun(r => { + + const entryIndex = indicies.entryIndex.read(r); + const changeIndex = indicies.changeIndex.read(r); + + const entries = session.entries.read(r); + + let activeIdx = entryIndex !== undefined && changeIndex !== undefined + ? changeIndex + : -1; + + let totalChangesCount = 0; + for (let i = 0; i < entries.length; i++) { + const changesCount = entries[i].changesCount.read(r); + totalChangesCount += changesCount; + + if (entryIndex !== undefined && i < entryIndex) { + activeIdx += changesCount; + } + } + + this._navigationBearings.set({ changeCount: totalChangesCount, activeIdx, entriesCount: entries.length }, undefined); + })); + + + this._domNode.appendChild(this._toolbarNode); + this._showStore.add(toDisposable(() => this._toolbarNode.remove())); + + this._showStore.add(this._instaService.createInstance(MenuWorkbenchToolBar, this._toolbarNode, MenuId.ChatEditingEditorContent, { telemetrySource: 'chatEditor.overlayToolbar', hiddenItemStrategy: HiddenItemStrategy.Ignore, toolbarOptions: { @@ -94,7 +189,8 @@ class ChatEditorOverlayWidget { const n = activeIdx === -1 ? '1' : `${activeIdx + 1}`; this.label.innerText = localize('nOfM', "{0} of {1}", n, changeCount); } else { - this.label.innerText = localize('0Of0', "0 of 0"); + // allow-any-unicode-next-line + this.label.innerText = localize('0Of0', "—"); } this.updateTooltip(); @@ -105,15 +201,21 @@ class ChatEditorOverlayWidget { const { changeCount, entriesCount } = that._navigationBearings.get(); if (changeCount === -1 || entriesCount === -1) { return undefined; - } else if (changeCount === 1 && entriesCount === 1) { - return localize('tooltip_11', "1 change in 1 file"); - } else if (changeCount === 1) { - return localize('tooltip_1n', "1 change in {0} files", entriesCount); - } else if (entriesCount === 1) { - return localize('tooltip_n1', "{0} changes in 1 file", changeCount); - } else { - return localize('tooltip_nm', "{0} changes in {1} files", changeCount, entriesCount); } + let result: string | undefined; + if (changeCount === 1 && entriesCount === 1) { + result = localize('tooltip_11', "1 change in 1 file"); + } else if (changeCount === 1) { + result = localize('tooltip_1n', "1 change in {0} files", entriesCount); + } else if (entriesCount === 1) { + result = localize('tooltip_n1', "{0} changes in 1 file", changeCount); + } else { + result = localize('tooltip_nm', "{0} changes in {1} files", changeCount, entriesCount); + } + if (!that._isBusy.get()) { + return result; + } + return localize('tooltip_busy', "{0} - Working...", result); } }; } @@ -168,134 +270,8 @@ class ChatEditorOverlayWidget { }; } - if (action.id === 'inlineChat2.reveal' || action.id === CHAT_OPEN_ACTION_ID) { - return new class extends ActionViewItem { - - private _requestMessage: IObservable<{ message: string; paused?: boolean } | undefined>; - - constructor() { - super(undefined, action, options); - - this._requestMessage = derived(r => { - const session = that._session.read(r); - const chatModel = that._chatService.getSession(session?.chatSessionId ?? ''); - if (!session || !chatModel) { - return undefined; - } - - const response = that._entry.read(r)?.isCurrentlyBeingModifiedBy.read(r); - - if (response) { - - if (response?.isPaused.read(r)) { - return { message: localize('paused', "Edits Paused"), paused: true }; - } - - const entry = that._entry.read(r); - if (entry) { - const progress = entry?.rewriteRatio.read(r); - const message = progress === 0 - ? localize('generating', "Generating edits") - : localize('applyingPercentage', "{0}% Applying edits", Math.round(progress * 100)); - - return { message }; - } - } - - if (session.isGlobalEditingSession) { - return undefined; - } - - const request = observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)).read(r); - if (!request || request.response?.isComplete) { - return undefined; - } - return { message: request.message.text }; - }); - } - - override render(container: HTMLElement) { - super.render(container); - - container.classList.add('label-item'); - - this._store.add(autorun(r => { - assertType(this.label); - - const value = this._requestMessage.read(r); - if (!value) { - // normal rendering - this.options.icon = true; - this.options.label = false; - reset(this.label); - this.updateClass(); - this.updateLabel(); - this.updateTooltip(); - - } else { - this.options.icon = false; - this.options.label = true; - this.updateClass(); - this.updateTooltip(); - - const message = rcut(value.message, 47); - reset(this.label, message); - } - - const busy = Boolean(value && !value.paused); - that._domNode.classList.toggle('busy', busy); - this.label.classList.toggle('busy', busy); - - })); - } - }; - } return undefined; } - }); - } - - dispose() { - this.hide(); - this._showStore.dispose(); - this._toolbar.dispose(); - } - - getDomNode(): HTMLElement { - return this._domNode; - } - - show(session: IChatEditingSession, entry: IModifiedFileEntry | undefined, indicies: { entryIndex: IObservable; changeIndex: IObservable }) { - - this._showStore.clear(); - - transaction(tx => { - this._session.set(session, tx); - this._entry.set(entry, tx); - }); - - this._showStore.add(autorun(r => { - - const entryIndex = indicies.entryIndex.read(r); - const changeIndex = indicies.changeIndex.read(r); - - const entries = session.entries.read(r); - - let activeIdx = entryIndex !== undefined && changeIndex !== undefined - ? changeIndex - : -1; - - let totalChangesCount = 0; - for (let i = 0; i < entries.length; i++) { - const changesCount = entries[i].changesCount.read(r); - totalChangesCount += changesCount; - - if (entryIndex !== undefined && i < entryIndex) { - activeIdx += changesCount; - } - } - - this._navigationBearings.set({ changeCount: totalChangesCount, activeIdx, entriesCount: entries.length }, undefined); })); } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css index 9356b690797..0728892a4a0 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css @@ -35,11 +35,10 @@ .chat-editor-overlay-widget .chat-editor-overlay-progress { align-items: center; display: none; - padding: 0px 5px; + padding: 5px 0 5px 5px; font-size: 12px; - font-variant-numeric: tabular-nums; overflow: hidden; - white-space: nowrap; + gap: 6px; } .chat-editor-overlay-widget.busy .chat-editor-overlay-progress { @@ -47,43 +46,11 @@ } -@keyframes ellipsis { - 0% { - content: ""; - } - 25% { - content: "."; - } - 50% { - content: ".."; - } - 75% { - content: "..."; - } - 100% { - content: ""; - } -} - -.chat-editor-overlay-widget.busy.paused .chat-editor-overlay-progress { - .codicon-loading { - display: none; - } -} - .chat-editor-overlay-widget .action-item > .action-label { padding: 5px; font-size: 12px; } -.chat-editor-overlay-widget.busy .action-item > .action-label.busy::after { - content: ""; - display: inline-flex; - white-space: nowrap; - overflow: hidden; - width: 3ch; - animation: ellipsis steps(4, end) 1s infinite; -} .chat-editor-overlay-widget .action-item:first-child > .action-label { padding-left: 7px; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css index 79c7251217f..bac23cf6d3a 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css @@ -97,14 +97,6 @@ font-size: 12px; } -.chat-editor-overlay-widget .action-item:first-child > .action-label { - padding-left: 7px; -} - -.chat-editor-overlay-widget .action-item:last-child > .action-label { - padding-right: 7px; -} - .chat-editor-overlay-widget.busy .chat-editor-overlay-progress .codicon, .chat-editor-overlay-widget .action-item > .action-label.codicon { color: var(--vscode-button-foreground); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 13b42f47cca..8a3cb6349dd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -626,7 +626,9 @@ export class RevealWidget extends AbstractInline2ChatAction { f1: true, icon: Codicon.copilot, precondition: ContextKeyExpr.and(ctxIsGlobalEditingSession.negate(), ContextKeyExpr.greaterEquals(ctxRequestCount.key, 1)), - toggled: CTX_INLINE_CHAT_VISIBLE, + toggled: { + condition: CTX_INLINE_CHAT_VISIBLE, + }, keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KeyI @@ -660,12 +662,9 @@ export class CancelRequestAction extends AbstractInline2ChatAction { toggled: CTX_INLINE_CHAT_VISIBLE, menu: { id: MenuId.ChatEditingEditorContent, - when: ContextKeyExpr.and( - ctxHasRequestInProgress, - ctxIsGlobalEditingSession.negate(), - ), - group: 'navigate', - order: 14, + when: ContextKeyExpr.and(ctxIsGlobalEditingSession.negate(), ctxHasRequestInProgress), + group: 'a_request', + order: 1, } }); } From bcfd7a06bfb79eb1bc59a40b7972d1e69de30a2f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:00:04 +0100 Subject: [PATCH 074/114] SCM - fix issue with quick diff picker (#247225) --- .../contrib/scm/browser/quickDiffWidget.ts | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index ab210d09841..1c6bebfe09b 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -28,7 +28,7 @@ import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from ' import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { rot } from '../../../../base/common/numbers.js'; import { ISplice } from '../../../../base/common/sequence.js'; -import { ChangeType, getChangeHeight, getChangeType, getChangeTypeColor, getModifiedEndLineNumber, IQuickDiffService, lineIntersectsChange, QuickDiffChange } from '../common/quickDiff.js'; +import { ChangeType, getChangeHeight, getChangeType, getChangeTypeColor, getModifiedEndLineNumber, IQuickDiffService, lineIntersectsChange, QuickDiff, QuickDiffChange } from '../common/quickDiff.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { TextCompareEditorActiveContext } from '../../../common/contextkeys.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; @@ -58,33 +58,26 @@ export interface IQuickDiffSelectItem extends ISelectOptionItem { } export class QuickDiffPickerViewItem extends SelectActionViewItem { - private readonly optionsItems: IQuickDiffSelectItem[]; + private optionsItems: IQuickDiffSelectItem[] = []; constructor( action: IAction, - providers: string[], - selected: string, @IContextViewService contextViewService: IContextViewService, @IThemeService themeService: IThemeService ) { - const items = providers.map(provider => ({ provider, text: provider })); - let startingSelection = providers.indexOf(selected); - if (startingSelection === -1) { - startingSelection = 0; - } const styles = { ...defaultSelectBoxStyles }; const theme = themeService.getColorTheme(); const editorBackgroundColor = theme.getColor(editorBackground); const peekTitleColor = theme.getColor(peekViewTitleBackground); const opaqueTitleColor = peekTitleColor?.makeOpaque(editorBackgroundColor!) ?? editorBackgroundColor!; styles.selectBackground = opaqueTitleColor.lighten(.6).toString(); - super(null, action, items, startingSelection, contextViewService, styles, { ariaLabel: nls.localize('remotes', 'Switch quick diff base') }); - this.optionsItems = items; + super(null, action, [], 0, contextViewService, styles, { ariaLabel: nls.localize('remotes', 'Switch quick diff base') }); } - public setSelection(provider: string) { + public setSelection(providers: string[], provider: string) { + this.optionsItems = providers.map(provider => ({ provider, text: provider })); const index = this.optionsItems.findIndex(item => item.provider === provider); - this.select(index); + this.setOptions(this.optionsItems, index); } protected override getActionContext(_: string, index: number): IQuickDiffSelectItem { @@ -230,7 +223,6 @@ class QuickDiffWidget extends PeekViewWidget { return; } this.diffEditor.setModel(diffEditorModel); - this.dropdown?.setSelection(labeledChange.label); const position = new Position(getModifiedEndLineNumber(change), 1); @@ -239,6 +231,7 @@ class QuickDiffWidget extends PeekViewWidget { const editorHeightInLines = Math.floor(editorHeight / lineHeight); const height = Math.min(getChangeHeight(change) + /* padding */ 8, Math.floor(editorHeightInLines / 3)); + this.updateDropdown(labeledChange.label); this.renderTitle(labeledChange.label); const changeType = getChangeType(change); @@ -309,15 +302,7 @@ class QuickDiffWidget extends PeekViewWidget { } private shouldUseDropdown(): boolean { - const change = this.model.changes[this._index]; - const quickDiffWithChange = this.model.changes - .filter(c => change.change2.modified.overlapOrTouch(c.change2.modified)) - .map(c => c.providerId); - - const quickDiffs = this.model.quickDiffs - .filter(quickDiff => quickDiffWithChange.includes(quickDiff.id) && - this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)); - + const quickDiffs = this.getQuickDiffsContainingChange(); return quickDiffs.length > 1; } @@ -342,18 +327,31 @@ class QuickDiffWidget extends PeekViewWidget { this._actionbarWidget.push(this._disposables.add(new Action('peekview.close', nls.localize('label.close', "Close"), ThemeIcon.asClassName(Codicon.close), true, () => this.dispose())), { label: false, icon: true }); } + private updateDropdown(label: string): void { + const quickDiffs = this.getQuickDiffsContainingChange(); + this.dropdown?.setSelection(quickDiffs.map(quickDiff => quickDiff.label), label); + } + + private getQuickDiffsContainingChange(): QuickDiff[] { + const change = this.model.changes[this._index]; + + const quickDiffsWithChange = this.model.changes + .filter(c => change.change2.modified.overlapOrTouch(c.change2.modified)) + .map(c => c.providerId); + + return this.model.quickDiffs + .filter(quickDiff => quickDiffsWithChange.includes(quickDiff.id) && + this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)); + } + protected override _fillHead(container: HTMLElement): void { super._fillHead(container, true); - const visibleQuickDiffs = this.model.quickDiffs - .filter(quickDiff => this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)); - + // Render an empty picker which will be populated later this.dropdownContainer = dom.prepend(this._titleElement!, dom.$('.dropdown')); this.dropdown = this.instantiationService.createInstance(QuickDiffPickerViewItem, - new QuickDiffPickerBaseAction((event?: IQuickDiffSelectItem) => this.switchQuickDiff(event)), - visibleQuickDiffs.map(quickDiff => quickDiff.label), this.model.changes[this._index].label); + new QuickDiffPickerBaseAction((event?: IQuickDiffSelectItem) => this.switchQuickDiff(event))); this.dropdown.render(this.dropdownContainer); - this.updateActions(); } protected override _getActionBarOptions(): IActionBarOptions { From 8d4b26f8379177c7fd383adbfee40a73406d505d Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 23 Apr 2025 17:13:16 +0200 Subject: [PATCH 075/114] update milestone (#247226) --- .vscode/notebooks/api.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 3a0eaec922f..07b5f0c1ca0 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"March 2025\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"April 2025\"" }, { "kind": 1, From 481ed8e93900beb7cd908abef8bef8f3799dfcc3 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 23 Apr 2025 08:25:35 -0700 Subject: [PATCH 076/114] mcp: allow creating inputs when generating configs (#247229) --- .../browser/mcpCommandsAddConfiguration.ts | 39 ++++++++++++------- .../contrib/mcp/common/mcpRegistry.ts | 10 +++++ .../contrib/mcp/common/mcpRegistryTypes.ts | 2 + 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts index d365b23ec69..4a9c6e9c82e 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts @@ -225,7 +225,7 @@ export class McpAddConfigurationCommand { return targetPick?.target; } - private async getAssistedConfig(type: AssistedConfigurationType): Promise<{ name: string; config: McpConfigurationServer } | undefined> { + private async getAssistedConfig(type: AssistedConfigurationType): Promise<{ name: string; server: McpConfigurationServer; inputs?: ConfiguredInput[]; inputValues?: Record } | undefined> { const packageName = await this._quickInputService.input({ ignoreFocusLost: true, title: assistedTypes[type].title, @@ -301,20 +301,13 @@ export class McpAddConfigurationCommand { return undefined; } - const configWithName = await this._commandService.executeCommand( + return await this._commandService.executeCommand<{ name: string; server: McpConfigurationServer; inputs?: ConfiguredInput[]; inputValues?: Record }>( AddConfigurationCopilotCommand.StartFlow, { name: packageName, type: packageType } ); - - if (!configWithName) { - return undefined; - } - - const { name, ...config } = configWithName; - return { name, config }; } /** Shows the location of a server config once it's discovered. */ @@ -371,6 +364,8 @@ export class McpAddConfigurationCommand { // Step 2: Get server details based on type let serverConfig: McpConfigurationServer | undefined; let suggestedName: string | undefined; + let inputs: ConfiguredInput[] | undefined; + let inputValues: Record | undefined; switch (serverType) { case AddConfigurationType.Stdio: serverConfig = await this.getStdioConfig(); @@ -382,8 +377,10 @@ export class McpAddConfigurationCommand { case AddConfigurationType.PipPackage: case AddConfigurationType.DockerImage: { const r = await this.getAssistedConfig(serverType); - serverConfig = r?.config; + serverConfig = r?.server; suggestedName = r?.name; + inputs = r?.inputs; + inputValues = r?.inputValues; break; } default: @@ -418,12 +415,24 @@ export class McpAddConfigurationCommand { : undefined; if (writeToUriDirect) { - await this._jsonEditingService.write(writeToUriDirect, [{ - path: ['servers', serverId], - value: serverConfig - }], true); + await this._jsonEditingService.write(writeToUriDirect, [ + { + path: ['servers', serverId], + value: serverConfig + }, + ...(inputs || []).map(i => ({ + path: ['inputs', -1], + value: i, + })), + ], true); } else { - await this.writeToUserSetting(serverId, serverConfig, target!); + await this.writeToUserSetting(serverId, serverConfig, target!, inputs); + } + + if (inputValues) { + for (const [key, value] of Object.entries(inputValues)) { + await this._mcpRegistry.setSavedInput(key, target ?? ConfigurationTarget.WORKSPACE, value); + } } const packageType = this.getPackageType(serverType); diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts index 01533b48a6d..78aea6c1fb7 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts @@ -224,6 +224,16 @@ export class McpRegistry extends Disposable implements IMcpRegistry { await this._updateStorageWithExpressionInputs(storage, expr); } + public async setSavedInput(inputId: string, target: ConfigurationTarget, value: string): Promise { + const storage = this._getInputStorageInConfigTarget(target); + const expr = ConfigurationResolverExpression.parse(inputId); + for (const unresolved of expr.unresolved()) { + expr.resolve(unresolved, value); + break; + } + await this._updateStorageWithExpressionInputs(storage, expr); + } + public getSavedInputs(scope: StorageScope): Promise<{ [id: string]: IResolvedValue }> { return this._getInputStorage(scope).getMap(); } diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts index 07de8bc1a5b..83d37b7b710 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts @@ -72,6 +72,8 @@ export interface IMcpRegistry { clearSavedInputs(scope: StorageScope, inputId?: string): Promise; /** Edits a previously-saved input. */ editSavedInput(inputId: string, folderData: IWorkspaceFolderData | undefined, configSection: string, target: ConfigurationTarget): Promise; + /** Updates a saved input. */ + setSavedInput(inputId: string, target: ConfigurationTarget, value: string): Promise; /** Gets saved inputs from storage. */ getSavedInputs(scope: StorageScope): Promise<{ [id: string]: IResolvedValue }>; /** Creates a connection for the collection and definition. */ From dd5b6c1235e3c93a4ee59b81f090f20c5733326c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 23 Apr 2025 10:58:44 -0700 Subject: [PATCH 077/114] Revert "pass in an options object instead" This reverts commit c6c85b3e9d3c5b200ee446a209be5473fbcef00c. --- src/vs/workbench/api/common/extHostLanguageModelTools.ts | 8 ++++---- src/vscode-dts/vscode.proposed.toolProgress.d.ts | 9 +-------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 2cbe65e5701..faaa208029e 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -112,10 +112,9 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape throw new Error(`Unknown tool ${dto.toolId}`); } - const options: vscode.LanguageModelToolInvocation = { + const options: vscode.LanguageModelToolInvocationOptions = { input: dto.parameters, toolInvocationToken: dto.context as vscode.ChatParticipantToolToken | undefined, - progress: undefined!, }; if (isProposedApiEnabled(item.extension, 'chatParticipantPrivate')) { options.chatRequestId = dto.chatRequestId; @@ -139,8 +138,9 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } + let progress: vscode.Progress<{ message?: string; increment?: number }> | undefined; if (isProposedApiEnabled(item.extension, 'toolProgress')) { - options.progress = { + progress = { report: value => { this._proxy.$acceptToolProgress(dto.callId, { message: value.message, @@ -151,7 +151,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } - const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token)), token); + const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token, progress!)), token); if (!extensionResult) { throw new CancellationError(); } diff --git a/src/vscode-dts/vscode.proposed.toolProgress.d.ts b/src/vscode-dts/vscode.proposed.toolProgress.d.ts index 6eed0305a81..51562cd6ee3 100644 --- a/src/vscode-dts/vscode.proposed.toolProgress.d.ts +++ b/src/vscode-dts/vscode.proposed.toolProgress.d.ts @@ -19,14 +19,7 @@ declare module 'vscode' { increment?: number; } - export interface LanguageModelToolInvocation extends LanguageModelToolInvocationOptions { - /** - * Progress interface to report updates as the tool is running. - */ - progress: Progress; - } - export interface LanguageModelTool { - invoke(invocation: LanguageModelToolInvocation, token: CancellationToken): ProviderResult; + invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken, progress: Progress): ProviderResult; } } From d8359659bf786c7b92d15c185079b45ed34d4d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E8=BF=82=E8=BF=82?= Date: Thu, 24 Apr 2025 02:03:35 +0800 Subject: [PATCH 078/114] fix: remove necessary async declaration (#247213) --- src/vs/workbench/contrib/chat/common/chatServiceImpl.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 0b8627d26cf..cd07a81b1f0 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -734,7 +734,7 @@ export class ChatService extends Disposable implements IChatService { let chatTitlePromise: Promise | undefined; if (agentPart || (defaultAgent && !commandPart)) { - const prepareChatAgentRequest = async (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): Promise => { + const prepareChatAgentRequest = (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): IChatAgentRequest => { const initVariableData: IChatRequestVariableData = { variables: [] }; request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, agent, command, options?.confirmation, options?.locationData, options?.attachedContext, undefined, options?.userSelectedModelId); @@ -777,7 +777,7 @@ export class ChatService extends Disposable implements IChatService { const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, model.sessionId, location, defaultAgent.id); // Prepare the request object that we will send to the participant detection provider - const chatAgentRequest = await prepareChatAgentRequest(defaultAgent, undefined, enableCommandDetection, undefined, false); + const chatAgentRequest = prepareChatAgentRequest(defaultAgent, undefined, enableCommandDetection, undefined, false); const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token); if (result && this.chatAgentService.getAgent(result.agent.id)?.locations?.includes(location)) { @@ -795,7 +795,7 @@ export class ChatService extends Disposable implements IChatService { // Recompute history in case the agent or command changed const history = this.getHistoryEntriesFromModel(requests, model.sessionId, location, agent.id); - const requestProps = await prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent); + const requestProps = prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent); const pendingRequest = this._pendingRequests.get(sessionId); if (pendingRequest && !pendingRequest.requestId) { pendingRequest.requestId = requestProps.requestId; From 41cc1e28362b0511ac7c39ff42fe1bafc27c4e71 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 23 Apr 2025 11:09:15 -0700 Subject: [PATCH 079/114] update with api feedback --- .../api/browser/mainThreadLanguageModelTools.ts | 7 +++---- src/vs/workbench/api/common/extHost.protocol.ts | 4 ++-- .../api/common/extHostLanguageModelTools.ts | 7 ++++--- .../workbench/contrib/chat/browser/chatSetup.ts | 12 ++++++------ .../chatProgressTypes/chatToolInvocation.ts | 7 +++---- .../workbench/contrib/chat/common/chatService.ts | 2 +- .../chat/common/languageModelToolsService.ts | 12 ++++++++++-- .../contrib/chat/common/tools/editFileTool.ts | 5 ++--- .../chat/electron-sandbox/tools/fetchPageTool.ts | 9 ++++----- .../extensions/common/searchExtensionsTool.ts | 7 +++---- src/vs/workbench/contrib/mcp/common/mcpServer.ts | 16 ++++++++-------- .../workbench/contrib/mcp/common/mcpService.ts | 5 ++--- src/vs/workbench/contrib/mcp/common/mcpTypes.ts | 6 +++--- src/vscode-dts/vscode.proposed.toolProgress.d.ts | 2 +- 14 files changed, 52 insertions(+), 49 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 0a7db37edb5..62fcc73e15e 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -6,8 +6,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; -import { IProgress, IProgressStep } from '../../../platform/progress/common/progress.js'; -import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolProgressStep, IToolResult, ToolProgress } from '../../contrib/chat/common/languageModelToolsService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; import { ExtHostContext, ExtHostLanguageModelToolsShape, MainContext, MainThreadLanguageModelToolsShape } from '../common/extHost.protocol.js'; @@ -19,7 +18,7 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre private readonly _tools = this._register(new DisposableMap()); private readonly _runningToolCalls = new Map; + progress: ToolProgress; }>(); constructor( @@ -49,7 +48,7 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre }; } - $acceptToolProgress(callId: string, progress: IProgressStep): void { + $acceptToolProgress(callId: string, progress: IToolProgressStep): void { this._runningToolCalls.get(callId)?.progress.report(progress); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9eeabd64abd..d72909f4cae 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -59,7 +59,7 @@ import { IChatContentInlineReference, IChatFollowup, IChatNotebookEdit, IChatPro import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariables.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from '../../contrib/chat/common/languageModels.js'; -import { IPreparedToolInvocation, IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { IPreparedToolInvocation, IToolData, IToolInvocation, IToolProgressStep, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js'; import * as notebookCommon from '../../contrib/notebook/common/notebookCommon.js'; @@ -1380,7 +1380,7 @@ export type IToolDataDto = Omit; export interface MainThreadLanguageModelToolsShape extends IDisposable { $getTools(): Promise[]>; - $acceptToolProgress(callId: string, progress: IProgressStep): void; + $acceptToolProgress(callId: string, progress: IToolProgressStep): void; $invokeTool(dto: IToolInvocation, token?: CancellationToken): Promise>; $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise; $registerTool(id: string): void; diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index faaa208029e..9d3272028ee 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -138,12 +138,12 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } - let progress: vscode.Progress<{ message?: string; increment?: number }> | undefined; + let progress: vscode.Progress<{ message?: string | vscode.MarkdownString; increment?: number }> | undefined; if (isProposedApiEnabled(item.extension, 'toolProgress')) { progress = { report: value => { this._proxy.$acceptToolProgress(dto.callId, { - message: value.message, + message: typeConvert.MarkdownString.fromStrict(value.message), increment: value.increment, total: 100, }); @@ -151,7 +151,8 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } - const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token, progress!)), token); + // todo: 'any' cast because TS can't handle the overloads + const extensionResult = await raceCancellation(Promise.resolve((item.tool.invoke as any)(options, token, progress!)), token); if (!extensionResult) { throw new CancellationError(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 2fb469b94f2..3f7389ecdde 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -7,6 +7,7 @@ import { $ } from '../../../../base/browser/dom.js'; import { Dialog } from '../../../../base/browser/ui/dialog/dialog.js'; import { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; import { timeout } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { isCancellationError } from '../../../../base/common/errors.js'; @@ -37,7 +38,7 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import product from '../../../../platform/product/common/product.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; -import { IProgress, IProgressService, IProgressStep, ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; @@ -52,11 +53,13 @@ import { IHostService } from '../../../services/host/browser/host.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService } from '../common/chatEntitlementService.js'; -import { IChatRequestModel, ChatRequestModel, ChatModel, IChatRequestVariableData, IChatRequestToolEntry } from '../common/chatModel.js'; +import { ChatModel, ChatRequestModel, IChatRequestModel, IChatRequestToolEntry, IChatRequestVariableData } from '../common/chatModel.js'; +import { ChatRequestAgentPart, ChatRequestToolPart } from '../common/chatParserTypes.js'; import { IChatProgress, IChatService } from '../common/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../common/constants.js'; import { ILanguageModelsService } from '../common/languageModels.js'; @@ -64,9 +67,6 @@ import { CHAT_CATEGORY, CHAT_OPEN_ACTION_ID, CHAT_SETUP_ACTION_ID } from './acti import { ChatViewId, IChatWidgetService, showCopilotView } from './chat.js'; import { CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js'; import './media/chatSetup.css'; -import { ChatRequestAgentPart, ChatRequestToolPart } from '../common/chatParserTypes.js'; -import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -518,7 +518,7 @@ class SetupTool extends Disposable implements IToolImpl { super(); } - invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: IProgress, token: CancellationToken): Promise { + invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise { const result: IToolResult = { content: [ { diff --git a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts index 6df0d41ecaf..b9b27e255f4 100644 --- a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts @@ -7,9 +7,8 @@ import { DeferredPromise } from '../../../../../base/common/async.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { localize } from '../../../../../nls.js'; -import { IProgressStep } from '../../../../../platform/progress/common/progress.js'; import { IChatTerminalToolInvocationData, IChatToolInputInvocationData, IChatToolInvocation, IChatToolInvocationSerialized } from '../chatService.js'; -import { IPreparedToolInvocation, IToolConfirmationMessages, IToolData, IToolResult } from '../languageModelToolsService.js'; +import { IPreparedToolInvocation, IToolConfirmationMessages, IToolData, IToolProgressStep, IToolResult } from '../languageModelToolsService.js'; export class ChatToolInvocation implements IChatToolInvocation { public readonly kind: 'toolInvocation' = 'toolInvocation'; @@ -47,7 +46,7 @@ export class ChatToolInvocation implements IChatToolInvocation { public readonly toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData; - public readonly progress = observableValue<{ message?: string; progress: number }>(this, { progress: 0 }); + public readonly progress = observableValue<{ message?: string | IMarkdownString; progress: number }>(this, { progress: 0 }); constructor(preparedInvocation: IPreparedToolInvocation | undefined, toolData: IToolData, public readonly toolCallId: string) { const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${toolData.displayName}"`); @@ -88,7 +87,7 @@ export class ChatToolInvocation implements IChatToolInvocation { return this._confirmationMessages; } - public acceptProgress(step: IProgressStep) { + public acceptProgress(step: IToolProgressStep) { const prev = this.progress.get(); this.progress.set({ progress: step.increment ? (prev.progress + step.increment) : prev.progress, diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 3bd7b542c92..9e9cead60f9 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -235,7 +235,7 @@ export interface IChatToolInvocation { invocationMessage: string | IMarkdownString; pastTenseMessage: string | IMarkdownString | undefined; resultDetails: IToolResult['toolResultDetails']; - progress: IObservable<{ message?: string; progress: number }>; + progress: IObservable<{ message?: string | IMarkdownString; progress: number }>; readonly toolId: string; readonly toolCallId: string; diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index 75332b21169..b574302dbc2 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -15,7 +15,7 @@ import { Location } from '../../../../editor/common/languages.js'; import { ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; +import { IProgress } from '../../../../platform/progress/common/progress.js'; import { IChatTerminalToolInvocationData, IChatToolInputInvocationData } from './chatService.js'; import { PromptElementJSON, stringifyPromptElementJSON } from './tools/promptTsxTypes.js'; @@ -41,6 +41,14 @@ export interface IToolData { supportsToolPicker?: boolean; } +export interface IToolProgressStep { + readonly message: string | IMarkdownString | undefined; + readonly increment: number | undefined; + readonly total: number | undefined; +} + +export type ToolProgress = IProgress; + export type ToolDataSource = | { type: 'extension'; @@ -135,7 +143,7 @@ export interface IPreparedToolInvocation { } export interface IToolImpl { - invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: IProgress, token: CancellationToken): Promise; + invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise; prepareToolInvocation?(parameters: any, token: CancellationToken): Promise; } diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index 8206d59c212..8c4dc2cd9a4 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -9,7 +9,6 @@ import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; -import { IProgress, IProgressStep } from '../../../../../platform/progress/common/progress.js'; import { SaveReason } from '../../../../common/editor.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { CellUri } from '../../../notebook/common/notebookCommon.js'; @@ -17,7 +16,7 @@ import { INotebookService } from '../../../notebook/common/notebookService.js'; import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { ChatModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; -import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../common/languageModelToolsService.js'; export const ExtensionEditToolId = 'vscode_editFile'; export const InternalEditToolId = 'vscode_editFile_internal'; @@ -43,7 +42,7 @@ export class EditTool implements IToolImpl { @INotebookService private readonly notebookService: INotebookService, ) { } - async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, _progress: IProgress, token: CancellationToken): Promise { + async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { if (!invocation.context) { throw new Error('toolInvocationToken is required for this tool'); } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts b/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts index 7d361f97bb8..632a9b9bbc3 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts @@ -3,15 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../../nls.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { URI } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; import { IWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { ITrustedDomainService } from '../../../url/browser/trustedDomainService.js'; -import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultTextPart } from '../../common/languageModelToolsService.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultTextPart, ToolProgress } from '../../common/languageModelToolsService.js'; import { InternalFetchWebPageToolId } from '../../common/tools/tools.js'; -import { IProgress, IProgressStep } from '../../../../../platform/progress/common/progress.js'; export const FetchWebPageToolData: IToolData = { id: InternalFetchWebPageToolId, @@ -42,7 +41,7 @@ export class FetchWebPageTool implements IToolImpl { @ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService, ) { } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: IProgress, _token: CancellationToken): Promise { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { const parsedUriResults = this._parseUris((invocation.parameters as { urls?: string[] }).urls); const validUris = Array.from(parsedUriResults.values()).filter((uri): uri is URI => !!uri); if (!validUris.length) { diff --git a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts index 98d19d83a65..9668c594e98 100644 --- a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -9,9 +9,8 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { SortBy } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js'; -import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; -import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; -import { ExtensionState, IExtensionsWorkbenchService, IExtension } from '../common/extensions.js'; +import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js'; +import { ExtensionState, IExtension, IExtensionsWorkbenchService } from '../common/extensions.js'; export const SearchExtensionsToolId = 'vscode_searchExtensions_internal'; @@ -73,7 +72,7 @@ export class SearchExtensionsTool implements IToolImpl { @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, ) { } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: IProgress, token: CancellationToken): Promise { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { const params = invocation.parameters as InputParams; if (!params.keywords?.length && !params.category && !params.ids?.length) { return { diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index c6e9129988c..01f828d7211 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -4,31 +4,31 @@ *--------------------------------------------------------------------------------------------*/ import { raceCancellationError, Sequencer } from '../../../../base/common/async.js'; -import * as json from '../../../../base/common/json.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import * as json from '../../../../base/common/json.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { LRUCache } from '../../../../base/common/map.js'; import { autorun, autorunWithStore, derived, disposableObservableValue, IObservable, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js'; import { basename } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IOutputService } from '../../../services/output/common/output.js'; +import { ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { mcpActivationEvent } from './mcpConfiguration.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; import { extensionMcpCollectionPrefix, IMcpServer, IMcpServerConnection, IMcpTool, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, McpServerDefinition, McpServerToolsState } from './mcpTypes.js'; import { MCP } from './modelContextProtocol.js'; -import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; -import { localize } from '../../../../nls.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; type ServerBootData = { supportsLogging: boolean; @@ -502,7 +502,7 @@ export class McpTool implements IMcpTool { return this._server.callOn(h => h.callTool({ name, arguments: params }, token), token); } - callWithProgress(params: Record, progress: IProgress, token?: CancellationToken): Promise { + callWithProgress(params: Record, progress: ToolProgress, token?: CancellationToken): Promise { // serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it. const name = this._definition.serverToolName ?? this._definition.name; const progressToken = generateUuid(); diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts index bf4c7407011..a3578344379 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -14,9 +14,8 @@ import { localize } from '../../../../nls.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; -import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; -import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; import { McpServer, McpServerMetadataCache } from './mcpServer.js'; import { IMcpServer, IMcpService, IMcpTool, McpCollectionDefinition, McpServerDefinition, McpServerToolsState } from './mcpTypes.js'; @@ -247,7 +246,7 @@ class McpToolImplementation implements IToolImpl { }; } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, progress: IProgress, token: CancellationToken) { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken) { const result: IToolResult = { content: [] diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 529f0e9fb66..d87721225b8 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { equals as arraysEqual } from '../../../../base/common/arrays.js'; import { assertNever } from '../../../../base/common/assert.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { equals as objectsEqual } from '../../../../base/common/objects.js'; -import { equals as arraysEqual } from '../../../../base/common/arrays.js'; import { IObservable } from '../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { Location } from '../../../../editor/common/languages.js'; @@ -17,9 +17,9 @@ import { ExtensionIdentifier } from '../../../../platform/extensions/common/exte import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; +import { ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; import { MCP } from './modelContextProtocol.js'; -import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; export const extensionMcpCollectionPrefix = 'ext.'; @@ -264,7 +264,7 @@ export interface IMcpTool { /** * Identical to {@link call}, but reports progress. */ - callWithProgress(params: Record, progress: IProgress, token?: CancellationToken): Promise; + callWithProgress(params: Record, progress: ToolProgress, token?: CancellationToken): Promise; } export const enum McpServerTransportType { diff --git a/src/vscode-dts/vscode.proposed.toolProgress.d.ts b/src/vscode-dts/vscode.proposed.toolProgress.d.ts index 51562cd6ee3..0d20f626cc1 100644 --- a/src/vscode-dts/vscode.proposed.toolProgress.d.ts +++ b/src/vscode-dts/vscode.proposed.toolProgress.d.ts @@ -12,7 +12,7 @@ declare module 'vscode' { /** * A progress message that represents a chunk of work */ - message?: string; + message?: string | MarkdownString; /** * An increment for discrete progress. Increments will be summed up until 100 (100%) is reached */ From fc5d4141f9b55b266e972ece23601b898d0c2467 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 23 Apr 2025 11:23:06 -0700 Subject: [PATCH 080/114] mcp: fix URL encoding of entities (#247241) Fixes #247111 --- src/vs/workbench/api/common/extHostMcp.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index a163ff04aa0..39683cb2034 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -214,7 +214,7 @@ class McpHTTPHandle extends Disposable { headers['Mcp-Session-Id'] = sessionId; } - const res = await fetch(this._launch.uri.toString(), { + const res = await fetch(this._launch.uri.toString(true), { method: 'POST', signal: this._abortCtrl.signal, headers, @@ -313,7 +313,7 @@ class McpHTTPHandle extends Disposable { headers['Last-Event-ID'] = lastEventId; } - res = await fetch(this._launch.uri.toString(), { + res = await fetch(this._launch.uri.toString(true), { method: 'GET', signal: this._abortCtrl.signal, headers, @@ -356,7 +356,7 @@ class McpHTTPHandle extends Disposable { let res: Response; try { - res = await fetch(this._launch.uri.toString(), { + res = await fetch(this._launch.uri.toString(true), { method: 'GET', signal: this._abortCtrl.signal, headers: { @@ -377,7 +377,7 @@ class McpHTTPHandle extends Disposable { if (event.type === 'message') { this._proxy.$onDidReceiveMessage(this._id, event.data); } else if (event.type === 'endpoint') { - postEndpoint.complete(new URL(event.data, this._launch.uri.toString()).toString()); + postEndpoint.complete(new URL(event.data, this._launch.uri.toString(true)).toString()); } }); From 917dbd86947f691b1d9b75bb26ebd71041ece761 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 23 Apr 2025 11:24:07 -0700 Subject: [PATCH 081/114] mcp: support tildes in definitions (#247242) Fixes #247076 --- src/vs/workbench/api/node/extHostMcpNode.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/node/extHostMcpNode.ts b/src/vs/workbench/api/node/extHostMcpNode.ts index 594fe45d0be..1311da368b1 100644 --- a/src/vs/workbench/api/node/extHostMcpNode.ts +++ b/src/vs/workbench/api/node/extHostMcpNode.ts @@ -14,6 +14,7 @@ import { McpConnectionState, McpServerLaunch, McpServerTransportStdio, McpServer import { ExtHostMcpService } from '../common/extHostMcp.js'; import { IExtHostRpcService } from '../common/extHostRpcService.js'; import { findExecutable } from '../../../base/node/processes.js'; +import { untildify } from '../../../base/common/labels.js'; export class NodeExtHostMpcService extends ExtHostMcpService { constructor( @@ -80,8 +81,15 @@ export class NodeExtHostMpcService extends ExtHostMcpService { const abortCtrl = new AbortController(); let child: ChildProcessWithoutNullStreams; try { - const cwd = launch.cwd ? URI.revive(launch.cwd).fsPath : homedir(); - const { executable, args, shell } = await formatSubprocessArguments(launch.command, launch.args, cwd, env); + const home = homedir(); + const cwd = launch.cwd ? URI.revive(launch.cwd).fsPath : home; + const { executable, args, shell } = await formatSubprocessArguments( + untildify(launch.command, home), + launch.args.map(a => untildify(a, home)), + cwd, + env + ); + this._proxy.$onDidPublishLog(id, LogLevel.Debug, `Server command line: ${executable} ${args.join(' ')}`); child = spawn(executable, args, { stdio: 'pipe', From 68f70843d6098e4e3aa118d6ba3d915bd6acb40c Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:20:23 -0700 Subject: [PATCH 082/114] Add two-phase AI Settings Search (#247177) * Add AI settings search proposed API * Use Progress API * Implement new API into remote search provider --- .../common/extensionsApiProposals.ts | 3 + .../api/browser/extensionHost.contribution.ts | 1 + .../api/browser/mainThreadAiSettingsSearch.ts | 44 +++++++++ .../workbench/api/common/extHost.api.impl.ts | 7 ++ .../workbench/api/common/extHost.protocol.ts | 13 +++ .../api/common/extHostAiSettingsSearch.ts | 52 +++++++++++ .../api/common/extHostTypeConverters.ts | 24 +++++ src/vs/workbench/api/common/extHostTypes.ts | 6 ++ .../preferences/browser/preferencesSearch.ts | 92 +++++++------------ .../preferences/browser/settingsEditor2.ts | 22 ++++- .../common/aiSettingsSearch.ts | 44 +++++++++ .../common/aiSettingsSearchService.ts | 87 ++++++++++++++++++ src/vs/workbench/workbench.common.main.ts | 1 + .../vscode.proposed.aiSettingsSearch.d.ts | 30 ++++++ 14 files changed, 366 insertions(+), 60 deletions(-) create mode 100644 src/vs/workbench/api/browser/mainThreadAiSettingsSearch.ts create mode 100644 src/vs/workbench/api/common/extHostAiSettingsSearch.ts create mode 100644 src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearch.ts create mode 100644 src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearchService.ts create mode 100644 src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 5e5e982cdfa..c825f45db15 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -12,6 +12,9 @@ const _allApiProposals = { aiRelatedInformation: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', }, + aiSettingsSearch: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts', + }, aiTextSearchProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', version: 2 diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 7a778aa030b..d5430634469 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -88,6 +88,7 @@ import './mainThreadShare.js'; import './mainThreadProfileContentHandlers.js'; import './mainThreadAiRelatedInformation.js'; import './mainThreadAiEmbeddingVector.js'; +import './mainThreadAiSettingsSearch.js'; import './mainThreadMcp.js'; import './mainThreadChatStatus.js'; diff --git a/src/vs/workbench/api/browser/mainThreadAiSettingsSearch.ts b/src/vs/workbench/api/browser/mainThreadAiSettingsSearch.ts new file mode 100644 index 00000000000..8f7dde42d91 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadAiSettingsSearch.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; +import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; +import { AiSettingsSearchResult, IAiSettingsSearchProvider, IAiSettingsSearchService } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; +import { ExtHostContext, ExtHostAiSettingsSearchShape, MainContext, MainThreadAiSettingsSearchShape, } from '../common/extHost.protocol.js'; + +@extHostNamedCustomer(MainContext.MainThreadAiSettingsSearch) +export class MainThreadAiSettingsSearch extends Disposable implements MainThreadAiSettingsSearchShape { + private readonly _proxy: ExtHostAiSettingsSearchShape; + private readonly _registrations = this._register(new DisposableMap()); + + constructor( + context: IExtHostContext, + @IAiSettingsSearchService private readonly _settingsSearchService: IAiSettingsSearchService, + ) { + super(); + this._proxy = context.getProxy(ExtHostContext.ExtHostAiSettingsSearch); + } + + $registerAiSettingsSearchProvider(handle: number): void { + const provider: IAiSettingsSearchProvider = { + searchSettings: (query, option, token) => { + return this._proxy.$startSearch(handle, query, option, token); + } + }; + this._registrations.set(handle, this._settingsSearchService.registerSettingsSearchProvider(provider)); + } + + $unregisterAiSettingsSearchProvider(handle: number): void { + this._registrations.deleteAndDispose(handle); + } + + $handleSearchResult(handle: number, result: AiSettingsSearchResult): void { + if (!this._registrations.has(handle)) { + throw new Error(`No AI settings search provider found`); + } + + this._settingsSearchService.handleSearchResult(result); + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index b5699214188..522a47cfc8f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -110,6 +110,7 @@ import { ExtHostWebviewPanels } from './extHostWebviewPanels.js'; import { ExtHostWebviewViews } from './extHostWebviewView.js'; import { IExtHostWindow } from './extHostWindow.js'; import { IExtHostWorkspace } from './extHostWorkspace.js'; +import { ExtHostAiSettingsSearch } from './extHostAiSettingsSearch.js'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -218,6 +219,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostLanguageModels, extHostDiagnostics, extHostLanguageModelTools)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); + const extHostAiSettingsSearch = rpcProtocol.set(ExtHostContext.ExtHostAiSettingsSearch, new ExtHostAiSettingsSearch(rpcProtocol)); const extHostStatusBar = rpcProtocol.set(ExtHostContext.ExtHostStatusBar, new ExtHostStatusBar(rpcProtocol, extHostCommands.converter)); const extHostSpeech = rpcProtocol.set(ExtHostContext.ExtHostSpeech, new ExtHostSpeech(rpcProtocol)); const extHostEmbeddings = rpcProtocol.set(ExtHostContext.ExtHostEmbeddings, new ExtHostEmbeddings(rpcProtocol)); @@ -1438,6 +1440,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerEmbeddingVectorProvider(model: string, provider: vscode.EmbeddingVectorProvider) { checkProposedApiEnabled(extension, 'aiRelatedInformation'); return extHostAiEmbeddingVector.registerEmbeddingVectorProvider(extension, model, provider); + }, + registerSettingsSearchProvider(provider: vscode.SettingsSearchProvider) { + checkProposedApiEnabled(extension, 'aiSettingsSearch'); + return extHostAiSettingsSearch.registerSettingsSearchProvider(extension, provider); } }; @@ -1835,6 +1841,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatErrorLevel: extHostTypes.ChatErrorLevel, McpHttpServerDefinition: extHostTypes.McpHttpServerDefinition, McpStdioServerDefinition: extHostTypes.McpStdioServerDefinition, + SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d72909f4cae..214bb774b90 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -86,6 +86,7 @@ import { CandidatePort } from '../../services/remote/common/tunnelModel.js'; import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../services/search/common/queryBuilder.js'; import * as search from '../../services/search/common/search.js'; import { AISearchKeyword, TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js'; +import { AiSettingsSearchProviderOptions, AiSettingsSearchResult } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js'; import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; import * as tasks from './shared/tasks.js'; @@ -1986,6 +1987,16 @@ export interface MainThreadAiRelatedInformationShape { $unregisterAiRelatedInformationProvider(handle: number): void; } +export interface ExtHostAiSettingsSearchShape { + $startSearch(handle: number, query: string, option: AiSettingsSearchProviderOptions, token: CancellationToken): Promise; +} + +export interface MainThreadAiSettingsSearchShape { + $registerAiSettingsSearchProvider(handle: number): void; + $unregisterAiSettingsSearchProvider(handle: number): void; + $handleSearchResult(handle: number, result: AiSettingsSearchResult): void; +} + export interface ExtHostAiEmbeddingVectorShape { $provideAiEmbeddingVector(handle: number, strings: string[], token: CancellationToken): Promise; } @@ -3155,6 +3166,7 @@ export const MainContext = { MainThreadAiRelatedInformation: createProxyIdentifier('MainThreadAiRelatedInformation'), MainThreadAiEmbeddingVector: createProxyIdentifier('MainThreadAiEmbeddingVector'), MainThreadChatStatus: createProxyIdentifier('MainThreadChatStatus'), + MainThreadAiSettingsSearch: createProxyIdentifier('MainThreadAiSettingsSearch'), }; export const ExtHostContext = { @@ -3217,6 +3229,7 @@ export const ExtHostContext = { ExtHostEmbeddings: createProxyIdentifier('ExtHostEmbeddings'), ExtHostAiRelatedInformation: createProxyIdentifier('ExtHostAiRelatedInformation'), ExtHostAiEmbeddingVector: createProxyIdentifier('ExtHostAiEmbeddingVector'), + ExtHostAiSettingsSearch: createProxyIdentifier('ExtHostAiSettingsSearch'), ExtHostTheming: createProxyIdentifier('ExtHostTheming'), ExtHostTunnelService: createProxyIdentifier('ExtHostTunnelService'), ExtHostManagedSockets: createProxyIdentifier('ExtHostManagedSockets'), diff --git a/src/vs/workbench/api/common/extHostAiSettingsSearch.ts b/src/vs/workbench/api/common/extHostAiSettingsSearch.ts new file mode 100644 index 00000000000..c7c2d32f33d --- /dev/null +++ b/src/vs/workbench/api/common/extHostAiSettingsSearch.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { SettingsSearchProvider, SettingsSearchResult } from 'vscode'; +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { AiSettingsSearchProviderOptions } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; +import { ExtHostAiSettingsSearchShape, IMainContext, MainContext, MainThreadAiSettingsSearchShape } from './extHost.protocol.js'; +import { Disposable } from './extHostTypes.js'; +import { Progress } from '../../../platform/progress/common/progress.js'; +import { AiSettingsSearch } from './extHostTypeConverters.js'; + +export class ExtHostAiSettingsSearch implements ExtHostAiSettingsSearchShape { + private _settingsSearchProviders: Map = new Map(); + private _nextHandle = 0; + + private readonly _proxy: MainThreadAiSettingsSearchShape; + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadAiSettingsSearch); + } + + async $startSearch(handle: number, query: string, option: AiSettingsSearchProviderOptions, token: CancellationToken): Promise { + if (this._settingsSearchProviders.size === 0) { + throw new Error('No related information providers registered'); + } + + const provider = this._settingsSearchProviders.get(handle); + if (!provider) { + throw new Error('Settings search provider not found'); + } + + const progressReporter = new Progress((data) => { + this._proxy.$handleSearchResult(handle, AiSettingsSearch.fromSettingsSearchResult(data)); + }); + + return provider.provideSettingsSearchResults(query, option, progressReporter, token); + } + + registerSettingsSearchProvider(extension: IExtensionDescription, provider: SettingsSearchProvider): Disposable { + const handle = this._nextHandle; + this._nextHandle++; + this._settingsSearchProviders.set(handle, provider); + this._proxy.$registerAiSettingsSearchProvider(handle); + return new Disposable(() => { + this._proxy.$unregisterAiSettingsSearchProvider(handle); + this._settingsSearchProviders.delete(handle); + }); + } +} diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 3177861cb19..1bab9eeac00 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -63,6 +63,7 @@ import { getPrivateApiFor } from './extHostTestingPrivateApi.js'; import * as types from './extHostTypes.js'; import { LanguageModelPromptTsxPart, LanguageModelTextPart } from './extHostTypes.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; +import { AiSettingsSearchResult, AiSettingsSearchResultKind } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; import { McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; export namespace Command { @@ -3278,6 +3279,29 @@ export namespace IconPath { } } +export namespace AiSettingsSearch { + export function fromSettingsSearchResult(result: vscode.SettingsSearchResult): AiSettingsSearchResult { + return { + query: result.query, + kind: fromSettingsSearchResultKind(result.kind), + settings: result.settings + }; + } + + function fromSettingsSearchResultKind(kind: number): AiSettingsSearchResultKind { + switch (kind) { + case AiSettingsSearchResultKind.EMBEDDED: + return AiSettingsSearchResultKind.EMBEDDED; + case AiSettingsSearchResultKind.LLM_RANKED: + return AiSettingsSearchResultKind.LLM_RANKED; + case AiSettingsSearchResultKind.CANCELED: + return AiSettingsSearchResultKind.CANCELED; + default: + throw new Error('Unknown AiSettingsSearchResultKind'); + } + } +} + export namespace McpServerDefinition { function isHttpConfig(candidate: vscode.McpServerDefinition): candidate is vscode.McpHttpServerDefinition { return !!(candidate as vscode.McpHttpServerDefinition).uri; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index c7b7ea6ed80..7956f27742c 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -5108,6 +5108,12 @@ export enum RelatedInformationType { SettingInformation = 4 } +export enum SettingsSearchResultKind { + EMBEDDED = 1, + LLM_RANKED = 2, + CANCELED = 3, +} + //#endregion //#region Speech diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 762a4622a6b..833ba3ec78e 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -17,10 +17,10 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ExtensionType } from '../../../../platform/extensions/common/extensions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IAiRelatedInformationService, RelatedInformationType, SettingInformationResult } from '../../../services/aiRelatedInformation/common/aiRelatedInformation.js'; import { TfIdfCalculator, TfIdfDocument } from '../../../../base/common/tfIdf.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; import { nullRange } from '../../../services/preferences/common/preferencesModels.js'; +import { IAiSettingsSearchService } from '../../../services/aiSettingsSearch/common/aiSettingsSearch.js'; export interface IEndpointDetails { urlBase?: string; @@ -350,14 +350,11 @@ export class SettingMatches { } } -class AiRelatedInformationSearchKeysProvider { - private settingKeys: string[] = []; +class AiSettingsSearchKeysProvider { private settingsRecord: IStringDictionary = {}; private currentPreferencesModel: ISettingsEditorModel | undefined; - constructor( - private readonly aiRelatedInformationService: IAiRelatedInformationService - ) { } + constructor() { } updateModel(preferencesModel: ISettingsEditorModel) { if (preferencesModel === this.currentPreferencesModel) { @@ -369,13 +366,9 @@ class AiRelatedInformationSearchKeysProvider { } private refresh() { - this.settingKeys = []; this.settingsRecord = {}; - if ( - !this.currentPreferencesModel || - !this.aiRelatedInformationService.isEnabled() - ) { + if (!this.currentPreferencesModel) { return; } @@ -385,32 +378,25 @@ class AiRelatedInformationSearchKeysProvider { } for (const section of group.sections) { for (const setting of section.settings) { - this.settingKeys.push(setting.key); this.settingsRecord[setting.key] = setting; } } } } - getSettingKeys(): string[] { - return this.settingKeys; - } - getSettingsRecord(): IStringDictionary { return this.settingsRecord; } } -class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { - private static readonly AI_RELATED_INFORMATION_MAX_PICKS = 5; +class AiSettingsSearchProvider implements IRemoteSearchProvider { + private static readonly AI_SETTINGS_SEARCH_MAX_PICKS = 5; - private readonly _keysProvider: AiRelatedInformationSearchKeysProvider; + private readonly _keysProvider: AiSettingsSearchKeysProvider; private _filter: string = ''; - constructor( - @IAiRelatedInformationService private readonly aiRelatedInformationService: IAiRelatedInformationService - ) { - this._keysProvider = new AiRelatedInformationSearchKeysProvider(aiRelatedInformationService); + constructor(private readonly aiSettingsSearchService: IAiSettingsSearchService) { + this._keysProvider = new AiSettingsSearchKeysProvider(); } setFilter(filter: string) { @@ -420,41 +406,38 @@ class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise { if ( !this._filter || - !this.aiRelatedInformationService.isEnabled() + !this.aiSettingsSearchService.isEnabled() ) { return null; } this._keysProvider.updateModel(preferencesModel); + this.aiSettingsSearchService.startSearch(this._filter, token); return { - filterMatches: await this.getAiRelatedInformationItems(token), + filterMatches: await this.getAiSettingsSearchItems(token), exactMatch: false }; } - private async getAiRelatedInformationItems(token: CancellationToken) { + private async getAiSettingsSearchItems(token: CancellationToken): Promise { const settingsRecord = this._keysProvider.getSettingsRecord(); - const filterMatches: ISettingMatch[] = []; - const relatedInformation = await this.aiRelatedInformationService.getRelatedInformation( - this._filter, - [RelatedInformationType.SettingInformation], - token - ) as SettingInformationResult[]; - relatedInformation.sort((a, b) => b.weight - a.weight); + const settings = await this.aiSettingsSearchService.getEmbeddingsResults(this._filter, token); + if (!settings) { + return []; + } - for (const info of relatedInformation) { - if (filterMatches.length === AiRelatedInformationSearchProvider.AI_RELATED_INFORMATION_MAX_PICKS) { + for (const settingKey of settings) { + if (filterMatches.length === AiSettingsSearchProvider.AI_SETTINGS_SEARCH_MAX_PICKS) { break; } - const pick = info.setting; filterMatches.push({ - setting: settingsRecord[pick], - matches: [settingsRecord[pick].range], + setting: settingsRecord[settingKey], + matches: [settingsRecord[settingKey].range], matchType: SettingMatchType.RemoteMatch, keyMatchScore: 0, - score: info.weight + score: 0 // the results are sorted upstream. }); } @@ -560,29 +543,21 @@ class TfIdfSearchProvider implements IRemoteSearchProvider { } class RemoteSearchProvider implements IRemoteSearchProvider { - private adaSearchProvider: AiRelatedInformationSearchProvider | undefined; - private tfIdfSearchProvider: TfIdfSearchProvider | undefined; + private aiSettingsSearchProvider: AiSettingsSearchProvider; + private tfIdfSearchProvider: TfIdfSearchProvider; private filter: string = ''; constructor( - @IAiRelatedInformationService private readonly aiRelatedInformationService: IAiRelatedInformationService + @IAiSettingsSearchService private readonly aiSettingsSearchService: IAiSettingsSearchService ) { - } - - private initializeSearchProviders() { - if (this.aiRelatedInformationService.isEnabled()) { - this.adaSearchProvider ??= new AiRelatedInformationSearchProvider(this.aiRelatedInformationService); - } - this.tfIdfSearchProvider ??= new TfIdfSearchProvider(); + this.aiSettingsSearchProvider = new AiSettingsSearchProvider(this.aiSettingsSearchService); + this.tfIdfSearchProvider = new TfIdfSearchProvider(); } setFilter(filter: string): void { - this.initializeSearchProviders(); this.filter = filter; - if (this.adaSearchProvider) { - this.adaSearchProvider.setFilter(filter); - } - this.tfIdfSearchProvider!.setFilter(filter); + this.tfIdfSearchProvider.setFilter(filter); + this.aiSettingsSearchProvider.setFilter(filter); } async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise { @@ -590,17 +565,16 @@ class RemoteSearchProvider implements IRemoteSearchProvider { return null; } - if (!this.adaSearchProvider) { - return this.tfIdfSearchProvider!.searchModel(preferencesModel, token); + if (!this.aiSettingsSearchService.isEnabled()) { + return this.tfIdfSearchProvider.searchModel(preferencesModel, token); } - // Use TF-IDF search as a fallback, ref https://github.com/microsoft/vscode/issues/224946 - let results = await this.adaSearchProvider.searchModel(preferencesModel, token); + let results = await this.aiSettingsSearchProvider.searchModel(preferencesModel, token); if (results?.filterMatches.length) { return results; } if (!token.isCancellationRequested) { - results = await this.tfIdfSearchProvider!.searchModel(preferencesModel, token); + results = await this.tfIdfSearchProvider.searchModel(preferencesModel, token); if (results?.filterMatches.length) { return results; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index e5de67fb278..49e22168be0 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -68,6 +68,7 @@ import { IEditorProgressService } from '../../../../platform/progress/common/pro import { IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; import { CodeWindow } from '../../../../base/browser/window.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; +import { IAiSettingsSearchService } from '../../../services/aiSettingsSearch/common/aiSettingsSearch.js'; export const enum SettingsFocusContext { @@ -249,6 +250,7 @@ export class SettingsEditor2 extends EditorPane { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, @IUserDataProfileService userDataProfileService: IUserDataProfileService, + @IAiSettingsSearchService private readonly aiSettingsSearchService: IAiSettingsSearchService ) { super(SettingsEditor2.ID, group, telemetryService, themeService, storageService); this.searchDelayer = new Delayer(300); @@ -1700,13 +1702,31 @@ export class SettingsEditor2 extends EditorPane { return; } const localResults = await this.localFilterPreferences(query, searchInProgress.token); + let remoteResults = null; if (localResults && !localResults.exactMatch && !searchInProgress.token.isCancellationRequested) { - await this.remoteSearchPreferences(query, searchInProgress.token); + remoteResults = await this.remoteSearchPreferences(query, searchInProgress.token); + } + + if (searchInProgress.token.isCancellationRequested) { + return; } // Update UI only after all the search results are in // ref https://github.com/microsoft/vscode/issues/224946 this.onDidFinishSearch(); + + if (remoteResults) { + if (this.aiSettingsSearchService.isEnabled() && !searchInProgress.token.isCancellationRequested) { + const rankedResults = await this.aiSettingsSearchService.getLLMRankedResults(query, searchInProgress.token); + if (!searchInProgress.token.isCancellationRequested) { + if (rankedResults === null) { + this.logService.trace('No ranked results found'); + } else { + this.logService.trace(`Got ranked results ${rankedResults.join(', ')}`); + } + } + } + } }); } diff --git a/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearch.ts b/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearch.ts new file mode 100644 index 00000000000..f5d3a3b50bc --- /dev/null +++ b/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearch.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +export const IAiSettingsSearchService = createDecorator('IAiSettingsSearchService'); + +export enum AiSettingsSearchResultKind { + EMBEDDED = 1, + LLM_RANKED = 2, + CANCELED = 3, +} + +export interface AiSettingsSearchResult { + query: string; + kind: AiSettingsSearchResultKind; + settings: string[]; +} + +export interface AiSettingsSearchProviderOptions { + limit: number; +} + +export interface IAiSettingsSearchService { + readonly _serviceBrand: undefined; + + // Called from the Settings editor + isEnabled(): boolean; + startSearch(query: string, token: CancellationToken): void; + getEmbeddingsResults(query: string, token: CancellationToken): Promise; + getLLMRankedResults(query: string, token: CancellationToken): Promise; + + // Called from the main thread + registerSettingsSearchProvider(provider: IAiSettingsSearchProvider): IDisposable; + handleSearchResult(results: AiSettingsSearchResult): void; +} + +export interface IAiSettingsSearchProvider { + searchSettings(query: string, option: AiSettingsSearchProviderOptions, token: CancellationToken): void; +} diff --git a/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearchService.ts b/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearchService.ts new file mode 100644 index 00000000000..d34ec08f301 --- /dev/null +++ b/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearchService.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DeferredPromise, raceCancellation } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { AiSettingsSearchResult, AiSettingsSearchResultKind, IAiSettingsSearchProvider, IAiSettingsSearchService } from './aiSettingsSearch.js'; + +export class AiSettingsSearchService implements IAiSettingsSearchService { + readonly _serviceBrand: undefined; + private static readonly MAX_PICKS = 5; + + private _providers: IAiSettingsSearchProvider[] = []; + private _llmRankedResultsPromises: Map> = new Map(); + private _embeddingsResultsPromises: Map> = new Map(); + + isEnabled(): boolean { + return this._providers.length > 0; + } + + registerSettingsSearchProvider(provider: IAiSettingsSearchProvider): IDisposable { + this._providers.push(provider); + return { + dispose: () => { + const index = this._providers.indexOf(provider); + if (index !== -1) { + this._providers.splice(index, 1); + } + } + }; + } + + startSearch(query: string, token: CancellationToken): void { + if (!this.isEnabled()) { + throw new Error('No settings search providers registered'); + } + + this._providers.forEach(provider => provider.searchSettings(query, { limit: AiSettingsSearchService.MAX_PICKS }, token)); + } + + async getEmbeddingsResults(query: string, token: CancellationToken): Promise { + if (!this.isEnabled()) { + throw new Error('No settings search providers registered'); + } + + const promise = new DeferredPromise(); + this._embeddingsResultsPromises.set(query, promise); + const result = await raceCancellation(promise.p, token); + return result ?? null; + } + + async getLLMRankedResults(query: string, token: CancellationToken): Promise { + if (!this.isEnabled()) { + throw new Error('No settings search providers registered'); + } + + const promise = new DeferredPromise(); + this._llmRankedResultsPromises.set(query, promise); + const result = await raceCancellation(promise.p, token); + return result ?? null; + } + + handleSearchResult(result: AiSettingsSearchResult): void { + if (!this.isEnabled()) { + return; + } + + if (result.kind === AiSettingsSearchResultKind.EMBEDDED) { + const promise = this._embeddingsResultsPromises.get(result.query); + if (promise) { + promise.complete(result.settings); + this._embeddingsResultsPromises.delete(result.query); + } + } else if (result.kind === AiSettingsSearchResultKind.LLM_RANKED) { + const promise = this._llmRankedResultsPromises.get(result.query); + if (promise) { + promise.complete(result.settings); + this._llmRankedResultsPromises.delete(result.query); + } + } + } +} + +registerSingleton(IAiSettingsSearchService, AiSettingsSearchService, InstantiationType.Delayed); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index fc9c3ef222a..93e4c4fc944 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -69,6 +69,7 @@ import './services/editor/browser/editorService.js'; import './services/editor/browser/editorResolverService.js'; import './services/aiEmbeddingVector/common/aiEmbeddingVectorService.js'; import './services/aiRelatedInformation/common/aiRelatedInformationService.js'; +import './services/aiSettingsSearch/common/aiSettingsSearchService.js'; import './services/history/browser/historyService.js'; import './services/activity/browser/activityService.js'; import './services/keybinding/browser/keybindingService.js'; diff --git a/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts b/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts new file mode 100644 index 00000000000..373c5a42ae0 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * 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 enum SettingsSearchResultKind { + EMBEDDED = 1, + LLM_RANKED = 2, + CANCELED = 3 + } + + export interface SettingsSearchResult { + query: string; + kind: SettingsSearchResultKind; + settings: string[]; + } + + export interface SettingsSearchProviderOptions { + limit: number; + } + + export interface SettingsSearchProvider { + provideSettingsSearchResults(query: string, option: SettingsSearchProviderOptions, progress: Progress, token: CancellationToken): Thenable; + } + + export namespace ai { + export function registerSettingsSearchProvider(provider: SettingsSearchProvider): Disposable; + } +} From 30d16b20a155ed81944c99fb028ff8f615c71f4f Mon Sep 17 00:00:00 2001 From: Tuan Tran Van <4468525+huntertran@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:03:48 -0400 Subject: [PATCH 083/114] Replace single line break with double line break for commit description in git blame hover popup (#245779) * Replace single line break with double line break for commit description in git blame hover popup Fix issue #245773 * Remove unnecessary pattern `\n\n` when handling commit message * Remove non-matching group, and fix the same issue for the gragh hover as well --------- Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> --- extensions/git/src/blame.ts | 4 +++- src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 610513898d1..eb65a8ea0ab 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -253,6 +253,7 @@ export class GitBlameController { const authorDate = commitInformation?.authorDate ?? blameInformation.authorDate; const avatar = commitAvatar ? `![${authorName}](${commitAvatar}|width=${AVATAR_SIZE},height=${AVATAR_SIZE})` : '$(account)'; + if (authorName) { if (authorEmail) { const emailTitle = l10n.t('Email'); @@ -272,7 +273,8 @@ export class GitBlameController { } // Subject | Message - markdownString.appendMarkdown(`${emojify(commitMessageWithLinks ?? commitInformation?.message ?? blameInformation.subject ?? '')}\n\n`); + const message = commitMessageWithLinks ?? commitInformation?.message ?? blameInformation.subject ?? ''; + markdownString.appendMarkdown(`${emojify(message.replace(/\r\n|\r|\n/g, '\n\n'))}\n\n`); markdownString.appendMarkdown(`---\n\n`); // Short stats diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index fdde018b017..75ca8982fcd 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -497,7 +497,7 @@ class HistoryItemRenderer implements ITreeRenderer Date: Wed, 23 Apr 2025 14:22:49 -0700 Subject: [PATCH 084/114] mcp: adopt api feedback on extension API (#247245) --- src/vs/workbench/api/common/extHost.api.impl.ts | 7 +++++-- src/vs/workbench/api/common/extHostMcp.ts | 16 ++++++++++------ ...vscode.proposed.mcpConfigurationProvider.d.ts | 14 ++++++++------ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 522a47cfc8f..4376ba3408b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1447,7 +1447,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } }; - // namespace: chat + // namespace: chatregisterMcpServerDefinitionProvider const chat: typeof vscode.chat = { registerMappedEditsProvider(_selector: vscode.DocumentSelector, _provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); @@ -1527,12 +1527,15 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerIgnoredFileProvider(provider: vscode.LanguageModelIgnoredFileProvider) { return extHostLanguageModels.registerIgnoredFileProvider(extension, provider); }, - registerMcpConfigurationProvider(id, provider) { + registerMcpServerDefinitionProvider(id, provider) { checkProposedApiEnabled(extension, 'mcpConfigurationProvider'); return extHostMcp.registerMcpConfigurationProvider(extension, id, provider); } }; + // todo@connor4312: proposed API back-compat + (lm as any).registerMcpConfigurationProvider = lm.registerMcpServerDefinitionProvider; + // namespace: speech const speech: typeof vscode.speech = { registerSpeechProvider(id: string, provider: vscode.SpeechProvider) { diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index 39683cb2034..0a4c7429972 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -20,7 +20,7 @@ import * as Convert from './extHostTypeConverters.js'; export const IExtHostMpcService = createDecorator('IExtHostMpcService'); export interface IExtHostMpcService extends ExtHostMcpShape { - registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpConfigurationProvider): IDisposable; + registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpServerDefinitionProvider): IDisposable; } export class ExtHostMcpService extends Disposable implements IExtHostMpcService { @@ -28,7 +28,7 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService private readonly _initialProviderPromises = new Set>(); private readonly _sseEventSources = this._register(new DisposableMap()); private readonly _unresolvedMcpServers = new Map(); @@ -85,8 +85,8 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService return resolved ? Convert.McpServerDefinition.from(resolved) : undefined; } - /** {@link vscode.lm.registerMcpConfigurationProvider} */ - public registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpConfigurationProvider): IDisposable { + /** {@link vscode.lm.registerMcpServerDefinitionProvider} */ + public registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpServerDefinitionProvider): IDisposable { const store = new DisposableStore(); const metadata = extension.contributes?.modelContextServerCollections?.find(m => m.id === id); @@ -125,8 +125,12 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService this._proxy.$deleteMcpCollection(mcp.id); })); - if (provider.onDidChange) { - store.add(provider.onDidChange(update)); + if (provider.onDidChangeServerDefinitions) { + store.add(provider.onDidChangeServerDefinitions(update)); + } + // todo@connor4312: proposed API back-compat + if ((provider as any).onDidChange) { + store.add((provider as any).onDidChange(update)); } const promise = new Promise(resolve => { diff --git a/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts b/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts index 7e7e50e4d25..9f36dd5aca1 100644 --- a/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts @@ -8,13 +8,15 @@ declare module 'vscode' { /** * McpStdioServerDefinition represents an MCP server available by running - * a local process and listening to its stdin and stdout streams. + * a local process and listening to its stdin and stdout streams. The process + * will be spawned as a child process of the extension host and by default + * will not run in a shell environment. */ export class McpStdioServerDefinition { /** * The human-readable name of the server. */ - label: string; + readonly label: string; /** * The working directory used to start the server. @@ -62,7 +64,7 @@ declare module 'vscode' { /** * The human-readable name of the server. */ - label: string; + readonly label: string; /** * The URI of the server. The editor will make a POST request to this URI @@ -99,11 +101,11 @@ declare module 'vscode' { * To allow the editor to cache available servers, extensions should register * this before `activate()` resolves. */ - export interface McpConfigurationProvider { + export interface McpServerDefinitionProvider { /** * Optional event fired to signal that the set of available servers has changed. */ - onDidChange?: Event; + onDidChangeServerDefinitions?: Event; /** * Provides available MCP servers. The editor will call this method eagerly @@ -132,6 +134,6 @@ declare module 'vscode' { } namespace lm { - export function registerMcpConfigurationProvider(id: string, provider: McpConfigurationProvider): Disposable; + export function registerMcpServerDefinitionProvider(id: string, provider: McpServerDefinitionProvider): Disposable; } } From e9fdb47639f20d06460d1fb19b87fe9a619d984f Mon Sep 17 00:00:00 2001 From: Aaron Munger <2019016+amunger@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:48:02 -0700 Subject: [PATCH 085/114] allow skipping kernel selection if already selected (#247239) * allow skipping kernel selection if already selected * translate new param for API command * comment --- .../api/common/extHostNotebookKernels.ts | 3 +++ .../notebookKernelQuickPickStrategy.ts | 2 +- .../browser/viewParts/notebookKernelView.ts | 21 ++++++++++++++----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index 9c923a42745..1994bc803b5 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -84,6 +84,9 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { if (notebookEditorId === undefined) { throw new Error(`Cannot invoke 'notebook.selectKernel' for unrecognized notebook editor ${v.notebookEditor.notebook.uri.toString()}`); } + if ('skipIfAlreadySelected' in v) { + return { notebookEditorId, skipIfAlreadySelected: v.skipIfAlreadySelected }; + } return { notebookEditorId }; } return v; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 3e08af275e0..dec98508976 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -72,7 +72,7 @@ export type KernelQuickPickContext = { id: string; extension: string } | { notebookEditorId: string } | { id: string; extension: string; notebookEditorId: string } | - { ui?: boolean; notebookEditor?: NotebookEditorWidget }; + { ui?: boolean; notebookEditor?: NotebookEditorWidget; skipIfAlreadySelected?: boolean }; export interface IKernelPickerStrategy { showQuickPick(editor: IActiveNotebookEditor, wantedKernelId?: string): Promise; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts index b4c6ca0c38e..582e967c67c 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts @@ -18,7 +18,7 @@ import { selectKernelIcon } from '../notebookIcons.js'; import { KernelPickerMRUStrategy, KernelQuickPickContext } from './notebookKernelQuickPickStrategy.js'; import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; import { NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT } from '../../common/notebookContextKeys.js'; -import { INotebookKernelHistoryService, INotebookKernelService } from '../../common/notebookKernelService.js'; +import { INotebookKernel, INotebookKernelHistoryService, INotebookKernelService } from '../../common/notebookKernelService.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; function getEditorFromContext(editorService: IEditorService, context?: KernelQuickPickContext): INotebookEditor | undefined { @@ -39,6 +39,19 @@ function getEditorFromContext(editorService: IEditorService, context?: KernelQui return editor; } +function shouldSkip( + selected: INotebookKernel | undefined, + controllerId: string | undefined, + extensionId: string | undefined, + context: KernelQuickPickContext | undefined): boolean { + + return !!(selected && ( + (context && 'skipIfAlreadySelected' in context && context.skipIfAlreadySelected) || + // target kernel is already selected + (controllerId && selected.id === controllerId && ExtensionIdentifier.equals(selected.extension, extensionId)) + )); +} + registerAction2(class extends Action2 { constructor() { super({ @@ -115,11 +128,9 @@ registerAction2(class extends Action2 { const notebook = editor.textModel; const notebookKernelService = accessor.get(INotebookKernelService); - const matchResult = notebookKernelService.getMatchingKernel(notebook); - const { selected } = matchResult; + const { selected } = notebookKernelService.getMatchingKernel(notebook); - if (selected && controllerId && selected.id === controllerId && ExtensionIdentifier.equals(selected.extension, extensionId)) { - // current kernel is wanted kernel -> done + if (shouldSkip(selected, controllerId, extensionId, context)) { return true; } From bc8c3aa1c78344e42efe7a95a1a0e51426dcb836 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 23 Apr 2025 23:49:54 +0200 Subject: [PATCH 086/114] some prompt labels are wrong (#247249) --- .../dialogs/askToSelectPrompt/promptFilePickers.ts | 5 +---- .../promptInstructions/promptInstructionsWidget.ts | 14 ++++++++++---- .../createPromptCommand/createPromptCommand.ts | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts index 5bc0426e887..8a715bf52de 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts @@ -320,10 +320,7 @@ export class PromptFilePickers { // if a "user" prompt, don't show its filesystem path in // the user interface, but do that for all the "local" ones const description = (storage === 'user') - ? localize( - 'user-prompt.capitalized', - 'User prompt', - ) + ? localize('user-data-dir.capitalized', 'User data folder') : this._labelService.getUriLabel(dirname(uri), { relative: true }); const tooltip = (storage === 'user') diff --git a/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts b/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts index 4ff3586abaa..e03aeb95a21 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts @@ -106,12 +106,18 @@ export class InstructionsAttachmentWidget extends Disposable { const fileBasename = basename(file); const fileDirname = dirname(file); const friendlyName = `${fileBasename} ${fileDirname}`; - const ariaLabel = localize('chat.instructionsAttachment', "Instructions attachment, {0}", friendlyName); + const isPrompt = this.languageService.guessLanguageIdByFilepathOrFirstLine(file) === 'prompt'; + const ariaLabel = isPrompt + ? localize('chat.promptAttachment', "Prompt file, {0}", friendlyName) + : localize('chat.instructionsAttachment', "Instructions attachment, {0}", friendlyName); + const typeLabel = isPrompt + ? localize('prompt', "Prompt") + : localize('instructions', "Instructions"); + const uriLabel = this.labelService.getUriLabel(file, { relative: true }); - const instructionsLabel = localize('instructions', "Instructions"); - let title = `${instructionsLabel} ${uriLabel}`; + let title = `${typeLabel} ${uriLabel}`; // if there are some errors/warning during the process of resolving // attachment references (including all the nested child references), @@ -144,7 +150,7 @@ export class InstructionsAttachmentWidget extends Disposable { this.domNode.ariaLabel = ariaLabel; this.domNode.tabIndex = 0; - const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, instructionsLabel)); + const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, typeLabel)); this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); // create the `remove` button diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts index af2707a4e01..6a26d4b3963 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts @@ -112,12 +112,12 @@ const command = async ( return; } - // show suggestion to enable synchronization of the user prompts to the user + // show suggestion to enable synchronization of the user prompts and instructions to the user notificationService.prompt( Severity.Info, localize( 'workbench.command.prompts.create.user.enable-sync-notification', - "User prompts are not currently synchronized. Do you want to enable synchronization of the user prompts?", + "User prompts and instructions are not currently synchronized. Do you want to enable synchronization of the user prompts and instructions?", ), [ { From ac447af639f035a9e153ed5115c2385c0f958bfd Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 23 Apr 2025 23:50:58 +0200 Subject: [PATCH 087/114] separate settings for prompt and instructions locations (#247252) --- src/vs/platform/prompts/common/config.ts | 25 ++++++---- src/vs/platform/prompts/common/constants.ts | 14 +++++- .../prompts/test/common/config.test.ts | 28 +++++------ .../contrib/chat/browser/chat.contribution.ts | 47 +++++++++++++++---- .../promptSyntax/service/promptsService.ts | 2 +- .../promptSyntax/utils/promptFilesLocator.ts | 6 +-- .../utils/promptFilesLocator.test.ts | 4 +- 7 files changed, 85 insertions(+), 41 deletions(-) diff --git a/src/vs/platform/prompts/common/config.ts b/src/vs/platform/prompts/common/config.ts index 01f4e265cae..bfef2fdcf59 100644 --- a/src/vs/platform/prompts/common/config.ts +++ b/src/vs/platform/prompts/common/config.ts @@ -5,11 +5,11 @@ import { ContextKeyExpr } from '../../contextkey/common/contextkey.js'; import type { IConfigurationService } from '../../configuration/common/configuration.js'; -import { CONFIG_KEY, DEFAULT_SOURCE_FOLDER, LOCATIONS_CONFIG_KEY } from './constants.js'; +import { CONFIG_KEY, PROMPT_DEFAULT_SOURCE_FOLDER, INSTRUCTIONS_LOCATIONS_CONFIG_KEY, PROMPT_LOCATIONS_CONFIG_KEY, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER } from './constants.js'; /** * Configuration helper for the `reusable prompts` feature. - * @see {@link CONFIG_KEY} and {@link LOCATIONS_CONFIG_KEY}. + * @see {@link CONFIG_KEY}, {@link PROMPT_LOCATIONS_CONFIG_KEY} and {@link INSTRUCTIONS_LOCATIONS_CONFIG_KEY}. * * ### Functions * @@ -29,7 +29,8 @@ import { CONFIG_KEY, DEFAULT_SOURCE_FOLDER, LOCATIONS_CONFIG_KEY } from './const */ export namespace PromptsConfig { export const KEY = CONFIG_KEY; - export const LOCATIONS_KEY = LOCATIONS_CONFIG_KEY; + export const PROMPT_LOCATIONS_KEY = PROMPT_LOCATIONS_CONFIG_KEY; + export const INSTRUCTIONS_LOCATION_KEY = INSTRUCTIONS_LOCATIONS_CONFIG_KEY; /** * Checks if the feature is enabled. @@ -50,12 +51,14 @@ export namespace PromptsConfig { /** * Get value of the `reusable prompt locations` configuration setting. - * @see {@link LOCATIONS_CONFIG_KEY}. + * @see {@link PROMPT_LOCATIONS_CONFIG_KEY} or {@link INSTRUCTIONS_LOCATIONS_CONFIG_KEY}. */ export const getLocationsValue = ( configService: IConfigurationService, + type: 'instructions' | 'prompt' ): Record | undefined => { - const configValue = configService.getValue(LOCATIONS_CONFIG_KEY); + const key = type === 'instructions' ? INSTRUCTIONS_LOCATIONS_CONFIG_KEY : PROMPT_LOCATIONS_CONFIG_KEY; + const configValue = configService.getValue(key); if (configValue === undefined || configValue === null || Array.isArray(configValue)) { return undefined; @@ -85,26 +88,28 @@ export namespace PromptsConfig { /** * Gets list of source folders for prompt files. - * Defaults to {@link DEFAULT_SOURCE_FOLDER}. + * Defaults to {@link PROMPT_DEFAULT_SOURCE_FOLDER} or {@link INSTRUCTIONS_DEFAULT_SOURCE_FOLDER}. */ export const promptSourceFolders = ( configService: IConfigurationService, + type: 'instructions' | 'prompt' ): string[] => { - const value = getLocationsValue(configService); + const value = getLocationsValue(configService, type); + const defaultSourceFolder = type === 'instructions' ? INSTRUCTIONS_DEFAULT_SOURCE_FOLDER : PROMPT_DEFAULT_SOURCE_FOLDER; // note! the `value &&` part handles the `undefined`, `null`, and `false` cases if (value && (typeof value === 'object')) { const paths: string[] = []; // if the default source folder is not explicitly disabled, add it - if (value[DEFAULT_SOURCE_FOLDER] !== false) { - paths.push(DEFAULT_SOURCE_FOLDER); + if (value[defaultSourceFolder] !== false) { + paths.push(defaultSourceFolder); } // copy all the enabled paths to the result list for (const [path, enabled] of Object.entries(value)) { // we already added the default source folder, so skip it - if ((enabled === false) || (path === DEFAULT_SOURCE_FOLDER)) { + if ((enabled === false) || (path === defaultSourceFolder)) { continue; } diff --git a/src/vs/platform/prompts/common/constants.ts b/src/vs/platform/prompts/common/constants.ts index 594f6d538b3..b4102539986 100644 --- a/src/vs/platform/prompts/common/constants.ts +++ b/src/vs/platform/prompts/common/constants.ts @@ -30,12 +30,22 @@ export const CONFIG_KEY: string = 'chat.promptFiles'; /** * Configuration key for the locations of reusable prompt files. */ -export const LOCATIONS_CONFIG_KEY: string = 'chat.promptFilesLocations'; +export const PROMPT_LOCATIONS_CONFIG_KEY: string = 'chat.promptFilesLocations'; + +/** + * Configuration key for the locations of instructions files. + */ +export const INSTRUCTIONS_LOCATIONS_CONFIG_KEY: string = 'chat.instructionsFilesLocations'; /** * Default reusable prompt files source folder. */ -export const DEFAULT_SOURCE_FOLDER = '.github/prompts'; +export const PROMPT_DEFAULT_SOURCE_FOLDER = '.github/prompts'; + +/** + * Default reusable prompt files source folder. + */ +export const INSTRUCTIONS_DEFAULT_SOURCE_FOLDER = '.github/instructions'; /** * Gets the prompt file type from the provided path. diff --git a/src/vs/platform/prompts/test/common/config.test.ts b/src/vs/platform/prompts/test/common/config.test.ts index 560a676977a..5c6e6fd5b0a 100644 --- a/src/vs/platform/prompts/test/common/config.test.ts +++ b/src/vs/platform/prompts/test/common/config.test.ts @@ -22,7 +22,7 @@ const createMock = (value: T): IConfigurationService => { ); assert( - [PromptsConfig.KEY, PromptsConfig.LOCATIONS_KEY].includes(key), + [PromptsConfig.KEY, PromptsConfig.PROMPT_LOCATIONS_KEY, PromptsConfig.INSTRUCTIONS_LOCATION_KEY].includes(key), `Unsupported configuration key '${key}'.`, ); @@ -39,7 +39,7 @@ suite('PromptsConfig', () => { const configService = createMock(undefined); assert.strictEqual( - PromptsConfig.getLocationsValue(configService), + PromptsConfig.getLocationsValue(configService, 'prompt'), undefined, 'Must read correct value.', ); @@ -49,7 +49,7 @@ suite('PromptsConfig', () => { const configService = createMock(null); assert.strictEqual( - PromptsConfig.getLocationsValue(configService), + PromptsConfig.getLocationsValue(configService, 'prompt'), undefined, 'Must read correct value.', ); @@ -58,7 +58,7 @@ suite('PromptsConfig', () => { suite('• object', () => { test('• empty', () => { assert.deepStrictEqual( - PromptsConfig.getLocationsValue(createMock({})), + PromptsConfig.getLocationsValue(createMock({}), 'prompt'), {}, 'Must read correct value.', ); @@ -79,7 +79,7 @@ suite('PromptsConfig', () => { 'some/folder.with.dots/another.file': true, '/var/logs/app.01.05.error': true, './.tempfile': true, - })), + }), 'prompt'), { '/root/.bashrc': true, '../../folder/.hidden-folder/config.xml': true, @@ -123,7 +123,7 @@ suite('PromptsConfig', () => { '\f\f': true, '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - })), + }), 'prompt'), { '../assets/img/logo.v2.png': true, '/mnt/storage/video.archive/episode.01.mkv': false, @@ -150,7 +150,7 @@ suite('PromptsConfig', () => { '/var/data/datafile.2025-02-05.json': '\n', '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - })), + }), 'prompt'), { '/mnt/storage/video.archive/episode.01.mkv': false, }, @@ -165,7 +165,7 @@ suite('PromptsConfig', () => { const configService = createMock(undefined); assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(configService), + PromptsConfig.promptSourceFolders(configService, 'prompt'), [], 'Must read correct value.', ); @@ -175,7 +175,7 @@ suite('PromptsConfig', () => { const configService = createMock(null); assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(configService), + PromptsConfig.promptSourceFolders(configService, 'prompt'), [], 'Must read correct value.', ); @@ -184,7 +184,7 @@ suite('PromptsConfig', () => { suite('object', () => { test('empty', () => { assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock({})), + PromptsConfig.promptSourceFolders(createMock({}), 'prompt'), ['.github/prompts'], 'Must read correct value.', ); @@ -206,7 +206,7 @@ suite('PromptsConfig', () => { '/var/logs/app.01.05.error': true, '.GitHub/prompts': true, './.tempfile': true, - })), + }), 'prompt'), [ '.github/prompts', '/root/.bashrc', @@ -254,7 +254,7 @@ suite('PromptsConfig', () => { '\f\f': true, '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - })), + }), 'prompt'), [ '.github/prompts', '../assets/img/logo.v2.png', @@ -282,7 +282,7 @@ suite('PromptsConfig', () => { '/var/data/datafile.2025-02-05.json': '\n', '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - })), + }), 'prompt'), [ '.github/prompts', ], @@ -317,7 +317,7 @@ suite('PromptsConfig', () => { '\f\f': true, '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - })), + }), 'prompt'), [ '../assets/img/logo.v2.png', '../.local/bin/script.sh', diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index b79f4b81c5f..e49ce2746e3 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -20,7 +20,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { PromptsConfig } from '../../../../platform/prompts/common/config.js'; -import { DEFAULT_SOURCE_FOLDER as PROMPT_FILES_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../platform/prompts/common/constants.js'; +import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../platform/prompts/common/constants.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js'; @@ -48,7 +48,7 @@ import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } f import { ILanguageModelsService, LanguageModelsService } from '../common/languageModels.js'; import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; -import { PROMPT_DOCUMENTATION_URL } from '../common/promptSyntax/constants.js'; +import { INSTRUCTIONS_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL } from '../common/promptSyntax/constants.js'; import { registerReusablePromptLanguageFeatures } from '../common/promptSyntax/languageFeatures/providers/index.js'; import { PromptsService } from '../common/promptSyntax/service/promptsService.js'; import { IPromptsService } from '../common/promptSyntax/service/types.js'; @@ -282,8 +282,9 @@ configurationRegistry.registerConfiguration({ ), markdownDescription: nls.localize( 'chat.reusablePrompts.config.enabled.description', - "Enable reusable prompt files (`*{0}`) in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).", + "Enable reusable prompt (`*{0}`) and instruction files in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).", PROMPT_FILE_EXTENSION, + INSTRUCTION_FILE_EXTENSION, PROMPT_DOCUMENTATION_URL, ), default: true, @@ -293,12 +294,40 @@ configurationRegistry.registerConfiguration({ policy: { name: 'ChatPromptFiles', minimumVersion: '1.99', - description: nls.localize('chat.promptFiles.policy', "Enables reusable prompt files in Chat, Edits, and Inline Chat sessions."), + description: nls.localize('chat.promptFiles.policy', "Enables reusable prompt and instruction files in Chat, Edits, and Inline Chat sessions."), previewFeature: true, defaultValue: false } }, - [PromptsConfig.LOCATIONS_KEY]: { + [PromptsConfig.INSTRUCTIONS_LOCATION_KEY]: { + type: 'object', + title: nls.localize( + 'chat.instructions.config.locations.title', + "Instructions File Locations", + ), + markdownDescription: nls.localize( + 'chat.instructions.config.locations.description', + "Specify location(s) of instructions files (`*{0}`) that can be attached in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nRelative paths are resolved from the root folder(s) of your workspace.", + INSTRUCTION_FILE_EXTENSION, + INSTRUCTIONS_DOCUMENTATION_URL, + ), + default: { + [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true, + }, + additionalProperties: { type: 'boolean' }, + restricted: true, + tags: ['experimental', 'prompts', 'reusable prompts', 'prompt snippets', 'instructions'], + examples: [ + { + [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true, + }, + { + [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true, + '/Users/vscode/repos/instructions': true, + }, + ], + }, + [PromptsConfig.PROMPT_LOCATIONS_KEY]: { type: 'object', title: nls.localize( 'chat.reusablePrompts.config.locations.title', @@ -306,12 +335,12 @@ configurationRegistry.registerConfiguration({ ), markdownDescription: nls.localize( 'chat.reusablePrompts.config.locations.description', - "Specify location(s) of reusable prompt files (`*{0}`) that can be attached in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nRelative paths are resolved from the root folder(s) of your workspace.", + "Specify location(s) of reusable prompt files (`*{0}`) that can be run in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nRelative paths are resolved from the root folder(s) of your workspace.", PROMPT_FILE_EXTENSION, PROMPT_DOCUMENTATION_URL, ), default: { - [PROMPT_FILES_DEFAULT_SOURCE_FOLDER]: true, + [PROMPT_DEFAULT_SOURCE_FOLDER]: true, }, additionalProperties: { type: 'boolean' }, unevaluatedProperties: { type: 'boolean' }, @@ -319,10 +348,10 @@ configurationRegistry.registerConfiguration({ tags: ['experimental', 'prompts', 'reusable prompts', 'prompt snippets', 'instructions'], examples: [ { - [PROMPT_FILES_DEFAULT_SOURCE_FOLDER]: true, + [PROMPT_DEFAULT_SOURCE_FOLDER]: true, }, { - [PROMPT_FILES_DEFAULT_SOURCE_FOLDER]: true, + [PROMPT_DEFAULT_SOURCE_FOLDER]: true, '/Users/vscode/repos/prompts': true, }, ], diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index 3d562c76c2d..f8289002870 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -111,7 +111,7 @@ export class PromptsService extends Disposable implements IPromptsService { const result: IPromptPath[] = []; - for (const uri of this.fileLocator.getConfigBasedSourceFolders()) { + for (const uri of this.fileLocator.getConfigBasedSourceFolders(type)) { result.push({ uri, storage: 'local', type }); } const userHome = this.userDataService.currentProfile.promptsHome; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts index a9a168abd9a..179312fb361 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts @@ -32,7 +32,7 @@ export class PromptFilesLocator { * @returns List of prompt files found in the workspace. */ public async listFiles(type: TPromptsType): Promise { - const configuredLocations = PromptsConfig.promptSourceFolders(this.configService); + const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, type); const absoluteLocations = toAbsoluteLocations(configuredLocations, this.workspaceService); return await this.listFilesIn(absoluteLocations, type); @@ -65,8 +65,8 @@ export class PromptFilesLocator { * * @returns List of possible unambiguous prompt file folders. */ - public getConfigBasedSourceFolders(): readonly URI[] { - const configuredLocations = PromptsConfig.promptSourceFolders(this.configService); + public getConfigBasedSourceFolders(type: TPromptsType): readonly URI[] { + const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, type); const absoluteLocations = toAbsoluteLocations(configuredLocations, this.workspaceService); // locations in the settings can contain glob patterns so we need diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index 5a0920bd545..670cf234a7b 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -33,7 +33,7 @@ const mockConfigService = (value: T): IConfigurationService => { ); assert( - [PromptsConfig.KEY, PromptsConfig.LOCATIONS_KEY].includes(key), + [PromptsConfig.KEY, PromptsConfig.PROMPT_LOCATIONS_KEY].includes(key), `Unsupported configuration key '${key}'.`, ); @@ -2390,7 +2390,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - locator.getConfigBasedSourceFolders() + locator.getConfigBasedSourceFolders('prompt') .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/.github/prompts').fsPath, From 845c82152d2598911899e6110ee9558c921d8e1a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 23 Apr 2025 14:59:47 -0700 Subject: [PATCH 088/114] mcp: fix confusing default configuration value Fixes #244637 --- .../api/common/configurationExtensionPoint.ts | 4 +- src/vs/workbench/api/node/extHostMcpNode.ts | 1 + .../contrib/mcp/browser/mcpCommands.ts | 4 +- .../workbench/contrib/mcp/common/mcpServer.ts | 41 ++++++++++++++++++- .../workbench/contrib/mcp/common/mcpTypes.ts | 7 ++++ 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 798f28ae3d4..64386dde6b0 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -378,8 +378,8 @@ jsonRegistry.registerSchema('vscode://schemas/workspaceConfig', { inputs: [], servers: { 'mcp-server-time': { - command: 'python', - args: ['-m', 'mcp_server_time', '--local-timezone=America/Los_Angeles'] + command: 'uvx', + args: ['mcp_server_time', '--local-timezone=America/Los_Angeles'] } } }, diff --git a/src/vs/workbench/api/node/extHostMcpNode.ts b/src/vs/workbench/api/node/extHostMcpNode.ts index 1311da368b1..16e848a8d57 100644 --- a/src/vs/workbench/api/node/extHostMcpNode.ts +++ b/src/vs/workbench/api/node/extHostMcpNode.ts @@ -58,6 +58,7 @@ export class NodeExtHostMpcService extends ExtHostMcpService { private async startNodeMpc(id: number, launch: McpServerTransportStdio) { const onError = (err: Error | string) => this._proxy.$onDidChangeState(id, { state: McpConnectionState.Kind.Error, + code: err.hasOwnProperty('code') ? String((err as any).code) : undefined, message: typeof err === 'string' ? err : err.message, }); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 519b08137ef..858b6824c35 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -497,7 +497,7 @@ export class RestartServer extends Action2 { const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId); s?.showOutput(); await s?.stop(); - await s?.start(); + await s?.start(true); } } @@ -515,7 +515,7 @@ export class StartServer extends Action2 { async run(accessor: ServicesAccessor, serverId: string) { const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId); - await s?.start(); + await s?.start(true); } } diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 01f828d7211..f975b002592 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -16,7 +16,8 @@ import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; -import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; @@ -27,7 +28,7 @@ import { ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { mcpActivationEvent } from './mcpConfiguration.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; -import { extensionMcpCollectionPrefix, IMcpServer, IMcpServerConnection, IMcpTool, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, McpServerDefinition, McpServerToolsState } from './mcpTypes.js'; +import { extensionMcpCollectionPrefix, IMcpServer, IMcpServerConnection, IMcpTool, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, McpServerDefinition, McpServerToolsState, McpServerTransportType } from './mcpTypes.js'; import { MCP } from './modelContextProtocol.js'; type ServerBootData = { @@ -219,6 +220,8 @@ export class McpServer extends Disposable implements IMcpServer { @ITelemetryService private readonly _telemetryService: ITelemetryService, @ICommandService private readonly _commandService: ICommandService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @INotificationService private readonly _notificationService: INotificationService, + @IOpenerService private readonly _openerService: IOpenerService, ) { super(); @@ -322,10 +325,44 @@ export class McpServer extends Disposable implements IMcpServer { time: Date.now() - start, }); + if (state.state === McpConnectionState.Kind.Error && isFromInteraction) { + this.showInteractiveError(connection, state); + } + return state; }); } + private showInteractiveError(cnx: IMcpServerConnection, error: McpConnectionState.Error) { + if (error.code === 'ENOENT' && cnx.launchDefinition.type === McpServerTransportType.Stdio) { + let docsLink: string | undefined; + switch (cnx.launchDefinition.command) { + case 'uvx': + docsLink = `https://aka.ms/vscode-mcp-install/uvx`; + break; + case 'npx': + docsLink = `https://aka.ms/vscode-mcp-install/npx`; + break; + } + + const options: IPromptChoice[] = [{ + label: localize('mcp.command.showOutput', "Show Output"), + run: () => this.showOutput(), + }]; + + if (docsLink) { + options.push({ + label: localize('mcpServerInstall', 'Install {0}', cnx.launchDefinition.command), + run: () => this._openerService.open(URI.parse(docsLink)), + }); + } + + this._notificationService.prompt(Severity.Error, localize('mcpServerNotFound', 'The command "{0}" needed to run {1} was not found.', cnx.launchDefinition.command, cnx.definition.label), options); + } else { + this._notificationService.warn(localize('mcpServerError', 'The MCP server {0} could not be started: {1}', cnx.definition.label, error.message)); + } + } + public stop(): Promise { return this._connection.get()?.stop() || Promise.resolve(); } diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index d87721225b8..3ef40c12bf1 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -338,6 +338,12 @@ export interface IMcpServerConnection extends IDisposable { readonly state: IObservable; readonly handler: IObservable; + /** + * Resolved launch definition. Might not match the `definition.launch` due to + * resolution logic in extension-provided MCPs. + */ + readonly launchDefinition: McpServerLaunch; + /** * Starts the server if it's stopped. Returns a promise that resolves once * server exits a 'starting' state. @@ -412,6 +418,7 @@ export namespace McpConnectionState { export interface Error { readonly state: Kind.Error; + readonly code?: string; readonly message: string; } } From 52960810b18db0bfc21361ea643a94b573e7f994 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 23 Apr 2025 15:22:40 -0700 Subject: [PATCH 089/114] Implement switch model command + fix model picker command (#247256) * Add command for switching chat model * Fix open model picker command --- .../browser/actions/chatExecuteActions.ts | 39 +++++++++++++++++-- .../contrib/chat/browser/chatInputPart.ts | 10 ++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 459203230b4..01ddfe2bdc0 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -6,6 +6,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { assertType } from '../../../../../base/common/types.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; @@ -14,13 +15,15 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { chatVariableLeader } from '../../common/chatParserTypes.js'; import { IChatService } from '../../common/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../../common/constants.js'; +import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; -import { IChatWidget, IChatWidgetService } from '../chat.js'; +import { IChatWidget, IChatWidgetService, showChatView } from '../chat.js'; import { getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; import { CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js'; import { ACTION_ID_NEW_CHAT, waitForChatSessionCleared } from './chatClearActions.js'; @@ -287,15 +290,44 @@ class OpenModelPickerAction extends Action2 { }); } - override run(accessor: ServicesAccessor, ...args: any[]): void { + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const widgetService = accessor.get(IChatWidgetService); - const widget = widgetService.lastFocusedWidget; + let widget = widgetService.lastFocusedWidget; + if (widget?.location !== ChatAgentLocation.Notebook && widget?.location !== ChatAgentLocation.Terminal) { + widget = await showChatView(accessor.get(IViewsService)); + } if (widget) { widget.input.openModelPicker(); } } } +export const ChangeChatModelActionId = 'workbench.action.chat.changeModel'; +class ChangeChatModelAction extends Action2 { + static readonly ID = ChangeChatModelActionId; + + constructor() { + super({ + id: ChangeChatModelAction.ID, + title: localize2('interactive.changeModel.label', "Change Model"), + category: CHAT_CATEGORY, + f1: false, + precondition: ChatContextKeys.enabled, + }); + } + + override run(accessor: ServicesAccessor, ...args: any[]): void { + const modelInfo: Pick = args[0]; + // Type check the arg + assertType(typeof modelInfo.vendor === 'string' && typeof modelInfo.id === 'string' && typeof modelInfo.family === 'string'); + const widgetService = accessor.get(IChatWidgetService); + const widgets = widgetService.getAllWidgets(); + for (const widget of widgets) { + widget.input.switchModel(modelInfo); + } + } +} + export class ChatEditingSessionSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.edits.submit'; @@ -534,4 +566,5 @@ export function registerChatExecuteActions() { registerAction2(ToggleRequestPausedAction); registerAction2(SwitchToNextModelAction); registerAction2(OpenModelPickerAction); + registerAction2(ChangeChatModelAction); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index f93ad344435..1f2e726b551 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -79,7 +79,7 @@ import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../common/constants.js'; -import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; +import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; import { CancelAction, ChatEditingSessionSubmitAction, ChatOpenModelPickerActionId, ChatSubmitAction, IChatExecuteActionContext, IToggleChatModeArgs, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; import { AttachToolsAction } from './actions/chatToolActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; @@ -487,6 +487,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); } + public switchModel(modelMetadata: Pick) { + const models = this.getModels(); + const model = models.find(m => m.metadata.vendor === modelMetadata.vendor && m.metadata.id === modelMetadata.id && m.metadata.family === modelMetadata.family); + if (model) { + this.setCurrentLanguageModel(model); + } + } + public switchToNextModel(): void { const models = this.getModels(); if (models.length > 0) { From 0c03a8ed19330efdb66da6b27cf253d2f817b970 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 23 Apr 2025 15:36:47 -0700 Subject: [PATCH 090/114] mcp: gracefully handle streamable 4xx errors Fixes #246685 --- src/vs/workbench/api/common/extHostMcp.ts | 11 ++++++++++- .../workbench/contrib/mcp/common/mcpServer.ts | 18 ++++++++++++++---- .../workbench/contrib/mcp/common/mcpTypes.ts | 1 + 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index 0a4c7429972..4c54d7dfe1f 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -244,7 +244,16 @@ class McpHTTPHandle extends Disposable { } if (res.status >= 300) { - this._log(LogLevel.Warning, `${res.status} status sending message to ${this._launch.uri}: ${await this._getErrText(res)}`); + // "When a client receives HTTP 404 in response to a request containing an Mcp-Session-Id, it MUST start a new session by sending a new InitializeRequest without a session ID attached" + // Though this says only 404, some servers send 400s as well, including their example + // https://github.com/modelcontextprotocol/typescript-sdk/issues/389 + const retryWithSessionId = this._mode.value === HttpMode.Http && !!this._mode.sessionId; + + this._proxy.$onDidChangeState(this._id, { + state: McpConnectionState.Kind.Error, + message: `${res.status} status sending message to ${this._launch.uri}: ${await this._getErrText(res)}` + retryWithSessionId ? `; will retry with new session ID` : '', + shouldRetry: retryWithSessionId, + }); return; } diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index f975b002592..7e303106f04 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -481,7 +481,6 @@ export class McpServer extends Disposable implements IMcpServer { * connection started if it is not already. */ public async callOn(fn: (handler: McpServerRequestHandler) => Promise, token: CancellationToken = CancellationToken.None): Promise { - await this.start(); // idempotent let ranOnce = false; @@ -540,6 +539,10 @@ export class McpTool implements IMcpTool { } callWithProgress(params: Record, progress: ToolProgress, token?: CancellationToken): Promise { + return this._callWithProgress(params, progress, token); + } + + _callWithProgress(params: Record, progress: ToolProgress, token?: CancellationToken, allowRetry = true): Promise { // serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it. const name = this._definition.serverToolName ?? this._definition.name; const progressToken = generateUuid(); @@ -557,9 +560,16 @@ export class McpTool implements IMcpTool { } }); - return h.callTool({ name, arguments: params, _meta: { progressToken } }, token).finally(() => { - listener.dispose(); - }); + return h.callTool({ name, arguments: params, _meta: { progressToken } }, token) + .finally(() => listener.dispose()) + .catch(err => { + const state = this._server.connectionState.get(); + if (allowRetry && state.state === McpConnectionState.Kind.Error && state.shouldRetry) { + return this._callWithProgress(params, progress, token, false); + } else { + throw err; + } + }); }, token); } diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 3ef40c12bf1..f43dc1472ff 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -419,6 +419,7 @@ export namespace McpConnectionState { export interface Error { readonly state: Kind.Error; readonly code?: string; + readonly shouldRetry?: boolean; readonly message: string; } } From e975fc9be814a4a76d1a8537af8c9b273970a331 Mon Sep 17 00:00:00 2001 From: Samuel Rigaud <46346622+s-rigaud@users.noreply.github.com> Date: Wed, 23 Apr 2025 19:38:49 -0400 Subject: [PATCH 091/114] test: fix typos (#247259) Co-authored-by: Samuel Rigaud --- test/automation/src/playwrightDriver.ts | 2 +- test/unit/browser/index.js | 2 +- test/unit/electron/renderer.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 2b1d3a6ae40..a6fd72754f9 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -198,7 +198,7 @@ export class PlaywrightDriver { try { await measureAndLog(() => this.application.close(), 'playwright.close()', this.options.logger); } catch (error) { - this.options.logger.log(`Error closing appliction (${error})`); + this.options.logger.log(`Error closing application (${error})`); } // Server: via `teardown` diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 139fba95514..e5cf68ccab5 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -151,7 +151,7 @@ const testModules = (async function () { modules.push(file.replace(/\.js$/, '')); } else if (!isDefaultModules) { - console.warn(`DROPPONG ${file} because it cannot be run inside a browser`); + console.warn(`DROPPING ${file} because it cannot be run inside a browser`); } } return modules; diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index aa4f05984a2..661be873561 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -372,7 +372,7 @@ function safeStringify(obj) { function isObject(obj) { // The method can't do a type cast since there are type (like strings) which - // are subclasses of any put not positvely matched by the function. Hence type + // are subclasses of any put not positively matched by the function. Hence type // narrowing results in wrong results. return typeof obj === 'object' && obj !== null From b98832f58eee305b2685ac43945e7817fb8e0ab1 Mon Sep 17 00:00:00 2001 From: Samuel Rigaud Date: Wed, 23 Apr 2025 20:07:04 -0400 Subject: [PATCH 092/114] fix: vscode-dts typos --- src/vscode-dts/vscode.d.ts | 12 ++++++------ .../vscode.proposed.chatParticipantAdditions.d.ts | 2 +- ...ode.proposed.contribDebugCreateConfiguration.d.ts | 2 +- .../vscode.proposed.languageModelDataPart.d.ts | 2 +- src/vscode-dts/vscode.proposed.tunnelFactory.d.ts | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index d85284ac286..d7f9e6c103a 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -16490,7 +16490,7 @@ declare module 'vscode' { } /** - * Namespace for source control mangement. + * Namespace for source control management. */ export namespace scm { @@ -16804,7 +16804,7 @@ declare module 'vscode' { export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterNamedPipeServer | DebugAdapterInlineImplementation; /** - * A debug adaper factory that creates {@link DebugAdapterDescriptor debug adapter descriptors}. + * A debug adapter factory that creates {@link DebugAdapterDescriptor debug adapter descriptors}. */ export interface DebugAdapterDescriptorFactory { /** @@ -16858,7 +16858,7 @@ declare module 'vscode' { } /** - * A debug adaper factory that creates {@link DebugAdapterTracker debug adapter trackers}. + * A debug adapter factory that creates {@link DebugAdapterTracker debug adapter trackers}. */ export interface DebugAdapterTrackerFactory { /** @@ -19257,7 +19257,7 @@ declare module 'vscode' { readonly value: T; /** - * Creates a new telementry trusted value. + * Creates a new telemetry trusted value. * * @param value A value to trust */ @@ -19265,7 +19265,7 @@ declare module 'vscode' { } /** - * A telemetry logger which can be used by extensions to log usage and error telementry. + * A telemetry logger which can be used by extensions to log usage and error telemetry. * * A logger wraps around an {@link TelemetrySender sender} but it guarantees that * - user settings to disable or tweak telemetry are respected, and that @@ -20444,7 +20444,7 @@ declare module 'vscode' { */ export class LanguageModelToolResult { /** - * A list of tool result content parts. Includes `unknown` becauses this list may be extended with new content types in + * A list of tool result content parts. Includes `unknown` because this list may be extended with new content types in * the future. * @see {@link lm.invokeTool}. */ diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 6dce17bb29e..e43e2dca4d1 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -269,7 +269,7 @@ declare module 'vscode' { /** * Event that fires when a request is paused or unpaused. - * Chat requests are initialy unpaused in the {@link requestHandler}. + * Chat requests are initially unpaused in the {@link requestHandler}. */ onDidChangePauseState: Event; } diff --git a/src/vscode-dts/vscode.proposed.contribDebugCreateConfiguration.d.ts b/src/vscode-dts/vscode.proposed.contribDebugCreateConfiguration.d.ts index 9f866269987..c28b6b4cc42 100644 --- a/src/vscode-dts/vscode.proposed.contribDebugCreateConfiguration.d.ts +++ b/src/vscode-dts/vscode.proposed.contribDebugCreateConfiguration.d.ts @@ -3,4 +3,4 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// empty placeholder declaration for the `debugCreateConfiguation` menu +// empty placeholder declaration for the `debugCreateConfiguration` menu diff --git a/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts b/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts index a9e888ea892..29eb2f10e8c 100644 --- a/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts @@ -99,7 +99,7 @@ declare module 'vscode' { } /** - * Tagging onto this proposal, because otherwise managing two different extensions of LangaugeModelChatMessage could be confusing. + * Tagging onto this proposal, because otherwise managing two different extensions of LanguageModelChatMessage could be confusing. * A language model response part containing arbitrary model-specific data, returned from a {@link LanguageModelChatResponse}. * TODO@API naming, looking at LanguageModelChatRequestOptions.modelOptions, but LanguageModelModelData is not very good. * LanguageModelOpaqueData from prompt-tsx? diff --git a/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts b/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts index eab3ffa5d2e..aa5c4b7739c 100644 --- a/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts +++ b/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts @@ -31,7 +31,7 @@ declare module 'vscode' { export interface TunnelProvider { /** - * Provides port forwarding capabilities. If there is a resolver that already provids tunnels, then the resolver's provider will + * Provides port forwarding capabilities. If there is a resolver that already provides tunnels, then the resolver's provider will * be used. If multiple providers are registered, then only the first will be used. */ provideTunnel(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions, token: CancellationToken): ProviderResult; From f02abe34f9a82349d301ea7168cd926072ec49d0 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 23 Apr 2025 17:16:23 -0700 Subject: [PATCH 093/114] Fix edit text with backticks leaking into chat response (#247261) --- src/vs/workbench/contrib/chat/common/tools/editFileTool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index c2e8d61f62f..ac60d09b425 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -75,7 +75,7 @@ export class EditTool implements IToolImpl { }); model.acceptResponseProgress(request, { kind: 'markdownContent', - content: new MarkdownString(parameters.code + '\n````\n') + content: new MarkdownString('\n````\n') }); // Signal start. if (this.notebookService.hasSupportedNotebooks(uri) && (this.notebookService.getNotebookTextModel(uri))) { From ed40c856ecdb0444bce3f07febf00a5a2830a1cc Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 23 Apr 2025 18:28:44 -0700 Subject: [PATCH 094/114] Show Working message after "summarizing conversation..." (#247268) --- src/vs/workbench/contrib/chat/browser/chatListRenderer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 8dc6c9c7fae..b966a950be8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -839,7 +839,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer part.kind === 'toolInvocation' && !part.isComplete))) { + ((lastPart.kind === 'textEditGroup' || lastPart.kind === 'notebookEditGroup') && lastPart.done && !partsToRender.some(part => part.kind === 'toolInvocation' && !part.isComplete)) || + (lastPart.kind === 'progressTask' && lastPart.deferred.isSettled) + ) { return true; } From d9bc5b09ea14b6ff9d95445ce87de052d0ad9165 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 23 Apr 2025 18:42:00 -0700 Subject: [PATCH 095/114] Add 'isExclusive' flag for tool selection (#247267) * Add 'isExclusive' flag for tool selection For prompts with tools to exclude non-selected tools * Fix --- .../common/extensionsApiProposals.ts | 2 +- .../api/common/extHostChatAgents2.ts | 8 +++++-- .../api/common/extHostTypeConverters.ts | 4 ++-- .../contrib/chat/browser/chatWidget.ts | 2 +- .../contrib/chat/common/chatAgents.ts | 1 + .../contrib/chat/common/chatService.ts | 1 + .../contrib/chat/common/chatServiceImpl.ts | 1 + ...ode.proposed.chatParticipantAdditions.d.ts | 13 ---------- ...scode.proposed.chatParticipantPrivate.d.ts | 24 ++++++++++++++++++- 9 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index c825f45db15..35d88b02170 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -36,7 +36,7 @@ const _allApiProposals = { }, chatParticipantPrivate: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', - version: 8 + version: 9 }, chatProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 49c7a5feff3..00b6c7d69c9 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -560,12 +560,16 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return this._diagnostics.getDiagnostics(); } - private getToolsForRequest(extension: IExtensionDescription, request: Dto) { + private getToolsForRequest(extension: IExtensionDescription, request: Dto): vscode.ChatRequestToolSelection | undefined { if (!isNonEmptyArray(request.userSelectedTools)) { return undefined; } const selector = new Set(request.userSelectedTools); - return this._tools.getTools(extension).filter(candidate => selector.has(candidate.name)); + const tools = this._tools.getTools(extension).filter(candidate => selector.has(candidate.name)); + return { + tools, + isExclusive: request.toolSelectionIsExclusive, + }; } private async prepareHistoryTurns(extension: Readonly, agentId: string, context: { history: IChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 1bab9eeac00..a4c356ad7fb 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2898,7 +2898,7 @@ export namespace ChatResponsePart { } export namespace ChatAgentRequest { - export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], tools: vscode.LanguageModelToolInformation[] | undefined, extension: IRelaxedExtensionDescription): vscode.ChatRequest { + export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], toolSelection: vscode.ChatRequestToolSelection | undefined, extension: IRelaxedExtensionDescription): vscode.ChatRequest { const toolReferences = request.variables.variables.filter(v => v.kind === 'tool'); const variableReferences = request.variables.variables.filter(v => v.kind !== 'tool'); const requestWithAllProps: vscode.ChatRequest = { @@ -2915,7 +2915,7 @@ export namespace ChatAgentRequest { rejectedConfirmationData: request.rejectedConfirmationData, location2, toolInvocationToken: Object.freeze({ sessionId: request.sessionId }) as never, - tools, + toolSelection, model, editedFileEvents: request.editedFileEvents, }; diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index d6bae7b8045..c0edee9fa9e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1243,7 +1243,7 @@ export class ChatWidget extends Disposable implements IChatWidget { parserContext: { selectedAgent: this._lastSelectedAgent, mode: this.inputPart.currentMode }, attachedContext, noCommandDetection: options?.noCommandDetection, - userSelectedTools: this.input.currentMode === ChatMode.Agent ? this.inputPart.selectedToolsModel.tools.get().map(tool => tool.id) : undefined + userSelectedTools: this.input.currentMode === ChatMode.Agent ? this.inputPart.selectedToolsModel.tools.get().map(tool => tool.id) : undefined, }); if (result) { diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 9fe6e516395..7017c466ff8 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -138,6 +138,7 @@ export interface IChatAgentRequest { rejectedConfirmationData?: any[]; userSelectedModelId?: string; userSelectedTools?: string[]; + toolSelectionIsExclusive?: boolean; editedFileEvents?: IChatAgentEditedFileEvent[]; } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 9e9cead60f9..4a2512cf13e 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -466,6 +466,7 @@ export interface IChatSendRequestOptions { mode?: ChatMode; userSelectedModelId?: string; userSelectedTools?: string[]; + toolSelectionIsExclusive?: boolean; location?: ChatAgentLocation; locationData?: IChatLocationData; parserContext?: IChatParserContext; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index cd07a81b1f0..a55f2ca360e 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -768,6 +768,7 @@ export class ChatService extends Disposable implements IChatService { rejectedConfirmationData: options?.rejectedConfirmationData, userSelectedModelId: options?.userSelectedModelId, userSelectedTools: options?.userSelectedTools, + toolSelectionIsExclusive: options?.toolSelectionIsExclusive, editedFileEvents: request.editedFileEvents } satisfies IChatAgentRequest; }; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 6dce17bb29e..ffe4e988fea 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -227,19 +227,6 @@ declare module 'vscode' { } - export interface ChatRequest { - - /** - * A list of tools that the user selected for this request, when `undefined` any tool - * from {@link lm.tools} should be used. - * - * Tools can be called with {@link lm.invokeTool} with input that match their - * declared `inputSchema`. - */ - readonly tools: readonly LanguageModelToolInformation[] | undefined; - } - - /** * Does this piggy-back on the existing ChatRequest, or is it a different type of request entirely? * Does it show up in history? diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 43010b7300c..8b0d17fa0ca 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 8 +// version: 9 declare module 'vscode' { @@ -240,4 +240,26 @@ declare module 'vscode' { } // #endregion + + export interface ChatRequestToolSelection { + /** + * A list of tools that the user selected for this request. + * Tools can be called with {@link lm.invokeTool} with input that match their + * declared `inputSchema`. + */ + readonly tools: readonly LanguageModelToolInformation[]; + + /** + * When true, only this set of tools (and toolReferences) should be used. When false, the base set of agent tools can also be included. + */ + readonly isExclusive?: boolean; + } + + export interface ChatRequest { + /** + * A list of tools that the user selected for this request, when `undefined` any tool + * from {@link lm.tools} should be used. + */ + readonly toolSelection: ChatRequestToolSelection | undefined; + } } From 2d59f85dfc0b2c33925964640d830a206dcfa5b4 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Wed, 23 Apr 2025 20:46:18 -0700 Subject: [PATCH 096/114] cleanup new welcome experience setup (#247272) --- .../browser/gettingStarted.contribution.ts | 4 +- .../browser/gettingStarted.ts | 7 +-- .../browser/gettingStartedService.ts | 1 + .../common/gettingStartedContent.ts | 52 +++++++++++++++++++ 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index a1c7dac9944..14a43e1e92f 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -80,7 +80,7 @@ registerAction2(class extends Action2 { if (!selectedCategory && !selectedStep) { editorService.openEditor({ resource: GettingStartedInput.RESOURCE, - options: { preserveFocus: toSide ?? false, inactive } + options: { preserveFocus: toSide ?? false, inactive, forceReload: true } }, toSide ? SIDE_GROUP : undefined); return; } @@ -303,7 +303,7 @@ registerAction2(class extends Action2 { async run(accessor: ServicesAccessor) { const editorService = accessor.get(IEditorService); - const options: GettingStartedEditorOptions = { selectedCategory: 'Setup', showNewExperience: true }; + const options: GettingStartedEditorOptions = { selectedCategory: 'NewWelcomeExperience', showNewExperience: true, forceReload: true }; editorService.openEditor({ resource: GettingStartedInput.RESOURCE, diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 2cfbd675def..f36cdce1c36 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -60,7 +60,7 @@ import { IWebviewElement, IWebviewService } from '../../webview/browser/webview. import './gettingStartedColors.js'; import { GettingStartedDetailsRenderer } from './gettingStartedDetailsRenderer.js'; import { gettingStartedCheckedCodicon, gettingStartedUncheckedCodicon } from './gettingStartedIcons.js'; -import { GettingStartedInput } from './gettingStartedInput.js'; +import { GettingStartedEditorOptions, GettingStartedInput } from './gettingStartedInput.js'; import { IResolvedWalkthrough, IResolvedWalkthroughStep, IWalkthroughsService, hiddenEntriesConfigurationKey, parseDescription } from './gettingStartedService.js'; import { RestoreWalkthroughsConfigurationValue, restoreWalkthroughsConfigurationKey } from './startupPage.js'; import { startEntries } from '../common/gettingStartedContent.js'; @@ -343,6 +343,7 @@ export class GettingStartedPage extends EditorPane { override async setInput(newInput: GettingStartedInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken) { this.container.classList.remove('animatable'); this.editorInput = newInput; + this.editorInput.showNewExperience = (options as GettingStartedEditorOptions)?.showNewExperience ?? false; await super.setInput(newInput, options, context, token); await this.buildCategoriesSlide(); if (this.shouldAnimate()) { @@ -923,7 +924,7 @@ export class GettingStartedPage extends EditorPane { if (!this.currentWalkthrough) { this.gettingStartedCategories = this.gettingStartedService.getWalkthroughs(); - this.currentWalkthrough = this.gettingStartedCategories.find(category => category.id === this.editorInput.selectedCategory); + this.currentWalkthrough = this.editorInput.showNewExperience ? this.gettingStartedService.getWalkthrough(this.editorInput.selectedCategory) : this.gettingStartedCategories.find(category => category.id === this.editorInput.selectedCategory); if (this.currentWalkthrough) { if (this.editorInput.showNewExperience === true) { this.buildNewCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); @@ -1479,7 +1480,7 @@ export class GettingStartedPage extends EditorPane { this.detailsPageDisposables.clear(); this.mediaDisposables.clear(); - const category = this.gettingStartedCategories.find(category => category.id === categoryID); + const category = this.gettingStartedService.getWalkthrough(categoryID); if (!category) { throw Error('could not find category with ID ' + categoryID); } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 76113eb4c08..630a26f614c 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -500,6 +500,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ }; }) .filter(category => category.content.type !== 'steps' || category.content.steps.length) + .filter(category => category.id !== 'NewWelcomeExperience') .map(category => this.resolveWalkthrough(category)); return categoriesWithCompletion; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index c370ab068a9..44c048386fe 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -664,5 +664,57 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ }, ] } + }, + { + id: 'NewWelcomeExperience', + title: localize('gettingStarted.setup.title', "Get Started with VS Code"), + description: localize('gettingStarted.setup.description', "Customize your editor, learn the basics, and start coding"), + isFeatured: false, + icon: setupIcon, + when: '!isWeb', + walkthroughPageTitle: localize('gettingStarted.setup.walkthroughPageTitle', 'Setup VS Code'), + content: { + type: 'steps', + steps: [ + createCopilotSetupStep('CopilotSetupSignedOut', CopilotSignedOutButton, 'chatSetupSignedOut', true), + createCopilotSetupStep('CopilotSetupComplete', CopilotCompleteButton, 'chatSetupInstalled && (chatPlanPro || chatPlanLimited)', false), + createCopilotSetupStep('CopilotSetupSignedIn', CopilotSignedInButton, '!chatSetupSignedOut && (!chatSetupInstalled || chatPlanCanSignUp)', true), + { + id: 'pickColorTheme', + title: localize('gettingStarted.pickColor.title', "Choose your theme"), + description: localize('gettingStarted.pickColor.description.interpolated', "The right theme helps you focus on your code, is easy on your eyes, and is simply more fun to use.\n{0}", Button(localize('titleID', "Browse Color Themes"), 'command:workbench.action.selectTheme')), + completionEvents: [ + 'onSettingChanged:workbench.colorTheme', + 'onCommand:workbench.action.selectTheme' + ], + media: { type: 'markdown', path: 'theme_picker', } + }, + { + id: 'findLanguageExtensions', + title: localize('gettingStarted.findLanguageExts.title', "Rich support for all your languages"), + description: localize('gettingStarted.findLanguageExts.description.interpolated', "Code smarter with syntax highlighting, code completion, linting and debugging. While many languages are built-in, many more can be added as extensions.\n{0}", Button(localize('browseLangExts', "Browse Language Extensions"), 'command:workbench.extensions.action.showLanguageExtensions')), + when: 'workspacePlatform != \'webworker\'', + media: { + type: 'svg', altText: 'Language extensions', path: 'languages.svg' + }, + }, + { + id: 'settingsAndSync', + title: localize('gettingStarted.settings.title', "Tune your settings"), + description: localize('gettingStarted.settingsAndSync.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. [Back up and sync](command:workbench.userDataSync.actions.turnOn) your essential customizations across all your devices.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')), + when: 'syncStatus != uninitialized', + completionEvents: ['onEvent:sync-enabled'], + media: { + type: 'svg', altText: 'VS Code Settings', path: 'settings.svg' + }, + }, + { + id: 'commandPaletteTask', + title: localize('gettingStarted.commandPalette.title', "Unlock productivity with the Command Palette "), + description: localize('gettingStarted.commandPalette.description.interpolated', "Run commands without reaching for your mouse to accomplish any task in VS Code.\n{0}", Button(localize('commandPalette', "Open Command Palette"), 'command:workbench.action.showCommands')), + media: { type: 'svg', altText: 'Command Palette overlay for searching and executing commands.', path: 'commandPalette.svg' }, + } + ] + } } ]; From 3339a04f314e19d70391a4744337e135549be9e7 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 24 Apr 2025 14:33:48 +1000 Subject: [PATCH 097/114] Do not mark notebook as dirty upon execution (#247269) --- .../browser/chatEditing/chatEditingModifiedNotebookEntry.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index ac3ca516761..2df60310898 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -270,11 +270,6 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie return; } - // If user executes a cell, then mark this notebook as modified by the user. - if (e.rawEvents.some(e => e.kind === NotebookCellsChangeType.ChangeCellInternalMetadata && typeof e.internalMetadata.lastRunSuccess === 'boolean')) { - this._userEditScheduler.schedule(); - } - if (currentState === ModifiedFileEntryState.Rejected) { return; } From 55fa91dc2d9570fc0505f351d5a3ec449684d632 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 23 Apr 2025 23:01:58 -0700 Subject: [PATCH 098/114] Don't use .copilotmd files for implicit context (#247279) --- .../contrib/chat/browser/contrib/chatImplicitContext.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index da0807b736d..9b532a8c7b4 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -210,7 +210,10 @@ export class ChatImplicitContextContribution extends Disposable implements IWork } const uri = newValue instanceof URI ? newValue : newValue?.uri; - if (uri && await this.ignoredFilesService.fileIsIgnored(uri, cancelTokenSource.token)) { + if (uri && ( + await this.ignoredFilesService.fileIsIgnored(uri, cancelTokenSource.token) || + uri.path.endsWith('.copilotmd')) + ) { newValue = undefined; } From e59f82fc9fbe8b2098d08038c7963ecaa4003469 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 23 Apr 2025 23:02:59 -0700 Subject: [PATCH 099/114] Backspace/delete delete attachments (#247274) --- .../contrib/chat/browser/chatAttachmentWidgets.ts | 5 +++++ .../workbench/contrib/chat/browser/chatInputPart.ts | 11 +++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts index d55e4b9890b..1e2feeff347 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts @@ -85,6 +85,11 @@ abstract class AbstractChatAttachmentWidget extends Disposable { this._register(event.Event.once(clearButton.onDidClick)((e) => { this._onDidDelete.fire(e); })); + this._register(dom.addStandardDisposableListener(this.element, dom.EventType.KEY_DOWN, e => { + if (e.keyCode === KeyCode.Backspace || e.keyCode === KeyCode.Delete) { + this._onDidDelete.fire(e.browserEvent); + } + })); if (this.shouldFocusClearButton) { clearButton.focus(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 1f2e726b551..86362e1c08b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -7,7 +7,6 @@ import * as dom from '../../../../base/browser/dom.js'; import { addDisposableListener } from '../../../../base/browser/dom.js'; import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js'; import { IHistoryNavigationWidget } from '../../../../base/browser/history.js'; -import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import * as aria from '../../../../base/browser/ui/aria/aria.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; @@ -19,7 +18,6 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { HistoryNavigator2 } from '../../../../base/common/history.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { isMacintosh } from '../../../../base/common/platform.js'; @@ -1213,16 +1211,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } private handleAttachmentDeletion(e: KeyboardEvent | unknown, index: number, attachment: IChatRequestVariableEntry) { - this._attachmentModel.delete(attachment.id); - // Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click) if (dom.isKeyboardEvent(e)) { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - this._indexOfLastAttachedContextDeletedWithKeyboard = index; - } + this._indexOfLastAttachedContextDeletedWithKeyboard = index; } + this._attachmentModel.delete(attachment.id); + if (this._attachmentModel.size === 0) { this.focus(); } From 4a5e926665b40f45cf8b8d881fa08082ad0fbc41 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Wed, 23 Apr 2025 23:19:35 -0700 Subject: [PATCH 100/114] Clesn new welcome experience (#247281) --- .../browser/gettingStarted.contribution.ts | 4 ++-- .../browser/gettingStarted.ts | 5 ++--- .../common/gettingStartedContent.ts | 14 +++++++------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index 14a43e1e92f..662f7bc3abb 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -283,9 +283,9 @@ registerAction2(class extends Action2 { })); disposables.add(quickPick.onDidHide(() => disposables.dispose())); await extensionService.whenInstalledExtensionsRegistered(); - gettingStartedService.onDidAddWalkthrough(async () => { + disposables.add(gettingStartedService.onDidAddWalkthrough(async () => { quickPick.items = await this.getQuickPickItems(contextService, gettingStartedService); - }); + })); quickPick.show(); quickPick.busy = false; } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index f36cdce1c36..5d61a4b88d4 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -1603,9 +1603,6 @@ export class GettingStartedPage extends EditorPane { // Register listeners for step selection this.registerDispatchListeners(); - // Add handler for the step selection event - this.detailsPageDisposables.add(this.stepDisposables); - this.detailsScrollbar.scanDomNode(); this.detailsPageScrollbar?.scanDomNode(); } @@ -1633,6 +1630,8 @@ export class GettingStartedPage extends EditorPane { } private buildCategorySlide(categoryID: string, selectedStep?: string) { + this.container.classList.remove('newSlide'); + if (this.detailsScrollbar) { this.detailsScrollbar.dispose(); } this.extensionService.whenInstalledExtensionsRegistered().then(() => { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 44c048386fe..e225894e2d1 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -676,11 +676,11 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ content: { type: 'steps', steps: [ - createCopilotSetupStep('CopilotSetupSignedOut', CopilotSignedOutButton, 'chatSetupSignedOut', true), - createCopilotSetupStep('CopilotSetupComplete', CopilotCompleteButton, 'chatSetupInstalled && (chatPlanPro || chatPlanLimited)', false), - createCopilotSetupStep('CopilotSetupSignedIn', CopilotSignedInButton, '!chatSetupSignedOut && (!chatSetupInstalled || chatPlanCanSignUp)', true), + createCopilotSetupStep('NewCopilotSetupSignedOut', CopilotSignedOutButton, 'chatSetupSignedOut', true), + createCopilotSetupStep('NewCopilotSetupComplete', CopilotCompleteButton, 'chatSetupInstalled && (chatPlanPro || chatPlanLimited)', false), + createCopilotSetupStep('NewCopilotSetupSignedIn', CopilotSignedInButton, '!chatSetupSignedOut && (!chatSetupInstalled || chatPlanCanSignUp)', true), { - id: 'pickColorTheme', + id: 'newPickColorTheme', title: localize('gettingStarted.pickColor.title', "Choose your theme"), description: localize('gettingStarted.pickColor.description.interpolated', "The right theme helps you focus on your code, is easy on your eyes, and is simply more fun to use.\n{0}", Button(localize('titleID', "Browse Color Themes"), 'command:workbench.action.selectTheme')), completionEvents: [ @@ -690,7 +690,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ media: { type: 'markdown', path: 'theme_picker', } }, { - id: 'findLanguageExtensions', + id: 'newFindLanguageExtensions', title: localize('gettingStarted.findLanguageExts.title', "Rich support for all your languages"), description: localize('gettingStarted.findLanguageExts.description.interpolated', "Code smarter with syntax highlighting, code completion, linting and debugging. While many languages are built-in, many more can be added as extensions.\n{0}", Button(localize('browseLangExts', "Browse Language Extensions"), 'command:workbench.extensions.action.showLanguageExtensions')), when: 'workspacePlatform != \'webworker\'', @@ -699,7 +699,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ }, }, { - id: 'settingsAndSync', + id: 'newSettingsAndSync', title: localize('gettingStarted.settings.title', "Tune your settings"), description: localize('gettingStarted.settingsAndSync.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. [Back up and sync](command:workbench.userDataSync.actions.turnOn) your essential customizations across all your devices.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')), when: 'syncStatus != uninitialized', @@ -709,7 +709,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ }, }, { - id: 'commandPaletteTask', + id: 'newCommandPaletteTask', title: localize('gettingStarted.commandPalette.title', "Unlock productivity with the Command Palette "), description: localize('gettingStarted.commandPalette.description.interpolated', "Run commands without reaching for your mouse to accomplish any task in VS Code.\n{0}", Button(localize('commandPalette', "Open Command Palette"), 'command:workbench.action.showCommands')), media: { type: 'svg', altText: 'Command Palette overlay for searching and executing commands.', path: 'commandPalette.svg' }, From ec85c0dea994802758bfeec0e4c98f1a16f92cd8 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 24 Apr 2025 11:20:53 +0200 Subject: [PATCH 101/114] prompt slash commands: don't use prompt: suffix (#247248) --- .../dialogs/askToSelectPrompt/utils/runPrompt.ts | 4 ++-- .../chat/common/promptSyntax/service/promptsService.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/runPrompt.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/runPrompt.ts index 3d5acb6b4d8..f3a49ad2f3d 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/runPrompt.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/runPrompt.ts @@ -6,9 +6,9 @@ import { IChatWidget } from '../../../../../chat.js'; import { getChatWidgetObject } from './attachInstructions.js'; import { URI } from '../../../../../../../../../base/common/uri.js'; -import { basename } from '../../../../../../../../../base/common/resources.js'; import { IViewsService } from '../../../../../../../../services/views/common/viewsService.js'; import { ICommandService } from '../../../../../../../../../platform/commands/common/commands.js'; +import { getPromptCommandName } from '../../../../../../common/promptSyntax/service/promptsService.js'; /** * Options for the {@link runPromptFile} function. @@ -45,7 +45,7 @@ export const runPromptFile = async ( const widget = await getChatWidgetObject(options); - widget.setInput(`/${basename(file)}`); + widget.setInput(`/${getPromptCommandName(file.path)}`); // submit the prompt immediately await widget.acceptInput(); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index f8289002870..b7e7cb33a57 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -121,7 +121,7 @@ export class PromptsService extends Disposable implements IPromptsService { } public asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined { - if (command.match(/^prompt:[\w_\-\.]+/)) { + if (command.match(/^[\w_\-\.]+/)) { return { command, detail: localize('prompt.file.detail', 'Prompt file: {0}', command) }; } return undefined; @@ -133,13 +133,13 @@ export class PromptsService extends Disposable implements IPromptsService { } const files = await this.listPromptFiles('prompt'); const command = data.command; - return files.find(file => getCommandName(file.uri.path) === command); + return files.find(file => getPromptCommandName(file.uri.path) === command); } public async findPromptSlashCommands(): Promise { const promptFiles = await this.listPromptFiles('prompt'); return promptFiles.map(promptPath => { - const command = getCommandName(promptPath.uri.path); + const command = getPromptCommandName(promptPath.uri.path); return { command, detail: localize('prompt.file.detail', 'Prompt file: {0}', this.labelService.getUriLabel(promptPath.uri, { relative: true })), @@ -149,9 +149,9 @@ export class PromptsService extends Disposable implements IPromptsService { } } -function getCommandName(path: string) { +export function getPromptCommandName(path: string) { const name = basename(path, PROMPT_FILE_EXTENSION); - return `prompt:${name}`; + return name; } From 78655672bb1687a661338bdbc8b498048ccaafcc Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 24 Apr 2025 11:21:10 +0200 Subject: [PATCH 102/114] edit diffs should always honor whitespace changes (#247295) this ensures edits derived from diffs (for undo/accept hunk) are trustworthy and so that they don't wreck the document fixes https://github.com/microsoft/vscode-copilot/issues/16289 --- .../chatEditingModifiedDocumentEntry.ts | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index 44cc67fded2..0414389b526 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -6,7 +6,7 @@ import { assert } from '../../../../../base/common/assert.js'; import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { IReference, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { observableValue, IObservable, ITransaction, autorun, transaction } from '../../../../../base/common/observable.js'; +import { observableValue, ITransaction, autorun, transaction } from '../../../../../base/common/observable.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { themeColorFromId } from '../../../../../base/common/themables.js'; import { assertType } from '../../../../../base/common/types.js'; @@ -34,7 +34,6 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IMarkerService } from '../../../../../platform/markers/common/markers.js'; -import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { editorSelectionBackground } from '../../../../../platform/theme/common/colorRegistry.js'; import { IUndoRedoElement, IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; import { SaveReason, IEditorPane } from '../../../../common/editor.js'; @@ -92,9 +91,6 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie private readonly _editDecorationClear = this._register(new RunOnceScheduler(() => { this._editDecorations = this.modifiedModel.deltaDecorations(this._editDecorations, []); }, 500)); private _editDecorations: string[] = []; - - private readonly _diffTrimWhitespace: IObservable; - readonly originalURI: URI; constructor( @@ -160,12 +156,6 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie this._clearCurrentEditLineDecoration(); })); - this._diffTrimWhitespace = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, configService); - this._register(autorun(r => { - this._diffTrimWhitespace.read(r); - this._updateDiffInfoSeq(); - })); - const resourceFilter = this._register(new MutableDisposable()); this._register(autorun(r => { const res = this.isCurrentlyBeingModifiedBy.read(r); @@ -414,12 +404,14 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie const docVersionNow = this.modifiedModel.getVersionId(); const snapshotVersionNow = this.originalModel.getVersionId(); - const ignoreTrimWhitespace = this._diffTrimWhitespace.get(); - const diff = await this._editorWorkerService.computeDiff( this.originalModel.uri, this.modifiedModel.uri, - { ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 }, + { + ignoreTrimWhitespace: false, // NEVER ignore whitespace so that undo/accept edits are correct and so that all changes (1 of 2) are spelled out + computeMoves: false, + maxComputationTimeMs: 3000 + }, 'advanced' ); From 9c437220c803ba524d878b2dbc174f3e06694de2 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 24 Apr 2025 11:21:42 +0200 Subject: [PATCH 103/114] fix https://github.com/microsoft/vscode/issues/247292 (#247297) --- .../chat/browser/chatEditing/chatEditingEditorActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts index 597bd21a716..355b63eca0a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts @@ -264,7 +264,7 @@ class ToggleDiffAction extends ChatEditingEditorAction { constructor() { super({ id: 'chatEditor.action.toggleDiff', - title: localize2('diff', 'Toggle Diff Editor'), + title: localize2('diff', 'Toggle Diff Editor for Chat Edits'), category: CHAT_CATEGORY, toggled: { condition: ContextKeyExpr.or(EditorContextKeys.inDiffEditor, ActiveEditorContext.isEqualTo(TEXT_DIFF_EDITOR_ID))!, @@ -298,7 +298,7 @@ class ToggleAccessibleDiffViewAction extends ChatEditingEditorAction { constructor() { super({ id: 'chatEditor.action.showAccessibleDiffView', - title: localize2('accessibleDiff', 'Show Accessible Diff View'), + title: localize2('accessibleDiff', 'Show Accessible Diff View for Chat Edits'), f1: true, precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxHasRequestInProgress.negate()), keybinding: { From e68e5363444c5e7fad98c1cc8c024f736d4884e4 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 24 Apr 2025 12:20:17 +0200 Subject: [PATCH 104/114] remove "Done" button (#247300) https://github.com/microsoft/vscode-copilot-release/issues/7875 --- .../chat/browser/actions/chatClearActions.ts | 40 +------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 3124ba1249c..bb4f593a6a9 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -16,9 +16,8 @@ import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contex import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ActiveEditorContext } from '../../../../common/contextkeys.js'; -import { isChatViewTitleActionContext } from '../../common/chatActions.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingSession } from '../../common/chatEditingService.js'; +import { IChatEditingSession } from '../../common/chatEditingService.js'; import { IChatService } from '../../common/chatService.js'; import { ChatMode } from '../../common/constants.js'; import { ChatViewId, IChatWidget } from '../chat.js'; @@ -141,43 +140,6 @@ export function registerNewChatActions() { }); CommandsRegistry.registerCommandAlias(ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION); - registerAction2(class GlobalEditsDoneAction extends EditingSessionAction { - constructor() { - super({ - id: ChatDoneActionId, - title: localize2('chat.done.label', "Done"), - category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered), - f1: false, - menu: [{ - id: MenuId.ChatEditingWidgetToolbar, - when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey.negate(), hasAppliedChatEditsContextKey, ChatContextKeys.editingParticipantRegistered), - group: 'navigation', - order: 0 - }] - }); - } - - override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, widget: IChatWidget, ...args: any[]) { - const context = args[0]; - const accessibilitySignalService = accessor.get(IAccessibilitySignalService); - if (isChatViewTitleActionContext(context)) { - // Is running in the Chat view title - announceChatCleared(accessibilitySignalService); - if (widget) { - widget.clear(); - widget.attachmentModel.clear(); - widget.focusInput(); - } - } else { - // Is running from f1 or keybinding - announceChatCleared(accessibilitySignalService); - widget.clear(); - widget.attachmentModel.clear(); - widget.focusInput(); - } - } - }); registerAction2(class UndoChatEditInteractionAction extends EditingSessionAction { constructor() { From 445bb01935164f90afec8c41654718d83768a61c Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Thu, 24 Apr 2025 12:56:59 +0200 Subject: [PATCH 105/114] Bump `@azure/msal-node-runtime` to 0.18.2 (#247301) --- extensions/microsoft-authentication/package-lock.json | 6 +++--- extensions/microsoft-authentication/package.json | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/extensions/microsoft-authentication/package-lock.json b/extensions/microsoft-authentication/package-lock.json index ab718f4db28..c500a8c3d3e 100644 --- a/extensions/microsoft-authentication/package-lock.json +++ b/extensions/microsoft-authentication/package-lock.json @@ -71,9 +71,9 @@ } }, "node_modules/@azure/msal-node-runtime": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.17.1.tgz", - "integrity": "sha512-qAfTg+iGJsg+XvD9nmknI63+XuoX32oT+SX4wJdFz7CS6ETVpSHoroHVaUmsTU1H7H0+q1/ZkP988gzPRMYRsg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.18.2.tgz", + "integrity": "sha512-v45fyBQp80BrjZAeGJXl+qggHcbylQiFBihr0ijO2eniDCW9tz5TZBKYsqzH06VuiRaVG/Sa0Hcn4pjhJqFSTw==", "hasInstallScript": true, "license": "MIT" }, diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 928ffa68889..1356583844e 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -145,6 +145,9 @@ "keytar": "file:./packageMocks/keytar", "vscode-tas-client": "^0.1.84" }, + "overrides": { + "@azure/msal-node-runtime": "^0.18.2" + }, "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" From edcb3e4aa991d3ecb7dd992e01b04ac99f7a3563 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 24 Apr 2025 07:49:23 -0700 Subject: [PATCH 106/114] ensure properties are initialized before usage (#247312) --- build/lib/propertyInitOrderChecker.js | 3 --- build/lib/propertyInitOrderChecker.ts | 3 --- .../browser/inlineCompletionsAccessibleView.ts | 3 ++- .../common/capabilities/commandDetectionCapability.ts | 4 ++-- src/vs/platform/terminal/node/ptyService.ts | 11 +++++++---- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/build/lib/propertyInitOrderChecker.js b/build/lib/propertyInitOrderChecker.js index c7113c643ed..c4931788047 100644 --- a/build/lib/propertyInitOrderChecker.js +++ b/build/lib/propertyInitOrderChecker.js @@ -88,7 +88,6 @@ const ignored = new Set([ 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts', 'vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts', 'vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts', - 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts', 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts', 'vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts', 'vs/workbench/contrib/files/browser/views/openEditorsView.ts', @@ -106,7 +105,6 @@ const ignored = new Set([ 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts', 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts', 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts', - 'vs/platform/terminal/common/capabilities/commandDetectionCapability.ts', 'vs/workbench/services/authentication/browser/authenticationExtensionsService.ts', 'vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts', 'vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts', @@ -124,7 +122,6 @@ const ignored = new Set([ 'vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts', 'vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts', 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts', - 'vs/platform/terminal/node/ptyService.ts', 'vs/workbench/api/common/extHostLanguageFeatures.ts', 'vs/workbench/api/common/extHostSearch.ts', ]); diff --git a/build/lib/propertyInitOrderChecker.ts b/build/lib/propertyInitOrderChecker.ts index 33fe1dc4b60..bbc98c6f43f 100644 --- a/build/lib/propertyInitOrderChecker.ts +++ b/build/lib/propertyInitOrderChecker.ts @@ -57,7 +57,6 @@ const ignored = new Set([ 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts', 'vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts', 'vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts', - 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts', 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts', 'vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts', 'vs/workbench/contrib/files/browser/views/openEditorsView.ts', @@ -75,7 +74,6 @@ const ignored = new Set([ 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts', 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts', 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts', - 'vs/platform/terminal/common/capabilities/commandDetectionCapability.ts', 'vs/workbench/services/authentication/browser/authenticationExtensionsService.ts', 'vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts', 'vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts', @@ -93,7 +91,6 @@ const ignored = new Set([ 'vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts', 'vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts', 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts', - 'vs/platform/terminal/node/ptyService.ts', 'vs/workbench/api/common/extHostLanguageFeatures.ts', 'vs/workbench/api/common/extHostSearch.ts', ]); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts index 38a558a1e66..a47deb89b84 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts @@ -42,16 +42,17 @@ export class InlineCompletionsAccessibleView implements IAccessibleViewImplement class InlineCompletionsAccessibleViewContentProvider extends Disposable implements IAccessibleViewContentProvider { private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); public readonly onDidChangeContent: Event = this._onDidChangeContent.event; + public readonly options: { language: string | undefined; type: AccessibleViewType.View }; constructor( private readonly _editor: ICodeEditor, private readonly _model: InlineCompletionsModel, ) { super(); + this.options = { language: this._editor.getModel()?.getLanguageId() ?? undefined, type: AccessibleViewType.View }; } public readonly id = AccessibleViewProviderId.InlineCompletions; public readonly verbositySettingKey = 'accessibility.verbosity.inlineCompletions'; - public readonly options = { language: this._editor.getModel()?.getLanguageId() ?? undefined, type: AccessibleViewType.View }; public provideContent(): string { const state = this._model.state.get(); diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index e0ba3516012..3bdc360144a 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -28,7 +28,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe protected _commands: TerminalCommand[] = []; private _cwd: string | undefined; private _promptTerminator: string | undefined; - private _currentCommand: PartialTerminalCommand = new PartialTerminalCommand(this._terminal); + private _currentCommand: PartialTerminalCommand; private _commandMarkers: IMarker[] = []; private _dimensions: ITerminalDimensions; private __isCommandStorageDisabled: boolean = false; @@ -85,7 +85,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe @ILogService private readonly _logService: ILogService ) { super(); - + this._currentCommand = new PartialTerminalCommand(this._terminal); this._promptInputModel = this._register(new PromptInputModel(this._terminal, this.onCommandStarted, this.onCommandStartChanged, this.onCommandExecuted, this._logService)); // Pull command line from the buffer if it was not set explicitly diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 840334c8d4c..87edbb57a7b 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -78,7 +78,7 @@ export class PtyService extends Disposable implements IPtyService { // #region Pty service contribution RPC calls - private readonly _autoRepliesContribution = new AutoRepliesPtyServiceContribution(this._logService); + private readonly _autoRepliesContribution: AutoRepliesPtyServiceContribution; @traceRpc async installAutoReply(match: string, reply: string) { await this._autoRepliesContribution.installAutoReply(match, reply); @@ -90,9 +90,7 @@ export class PtyService extends Disposable implements IPtyService { // #endregion - private readonly _contributions: IPtyServiceContribution[] = [ - this._autoRepliesContribution - ]; + private readonly _contributions: IPtyServiceContribution[]; private _lastPtyId: number = 0; @@ -148,6 +146,11 @@ export class PtyService extends Disposable implements IPtyService { this._detachInstanceRequestStore = this._register(new RequestStore(undefined, this._logService)); this._detachInstanceRequestStore.onCreateRequest(this._onDidRequestDetach.fire, this._onDidRequestDetach); + + this._autoRepliesContribution = new AutoRepliesPtyServiceContribution(this._logService); + + this._contributions = [this._autoRepliesContribution]; + } @traceRpc From 0cffb673f50e4f9258b4a509e63e0abcb07ef786 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 24 Apr 2025 16:52:46 +0200 Subject: [PATCH 107/114] fix github extension bundling (#247314) --- extensions/github/extension.webpack.config.cjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/github/extension.webpack.config.cjs b/extensions/github/extension.webpack.config.cjs index e0ac991de2e..75b86c82b68 100644 --- a/extensions/github/extension.webpack.config.cjs +++ b/extensions/github/extension.webpack.config.cjs @@ -18,6 +18,9 @@ module.exports = withDefaults({ libraryTarget: 'module', chunkFormat: 'module', }, + externals: { + 'vscode': 'module vscode', + }, experiments: { outputModule: true } From 9cdce23a6bbf3706f056c233d9cddc9fbba12c13 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 24 Apr 2025 08:48:32 -0700 Subject: [PATCH 108/114] only use `listitem` role for suggest items on windows (#247316) fix #246253 --- src/vs/editor/contrib/suggest/browser/suggestWidget.ts | 3 ++- .../workbench/services/suggest/browser/simpleSuggestWidget.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index f22622e5ac3..71ae4e428fe 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -36,6 +36,7 @@ import { ItemRenderer } from './suggestWidgetRenderer.js'; import { getListStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { status } from '../../../../base/browser/ui/aria/aria.js'; import { CompletionItemKinds } from '../../../common/languages.js'; +import { isWindows } from '../../../../base/common/platform.js'; /** * Suggest widget colors @@ -231,7 +232,7 @@ export class SuggestWidget implements IDisposable { mouseSupport: false, multipleSelectionSupport: false, accessibilityProvider: { - getRole: () => 'listitem', + getRole: () => isWindows ? 'listitem' : 'option', getWidgetAriaLabel: () => nls.localize('suggest', "Suggest"), getWidgetRole: () => 'listbox', getAriaLabel: (item: CompletionItem) => { diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index 8d4b120c568..51e1b5c24ab 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -25,6 +25,7 @@ import { canExpandCompletionItem, SimpleSuggestDetailsOverlay, SimpleSuggestDeta import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import * as strings from '../../../../base/common/strings.js'; import { status } from '../../../../base/browser/ui/aria/aria.js'; +import { isWindows } from '../../../../base/common/platform.js'; const $ = dom.$; @@ -196,7 +197,7 @@ export class SimpleSuggestWidget, TI mouseSupport: false, multipleSelectionSupport: false, accessibilityProvider: { - getRole: () => 'listitem', + getRole: () => isWindows ? 'listitem' : 'option', getWidgetAriaLabel: () => localize('suggest', "Suggest"), getWidgetRole: () => 'listbox', getAriaLabel: (item: SimpleCompletionItem) => { From 65ce24691a40bf344bdfbccd1d64e9b553f3dc27 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 24 Apr 2025 08:49:38 -0700 Subject: [PATCH 109/114] mcp: apply tool picker UX improvements (#247158) * mcp: apply tool picker UX improvements Applies the suggestions from #245721 - Adjusts 'From' to 'from' - Remove status wording - Add item buttons for configuration. I also show a button to view the MCP output when it's in a failed state, since I think showing that is important. * fix hygenie * avoid using `alwaysVisible` and some polish --------- Co-authored-by: Johannes --- .../chat/browser/actions/chatToolActions.ts | 65 +++++++++++++++---- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts index 87f68f6406e..e574f8e143e 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts @@ -18,11 +18,13 @@ import { ICommandService } from '../../../../../platform/commands/common/command import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; +import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { AddConfigurationAction } from '../../../mcp/browser/mcpCommands.js'; -import { IMcpService, IMcpServer, McpConnectionState } from '../../../mcp/common/mcpTypes.js'; +import { IMcpRegistry } from '../../../mcp/common/mcpRegistryTypes.js'; +import { IMcpServer, IMcpService, McpConnectionState } from '../../../mcp/common/mcpTypes.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatToolInvocation } from '../../common/chatService.js'; import { isResponseVM } from '../../common/chatViewModel.js'; @@ -109,11 +111,13 @@ export class AttachToolsAction extends Action2 { const quickPickService = accessor.get(IQuickInputService); const mcpService = accessor.get(IMcpService); + const mcpRegistry = accessor.get(IMcpRegistry); const toolsService = accessor.get(ILanguageModelToolsService); const chatWidgetService = accessor.get(IChatWidgetService); const telemetryService = accessor.get(ITelemetryService); const commandService = accessor.get(ICommandService); const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const editorService = accessor.get(IEditorService); let widget = chatWidgetService.lastFocusedWidget; if (!widget) { @@ -143,6 +147,7 @@ export class AttachToolsAction extends Action2 { type ToolPick = IQuickPickItem & { picked: boolean; tool: IToolData; parent: BucketPick }; type AddPick = IQuickPickItem & { pickable: false; run: () => void }; type MyPick = ToolPick | BucketPick | AddPick; + type ActionableButton = IQuickInputButton & { action: () => void }; const addMcpPick: AddPick = { type: 'item', label: localize('addServer', "Add MCP Server..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => commandService.executeCommand(AddConfigurationAction.ID) }; const addExpPick: AddPick = { type: 'item', label: localize('addExtension', "Install Extension..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => extensionWorkbenchService.openSearch('@tag:language-model-tools') }; @@ -176,7 +181,7 @@ export class AttachToolsAction extends Action2 { continue; } - let bucket: BucketPick; + let bucket: BucketPick | undefined; if (tool.source.type === 'mcp') { const mcpServer = mcpServerByTool.get(tool.id); @@ -184,16 +189,40 @@ export class AttachToolsAction extends Action2 { continue; } const key = tool.source.type + mcpServer.definition.id; - bucket = toolBuckets.get(key) ?? { - type: 'item', - label: localize('mcplabel', "MCP Server: {0}", mcpServer?.definition.label), - status: localize('mcpstatus', "From {0} ({1})", mcpServer.collection.label, McpConnectionState.toString(mcpServer.connectionState.get())), - ordinal: BucketOrdinal.Mcp, - source: tool.source, - picked: false, - children: [] - }; - toolBuckets.set(key, bucket); + bucket = toolBuckets.get(key); + + if (!bucket) { + const collection = mcpRegistry.collections.get().find(c => c.id === mcpServer.collection.id); + const buttons: ActionableButton[] = []; + if (collection?.presentation?.origin) { + buttons.push({ + iconClass: ThemeIcon.asClassName(Codicon.settingsGear), + tooltip: localize('configMcpCol', "Configure {0}", collection.label), + action: () => editorService.openEditor({ + resource: collection!.presentation!.origin, + }) + }); + } + if (mcpServer.connectionState.get().state === McpConnectionState.Kind.Error) { + buttons.push({ + iconClass: ThemeIcon.asClassName(Codicon.warning), + tooltip: localize('mcpShowOutput', "Show Output"), + action: () => mcpServer.showOutput(), + }); + } + + bucket = { + type: 'item', + label: localize('mcplabel', "MCP Server: {0}", mcpServer?.definition.label), + status: localize('mcpstatus', "from {0}", mcpServer.collection.label), + ordinal: BucketOrdinal.Mcp, + source: tool.source, + picked: false, + children: [], + buttons, + }; + toolBuckets.set(key, bucket); + } } else if (tool.source.type === 'extension') { const key = tool.source.type + ExtensionIdentifier.toKey(tool.source.extensionId); @@ -238,6 +267,9 @@ export class AttachToolsAction extends Action2 { function isAddPick(obj: any): obj is AddPick { return Boolean((obj as AddPick).run); } + function isActionableButton(obj: IQuickInputButton): obj is ActionableButton { + return typeof (obj as ActionableButton).action === 'function'; + } const store = new DisposableStore(); @@ -306,6 +338,13 @@ export class AttachToolsAction extends Action2 { picker.items = picks; picker.show(); + store.add(picker.onDidTriggerItemButton(e => { + if (isActionableButton(e.button)) { + e.button.action(); + store.dispose(); + } + })); + store.add(picker.onDidChangeSelection(selectedPicks => { if (ignoreEvent) { return; From 71249a7d4d94d1f7cc3b4681067b02a10a71aa10 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:19:49 +0200 Subject: [PATCH 110/114] Finalize commentReplyAuthor (#247303) Fixes #246088 --- .../common/extensionsApiProposals.ts | 3 --- .../workbench/api/common/extHostComments.ts | 4 ---- src/vscode-dts/vscode.d.ts | 2 +- .../vscode.proposed.commentReplyAuthor.d.ts | 22 ------------------- 4 files changed, 1 insertion(+), 30 deletions(-) delete mode 100644 src/vscode-dts/vscode.proposed.commentReplyAuthor.d.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 35d88b02170..dbd47652aa6 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -65,9 +65,6 @@ const _allApiProposals = { commentReactor: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReactor.d.ts', }, - commentReplyAuthor: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReplyAuthor.d.ts', - }, commentReveal: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReveal.d.ts', }, diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 6605a713d23..3f1ac7829cb 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -319,10 +319,6 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo private _canReply: boolean | vscode.CommentAuthorInformation = true; set canReply(state: boolean | vscode.CommentAuthorInformation) { - if (typeof state !== 'boolean') { - checkProposedApiEnabled(this.extensionDescription, 'commentReplyAuthor'); - } - if (this._canReply !== state) { this._canReply = state; this.modifications.canReply = state; diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index d7f9e6c103a..cefcb1a81b3 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -17395,7 +17395,7 @@ declare module 'vscode' { * Whether the thread supports reply. * Defaults to true. */ - canReply: boolean; + canReply: boolean | CommentAuthorInformation; /** * Context value of the comment thread. This can be used to contribute thread specific actions. diff --git a/src/vscode-dts/vscode.proposed.commentReplyAuthor.d.ts b/src/vscode-dts/vscode.proposed.commentReplyAuthor.d.ts deleted file mode 100644 index d91462b7dd8..00000000000 --- a/src/vscode-dts/vscode.proposed.commentReplyAuthor.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // @alexr00 https://github.com/microsoft/vscode/issues/246088 - - export interface CommentThread2 { - canReply: boolean | CommentAuthorInformation; - - readonly uri: Uri; - range: Range | undefined; - comments: readonly Comment[]; - collapsibleState: CommentThreadCollapsibleState; - contextValue?: string; - label?: string; - state?: CommentThreadState | { resolved?: CommentThreadState; applicability?: CommentThreadApplicability }; - dispose(): void; - } -} From 6ab0b427af36c650ac9c27717d04318d51fd9c7f Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 24 Apr 2025 18:21:12 +0200 Subject: [PATCH 111/114] handlePromptSlashCommand: check for duplicate variables (#247318) --- .../contrib/chat/browser/chatWidget.ts | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index c0edee9fa9e..eaa1e915669 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -18,11 +18,12 @@ import { combinedDisposable, Disposable, DisposableStore, IDisposable, MutableDi import { ResourceSet } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; import { autorun, autorunWithStore, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; -import { extUri, isEqual } from '../../../../base/common/resources.js'; +import { basename, extUri, isEqual } from '../../../../base/common/resources.js'; import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { isLocation } from '../../../../editor/common/languages.js'; import { localize } from '../../../../nls.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -1147,21 +1148,27 @@ export class ChatWidget extends Disposable implements IChatWidget { return inputState; } - private async _handlePromptSlashCommand(): Promise<{ input?: string; attachment?: IChatRequestVariableEntry }> { + private async _handlePromptSlashCommand(input: string, attachedContext: IChatRequestVariableEntry[]): Promise { const agentSlashPromptPart = this.parsedInput.parts.find((r): r is ChatRequestSlashPromptPart => r instanceof ChatRequestSlashPromptPart); if (!agentSlashPromptPart) { - return {}; + return input; } + // remove the slash command the input + input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim(); const promptPath = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.slashPromptCommand); if (!promptPath) { - return {}; + return input; } - const attachment = toChatVariable({ uri: promptPath.uri, isPromptFile: true }, true); - const input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim(); - return { attachment, input }; + const getUri = (variable: IPromptVariableEntry) => isLocation(variable.value) ? variable.value.uri : variable.value; + if (!attachedContext.some(variable => isPromptFileChatVariable(variable) && isEqual(getUri(variable), promptPath.uri))) { + // not yet attached, so attach it + const variable = toChatVariable({ uri: promptPath.uri, isPromptFile: true }, true); + attachedContext.push(variable); + } + return `Follow the prompt instructions from ${basename(promptPath.uri)}\n${input}`; } private async _acceptInput(query: { query: string } | undefined, options?: IChatAcceptInputOptions): Promise { @@ -1183,21 +1190,8 @@ export class ChatWidget extends Disposable implements IChatWidget { const { promptInstructions } = this.inputPart.attachmentModel; const instructionsEnabled = promptInstructions.featureEnabled; if (instructionsEnabled) { - const result = await this._handlePromptSlashCommand(); - if (result.input !== undefined) { - input = result.input; - } - - if (result.attachment) { - attachedContext.push(result.attachment); - } - - const promptFileVariables = attachedContext.filter(isPromptFileChatVariable); - await this.setupChatModeAndTools(promptFileVariables); - - if (promptFileVariables.length > 0) { - input = `Follow the prompt instructions from ${promptFileVariables.map(v => v.name).join(', ')}\n${input}`; - } + input = await this._handlePromptSlashCommand(input, attachedContext); + this.setupChatModeAndTools(attachedContext.filter(isPromptFileChatVariable)); } if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentMode === ChatMode.Edit && !this.chatService.edits2Enabled) { From 7d87048781add8680f007053430a3d930179ad5d Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 24 Apr 2025 09:27:52 -0700 Subject: [PATCH 112/114] LanguageModelDataPart as arbitrary mimeType+data+factory functions (#247238) * LanguageModelDataPart as arbitrary mimeType+data+factory functions Like NotebookCellOutputItem * change languagemodel data part shape * rm whitespace --------- Co-authored-by: Your Name --- .../workbench/api/common/extHost.api.impl.ts | 2 +- .../api/common/extHostTypeConverters.ts | 11 ++--- src/vs/workbench/api/common/extHostTypes.ts | 47 +++++++++++-------- ...vscode.proposed.languageModelDataPart.d.ts | 42 +++++++++-------- 4 files changed, 55 insertions(+), 47 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 4376ba3408b..09983876218 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1790,6 +1790,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I PartialAcceptTriggerKind: extHostTypes.PartialAcceptTriggerKind, InlineCompletionEndOfLifeReasonKind: extHostTypes.InlineCompletionEndOfLifeReasonKind, KeywordRecognitionStatus: extHostTypes.KeywordRecognitionStatus, + ChatImageMimeType: extHostTypes.ChatImageMimeType, ChatResponseMarkdownPart: extHostTypes.ChatResponseMarkdownPart, ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, ChatResponseAnchorPart: extHostTypes.ChatResponseAnchorPart, @@ -1826,7 +1827,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I LanguageModelToolResult: extHostTypes.LanguageModelToolResult, LanguageModelDataPart: extHostTypes.LanguageModelDataPart, LanguageModelExtraDataPart: extHostTypes.LanguageModelExtraDataPart, - ChatImageMimeType: extHostTypes.ChatImageMimeType, ExtendedLanguageModelToolResult: extHostTypes.ExtendedLanguageModelToolResult, PreparedTerminalToolInvocation: extHostTypes.PreparedTerminalToolInvocation, LanguageModelChatToolMode: extHostTypes.LanguageModelChatToolMode, diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index a4c356ad7fb..06eb006927f 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2423,12 +2423,7 @@ export namespace LanguageModelChatMessage2 { }); return new types.LanguageModelToolResultPart(c.toolCallId, content, c.isError); } else if (c.type === 'image_url') { - const value: vscode.ChatImagePart = { - mimeType: c.value.mimeType, - data: c.value.data.buffer, - }; - - return new types.LanguageModelDataPart(value); + return new types.LanguageModelDataPart(c.value.data.buffer, c.value.mimeType); } else if (c.type === 'extra_data') { return new types.LanguageModelExtraDataPart(c.kind, c.data); } else { @@ -2475,8 +2470,8 @@ export namespace LanguageModelChatMessage2 { }; } else if (c instanceof types.LanguageModelDataPart) { const value: chatProvider.IChatImageURLPart = { - mimeType: c.value.mimeType, - data: VSBuffer.wrap(c.value.data), + mimeType: c.mimeType as chatProvider.ChatImageMimeType, + data: VSBuffer.wrap(c.data), }; return { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 7956f27742c..25ebb24b9df 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4945,20 +4945,44 @@ export class LanguageModelTextPart implements vscode.LanguageModelTextPart { } export class LanguageModelDataPart implements vscode.LanguageModelDataPart { - value: vscode.ChatImagePart; + mimeType: string; + data: Uint8Array; - constructor(value: vscode.ChatImagePart) { - this.value = value; + constructor(data: Uint8Array, mimeType: string) { + this.mimeType = mimeType; + this.data = data; + } + + static image(data: Uint8Array, mimeType: ChatImageMimeType): vscode.LanguageModelDataPart { + return new LanguageModelDataPart(data, mimeType as string); + } + + static json(value: object): vscode.LanguageModelDataPart { + const rawStr = JSON.stringify(value, undefined, '\t'); + return new LanguageModelDataPart(VSBuffer.fromString(rawStr).buffer, 'json'); + } + + static text(value: string): vscode.LanguageModelDataPart { + return new LanguageModelDataPart(VSBuffer.fromString(value).buffer, 'text/plain'); } toJSON() { return { $mid: MarshalledId.LanguageModelDataPart, - value: this.value, + mimeType: this.mimeType, + data: this.data, }; } } +export enum ChatImageMimeType { + PNG = 'image/png', + JPEG = 'image/jpeg', + GIF = 'image/gif', + WEBP = 'image/webp', + BMP = 'image/bmp', +} + export class LanguageModelExtraDataPart implements vscode.LanguageModelExtraDataPart { kind: string; data: any; @@ -4977,21 +5001,6 @@ export class LanguageModelExtraDataPart implements vscode.LanguageModelExtraData } } -/** - * Enum for supported image MIME types. - */ -export enum ChatImageMimeType { - PNG = 'image/png', - JPEG = 'image/jpeg', - GIF = 'image/gif', - WEBP = 'image/webp', - BMP = 'image/bmp', -} - -export interface ChatImagePart { - mimeType: ChatImageMimeType; - data: VSBuffer; -} export class LanguageModelPromptTsxPart { value: unknown; diff --git a/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts b/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts index 29eb2f10e8c..5164118ef59 100644 --- a/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts @@ -60,19 +60,35 @@ declare module 'vscode' { } /** - * A language model response part containing an image, returned from a {@link LanguageModelChatResponse}. - */ + * A language model response part containing arbitrary data, returned from a {@link LanguageModelChatResponse}. + */ export class LanguageModelDataPart { /** - * The image content of the part. + * Factory function to create a `LanguageModelDataPart` for an image. + * @param data Binary image data + * @param mimeType The MIME type of the image */ - value: ChatImagePart; + static image(data: Uint8Array, mimeType: ChatImageMimeType): LanguageModelDataPart; + + static json(value: object): LanguageModelDataPart; + + static text(value: string): LanguageModelDataPart; /** - * Construct an image part with the given content. - * @param value The image content of the part. + * The mime type which determines how the data property is interpreted. */ - constructor(value: ChatImagePart); + mimeType: string; + + /** + * The data of the part. + */ + data: Uint8Array; + + /** + * Construct a generic data part with the given content. + * @param value The data of the part. + */ + constructor(data: Uint8Array, mimeType: string); } /** @@ -86,18 +102,6 @@ declare module 'vscode' { BMP = 'image/bmp', } - export interface ChatImagePart { - /** - * The image's MIME type. - */ - mimeType: ChatImageMimeType; - - /** - * The raw binary data of the image, encoded as a Uint8Array. Note: do not use base64 encoding. Maximum image size is 5MB. - */ - data: Uint8Array; - } - /** * Tagging onto this proposal, because otherwise managing two different extensions of LanguageModelChatMessage could be confusing. * A language model response part containing arbitrary model-specific data, returned from a {@link LanguageModelChatResponse}. From 1a1f7704d9b494573075b79bf3bc2b7bd9d16a75 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 24 Apr 2025 18:44:18 +0200 Subject: [PATCH 113/114] for inline chat v2 add a `inlineChat.hideOnRequest` setting (#247319) this setting control if the inline chat widget hides after making the request or not --- .../chatEditing/chatEditingEditorOverlay.ts | 6 + .../browser/inlineChat.contribution.ts | 12 +- .../inlineChat/browser/inlineChatActions.ts | 112 +++++++++++++++--- .../browser/inlineChatController.ts | 78 +++++++----- .../browser/inlineChatSessionService.ts | 3 + .../browser/inlineChatSessionServiceImpl.ts | 9 +- .../contrib/inlineChat/common/inlineChat.ts | 9 +- 7 files changed, 180 insertions(+), 49 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index 8069c2810fc..bfcdefb385d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -377,6 +377,12 @@ class ChatEditingOverlayController { const { session, entry } = data; + if (!session.isGlobalEditingSession && !inlineChatService.hideOnRequest.read(r)) { + // inline chat - no chat overlay unless hideOnRequest is on + hide(); + return; + } + if ( entry?.state.read(r) === ModifiedFileEntryState.Modified // any entry changing || (!session.isGlobalEditingSession && isInProgress.read(r)) // inline chat request diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index fbdcc1d5f36..ab86b66a756 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -7,7 +7,7 @@ import { EditorContributionInstantiation, registerEditorContribution } from '../ import { IMenuItem, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { InlineChatController, InlineChatController1, InlineChatController2 } from './inlineChatController.js'; import * as InlineChatActions from './inlineChatActions.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, INLINE_CHAT_ID, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, INLINE_CHAT_ID, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; @@ -28,7 +28,9 @@ registerEditorContribution(InlineChatController2.ID, InlineChatController2, Edit registerEditorContribution(INLINE_CHAT_ID, InlineChatController1, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors registerEditorContribution(InlineChatController.ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors -registerAction2(InlineChatActions.StopSessionAction2); +registerAction2(InlineChatActions.KeepSessionAction2); +registerAction2(InlineChatActions.UndoSessionAction2); +registerAction2(InlineChatActions.CloseSessionAction2); registerAction2(InlineChatActions.RevealWidget); registerAction2(InlineChatActions.CancelRequestAction); @@ -54,7 +56,8 @@ const editActionMenuItem: IMenuItem = { when: ContextKeyExpr.and( ChatContextKeys.inputHasText, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), - CTX_INLINE_CHAT_EDITING + CTX_INLINE_CHAT_EDITING, + CTX_INLINE_CHAT_HAS_AGENT ), }; @@ -68,7 +71,8 @@ const generateActionMenuItem: IMenuItem = { when: ContextKeyExpr.and( ChatContextKeys.inputHasText, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), - CTX_INLINE_CHAT_EDITING.toNegated() + CTX_INLINE_CHAT_EDITING.toNegated(), + CTX_INLINE_CHAT_HAS_AGENT ), }; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 8a3cb6349dd..d86b832a7df 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -178,6 +178,22 @@ export abstract class AbstractInline1ChatAction extends EditorAction2 { static readonly category = localize2('cat', "Inline Chat"); constructor(desc: IAction2Options) { + + const massageMenu = (menu: IAction2Options['menu'] | undefined) => { + if (Array.isArray(menu)) { + for (const entry of menu) { + entry.when = ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT, entry.when); + } + } else if (menu) { + menu.when = ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT, menu.when); + } + }; + if (Array.isArray(desc.menu)) { + massageMenu(desc.menu); + } else { + massageMenu(desc.menu); + } + super({ ...desc, category: AbstractInline1ChatAction.category, @@ -385,9 +401,7 @@ export class CloseAction extends AbstractInline1ChatAction { id: MENU_INLINE_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and( - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() - ), + when: CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() }, { id: MENU_INLINE_CHAT_SIDE, group: 'navigation', @@ -537,6 +551,21 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { static readonly category = localize2('cat', "Inline Chat"); constructor(desc: IAction2Options) { + const massageMenu = (menu: IAction2Options['menu'] | undefined) => { + if (Array.isArray(menu)) { + for (const entry of menu) { + entry.when = ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT2, entry.when); + } + } else if (menu) { + menu.when = ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT2, menu.when); + } + }; + if (Array.isArray(desc.menu)) { + massageMenu(desc.menu); + } else { + massageMenu(desc.menu); + } + super({ ...desc, category: AbstractInline2ChatAction.category, @@ -583,15 +612,71 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController2, editor: ICodeEditor, ...args: any[]): void; } -export class StopSessionAction2 extends AbstractInline2ChatAction { +class KeepOrUndoSessionAction extends AbstractInline2ChatAction { + + constructor(id: string, private readonly _keep: boolean) { + super({ + id, + title: _keep + ? localize2('Keep', "Keep") + : localize2('Undo', "Undo"), + f1: true, + icon: _keep ? Codicon.check : Codicon.discard, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, ctxHasRequestInProgress.negate()), + keybinding: [{ + weight: KeybindingWeight.WorkbenchContrib, + primary: _keep + ? KeyMod.CtrlCmd | KeyCode.Enter + : KeyMod.CtrlCmd | KeyCode.Backspace + }], + menu: [{ + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT2, ContextKeyExpr.greater(ctxRequestCount.key, 0)), + }] + }); + } + + override runInlineChatCommand(accessor: ServicesAccessor, _ctrl: InlineChatController2, editor: ICodeEditor, ..._args: any[]): void { + const inlineChatSessions = accessor.get(IInlineChatSessionService); + if (!editor.hasModel()) { + return; + } + const textModel = editor.getModel(); + const session = inlineChatSessions.getSession2(textModel.uri); + if (!session) { + return; + } + if (this._keep) { + session.editingSession.accept(); + } else { + session.editingSession.reject(); + } + } +} + +export class KeepSessionAction2 extends KeepOrUndoSessionAction { + constructor() { + super('inlineChat2.keep', true); + } +} + +export class UndoSessionAction2 extends KeepOrUndoSessionAction { + constructor() { + super('inlineChat2.undo', false); + } +} + +export class CloseSessionAction2 extends AbstractInline2ChatAction { constructor() { super({ - id: 'inlineChat2.stop', - title: localize2('stop', "Undo & Close"), + id: 'inlineChat2.close', + title: localize2('close2', "Close"), f1: true, icon: Codicon.close, - precondition: CTX_INLINE_CHAT_VISIBLE, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, ctxHasRequestInProgress.negate(), ContextKeyExpr.equals(ctxRequestCount.key, 0)), keybinding: [{ when: ctxRequestCount.isEqualTo(0), weight: KeybindingWeight.WorkbenchContrib, @@ -600,21 +685,20 @@ export class StopSessionAction2 extends AbstractInline2ChatAction { weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Escape, }], - menu: { + menu: [{ id: MENU_INLINE_CHAT_SIDE, group: 'navigation', - when: CTX_INLINE_CHAT_HAS_AGENT2 - } + when: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT2, ContextKeyExpr.equals(ctxRequestCount.key, 0)), + }] }); } runInlineChatCommand(accessor: ServicesAccessor, _ctrl: InlineChatController2, editor: ICodeEditor, ...args: any[]): void { const inlineChatSessions = accessor.get(IInlineChatSessionService); - if (!editor.hasModel()) { - return; + if (editor.hasModel()) { + const textModel = editor.getModel(); + inlineChatSessions.getSession2(textModel.uri)?.dispose(); } - const textModel = editor.getModel(); - inlineChatSessions.getSession2(textModel.uri)?.dispose(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 2f86f76b182..6e9c62b6648 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -13,7 +13,7 @@ import { Lazy } from '../../../../base/common/lazy.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; import { MovingAverage } from '../../../../base/common/numbers.js'; -import { autorun, autorunWithStore, derived, IObservable, observableSignalFromEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { assertType } from '../../../../base/common/types.js'; @@ -1232,6 +1232,8 @@ export class InlineChatController2 implements IEditorContribution { @ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService, @IFileService private readonly _fileService: IFileService, @IDialogService private readonly _dialogService: IDialogService, + @IEditorService private readonly _editorService: IEditorService, + @IInlineChatSessionService inlineChatService: IInlineChatSessionService, ) { const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); @@ -1329,40 +1331,44 @@ export class InlineChatController2 implements IEditorContribution { const { chatModel } = session; const showShowUntil = this._showWidgetOverrideObs.read(r); const hasNoRequests = chatModel.getRequests().length === 0; - + const hideOnRequest = inlineChatService.hideOnRequest.read(r); const responseListener = store.add(new MutableDisposable()); - store.add(chatModel.onDidChange(e => { - if (e.kind === 'addRequest') { - transaction(tx => { - this._showWidgetOverrideObs.set(false, tx); - visibleSessionObs.set(undefined, tx); - }); - const { response } = e.request; - if (!response) { - return; - } - responseListener.value = response.onDidChange(async e => { - - if (!response.isComplete) { + if (hideOnRequest) { + // hide the request once the request has been added, reveal it again when no edit was made + // or when an error happened + store.add(chatModel.onDidChange(e => { + if (e.kind === 'addRequest') { + transaction(tx => { + this._showWidgetOverrideObs.set(false, tx); + visibleSessionObs.set(undefined, tx); + }); + const { response } = e.request; + if (!response) { return; } + responseListener.value = response.onDidChange(async e => { - const shouldShow = response.isCanceled // cancelled - || response.result?.errorDetails // errors - || !response.response.value.find(part => part.kind === 'textEditGroup' - && part.edits.length > 0 - && isEqual(part.uri, model.uri)); // NO edits for file + if (!response.isComplete) { + return; + } - if (shouldShow) { - visibleSessionObs.set(session, undefined); - } - }); - } - })); + const shouldShow = response.isCanceled // cancelled + || response.result?.errorDetails // errors + || !response.response.value.find(part => part.kind === 'textEditGroup' + && part.edits.length > 0 + && isEqual(part.uri, model.uri)); // NO edits for file - if (showShowUntil || hasNoRequests) { + if (shouldShow) { + visibleSessionObs.set(session, undefined); + } + }); + } + })); + } + + if (showShowUntil || hasNoRequests || !hideOnRequest) { visibleSessionObs.set(session, undefined); } else { visibleSessionObs.set(undefined, undefined); @@ -1385,7 +1391,23 @@ export class InlineChatController2 implements IEditorContribution { } this._zone.value.reveal(this._zone.value.position!); this._zone.value.widget.focus(); - session.editingSession.getEntry(session.uri)?.autoAcceptController.get()?.cancel(); + this._zone.value.widget.updateToolbar(true); + const entry = session.editingSession.getEntry(session.uri); + + entry?.autoAcceptController.get()?.cancel(); + + const requestCount = observableFromEvent(this, session.chatModel.onDidChange, () => session.chatModel.getRequests().length).read(r); + this._zone.value.widget.updateToolbar(requestCount > 0); + } + })); + + this._store.add(autorun(r => { + + const session = visibleSessionObs.read(r); + const entry = session?.editingSession.readEntry(session.uri, r); + const pane = this._editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this._editor); + if (pane && entry) { + entry?.getEditorIntegration(pane); } })); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index a6e06ad4b36..c20396091f0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -5,6 +5,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Position } from '../../../../editor/common/core/position.js'; @@ -63,6 +64,8 @@ export interface IInlineChatSessionService { dispose(): void; + hideOnRequest: IObservable; + createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise; getSession2(uri: URI): IInlineChatSession2 | undefined; onDidChangeSessions: Event; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 3304aba0a9f..52169f74762 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; -import { autorun, observableFromEvent } from '../../../../base/common/observable.js'; +import { autorun, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; @@ -76,6 +76,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { private readonly _sessions = new Map(); private readonly _keyComputers = new Map(); + readonly hideOnRequest: IObservable; + constructor( @ITelemetryService private readonly _telemetryService: ITelemetryService, @IModelService private readonly _modelService: IModelService, @@ -89,7 +91,10 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { @IChatService private readonly _chatService: IChatService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, - ) { } + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + this.hideOnRequest = observableConfigValue(InlineChatConfigKeys.HideOnRequest, false, this._configurationService); + } dispose() { this._store.dispose(); diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 157690e478e..a3436539e99 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -19,7 +19,8 @@ export const enum InlineChatConfigKeys { AccessibleDiffView = 'inlineChat.accessibleDiffView', LineEmptyHint = 'inlineChat.lineEmptyHint', LineNLHint = 'inlineChat.lineNaturalLanguageHint', - EnableV2 = 'inlineChat.enableV2' + EnableV2 = 'inlineChat.enableV2', + HideOnRequest = 'inlineChat.hideOnRequest' } Registry.as(Extensions.Configuration).registerConfiguration({ @@ -64,6 +65,12 @@ Registry.as(Extensions.Configuration).registerConfigurat type: 'boolean', tags: ['preview', 'onExp'], }, + [InlineChatConfigKeys.HideOnRequest]: { + description: localize('hideOnRequest', "Whether to hide the inline chat widget after making a request. When enabled, the widget hides after a request has been made and instead the chat overlay shows. When hidden, the widget can always be shown again with the inline chat keybinding or from the chat overlay widget."), + default: false, + type: 'boolean', + tags: ['preview', 'onExp'], + }, } }); From cb6da5bcc3312278b7613b38ff96a19f7f7767e6 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Thu, 24 Apr 2025 09:47:50 -0700 Subject: [PATCH 114/114] Make the custom dropdown component much easier to reuse (#247321) * Work on making action widget dropdown more reusable * Make the custom dropdown significantly easier to reuse --- src/vs/base/browser/ui/dropdown/dropdown.ts | 2 +- .../ui/dropdown/dropdownActionViewItem.ts | 89 +-------- .../browser/actionWidgetDropdown.ts | 106 +++++++++++ .../actionWidgetDropdownActionViewItem.ts | 88 +++++++++ .../contrib/chat/browser/chatInputPart.ts | 1 + .../modelPicker/modelPickerActionItem.ts | 98 +++++++--- .../browser/modelPicker/modelPickerWidget.ts | 177 ------------------ 7 files changed, 271 insertions(+), 290 deletions(-) create mode 100644 src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts create mode 100644 src/vs/platform/actions/browser/actionWidgetDropdownActionViewItem.ts delete mode 100644 src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 67a53c3b0d4..4bb319918f3 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -22,7 +22,7 @@ export interface ILabelRenderer { (container: HTMLElement): IDisposable | null; } -interface IBaseDropdownOptions { +export interface IBaseDropdownOptions { label?: string; labelRenderer?: ILabelRenderer; } diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 97ec7fef7cc..89a3bd7b4f3 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -20,7 +20,7 @@ import { AnchorAlignment } from '../contextview/contextview.js'; import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js'; import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js'; import './dropdown.css'; -import { BaseDropdown, DropdownMenu, IActionProvider, IDropdownMenuOptions, ILabelRenderer } from './dropdown.js'; +import { DropdownMenu, IActionProvider, IDropdownMenuOptions, ILabelRenderer } from './dropdown.js'; export interface IKeybindingProvider { (action: IAction): ResolvedKeybinding | undefined; @@ -40,93 +40,6 @@ export interface IDropdownMenuActionViewItemOptions extends IBaseActionViewItemO readonly skipTelemetry?: boolean; } -export class BaseDropdownMenuActionViewItem extends BaseActionViewItem { - private dropdownMenu: BaseDropdown | undefined; - private actionItem: HTMLElement | null = null; - - private _onDidChangeVisibility = this._register(new Emitter()); - readonly onDidChangeVisibility = this._onDidChangeVisibility.event; - - protected override readonly options: IDropdownMenuActionViewItemOptions = Object.create(null); - - constructor( - action: IAction, - private readonly _dropdownFactory: (container: HTMLElement, labelRender: ILabelRenderer) => BaseDropdown, - ) { - super(null, action); - } - - override render(container: HTMLElement): void { - this.actionItem = container; - - const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => { - this.element = append(el, $('a.action-label')); - return this.renderLabel(this.element); - }; - - this.dropdownMenu = this._register(this._dropdownFactory(container, labelRenderer)); - this._register(this.dropdownMenu.onDidChangeVisibility(visible => { - this.element?.setAttribute('aria-expanded', `${visible}`); - this._onDidChangeVisibility.fire(visible); - })); - - this.updateTooltip(); - this.updateEnabled(); - } - - protected renderLabel(element: HTMLElement): IDisposable | null { - let classNames: string[] = []; - - if (typeof this.options.classNames === 'string') { - classNames = this.options.classNames.split(/\s+/g).filter(s => !!s); - } else if (this.options.classNames) { - classNames = this.options.classNames; - } - - // todo@aeschli: remove codicon, should come through `this.options.classNames` - if (!classNames.find(c => c === 'icon')) { - classNames.push('codicon'); - } - - element.classList.add(...classNames); - - if (this._action.label) { - this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), element, this._action.label)); - } - - return null; - } - - protected setAriaLabelAttributes(element: HTMLElement): void { - element.setAttribute('role', 'button'); - element.setAttribute('aria-haspopup', 'true'); - element.setAttribute('aria-expanded', 'false'); - element.ariaLabel = this._action.label || ''; - } - - protected override getTooltip(): string | undefined { - let title: string | null = null; - - if (this.action.tooltip) { - title = this.action.tooltip; - } else if (this.action.label) { - title = this.action.label; - } - - return title ?? undefined; - } - - show(): void { - this.dropdownMenu?.show(); - } - - protected override updateEnabled(): void { - const disabled = !this.action.enabled; - this.actionItem?.classList.toggle('disabled', disabled); - this.element?.classList.toggle('disabled', disabled); - } -} - export class DropdownMenuActionViewItem extends BaseActionViewItem { private menuActionsOrProvider: readonly IAction[] | IActionProvider; private dropdownMenu: DropdownMenu | undefined; diff --git a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts new file mode 100644 index 00000000000..a58463cf916 --- /dev/null +++ b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionWidgetService } from './actionWidget.js'; +import { IAction } from '../../../base/common/actions.js'; +import { BaseDropdown, IActionProvider, IBaseDropdownOptions } from '../../../base/browser/ui/dropdown/dropdown.js'; +import { ActionListItemKind, IActionListDelegate, IActionListItem } from './actionList.js'; +import { ThemeIcon } from '../../../base/common/themables.js'; +import { Codicon } from '../../../base/common/codicons.js'; + +export interface IActionWidgetDropdownAction extends IAction { + category?: { label: string; order: number }; + description?: string; +} + +// TODO @lramos15 - Should we just make IActionProvider templated? +export interface IActionWidgetDropdownActionProvider { + getActions(): IActionWidgetDropdownAction[]; +} + +export interface IActionWidgetDropdownOptions extends IBaseDropdownOptions { + // These are the actions that are shown in the action widget split up by category + readonly actions?: IActionWidgetDropdownAction[]; + readonly actionProvider?: IActionWidgetDropdownActionProvider; + + // These actions are those shown at the bottom of the action widget + readonly actionBarActions?: IAction[]; + readonly actionBarActionProvider?: IActionProvider; +} + +/** + * Action widget dropdown is a dropdown that uses the action widget under the hood to simulate a native dropdown menu + * The benefits of this include non native features such as headers, descriptions, icons, and button bar + */ +export class ActionWidgetDropdown extends BaseDropdown { + constructor( + container: HTMLElement, + private readonly _options: IActionWidgetDropdownOptions, + @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, + ) { + super(container, _options); + } + + override show(): void { + const actionBarActions = this._options.actionBarActions ?? this._options.actionBarActionProvider?.getActions() ?? []; + const actions = this._options.actions ?? this._options.actionProvider?.getActions() ?? []; + const actionWidgetItems: IActionListItem[] = []; + + const actionsByCategory = new Map(); + for (const action of actions) { + let category = action.category; + if (!category) { + category = { label: '', order: Number.MAX_SAFE_INTEGER }; + } + if (!actionsByCategory.has(category.label)) { + actionsByCategory.set(category.label, []); + } + actionsByCategory.get(category.label)!.push(action); + } + + for (const [categoryLabel, categoryActions] of actionsByCategory) { + // Push headers for each category + actionWidgetItems.push({ + label: categoryLabel, + kind: ActionListItemKind.Header, + canPreview: false, + disabled: false, + hideIcon: false, + }); + + // Push actions for each category + for (const action of categoryActions) { + actionWidgetItems.push({ + item: action, + tooltip: action.tooltip, + description: action.description, + kind: ActionListItemKind.Action, + canPreview: false, + group: { title: '', icon: ThemeIcon.fromId(action.checked ? Codicon.check.id : Codicon.blank.id) }, + disabled: false, + hideIcon: false, + label: action.label, + }); + } + } + + const actionWidgetDelegate: IActionListDelegate = { + onSelect(action, preview) { + action.run(); + }, + onHide: () => { } + }; + + this.actionWidgetService.show( + this._options.label ?? '', + false, + actionWidgetItems, + actionWidgetDelegate, + this.element, + undefined, + actionBarActions + ); + } +} diff --git a/src/vs/platform/actions/browser/actionWidgetDropdownActionViewItem.ts b/src/vs/platform/actions/browser/actionWidgetDropdownActionViewItem.ts new file mode 100644 index 00000000000..6358114fec5 --- /dev/null +++ b/src/vs/platform/actions/browser/actionWidgetDropdownActionViewItem.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, append } from '../../../base/browser/dom.js'; +import { BaseActionViewItem } from '../../../base/browser/ui/actionbar/actionViewItems.js'; +import { ILabelRenderer } from '../../../base/browser/ui/dropdown/dropdown.js'; +import { getBaseLayerHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegate2.js'; +import { getDefaultHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { IAction } from '../../../base/common/actions.js'; +import { IDisposable } from '../../../base/common/lifecycle.js'; +import { IActionWidgetService } from '../../actionWidget/browser/actionWidget.js'; +import { ActionWidgetDropdown, IActionWidgetDropdownOptions } from '../../actionWidget/browser/actionWidgetDropdown.js'; + +/** + * Action view item for the custom action widget dropdown widget. + * Very closely based off of `DropdownMenuActionViewItem`, would be good to have some code re-use in the future + */ +export class ActionWidgetDropdownActionViewItem extends BaseActionViewItem { + private actionWidgetDropdown: ActionWidgetDropdown | undefined; + private actionItem: HTMLElement | null = null; + constructor( + action: IAction, + private readonly actionWidgetOptions: Omit, + @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService + ) { + super(undefined, action); + } + + override render(container: HTMLElement): void { + this.actionItem = container; + + const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => { + this.element = append(el, $('a.action-label')); + return this.renderLabel(this.element); + }; + + this.actionWidgetDropdown = this._register(new ActionWidgetDropdown(container, { ...this.actionWidgetOptions, labelRenderer }, this._actionWidgetService)); + this._register(this.actionWidgetDropdown.onDidChangeVisibility(visible => { + this.element?.setAttribute('aria-expanded', `${visible}`); + })); + + this.updateTooltip(); + this.updateEnabled(); + } + + protected renderLabel(element: HTMLElement): IDisposable | null { + // todo@aeschli: remove codicon, should come through `this.options.classNames` + element.classList.add('codicon'); + + if (this._action.label) { + this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), element, this._action.label)); + } + + return null; + } + + protected setAriaLabelAttributes(element: HTMLElement): void { + element.setAttribute('role', 'button'); + element.setAttribute('aria-haspopup', 'true'); + element.setAttribute('aria-expanded', 'false'); + element.ariaLabel = this._action.label || ''; + } + + protected override getTooltip(): string | undefined { + let title: string | null = null; + + if (this.action.tooltip) { + title = this.action.tooltip; + } else if (this.action.label) { + title = this.action.label; + } + + return title ?? undefined; + } + + show(): void { + this.actionWidgetDropdown?.show(); + } + + protected override updateEnabled(): void { + const disabled = !this.action.enabled; + this.actionItem?.classList.toggle('disabled', disabled); + this.element?.classList.toggle('disabled', disabled); + } + +} diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 86362e1c08b..32130d6f991 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -1036,6 +1036,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (this._currentLanguageModel) { const itemDelegate: IModelPickerDelegate = { + getCurrentModel: () => this._currentLanguageModel, onDidChangeModel: this._onDidChangeCurrentLanguageModel.event, setModel: (model: ILanguageModelChatMetadataAndIdentifier) => { // The user changed the language model, so we don't wait for the persisted option to be registered diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts index 4a29ae68b39..ca170e5c311 100644 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts @@ -7,28 +7,90 @@ import { IAction } from '../../../../../base/common/actions.js'; import { Event } from '../../../../../base/common/event.js'; import { ILanguageModelChatMetadataAndIdentifier } from '../../common/languageModels.js'; import { localize } from '../../../../../nls.js'; -import { ModelPickerWidget } from './modelPickerWidget.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import * as dom from '../../../../../base/browser/dom.js'; import { renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { BaseDropdownMenuActionViewItem } from '../../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { ActionWidgetDropdownActionViewItem } from '../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; +import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { getFlatActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { ChatEntitlement, IChatEntitlementService } from '../../common/chatEntitlementService.js'; export interface IModelPickerDelegate { readonly onDidChangeModel: Event; + getCurrentModel(): ILanguageModelChatMetadataAndIdentifier | undefined; setModel(model: ILanguageModelChatMetadataAndIdentifier): void; getModels(): ILanguageModelChatMetadataAndIdentifier[]; } +function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate): IActionWidgetDropdownActionProvider { + return { + getActions: () => { + return delegate.getModels().map(model => { + return { + id: model.metadata.id, + enabled: true, + checked: model.metadata.id === delegate.getCurrentModel()?.metadata.id, + category: { ...model.metadata.modelPickerCategory, order: 0 }, + class: undefined, + description: model.metadata.description, + tooltip: model.metadata.name, + label: model.metadata.name, + run: () => { + delegate.setModel(model); + } + } satisfies IActionWidgetDropdownAction; + }); + } + }; +} + +function getModelPickerActionBarActions(menuService: IMenuService, contextKeyService: IContextKeyService, commandService: ICommandService, chatEntitlementService: IChatEntitlementService): IAction[] { + const menuActions = menuService.createMenu(MenuId.ChatModelPicker, contextKeyService); + const menuContributions = getFlatActionBarActions(menuActions.getActions()); + menuActions.dispose(); + + const additionalActions: IAction[] = []; + + // Add menu contributions from extensions + if (menuContributions.length > 0) { + additionalActions.push(...menuContributions); + } + + // Add upgrade option if entitlement is limited + if (chatEntitlementService.entitlement === ChatEntitlement.Limited) { + additionalActions.push({ + id: 'moreModels', + label: localize('chat.moreModels', "Add Premium Models"), + enabled: true, + tooltip: localize('chat.moreModels.tooltip', "Add premium models"), + class: undefined, + run: () => { + const commandId = 'workbench.action.chat.upgradePlan'; + commandService.executeCommand(commandId); + } + }); + } + + return additionalActions; +} + /** * Action view item for selecting a language model in the chat interface. */ -export class ModelPickerActionItem extends BaseDropdownMenuActionViewItem { +export class ModelPickerActionItem extends ActionWidgetDropdownActionViewItem { constructor( action: IAction, private currentModel: ILanguageModelChatMetadataAndIdentifier, - private readonly delegate: IModelPickerDelegate, - @IInstantiationService private readonly instantiationService: IInstantiationService, + delegate: IModelPickerDelegate, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService ) { // Modify the original action with a different label and make it show the current model const actionWithLabel: IAction = { @@ -38,24 +100,12 @@ export class ModelPickerActionItem extends BaseDropdownMenuActionViewItem { run: () => { } }; - super(actionWithLabel, (container, labelRenderer) => { - const widget = this.instantiationService.createInstance( - ModelPickerWidget, - container, - labelRenderer, - () => this.currentModel, - () => this.delegate.getModels(), - (model) => this.delegate.setModel(model) - ); - this._register(widget); - this._register(widget.onDidChangeModel(model => { - this.currentModel = model; - if (this.element) { - this.renderLabel(this.element); - } - })); - return widget; - }); + const modelPickerActionWidgetOptions: Omit = { + actionProvider: modelDelegateToWidgetActionsProvider(delegate), + actionBarActions: getModelPickerActionBarActions(menuService, contextKeyService, commandService, chatEntitlementService) + }; + + super(actionWithLabel, modelPickerActionWidgetOptions, actionWidgetService); // Listen for model changes from the delegate this._register(delegate.onDidChangeModel(model => { diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts deleted file mode 100644 index 36b8a01be08..00000000000 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts +++ /dev/null @@ -1,177 +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 { IAction } from '../../../../../base/common/actions.js'; -import { Emitter } from '../../../../../base/common/event.js'; -import { ILanguageModelChatMetadataAndIdentifier } from '../../common/languageModels.js'; -import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; -import { localize } from '../../../../../nls.js'; -import { MenuId, IMenuService } from '../../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IChatEntitlementService, ChatEntitlement } from '../../common/chatEntitlementService.js'; -import { getFlatActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { ActionListItemKind, IActionListItem } from '../../../../../platform/actionWidget/browser/actionList.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { BaseDropdown, ILabelRenderer } from '../../../../../base/browser/ui/dropdown/dropdown.js'; - -interface IModelPickerActionItem { - model: ILanguageModelChatMetadataAndIdentifier; - isCurrent: boolean; -} - -/** - * Widget for picking a language model for chat. - */ -export class ModelPickerWidget extends BaseDropdown { - private readonly _onDidChangeModel = this._register(new Emitter()); - readonly onDidChangeModel = this._onDidChangeModel.event; - - constructor( - container: HTMLElement, - labelRenderer: ILabelRenderer, - private readonly getCurrentModel: () => ILanguageModelChatMetadataAndIdentifier, - private readonly getModels: () => ILanguageModelChatMetadataAndIdentifier[], - private readonly setModel: (model: ILanguageModelChatMetadataAndIdentifier) => void, - @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, - @IMenuService private readonly menuService: IMenuService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, - @ICommandService private readonly commandService: ICommandService, - ) { - super(container, { labelRenderer }); - } - - /** - * Convert available models to action items for display - */ - private getActionItems(): IModelPickerActionItem[] { - const items: IModelPickerActionItem[] = this.getModels().map(model => ({ - model, - isCurrent: model.identifier === this.getCurrentModel().identifier - })); - - return items; - } - - /** - * Get any additional actions to add to the picker menu - */ - private getAdditionalActions(): IAction[] { - const menuActions = this.menuService.createMenu(MenuId.ChatModelPicker, this.contextKeyService); - const menuContributions = getFlatActionBarActions(menuActions.getActions()); - menuActions.dispose(); - - const additionalActions: IAction[] = []; - - // Add menu contributions from extensions - if (menuContributions.length > 0) { - additionalActions.push(...menuContributions); - } - - // Add upgrade option if entitlement is limited - if (this.chatEntitlementService.entitlement === ChatEntitlement.Limited) { - additionalActions.push({ - id: 'moreModels', - label: localize('chat.moreModels', "Add Premium Models"), - enabled: true, - tooltip: localize('chat.moreModels.tooltip', "Add premium models"), - class: undefined, - run: () => { - const commandId = 'workbench.action.chat.upgradePlan'; - this.commandService.executeCommand(commandId); - } - }); - } - - return additionalActions; - } - - /** - * Shows the picker at the specified anchor - */ - override show(): void { - const actionItems = this.getActionItems(); - const items: IActionListItem[] = []; - - // Group models by categories - const modelsByCategory = new Map(); - - // First, group models by their categories - for (const item of actionItems) { - const category = item.model.metadata.modelPickerCategory; - if (!modelsByCategory.has(category.label)) { - modelsByCategory.set(category.label, []); - } - modelsByCategory.get(category.label)!.push(item); - } - - for (const [categoryLabel, modelsInCategory] of modelsByCategory.entries()) { - // Skip empty categories - if (modelsInCategory.length === 0) { - continue; - } - - // Add category header - items.push({ - label: categoryLabel, - kind: ActionListItemKind.Header, - canPreview: false, - disabled: false, - hideIcon: true, - } satisfies IActionListItem); - - // Add models in this category - for (const item of modelsInCategory) { - items.push({ - item: item.model, - description: item.model.metadata.description, - kind: ActionListItemKind.Action, - canPreview: false, - group: { title: '', icon: ThemeIcon.fromId(item.isCurrent ? Codicon.check.id : Codicon.blank.id) }, - disabled: false, - hideIcon: false, - label: item.model.metadata.name, - } satisfies IActionListItem); - } - - // Remove this category from the map so we don't process it again - modelsByCategory.delete(categoryLabel); - } - - const delegate = { - onSelect: (item: ILanguageModelChatMetadataAndIdentifier) => { - if (item.identifier !== this.getCurrentModel().identifier) { - this.setModel(item); - this._onDidChangeModel.fire(item); - } - this.actionWidgetService.hide(false); - return true; - }, - onHide: () => { }, - getWidgetAriaLabel: () => localize('modelPicker', "Model Picker") - }; - - // Get additional actions to show in the picker - const additionalActions = this.getAdditionalActions(); - let buttonBar: IAction[] = []; - - // If we have additional actions, add them to the button bar - if (additionalActions.length > 0) { - buttonBar = additionalActions; - } - - this.actionWidgetService.show( - 'modelPicker', - false, - items, - delegate, - this.element, - undefined, - buttonBar - ); - } -}