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, 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, 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) 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/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/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/extensions/github/extension.webpack.config.js b/extensions/github/extension.webpack.config.cjs similarity index 77% rename from extensions/github/extension.webpack.config.js rename to extensions/github/extension.webpack.config.cjs index 45600607fc5..75b86c82b68 100644 --- a/extensions/github/extension.webpack.config.js +++ b/extensions/github/extension.webpack.config.cjs @@ -13,5 +13,15 @@ module.exports = withDefaults({ context: __dirname, entry: { extension: './src/extension.ts' + }, + output: { + libraryTarget: 'module', + chunkFormat: 'module', + }, + externals: { + 'vscode': 'module vscode', + }, + 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" ] 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", 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": { 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" 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..2547b30e88c 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -231,6 +231,7 @@ "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.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 3012658036f..6c42d642f0f 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.v590)) { + 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/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); 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", ] } diff --git a/extensions/yaml/package.json b/extensions/yaml/package.json index 486458e8ad6..66f0a200d08 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": "on" + } }, "[dockercompose]": { "editor.insertSpaces": true, 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" }, diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index b4a7d38a6fb..4bb319918f3 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -22,12 +22,12 @@ export interface ILabelRenderer { (container: HTMLElement): IDisposable | null; } -interface IBaseDropdownOptions { +export interface IBaseDropdownOptions { label?: string; 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/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/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts index 959a99ea366..a47deb89b84 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; @@ -43,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(); @@ -70,7 +70,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/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/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/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/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 { 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/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 1636bf4db20..dbd47652aa6 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 @@ -33,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', @@ -62,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', }, @@ -395,6 +395,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/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/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 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/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}`; 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/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/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 103aface9f7..3d0c074438b 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 { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolResult, toolResultHasBuffers } from '../../contrib/chat/common/languageModelToolsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolProgressStep, IToolResult, ToolProgress, toolResultHasBuffers, IToolProgressStep } from '../../contrib/chat/common/languageModelToolsService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; import { ExtHostContext, ExtHostLanguageModelToolsShape, MainContext, MainThreadLanguageModelToolsShape } from '../common/extHost.protocol.js'; @@ -16,7 +16,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, @@ -44,27 +47,31 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre return toolResultHasBuffers(result) ? new SerializableObjectWithBuffers(out) : out; } + $acceptToolProgress(callId: string, progress: IToolProgressStep): 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, token) => { + invoke: async (dto, countTokens, progress, token) => { try { this._countTokenCallbacks.set(dto.callId, countTokens); const resultSerialized = await this._proxy.$invokeTool(dto, token); const resultDto: Dto = resultSerialized instanceof SerializableObjectWithBuffers ? resultSerialized.value : resultSerialized; return revive(resultDto); } 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/browser/mainThreadQuickDiff.ts b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts index 1db282c0abe..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, visible: boolean): 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, 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/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/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 403e940caa1..71f70cb8489 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)); @@ -917,9 +919,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; @@ -1438,10 +1440,14 @@ 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); } }; - // namespace: chat + // namespace: chatregisterMcpServerDefinitionProvider const chat: typeof vscode.chat = { registerMappedEditsProvider(_selector: vscode.DocumentSelector, _provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); @@ -1521,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) { @@ -1781,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, @@ -1819,7 +1829,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I LanguageModelToolResult2: extHostTypes.LanguageModelToolResult2, LanguageModelDataPart: extHostTypes.LanguageModelDataPart, LanguageModelExtraDataPart: extHostTypes.LanguageModelExtraDataPart, - ChatImageMimeType: extHostTypes.ChatImageMimeType, ExtendedLanguageModelToolResult: extHostTypes.ExtendedLanguageModelToolResult, PreparedTerminalToolInvocation: extHostTypes.PreparedTerminalToolInvocation, LanguageModelChatToolMode: extHostTypes.LanguageModelChatToolMode, @@ -1837,6 +1846,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 fd2935f5864..cd387657765 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'; @@ -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'; @@ -1380,6 +1381,7 @@ export type IToolDataDto = Omit; export interface MainThreadLanguageModelToolsShape extends IDisposable { $getTools(): Promise[]>; + $acceptToolProgress(callId: string, progress: IToolProgressStep): void; $invokeTool(dto: IToolInvocation, token?: CancellationToken): Promise | SerializableObjectWithBuffers>>; $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise; $registerTool(id: string): void; @@ -1669,7 +1671,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[], id: string, label: string, rootUri: UriComponents | undefined): Promise; $unregisterQuickDiffProvider(handle: number): Promise; } @@ -1985,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; } @@ -3154,6 +3166,7 @@ export const MainContext = { MainThreadAiRelatedInformation: createProxyIdentifier('MainThreadAiRelatedInformation'), MainThreadAiEmbeddingVector: createProxyIdentifier('MainThreadAiEmbeddingVector'), MainThreadChatStatus: createProxyIdentifier('MainThreadChatStatus'), + MainThreadAiSettingsSearch: createProxyIdentifier('MainThreadAiSettingsSearch'), }; export const ExtHostContext = { @@ -3216,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/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/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/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/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; diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 05101ffeb99..67eeb33856f 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -140,7 +140,21 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } - const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token)), token); + 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: typeConvert.MarkdownString.fromStrict(value.message), + increment: value.increment, + total: 100, + }); + } + }; + } + + // 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/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 3885d97ca22..3b25b4cd08e 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -26,6 +26,7 @@ import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; import { VSBuffer } from '../../../base/common/buffer.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../contrib/chat/common/modelPicker/modelPickerWidget.js'; export interface IExtHostLanguageModels extends ExtHostLanguageModels { } @@ -184,6 +185,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/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index e8ccfc48f4d..4c54d7dfe1f 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); @@ -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) }); } @@ -124,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 => { @@ -213,7 +218,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, @@ -239,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; } @@ -312,7 +326,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, @@ -355,7 +369,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: { @@ -376,7 +390,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()); } }); 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/api/common/extHostQuickDiff.ts b/src/vs/workbench/api/common/extHostQuickDiff.ts index 2b04fcd6222..b17b67c6a3d 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 { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; export class ExtHostQuickDiff implements ExtHostQuickDiffShape { private static handlePool: number = 0; @@ -36,10 +37,12 @@ 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, quickDiffProvider.visible ?? true); + + 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/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 1a633aa71cc..f90ed516f12 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 { LanguageModelDataPart, 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 { @@ -2424,12 +2425,7 @@ export namespace LanguageModelChatMessage2 { }); return new types.LanguageModelToolResultPart2(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 { @@ -2484,8 +2480,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 { @@ -2907,7 +2903,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 = { @@ -2924,7 +2920,7 @@ export namespace ChatAgentRequest { rejectedConfirmationData: request.rejectedConfirmationData, location2, toolInvocationToken: Object.freeze({ sessionId: request.sessionId }) as never, - tools, + toolSelection, model, editedFileEvents: request.editedFileEvents, }; @@ -3344,6 +3340,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 a44b634aef8..4590e039171 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4958,20 +4958,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; @@ -4990,21 +5014,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; @@ -5132,6 +5141,12 @@ export enum RelatedInformationType { SettingInformation = 4 } +export enum SettingsSearchResultKind { + EMBEDDED = 1, + LLM_RANKED = 2, + CANCELED = 3, +} + //#endregion //#region Speech @@ -5181,7 +5196,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/api/node/extHostMcpNode.ts b/src/vs/workbench/api/node/extHostMcpNode.ts index 594fe45d0be..16e848a8d57 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( @@ -57,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, }); @@ -80,8 +82,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', diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 409a37c512c..21be375a627 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'; @@ -61,6 +62,7 @@ interface ICodeBlock { endLine: number; code: string; languageId?: string; + chatSessionId: string | undefined; } export class AccessibleView extends Disposable implements ITextModelContentProvider { @@ -109,7 +111,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 +188,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); } @@ -239,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 { @@ -383,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/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/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() { 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/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/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/actions/chatToolActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts index b70f7e04a70..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'; @@ -93,9 +95,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)), @@ -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; 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/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/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/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/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/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/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/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/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..9303390e793 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'; @@ -289,7 +290,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 +301,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 +413,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 +425,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()); @@ -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.toolInvocation.kind === 'toolInvocation' ? this.toolInvocation.progress : undefined; + this._register(autorunWithStore((reader, store) => { + const progress = progressObservable?.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 { @@ -533,7 +548,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/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/chatEditingEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts index 8074b03af83..355b63eca0a 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'; @@ -261,13 +264,13 @@ 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))!, 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) }] }); } @@ -295,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: { @@ -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..bfcdefb385d 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); })); } @@ -401,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/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index e6d83e24c38..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' ); @@ -507,6 +499,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/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; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index cb5b7ea96e0..2df60310898 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -258,24 +258,28 @@ 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 (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 +508,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 +522,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie this._stateObs.set(redoState, undefined); this.updateCellDiffInfo([], undefined); this.initializeModelsFromDiff(); + this._notifyAction('userModified'); } }; } @@ -679,6 +685,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'); } } 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..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(); @@ -250,7 +252,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(() => { @@ -286,10 +288,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')); + } })); } @@ -393,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; @@ -421,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)); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index d33d5a96808..32130d6f991 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'; @@ -79,7 +77,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'; @@ -163,7 +161,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 @@ -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); @@ -477,6 +485,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) { @@ -891,7 +907,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), + ); + this._register(this._implicitContext.onDidChangeValue(() => this._handleAttachedContextChange())); } @@ -1017,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 @@ -1121,6 +1141,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; @@ -1191,16 +1212,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(); } 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; } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 7b15fa89f3f..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'; @@ -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, 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/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 40d3f741b27..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 { @@ -1178,52 +1185,13 @@ 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) { - input = result.input; - } - - if (result.attachment) { - 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); - } - } + input = await this._handlePromptSlashCommand(input, attachedContext); + this.setupChatModeAndTools(attachedContext.filter(isPromptFileChatVariable)); } if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentMode === ChatMode.Edit && !this.chatService.edits2Enabled) { @@ -1269,7 +1237,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) { @@ -1484,11 +1452,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 +1544,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 +1560,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; diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 399a2f9e36f..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); } @@ -704,7 +709,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 = ''; } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index f601d70bedb..1e9f6118492 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 })) || {}; @@ -347,23 +369,26 @@ export async function searchFolders( shouldGlobMatchFilePattern: true, cacheKey, excludePattern: searchExcludePattern, + sortByScore: true, }; - 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/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index 20da77d613a..9b532a8c7b4 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -9,12 +9,13 @@ 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 { 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'; @@ -25,8 +26,9 @@ 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 { IPromptsService, TSharedPrompt } 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'; @@ -208,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; } @@ -235,20 +240,17 @@ export class ChatImplicitContextContribution extends Disposable implements IWork } export class ChatImplicitContext extends Disposable implements IChatRequestImplicitVariableEntry { + /** + * If the implicit context references a prompt file, this field + * holds a reference to an associated prompt parser instance. + */ + private prompt: TSharedPrompt | 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 +267,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 +285,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 +315,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,25 +325,97 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli this._onDidChangeValue.fire(); } - constructor(value?: Location | URI) { + constructor( + @IPromptsService private readonly promptsService: IPromptsService, + @IModelService private readonly modelService: IModelService, + @ILogService private readonly logService: ILogService, + ) { super(); - this._value = value; } 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 { - return { - kind: 'file', - id: this.id, - name: this.name, - value: this.value, - modelDescription: this.modelDescription - }; + 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, + }]; + + } + + // 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) => { + 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), + ]; + } + + /** + * Whether the implicit context references a prompt file. + */ + public get isPromptFile() { + return (this.prompt !== undefined); + } + + /** + * Add prompt parser instance for the provided value. + */ + private addPrompt( + value: URI | Location, + ): void { + const uri = URI.isUri(value) + ? value + : value.uri; + + const model = this.modelService.getModel(uri); + 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); + } + + /** + * Remove and dispose prompt parser instance. + */ + private removePrompt(): void { + delete this.prompt; + } + + public override dispose(): void { + this.removePrompt(); + super.dispose(); } } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 41da2f77280..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, searchFolders } 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)); } } } @@ -692,8 +668,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; } @@ -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,89 +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 = await Promise.all(workspaces.map(workspace => searchFolders(workspace, pattern, true, token, cacheKey.key, this.configurationService, this.searchService))); - for (const resource of folders.flat()) { - if (seen.has(resource)) { - // already included via history - continue; - } - seen.add(resource); - result.suggestions.push(makeFolderCompletionItem(resource)); } } diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 10437007c67..510c8e92f39 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'; @@ -40,6 +40,11 @@ interface IToolEntry { impl?: IToolImpl; } +interface ITrackedCall { + invocation?: ChatToolInvocation; + store: IDisposable; +} + export class LanguageModelToolsService extends Disposable implements ILanguageModelToolsService { _serviceBrand: undefined; @@ -53,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; @@ -235,7 +240,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: ITrackedCall = { store }; + this._callsByRequestId.get(requestId)!.push(trackedCall); const source = new CancellationTokenSource(); store.add(toDisposable(() => { @@ -252,6 +258,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 +292,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( @@ -406,7 +417,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); } @@ -420,7 +431,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); } } @@ -428,7 +439,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/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/chat/browser/modelPicker/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts index 25b0c02279c..ca170e5c311 100644 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts @@ -7,116 +7,123 @@ 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 { 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 { 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 ActionViewItem { - private widget: ModelPickerWidget | undefined; - +export class ModelPickerActionItem extends ActionWidgetDropdownActionViewItem { constructor( action: IAction, - private readonly currentModel: ILanguageModelChatMetadataAndIdentifier, - private readonly delegate: IModelPickerDelegate, - @IInstantiationService private readonly instantiationService: IInstantiationService, + private currentModel: ILanguageModelChatMetadataAndIdentifier, + 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 = { ...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 }); + 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 => { - 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 deleted file mode 100644 index 3bec3dc0419..00000000000 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts +++ /dev/null @@ -1,148 +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 { 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'; -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 { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; - -interface IModelPickerActionItem { - model: ILanguageModelChatMetadataAndIdentifier; - isCurrent: boolean; -} - -/** - * Widget for picking a language model for chat. - */ -export class ModelPickerWidget extends Disposable { - private readonly _onDidChangeModel = this._register(new Emitter()); - readonly onDidChangeModel = this._onDidChangeModel.event; - - constructor( - private currentModel: 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(); - } - - /** - * Get the label to display in the button that shows the current model - */ - get buttonLabel(): string { - return this.currentModel.metadata.name; - } - - /** - * Convert available models to action items for display - */ - private getActionItems(): IModelPickerActionItem[] { - const items: IModelPickerActionItem[] = this.getModels().map(model => ({ - model, - isCurrent: model.identifier === this.currentModel.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 - */ - 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 delegate = { - onSelect: (item: ILanguageModelChatMetadataAndIdentifier) => { - if (item.identifier !== this.currentModel.identifier) { - this.setModel(item); - this.currentModel = 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, - anchor, - container, - buttonBar - ); - } -} 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?", ), [ { 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/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/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 7aedf0ee4c6..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 { @@ -1449,22 +1450,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 - // }); - // } - // } - // }); - // })); }); } diff --git a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts index ee387ceaa1f..b9b27e255f4 100644 --- a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts @@ -5,9 +5,10 @@ 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 { 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'; @@ -45,6 +46,8 @@ export class ChatToolInvocation implements IChatToolInvocation { public readonly toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData; + 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}"`); const invocationMessage = preparedInvocation?.invocationMessage ?? defaultMessage; @@ -84,6 +87,14 @@ export class ChatToolInvocation implements IChatToolInvocation { return this._confirmationMessages; } + public acceptProgress(step: IToolProgressStep) { + 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 ec1d6c7b97e..4a2512cf13e 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 | IMarkdownString; progress: number }>; readonly toolId: string; readonly toolCallId: string; @@ -464,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 0b8627d26cf..a55f2ca360e 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); @@ -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; }; @@ -777,7 +778,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 +796,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; diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index c6c5ab3ca15..0ea932c73fb 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 } 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'; import { VSBuffer } from '../../../../base/common/buffer.js'; @@ -41,6 +42,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'; @@ -147,7 +156,7 @@ export interface IPreparedToolInvocation { } export interface IToolImpl { - invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, 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/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index bbec0440163..1649d902cbc 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -133,6 +133,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/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/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index dac5234b67f..f87b2d79e7c 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. @@ -492,29 +493,44 @@ 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 {}; + return { + mode: ChatMode.Ask, + }; } const { metadata } = this.header; if (metadata === undefined) { - return {}; + return { + mode: ChatMode.Ask, + }; } - const result: IPromptMetadata = {}; - const { tools, description } = metadata; - if (tools !== undefined) { - result.tools = tools.toolNames; - } + const { tools, mode, description } = metadata; + + // 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; } @@ -526,13 +542,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/common/promptSyntax/parsers/promptHeader/header.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts index 5e6f4829681..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,14 +3,16 @@ * 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 { PromptToolsMetadata } from './metadata/tools.js'; -import { PromptDescriptionMetadata } from './metadata/description.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'; 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 +29,11 @@ export interface IHeaderMetadata { * Description metadata in the prompt header. */ description?: PromptDescriptionMetadata; + + /** + * Chat mode metadata in the prompt header. + */ + mode?: PromptModeMetadata; } /** @@ -46,7 +53,9 @@ export class PromptHeader extends Disposable { * Metadata records. */ public get metadata(): Readonly { - return this.meta; + return Object.freeze({ + ...this.meta, + }); } /** @@ -132,19 +141,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)) { @@ -157,6 +153,32 @@ 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.validateToolsAndModeCompatibility(); + } + + // 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 this.validateToolsAndModeCompatibility(); + } + // all other records are currently not supported this.issues.push( new PromptMetadataWarning( @@ -170,6 +192,69 @@ export class PromptHeader extends Disposable { ); } + /** + * Check if value of `tools` and `mode` metadata + * are compatible with each other. + */ + private get 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; + } + + /** + * Validate that the `tools` and `mode` metadata are compatible + * with each other. If not, add a warning diagnostic. + */ + private validateToolsAndModeCompatibility(): void { + if (this.toolsAndModeCompatible === true) { + return; + } + + const { tools, mode } = this.meta; + + // sanity checks on the behavior of the `toolsAndModeCompatible` getter + 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', + "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, + ), + ), + ); + } + /** * 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..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 @@ -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. @@ -78,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/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'; 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..f2f66370d62 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + public override get recordName(): string { + return RECORD_NAME; + } + + /** + * 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 value: ChatMode | undefined; + /** + * Chat mode value of the metadata record. + */ + public get chatMode(): ChatMode | undefined { + return this.value; + } + + 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.value = 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; + } +} 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..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 @@ -22,6 +22,11 @@ export abstract class PromptMetadataRecord { public readonly range: Range, ) { } + /** + * Name of the metadata record. + */ + 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..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 @@ -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. @@ -75,8 +79,8 @@ 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}.", - TOOLS_NAME, + "Value of the '{0}' metadata must be '{1}', got '{2}'.", + 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; } 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 97% 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 b9e8d297a7e..0d10f67183c 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,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ChatMode } from '../../constants.js'; 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. @@ -53,15 +53,20 @@ export interface ITopError extends IResolveError { * 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; } /** 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..b7e7cb33a57 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.', ); @@ -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; @@ -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; } 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. 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/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index ba9a79c3f1a..ac60d09b425 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -16,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'; @@ -42,7 +42,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: ToolProgress, token: CancellationToken): Promise { if (!invocation.context) { throw new Error('toolInvocationToken is required for this tool'); } @@ -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))) { @@ -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/chat/electron-sandbox/tools/fetchPageTool.ts b/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts index b81143b9467..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,13 +3,13 @@ * 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'; export const FetchWebPageToolData: IToolData = { @@ -41,7 +41,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: 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/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 855c89aefed..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 }); 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/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/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index 32f0d97749e..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 @@ -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 { 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. */ @@ -121,7 +129,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( @@ -272,21 +286,23 @@ 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'), [ /* 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 +311,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,101 +324,433 @@ 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 () { - 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 */" 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.", + ], + ); - 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: 8, - 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 ExpectedDiagnosticWarning( - new Range(2, 2, 2, 2 + 15), - 'Unknown metadata record \'something\' will be ignored.', - ), - new ExpectedDiagnosticWarning( - new Range(3, 38, 3, 38 + 12), - 'Duplicate tool name \'tool_name1\'.', - ), - new ExpectedDiagnosticWarning( - new Range(3, 52, 3, 52 + 4), - 'Expected a tool name (string), got \'true\'.', - ), - new ExpectedDiagnosticWarning( - new Range(3, 58, 3, 58 + 5), - 'Expected a tool name (string), got \'false\'.', - ), - new ExpectedDiagnosticWarning( - new Range(3, 65, 3, 65 + 2), - 'Tool name cannot be empty.', - ), - new ExpectedDiagnosticWarning( - new Range(3, 70, 3, 70 + 12), - 'Duplicate tool name \'tool_name2\'.', - ), - new ExpectedDiagnosticWarning( - new Range(4, 3, 4, 3 + 37), - 'Duplicate metadata record \'tools\' will be ignored.', - ), - new ExpectedDiagnosticWarning( - new Range(5, 1, 5, 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, + ChatMode.Agent, + '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, + ChatMode.Agent, + '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, + ChatMode.Agent, + '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, + ChatMode.Ask, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([]); + }); + }); + }); }); }); 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..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 @@ -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 @@ -732,6 +733,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 +765,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 +813,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 +830,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 +858,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 +872,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 +881,7 @@ suite('PromptFileReference (Unix)', function () { new ExpectedReference( URI.joinPath(rootUri, './some-other-folder/folder1'), new MarkdownLink( - 9, 48, + 10, 48, '[]', '(../../folder1/)', ), new FolderReference( @@ -898,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( @@ -914,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, + ChatMode.Agent, + '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.', + ); + }); + }); }); }); 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, diff --git a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts index 3d587cef983..9668c594e98 100644 --- a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -9,8 +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 { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; -import { ExtensionState, IExtensionsWorkbenchService } 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'; @@ -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 = { @@ -64,18 +72,37 @@ 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: ToolProgress, 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), 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 13b42f47cca..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(); } } @@ -626,7 +710,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 +746,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, } }); } 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'], + }, } }); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 6f536d3a7c1..858b6824c35 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(); @@ -496,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); } } @@ -514,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/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/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/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/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts index 65ba4094e3a..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(); } @@ -370,7 +380,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/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. */ diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 6545737fb4a..7e303106f04 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -4,29 +4,32 @@ *--------------------------------------------------------------------------------------------*/ 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, 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'; +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 { extensionMcpCollectionPrefix, IMcpServer, IMcpServerConnection, IMcpTool, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, McpServerDefinition, McpServerToolsState, McpServerTransportType } 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'; type ServerBootData = { supportsLogging: boolean; @@ -55,6 +58,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 +113,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 +158,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 +192,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; @@ -196,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(); @@ -228,27 +254,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)); }); @@ -306,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(); } @@ -384,7 +437,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 +447,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 +464,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, @@ -426,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; @@ -481,7 +535,42 @@ 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: 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(); + + 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()) + .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); } 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 4f8cd6f655f..11bcfa4bb14 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -8,7 +8,7 @@ import { decodeBase64 } from '../../../../base/common/buffer.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'; @@ -16,15 +16,14 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { IProductService } from '../../../../platform/product/common/productService.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'; interface ISyncedToolData { toolData: IToolData; - toolDispose: IDisposable; - implDispose: IDisposable; + store: DisposableStore; } type IMcpServerRec = IReference; @@ -112,27 +111,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); } } @@ -140,8 +144,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(); } })); } @@ -247,7 +250,7 @@ class McpToolImplementation implements IToolImpl { }; } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, token: CancellationToken) { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken) { const result: IToolResult = { content: [] @@ -255,7 +258,7 @@ class McpToolImplementation implements IToolImpl { const outputParts: string[] = []; - const callResult = await this._tool.call(invocation.parameters as Record, 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/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index a590925c9a8..f43dc1472ff 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,6 +17,7 @@ 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'; @@ -104,6 +105,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 +120,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 +133,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 +238,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 */ @@ -253,6 +260,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: ToolProgress, token?: CancellationToken): Promise; } export const enum McpServerTransportType { @@ -326,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. @@ -400,6 +418,8 @@ export namespace McpConnectionState { export interface Error { readonly state: Kind.Error; + readonly code?: string; + readonly shouldRetry?: boolean; readonly message: string; } } 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(); 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'; 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; } 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/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/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; } diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts index a4d035a6230..5dd8d643aa9 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); @@ -72,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(); @@ -132,15 +135,15 @@ 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 + // Skip quick diffs that are not visible + if (!quickDiff || !this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)) { continue; } @@ -218,6 +221,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 +233,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 +245,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 +316,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben this.onEditorsChanged(); this.onDidActiveEditorChange(); + this.onDidChangeQuickDiffProviders(); this.enabled = true; } @@ -348,8 +357,8 @@ 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; + 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; }); store.add(autorun(reader => { @@ -359,6 +368,37 @@ 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]; + 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.id}`, + title: provider.label, + toggled: visible ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE, + menu: { + id: MenuId.SCMQuickDiffDecorations, group, order + }, + f1: false + }); + } + override run(accessor: ServicesAccessor): void { + const quickDiffService = accessor.get(IQuickDiffService); + quickDiffService.toggleQuickDiffProviderVisibility(provider.id); + } + })); + } + })); + } + private onEditorsChanged(): void { for (const editor of this.editorService.visibleTextEditorControls) { if (!isCodeEditor(editor)) { @@ -384,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 b5b24d2f8bf..2a88b551251 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -178,15 +178,14 @@ 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 { - 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, @@ -339,7 +339,11 @@ 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.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) { @@ -411,11 +416,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 +443,8 @@ export class QuickDiffModel extends Disposable { } // Skip quick diffs that are not visible - if (!this.quickDiffs.find(quickDiff => quickDiff.label === this.changes[i].label)?.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 c505713f34a..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, 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 { @@ -168,7 +161,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); @@ -229,7 +223,6 @@ class QuickDiffWidget extends PeekViewWidget { return; } this.diffEditor.setModel(diffEditorModel); - this.dropdown?.setSelection(labeledChange.label); const position = new Position(getModifiedEndLineNumber(change), 1); @@ -238,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); @@ -308,14 +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.label); - - const quickDiffs = this.model.quickDiffs - .filter(quickDiff => quickDiff.visible && quickDiffWithChange.includes(quickDiff.label)); - + const quickDiffs = this.getQuickDiffsContainingChange(); return quickDiffs.length > 1; } @@ -340,17 +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 => quickDiff.visible); - + // 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 { 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/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; } export interface QuickDiff { - label: string; - originalResource: URI; - visible: boolean; + readonly id: string; + readonly label: string; + readonly originalResource: URI; readonly kind: 'primary' | 'secondary' | 'contributed'; } export interface QuickDiffChange { + readonly providerId: string; readonly label: string; readonly original: URI; readonly modified: URI; @@ -91,7 +92,6 @@ export interface QuickDiffChange { } export interface QuickDiffResult { - readonly label: string; readonly original: URI; readonly modified: URI; readonly changes: IChange[]; @@ -102,8 +102,11 @@ 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; + 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 a4f211541b3..97811516760 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) => { @@ -25,16 +26,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,50 +37,120 @@ 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 static readonly STORAGE_KEY = 'workbench.scm.quickDiffProviders.hidden'; + + private quickDiffProviders: Map = new Map(); + get providers(): readonly QuickDiffProvider[] { + return Array.from(this.quickDiffProviders.values()).sort(providerComparer); + } - private quickDiffProviders: Set = new Set(); 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 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 { - originalResource, - label: provider.label, - visible: provider.visible, - kind: provider.kind - } 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, + originalResource, + } satisfies QuickDiff); + } + + return quickDiffs; + } + + toggleQuickDiffProviderVisibility(id: string): void { + if (!this.quickDiffProviders.has(id)) { + return; + } + + 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); + } } } 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) { 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/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', () => { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index a1c7dac9944..662f7bc3abb 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; } @@ -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; } @@ -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..5d61a4b88d4 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); } @@ -1602,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(); } @@ -1632,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/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..e225894e2d1 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('NewCopilotSetupSignedOut', CopilotSignedOutButton, 'chatSetupSignedOut', true), + createCopilotSetupStep('NewCopilotSetupComplete', CopilotCompleteButton, 'chatSetupInstalled && (chatPlanPro || chatPlanLimited)', false), + createCopilotSetupStep('NewCopilotSetupSignedIn', CopilotSignedInButton, '!chatSetupSignedOut && (!chatSetupInstalled || chatPlanCanSignUp)', true), + { + 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: [ + 'onSettingChanged:workbench.colorTheme', + 'onCommand:workbench.action.selectTheme' + ], + media: { type: 'markdown', path: 'theme_picker', } + }, + { + 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\'', + media: { + type: 'svg', altText: 'Language extensions', path: 'languages.svg' + }, + }, + { + 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', + completionEvents: ['onEvent:sync-enabled'], + media: { + type: 'svg', altText: 'VS Code Settings', path: 'settings.svg' + }, + }, + { + 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' }, + } + ] + } } ]; 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/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. 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) => { 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.d.ts b/src/vscode-dts/vscode.d.ts index d85284ac286..cefcb1a81b3 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 { /** @@ -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. @@ -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.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; + } +} diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 6dce17bb29e..2c50b36703a 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? @@ -269,7 +256,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.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; + } } 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 { 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; - } -} 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 65821c87e4f..2ab5956a46d 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,20 +102,8 @@ 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 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.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 { 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; } } diff --git a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts index 5c83a2d3557..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,7 +16,7 @@ declare module 'vscode' { } export interface QuickDiffProvider { - label?: string; - readonly visible?: boolean; + readonly id?: string; + readonly label?: string; } } 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..0d20f626cc1 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.toolProgress.d.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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' { + + /** + * A progress update during an {@link LanguageModelTool.invoke} call. + */ + export interface ToolProgressStep { + /** + * A progress message that represents a chunk of work + */ + message?: string | MarkdownString; + /** + * An increment for discrete progress. Increments will be summed up until 100 (100%) is reached + */ + increment?: number; + } + + export interface LanguageModelTool { + invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken, progress: Progress): ProviderResult; + } +} 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; 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