diff --git a/.vscode/settings.json b/.vscode/settings.json index bec2efbe49192..d9aeed8443206 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -210,6 +210,5 @@ ], "azureMcp.serverMode": "all", "azureMcp.readOnly": true, - "chat.tools.terminal.outputLocation": "none", "debug.breakpointsView.presentation": "tree" } diff --git a/build/gulpfile.cli.ts b/build/gulpfile.cli.ts index e746a00e2bb2d..974cf892e4f36 100644 --- a/build/gulpfile.cli.ts +++ b/build/gulpfile.cli.ts @@ -13,9 +13,8 @@ import { tmpdir } from 'os'; import { existsSync, mkdirSync, rmSync } from 'fs'; import * as task from './lib/task.ts'; import watcher from './lib/watch/index.ts'; -import { debounce } from './lib/util.ts'; +import { debounce, untar } from './lib/util.ts'; import { createReporter } from './lib/reporter.ts'; -import untar from 'gulp-untar'; import gunzip from 'gulp-gunzip'; const root = 'cli'; diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts index cb1a0a5fd6964..27149338d9f7e 100644 --- a/build/gulpfile.reh.ts +++ b/build/gulpfile.reh.ts @@ -21,7 +21,7 @@ import vfs from 'vinyl-fs'; import packageJson from '../package.json' with { type: 'json' }; import flatmap from 'gulp-flatmap'; import gunzip from 'gulp-gunzip'; -import untar from 'gulp-untar'; +import { untar } from './lib/util.ts'; import File from 'vinyl'; import * as fs from 'fs'; import glob from 'glob'; diff --git a/build/lib/typings/gulp-untar.d.ts b/build/lib/typings/gulp-untar.d.ts deleted file mode 100644 index b4007983cac4a..0000000000000 --- a/build/lib/typings/gulp-untar.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'gulp-untar' { - import type { Transform } from 'stream'; - - function untar(): Transform; - - export = untar; -} diff --git a/build/lib/util.ts b/build/lib/util.ts index f1354b858c9fe..e4d01e143c93b 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -15,6 +15,8 @@ import through from 'through'; import sm from 'source-map'; import { pathToFileURL } from 'url'; import ternaryStream from 'ternary-stream'; +import type { Transform } from 'stream'; +import * as tar from 'tar'; const root = path.dirname(path.dirname(import.meta.dirname)); @@ -429,3 +431,39 @@ export class VinylStat implements fs.Stats { isFIFO(): boolean { return false; } isSocket(): boolean { return false; } } + +export function untar(): Transform { + return es.through(function (this: through.ThroughStream, f: VinylFile) { + if (!f.contents || !Buffer.isBuffer(f.contents)) { + this.emit('error', new Error('Expected file with Buffer contents')); + return; + } + + const self = this; + const parser = new tar.Parser(); + + parser.on('entry', (entry: tar.ReadEntry) => { + if (entry.type === 'File') { + const chunks: Buffer[] = []; + entry.on('data', (chunk: Buffer) => chunks.push(chunk)); + entry.on('end', () => { + const file = new VinylFile({ + path: entry.path, + contents: Buffer.concat(chunks), + stat: new VinylStat({ + mode: entry.mode, + mtime: entry.mtime, + size: entry.size + }) + }); + self.emit('data', file); + }); + } else { + entry.resume(); + } + }); + + parser.on('error', (err: Error) => self.emit('error', err)); + parser.end(f.contents); + }) as Transform; +} diff --git a/package-lock.json b/package-lock.json index 2b2332b3383b3..74e77c2e608de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -121,7 +121,6 @@ "gulp-replace": "^0.5.4", "gulp-sourcemaps": "^3.0.0", "gulp-svgmin": "^4.1.0", - "gulp-untar": "^0.0.7", "husky": "^0.13.1", "innosetup": "^6.4.1", "istanbul-lib-coverage": "^3.2.0", @@ -148,6 +147,7 @@ "source-map": "0.6.1", "source-map-support": "^0.3.2", "style-loader": "^3.3.2", + "tar": "^7.5.2", "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", @@ -1267,6 +1267,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "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" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "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" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -3350,6 +3373,40 @@ "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" + } + }, "node_modules/@vscode/sqlite3/node_modules/node-addon-api": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.0.tgz", @@ -3359,6 +3416,29 @@ "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", @@ -4730,18 +4810,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= sha512-OorbnJVPII4DuUKbjARAe8u8EfqOmkEEaSFIyoQ7OjTHn6kafxWl0wLgoZ2rXaYd7MyLcDaU4TmhfxtwgcccMQ==", - "dev": true, - "dependencies": { - "inherits": "~2.0.0" - }, - "engines": { - "node": "0.4 || >=0.5.8" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -5118,11 +5186,13 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "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": ">=10" + "node": ">=18" } }, "node_modules/chrome-remote-interface": { @@ -7925,6 +7995,7 @@ "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" }, @@ -7933,9 +8004,10 @@ } }, "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "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" }, @@ -7946,7 +8018,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==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" }, "node_modules/fs-mkdirp-stream": { "version": "1.0.0", @@ -8006,34 +8079,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -9832,94 +9877,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/gulp-untar": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/gulp-untar/-/gulp-untar-0.0.7.tgz", - "integrity": "sha512-0QfbCH2a1k2qkTLWPqTX+QO4qNsHn3kC546YhAP3/n0h+nvtyGITDuDrYBMDZeW4WnFijmkOvBWa5HshTic1tw==", - "dev": true, - "dependencies": { - "event-stream": "~3.3.4", - "streamifier": "~0.1.1", - "tar": "^2.2.1", - "through2": "~2.0.3", - "vinyl": "^1.2.0" - } - }, - "node_modules/gulp-untar/node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4= sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/gulp-untar/node_modules/clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE= sha512-dhUqc57gSMCo6TX85FLfe51eC/s+Im2MLkAgJwfaRRexR2tA4dd3eLEW4L6efzHc2iNorrRRXITifnDLlRrhaA==", - "dev": true - }, - "node_modules/gulp-untar/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/gulp-untar/node_modules/replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ= sha512-AFBWBy9EVRTa/LhEcG8QDP3FvpwZqmvN2QFDuJswFeaVhWnZMp8q3E6Zd90SR04PlIwfGdyVjNyLPyen/ek5CQ==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gulp-untar/node_modules/tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "deprecated": "This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.", - "dev": true, - "dependencies": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" - } - }, - "node_modules/gulp-untar/node_modules/through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4= sha512-tmNYYHFqXmaKSSlOU4ZbQ82cxmFQa5LRWKFtWCNkGIiZ3/VHmOffCeWfBRZZRyXAhNP9itVMR+cuvomBOPlm8g==", - "dev": true, - "dependencies": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - } - }, - "node_modules/gulp-untar/node_modules/vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ= sha512-Ci3wnR2uuSAWFMSglZuB8Z2apBdtOyz8CV7dC6/U1XbltXBC+IuutUkXQISz01P+US2ouBuesSbV6zILZ6BuzQ==", - "dev": true, - "dependencies": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - }, - "engines": { - "node": ">= 0.9" - } - }, "node_modules/gulp-vinyl-zip": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.2.tgz", @@ -12394,33 +12351,28 @@ } }, "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "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": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">= 8" + "node": ">= 18" } }, "node_modules/minizlib/node_modules/minipass": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", - "dependencies": { - "yallist": "^4.0.0" - }, + "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": ">=8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -16446,19 +16398,20 @@ } }, "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==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "dev": true, + "license": "BlueOak-1.0.0", "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" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-fs": { @@ -16497,10 +16450,25 @@ "streamx": "^2.15.0" } }, + "node_modules/tar/node_modules/minipass": { + "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" + } + }, "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "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" + } }, "node_modules/tas-client": { "version": "0.3.1", diff --git a/package.json b/package.json index 44e6740de43f2..b76374ed584bf 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,6 @@ "gulp-replace": "^0.5.4", "gulp-sourcemaps": "^3.0.0", "gulp-svgmin": "^4.1.0", - "gulp-untar": "^0.0.7", "husky": "^0.13.1", "innosetup": "^6.4.1", "istanbul-lib-coverage": "^3.2.0", @@ -211,6 +210,7 @@ "source-map": "0.6.1", "source-map-support": "^0.3.2", "style-loader": "^3.3.2", + "tar": "^7.5.2", "ts-loader": "^9.5.1", "tsec": "0.2.7", "tslib": "^2.6.3", diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 74cb106fd3cef..00859ca7344e6 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -90,6 +90,9 @@ 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/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index de412e896a2a4..2a26c187d7317 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -28,6 +28,7 @@ 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'; @@ -100,6 +101,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA private readonly _promptFileProviders = this._register(new DisposableMap()); private readonly _promptFileProviderEmitters = this._register(new DisposableMap>()); + private readonly _promptFileContentRegistrations = this._register(new DisposableMap>()); private readonly _pendingProgress = new Map void; chatSession: IChatModel | undefined }>(); private readonly _proxy: ExtHostChatAgentsShape2; @@ -121,6 +123,7 @@ 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(); @@ -471,6 +474,10 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA const emitter = new Emitter(); this._promptFileProviderEmitters.set(handle, emitter); + // Track content registrations for this provider so they can be disposed when provider is unregistered + const contentRegistrations = new DisposableMap(); + this._promptFileContentRegistrations.set(handle, contentRegistrations); + const disposable = this._promptsService.registerPromptFileProvider(extension, type, { onDidChangePromptFiles: emitter.event, providePromptFiles: async (context: IPromptFileContext, token: CancellationToken) => { @@ -478,11 +485,21 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA if (!contributions) { return undefined; } - // Convert UriComponents to URI - return contributions.map(c => ({ - ...c, - uri: URI.revive(c.uri) - })); + // 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 + }; + }); } }); @@ -492,6 +509,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA $unregisterPromptFileProvider(handle: number): void { this._promptFileProviders.deleteAndDispose(handle); this._promptFileProviderEmitters.deleteAndDispose(handle); + this._promptFileContentRegistrations.deleteAndDispose(handle); } $onDidChangePromptFiles(handle: number): void { diff --git a/src/vs/workbench/api/browser/mainThreadChatContext.ts b/src/vs/workbench/api/browser/mainThreadChatContext.ts index d94ff704768b2..917aeb8c02af9 100644 --- a/src/vs/workbench/api/browser/mainThreadChatContext.ts +++ b/src/vs/workbench/api/browser/mainThreadChatContext.ts @@ -23,12 +23,13 @@ export class MainThreadChatContext extends Disposable implements MainThreadChatC ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatContext); + this._chatContextService.setExecuteCommandCallback((itemHandle) => this._proxy.$executeChatContextItemCommand(itemHandle)); } $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[] | undefined, _options: { icon: ThemeIcon }, support: IChatContextSupport): void { this._providers.set(handle, { selector, support, id }); this._chatContextService.registerChatContextProvider(id, selector, { - provideChatContext: (token: CancellationToken) => { + provideChatContext: (_options: {}, token: CancellationToken) => { return this._proxy.$provideChatContext(handle, token); }, resolveChatContext: support.supportsResolve ? (context: IChatContextItem, token: CancellationToken) => { @@ -36,7 +37,7 @@ export class MainThreadChatContext extends Disposable implements MainThreadChatC } : undefined, provideChatContextForResource: support.supportsResource ? (resource: URI, withValue: boolean, token: CancellationToken) => { return this._proxy.$provideChatContextForResource(handle, { resource, withValue }, token); - } : undefined + } : undefined, }); } @@ -56,4 +57,8 @@ export class MainThreadChatContext extends Disposable implements MainThreadChatC } this._chatContextService.updateWorkspaceContextItems(provider.id, items); } + + $executeChatContextItemCommand(itemHandle: number): Promise { + return this._proxy.$executeChatContextItemCommand(itemHandle); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 2a0d70fdbddb8..120c1acb4bca2 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -229,7 +229,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostLanguageModelTools = rpcProtocol.set(ExtHostContext.ExtHostLanguageModelTools, new ExtHostLanguageModelTools(rpcProtocol, extHostLanguageModels)); const extHostChatSessions = rpcProtocol.set(ExtHostContext.ExtHostChatSessions, new ExtHostChatSessions(extHostCommands, extHostLanguageModels, rpcProtocol, extHostLogService)); const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostDocumentsAndEditors, extHostLanguageModels, extHostDiagnostics, extHostLanguageModelTools)); - const extHostChatContext = rpcProtocol.set(ExtHostContext.ExtHostChatContext, new ExtHostChatContext(rpcProtocol)); + const extHostChatContext = rpcProtocol.set(ExtHostContext.ExtHostChatContext, new ExtHostChatContext(rpcProtocol, extHostCommands)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); const extHostAiSettingsSearch = rpcProtocol.set(ExtHostContext.ExtHostAiSettingsSearch, new ExtHostAiSettingsSearch(rpcProtocol)); @@ -1960,6 +1960,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I McpStdioServerDefinition2: extHostTypes.McpStdioServerDefinition, McpToolAvailability: extHostTypes.McpToolAvailability, SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind, + CustomAgentChatResource: extHostTypes.CustomAgentChatResource, + InstructionsChatResource: extHostTypes.InstructionsChatResource, + PromptFileChatResource: extHostTypes.PromptFileChatResource, }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3906ecde7f72d..498dc1cd16271 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1358,12 +1358,14 @@ export interface ExtHostChatContextShape { $provideChatContext(handle: number, token: CancellationToken): Promise; $provideChatContextForResource(handle: number, options: { resource: UriComponents; withValue: boolean }, token: CancellationToken): Promise; $resolveChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise; + $executeChatContextItemCommand(itemHandle: number): Promise; } export interface MainThreadChatContextShape extends IDisposable { $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[] | undefined, options: {}, support: IChatContextSupport): void; $unregisterChatContextProvider(handle: number): void; $updateWorkspaceContextItems(handle: number, items: IChatContextItem[]): void; + $executeChatContextItemCommand(itemHandle: number): Promise; } export interface MainThreadEmbeddingsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 112faf74966c6..12fe6f307c7e2 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -6,7 +6,7 @@ import { isFalsyOrEmpty } from '../../../base/common/arrays.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { Schemas, matchesSomeScheme } from '../../../base/common/network.js'; -import { URI } from '../../../base/common/uri.js'; +import { URI, UriComponents } from '../../../base/common/uri.js'; import { IPosition } from '../../../editor/common/core/position.js'; import { IRange } from '../../../editor/common/core/range.js'; import { ISelection } from '../../../editor/common/core/selection.js'; @@ -22,6 +22,7 @@ import * as types from './extHostTypes.js'; import { TransientCellMetadata, TransientDocumentMetadata } from '../../contrib/notebook/common/notebookCommon.js'; import * as search from '../../contrib/search/common/search.js'; import type * as vscode from 'vscode'; +import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js'; //#region --- NEW world @@ -554,6 +555,23 @@ const newCommands: ApiCommand[] = [ }; })], ApiCommandResult.Void + ), + // --- extension prompt files + new ApiCommand( + 'vscode.extensionPromptFileProvider', '_listExtensionPromptFiles', 'Get all extension-contributed prompt files (custom agents, instructions, and prompt files).', + [], + new ApiCommandResult<{ uri: UriComponents; type: PromptsType }[], { uri: vscode.Uri; type: PromptsType }[]>( + 'A promise that resolves to an array of objects containing uri and type.', + (value) => { + if (!value) { + return []; + } + return value.map(item => ({ + uri: URI.revive(item.uri), + type: item.type + })); + } + ) ) ]; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 994bf6dfb232b..7a724abf1c2ac 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -38,6 +38,7 @@ 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 { @@ -553,16 +554,54 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } const provider = providerData.provider; + let resources: vscode.CustomAgentChatResource[] | vscode.InstructionsChatResource[] | vscode.PromptFileChatResource[] | undefined; switch (type) { case PromptsType.agent: - return await (provider as vscode.CustomAgentProvider).provideCustomAgents(context, token) ?? undefined; + resources = await (provider as vscode.CustomAgentProvider).provideCustomAgents(context, token) ?? undefined; + break; case PromptsType.instructions: - return await (provider as vscode.InstructionsProvider).provideInstructions(context, token) ?? undefined; + resources = await (provider as vscode.InstructionsProvider).provideInstructions(context, token) ?? undefined; + break; case PromptsType.prompt: - return await (provider as vscode.PromptFileProvider).providePromptFiles(context, token) ?? undefined; + resources = await (provider as vscode.PromptFileProvider).providePromptFiles(context, token) ?? undefined; + break; case PromptsType.skill: throw new Error('Skills prompt file provider not implemented yet'); } + + // Convert ChatResourceDescriptor to IPromptFileResource format + return resources?.map(r => this.convertChatResourceDescriptorToPromptFileResource(r.resource, providerData.extension.identifier.value)); + } + + /** + * Creates a virtual URI for a prompt file. + */ + createVirtualPromptUri(id: string, extensionId: string): URI { + return URI.from({ + scheme: Schemas.vscodeChatPrompt, + path: `/${extensionId}/${id}` + }); + } + + convertChatResourceDescriptorToPromptFileResource(resource: vscode.ChatResourceDescriptor, extensionId: string): 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), + 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)}`); } 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/extHostChatContext.ts b/src/vs/workbench/api/common/extHostChatContext.ts index 0e3aaac540b16..761fca70dbb71 100644 --- a/src/vs/workbench/api/common/extHostChatContext.ts +++ b/src/vs/workbench/api/common/extHostChatContext.ts @@ -11,6 +11,7 @@ import { DocumentSelector } from './extHostTypeConverters.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import { IChatContextItem } from '../../contrib/chat/common/contextContrib/chatContext.js'; import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; +import { IExtHostCommands } from './extHostCommands.js'; export class ExtHostChatContext extends Disposable implements ExtHostChatContextShape { declare _serviceBrand: undefined; @@ -19,16 +20,21 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext private _handlePool: number = 0; private _providers: Map = new Map(); private _itemPool: number = 0; - private _items: Map> = new Map(); // handle -> itemHandle -> item - - constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService, + /** Global map of itemHandle -> original item for command execution with reference equality */ + private _globalItems: Map = new Map(); + /** Track which items belong to which provider for cleanup */ + private _providerItems: Map> = new Map(); // providerHandle -> Set + + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService, + @IExtHostCommands private readonly _commands: IExtHostCommands, ) { super(); this._proxy = extHostRpc.getProxy(MainContext.MainThreadChatContext); } async $provideChatContext(handle: number, token: CancellationToken): Promise { - this._items.delete(handle); // clear previous items + this._clearProviderItems(handle); // clear previous items for this provider const provider = this._getProvider(handle); if (!provider.provideChatContextExplicit) { throw new Error('provideChatContext not implemented'); @@ -42,18 +48,30 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext icon: item.icon, label: item.label, modelDescription: item.modelDescription, - value: item.value + value: item.value, + command: item.command ? { id: item.command.command } : undefined }); } return items; } - private _addTrackedItem(handle: number, item: vscode.ChatContextItem): number { + private _clearProviderItems(handle: number): void { + const itemHandles = this._providerItems.get(handle); + if (itemHandles) { + for (const itemHandle of itemHandles) { + this._globalItems.delete(itemHandle); + } + itemHandles.clear(); + } + } + + private _addTrackedItem(providerHandle: number, item: vscode.ChatContextItem): number { const itemHandle = this._itemPool++; - if (!this._items.has(handle)) { - this._items.set(handle, new Map()); + this._globalItems.set(itemHandle, item); + if (!this._providerItems.has(providerHandle)) { + this._providerItems.set(providerHandle, new Set()); } - this._items.get(handle)!.set(itemHandle, item); + this._providerItems.get(providerHandle)!.add(itemHandle); return itemHandle; } @@ -75,7 +93,8 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext icon: result.icon, label: result.label, modelDescription: result.modelDescription, - value: options.withValue ? result.value : undefined + value: options.withValue ? result.value : undefined, + command: result.command ? { id: result.command.command } : undefined }; if (options.withValue && !item.value && provider.resolveChatContext) { const resolved = await provider.resolveChatContext(result, token); @@ -87,14 +106,17 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext private async _doResolve(provider: vscode.ChatContextProvider, context: IChatContextItem, extItem: vscode.ChatContextItem, token: CancellationToken): Promise { const extResult = await provider.resolveChatContext(extItem, token); - const result = extResult ?? context; - return { - handle: context.handle, - icon: result.icon, - label: result.label, - modelDescription: result.modelDescription, - value: result.value - }; + if (extResult) { + return { + handle: context.handle, + icon: extResult.icon, + label: extResult.label, + modelDescription: extResult.modelDescription, + value: extResult.value, + command: extResult.command ? { id: extResult.command.command } : undefined + }; + } + return context; } async $resolveChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise { @@ -103,13 +125,26 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (!provider.resolveChatContext) { throw new Error('resolveChatContext not implemented'); } - const extItem = this._items.get(handle)?.get(context.handle); + const extItem = this._globalItems.get(context.handle); if (!extItem) { throw new Error('Chat context item not found'); } return this._doResolve(provider, context, extItem, token); } + async $executeChatContextItemCommand(itemHandle: number): Promise { + const extItem = this._globalItems.get(itemHandle); + if (!extItem) { + throw new Error('Chat context item not found'); + } + if (!extItem.command) { + throw new Error('Chat context item has no command'); + } + // Execute the command with the original extension item as an argument (reference equality) + const args = extItem.command.arguments ? [extItem, ...extItem.command.arguments] : [extItem]; + await this._commands.executeCommand(extItem.command.command, ...args); + } + registerChatContextProvider(selector: vscode.DocumentSelector | undefined, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { const handle = this._handlePool++; const disposables = new DisposableStore(); @@ -120,6 +155,8 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext return { dispose: () => { this._providers.delete(handle); + this._clearProviderItems(handle); // Clean up tracked items + this._providerItems.delete(handle); this._proxy.$unregisterChatContextProvider(handle); disposables.dispose(); } @@ -134,12 +171,14 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext const workspaceContexts = await provider.provideWorkspaceChatContext!(CancellationToken.None); const resolvedContexts: IChatContextItem[] = []; for (const item of workspaceContexts ?? []) { + const itemHandle = this._addTrackedItem(handle, item); const contextItem: IChatContextItem = { icon: item.icon, label: item.label, modelDescription: item.modelDescription, value: item.value, - handle: this._itemPool++ + handle: itemHandle, + command: item.command ? { id: item.command.command } : undefined }; const resolved = await this._doResolve(provider, contextItem, item, CancellationToken.None); resolvedContexts.push(resolved); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index b8d92947c99cd..03dc0c2207562 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3885,3 +3885,21 @@ 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) { } +} +//#endregion diff --git a/src/vs/workbench/api/test/browser/extHostTypes.test.ts b/src/vs/workbench/api/test/browser/extHostTypes.test.ts index 105b13734bcbf..ef844667ecc91 100644 --- a/src/vs/workbench/api/test/browser/extHostTypes.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypes.test.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { URI } from '../../../../base/common/uri.js'; -import * as types from '../../common/extHostTypes.js'; +import { CancellationError } from '../../../../base/common/errors.js'; +import { MarshalledId } from '../../../../base/common/marshallingIds.js'; +import { Mimes } from '../../../../base/common/mime.js'; import { isWindows } from '../../../../base/common/platform.js'; import { assertType } from '../../../../base/common/types.js'; -import { Mimes } from '../../../../base/common/mime.js'; -import { MarshalledId } from '../../../../base/common/marshallingIds.js'; -import { CancellationError } from '../../../../base/common/errors.js'; +import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import * as types from '../../common/extHostTypes.js'; function assertToJSON(a: any, expected: any) { const raw = JSON.stringify(a); @@ -788,4 +788,118 @@ 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/contrib/chat/browser/actions/chatLanguageModelActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatLanguageModelActions.ts index ae11a7c88a204..a9c3343b1e342 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatLanguageModelActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatLanguageModelActions.ts @@ -248,7 +248,27 @@ class ConfigureLanguageModelsGroupAction extends Action2 { } } +class MigrateLanguageModelsGroupAction extends Action2 { + constructor() { + super({ + id: 'lm.migrateLanguageModelsProviderGroup', + title: localize('lm.migrateGroup', 'Migrate Language Models Group'), + }); + } + + async run(accessor: ServicesAccessor, languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise { + const languageModelsService = accessor.get(ILanguageModelsService); + + if (!languageModelsProviderGroup) { + throw new Error('Language model group is required'); + } + + await languageModelsService.migrateLanguageModelsProviderGroup(languageModelsProviderGroup); + } +} + export function registerLanguageModelActions() { registerAction2(ManageLanguageModelAuthenticationAction); registerAction2(ConfigureLanguageModelsGroupAction); + registerAction2(MigrateLanguageModelsGroupAction); } diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index df8d5a18201ed..478d6c0e848ba 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -57,10 +57,11 @@ import { toHistoryItemHoverContent } from '../../../scm/browser/scmHistory.js'; import { getHistoryItemEditorTitle } from '../../../scm/browser/util.js'; import { ITerminalService } from '../../../terminal/browser/terminal.js'; import { IChatContentReference } from '../../common/chatService/chatService.js'; -import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry, ITerminalVariableEntry } from '../../common/attachments/chatVariableEntries.js'; +import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry, ITerminalVariableEntry, isStringVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../common/languageModels.js'; import { ILanguageModelToolsService, ToolSet } from '../../common/tools/languageModelToolsService.js'; import { getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js'; +import { IChatContextService } from '../contextContrib/chatContextService.js'; const commonHoverOptions: Partial = { style: HoverStyle.Pointer, @@ -584,6 +585,16 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { this._register(this.instantiationService.invokeFunction(hookUpSymbolAttachmentDragAndContextMenu, this.element, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext)); } + // Handle click for string context attachments with context commands + if (isStringVariableEntry(attachment) && attachment.commandId) { + this.element.style.cursor = 'pointer'; + const contextItemHandle = attachment.handle; + this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async () => { + const chatContextService = this.instantiationService.invokeFunction(accessor => accessor.get(IChatContextService)); + await chatContextService.executeChatContextItemCommand(contextItemHandle); + })); + } + if (resource) { this.addResourceOpenHandlers(resource, range); } diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts index eac7f98336058..48aee8ff8874b 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts @@ -388,7 +388,9 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli value: this.value.value ?? this.name, modelDescription: this.modelDescription, icon: this.value.icon, - uri: this.value.uri + uri: this.value.uri, + handle: this.value.handle, + commandId: this.value.commandId } ]; } diff --git a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts index 5a4815ff15d76..cc939a7ff93ff 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts @@ -209,7 +209,9 @@ export class ImplicitContextAttachmentWidget extends Disposable { name: this.attachment.name, icon: this.attachment.value.icon, modelDescription: this.attachment.value.modelDescription, - uri: this.attachment.value.uri + uri: this.attachment.value.uri, + commandId: this.attachment.value.commandId, + handle: this.attachment.value.handle }; this.attachmentModel.addContext(context); } else { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 77992fd260918..fef14beefadb5 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -53,6 +53,7 @@ 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 } from '../common/promptSyntax/config/promptFileLocations.js'; import { PromptLanguageFeaturesProvider } from '../common/promptSyntax/promptFileContributions.js'; @@ -94,6 +95,7 @@ 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 { ChatEditingEditorAccessibility } from './chatEditing/chatEditingEditorAccessibility.js'; import { registerChatEditorActions } from './chatEditing/chatEditingEditorActions.js'; import { ChatEditingEditorContextKeys } from './chatEditing/chatEditingEditorContextKeys.js'; @@ -1120,6 +1122,7 @@ AccessibleViewRegistry.register(new EditsChatAccessibilityHelp()); AccessibleViewRegistry.register(new AgentChatAccessibilityHelp()); registerEditorFeature(ChatInputBoxContentProvider); +registerEditorFeature(ChatPromptContentProvider); class ChatSlashStaticSlashCommandsContribution extends Disposable { @@ -1280,6 +1283,7 @@ 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/chatEditing/media/chatEditingEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css index 1033ada08b1e9..a29b827631234 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css @@ -6,7 +6,7 @@ .chat-editor-overlay-widget { padding: 2px 4px; color: var(--vscode-foreground); - background-color: var(--vscode-editor-background); + background-color: var(--vscode-editorWidget-background); border-radius: 6px; border: 1px solid var(--vscode-contrastBorder); display: flex; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css index 5e4b3de1ebc60..402bd4b6b0b73 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css @@ -19,7 +19,7 @@ .chat-diff-change-content-widget .monaco-action-bar { padding: 4px 4px; border-radius: 6px; - background-color: var(--vscode-editor-background); + background-color: var(--vscode-editorWidget-background); color: var(--vscode-foreground); border: 1px solid var(--vscode-contrastBorder); overflow: hidden; diff --git a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts index 5538c84c4b13f..57850cea5cac0 100644 --- a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts +++ b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts @@ -34,6 +34,7 @@ export class ChatContextService extends Disposable { private readonly _workspaceContext = new Map(); private readonly _registeredPickers = this._register(new DisposableMap()); private _lastResourceContext: Map = new Map(); + private _executeCommandCallback: ((itemHandle: number) => Promise) | undefined; constructor( @IChatContextPickService private readonly _contextPickService: IChatContextPickService, @@ -42,6 +43,17 @@ export class ChatContextService extends Disposable { super(); } + setExecuteCommandCallback(callback: (itemHandle: number) => Promise): void { + this._executeCommandCallback = callback; + } + + async executeChatContextItemCommand(handle: number): Promise { + if (!this._executeCommandCallback) { + return; + } + await this._executeCommandCallback(handle); + } + setChatContextProvider(id: string, picker: { title: string; icon: ThemeIcon }): void { const providerEntry = this._providers.get(id) ?? { picker: undefined }; providerEntry.picker = picker; @@ -110,7 +122,8 @@ export class ChatContextService extends Disposable { if (scoredProviders.length === 0 || scoredProviders[0].score <= 0) { return; } - const context = (await scoredProviders[0].provider.provideChatContextForResource!(uri, withValue, CancellationToken.None)); + const provider = scoredProviders[0].provider; + const context = (await provider.provideChatContextForResource!(uri, withValue, CancellationToken.None)); if (!context) { return; } @@ -119,10 +132,12 @@ export class ChatContextService extends Disposable { name: context.label, icon: context.icon, uri: uri, - modelDescription: context.modelDescription + modelDescription: context.modelDescription, + commandId: context.command?.id, + handle: context.handle }; this._lastResourceContext.clear(); - this._lastResourceContext.set(contextValue, { originalItem: context, provider: scoredProviders[0].provider }); + this._lastResourceContext.set(contextValue, { originalItem: context, provider }); return contextValue; } @@ -183,7 +198,7 @@ export class ChatContextService extends Disposable { id: contextValue.label, name: contextValue.label, icon: contextValue.icon, - value: contextValue.value + value: contextValue.value, }; } })); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptContentProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptContentProvider.ts new file mode 100644 index 0000000000000..b193910212ffe --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/chatPromptContentProvider.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * 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/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index a98c158ad8c5c..00f852701f399 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -86,7 +86,7 @@ import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes import { IChatFollowup, IChatService } from '../../../common/chatService/chatService.js'; import { IChatSessionFileChange, IChatSessionProviderOptionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; import { getChatSessionType } from '../../../common/model/chatUri.js'; -import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; +import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; import { IChatResponseViewModel } from '../../../common/model/chatViewModel.js'; import { ChatHistoryNavigator } from '../../../common/widget/chatWidgetHistoryService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../../../common/constants.js'; @@ -217,7 +217,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const contextArr = this.getAttachedContext(sessionResource); - if ((this.implicitContext?.enabled && this.implicitContext?.value) || (this.implicitContext && !URI.isUri(this.implicitContext.value) && this.configurationService.getValue('chat.implicitContext.suggestedContext'))) { + if ((this.implicitContext?.enabled && this.implicitContext?.value) || (this.implicitContext && !URI.isUri(this.implicitContext.value) && !isStringImplicitContextValue(this.implicitContext.value) && this.configurationService.getValue('chat.implicitContext.suggestedContext'))) { const implicitChatVariables = this.implicitContext.toBaseEntries(); contextArr.add(...implicitChatVariables); } diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index 54452201d19e7..c45c4899abccc 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -73,6 +73,11 @@ export interface StringChatContextValue { modelDescription?: string; icon: ThemeIcon; uri: URI; + /** + * Command ID to execute when this context item is clicked. + */ + readonly commandId?: string; + readonly handle: number; } export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVariableEntry { @@ -90,6 +95,11 @@ export interface IChatRequestStringVariableEntry extends IBaseChatRequestVariabl readonly modelDescription?: string; readonly icon: ThemeIcon; readonly uri: URI; + /** + * Command ID to execute when this context item is clicked. + */ + readonly commandId?: string; + readonly handle: number; } export interface IChatRequestWorkspaceVariableEntry extends IBaseChatRequestVariableEntry { @@ -329,7 +339,6 @@ export namespace IChatRequestVariableEntry { } } - export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry { return obj.kind === 'implicit'; } diff --git a/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts b/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts index 854d415e2086d..6973240728def 100644 --- a/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts +++ b/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts @@ -13,6 +13,9 @@ export interface IChatContextItem { modelDescription?: string; handle: number; value?: string; + command?: { + id: string; + }; } export interface IChatContextSupport { diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 92567694b67e3..c3d0baa61adf5 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -303,9 +303,8 @@ export interface ILanguageModelsService { /** * Given a selector, returns a list of model identifiers * @param selector The selector to lookup for language models. If the selector is empty, all language models are returned. - * @param allowPromptingUser If true the user may be prompted for things like API keys for us to select the model. */ - selectLanguageModels(selector: ILanguageModelChatSelector, allowPromptingUser?: boolean): Promise; + selectLanguageModels(selector: ILanguageModelChatSelector): Promise; registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable; @@ -319,6 +318,8 @@ export interface ILanguageModelsService { removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise; configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise; + + migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise; } const languageModelChatProviderType = { @@ -477,6 +478,10 @@ export class LanguageModelsService implements ILanguageModelsService { }); } + private _saveModelPickerPreferences(): void { + this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER); + } + updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void { const model = this._modelCache.get(modelIdentifier); if (!model) { @@ -487,9 +492,9 @@ export class LanguageModelsService implements ILanguageModelsService { this._modelPickerUserPreferences[modelIdentifier] = showInModelPicker; if (showInModelPicker === model.isUserSelectable) { delete this._modelPickerUserPreferences[modelIdentifier]; - this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER); + this._saveModelPickerPreferences(); } else if (model.isUserSelectable !== showInModelPicker) { - this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER); + this._saveModelPickerPreferences(); } this._onLanguageModelChange.fire(model.vendor); this._logService.trace(`[LM] Updated model picker preference for ${modelIdentifier} to ${showInModelPicker}`); @@ -543,10 +548,17 @@ export class LanguageModelsService implements ILanguageModelsService { const languageModelsGroups: ILanguageModelsGroup[] = []; try { - const models = await this._resolveLanguageModels(provider, { silent }); + const models = await provider.provideLanguageModelChatInfo({ silent }, CancellationToken.None); if (models.length) { allModels.push(...models); - languageModelsGroups.push({ modelIdentifiers: models.map(m => m.identifier) }); + const modelIdentifiers = []; + for (const m of models) { + // Special case for copilot models - they are all user selectable unless marked otherwise + if (vendorId === 'copilot' && (m.metadata.isUserSelectable || this._modelPickerUserPreferences[m.identifier] === true)) { + modelIdentifiers.push(m.identifier); + } + } + languageModelsGroups.push({ modelIdentifiers }); } } catch (error) { languageModelsGroups.push({ @@ -567,7 +579,7 @@ export class LanguageModelsService implements ILanguageModelsService { const configuration = await this._resolveConfiguration(group, vendor.configuration); try { - const models = await this._resolveLanguageModels(provider, { group: group.name, silent, configuration }); + const models = await provider.provideLanguageModelChatInfo({ group: group.name, silent, configuration }, CancellationToken.None); if (models.length) { allModels.push(...models); languageModelsGroups.push({ group, modelIdentifiers: models.map(m => m.identifier) }); @@ -598,29 +610,18 @@ export class LanguageModelsService implements ILanguageModelsService { }); } - private async _resolveLanguageModels(provider: ILanguageModelChatProvider, options: ILanguageModelChatInfoOptions): Promise { - let models = await provider.provideLanguageModelChatInfo(options, CancellationToken.None); - if (models.length) { - // This is a bit of a hack, when prompting user if the provider returns any models that are user selectable then we only want to show those and not the entire model list - if (!options.silent && models.some(m => m.metadata.isUserSelectable)) { - models = models.filter(m => m.metadata.isUserSelectable || this._modelPickerUserPreferences[m.identifier] === true); - } - } - return models; - } - async fetchLanguageModelGroups(vendor: string): Promise { await this._resolveAllLanguageModels(vendor, true); return this._modelsGroups.get(vendor) ?? []; } - async selectLanguageModels(selector: ILanguageModelChatSelector, allowPromptingUser?: boolean): Promise { + async selectLanguageModels(selector: ILanguageModelChatSelector): Promise { if (selector.vendor) { - await this._resolveAllLanguageModels(selector.vendor, !allowPromptingUser); + await this._resolveAllLanguageModels(selector.vendor, true); } else { const allVendors = Array.from(this._vendors.keys()); - await Promise.all(allVendors.map(vendor => this._resolveAllLanguageModels(vendor, !allowPromptingUser))); + await Promise.all(allVendors.map(vendor => this._resolveAllLanguageModels(vendor, true))); } const result: string[] = []; @@ -696,7 +697,7 @@ export class LanguageModelsService implements ILanguageModelsService { } if (vendor.managementCommand) { - await this.selectLanguageModels({ vendor: vendor.vendor }, true); + await this._resolveAllLanguageModels(vendor.vendor, false); return; } @@ -1050,6 +1051,31 @@ export class LanguageModelsService implements ILanguageModelsService { } } + async migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise { + const { vendor, name, ...configuration } = languageModelsProviderGroup; + if (!this._vendors.get(vendor)) { + throw new Error(`Vendor ${vendor} not found.`); + } + + await this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendor}`); + const provider = this._providers.get(vendor); + if (!provider) { + throw new Error(`Chat model provider for vendor ${vendor} is not registered.`); + } + + const models = await provider.provideLanguageModelChatInfo({ group: name, silent: false, configuration }, CancellationToken.None); + for (const model of models) { + const oldIdentifier = `${vendor}/${model.metadata.id}`; + if (this._modelPickerUserPreferences[oldIdentifier] === true) { + this._modelPickerUserPreferences[model.identifier] = true; + } + delete this._modelPickerUserPreferences[oldIdentifier]; + } + this._saveModelPickerPreferences(); + + await this.addLanguageModelsProviderGroup(name, vendor, configuration); + } + dispose() { this._store.dispose(); this._providers.clear(); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts new file mode 100644 index 0000000000000..30de1ac7e0c8a --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * 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(); + } + + registerContent(uri: URI, content: string): { dispose: () => void } { + const key = uri.toString(); + this._contentMap.set(key, content); + + const dispose = () => { + this._contentMap.delete(key); + }; + + return { dispose }; + } + + getContent(uri: URI): string | undefined { + return this._contentMap.get(uri.toString()); + } + + override dispose(): void { + this._contentMap.clear(); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts index a5352e3e7b0a8..6952e85834f01 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts @@ -3,14 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { DisposableMap } from '../../../../../base/common/lifecycle.js'; +import { joinPath, isEqualOrParent } from '../../../../../base/common/resources.js'; +import { UriComponents } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; +import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; +import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; -import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; -import { joinPath, isEqualOrParent } from '../../../../../base/common/resources.js'; -import { IPromptsService } from './service/promptsService.js'; +import { IPromptsService, PromptsStorage } from './service/promptsService.js'; import { PromptsType } from './promptTypes.js'; -import { DisposableMap } from '../../../../../base/common/lifecycle.js'; interface IRawChatFileContribution { readonly path: string; @@ -117,3 +120,35 @@ export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribut }); } } + +/** + * Result type for the extension prompt file provider command. + */ +export interface IExtensionPromptFileResult { + readonly uri: UriComponents; + readonly type: PromptsType; +} + +/** + * Register the command to list all extension-contributed prompt files. + */ +CommandsRegistry.registerCommand('_listExtensionPromptFiles', async (accessor): Promise => { + const promptsService = accessor.get(IPromptsService); + + // Get extension prompt files for all prompt types in parallel + const [agents, instructions, prompts] = await Promise.all([ + promptsService.listPromptFiles(PromptsType.agent, CancellationToken.None), + promptsService.listPromptFiles(PromptsType.instructions, CancellationToken.None), + promptsService.listPromptFiles(PromptsType.prompt, CancellationToken.None), + ]); + + // Combine all files and collect extension-contributed ones + const result: IExtensionPromptFileResult[] = []; + for (const file of [...agents, ...instructions, ...prompts]) { + if (file.storage === PromptsStorage.extension) { + result.push({ uri: file.uri.toJSON(), type: file.type }); + } + } + + return result; +}); 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 ccf1a4c42f4e3..f84cc15db382f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -41,6 +41,13 @@ export interface IPromptFileResource { * 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 eac1705a2d044..47b297427bbb7 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -35,6 +35,7 @@ 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'; /** * Provides prompt services. @@ -98,7 +99,8 @@ export class PromptsService extends Disposable implements IPromptsService { @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IChatPromptContentStore private readonly chatPromptContentStore: IChatPromptContentStore ) { super(); @@ -502,6 +504,16 @@ export class PromptsService extends Disposable implements IPromptsService { if (model) { 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/chatManagement/chatModelsViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts index bcf006ffa271b..d008e7e89a8cf 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts @@ -13,7 +13,7 @@ import { IChatEntitlementService, ChatEntitlement } from '../../../../../service import { IObservable, observableValue } from '../../../../../../base/common/observable.js'; import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; import { IStringDictionary } from '../../../../../../base/common/collections.js'; -import { ILanguageModelsConfigurationService } from '../../../common/languageModelsConfiguration.js'; +import { ILanguageModelsConfigurationService, ILanguageModelsProviderGroup } from '../../../common/languageModelsConfiguration.js'; import { mock } from '../../../../../../base/test/common/mock.js'; import { ChatAgentLocation } from '../../../common/constants.js'; @@ -119,6 +119,8 @@ class MockLanguageModelsService implements ILanguageModelsService { async removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise { } + + async migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise { } } class MockChatEntitlementService implements IChatEntitlementService { 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 new file mode 100644 index 0000000000000..6323d234eba1e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptContentProvider.test.ts @@ -0,0 +1,215 @@ +/*--------------------------------------------------------------------------------------------- + * 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/common/languageModels.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.ts index f38801abec300..8b60a21829111 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.ts @@ -9,6 +9,7 @@ import { Event } from '../../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { IChatMessage, ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatProvider, ILanguageModelChatResponse, ILanguageModelChatSelector, ILanguageModelsGroup, ILanguageModelsService, IUserFriendlyLanguageModel } from '../../common/languageModels.js'; +import { ILanguageModelsProviderGroup } from '../../common/languageModelsConfiguration.js'; export class NullLanguageModelsService implements ILanguageModelsService { _serviceBrand: undefined; @@ -74,4 +75,6 @@ export class NullLanguageModelsService implements ILanguageModelsService { async removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise { } + + async migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise { } } 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 new file mode 100644 index 0000000000000..3d3236124b97e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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); + }); +}); diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 9bda6c319fc28..0ba9d0381a8a3 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -269,13 +269,13 @@ /* single install */ .extension-list-item .monaco-action-bar > .actions-container > .action-item.action-dropdown-item.empty > .extension-action { - border-radius: 2px; + border-radius: var(--vscode-cornerRadius-small); } /* split install */ .extension-list-item .monaco-action-bar > .actions-container > .action-item.action-dropdown-item:not(.empty) > .extension-action.label:not(.dropdown) { - border-radius: 2px 0 0 2px; + border-radius: var(--vscode-cornerRadius-small) 0 0 var(--vscode-cornerRadius-small); } .extension-list-item .monaco-action-bar > .actions-container > .action-item.action-dropdown-item > .monaco-dropdown .extension-action { - border-radius: 0 2px 2px 0; + border-radius: 0 var(--vscode-cornerRadius-small) var(--vscode-cornerRadius-small) 0; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 33bac6a02734b..700b138b98dc2 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -437,7 +437,7 @@ export class InlineChatController implements IEditorContribution { const persistModelChoice = this._configurationService.getValue(InlineChatConfigKeys.PersistModelChoice); const model = this._zone.value.widget.chatWidget.input.selectedLanguageModel; if (!persistModelChoice && InlineChatController._selectVendorDefaultLanguageModel && model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) { - const ids = await this._languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }, false); + const ids = await this._languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }); for (const identifier of ids) { const candidate = this._languageModelService.lookupLanguageModel(identifier); if (candidate?.isDefaultForLocation[session.chatModel.initialLocation]) { @@ -484,7 +484,7 @@ export class InlineChatController implements IEditorContribution { delete arg.attachments; } if (arg.modelSelector) { - const id = (await this._languageModelService.selectLanguageModels(arg.modelSelector, false)).sort().at(0); + const id = (await this._languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0); if (!id) { throw new Error(`No language models found matching selector: ${JSON.stringify(arg.modelSelector)}.`); } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts index 6de2cb80cc32f..9c146f6a13a5e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts @@ -9,7 +9,7 @@ import { timeout, type MaybePromise } from '../../../../../../../base/common/asy import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; -import { Disposable, type IDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable, type IDisposable } from '../../../../../../../base/common/lifecycle.js'; import { isObject, isString } from '../../../../../../../base/common/types.js'; import { localize } from '../../../../../../../nls.js'; import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; @@ -66,7 +66,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { * This is used to skip showing prompts if the user already provided input. */ private _userInputtedSinceIdleDetected = false; - private _userInputListener: IDisposable | undefined; + private readonly _userInputListener = this._register(new MutableDisposable()); private readonly _outputMonitorTelemetryCounters: IOutputMonitorTelemetryCounters = { inputToolManualAcceptCount: 0, @@ -168,8 +168,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { resources }; // Clean up idle input listener if still active - this._userInputListener?.dispose(); - this._userInputListener = undefined; + this._userInputListener.clear(); const promptPart = this._promptPart; this._promptPart = undefined; if (promptPart) { @@ -346,15 +345,11 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { * This ensures we catch any input that happens between idle detection and prompt creation. */ private _setupIdleInputListener(): void { - // Clean up any existing listener - this._userInputListener?.dispose(); this._userInputtedSinceIdleDetected = false; - // Set up new listener - this._userInputListener = this._execution.instance.onDidInputData((data) => { - if (data === '\r' || data === '\n' || data === '\r\n') { - this._userInputtedSinceIdleDetected = true; - } + // Set up new listener (MutableDisposable auto-disposes previous) + this._userInputListener.value = this._execution.instance.onDidInputData(() => { + this._userInputtedSinceIdleDetected = true; }); } @@ -363,8 +358,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { */ private _cleanupIdleInputListener(): void { this._userInputtedSinceIdleDetected = false; - this._userInputListener?.dispose(); - this._userInputListener = undefined; + this._userInputListener.clear(); } private async _assessOutputForErrors(buffer: string, token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index cf7c1be09b43f..ea7f36418535b 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -1180,12 +1180,12 @@ export class ExtensionManagementService extends CommontExtensionManagementServic const trustedPublishers = this.storageService.getObject>(TrustedPublishersStorageKey, StorageScope.APPLICATION, {}); if (Array.isArray(trustedPublishers)) { this.storageService.remove(TrustedPublishersStorageKey, StorageScope.APPLICATION); - return {}; + return Object.create(null); } return Object.keys(trustedPublishers).reduce>((result, publisher) => { result[publisher.toLowerCase()] = trustedPublishers[publisher]; return result; - }, {}); + }, Object.create(null)); } } diff --git a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts index e23095918243e..81515d9748074 100644 --- a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts @@ -43,6 +43,11 @@ declare module 'vscode' { * The value of the context item. Can be omitted when returned from one of the `provide` methods if the provider supports `resolveChatContext`. */ value?: string; + /** + * An optional command that is executed when the context item is clicked. + * The original context item will be passed as the first argument to the command. + */ + command?: Command; } export interface ChatContextProvider { diff --git a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts index 8a9017558070c..b0da5fe132161 100644 --- a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts +++ b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts @@ -7,68 +7,103 @@ declare module 'vscode' { - // #region CustomAgentProvider + // #region Resource Classes /** - * Represents a custom agent resource file (e.g., .agent.md) available for a repository. + * Describes a chat resource file. */ - export interface CustomAgentResource { + 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 { /** - * The URI to the custom agent resource file. + * The custom agent resource descriptor. */ - readonly uri: Uri; + readonly resource: ChatResourceDescriptor; /** - * Indicates whether the custom agent is editable. Defaults to false. + * Creates a new custom agent resource from the specified resource. + * @param resource The chat resource descriptor. */ - readonly isEditable?: boolean; + constructor(resource: ChatResourceDescriptor); } /** - * Context for querying custom agents. + * Represents an instructions resource file. */ - export type CustomAgentContext = object; + export class InstructionsChatResource { + /** + * The instructions resource descriptor. + */ + readonly resource: ChatResourceDescriptor; - /** - * A provider that supplies custom agent resources (from .agent.md files) for repositories. - */ - export interface CustomAgentProvider { /** - * A human-readable label for this provider. + * Creates a new instructions resource from the specified resource. + * @param resource The chat resource descriptor. */ - readonly label: string; + constructor(resource: ChatResourceDescriptor); + } + /** + * Represents a prompt file resource (e.g., .prompt.md). + */ + export class PromptFileChatResource { /** - * An optional event to signal that custom agents have changed. + * The prompt file resource descriptor. */ - readonly onDidChangeCustomAgents?: Event; + readonly resource: ChatResourceDescriptor; /** - * Provide the list of custom agents available. - * @param context Context for the query. - * @param token A cancellation token. - * @returns An array of custom agent resources or a promise that resolves to such. + * Creates a new prompt file resource from the specified resource. + * @param resource The chat resource descriptor. */ - provideCustomAgents(context: CustomAgentContext, token: CancellationToken): ProviderResult; + constructor(resource: ChatResourceDescriptor); } // #endregion - // #region InstructionsProvider + // #region Providers + + /** + * Options for querying custom agents. + */ + export type CustomAgentContext = object; /** - * Represents an instructions resource file available for a repository. + * A provider that supplies custom agent resources (from .agent.md files) for repositories. */ - export interface InstructionsResource { + export interface CustomAgentProvider { + /** + * A human-readable label for this provider. + */ + readonly label: string; + /** - * The URI to the instructions resource file. + * An optional event to signal that custom agents have changed. */ - readonly uri: Uri; + readonly onDidChangeCustomAgents?: Event; /** - * Indicates whether the instructions are editable. Defaults to false. + * Provide the list of custom agents available. + * @param context Context for the query. + * @param token A cancellation token. + * @returns An array of custom agents or a promise that resolves to such. */ - readonly isEditable?: boolean; + provideCustomAgents( + context: CustomAgentContext, + token: CancellationToken + ): ProviderResult; } /** @@ -94,28 +129,12 @@ declare module 'vscode' { * Provide the list of instructions available. * @param context Context for the query. * @param token A cancellation token. - * @returns An array of instructions resources or a promise that resolves to such. - */ - provideInstructions(context: InstructionsContext, token: CancellationToken): ProviderResult; - } - - // #endregion - - // #region PromptFileProvider - - /** - * Represents a prompt file resource (e.g., .prompt.md) available for a repository. - */ - export interface PromptFileResource { - /** - * The URI to the prompt file resource. - */ - readonly uri: Uri; - - /** - * Indicates whether the prompt file is editable. Defaults to false. + * @returns An array of instructions or a promise that resolves to such. */ - readonly isEditable?: boolean; + provideInstructions( + context: InstructionsContext, + token: CancellationToken + ): ProviderResult; } /** @@ -141,9 +160,12 @@ declare module 'vscode' { * Provide the list of prompt files available. * @param context Context for the query. * @param token A cancellation token. - * @returns An array of prompt file resources or a promise that resolves to such. + * @returns An array of prompt files or a promise that resolves to such. */ - providePromptFiles(context: PromptFileContext, token: CancellationToken): ProviderResult; + providePromptFiles( + context: PromptFileContext, + token: CancellationToken + ): ProviderResult; } // #endregion @@ -156,21 +178,27 @@ 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: CustomAgentProvider + ): 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: InstructionsProvider + ): 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: PromptFileProvider + ): Disposable; } // #endregion diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 7438ab0d27a1c..00b5019d0b4c7 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -839,6 +839,16 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", + "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",