diff --git a/.gitignore b/.gitignore index 92971a7a573..1dbe527cb8a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ product.overrides.json *.tsbuildinfo .vscode-test vscode-telemetry-docs/ +test-output.json diff --git a/build/npm/gyp/package-lock.json b/build/npm/gyp/package-lock.json index 19a8610c339..1ca858e42d2 100644 --- a/build/npm/gyp/package-lock.json +++ b/build/npm/gyp/package-lock.json @@ -682,9 +682,9 @@ "license": "ISC" }, "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -694,22 +694,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1085,17 +1069,16 @@ } }, "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz", + "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", + "minizlib": "^3.1.0", "yallist": "^5.0.0" }, "engines": { diff --git a/package-lock.json b/package-lock.json index 6bbc06b5856..532300c6956 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@vscode/proxy-agent": "^0.36.0", "@vscode/ripgrep": "^1.15.13", "@vscode/spdlog": "^0.15.2", - "@vscode/sqlite3": "5.1.11-vscode", + "@vscode/sqlite3": "5.1.12-vscode", "@vscode/sudo-prompt": "9.3.2", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", @@ -45,7 +45,7 @@ "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.8", - "native-is-elevated": "0.8.0", + "native-is-elevated": "0.9.0", "native-keymap": "^3.3.5", "node-pty": "^1.2.0-beta.7", "open": "^10.1.2", @@ -1301,7 +1301,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -1314,7 +1313,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -3414,48 +3412,14 @@ } }, "node_modules/@vscode/sqlite3": { - "version": "5.1.11-vscode", - "resolved": "https://registry.npmjs.org/@vscode/sqlite3/-/sqlite3-5.1.11-vscode.tgz", - "integrity": "sha512-x2vBjFRZj/34Ji46lrxotjUtgljistPZU3cbxpckml3bMwF+Z0zbJYiplIeskHLo2g0Kj3kvR8MRRJ+o2nxNug==", + "version": "5.1.12-vscode", + "resolved": "https://registry.npmjs.org/@vscode/sqlite3/-/sqlite3-5.1.12-vscode.tgz", + "integrity": "sha512-WLTftbMtK3Ni0s+q46qtKJ2CFtA3YrS5N4GcrETDCxqNTQAvk1LlYlG3RwGE6vZLcUqPt3TCHobijYeNUhEQ9Q==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "node-addon-api": "^8.2.0", - "tar": "^6.1.11" - } - }, - "node_modules/@vscode/sqlite3/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/@vscode/sqlite3/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@vscode/sqlite3/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" + "tar": "^7.5.4" } }, "node_modules/@vscode/sqlite3/node_modules/node-addon-api": { @@ -3467,29 +3431,6 @@ "node": "^18 || ^20 || >= 21" } }, - "node_modules/@vscode/sqlite3/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@vscode/sqlite3/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/@vscode/sudo-prompt": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz", @@ -3674,9 +3615,9 @@ } }, "node_modules/@vscode/windows-ca-certs": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.3.tgz", - "integrity": "sha512-C0Iq5RcH+H31GUZ8bsMORsX3LySVkGAqe4kQfUSVcCqJ0QOhXkhgwUMU7oCiqYLXaQWyXFp6Fj6eMdt05uK7VA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.4.tgz", + "integrity": "sha512-DcDLjBpu8srh6wUiZqEMyhXHzNDO81ecZOttL3+1u3Iht4CS6Qtxy5WkTPX/aDgbheASO/MK8yg6uLq58RzEWg==", "hasInstallScript": true, "license": "BSD", "optional": true, @@ -5240,7 +5181,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -8042,36 +7982,6 @@ "node": ">=14.14" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/fs-mkdirp-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", @@ -12403,6 +12313,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -12411,7 +12322,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -12424,7 +12334,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -12828,9 +12737,9 @@ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" }, "node_modules/native-is-elevated": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/native-is-elevated/-/native-is-elevated-0.8.0.tgz", - "integrity": "sha512-utx846s63JTqN2DcsLSAd0YpwOMcBezBzN55gSyVJX2kZAsvqOt6+ypdyogNqjSnzd7NvOCEvzMRq+AB2ekVxQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/native-is-elevated/-/native-is-elevated-0.9.0.tgz", + "integrity": "sha512-DTPZixwPTtWHU9g46hXMyK6eW77e8L69uZNkB/cjY00BF8NCXQ5cyJEDim91WmxHruJoIQzLdZBb4TEjrI+PQw==", "hasInstallScript": true, "license": "MIT" }, @@ -16461,10 +16370,9 @@ } }, "node_modules/tar": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.4.tgz", - "integrity": "sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==", - "dev": true, + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz", + "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -16517,7 +16425,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -16527,7 +16434,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 4b401a1b37a..c3241820c69 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "@vscode/proxy-agent": "^0.36.0", "@vscode/ripgrep": "^1.15.13", "@vscode/spdlog": "^0.15.2", - "@vscode/sqlite3": "5.1.11-vscode", + "@vscode/sqlite3": "5.1.12-vscode", "@vscode/sudo-prompt": "9.3.2", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", @@ -108,7 +108,7 @@ "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.8", - "native-is-elevated": "0.8.0", + "native-is-elevated": "0.9.0", "native-keymap": "^3.3.5", "node-pty": "^1.2.0-beta.7", "open": "^10.1.2", diff --git a/remote/package-lock.json b/remote/package-lock.json index 8ed8a663e27..c53948ab5ce 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -485,9 +485,9 @@ } }, "node_modules/@vscode/windows-ca-certs": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.3.tgz", - "integrity": "sha512-C0Iq5RcH+H31GUZ8bsMORsX3LySVkGAqe4kQfUSVcCqJ0QOhXkhgwUMU7oCiqYLXaQWyXFp6Fj6eMdt05uK7VA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.4.tgz", + "integrity": "sha512-DcDLjBpu8srh6wUiZqEMyhXHzNDO81ecZOttL3+1u3Iht4CS6Qtxy5WkTPX/aDgbheASO/MK8yg6uLq58RzEWg==", "hasInstallScript": true, "license": "BSD", "optional": true, diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index b5006737fb6..e3f20d96726 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -600,7 +600,6 @@ function getDomSanitizerConfig(mdStrConfig: MdStrConfig, options: MarkdownSaniti Schemas.vscodeRemote, Schemas.vscodeRemoteResource, Schemas.vscodeNotebookCell, - Schemas.vscodeChatPrompt, // For links that are handled entirely by the action handler Schemas.internal, ]; diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 00859ca7344..74cb106fd3c 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -90,9 +90,6 @@ export namespace Schemas { /** Scheme used for local chat session content */ export const vscodeLocalChatSession = 'vscode-chat-session'; - /** Scheme used for virtual chat prompt files with embedded content */ - export const vscodeChatPrompt = 'vscode-chat-prompt'; - /** * Scheme used internally for webviews that aren't linked to a resource (i.e. not custom editors) */ diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 34d897d6ea4..0c018c2a75b 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -1226,6 +1226,12 @@ export interface ICodeEditor extends editorCommon.IEditor { getWidthOfLine(lineNumber: number): number; + /** + * Reset cached line widths. Call this when the editor becomes visible after being hidden. + * @internal + */ + resetLineWidthCaches(): void; + /** * Force an editor render now. */ diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 6c7f5cddaa0..4c415ec53dd 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -497,6 +497,53 @@ export class ObservableCodeEditor extends Disposable { private readonly _onDidHiddenAreasChanged; private readonly _onDidLineHeightChanged; + /** + * Tracks whether getWidthOfLine returned 0, indicating the editor may be hidden. + * When resize happens and this flag is set, we reset cached line widths. + */ + private _sawZeroLineWidth = false; + + /** + * Fires when the editor container resizes. + * This is lazily created only when someone subscribes to it. + * Useful for detecting when a parent element's display changes from 'none' to 'block'. + */ + private readonly _onDidContainerResize = observableFromEventOpts( + { owner: this, getTransaction: () => this._currentTransaction }, + e => { + const container = this.editor.getContainerDomNode(); + const resizeObserver = new ResizeObserver(() => { + // If we previously saw a 0 width, the editor was likely hidden. + // Now that it resized (became visible), flush the cached widths. + if (this._sawZeroLineWidth) { + this._sawZeroLineWidth = false; + this.editor.resetLineWidthCaches(); + } + e(undefined); + }); + resizeObserver.observe(container); + return { dispose: () => resizeObserver.disconnect() }; + }, + () => ({}) // Return new object each time to ensure change detection + ); + + /** + * Get the width of a line in pixels. + * Reading the returned value depends on layoutInfo, value, scrollTop, and container resize events. + * The container resize dependency ensures correct values when the editor becomes visible after being hidden. + */ + getWidthOfLine(lineNumber: number, reader: IReader | undefined): number { + this.layoutInfo.read(reader); + this.value.read(reader); + this.scrollTop.read(reader); + const width = this.editor.getWidthOfLine(lineNumber); + this._onDidContainerResize.read(reader); + if (width === 0) { + this._sawZeroLineWidth = true; + } + return width; + } + /** * Get the vertical position (top offset) for the line's bottom w.r.t. to the first line. */ diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 2521a18eec7..ad08f4e10f0 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -688,6 +688,10 @@ export class View extends ViewEventHandler { return width; } + public resetLineWidthCaches(): void { + this._viewLines.resetLineWidthCaches(); + } + public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget | null { const mouseTarget = this._pointerHandler.getTargetAtClientPoint(clientX, clientY); if (!mouseTarget) { diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index a3763b98430..4aaa9200561 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -323,6 +323,10 @@ export class ViewLine implements IVisibleLine { } return this._renderedViewLine.getColumnOfNodeOffset(spanNode, offset); } + + public resetCachedWidth(): void { + this._renderedViewLine?.resetCachedWidth(); + } } interface IRenderedViewLine { @@ -330,6 +334,7 @@ interface IRenderedViewLine { readonly input: RenderLineInput; getWidth(context: DomReadingContext | null): number; getWidthIsFast(): boolean; + resetCachedWidth(): void; getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null; getColumnOfNodeOffset(spanNode: HTMLElement, offset: number): number; } @@ -391,6 +396,10 @@ class FastRenderedViewLine implements IRenderedViewLine { return (this.input.lineContent.length < Constants.MaxMonospaceDistance) || this._cachedWidth !== -1; } + public resetCachedWidth(): void { + this._cachedWidth = -1; + } + public monospaceAssumptionsAreValid(): boolean { if (!this.domNode) { return monospaceAssumptionsAreValid; @@ -528,6 +537,15 @@ class RenderedViewLine implements IRenderedViewLine { return true; } + public resetCachedWidth(): void { + this._cachedWidth = -1; + if (this._pixelOffsetCache !== null) { + for (let column = 0, len = this._pixelOffsetCache.length; column < len; column++) { + this._pixelOffsetCache[column] = -1; + } + } + } + /** * Visible ranges for a model range */ diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts index 2b54e9a46a9..cb819821291 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts @@ -412,6 +412,14 @@ export class ViewLines extends ViewPart implements IViewLines { return result; } + public resetLineWidthCaches(): void { + const rendStartLineNumber = this._visibleLines.getStartLineNumber(); + const rendEndLineNumber = this._visibleLines.getEndLineNumber(); + for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) { + this._visibleLines.getVisibleLine(lineNumber).resetCachedWidth(); + } + } + public linesVisibleRangesForRange(_range: Range, includeNewLines: boolean): LineVisibleRanges[] | null { const originalEndLineNumber = _range.endLineNumber; const range = Range.intersectRanges(_range, this._lastRenderedData.getCurrentVisibleRange()); diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 9a4ae242fce..10dd3e6db9f 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -1694,6 +1694,13 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._modelData.view.getLineWidth(lineNumber); } + public resetLineWidthCaches(): void { + if (!this._modelData || !this._modelData.hasRealView) { + return; + } + this._modelData.view.resetLineWidthCaches(); + } + public render(forceRedraw: boolean = false): void { if (!this._modelData || !this._modelData.hasRealView) { return; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts index 401a8f2dc25..580d401fa7a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts @@ -34,16 +34,12 @@ import { Size2D } from '../../../../../../common/core/2d/size.js'; * Warning: might return 0. */ export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): number { - editor.layoutInfo.read(reader); - editor.value.read(reader); - const model = editor.model.read(reader); if (!model) { return 0; } let maxContentWidth = 0; - editor.scrollTop.read(reader); for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) { - const lineContentWidth = editor.editor.getWidthOfLine(i); + const lineContentWidth = editor.getWidthOfLine(i, reader); maxContentWidth = Math.max(maxContentWidth, lineContentWidth); } const lines = range.mapToLineArray(l => model.getLineContent(l)); @@ -55,8 +51,6 @@ export function maxContentWidthInRange(editor: ObservableCodeEditor, range: Line } export function getContentSizeOfLines(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): Size2D[] { - editor.layoutInfo.read(reader); - editor.value.read(reader); observableSignalFromEvent(editor, editor.editor.onDidChangeLineHeight).read(reader); const model = editor.model.read(reader); @@ -64,9 +58,8 @@ export function getContentSizeOfLines(editor: ObservableCodeEditor, range: LineR const sizes: Size2D[] = []; - editor.scrollTop.read(reader); for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) { - let lineContentWidth = editor.editor.getWidthOfLine(i); + let lineContentWidth = editor.getWidthOfLine(i, reader); if (lineContentWidth === -1) { // approximation const column = model.getLineMaxColumn(i); diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 1f66943f486..124aae0d4a6 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -283,6 +283,7 @@ const _allApiProposals = { }, languageModelToolSupportsModel: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelToolSupportsModel.d.ts', + version: 1 }, languageStatusText: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageStatusText.d.ts', diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index f0fd0935c21..e0416a2d09b 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -28,7 +28,6 @@ import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../cont import { IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../contrib/chat/common/participants/chatAgents.js'; import { IPromptFileContext, IPromptsService } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { isValidPromptType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; -import { IChatPromptContentStore } from '../../contrib/chat/common/promptSyntax/chatPromptContentStore.js'; import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/editing/chatEditingService.js'; import { IChatModel } from '../../contrib/chat/common/model/chatModel.js'; import { ChatRequestAgentPart } from '../../contrib/chat/common/requestParser/chatParserTypes.js'; @@ -123,7 +122,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @IExtensionService private readonly _extensionService: IExtensionService, @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, @IPromptsService private readonly _promptsService: IPromptsService, - @IChatPromptContentStore private readonly _chatPromptContentStore: IChatPromptContentStore, @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, ) { super(); @@ -487,17 +485,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA } // Convert UriComponents to URI and register any inline content return contributions.map(c => { - const uri = URI.revive(c.uri); - // If this is a virtual prompt with inline content, register it with the store - if (c.content && uri.scheme === Schemas.vscodeChatPrompt) { - const uriKey = uri.toString(); - // Dispose any previous registration for this URI before registering new content - contentRegistrations.deleteAndDispose(uriKey); - contentRegistrations.set(uriKey, this._chatPromptContentStore.registerContent(uri, c.content)); - } return { - uri, - isEditable: c.isEditable + uri: URI.revive(c.uri), }; }); } diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 38de78caf4a..27250fdab03 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -17,16 +17,17 @@ import { localize } from '../../../nls.js'; import { IDialogService } from '../../../platform/dialogs/common/dialogs.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { hasValidDiff, IAgentSession } from '../../contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { IAgentSessionsService } from '../../contrib/chat/browser/agentSessions/agentSessionsService.js'; import { ChatViewPaneTarget, IChatWidgetService, isIChatViewViewContext } from '../../contrib/chat/browser/chat.js'; import { IChatEditorOptions } from '../../contrib/chat/browser/widgetHosts/editor/chatEditor.js'; import { ChatEditorInput } from '../../contrib/chat/browser/widgetHosts/editor/chatEditorInput.js'; +import { IChatRequestVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; import { awaitStatsForSession } from '../../contrib/chat/common/chat.js'; -import { IChatAgentRequest } from '../../contrib/chat/common/participants/chatAgents.js'; -import { IChatModel } from '../../contrib/chat/common/model/chatModel.js'; import { IChatContentInlineReference, IChatProgress, IChatService, ResponseModelState } from '../../contrib/chat/common/chatService/chatService.js'; import { ChatSessionStatus, IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionItem, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; -import { IChatRequestVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; +import { IChatModel } from '../../contrib/chat/common/model/chatModel.js'; +import { IChatAgentRequest } from '../../contrib/chat/common/participants/chatAgents.js'; import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../services/editor/common/editorService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; @@ -334,6 +335,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat constructor( private readonly _extHostContext: IExtHostContext, + @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, @IChatSessionsService private readonly _chatSessionsService: IChatSessionsService, @IChatService private readonly _chatService: IChatService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @@ -352,6 +354,14 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat await this.notifyOptionsChange(handle, sessionResource, updates); } }); + + this._register(this._agentSessionsService.model.onDidChangeSessionArchivedState(session => { + for (const [handle, { provider }] of this._itemProvidersRegistrations) { + if (provider.chatSessionType === session.providerType) { + this._proxy.$onDidChangeChatSessionItemState(handle, session.resource, session.isArchived()); + } + } + })); } private _getHandleForSessionType(chatSessionType: string): number | undefined { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 2eff0efe464..f353fb12823 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1546,19 +1546,19 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatContextProvider'); return extHostChatContext.registerChatContextProvider(selector ? checkSelector(selector) : undefined, `${extension.id}-${id}`, provider); }, - registerCustomAgentProvider(provider: vscode.CustomAgentProvider): vscode.Disposable { + registerCustomAgentProvider(provider: vscode.ChatCustomAgentProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.agent, provider); }, - registerInstructionsProvider(provider: vscode.InstructionsProvider): vscode.Disposable { + registerInstructionsProvider(provider: vscode.ChatInstructionsProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.instructions, provider); }, - registerPromptFileProvider(provider: vscode.PromptFileProvider): vscode.Disposable { + registerPromptFileProvider(provider: vscode.ChatPromptFileProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.prompt, provider); }, - registerSkillProvider(provider: vscode.SkillProvider): vscode.Disposable { + registerSkillProvider(provider: vscode.ChatSkillProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.skill, provider); }, @@ -1979,10 +1979,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I McpStdioServerDefinition2: extHostTypes.McpStdioServerDefinition, McpToolAvailability: extHostTypes.McpToolAvailability, SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind, - CustomAgentChatResource: extHostTypes.CustomAgentChatResource, - InstructionsChatResource: extHostTypes.InstructionsChatResource, - PromptFileChatResource: extHostTypes.PromptFileChatResource, - SkillChatResource: extHostTypes.SkillChatResource, }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 744fb866989..519b556048d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3377,6 +3377,7 @@ export interface MainThreadChatSessionsShape extends IDisposable { export interface ExtHostChatSessionsShape { $provideChatSessionItems(providerHandle: number, token: CancellationToken): Promise[]>; + $onDidChangeChatSessionItemState(providerHandle: number, sessionResource: UriComponents, archived: boolean): void; $provideChatSessionContent(providerHandle: number, sessionResource: UriComponents, token: CancellationToken): Promise; $interruptChatSessionActiveResponse(providerHandle: number, sessionResource: UriComponents, requestId: string): Promise; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 23fd49095d5..c93068b100a 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -38,7 +38,6 @@ import * as extHostTypes from './extHostTypes.js'; import { IPromptFileContext, IPromptFileResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js'; -import { Schemas } from '../../../base/common/network.js'; export class ChatAgentResponseStream { @@ -426,7 +425,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private readonly _relatedFilesProviders = new Map(); private static _contributionsProviderIdPool = 0; - private readonly _promptFileProviders = new Map(); + private readonly _promptFileProviders = new Map(); private readonly _sessionDisposables: DisposableResourceMap = this._register(new DisposableResourceMap()); private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); @@ -510,7 +509,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS * Internal method that handles all prompt file provider types. * Routes custom agents, instructions, prompt files, and skills to the unified internal implementation. */ - registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: vscode.CustomAgentProvider | vscode.InstructionsProvider | vscode.PromptFileProvider | vscode.SkillProvider): vscode.Disposable { + registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: vscode.ChatCustomAgentProvider | vscode.ChatInstructionsProvider | vscode.ChatPromptFileProvider | vscode.ChatSkillProvider): vscode.Disposable { const handle = ExtHostChatAgents2._contributionsProviderIdPool++; this._promptFileProviders.set(handle, { extension, provider }); this._proxy.$registerPromptFileProvider(handle, type, extension.identifier); @@ -522,16 +521,16 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS let changeEvent: vscode.Event | undefined; switch (type) { case PromptsType.agent: - changeEvent = (provider as vscode.CustomAgentProvider).onDidChangeCustomAgents; + changeEvent = (provider as vscode.ChatCustomAgentProvider).onDidChangeCustomAgents; break; case PromptsType.instructions: - changeEvent = (provider as vscode.InstructionsProvider).onDidChangeInstructions; + changeEvent = (provider as vscode.ChatInstructionsProvider).onDidChangeInstructions; break; case PromptsType.prompt: - changeEvent = (provider as vscode.PromptFileProvider).onDidChangePromptFiles; + changeEvent = (provider as vscode.ChatPromptFileProvider).onDidChangePromptFiles; break; case PromptsType.skill: - changeEvent = (provider as vscode.SkillProvider).onDidChangeSkills; + changeEvent = (provider as vscode.ChatSkillProvider).onDidChangeSkills; break; } @@ -566,77 +565,23 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } const provider = providerData.provider; - let resources: vscode.CustomAgentChatResource[] | vscode.InstructionsChatResource[] | vscode.PromptFileChatResource[] | vscode.SkillChatResource[] | undefined; + let resources: vscode.ChatResource[] | undefined; switch (type) { case PromptsType.agent: - resources = await (provider as vscode.CustomAgentProvider).provideCustomAgents(context, token) ?? undefined; + resources = await (provider as vscode.ChatCustomAgentProvider).provideCustomAgents(context, token) ?? undefined; break; case PromptsType.instructions: - resources = await (provider as vscode.InstructionsProvider).provideInstructions(context, token) ?? undefined; + resources = await (provider as vscode.ChatInstructionsProvider).provideInstructions(context, token) ?? undefined; break; case PromptsType.prompt: - resources = await (provider as vscode.PromptFileProvider).providePromptFiles(context, token) ?? undefined; + resources = await (provider as vscode.ChatPromptFileProvider).providePromptFiles(context, token) ?? undefined; break; case PromptsType.skill: - resources = await (provider as vscode.SkillProvider).provideSkills(context, token) ?? undefined; + resources = await (provider as vscode.ChatSkillProvider).provideSkills(context, token) ?? undefined; break; } - // Convert ChatResourceDescriptor to IPromptFileResource format - return resources?.map(r => this.convertChatResourceDescriptorToPromptFileResource(r.resource, providerData.extension.identifier.value, type)); - } - - /** - * Creates a virtual URI for a prompt file. - * Format varies by type: - * - Skills: /${extensionId}/skills/${id}/SKILL.md - * - Agents: /${extensionId}/agents/${id}.agent.md - * - Instructions: /${extensionId}/instructions/${id}.instructions.md - * - Prompts: /${extensionId}/prompts/${id}.prompt.md - */ - createVirtualPromptUri(id: string, extensionId: string, type: PromptsType): URI { - let path: string; - switch (type) { - case PromptsType.skill: - path = `/${extensionId}/skills/${id}/SKILL.md`; - break; - case PromptsType.agent: - path = `/${extensionId}/agents/${id}.agent.md`; - break; - case PromptsType.instructions: - path = `/${extensionId}/instructions/${id}.instructions.md`; - break; - case PromptsType.prompt: - path = `/${extensionId}/prompts/${id}.prompt.md`; - break; - default: - throw new Error(`Unsupported PromptsType: ${type}`); - } - return URI.from({ - scheme: Schemas.vscodeChatPrompt, - path - }); - } - - convertChatResourceDescriptorToPromptFileResource(resource: vscode.ChatResourceDescriptor, extensionId: string, type: PromptsType): IPromptFileResource { - if (URI.isUri(resource)) { - // Plain URI - return { uri: resource }; - } else if ('id' in resource && 'content' in resource) { - // { id, content } - return { - content: resource.content, - uri: this.createVirtualPromptUri(resource.id, extensionId, type), - isEditable: undefined - }; - } else if ('uri' in resource && URI.isUri(resource.uri)) { - // { uri, isEditable? } - return { - uri: URI.revive(resource.uri), - isEditable: resource.isEditable - }; - } - throw new Error(`Invalid ChatResourceDescriptor: ${JSON.stringify(resource)}`); + return resources; } async $detectChatParticipant(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index c4d34921e45..24064e799da 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -240,6 +240,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio readonly controller: vscode.ChatSessionItemController; readonly extension: IExtensionDescription; readonly disposable: DisposableStore; + readonly onDidChangeChatSessionItemStateEmitter: Emitter; }>(); private _nextChatSessionItemProviderHandle = 0; private readonly _chatSessionContentProviders = new Map()); + const onDidChangeChatSessionItemStateEmitter = disposables.add(new Emitter()); const collection = new ChatSessionItemCollectionImpl(() => { this._proxy.$onDidChangeChatSessionItems(controllerHandle); @@ -334,11 +334,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio let isDisposed = false; - const controller: vscode.ChatSessionItemController = { + const controller = Object.freeze({ id, refreshHandler, items: collection, - onDidArchiveChatSessionItem: onDidArchiveChatSessionItem.event, + onDidChangeChatSessionItemState: onDidChangeChatSessionItemStateEmitter.event, createChatSessionItem: (resource: vscode.Uri, label: string) => { if (isDisposed) { throw new Error('ChatSessionItemController has been disposed'); @@ -353,9 +353,9 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio isDisposed = true; disposables.dispose(); }, - }; + }); - this._chatSessionItemControllers.set(controllerHandle, { controller, extension, disposable: disposables, sessionType: id }); + this._chatSessionItemControllers.set(controllerHandle, { controller, extension, disposable: disposables, sessionType: id, onDidChangeChatSessionItemStateEmitter }); this._proxy.$registerChatSessionItemProvider(controllerHandle, id); disposables.add(toDisposable(() => { @@ -734,4 +734,22 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio return []; } } + + $onDidChangeChatSessionItemState(providerHandle: number, sessionResourceComponents: UriComponents, archived: boolean): void { + const controllerData = this._chatSessionItemControllers.get(providerHandle); + if (!controllerData) { + this._logService.warn(`No controller found for provider handle ${providerHandle}`); + return; + } + + const sessionResource = URI.revive(sessionResourceComponents); + const item = controllerData.controller.items.get(sessionResource); + if (!item) { + this._logService.warn(`No item found for session resource ${sessionResource.toString()}`); + return; + } + + item.archived = archived; + controllerData.onDidChangeChatSessionItemStateEmitter.fire(item); + } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index b4f2638fc95..c142462a992 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3895,24 +3895,4 @@ export class McpHttpServerDefinition implements vscode.McpHttpServerDefinition { //#endregion //#region Chat Prompt Files - -@es5ClassCompat -export class CustomAgentChatResource implements vscode.CustomAgentChatResource { - constructor(public readonly resource: vscode.ChatResourceDescriptor) { } -} - -@es5ClassCompat -export class InstructionsChatResource implements vscode.InstructionsChatResource { - constructor(public readonly resource: vscode.ChatResourceDescriptor) { } -} - -@es5ClassCompat -export class PromptFileChatResource implements vscode.PromptFileChatResource { - constructor(public readonly resource: vscode.ChatResourceDescriptor) { } -} - -@es5ClassCompat -export class SkillChatResource implements vscode.SkillChatResource { - constructor(public readonly resource: vscode.ChatResourceDescriptor) { } -} //#endregion diff --git a/src/vs/workbench/api/test/browser/extHostTypes.test.ts b/src/vs/workbench/api/test/browser/extHostTypes.test.ts index ef844667ecc..8ee4767df55 100644 --- a/src/vs/workbench/api/test/browser/extHostTypes.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypes.test.ts @@ -788,118 +788,4 @@ suite('ExtHostTypes', function () { m.content = 'Hello'; assert.deepStrictEqual(m.content, [new types.LanguageModelTextPart('Hello')]); }); - - test('CustomAgentChatResource - URI constructor', function () { - const uri = URI.file('/path/to/agent.md'); - const resource = new types.CustomAgentChatResource(uri); - - assert.ok(URI.isUri(resource.resource)); - assert.strictEqual(resource.resource.toString(), uri.toString()); - }); - - test('CustomAgentChatResource - URI constructor with options', function () { - const uri = URI.file('/path/to/agent.md'); - const resource = new types.CustomAgentChatResource({ uri, isEditable: true }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { uri: URI; isEditable?: boolean }; - assert.strictEqual(descriptor.uri.toString(), uri.toString()); - assert.strictEqual(descriptor.isEditable, true); - }); - - test('CustomAgentChatResource - content constructor', function () { - const content = '# My Agent\nThis is agent content'; - const resource = new types.CustomAgentChatResource({ id: 'my-agent-id', content }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { id: string; content: string }; - assert.strictEqual(descriptor.id, 'my-agent-id'); - assert.strictEqual(descriptor.content, content); - }); - - - - test('InstructionsChatResource - URI constructor', function () { - const uri = URI.file('/path/to/instructions.md'); - const resource = new types.InstructionsChatResource(uri); - - assert.ok(URI.isUri(resource.resource)); - assert.strictEqual(resource.resource.toString(), uri.toString()); - }); - - test('InstructionsChatResource - URI constructor with options', function () { - const uri = URI.file('/path/to/instructions.md'); - const resource = new types.InstructionsChatResource({ uri, isEditable: true }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { uri: URI; isEditable?: boolean }; - assert.strictEqual(descriptor.uri.toString(), uri.toString()); - assert.strictEqual(descriptor.isEditable, true); - }); - - test('InstructionsChatResource - content constructor', function () { - const content = '# Instructions\nFollow these steps'; - const resource = new types.InstructionsChatResource({ id: 'my-instructions-id', content }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { id: string; content: string }; - assert.strictEqual(descriptor.id, 'my-instructions-id'); - assert.strictEqual(descriptor.content, content); - }); - - - - test('PromptFileChatResource - URI constructor', function () { - const uri = URI.file('/path/to/prompt.md'); - const resource = new types.PromptFileChatResource(uri); - - assert.ok(URI.isUri(resource.resource)); - assert.strictEqual(resource.resource.toString(), uri.toString()); - }); - - test('PromptFileChatResource - URI constructor with options', function () { - const uri = URI.file('/path/to/prompt.md'); - const resource = new types.PromptFileChatResource({ uri, isEditable: true }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { uri: URI; isEditable?: boolean }; - assert.strictEqual(descriptor.uri.toString(), uri.toString()); - assert.strictEqual(descriptor.isEditable, true); - }); - - test('PromptFileChatResource - content constructor', function () { - const content = '# Prompt\nThis is my prompt content'; - const resource = new types.PromptFileChatResource({ id: 'my-prompt-id', content }); - - assert.ok(!URI.isUri(resource.resource)); - const descriptor = resource.resource as { id: string; content: string }; - assert.strictEqual(descriptor.id, 'my-prompt-id'); - assert.strictEqual(descriptor.content, content); - }); - - - - test('Chat prompt resources store different descriptors for different IDs', function () { - const resource1 = new types.CustomAgentChatResource({ id: 'id-one', content: 'content1' }); - const resource2 = new types.CustomAgentChatResource({ id: 'id-two', content: 'content2' }); - - const desc1 = resource1.resource as { id: string; content: string }; - const desc2 = resource2.resource as { id: string; content: string }; - assert.strictEqual(desc1.id, 'id-one'); - assert.strictEqual(desc2.id, 'id-two'); - assert.notStrictEqual(desc1.id, desc2.id); - }); - - test('Chat prompt resources store resource descriptors correctly', function () { - const agent = new types.CustomAgentChatResource({ id: 'test', content: 'content' }); - const instructions = new types.InstructionsChatResource({ id: 'test', content: 'content' }); - const prompt = new types.PromptFileChatResource({ id: 'test', content: 'content' }); - - assert.ok(!URI.isUri(agent.resource)); - assert.ok(!URI.isUri(instructions.resource)); - assert.ok(!URI.isUri(prompt.resource)); - assert.strictEqual((agent.resource as { id: string; content: string }).id, 'test'); - assert.strictEqual((instructions.resource as { id: string; content: string }).id, 'test'); - assert.strictEqual((prompt.resource as { id: string; content: string }).id, 'test'); - }); }); diff --git a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts index 9e818d0a34a..438a6769d47 100644 --- a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts @@ -32,6 +32,9 @@ import { MainThreadChatSessions, ObservableChatSession } from '../../browser/mai import { ExtHostChatSessionsShape, IChatProgressDto, IChatSessionProviderOptions } from '../../common/extHost.protocol.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { MockChatService } from '../../../contrib/chat/test/common/chatService/mockChatService.js'; +import { IAgentSessionsService } from '../../../contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { IAgentSessionsModel } from '../../../contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { Event } from '../../../../base/common/event.js'; suite('ObservableChatSession', function () { let disposables: DisposableStore; @@ -58,6 +61,7 @@ suite('ObservableChatSession', function () { $invokeChatSessionRequestHandler: sinon.stub(), $disposeChatSessionContent: sinon.stub(), $provideChatSessionItems: sinon.stub(), + $onDidChangeChatSessionItemState: sinon.stub(), }; }); @@ -354,6 +358,7 @@ suite('MainThreadChatSessions', function () { $invokeChatSessionRequestHandler: sinon.stub(), $disposeChatSessionContent: sinon.stub(), $provideChatSessionItems: sinon.stub(), + $onDidChangeChatSessionItemState: sinon.stub(), }; const extHostContext = new class implements IExtHostContext { @@ -387,6 +392,14 @@ suite('MainThreadChatSessions', function () { } }); instantiationService.stub(IChatService, new MockChatService()); + instantiationService.stub(IAgentSessionsService, new class extends mock() { + override get model(): IAgentSessionsModel { + return new class extends mock() { + override onDidChangeSessionArchivedState = Event.None; + }; + } + + }); chatSessionsService = disposables.add(instantiationService.createInstance(ChatSessionsService)); instantiationService.stub(IChatSessionsService, chatSessionsService); diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts index 3de7e243cb1..c4866fed119 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts @@ -5,7 +5,7 @@ import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; -import { IMarkdownString, isMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IMarkdownString, isMarkdownString } from '../../../../../base/common/htmlContent.js'; import { stripIcons } from '../../../../../base/common/iconLabels.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -214,79 +214,108 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi } private _getContent(item: ChatTreeItem): string { - let responseContent = isResponseVM(item) ? item.response.toString() : ''; - if (!responseContent && 'errorDetails' in item && item.errorDetails) { - responseContent = item.errorDetails.message; + const contentParts: string[] = []; + + if (!isResponseVM(item)) { + return ''; } - if (isResponseVM(item)) { - item.response.value.filter(item => item.kind === 'elicitation2' || item.kind === 'elicitationSerialized').forEach(elicitation => { - const title = elicitation.title; - if (typeof title === 'string') { - responseContent += `${title}\n`; - } else if (isMarkdownString(title)) { - responseContent += renderAsPlaintext(title, { includeCodeBlocksFences: true }) + '\n'; - } - const message = elicitation.message; - if (isMarkdownString(message)) { - responseContent += renderAsPlaintext(message, { includeCodeBlocksFences: true }); - } else { - responseContent += message; - } - }); - const toolInvocations = item.response.value.filter(item => item.kind === 'toolInvocation'); - for (const toolInvocation of toolInvocations) { - const state = toolInvocation.state.get(); - if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation && state.confirmationMessages?.title) { - const title = this._renderMessageAsPlaintext(state.confirmationMessages.title); - const message = state.confirmationMessages.message ? this._renderMessageAsPlaintext(state.confirmationMessages.message) : ''; - const toolDataDesc = getToolSpecificDataDescription(toolInvocation.toolSpecificData); - responseContent += `${title}`; - if (toolDataDesc) { - responseContent += `: ${toolDataDesc}`; + + if ('errorDetails' in item && item.errorDetails) { + contentParts.push(item.errorDetails.message); + } + + // Process all parts in order to maintain the natural flow + for (const part of item.response.value) { + switch (part.kind) { + case 'thinking': { + const thinkingValue = Array.isArray(part.value) ? part.value.join('') : (part.value || ''); + const trimmed = thinkingValue.trim(); + if (trimmed) { + contentParts.push(localize('thinkingContent', "Thinking: {0}", trimmed)); } - if (message) { - responseContent += `\n${message}`; + break; + } + case 'markdownContent': { + const text = renderAsPlaintext(part.content, { includeCodeBlocksFences: true, useLinkFormatter: true }); + if (text.trim()) { + contentParts.push(text); } - responseContent += '\n'; - } else if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { - const postApprovalDetails = isToolResultInputOutputDetails(state.resultDetails) - ? state.resultDetails.input - : isToolResultOutputDetails(state.resultDetails) - ? undefined - : toolContentToA11yString(state.contentForModel); - responseContent += localize('toolPostApprovalA11yView', "Approve results of {0}? Result: ", toolInvocation.toolId) + (postApprovalDetails ?? '') + '\n'; - } else { - const resultDetails = IChatToolInvocation.resultDetails(toolInvocation); - const isComplete = IChatToolInvocation.isComplete(toolInvocation); + break; + } + case 'elicitation2': + case 'elicitationSerialized': { + const title = part.title; + let elicitationContent = ''; + if (typeof title === 'string') { + elicitationContent += `${title}\n`; + } else if (isMarkdownString(title)) { + elicitationContent += renderAsPlaintext(title, { includeCodeBlocksFences: true }) + '\n'; + } + const message = part.message; + if (isMarkdownString(message)) { + elicitationContent += renderAsPlaintext(message, { includeCodeBlocksFences: true }); + } else { + elicitationContent += message; + } + if (elicitationContent.trim()) { + contentParts.push(elicitationContent); + } + break; + } + case 'toolInvocation': { + const state = part.state.get(); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation && state.confirmationMessages?.title) { + const title = this._renderMessageAsPlaintext(state.confirmationMessages.title); + const message = state.confirmationMessages.message ? this._renderMessageAsPlaintext(state.confirmationMessages.message) : ''; + const toolDataDesc = getToolSpecificDataDescription(part.toolSpecificData); + let toolContent = title; + if (toolDataDesc) { + toolContent += `: ${toolDataDesc}`; + } + if (message) { + toolContent += `\n${message}`; + } + contentParts.push(toolContent); + } else if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + const postApprovalDetails = isToolResultInputOutputDetails(state.resultDetails) + ? state.resultDetails.input + : isToolResultOutputDetails(state.resultDetails) + ? undefined + : toolContentToA11yString(state.contentForModel); + contentParts.push(localize('toolPostApprovalA11yView', "Approve results of {0}? Result: ", part.toolId) + (postApprovalDetails ?? '')); + } else { + const resultDetails = IChatToolInvocation.resultDetails(part); + const isComplete = IChatToolInvocation.isComplete(part); + const description = getToolInvocationA11yDescription( + this._renderMessageAsPlaintext(part.invocationMessage), + part.pastTenseMessage ? this._renderMessageAsPlaintext(part.pastTenseMessage) : undefined, + part.toolSpecificData, + resultDetails, + isComplete + ); + if (description) { + contentParts.push(description); + } + } + break; + } + case 'toolInvocationSerialized': { const description = getToolInvocationA11yDescription( - this._renderMessageAsPlaintext(toolInvocation.invocationMessage), - toolInvocation.pastTenseMessage ? this._renderMessageAsPlaintext(toolInvocation.pastTenseMessage) : undefined, - toolInvocation.toolSpecificData, - resultDetails, - isComplete + this._renderMessageAsPlaintext(part.invocationMessage), + part.pastTenseMessage ? this._renderMessageAsPlaintext(part.pastTenseMessage) : undefined, + part.toolSpecificData, + part.resultDetails, + part.isComplete ); if (description) { - responseContent += '\n' + description + '\n'; + contentParts.push(description); } - } - } - - const pastConfirmations = item.response.value.filter(item => item.kind === 'toolInvocationSerialized'); - for (const pastConfirmation of pastConfirmations) { - const description = getToolInvocationA11yDescription( - this._renderMessageAsPlaintext(pastConfirmation.invocationMessage), - pastConfirmation.pastTenseMessage ? this._renderMessageAsPlaintext(pastConfirmation.pastTenseMessage) : undefined, - pastConfirmation.toolSpecificData, - pastConfirmation.resultDetails, - pastConfirmation.isComplete - ); - if (description) { - responseContent += '\n' + description + '\n'; + break; } } } - const plainText = renderAsPlaintext(new MarkdownString(responseContent), { includeCodeBlocksFences: true, useLinkFormatter: true }); - return this._normalizeWhitespace(plainText); + + return this._normalizeWhitespace(contentParts.join('\n')); } private _normalizeWhitespace(content: string): string { diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatThinkingAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatThinkingAccessibleView.ts deleted file mode 100644 index 0c8e067e875..00000000000 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatThinkingAccessibleView.ts +++ /dev/null @@ -1,72 +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 { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { IChatWidgetService } from '../chat.js'; -import { IChatResponseViewModel, isResponseVM } from '../../common/model/chatViewModel.js'; - -export class ChatThinkingAccessibleView implements IAccessibleViewImplementation { - readonly priority = 105; - readonly name = 'chatThinking'; - readonly type = AccessibleViewType.View; - // Never match via the registry - this view is only opened via the explicit command (Alt+Shift+F2) - readonly when = ContextKeyExpr.false(); - - getProvider(accessor: ServicesAccessor) { - const widgetService = accessor.get(IChatWidgetService); - const widget = widgetService.lastFocusedWidget; - if (!widget) { - return; - } - - const viewModel = widget.viewModel; - if (!viewModel) { - return; - } - - // Get the latest response from the chat - const items = viewModel.getItems(); - const latestResponse = [...items].reverse().find(item => isResponseVM(item)); - if (!latestResponse || !isResponseVM(latestResponse)) { - return; - } - - // Extract thinking content from the response - const thinkingContent = this._extractThinkingContent(latestResponse); - if (!thinkingContent) { - return; - } - - return new AccessibleContentProvider( - AccessibleViewProviderId.ChatThinking, - { type: AccessibleViewType.View, id: AccessibleViewProviderId.ChatThinking, language: 'markdown' }, - () => thinkingContent, - () => widget.focusInput(), - AccessibilityVerbositySettingId.Chat - ); - } - - private _extractThinkingContent(response: IChatResponseViewModel): string | undefined { - const thinkingParts: string[] = []; - for (const part of response.response.value) { - if (part.kind === 'thinking') { - const value = Array.isArray(part.value) ? part.value.join('') : (part.value || ''); - const trimmed = value.trim(); - if (trimmed) { - thinkingParts.push(trimmed); - } - } - } - - if (thinkingParts.length === 0) { - return undefined; - } - return thinkingParts.join('\n\n'); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts index 118e7128b4e..f1ec099751c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts @@ -6,19 +6,15 @@ import { alert } from '../../../../../base/browser/ui/aria/aria.js'; import { localize } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { IChatWidgetService } from '../chat.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { isResponseVM } from '../../common/model/chatViewModel.js'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../../platform/accessibility/common/accessibility.js'; -import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { ChatThinkingAccessibleView } from '../accessibility/chatThinkingAccessibleView.js'; -import { CHAT_CATEGORY } from './chatActions.js'; export const ACTION_ID_FOCUS_CHAT_CONFIRMATION = 'workbench.action.chat.focusConfirmation'; -export const ACTION_ID_OPEN_THINKING_ACCESSIBLE_VIEW = 'workbench.action.chat.openThinkingAccessibleView'; class AnnounceChatConfirmationAction extends Action2 { constructor() { @@ -71,42 +67,6 @@ class AnnounceChatConfirmationAction extends Action2 { } } -class OpenThinkingAccessibleViewAction extends Action2 { - constructor() { - super({ - id: ACTION_ID_OPEN_THINKING_ACCESSIBLE_VIEW, - title: { value: localize('openThinkingAccessibleView', 'Open Thinking Accessible View'), original: 'Open Thinking Accessible View' }, - category: CHAT_CATEGORY, - precondition: ChatContextKeys.enabled, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F2, - linux: { - primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F3, - }, - when: ChatContextKeys.inChatSession - } - }); - } - - async run(accessor: ServicesAccessor): Promise { - const accessibleViewService = accessor.get(IAccessibleViewService); - const instantiationService = accessor.get(IInstantiationService); - - const thinkingView = new ChatThinkingAccessibleView(); - const provider = instantiationService.invokeFunction(thinkingView.getProvider.bind(thinkingView)); - - if (!provider) { - alert(localize('noThinking', 'No thinking')); - return; - } - - accessibleViewService.show(provider); - } -} - export function registerChatAccessibilityActions(): void { registerAction2(AnnounceChatConfirmationAction); - registerAction2(OpenThinkingAccessibleViewAction); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 9c42a79f6ef..b82569c057a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -17,7 +17,6 @@ import { TerminalContribCommandId } from '../../../terminal/terminalContribExpor import { ChatContextKeyExprs, ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { FocusAgentSessionsAction } from '../agentSessions/agentSessionsActions.js'; -import { ACTION_ID_OPEN_THINKING_ACCESSIBLE_VIEW } from './chatAccessibilityActions.js'; import { IChatWidgetService } from '../chat.js'; import { ChatEditingShowChangesAction, ViewPreviousEditsAction } from '../chatEditing/chatEditingActions.js'; @@ -75,8 +74,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'age } content.push(localize('chat.requestHistory', 'In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.')); content.push(localize('chat.attachments.removal', 'To remove attached contexts, focus an attachment and press Delete or Backspace.')); - content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}.', '')); - content.push(localize('chat.openThinkingAccessibleView', 'To inspect thinking content from the latest response, invoke the Open Thinking Accessible View command{0}.', ``)); + content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}. Thinking content is included in order.', '')); content.push(localize('workbench.action.chat.focus', 'To focus the chat request and response list, invoke the Focus Chat command{0}. This will move focus to the most recent response, which you can then navigate using the up and down arrow keys.', getChatFocusKeybindingLabel(keybindingService, type, 'last'))); content.push(localize('workbench.action.chat.focusLastFocusedItem', 'To return to the last chat response you focused, invoke the Focus Last Focused Chat Response command{0}.', getChatFocusKeybindingLabel(keybindingService, type, 'lastFocused'))); content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command{0}.', getChatFocusKeybindingLabel(keybindingService, type, 'input'))); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index e610c8500a8..bcbb0b5ba83 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -24,7 +24,7 @@ import { applyingChatEditsFailedContextKey, isChatEditingActionContext } from '. import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatService } from '../../common/chatService/chatService.js'; import { isResponseVM } from '../../common/model/chatViewModel.js'; import { ChatModeKind } from '../../common/constants.js'; -import { IChatWidgetService } from '../chat.js'; +import { IChatAccessibilityService, IChatWidgetService } from '../chat.js'; import { CHAT_CATEGORY } from './chatActions.js'; export const MarkUnhelpfulActionId = 'workbench.action.chat.markUnhelpful'; @@ -260,6 +260,8 @@ export function registerChatTitleActions() { const request = chatModel?.getRequests().find(candidate => candidate.id === item.requestId); const languageModelId = widget?.input.currentLanguageModel; + const chatAccessibilityService = accessor.get(IChatAccessibilityService); + chatAccessibilityService.acceptRequest(item.sessionResource); chatService.resendRequest(request!, { userSelectedModelId: languageModelId, attempt: (request?.attempt ?? -1) + 1, diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index a41ddd33bb8..5f2d7f4515d 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -34,6 +34,7 @@ export interface IAgentSessionsModel { readonly onDidResolve: Event; readonly onDidChangeSessions: Event; + readonly onDidChangeSessionArchivedState: Event; readonly sessions: IAgentSession[]; getSession(resource: URI): IAgentSession | undefined; @@ -348,6 +349,9 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode private readonly _onDidChangeSessions = this._register(new Emitter()); readonly onDidChangeSessions = this._onDidChangeSessions.event; + private readonly _onDidChangeSessionArchivedState = this._register(new Emitter()); + readonly onDidChangeSessionArchivedState = this._onDidChangeSessionArchivedState.event; + private _sessions: ResourceMap; get sessions(): IAgentSession[] { return Array.from(this._sessions.values()); } @@ -562,6 +566,11 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode const state = this.sessionStates.get(session.resource) ?? { archived: false, read: 0 }; this.sessionStates.set(session.resource, { ...state, archived }); + const agentSession = this._sessions.get(session.resource); + if (agentSession) { + this._onDidChangeSessionArchivedState.fire(agentSession); + } + this._onDidChangeSessions.fire(); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts index 2755033c631..071059491be 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts @@ -14,7 +14,7 @@ import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IEditorGroupsService, IEditorWorkingSet } from '../../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; -import { IAgentSession } from '../agentSessionsModel.js'; +import { IAgentSession, isSessionInProgressStatus } from '../agentSessionsModel.js'; import { ChatViewPaneTarget, IChatWidgetService } from '../../chat.js'; import { AgentSessionProviders } from '../agentSessions.js'; import { IChatSessionsService } from '../../../common/chatSessionsService.js'; @@ -27,6 +27,7 @@ import { inAgentSessionProjection } from './agentSessionProjection.js'; import { ChatConfiguration } from '../../../common/constants.js'; import { ISessionOpenOptions, sessionOpenerRegistry } from '../agentSessionsOpener.js'; import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; +import { IAgentSessionsService } from '../agentSessionsService.js'; //#region Configuration @@ -70,8 +71,9 @@ export interface IAgentSessionProjectionService { /** * Exit projection mode. + * @param options.startNewChat If true (default), starts a new chat after exiting. Set to false to keep the current chat open. */ - exitProjection(): Promise; + exitProjection(options?: { startNewChat?: boolean }): Promise; } export const IAgentSessionProjectionService = createDecorator('agentSessionProjectionService'); @@ -116,6 +118,7 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS @ICommandService private readonly commandService: ICommandService, @IChatEditingService private readonly chatEditingService: IChatEditingService, @IAgentTitleBarStatusService private readonly agentTitleBarStatusService: IAgentTitleBarStatusService, + @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, ) { super(); @@ -123,6 +126,11 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS // Listen for editor close events to exit projection mode when all editors are closed this._register(this.editorService.onDidCloseEditor(() => this._checkForEmptyEditors())); + + // Listen for session changes to exit projection mode if active session becomes in progress + // Note: onDidChangeSessions fires for any session change, but _checkForInProgressSession() + // has early exit guards and only checks when projection mode is active, making this efficient + this._register(this.agentSessionsService.model.onDidChangeSessions(() => this._checkForInProgressSession())); } private _isEnabled(): boolean { @@ -144,6 +152,37 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS } } + private _checkForInProgressSession(): void { + // Only check if we're in projection mode + if (!this._isActive || !this._activeSession) { + return; + } + + // Get the updated session from the model + const updatedSession = this.agentSessionsService.getSession(this._activeSession.resource); + if (!updatedSession) { + return; + } + + // If the session is now in progress, exit projection mode + if (isSessionInProgressStatus(updatedSession.status)) { + this.logService.trace('[AgentSessionProjection] Active session transitioned to in-progress, exiting projection mode'); + this.exitProjection({ startNewChat: false }); + } + } + + /** + * Opens a session in the chat panel without entering projection mode. + */ + private async _openSessionInChatPanel(session: IAgentSession): Promise { + session.setRead(true); + await this.chatSessionsService.activateChatSessionItemProvider(session.providerType); + await this.chatWidgetService.openSession(session.resource, ChatViewPaneTarget, { + title: { preferred: session.label }, + revealIfOpened: true + }); + } + /** * Open the session's files in a multi-diff editor. * @returns true if any files were opened, false if nothing to display @@ -208,6 +247,14 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS return; } + // Never enter projection mode for sessions that are in progress + // The user should only be in projection mode when reviewing completed code + if (isSessionInProgressStatus(session.status)) { + this.logService.trace('[AgentSessionProjection] Session is in progress, opening chat without projection mode'); + await this._openSessionInChatPanel(session); + return; + } + // For local sessions, check if there are pending edits to show // If there's nothing to focus, just open the chat without entering projection mode let hasUndecidedChanges = true; @@ -273,12 +320,7 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS } // Open the session in the chat panel (always, even without changes) - session.setRead(true); - await this.chatSessionsService.activateChatSessionItemProvider(session.providerType); - await this.chatWidgetService.openSession(session.resource, ChatViewPaneTarget, { - title: { preferred: session.label }, - revealIfOpened: true - }); + await this._openSessionInChatPanel(session); // For local sessions with changes, also pop open the edit session's changes view // Must be after openSession so the editing session context is available @@ -287,11 +329,13 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS } } - async exitProjection(): Promise { + async exitProjection(options?: { startNewChat?: boolean }): Promise { if (!this._isActive) { return; } + const startNewChat = options?.startNewChat ?? true; + // Save the current session's working set before exiting if (this._activeSession) { const sessionKey = this._activeSession.resource.toString(); @@ -323,8 +367,10 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS this._onDidChangeProjectionMode.fire(false); this._onDidChangeActiveSession.fire(undefined); - // Start a new chat to clear the sidebar - await this.commandService.executeCommand(ACTION_ID_NEW_CHAT); + // Start a new chat to clear the sidebar (unless caller wants to keep current chat) + if (startNewChat) { + await this.commandService.executeCommand(ACTION_ID_NEW_CHAT); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts index 7afb4fbac28..968304cc64e 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts @@ -50,6 +50,8 @@ const QUICK_OPEN_ACTION_ID = 'workbench.action.quickOpenWithModes'; // Storage key for filter state const FILTER_STORAGE_KEY = 'agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu'; +// Storage key for saving user's filter state before we override it +const PREVIOUS_FILTER_STORAGE_KEY = 'agentSessions.filterExcludes.previousUserFilter'; const NLS_EXTENSION_HOST = localize('devExtensionWindowTitlePrefix', "[Extension Development Host]"); const TITLE_DIRTY = '\u25cf '; @@ -673,14 +675,14 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { /** * Clear the filter if the currently filtered category becomes empty. - * For example, if filtered to "unread" but no unread sessions exist, clear the filter. + * For example, if filtered to "unread" but no unread sessions exist, restore user's previous filter. */ private _clearFilterIfCategoryEmpty(hasUnreadSessions: boolean, hasActiveSessions: boolean): void { const { isFilteredToUnread, isFilteredToInProgress } = this._getCurrentFilterState(); - // Clear filter if filtered category is now empty + // Restore user's filter if filtered category is now empty if ((isFilteredToUnread && !hasUnreadSessions) || (isFilteredToInProgress && !hasActiveSessions)) { - this._clearFilter(); + this._restoreUserFilter(); } } @@ -736,7 +738,48 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { } /** - * Opens the agent sessions view with a specific filter applied, or clears filter if already applied. + * Save the current user filter before we override it with a badge filter. + * Only saves if the current filter is NOT already a badge filter (unread or in-progress). + * This preserves the original user filter when switching between badge filters. + */ + private _saveUserFilter(): void { + const { isFilteredToUnread, isFilteredToInProgress } = this._getCurrentFilterState(); + + // Don't overwrite the saved filter if we're already in a badge-filtered state + // The previous user filter should already be saved + if (isFilteredToUnread || isFilteredToInProgress) { + return; + } + + const currentFilter = this._getStoredFilter(); + if (currentFilter) { + this.storageService.store(PREVIOUS_FILTER_STORAGE_KEY, JSON.stringify(currentFilter), StorageScope.PROFILE, StorageTarget.USER); + } + } + + /** + * Restore the user's previous filter (saved before we applied a badge filter). + */ + private _restoreUserFilter(): void { + const previousFilterStr = this.storageService.get(PREVIOUS_FILTER_STORAGE_KEY, StorageScope.PROFILE); + if (previousFilterStr) { + try { + const previousFilter = JSON.parse(previousFilterStr); + this._storeFilter(previousFilter); + } catch { + // Fall back to clearing if parse fails + this._clearFilter(); + } + } else { + // No previous filter saved, clear to default + this._clearFilter(); + } + // Clear the saved filter after restoring + this.storageService.remove(PREVIOUS_FILTER_STORAGE_KEY, StorageScope.PROFILE); + } + + /** + * Opens the agent sessions view with a specific filter applied, or restores previous filter if already applied. * @param filterType 'unread' to show only unread sessions, 'inProgress' to show only in-progress sessions */ private _openSessionsWithFilter(filterType: 'unread' | 'inProgress'): void { @@ -745,8 +788,11 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { // Toggle filter based on current state if (filterType === 'unread') { if (isFilteredToUnread) { - this._clearFilter(); + // Already filtered to unread - restore user's previous filter + this._restoreUserFilter(); } else { + // Save current filter before applying our own + this._saveUserFilter(); // Exclude read sessions to show only unread this._storeFilter({ providers: [], @@ -757,8 +803,11 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { } } else { if (isFilteredToInProgress) { - this._clearFilter(); + // Already filtered to in-progress - restore user's previous filter + this._restoreUserFilter(); } else { + // Save current filter before applying our own + this._saveUserFilter(); // Exclude Completed and Failed to show InProgress and NeedsInput this._storeFilter({ providers: [], diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css index 7f64094c2b4..cf18ea2566a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css @@ -5,12 +5,12 @@ /* Style all tabs with the same background as the agent status */ .monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent) !important; + background-color: var(--vscode-quickInput-background) !important; } /* Active tab gets slightly stronger tint */ .monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent) !important; + background-color: var(--vscode-chat-requestBubbleBackground) !important; } .hc-black .monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab, @@ -27,15 +27,15 @@ @keyframes agent-session-projection-glow-pulse { 0%, 100% { box-shadow: - 0 0 8px 2px color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent), - 0 0 20px 4px color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent), - inset 0 0 15px 2px color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); + 0 0 8px 2px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 50%, transparent), + 0 0 20px 4px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 25%, transparent), + inset 0 0 15px 2px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 15%, transparent); } 50% { box-shadow: - 0 0 15px 4px color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent), - 0 0 35px 8px color-mix(in srgb, var(--vscode-progressBar-background) 35%, transparent), - inset 0 0 25px 4px color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); + 0 0 15px 4px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 70%, transparent), + 0 0 35px 8px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 35%, transparent), + inset 0 0 25px 4px color-mix(in srgb, var(--vscode-chat-requestBubbleBackground) 25%, transparent); } } @@ -45,7 +45,7 @@ inset: 0; pointer-events: none; z-index: 1000; - border: 2px solid var(--vscode-progressBar-background); + border: 2px solid var(--vscode-chat-requestBubbleBackground); border-radius: 4px; animation: agent-session-projection-glow-pulse 2s ease-in-out infinite; } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css index 0dfed92e7b6..7910d82f62f 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css @@ -88,13 +88,13 @@ /* Active state - has running sessions */ .agent-status-pill.chat-input-mode.has-active { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); - border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); + background-color: var(--vscode-quickInput-background); + border: 1px solid var(--vscode-commandCenter-border, transparent); } .agent-status-pill.chat-input-mode.has-active:hover { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); - border-color: color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent); + background-color: var(--vscode-chat-requestBubbleBackground); + border-color: var(--vscode-commandCenter-border, transparent); } .agent-status-pill.chat-input-mode.has-active .agent-status-label { @@ -105,17 +105,18 @@ /* Unread state - has unread sessions (no background change, just indicator) */ .agent-status-pill.chat-input-mode.has-unread .agent-status-icon { font-size: 8px; + } /* Needs attention state - session requires user approval/confirmation/input */ .agent-status-pill.chat-input-mode.needs-attention { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); - border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); + background-color: var(--vscode-quickInput-background); + border: 1px solid var(--vscode-commandCenter-border, transparent); } .agent-status-pill.chat-input-mode.needs-attention:hover { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); - border-color: color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent); + background-color: var(--vscode-chat-requestBubbleBackground); + border-color: var(--vscode-commandCenter-border, transparent); } .agent-status-pill.chat-input-mode.needs-attention .agent-status-label { @@ -125,14 +126,14 @@ /* Session mode (viewing a session) */ .agent-status-pill.session-mode { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent); - border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent); + background-color: var(--vscode-quickInput-background); + border: 1px solid var(--vscode-commandCenter-border, transparent); padding: 0 12px; } .agent-status-pill.session-mode:hover { - background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent); - border-color: color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent); + background-color: var(--vscode-chat-requestBubbleBackground); + border-color: var(--vscode-commandCenter-border, transparent); } /* Label (workspace name, centered) */ diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index 81873cd34c1..c93d58d4be3 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -600,7 +600,7 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { } // Setup tooltip hover for string context attachments - if (isStringVariableEntry(attachment) && attachment.tooltip) { + if ((isStringVariableEntry(attachment) || attachment.kind === 'generic') && attachment.tooltip) { this._setupTooltipHover(attachment.tooltip); } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index c834654c56a..2247e57ad5a 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -53,7 +53,6 @@ import { ILanguageModelStatsService, LanguageModelStatsService } from '../common import { ILanguageModelToolsConfirmationService } from '../common/tools/languageModelToolsConfirmationService.js'; import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js'; import { ChatPromptFilesExtensionPointHandler } from '../common/promptSyntax/chatPromptFilesContribution.js'; -import { ChatPromptContentStore, IChatPromptContentStore } from '../common/promptSyntax/chatPromptContentStore.js'; import { PromptsConfig } from '../common/promptSyntax/config/config.js'; import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, DEFAULT_SKILL_SOURCE_FOLDERS, AGENTS_SOURCE_FOLDER, AGENT_FILE_EXTENSION, SKILL_FILENAME } from '../common/promptSyntax/config/promptFileLocations.js'; import { PromptLanguageFeaturesProvider } from '../common/promptSyntax/promptFileContributions.js'; @@ -95,8 +94,6 @@ import { ChatAttachmentResolveService, IChatAttachmentResolveService } from './a import { ChatMarkdownAnchorService, IChatMarkdownAnchorService } from './widget/chatContentParts/chatMarkdownAnchorService.js'; import { ChatContextPickService, IChatContextPickService } from './attachments/chatContextPickService.js'; import { ChatInputBoxContentProvider } from './widget/input/editor/chatEditorInputContentProvider.js'; -import { ChatPromptContentProvider } from './promptSyntax/chatPromptContentProvider.js'; -import './promptSyntax/chatPromptFileSystemProvider.js'; import { ChatEditingEditorAccessibility } from './chatEditing/chatEditingEditorAccessibility.js'; import { registerChatEditorActions } from './chatEditing/chatEditingEditorActions.js'; import { ChatEditingEditorContextKeys } from './chatEditing/chatEditingEditorContextKeys.js'; @@ -116,7 +113,6 @@ import { ChatPasteProvidersFeature } from './widget/input/editor/chatPasteProvid import { QuickChatService } from './widgetHosts/chatQuick.js'; import { ChatResponseAccessibleView } from './accessibility/chatResponseAccessibleView.js'; import { ChatTerminalOutputAccessibleView } from './accessibility/chatTerminalOutputAccessibleView.js'; -import { ChatThinkingAccessibleView } from './accessibility/chatThinkingAccessibleView.js'; import { ChatSetupContribution, ChatTeardownContribution } from './chatSetup/chatSetupContributions.js'; import { ChatStatusBarEntry } from './chatStatus/chatStatusEntry.js'; import { ChatVariablesService } from './attachments/chatVariables.js'; @@ -589,7 +585,6 @@ configurationRegistry.registerConfiguration({ type: 'boolean', description: nls.localize('chat.mathEnabled.description', "Enable math rendering in chat responses using KaTeX."), default: true, - tags: ['preview'], }, [ChatConfiguration.ShowCodeBlockProgressAnimation]: { type: 'boolean', @@ -1180,7 +1175,6 @@ class ToolReferenceNamesContribution extends Disposable implements IWorkbenchCon } AccessibleViewRegistry.register(new ChatTerminalOutputAccessibleView()); -AccessibleViewRegistry.register(new ChatThinkingAccessibleView()); AccessibleViewRegistry.register(new ChatResponseAccessibleView()); AccessibleViewRegistry.register(new PanelChatAccessibilityHelp()); AccessibleViewRegistry.register(new QuickChatAccessibilityHelp()); @@ -1188,7 +1182,6 @@ AccessibleViewRegistry.register(new EditsChatAccessibilityHelp()); AccessibleViewRegistry.register(new AgentChatAccessibilityHelp()); registerEditorFeature(ChatInputBoxContentProvider); -registerEditorFeature(ChatPromptContentProvider); class ChatSlashStaticSlashCommandsContribution extends Disposable { @@ -1350,7 +1343,6 @@ registerSingleton(IChatEditingService, ChatEditingService, InstantiationType.Del registerSingleton(IChatMarkdownAnchorService, ChatMarkdownAnchorService, InstantiationType.Delayed); registerSingleton(ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService, InstantiationType.Delayed); registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed); -registerSingleton(IChatPromptContentStore, ChatPromptContentStore, InstantiationType.Delayed); registerSingleton(IChatContextPickService, ChatContextPickService, InstantiationType.Delayed); registerSingleton(IChatModeService, ChatModeService, InstantiationType.Delayed); registerSingleton(IChatAttachmentResolveService, ChatAttachmentResolveService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptContentProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptContentProvider.ts deleted file mode 100644 index b193910212f..00000000000 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptContentProvider.ts +++ /dev/null @@ -1,49 +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 } from '../../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { ILanguageService } from '../../../../../editor/common/languages/language.js'; -import { ITextModel } from '../../../../../editor/common/model.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; -import { ITextModelContentProvider, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { IChatPromptContentStore } from '../../common/promptSyntax/chatPromptContentStore.js'; -import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js'; - -/** - * Content provider for virtual chat prompt files created with inline content. - * These URIs have the scheme 'vscode-chat-prompt' and retrieve their content - * from the {@link IChatPromptContentStore} which maintains an in-memory map - * of content indexed by URI. This approach avoids putting content in the URI - * query string which is a misuse of URIs. - */ -export class ChatPromptContentProvider extends Disposable implements ITextModelContentProvider { - constructor( - @ITextModelService textModelService: ITextModelService, - @IModelService private readonly modelService: IModelService, - @ILanguageService private readonly languageService: ILanguageService, - @IChatPromptContentStore private readonly chatPromptContentStore: IChatPromptContentStore - ) { - super(); - this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeChatPrompt, this)); - } - - async provideTextContent(resource: URI): Promise { - const existing = this.modelService.getModel(resource); - if (existing) { - return existing; - } - - // Get the content from the content store - const content = this.chatPromptContentStore.getContent(resource) ?? ''; - - return this.modelService.createModel( - content, - this.languageService.createById(PROMPT_LANGUAGE_ID), - resource - ); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptFileSystemProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptFileSystemProvider.ts deleted file mode 100644 index daee1c26006..00000000000 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptFileSystemProvider.ts +++ /dev/null @@ -1,119 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event } from '../../../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileType, IFileWriteOptions, IFileSystemProvider, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, createFileSystemProviderError, FileSystemProviderErrorCode, IFileService } from '../../../../../platform/files/common/files.js'; -import { IChatPromptContentStore } from '../../common/promptSyntax/chatPromptContentStore.js'; -import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../../common/contributions.js'; - -/** - * File system provider for virtual chat prompt files created with inline content. - * These URIs have the scheme 'vscode-chat-prompt' and retrieve their content - * from the {@link IChatPromptContentStore} which maintains an in-memory map - * of content indexed by URI. - * - * This enables external extensions to use VS Code's file system API to read - * these virtual prompt files. - */ -export class ChatPromptFileSystemProvider implements IFileSystemProvider, IFileSystemProviderWithFileReadWriteCapability { - - get capabilities() { - return FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.Readonly; - } - - constructor( - private readonly chatPromptContentStore: IChatPromptContentStore - ) { } - - //#region Supported File Operations - - async stat(resource: URI): Promise { - const content = this.chatPromptContentStore.getContent(resource); - if (content === undefined) { - throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound); - } - - const size = VSBuffer.fromString(content).byteLength; - - return { - type: FileType.File, - ctime: 0, - mtime: 0, - size - }; - } - - async readFile(resource: URI): Promise { - const content = this.chatPromptContentStore.getContent(resource); - if (content === undefined) { - throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound); - } - - return VSBuffer.fromString(content).buffer; - } - - //#endregion - - //#region Unsupported File Operations - - readonly onDidChangeCapabilities = Event.None; - readonly onDidChangeFile = Event.None; - - async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise { - throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); - } - - async mkdir(resource: URI): Promise { - throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); - } - - async readdir(resource: URI): Promise<[string, FileType][]> { - return []; - } - - async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { - throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); - } - - async delete(resource: URI, opts: IFileDeleteOptions): Promise { - throw createFileSystemProviderError('not allowed', FileSystemProviderErrorCode.NoPermissions); - } - - watch(resource: URI, opts: IWatchOptions): IDisposable { - return Disposable.None; - } - - //#endregion -} - -/** - * Workbench contribution that registers the chat prompt file system provider. - */ -export class ChatPromptFileSystemProviderContribution extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'workbench.contrib.chatPromptFileSystemProvider'; - - constructor( - @IFileService fileService: IFileService, - @IChatPromptContentStore chatPromptContentStore: IChatPromptContentStore - ) { - super(); - - this._register(fileService.registerProvider( - Schemas.vscodeChatPrompt, - new ChatPromptFileSystemProvider(chatPromptContentStore) - )); - } -} - -registerWorkbenchContribution2( - ChatPromptFileSystemProviderContribution.ID, - ChatPromptFileSystemProviderContribution, - WorkbenchPhase.Eventually -); diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 350a8b6f0f3..5e392640d39 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -205,6 +205,24 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo this._logService.trace(`LanguageModelToolsService#isPermitted: ToolSet ${toolOrToolSet.id} (${toolOrToolSet.referenceName}) permitted=${permitted}`); return permitted; } + for (const toolSet of this._toolSets) { + if (toolSet.source.type === 'internal' && permittedInternalToolSetIds.includes(toolSet.referenceName)) { + for (const memberTool of toolSet.getTools()) { + if (memberTool.id === toolOrToolSet.id) { + this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=true (member of ${toolSet.referenceName})`); + return true; + } + } + } + } + + // Special case for 'vscode_fetchWebPage_internal', which is allowed if we allow 'web' tools + // Fetch is implemented with two tools, this one and 'copilot_fetchWebPage' + if (toolOrToolSet.id === 'vscode_fetchWebPage_internal' && permittedInternalToolSetIds.includes(SpecedToolAliases.web)) { + this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=true (special case)`); + return true; + } + this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=false`); return false; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 6ed7bee0fb8..d52eb041205 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -549,34 +549,49 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen - Summarize ALL actions, not just tool calls. If there's reasoning or analysis without tool calls, summarize that too - Examples of non-tool actions: "Analyzing code structure", "Planning implementation", "Reviewing dependencies" + TOOL NAME FILTERING: + - NEVER include tool names like "Replace String in File", "Multi Replace String in File", "Create File", "Read File", etc. in the output + - If an action says "Edited X and used Replace String in File", output ONLY the action on X + - Tool names describe HOW something was done, not WHAT was done - always omit them + + VOCABULARY - Use varied synonyms for natural-sounding summaries: + - For edits: "Updated", "Modified", "Changed", "Refactored", "Fixed", "Adjusted" + - For reads: "Reviewed", "Examined", "Checked", "Inspected", "Analyzed", "Explored" + - For creates: "Created", "Added", "Generated" + - For searches: "Searched for", "Looked up", "Investigated" + - For terminal: "Ran command", "Executed" + - Choose the synonym that best fits the context of what was done + RULES FOR TOOL CALLS: - 1. If the SAME file was both edited AND read: Start with "Read and edited " - 2. If exactly ONE file was edited: Start with "Edited " (include actual filename) - 3. If exactly ONE file was read: Start with "Read " (include actual filename) - 4. If MULTIPLE files were edited: Start with "Edited X files" - 5. If MULTIPLE files were read: Start with "Read X files" - 6. If BOTH edits AND reads occurred on DIFFERENT files: Start with "Edited and read " if one each, otherwise "Edited X files and read Y files" - 7. For searches: Say "searched for " with the actual search term, NOT "searched for files" - 8. After the file info, you may add a brief summary of other actions (e.g., ran terminal, searched for X) if space permits + 1. If the SAME file was both edited AND read: Use a combined phrase like "Reviewed and updated " + 2. If exactly ONE file was edited: Start with an edit synonym + "" (include actual filename) + 3. If exactly ONE file was read: Start with a read synonym + "" (include actual filename) + 4. If MULTIPLE files were edited: Start with an edit synonym + "X files" + 5. If MULTIPLE files were read: Start with a read synonym + "X files" + 6. If BOTH edits AND reads occurred on DIFFERENT files: Combine them naturally + 7. For searches: Say "searched for " or "looked up " with the actual search term, NOT "searched for files" + 8. After the file info, you may add a brief summary of other actions if space permits 9. NEVER say "1 file" - always use the actual filename when there's only one file EXAMPLES: - - "Read HomePage.tsx, Edited HomePage.tsx" → "Read and edited HomePage.tsx" - - "Edited HomePage.tsx" → "Edited HomePage.tsx" - - "Read config.json, Read package.json" → "Read 2 files" - - "Edited App.tsx, Read utils.ts" → "Edited App.tsx and read utils.ts" - - "Edited App.tsx, Read utils.ts, Read types.ts" → "Edited App.tsx and read 2 files" - - "Edited index.ts, Edited styles.css, Ran terminal command" → "Edited 2 files and ran command" - - "Read README.md, Searched for AuthService" → "Read README.md and searched for AuthService" + - "Read HomePage.tsx, Edited HomePage.tsx" → "Reviewed and updated HomePage.tsx" + - "Edited HomePage.tsx" → "Updated HomePage.tsx" + - "Edited config.css and used Replace String in File" → "Modified config.css" + - "Edited App.tsx, used Multi Replace String in File" → "Refactored App.tsx" + - "Read config.json, Read package.json" → "Reviewed 2 files" + - "Edited App.tsx, Read utils.ts" → "Updated App.tsx and checked utils.ts" + - "Edited App.tsx, Read utils.ts, Read types.ts" → "Updated App.tsx and reviewed 2 files" + - "Edited index.ts, Edited styles.css, Ran terminal command" → "Modified 2 files and ran command" + - "Read README.md, Searched for AuthService" → "Checked README.md and searched for AuthService" - "Searched for login, Searched for authentication" → "Searched for login and authentication" - - "Edited api.ts, Edited models.ts, Read schema.json" → "Edited 2 files and read schema.json" - - "Edited Button.tsx, Edited Button.css, Edited index.ts" → "Edited 3 files" - - "Searched codebase for error handling" → "Searched for error handling" - - "Grep search for useState, Read App.tsx" → "Read App.tsx and searched for useState" + - "Edited api.ts, Edited models.ts, Read schema.json" → "Updated 2 files and reviewed schema.json" + - "Edited Button.tsx, Edited Button.css, Edited index.ts" → "Modified 3 files" + - "Searched codebase for error handling" → "Looked up error handling" + - "Grep search for useState, Read App.tsx" → "Examined App.tsx and searched for useState" - "Analyzing component architecture" → "Analyzed component architecture" - - "Planning refactor strategy, Read utils.ts" → "Planned refactor and read utils.ts" + - "Planning refactor strategy, Read utils.ts" → "Planned refactor and reviewed utils.ts" - No quotes, no trailing punctuation. Never say "searched for files" - always include the actual search term. + No quotes, no trailing punctuation. Never say "searched for files" - always include the actual search term. Never include tool names. Actions: ${context}`; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css index 3d606a77814..3821ce19322 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatSubagentContent.css @@ -37,6 +37,11 @@ display: none; } + /* renable icons in confirmation widget */ + .chat-confirmation-widget-container .codicon:not(.codicon-terminal) { + display: inline-flex; + } + .chat-collapsible-markdown-content { .rendered-markdown { font-size: var(--vscode-chat-font-size-body-s); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts index 9360828c6ed..e2b59181043 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts @@ -9,6 +9,8 @@ import { disposableTimeout } from '../../../../../../../base/common/async.js'; import { decodeBase64 } from '../../../../../../../base/common/buffer.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../../../../base/common/event.js'; +import { hash } from '../../../../../../../base/common/hash.js'; +import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { autorun, autorunSelfDisposable, derived, IObservable, observableValue } from '../../../../../../../base/common/observable.js'; import { basename } from '../../../../../../../base/common/resources.js'; @@ -396,8 +398,12 @@ export class ChatMcpAppModel extends Disposable { result = await this._handleUiMessage(request.params); break; + case 'ui/update-model-context': + result = await this._handleUpdateModelContext(request.params); + break; + case 'notifications/message': - await this._mcpToolCallUI.log(request.params); + await this._mcpToolCallUI.log(request.params as MCP.LoggingMessageNotification['params']); break; default: { @@ -474,8 +480,15 @@ export class ChatMcpAppModel extends Disposable { logging: {}, sandbox: { csp: this._latestCsp, - permissions: { clipboardWrite: true }, + permissions: { clipboardWrite: {} }, }, + updateModelContext: { + audio: {}, + image: {}, + resourceLink: {}, + resource: {}, + structuredContent: {}, + } }, hostContext: this.hostContext.get(), } satisfies Required; @@ -520,6 +533,68 @@ export class ChatMcpAppModel extends Disposable { return { isError: false }; } + private async _handleUpdateModelContext(params: McpApps.McpUiUpdateModelContextRequest['params']): Promise { + const widget = this._chatWidgetService.getWidgetBySessionResource(this.renderData.sessionResource); + if (!widget) { + return {}; + } + + const idPrefix = `mcpui-context-${hash(this.renderData.serverDefinitionId)}-`; + const toDelete = widget.attachmentModel.getAttachmentIDs(); + const idsToDelete = Array.from(toDelete).filter(id => id.startsWith(idPrefix)); + const entries: IChatRequestVariableEntry[] = []; + let entryIndex = 0; + + if (params.content) { + for (const block of params.content) { + const id = `${idPrefix}${entryIndex++}`; + if (block.type === 'image') { + entries.push({ + kind: 'image', + value: decodeBase64(block.data).buffer, + id, + name: 'Image', + mimeType: block.mimeType, + }); + } else if (block.type === 'resource_link') { + const uri = McpResourceURI.fromServer({ id: this.renderData.serverDefinitionId, label: '' }, block.uri); + entries.push({ + kind: 'file', + value: uri, + id, + name: basename(uri), + }); + } else if (block.type === 'text') { + const preview = block.text.replaceAll(/\s+/g, ' ').trim(); + const truncateTo = 20; + entries.push({ + kind: 'generic', + value: block.text, + id, + tooltip: new MarkdownString().appendCodeblock('plaintext', block.text), + name: preview.length > truncateTo ? preview.slice(0, truncateTo) + '…' : preview, + }); + } + } + } + + if (params.structuredContent && Object.keys(params.structuredContent).length > 0) { + const id = `${idPrefix}structured`; + const value = JSON.stringify(params.structuredContent, null, 2); + entries.push({ + kind: 'generic', + value, + tooltip: new MarkdownString().appendCodeblock('json', value), + id, + name: 'UI Data', + }); + } + + widget.attachmentModel.updateContext(idsToDelete, entries); + + return {}; + } + private _handleSizeChanged(params: McpApps.McpUiSizeChangedNotification['params']): void { if (params.height !== undefined) { this._height = params.height; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts index c26ee8b2f81..11d2b3e74c7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts @@ -30,7 +30,7 @@ import { IChatRequestModeInfo } from '../../common/model/chatModel.js'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; import { ChatAccessibilityProvider } from '../accessibility/chatAccessibilityProvider.js'; -import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions } from '../chat.js'; +import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions } from '../chat.js'; import { CodeBlockPart } from './chatContentParts/codeBlockPart.js'; import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js'; import { ChatEditorOptions } from './chatOptions.js'; @@ -257,6 +257,7 @@ export class ChatListWidget extends Disposable { @IContextMenuService private readonly contextMenuService: IContextMenuService, @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService, ) { super(); @@ -343,6 +344,7 @@ export class ChatListWidget extends Disposable { userSelectedModelId: this._getCurrentLanguageModelId?.(), modeInfo: this._getCurrentModeInfo?.(), }; + this.chatAccessibilityService.acceptRequest(e.sessionResource); this.chatService.resendRequest(request, sendOptions).catch(e => this.logService.error('FAILED to rerun request', e)); } })); diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index 39bc9522e0a..0426c7fba40 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -41,6 +41,7 @@ interface IBaseChatRequestVariableEntry { export interface IGenericChatRequestVariableEntry extends IBaseChatRequestVariableEntry { kind: 'generic'; + tooltip?: IMarkdownString; } export interface IChatRequestDirectoryEntry extends IBaseChatRequestVariableEntry { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts deleted file mode 100644 index a28402f8897..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts +++ /dev/null @@ -1,74 +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 } from '../../../../../base/common/lifecycle.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; - -export const IChatPromptContentStore = createDecorator('chatPromptContentStore'); - -/** - * Service for managing virtual chat prompt content. - * - * This store maintains an in-memory map of content indexed by URI. - * URIs use the vscode-chat-prompt scheme with just the ID in the path, - * avoiding the need to encode large content in the URI query string. - */ -export interface IChatPromptContentStore { - readonly _serviceBrand: undefined; - - /** - * Registers content for a given URI. - * @param uri The URI to associate with the content. - * @param content The content to store. - * @returns A disposable that removes the content when disposed. - */ - registerContent(uri: URI, content: string): { dispose: () => void }; - - /** - * Retrieves content by URI. - * @param uri The URI to look up. - * @returns The content if found, or undefined. - */ - getContent(uri: URI): string | undefined; -} - -export class ChatPromptContentStore extends Disposable implements IChatPromptContentStore { - readonly _serviceBrand: undefined; - - private readonly _contentMap = new Map(); - - constructor() { - super(); - } - - /** - * Normalizes a URI by stripping query and fragment for consistent lookup. - * Query parameters like vscodeLinkType are metadata for rendering, not content identification. - */ - private normalizeUri(uri: URI): string { - return uri.with({ query: '', fragment: '' }).toString(); - } - - registerContent(uri: URI, content: string): { dispose: () => void } { - const key = this.normalizeUri(uri); - this._contentMap.set(key, content); - - const dispose = () => { - this._contentMap.delete(key); - }; - - return { dispose }; - } - - getContent(uri: URI): string | undefined { - return this._contentMap.get(this.normalizeUri(uri)); - } - - override dispose(): void { - this._contentMap.clear(); - super.dispose(); - } -} 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 69735174644..604ea54f149 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -36,18 +36,6 @@ export interface IPromptFileResource { * The URI to the agent or prompt resource file. */ readonly uri: URI; - - /** - * Indicates whether the custom agent resource is editable. Defaults to false. - */ - readonly isEditable?: boolean; - - /** - * The inline content for virtual prompt files. This property is only used - * during IPC transfer from extension host to main thread - the content is - * immediately registered with the ChatPromptContentStore and not passed further. - */ - readonly content?: string; } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index d25cc1fd8b5..20f827a4d70 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -34,7 +34,6 @@ import { PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../p import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; -import { IChatPromptContentStore } from '../chatPromptContentStore.js'; /** * Error thrown when a skill file is missing the required name attribute. @@ -129,7 +128,6 @@ export class PromptsService extends Disposable implements IPromptsService { @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IChatPromptContentStore private readonly chatPromptContentStore: IChatPromptContentStore ) { super(); @@ -298,15 +296,6 @@ export class PromptsService extends Disposable implements IPromptsService { } for (const file of files) { - if (!file.isEditable) { - try { - await this.filesConfigService.updateReadonly(file.uri, true); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - this.logger.error(`[listFromProviders] Failed to make file readonly: ${file.uri}`, msg); - } - } - result.push({ uri: file.uri, storage: PromptsStorage.extension, @@ -543,15 +532,6 @@ export class PromptsService extends Disposable implements IPromptsService { return this.getParsedPromptFile(model); } - // Handle virtual prompt URIs - get content from the content store - if (uri.scheme === Schemas.vscodeChatPrompt) { - const content = this.chatPromptContentStore.getContent(uri); - if (content !== undefined) { - return new PromptFileParser().parse(uri, content); - } - throw new Error(`Content not found in store for virtual prompt URI: ${uri.toString()}`); - } - const fileContent = await this.fileService.readFile(uri); if (token.isCancellationRequested) { throw new CancellationError(); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts index 8976bdfe7e4..8906d8756f9 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts @@ -54,6 +54,7 @@ suite('AgentSessionsDataSource', () => { onWillResolve: Event.None, onDidResolve: Event.None, onDidChangeSessions: Event.None, + onDidChangeSessionArchivedState: Event.None, resolve: async () => { }, }; } diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptContentProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptContentProvider.test.ts deleted file mode 100644 index 6323d234eba..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptContentProvider.test.ts +++ /dev/null @@ -1,215 +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 assert from 'assert'; -import { URI } from '../../../../../../base/common/uri.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { ILanguageService, ILanguageSelection } from '../../../../../../editor/common/languages/language.js'; -import { ITextModel } from '../../../../../../editor/common/model.js'; -import { IModelService } from '../../../../../../editor/common/services/model.js'; -import { ITextModelService } from '../../../../../../editor/common/services/resolverService.js'; -import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { ChatPromptContentProvider } from '../../../browser/promptSyntax/chatPromptContentProvider.js'; -import { ChatPromptContentStore, IChatPromptContentStore } from '../../../common/promptSyntax/chatPromptContentStore.js'; -import { PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/promptTypes.js'; -import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../../../base/common/network.js'; - -suite('ChatPromptContentProvider', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - - let instantiationService: TestInstantiationService; - let contentStore: ChatPromptContentStore; - let mockModelService: MockModelService; - let mockLanguageService: MockLanguageService; - let mockTextModelService: MockTextModelService; - let contentProvider: ChatPromptContentProvider; - - class MockLanguageSelection implements ILanguageSelection { - readonly languageId = PROMPT_LANGUAGE_ID; - readonly onDidChange = testDisposables.add(new (class extends Disposable { readonly event = () => ({ dispose: () => { } }); })()).event; - } - - class MockLanguageService { - createById(languageId: string): ILanguageSelection { - return new MockLanguageSelection(); - } - } - - class MockTextModel implements Partial { - constructor( - readonly uri: URI, - readonly content: string, - readonly languageId: string - ) { } - - getValue(): string { - return this.content; - } - - getLanguageId(): string { - return this.languageId; - } - } - - class MockModelService { - private models = new Map(); - - getModel(resource: URI): ITextModel | null { - return this.models.get(resource.toString()) ?? null; - } - - createModel(content: string, languageSelection: ILanguageSelection, resource: URI): ITextModel { - const model = new MockTextModel(resource, content, languageSelection.languageId) as unknown as ITextModel; - this.models.set(resource.toString(), model); - return model; - } - - setExistingModel(uri: URI, model: ITextModel): void { - this.models.set(uri.toString(), model); - } - - clear(): void { - this.models.clear(); - } - } - - class MockTextModelService { - private providers = new Map Promise }>(); - - registerTextModelContentProvider(scheme: string, provider: { provideTextContent: (resource: URI) => Promise }): IDisposable { - this.providers.set(scheme, provider); - return { dispose: () => this.providers.delete(scheme) }; - } - - getProvider(scheme: string) { - return this.providers.get(scheme); - } - } - - setup(() => { - instantiationService = testDisposables.add(new TestInstantiationService()); - - contentStore = testDisposables.add(new ChatPromptContentStore()); - mockModelService = new MockModelService(); - mockLanguageService = new MockLanguageService(); - mockTextModelService = new MockTextModelService(); - - instantiationService.stub(IChatPromptContentStore, contentStore); - instantiationService.stub(IModelService, mockModelService); - instantiationService.stub(ILanguageService, mockLanguageService as unknown as ILanguageService); - instantiationService.stub(ITextModelService, mockTextModelService as unknown as ITextModelService); - - contentProvider = testDisposables.add(instantiationService.createInstance(ChatPromptContentProvider)); - }); - - teardown(() => { - mockModelService.clear(); - }); - - test('registers as content provider for vscode-chat-prompt scheme', () => { - const provider = mockTextModelService.getProvider(Schemas.vscodeChatPrompt); - assert.ok(provider, 'Provider should be registered for vscode-chat-prompt scheme'); - }); - - test('provideTextContent creates model from stored content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/test-agent'); - const content = '# Test Agent\nThis is the agent content.'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const model = await contentProvider.provideTextContent(uri); - - assert.ok(model, 'Model should be created'); - assert.strictEqual((model as unknown as MockTextModel).getValue(), content); - assert.strictEqual((model as unknown as MockTextModel).getLanguageId(), PROMPT_LANGUAGE_ID); - }); - - test('provideTextContent returns existing model if available', async () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/existing'); - const existingContent = 'Existing model content'; - - const existingModel = new MockTextModel(uri, existingContent, PROMPT_LANGUAGE_ID) as unknown as ITextModel; - mockModelService.setExistingModel(uri, existingModel); - - const model = await contentProvider.provideTextContent(uri); - - assert.strictEqual(model, existingModel, 'Should return existing model'); - }); - - test('provideTextContent creates model with empty content when URI has no stored content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.instructions.md/missing'); - - const model = await contentProvider.provideTextContent(uri); - - assert.ok(model, 'Model should be created even without stored content'); - assert.strictEqual((model as unknown as MockTextModel).getValue(), ''); - }); - - test('provideTextContent uses prompt language ID', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/language-test'); - const content = 'Test content'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const model = await contentProvider.provideTextContent(uri); - - assert.ok(model); - assert.strictEqual((model as unknown as MockTextModel).getLanguageId(), PROMPT_LANGUAGE_ID); - }); - - test('handles multiple sequential requests for different URIs', async () => { - const uri1 = URI.parse('vscode-chat-prompt:/.agent.md/agent-1'); - const uri2 = URI.parse('vscode-chat-prompt:/.instructions.md/instructions-1'); - const uri3 = URI.parse('vscode-chat-prompt:/.prompt.md/prompt-1'); - - const content1 = 'Agent content'; - const content2 = 'Instructions content'; - const content3 = 'Prompt content'; - - testDisposables.add(contentStore.registerContent(uri1, content1)); - testDisposables.add(contentStore.registerContent(uri2, content2)); - testDisposables.add(contentStore.registerContent(uri3, content3)); - - const model1 = await contentProvider.provideTextContent(uri1); - const model2 = await contentProvider.provideTextContent(uri2); - const model3 = await contentProvider.provideTextContent(uri3); - - assert.strictEqual((model1 as unknown as MockTextModel).getValue(), content1); - assert.strictEqual((model2 as unknown as MockTextModel).getValue(), content2); - assert.strictEqual((model3 as unknown as MockTextModel).getValue(), content3); - }); - - test('content with special characters is handled correctly', async () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/special'); - const content = '# Unicode Test\n\n日本語テスト 🎉\n\n```typescript\nconst x = "hello";\n```'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const model = await contentProvider.provideTextContent(uri); - - assert.ok(model); - assert.strictEqual((model as unknown as MockTextModel).getValue(), content); - }); - - test('disposed content results in empty model', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/disposed-test'); - const content = 'Content that will be disposed'; - - const registration = contentStore.registerContent(uri, content); - - // Verify content exists - const model1 = await contentProvider.provideTextContent(uri); - assert.strictEqual((model1 as unknown as MockTextModel).getValue(), content); - - // Clear the model cache and dispose the content - mockModelService.clear(); - registration.dispose(); - - // Now requesting should return model with empty content - const model2 = await contentProvider.provideTextContent(uri); - assert.strictEqual((model2 as unknown as MockTextModel).getValue(), ''); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptFileSystemProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptFileSystemProvider.test.ts deleted file mode 100644 index f43e53d2cce..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptFileSystemProvider.test.ts +++ /dev/null @@ -1,257 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import assert from 'assert'; -import { URI } from '../../../../../../base/common/uri.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { VSBuffer } from '../../../../../../base/common/buffer.js'; -import { FileSystemProviderErrorCode, toFileSystemProviderErrorCode } from '../../../../../../platform/files/common/files.js'; -import { ChatPromptFileSystemProvider } from '../../../browser/promptSyntax/chatPromptFileSystemProvider.js'; -import { ChatPromptContentStore } from '../../../common/promptSyntax/chatPromptContentStore.js'; - -suite('ChatPromptFileSystemProvider', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - - let contentStore: ChatPromptContentStore; - let provider: ChatPromptFileSystemProvider; - - setup(() => { - contentStore = testDisposables.add(new ChatPromptContentStore()); - provider = new ChatPromptFileSystemProvider(contentStore); - }); - - suite('stat', () => { - test('returns stat for registered content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/test-agent'); - const content = '# Test Agent\nThis is test content.'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const stat = await provider.stat(uri); - - assert.strictEqual(stat.type, 1); // FileType.File - assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); - }); - - test('throws FileNotFound for unregistered URI', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/missing'); - - await assert.rejects( - () => provider.stat(uri), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; - }, - 'Should throw FileNotFound error' - ); - }); - - test('returns correct size for empty content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/empty'); - - testDisposables.add(contentStore.registerContent(uri, '')); - - const stat = await provider.stat(uri); - - assert.strictEqual(stat.size, 0); - }); - - test('returns correct size for unicode content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.instructions.md/unicode'); - const content = '日本語テスト 🎉'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const stat = await provider.stat(uri); - - assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); - }); - }); - - suite('readFile', () => { - test('returns content for registered URI', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/test-agent'); - const content = '# Test Agent\nThis is test content.'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const result = await provider.readFile(uri); - - assert.strictEqual(VSBuffer.wrap(result).toString(), content); - }); - - test('throws FileNotFound for unregistered URI', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/missing'); - - await assert.rejects( - () => provider.readFile(uri), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; - }, - 'Should throw FileNotFound error' - ); - }); - - test('returns empty buffer for empty content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/empty'); - - testDisposables.add(contentStore.registerContent(uri, '')); - - const result = await provider.readFile(uri); - - assert.strictEqual(result.byteLength, 0); - }); - - test('preserves unicode content', async () => { - const uri = URI.parse('vscode-chat-prompt:/.instructions.md/unicode'); - const content = '日本語テスト 🎉\n\n```typescript\nconst greeting = "こんにちは";\n```'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const result = await provider.readFile(uri); - - assert.strictEqual(VSBuffer.wrap(result).toString(), content); - }); - - test('handles content with special markdown characters', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/markdown'); - const content = '# Heading\n\n- List item\n- Another item\n\n> Blockquote\n\n```\ncode block\n```'; - - testDisposables.add(contentStore.registerContent(uri, content)); - - const result = await provider.readFile(uri); - - assert.strictEqual(VSBuffer.wrap(result).toString(), content); - }); - }); - - suite('content lifecycle', () => { - test('readFile fails after content is disposed', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/lifecycle-test'); - const content = 'Temporary content'; - - const registration = contentStore.registerContent(uri, content); - - // Verify content is readable - const result = await provider.readFile(uri); - assert.strictEqual(VSBuffer.wrap(result).toString(), content); - - // Dispose the content - registration.dispose(); - - // Now reading should fail - await assert.rejects( - () => provider.readFile(uri), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; - } - ); - }); - - test('stat fails after content is disposed', async () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/lifecycle-stat'); - const content = 'Content for stat test'; - - const registration = contentStore.registerContent(uri, content); - - // Verify stat works - const stat = await provider.stat(uri); - assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); - - // Dispose the content - registration.dispose(); - - // Now stat should fail - await assert.rejects( - () => provider.stat(uri), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.FileNotFound; - } - ); - }); - }); - - suite('URI normalization', () => { - test('readFile succeeds when URI has query parameters', async () => { - const baseUri = URI.parse('vscode-chat-prompt:/.agent.md/query-test'); - const content = 'Content for query test'; - - testDisposables.add(contentStore.registerContent(baseUri, content)); - - // Read with query parameters - const uriWithQuery = baseUri.with({ query: 'vscodeLinkType=prompt' }); - const result = await provider.readFile(uriWithQuery); - - assert.strictEqual(VSBuffer.wrap(result).toString(), content); - }); - - test('stat succeeds when URI has fragment', async () => { - const baseUri = URI.parse('vscode-chat-prompt:/.instructions.md/fragment-test'); - const content = 'Content for fragment test'; - - testDisposables.add(contentStore.registerContent(baseUri, content)); - - // Stat with fragment - const uriWithFragment = baseUri.with({ fragment: 'section1' }); - const stat = await provider.stat(uriWithFragment); - - assert.strictEqual(stat.size, VSBuffer.fromString(content).byteLength); - }); - }); - - suite('unsupported operations', () => { - test('writeFile throws NoPermissions error', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/write-test'); - - await assert.rejects( - () => provider.writeFile(uri, new Uint8Array(), { create: true, overwrite: true, unlock: false, atomic: false }), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; - } - ); - }); - - test('mkdir throws NoPermissions error', async () => { - const uri = URI.parse('vscode-chat-prompt:/test-dir'); - - await assert.rejects( - () => provider.mkdir(uri), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; - } - ); - }); - - test('delete throws NoPermissions error', async () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/delete-test'); - - await assert.rejects( - () => provider.delete(uri, { recursive: false, useTrash: false, atomic: false }), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; - } - ); - }); - - test('rename throws NoPermissions error', async () => { - const from = URI.parse('vscode-chat-prompt:/.agent.md/rename-from'); - const to = URI.parse('vscode-chat-prompt:/.agent.md/rename-to'); - - await assert.rejects( - () => provider.rename(from, to, { overwrite: false }), - (err: Error & { code?: string }) => { - return toFileSystemProviderErrorCode(err) === FileSystemProviderErrorCode.NoPermissions; - } - ); - }); - - test('readdir returns empty array', async () => { - const uri = URI.parse('vscode-chat-prompt:/'); - - const result = await provider.readdir(uri); - - assert.deepStrictEqual(result, []); - }); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts index 1637d051f47..abc09fce8e3 100644 --- a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts @@ -3140,4 +3140,342 @@ suite('LanguageModelToolsService', () => { assert.strictEqual(tools[0].id, 'dynamicTool2', 'should be dynamicTool2 after context change'); }); }); + + test('isPermitted allows tools in permitted toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create tool in the 'read' toolset (permitted) + const readTool: IToolData = { + id: 'readToolInSet', + toolReferenceName: 'readToolRef', + modelDescription: 'Read Tool in Set', + displayName: 'Read Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(readTool)); + store.add(service.readToolSet.addTool(readTool)); + + // Create standalone tool not in any permitted toolset + const standaloneTool: IToolData = { + id: 'standaloneTool', + toolReferenceName: 'standaloneRef', + modelDescription: 'Standalone Tool', + displayName: 'Standalone Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(standaloneTool)); + + // Get tools - should include the tool in the read toolset but not the standalone tool + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('readToolInSet'), 'Tool in read toolset should be permitted when agent mode is disabled'); + assert.ok(!toolIds.includes('standaloneTool'), 'Standalone tool not in permitted toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted allows all tools when agent mode is enabled', () => { + // Enable agent mode (default) + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, true); + + // Create tool in the 'read' toolset + const readTool: IToolData = { + id: 'readToolEnabled', + toolReferenceName: 'readToolEnabledRef', + modelDescription: 'Read Tool', + displayName: 'Read Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(readTool)); + store.add(service.readToolSet.addTool(readTool)); + + // Create standalone tool not in any permitted toolset + const standaloneTool: IToolData = { + id: 'standaloneToolEnabled', + toolReferenceName: 'standaloneEnabledRef', + modelDescription: 'Standalone Tool', + displayName: 'Standalone Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(standaloneTool)); + + // Get tools - both should be available when agent mode is enabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('readToolEnabled'), 'Tool in read toolset should be permitted when agent mode is enabled'); + assert.ok(toolIds.includes('standaloneToolEnabled'), 'Standalone tool should be permitted when agent mode is enabled'); + }); + + test('isPermitted filters toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a custom internal toolset that is NOT in the permitted list + const customToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'customToolSet', + 'customToolSetRef', + { description: 'Custom Tool Set' } + )); + + const customTool: IToolData = { + id: 'customToolInSet', + toolReferenceName: 'customToolRef', + modelDescription: 'Custom Tool', + displayName: 'Custom Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(customTool)); + store.add(customToolSet.addTool(customTool)); + + // Get toolsets - read/search/web should be available, custom should not + const toolSets = Array.from(service.toolSets.get()); + const toolSetIds = Array.from(toolSets).map(ts => ts.id); + + assert.ok(toolSetIds.includes('read'), 'read toolset should be permitted when agent mode is disabled'); + assert.ok(!toolSetIds.includes('customToolSet'), 'custom toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted allows execute toolset tools when agent mode is enabled', () => { + // Enable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, true); + + // Create tool in the 'execute' toolset (only permitted when agent mode is enabled) + const executeTool: IToolData = { + id: 'executeToolInSet', + toolReferenceName: 'executeToolRef', + modelDescription: 'Execute Tool', + displayName: 'Execute Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(executeTool)); + store.add(service.executeToolSet.addTool(executeTool)); + + // Get tools - execute tool should be available when agent mode is enabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('executeToolInSet'), 'Tool in execute toolset should be permitted when agent mode is enabled'); + }); + + test('isPermitted blocks execute toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create tool in the 'execute' toolset (NOT permitted when agent mode is disabled) + const executeTool: IToolData = { + id: 'executeToolBlocked', + toolReferenceName: 'executeToolBlockedRef', + modelDescription: 'Execute Tool', + displayName: 'Execute Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(executeTool)); + store.add(service.executeToolSet.addTool(executeTool)); + + // Get tools - execute tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('executeToolBlocked'), 'Tool in execute toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted allows search toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a 'search' toolset (permitted when agent mode is disabled) + const searchToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'search', + SpecedToolAliases.search, + { description: 'Search Tool Set' } + )); + + const searchTool: IToolData = { + id: 'searchToolInSet', + toolReferenceName: 'searchToolRef', + modelDescription: 'Search Tool', + displayName: 'Search Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(searchTool)); + store.add(searchToolSet.addTool(searchTool)); + + // Get tools - search tool should be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('searchToolInSet'), 'Tool in search toolset should be permitted when agent mode is disabled'); + }); + + test('isPermitted allows web toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a 'web' toolset (permitted when agent mode is disabled) + const webToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'web', + SpecedToolAliases.web, + { description: 'Web Tool Set' } + )); + + const webTool: IToolData = { + id: 'webToolInSet', + toolReferenceName: 'webToolRef', + modelDescription: 'Web Tool', + displayName: 'Web Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(webTool)); + store.add(webToolSet.addTool(webTool)); + + // Get tools - web tool should be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('webToolInSet'), 'Tool in web toolset should be permitted when agent mode is disabled'); + }); + + test('isPermitted allows vscode_fetchWebPage_internal special case when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Register the special-cased fetch tool (not added to any toolset) + const fetchTool: IToolData = { + id: 'vscode_fetchWebPage_internal', + toolReferenceName: 'fetchWebPage', + modelDescription: 'Fetch Web Page', + displayName: 'Fetch Web Page', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(fetchTool)); + + // Get tools - this special tool should be available even when not in a toolset + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('vscode_fetchWebPage_internal'), 'vscode_fetchWebPage_internal should be permitted as special case when agent mode is disabled'); + }); + + test('isPermitted blocks extension tools not in permitted toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create extension tool not in any permitted toolset + const extensionTool: IToolData = { + id: 'extensionToolBlocked', + toolReferenceName: 'extensionToolRef', + modelDescription: 'Extension Tool', + displayName: 'Extension Tool', + source: { type: 'extension', label: 'Test Extension', extensionId: new ExtensionIdentifier('test.extension') }, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(extensionTool)); + + // Get tools - extension tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('extensionToolBlocked'), 'Extension tool not in permitted toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted blocks MCP tools not in permitted toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create MCP toolset (not in permitted list) + const mcpToolSet = store.add(service.createToolSet( + { type: 'mcp', label: 'Test MCP', serverLabel: 'Test MCP Server', instructions: undefined, collectionId: 'testMcp', definitionId: 'testMcpDef' }, + 'mcpToolSetBlocked', + 'mcpToolSetBlockedRef', + { description: 'MCP Tool Set' } + )); + + const mcpTool: IToolData = { + id: 'mcpToolBlocked', + toolReferenceName: 'mcpToolRef', + modelDescription: 'MCP Tool', + displayName: 'MCP Tool', + source: { type: 'mcp', label: 'Test MCP', serverLabel: 'Test MCP Server', instructions: undefined, collectionId: 'testMcp', definitionId: 'testMcpDef' }, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(mcpTool)); + store.add(mcpToolSet.addTool(mcpTool)); + + // Get tools - MCP tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('mcpToolBlocked'), 'MCP tool should NOT be permitted when agent mode is disabled'); + + // Get toolsets - MCP toolset should NOT be available + const toolSets = Array.from(service.toolSets.get()); + const toolSetIds = Array.from(toolSets).map(ts => ts.id); + + assert.ok(!toolSetIds.includes('mcpToolSetBlocked'), 'MCP toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted blocks agent toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create tool in the 'agent' toolset (NOT permitted when agent mode is disabled) + const agentTool: IToolData = { + id: 'agentToolBlocked', + toolReferenceName: 'agentToolBlockedRef', + modelDescription: 'Agent Tool', + displayName: 'Agent Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(agentTool)); + store.add(service.agentToolSet.addTool(agentTool)); + + // Get tools - agent tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('agentToolBlocked'), 'Tool in agent toolset should NOT be permitted when agent mode is disabled'); + + // Get toolsets - agent toolset should NOT be available + const toolSets = Array.from(service.toolSets.get()); + const toolSetIds = Array.from(toolSets).map(ts => ts.id); + + assert.ok(!toolSetIds.includes('agent'), 'agent toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted includes tool in multiple toolsets if one is permitted', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a tool that is added to both a permitted toolset (read) and a non-permitted toolset + const multiSetTool: IToolData = { + id: 'multiSetTool', + toolReferenceName: 'multiSetToolRef', + modelDescription: 'Multi Set Tool', + displayName: 'Multi Set Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(multiSetTool)); + + // Add to read toolset (permitted) + store.add(service.readToolSet.addTool(multiSetTool)); + + // Also create and add to a non-permitted toolset + const customToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'customMultiSet', + 'customMultiSetRef', + { description: 'Custom Multi Set' } + )); + store.add(customToolSet.addTool(multiSetTool)); + + // Get tools - tool should be available because it's in the 'read' toolset + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('multiSetTool'), 'Tool should be permitted if it belongs to at least one permitted toolset'); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts deleted file mode 100644 index 75f4496a0e3..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts +++ /dev/null @@ -1,193 +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 assert from 'assert'; -import { URI } from '../../../../../../base/common/uri.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { ChatPromptContentStore } from '../../../common/promptSyntax/chatPromptContentStore.js'; - -suite('ChatPromptContentStore', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - - let store: ChatPromptContentStore; - - setup(() => { - store = testDisposables.add(new ChatPromptContentStore()); - }); - - test('registerContent stores content retrievable by URI', () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/test-id'); - const content = '# Test Agent\nThis is test content'; - - const disposable = store.registerContent(uri, content); - testDisposables.add(disposable); - - const retrieved = store.getContent(uri); - assert.strictEqual(retrieved, content); - }); - - test('getContent returns undefined for unregistered URI', () => { - const uri = URI.parse('vscode-chat-prompt:/.agent.md/unknown-id'); - - const retrieved = store.getContent(uri); - assert.strictEqual(retrieved, undefined); - }); - - test('registerContent returns disposable that removes content', () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/disposable-test'); - const content = 'Content to be disposed'; - - const disposable = store.registerContent(uri, content); - - // Content should exist before disposal - assert.strictEqual(store.getContent(uri), content); - - // Dispose and verify content is removed - disposable.dispose(); - assert.strictEqual(store.getContent(uri), undefined); - }); - - test('multiple registrations for different URIs are independent', () => { - const uri1 = URI.parse('vscode-chat-prompt:/.agent.md/id-1'); - const uri2 = URI.parse('vscode-chat-prompt:/.instructions.md/id-2'); - const content1 = 'Content 1'; - const content2 = 'Content 2'; - - const disposable1 = store.registerContent(uri1, content1); - const disposable2 = store.registerContent(uri2, content2); - testDisposables.add(disposable1); - testDisposables.add(disposable2); - - assert.strictEqual(store.getContent(uri1), content1); - assert.strictEqual(store.getContent(uri2), content2); - - // Disposing one should not affect the other - disposable1.dispose(); - assert.strictEqual(store.getContent(uri1), undefined); - assert.strictEqual(store.getContent(uri2), content2); - }); - - test('re-registering same URI overwrites content', () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/overwrite-test'); - const content1 = 'Original content'; - const content2 = 'Updated content'; - - const disposable1 = store.registerContent(uri, content1); - testDisposables.add(disposable1); - - assert.strictEqual(store.getContent(uri), content1); - - const disposable2 = store.registerContent(uri, content2); - testDisposables.add(disposable2); - - assert.strictEqual(store.getContent(uri), content2); - }); - - test('store disposal clears all content', () => { - const uri1 = URI.parse('vscode-chat-prompt:/.agent.md/clear-1'); - const uri2 = URI.parse('vscode-chat-prompt:/.agent.md/clear-2'); - - store.registerContent(uri1, 'Content 1'); - store.registerContent(uri2, 'Content 2'); - - assert.strictEqual(store.getContent(uri1), 'Content 1'); - assert.strictEqual(store.getContent(uri2), 'Content 2'); - - // Create a new store for this test that we can dispose independently - const localStore = new ChatPromptContentStore(); - const localUri = URI.parse('vscode-chat-prompt:/.agent.md/local'); - localStore.registerContent(localUri, 'Local content'); - - assert.strictEqual(localStore.getContent(localUri), 'Local content'); - - localStore.dispose(); - assert.strictEqual(localStore.getContent(localUri), undefined); - }); - - test('empty string content is stored correctly', () => { - const uri = URI.parse('vscode-chat-prompt:/.prompt.md/empty-content'); - - const disposable = store.registerContent(uri, ''); - testDisposables.add(disposable); - - const retrieved = store.getContent(uri); - assert.strictEqual(retrieved, ''); - }); - - test('content with special characters is stored correctly', () => { - const uri = URI.parse('vscode-chat-prompt:/.instructions.md/special-chars'); - const content = '# Test\n\nUnicode: 你好世界 🎉\nSpecial: ${{variable}} @mention #tag'; - - const disposable = store.registerContent(uri, content); - testDisposables.add(disposable); - - const retrieved = store.getContent(uri); - assert.strictEqual(retrieved, content); - }); - - test('URI comparison is string-based', () => { - // Same logical URI created two different ways - const uri1 = URI.parse('vscode-chat-prompt:/.agent.md/test'); - const uri2 = URI.from({ - scheme: 'vscode-chat-prompt', - path: '/.agent.md/test' - }); - - const content = 'Test content'; - const disposable = store.registerContent(uri1, content); - testDisposables.add(disposable); - - // Should be retrievable with equivalent URI - assert.strictEqual(store.getContent(uri2), content); - }); - - test('getContent normalizes URI by stripping query parameters', () => { - const baseUri = URI.parse('vscode-chat-prompt:/.agent.md/normalize-test'); - const content = 'Normalized content'; - - const disposable = store.registerContent(baseUri, content); - testDisposables.add(disposable); - - // Should retrieve content when queried with extra query parameters - const uriWithQuery = baseUri.with({ query: 'vscodeLinkType=prompt' }); - assert.strictEqual(store.getContent(uriWithQuery), content); - }); - - test('getContent normalizes URI by stripping fragment', () => { - const baseUri = URI.parse('vscode-chat-prompt:/.instructions.md/fragment-test'); - const content = 'Content with fragment lookup'; - - const disposable = store.registerContent(baseUri, content); - testDisposables.add(disposable); - - // Should retrieve content when queried with fragment - const uriWithFragment = baseUri.with({ fragment: 'section1' }); - assert.strictEqual(store.getContent(uriWithFragment), content); - }); - - test('getContent normalizes URI by stripping both query and fragment', () => { - const baseUri = URI.parse('vscode-chat-prompt:/.prompt.md/full-normalize'); - const content = 'Fully normalized content'; - - const disposable = store.registerContent(baseUri, content); - testDisposables.add(disposable); - - // Should retrieve content when queried with both query and fragment - const uriWithBoth = baseUri.with({ query: 'vscodeLinkType=skill&foo=bar', fragment: 'heading' }); - assert.strictEqual(store.getContent(uriWithBoth), content); - }); - - test('registerContent normalizes URI so content registered with query is found without it', () => { - const uriWithQuery = URI.parse('vscode-chat-prompt:/.agent.md/register-with-query?vscodeLinkType=agent'); - const content = 'Content registered with query'; - - const disposable = store.registerContent(uriWithQuery, content); - testDisposables.add(disposable); - - // Should retrieve content using base URI without query - const baseUri = uriWithQuery.with({ query: '' }); - assert.strictEqual(store.getContent(baseUri), content); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index af040c6e439..044a0f1db1a 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -1742,78 +1742,6 @@ suite('PromptsService', () => { assert.strictEqual(actualAfterDispose.length, 0); }); - test('Custom agent provider with isEditable', async () => { - const readonlyAgentUri = URI.parse('file://extensions/my-extension/readonlyAgent.agent.md'); - const editableAgentUri = URI.parse('file://extensions/my-extension/editableAgent.agent.md'); - const extension = { - identifier: { value: 'test.my-extension' }, - enabledApiProposals: ['chatParticipantPrivate'] - } as unknown as IExtensionDescription; - - // Mock the agent file content - await mockFiles(fileService, [ - { - path: readonlyAgentUri.path, - contents: [ - '---', - 'description: \'Readonly agent from provider\'', - '---', - 'I am a readonly agent.', - ] - }, - { - path: editableAgentUri.path, - contents: [ - '---', - 'description: \'Editable agent from provider\'', - '---', - 'I am an editable agent.', - ] - } - ]); - - const provider = { - providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { - return [ - { - uri: readonlyAgentUri, - isEditable: false - }, - { - uri: editableAgentUri, - isEditable: true - } - ]; - } - }; - - const registered = service.registerPromptFileProvider(extension, PromptsType.agent, provider); - - // Spy on updateReadonly to verify it's called correctly - const filesConfigService = instaService.get(IFilesConfigurationService); - const updateReadonlySpy = sinon.spy(filesConfigService, 'updateReadonly'); - - // List prompt files to trigger the readonly check - await service.listPromptFiles(PromptsType.agent, CancellationToken.None); - - // Verify updateReadonly was called only for the non-editable agent - assert.strictEqual(updateReadonlySpy.callCount, 1, 'updateReadonly should be called once'); - assert.ok(updateReadonlySpy.calledWith(readonlyAgentUri, true), 'updateReadonly should be called with readonly agent URI and true'); - - const actual = await service.getCustomAgents(CancellationToken.None); - assert.strictEqual(actual.length, 2); - - const readonlyAgent = actual.find(a => a.name === 'readonlyAgent'); - const editableAgent = actual.find(a => a.name === 'editableAgent'); - - assert.ok(readonlyAgent, 'Readonly agent should be found'); - assert.ok(editableAgent, 'Editable agent should be found'); - assert.strictEqual(readonlyAgent!.description, 'Readonly agent from provider'); - assert.strictEqual(editableAgent!.description, 'Editable agent from provider'); - - registered.dispose(); - }); - test('Contributed agent file that does not exist should not crash', async () => { const nonExistentUri = URI.parse('file://extensions/my-extension/nonexistent.agent.md'); const existingUri = URI.parse('file://extensions/my-extension/existing.agent.md'); @@ -1911,68 +1839,6 @@ suite('PromptsService', () => { assert.strictEqual(foundAfterDispose, undefined); }); - test('Instructions provider with isEditable flag', async () => { - const readonlyInstructionUri = URI.parse('file://extensions/my-extension/readonly.instructions.md'); - const editableInstructionUri = URI.parse('file://extensions/my-extension/editable.instructions.md'); - const extension = { - identifier: { value: 'test.my-extension' }, - enabledApiProposals: ['chatParticipantPrivate'] - } as unknown as IExtensionDescription; - - // Mock the instruction file content - await mockFiles(fileService, [ - { - path: readonlyInstructionUri.path, - contents: [ - '# Readonly instruction content' - ] - }, - { - path: editableInstructionUri.path, - contents: [ - '# Editable instruction content' - ] - } - ]); - - const provider = { - providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { - return [ - { - uri: readonlyInstructionUri, - isEditable: false - }, - { - uri: editableInstructionUri, - isEditable: true - } - ]; - } - }; - - const registered = service.registerPromptFileProvider(extension, PromptsType.instructions, provider); - - // Spy on updateReadonly to verify it's called correctly - const filesConfigService = instaService.get(IFilesConfigurationService); - const updateReadonlySpy = sinon.spy(filesConfigService, 'updateReadonly'); - - // List prompt files to trigger the readonly check - await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - - // Verify updateReadonly was called only for the non-editable instruction - assert.strictEqual(updateReadonlySpy.callCount, 1, 'updateReadonly should be called once'); - assert.ok(updateReadonlySpy.calledWith(readonlyInstructionUri, true), 'updateReadonly should be called with readonly instruction URI and true'); - - const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - const readonlyInstruction = actual.find(i => i.uri.toString() === readonlyInstructionUri.toString()); - const editableInstruction = actual.find(i => i.uri.toString() === editableInstructionUri.toString()); - - assert.ok(readonlyInstruction, 'Readonly instruction should be found'); - assert.ok(editableInstruction, 'Editable instruction should be found'); - - registered.dispose(); - }); - test('Prompt file provider', async () => { const promptUri = URI.parse('file://extensions/my-extension/myPrompt.prompt.md'); const extension = { @@ -2018,68 +1884,6 @@ suite('PromptsService', () => { assert.strictEqual(foundAfterDispose, undefined); }); - test('Prompt file provider with isEditable flag', async () => { - const readonlyPromptUri = URI.parse('file://extensions/my-extension/readonly.prompt.md'); - const editablePromptUri = URI.parse('file://extensions/my-extension/editable.prompt.md'); - const extension = { - identifier: { value: 'test.my-extension' }, - enabledApiProposals: ['chatParticipantPrivate'] - } as unknown as IExtensionDescription; - - // Mock the prompt file content - await mockFiles(fileService, [ - { - path: readonlyPromptUri.path, - contents: [ - '# Readonly prompt content' - ] - }, - { - path: editablePromptUri.path, - contents: [ - '# Editable prompt content' - ] - } - ]); - - const provider = { - providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { - return [ - { - uri: readonlyPromptUri, - isEditable: false - }, - { - uri: editablePromptUri, - isEditable: true - } - ]; - } - }; - - const registered = service.registerPromptFileProvider(extension, PromptsType.prompt, provider); - - // Spy on updateReadonly to verify it's called correctly - const filesConfigService = instaService.get(IFilesConfigurationService); - const updateReadonlySpy = sinon.spy(filesConfigService, 'updateReadonly'); - - // List prompt files to trigger the readonly check - await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); - - // Verify updateReadonly was called only for the non-editable prompt - assert.strictEqual(updateReadonlySpy.callCount, 1, 'updateReadonly should be called once'); - assert.ok(updateReadonlySpy.calledWith(readonlyPromptUri, true), 'updateReadonly should be called with readonly prompt URI and true'); - - const actual = await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); - const readonlyPrompt = actual.find(i => i.uri.toString() === readonlyPromptUri.toString()); - const editablePrompt = actual.find(i => i.uri.toString() === editablePromptUri.toString()); - - assert.ok(readonlyPrompt, 'Readonly prompt should be found'); - assert.ok(editablePrompt, 'Editable prompt should be found'); - - registered.dispose(); - }); - test('Skill file provider', async () => { const skillUri = URI.parse('file://extensions/my-extension/mySkill/SKILL.md'); const extension = { @@ -2129,76 +1933,6 @@ suite('PromptsService', () => { assert.strictEqual(foundAfterDispose, undefined); }); - test('Skill file provider with isEditable flag', async () => { - const readonlySkillUri = URI.parse('file://extensions/my-extension/readonlySkill/SKILL.md'); - const editableSkillUri = URI.parse('file://extensions/my-extension/editableSkill/SKILL.md'); - const extension = { - identifier: { value: 'test.my-extension' }, - enabledApiProposals: ['chatParticipantPrivate'] - } as unknown as IExtensionDescription; - - // Mock the skill file content - await mockFiles(fileService, [ - { - path: readonlySkillUri.path, - contents: [ - '---', - 'name: "Readonly Skill"', - 'description: "A readonly skill"', - '---', - 'Readonly skill content.', - ] - }, - { - path: editableSkillUri.path, - contents: [ - '---', - 'name: "Editable Skill"', - 'description: "An editable skill"', - '---', - 'Editable skill content.', - ] - } - ]); - - const provider = { - providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { - return [ - { - uri: readonlySkillUri, - isEditable: false - }, - { - uri: editableSkillUri, - isEditable: true - } - ]; - } - }; - - const registered = service.registerPromptFileProvider(extension, PromptsType.skill, provider); - - // Spy on updateReadonly to verify it's called correctly - const filesConfigService = instaService.get(IFilesConfigurationService); - const updateReadonlySpy = sinon.spy(filesConfigService, 'updateReadonly'); - - // List prompt files to trigger the readonly check - await service.listPromptFiles(PromptsType.skill, CancellationToken.None); - - // Verify updateReadonly was called only for the non-editable skill - assert.strictEqual(updateReadonlySpy.callCount, 1, 'updateReadonly should be called once'); - assert.ok(updateReadonlySpy.calledWith(readonlySkillUri, true), 'updateReadonly should be called with readonly skill URI and true'); - - const actual = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); - const readonlySkill = actual.find(i => i.uri.toString() === readonlySkillUri.toString()); - const editableSkill = actual.find(i => i.uri.toString() === editableSkillUri.toString()); - - assert.ok(readonlySkill, 'Readonly skill should be found'); - assert.ok(editableSkill, 'Editable skill should be found'); - - registered.dispose(); - }); - suite('findAgentSkills', () => { teardown(() => { sinon.restore(); diff --git a/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts b/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts index dd2f5f70035..d69b1f8b653 100644 --- a/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts +++ b/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts @@ -19,6 +19,7 @@ export namespace McpApps { | MCP.ReadResourceRequest | MCP.PingRequest | (McpUiOpenLinkRequest & MCP.JSONRPCRequest) + | (McpUiUpdateModelContextRequest & MCP.JSONRPCRequest) | (McpUiMessageRequest & MCP.JSONRPCRequest) | (McpUiRequestDisplayModeRequest & MCP.JSONRPCRequest) | (McpApps.McpUiInitializeRequest & MCP.JSONRPCRequest); @@ -427,6 +428,28 @@ export namespace McpApps { params: McpUiHostContext; } + /** + * @description Request to update the agent's context without requiring a follow-up action (Guest UI -> Host). + * + * Unlike `notifications/message` which is for debugging/logging, this request is intended + * to update the Host's model context. Each request overwrites the previous context sent by the Guest UI. + * Unlike messages, context updates do not trigger follow-ups. + * + * The host will typically defer sending the context to the model until the next user message + * (including `ui/message`), and will only send the last update received. + * + * @see {@link app.App.updateModelContext} for the method that sends this request + */ + export interface McpUiUpdateModelContextRequest { + method: "ui/update-model-context"; + params: { + /** @description Context content blocks (text, image, etc.). */ + content?: ContentBlock[]; + /** @description Structured content for machine-readable context data. */ + structuredContent?: Record; + }; + } + /** * @description Request for graceful shutdown of the Guest UI (Host -> Guest UI). * @see {@link app-bridge.AppBridge.teardownResource} for the host method that sends this @@ -447,6 +470,21 @@ export namespace McpApps { [key: string]: unknown; } + export interface McpUiSupportedContentBlockModalities { + /** @description Host supports text content blocks. */ + text?: {}; + /** @description Host supports image content blocks. */ + image?: {}; + /** @description Host supports audio content blocks. */ + audio?: {}; + /** @description Host supports resource content blocks. */ + resource?: {}; + /** @description Host supports resource link content blocks. */ + resourceLink?: {}; + /** @description Host supports structured content. */ + structuredContent?: {}; + } + /** * @description Capabilities supported by the host application. * @see {@link McpUiInitializeResult} for the initialization result that includes these capabilities @@ -475,6 +513,10 @@ export namespace McpApps { /** @description CSP domains approved by the host. */ csp?: McpUiResourceCsp; }; + /** @description Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns. */ + updateModelContext?: McpUiSupportedContentBlockModalities; + /** @description Host supports receiving content messages (ui/message) from the Guest UI. */ + message?: McpUiSupportedContentBlockModalities; } /** @@ -674,4 +716,6 @@ export namespace McpApps { "ui/notifications/initialized"; export const REQUEST_DISPLAY_MODE_METHOD: McpUiRequestDisplayModeRequest["method"] = "ui/request-display-mode"; + export const UPDATE_MODEL_CONTEXT_METHOD: McpUiUpdateModelContextRequest["method"] = + "ui/update-model-context"; } diff --git a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts index 3ea882a7f34..95908faac2f 100644 --- a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts +++ b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts @@ -576,7 +576,14 @@ export class DetachedTerminalSnapshotMirror extends Disposable { return getChatTerminalBackgroundColor(theme, this._contextKeyService, storedBackground); } } - }).then(terminal => this._register(terminal)); + }).then(terminal => { + // If the store is already disposed, dispose the terminal immediately + if (this._store.isDisposed) { + terminal.dispose(); + return terminal; + } + return this._register(terminal); + }); } private async _getTerminal(): Promise { @@ -593,6 +600,9 @@ export class DetachedTerminalSnapshotMirror extends Disposable { public async attach(container: HTMLElement): Promise { const terminal = await this._getTerminal(); + if (this._store.isDisposed) { + return; + } container.classList.add('chat-terminal-output-terminal'); const needsAttach = this._attachedContainer !== container || container.firstChild === null; if (needsAttach) { @@ -613,6 +623,9 @@ export class DetachedTerminalSnapshotMirror extends Disposable { return { lineCount: this._lastRenderedLineCount ?? output.lineCount, maxColumnWidth: this._lastRenderedMaxColumnWidth }; } const terminal = await this._getTerminal(); + if (this._store.isDisposed) { + return undefined; + } if (this._container) { this._applyTheme(this._container); } @@ -625,6 +638,9 @@ export class DetachedTerminalSnapshotMirror extends Disposable { return { lineCount: 0, maxColumnWidth: 0 }; } await new Promise(resolve => terminal.xterm.write(text, resolve)); + if (this._store.isDisposed) { + return undefined; + } this._dirty = false; this._lastRenderedLineCount = lineCount; // Only compute max column width for small outputs to avoid performance issues diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts index 7f8f1e8d39e..2c592de63d9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts @@ -257,7 +257,10 @@ export async function collectTerminalResults( } const outputMonitor = disposableStore.add(instantiationService.createInstance(OutputMonitor, execution, taskProblemPollFn, invocationContext, token, task._label)); - await Event.toPromise(outputMonitor.onDidFinishCommand); + await Promise.race([ + Event.toPromise(outputMonitor.onDidFinishCommand), + Event.toPromise(token.onCancellationRequested as Event) + ]); const pollingResult = outputMonitor.pollingResult; return { name: instance.shellLaunchConfig.name ?? instance.title ?? 'unknown', diff --git a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts index f97cb106c10..e683d6ce600 100644 --- a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts +++ b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts @@ -9,99 +9,23 @@ declare module 'vscode' { // #region Resource Classes /** - * Describes a chat resource file. + * Represents a chat-related resource, such as a custom agent, instructions, prompt file, or skill. */ - export type ChatResourceDescriptor = - | Uri - | { uri: Uri; isEditable?: boolean } - | { - id: string; - content: string; - }; - - /** - * Represents a custom agent resource file (e.g., .agent.md). - */ - export class CustomAgentChatResource { + export interface ChatResource { /** - * The custom agent resource descriptor. + * Uri to the chat resource. This is typically a `.agent.md`, `.instructions.md`, `.prompt.md`, or `SKILL.md` file. */ - readonly resource: ChatResourceDescriptor; - - /** - * Creates a new custom agent resource from the specified resource. - * @param resource The chat resource descriptor. - */ - constructor(resource: ChatResourceDescriptor); - } - - /** - * Represents an instructions resource file. - */ - export class InstructionsChatResource { - /** - * The instructions resource descriptor. - */ - readonly resource: ChatResourceDescriptor; - - /** - * Creates a new instructions resource from the specified resource. - * @param resource The chat resource descriptor. - */ - constructor(resource: ChatResourceDescriptor); - } - - /** - * Represents a prompt file resource (e.g., .prompt.md). - */ - export class PromptFileChatResource { - /** - * The prompt file resource descriptor. - */ - readonly resource: ChatResourceDescriptor; - - /** - * Creates a new prompt file resource from the specified resource. - * @param resource The chat resource descriptor. - */ - constructor(resource: ChatResourceDescriptor); - } - - /** - * Represents a skill file resource (SKILL.md) - */ - export class SkillChatResource { - /** - * The skill resource descriptor. - */ - readonly resource: ChatResourceDescriptor; - - /** - * Creates a new skill resource from the specified resource URI pointing to SKILL.md. - * The parent folder name needs to match the name of the skill in the frontmatter. - * @param resource The chat resource descriptor. - */ - constructor(resource: ChatResourceDescriptor); + readonly uri: Uri; } // #endregion // #region Providers - /** - * Options for querying custom agents. - */ - export type CustomAgentContext = object; - /** * A provider that supplies custom agent resources (from .agent.md files) for repositories. */ - export interface CustomAgentProvider { - /** - * A human-readable label for this provider. - */ - readonly label: string; - + export interface ChatCustomAgentProvider { /** * An optional event to signal that custom agents have changed. */ @@ -109,30 +33,17 @@ declare module 'vscode' { /** * Provide the list of custom agents available. - * @param context Context for the query. + * @param context Context for the provide call. * @param token A cancellation token. * @returns An array of custom agents or a promise that resolves to such. */ - provideCustomAgents( - context: CustomAgentContext, - token: CancellationToken - ): ProviderResult; + provideCustomAgents(context: unknown, token: CancellationToken): ProviderResult; } - /** - * Context for querying instructions. - */ - export type InstructionsContext = object; - /** * A provider that supplies instructions resources for repositories. */ - export interface InstructionsProvider { - /** - * A human-readable label for this provider. - */ - readonly label: string; - + export interface ChatInstructionsProvider { /** * An optional event to signal that instructions have changed. */ @@ -140,30 +51,17 @@ declare module 'vscode' { /** * Provide the list of instructions available. - * @param context Context for the query. + * @param context Context for the provide call. * @param token A cancellation token. * @returns An array of instructions or a promise that resolves to such. */ - provideInstructions( - context: InstructionsContext, - token: CancellationToken - ): ProviderResult; + provideInstructions(context: unknown, token: CancellationToken): ProviderResult; } - /** - * Context for querying prompt files. - */ - export type PromptFileContext = object; - /** * A provider that supplies prompt file resources (from .prompt.md files) for repositories. */ - export interface PromptFileProvider { - /** - * A human-readable label for this provider. - */ - readonly label: string; - + export interface ChatPromptFileProvider { /** * An optional event to signal that prompt files have changed. */ @@ -171,34 +69,21 @@ declare module 'vscode' { /** * Provide the list of prompt files available. - * @param context Context for the query. + * @param context Context for the provide call. * @param token A cancellation token. * @returns An array of prompt files or a promise that resolves to such. */ - providePromptFiles( - context: PromptFileContext, - token: CancellationToken - ): ProviderResult; + providePromptFiles(context: unknown, token: CancellationToken): ProviderResult; } // #endregion // #region SkillProvider - /** - * Context for querying skills. - */ - export type SkillContext = object; - /** * A provider that supplies SKILL.md resources for agents. */ - export interface SkillProvider { - /** - * A human-readable label for this provider. - */ - readonly label: string; - + export interface ChatSkillProvider { /** * An optional event to signal that skills have changed. */ @@ -206,14 +91,11 @@ declare module 'vscode' { /** * Provide the list of skills available. - * @param context Context for the query. + * @param context Context for the provide call. * @param token A cancellation token. * @returns An array of skill resources or a promise that resolves to such. */ - provideSkills( - context: SkillContext, - token: CancellationToken - ): ProviderResult; + provideSkills(context: unknown, token: CancellationToken): ProviderResult; } // #endregion @@ -226,34 +108,28 @@ declare module 'vscode' { * @param provider The custom agent provider. * @returns A disposable that unregisters the provider when disposed. */ - export function registerCustomAgentProvider( - provider: CustomAgentProvider - ): Disposable; + export function registerCustomAgentProvider(provider: ChatCustomAgentProvider): Disposable; /** * Register a provider for instructions. * @param provider The instructions provider. * @returns A disposable that unregisters the provider when disposed. */ - export function registerInstructionsProvider( - provider: InstructionsProvider - ): Disposable; + export function registerInstructionsProvider(provider: ChatInstructionsProvider): Disposable; /** * Register a provider for prompt files. * @param provider The prompt file provider. * @returns A disposable that unregisters the provider when disposed. */ - export function registerPromptFileProvider( - provider: PromptFileProvider - ): Disposable; + export function registerPromptFileProvider(provider: ChatPromptFileProvider): Disposable; /** * Register a provider for skills. * @param provider The skill provider. * @returns A disposable that unregisters the provider when disposed. */ - export function registerSkillProvider(provider: SkillProvider): Disposable; + export function registerSkillProvider(provider: ChatSkillProvider): Disposable; } // #endregion diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 016b45c2916..3f8a6a0b89f 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -100,11 +100,9 @@ declare module 'vscode' { refreshHandler: () => Thenable; /** - * Fired when an item is archived by the editor - * - * TODO: expose archive state on the item too? + * Fired when an item's archived state changes. */ - readonly onDidArchiveChatSessionItem: Event; + readonly onDidChangeChatSessionItemState: Event; } /** diff --git a/src/vscode-dts/vscode.proposed.languageModelToolSupportsModel.d.ts b/src/vscode-dts/vscode.proposed.languageModelToolSupportsModel.d.ts index 5d4b8d70f31..ab481a3d9d8 100644 --- a/src/vscode-dts/vscode.proposed.languageModelToolSupportsModel.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModelToolSupportsModel.d.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// version: 1 + declare module 'vscode' { export interface LanguageModelToolDefinition extends LanguageModelToolInformation {