diff --git a/.vscode/settings.json b/.vscode/settings.json index bfb8f5e3e9a..d9aeed84432 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -210,6 +210,5 @@ ], "azureMcp.serverMode": "all", "azureMcp.readOnly": true, - "chat.tools.terminal.outputLocation": "chat", "debug.breakpointsView.presentation": "tree" } diff --git a/build/azure-pipelines/common/releaseBuild.ts b/build/azure-pipelines/common/releaseBuild.ts index 01792fd22e1..92b6d22614d 100644 --- a/build/azure-pipelines/common/releaseBuild.ts +++ b/build/azure-pipelines/common/releaseBuild.ts @@ -4,7 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { CosmosClient } from '@azure/cosmos'; +import path from 'path'; +import fs from 'fs'; import { retry } from './retry.ts'; +import { type IExtensionManifest, parseApiProposalsFromSource, checkExtensionCompatibility } from './versionCompatibility.ts'; + +const root = path.dirname(path.dirname(path.dirname(import.meta.dirname))); function getEnv(name: string): string { const result = process.env[name]; @@ -16,6 +21,80 @@ function getEnv(name: string): string { return result; } +async function fetchLatestExtensionManifest(extensionId: string): Promise { + // Use the vscode-unpkg service to get the latest extension package.json + const [publisher, name] = extensionId.split('.'); + + // First, get the latest version from the gallery endpoint + const galleryUrl = `https://main.vscode-unpkg.net/_gallery/${publisher}/${name}/latest`; + const galleryResponse = await fetch(galleryUrl, { + headers: { 'User-Agent': 'VSCode Build' } + }); + + if (!galleryResponse.ok) { + throw new Error(`Failed to fetch latest version for ${extensionId}: ${galleryResponse.status} ${galleryResponse.statusText}`); + } + + const galleryData = await galleryResponse.json() as { versions: { version: string }[] }; + const version = galleryData.versions[0].version; + + // Now fetch the package.json using the actual version + const url = `https://${publisher}.vscode-unpkg.net/${publisher}/${name}/${version}/extension/package.json`; + + const response = await fetch(url, { + headers: { 'User-Agent': 'VSCode Build' } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch extension ${extensionId} from unpkg: ${response.status} ${response.statusText}`); + } + + return await response.json() as IExtensionManifest; +} + +async function checkCopilotChatCompatibility(): Promise { + const extensionId = 'github.copilot-chat'; + + console.log(`Checking compatibility of ${extensionId}...`); + + // Get product version from package.json + const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); + const productVersion = packageJson.version; + + console.log(`Product version: ${productVersion}`); + + // Get API proposals from the generated file + const apiProposalsPath = path.join(root, 'src/vs/platform/extensions/common/extensionsApiProposals.ts'); + const apiProposalsContent = fs.readFileSync(apiProposalsPath, 'utf8'); + const allApiProposals = parseApiProposalsFromSource(apiProposalsContent); + + const proposalCount = Object.keys(allApiProposals).length; + if (proposalCount === 0) { + throw new Error('Failed to load API proposals from source'); + } + + console.log(`Loaded ${proposalCount} API proposals from source`); + + // Fetch the latest extension manifest + const manifest = await retry(() => fetchLatestExtensionManifest(extensionId)); + + console.log(`Extension ${extensionId}@${manifest.version}:`); + console.log(` engines.vscode: ${manifest.engines.vscode}`); + console.log(` enabledApiProposals:\n ${manifest.enabledApiProposals?.join('\n ') || 'none'}`); + + // Check compatibility + const result = checkExtensionCompatibility(productVersion, allApiProposals, manifest); + if (!result.compatible) { + throw new Error(`Compatibility check failed:\n ${result.errors.join('\n ')}`); + } + + console.log(` ✓ Engine version compatible`); + if (manifest.enabledApiProposals?.length) { + console.log(` ✓ API proposals compatible`); + } + console.log(`✓ ${extensionId} is compatible with this build`); +} + interface Config { id: string; frozen: boolean; @@ -43,6 +122,12 @@ async function getConfig(client: CosmosClient, quality: string): Promise async function main(force: boolean): Promise { const commit = getEnv('BUILD_SOURCEVERSION'); const quality = getEnv('VSCODE_QUALITY'); + + // Check Copilot Chat compatibility before releasing insider builds + if (quality === 'insider') { + await checkCopilotChatCompatibility(); + } + const { cosmosDBAccessToken } = JSON.parse(getEnv('PUBLISH_AUTH_TOKENS')); const client = new CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT']!, tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken.token}`) }); diff --git a/build/azure-pipelines/common/versionCompatibility.ts b/build/azure-pipelines/common/versionCompatibility.ts new file mode 100644 index 00000000000..3246ef04df5 --- /dev/null +++ b/build/azure-pipelines/common/versionCompatibility.ts @@ -0,0 +1,347 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +export interface IExtensionManifest { + name: string; + publisher: string; + version: string; + engines: { vscode: string }; + main?: string; + browser?: string; + enabledApiProposals?: string[]; +} + +export function isEngineCompatible(productVersion: string, engineVersion: string): { compatible: boolean; error?: string } { + if (engineVersion === '*') { + return { compatible: true }; + } + + const versionMatch = engineVersion.match(/^(\^|>=)?(\d+)\.(\d+)\.(\d+)/); + if (!versionMatch) { + return { compatible: false, error: `Could not parse engines.vscode value: ${engineVersion}` }; + } + + const [, prefix, major, minor, patch] = versionMatch; + const productMatch = productVersion.match(/^(\d+)\.(\d+)\.(\d+)/); + if (!productMatch) { + return { compatible: false, error: `Could not parse product version: ${productVersion}` }; + } + + const [, prodMajor, prodMinor, prodPatch] = productMatch; + + const reqMajor = parseInt(major); + const reqMinor = parseInt(minor); + const reqPatch = parseInt(patch); + const pMajor = parseInt(prodMajor); + const pMinor = parseInt(prodMinor); + const pPatch = parseInt(prodPatch); + + if (prefix === '>=') { + // Minimum version check + if (pMajor > reqMajor) { return { compatible: true }; } + if (pMajor < reqMajor) { return { compatible: false, error: `Extension requires VS Code >=${engineVersion}, but product version is ${productVersion}` }; } + if (pMinor > reqMinor) { return { compatible: true }; } + if (pMinor < reqMinor) { return { compatible: false, error: `Extension requires VS Code >=${engineVersion}, but product version is ${productVersion}` }; } + if (pPatch >= reqPatch) { return { compatible: true }; } + return { compatible: false, error: `Extension requires VS Code >=${engineVersion}, but product version is ${productVersion}` }; + } + + // Caret or exact version check + if (pMajor !== reqMajor) { + return { compatible: false, error: `Extension requires VS Code ${engineVersion}, but product version is ${productVersion} (major version mismatch)` }; + } + + if (prefix === '^') { + // Caret: same major, minor and patch must be >= required + if (pMinor > reqMinor) { return { compatible: true }; } + if (pMinor < reqMinor) { return { compatible: false, error: `Extension requires VS Code ${engineVersion}, but product version is ${productVersion}` }; } + if (pPatch >= reqPatch) { return { compatible: true }; } + return { compatible: false, error: `Extension requires VS Code ${engineVersion}, but product version is ${productVersion}` }; + } + + // Exact or default behavior + if (pMinor < reqMinor) { return { compatible: false, error: `Extension requires VS Code ${engineVersion}, but product version is ${productVersion}` }; } + if (pMinor > reqMinor) { return { compatible: true }; } + if (pPatch >= reqPatch) { return { compatible: true }; } + return { compatible: false, error: `Extension requires VS Code ${engineVersion}, but product version is ${productVersion}` }; +} + +export function parseApiProposals(enabledApiProposals: string[]): { proposalName: string; version?: number }[] { + return enabledApiProposals.map(proposal => { + const [proposalName, version] = proposal.split('@'); + return { proposalName, version: version ? parseInt(version) : undefined }; + }); +} + +export function areApiProposalsCompatible( + apiProposals: string[], + productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }> +): { compatible: boolean; errors: string[] } { + if (apiProposals.length === 0) { + return { compatible: true, errors: [] }; + } + + const errors: string[] = []; + const parsedProposals = parseApiProposals(apiProposals); + + for (const { proposalName, version } of parsedProposals) { + if (!version) { + continue; + } + const existingProposal = productApiProposals[proposalName]; + if (!existingProposal) { + errors.push(`API proposal '${proposalName}' does not exist in this version of VS Code`); + } else if (existingProposal.version !== version) { + errors.push(`API proposal '${proposalName}' version mismatch: extension requires version ${version}, but VS Code has version ${existingProposal.version ?? 'unversioned'}`); + } + } + + return { compatible: errors.length === 0, errors }; +} + +export function parseApiProposalsFromSource(content: string): { [proposalName: string]: { proposal: string; version?: number } } { + const allApiProposals: { [proposalName: string]: { proposal: string; version?: number } } = {}; + + // Match proposal blocks like: proposalName: {\n\t\tproposal: '...',\n\t\tversion: N\n\t} + // or: proposalName: {\n\t\tproposal: '...',\n\t} + const proposalBlockRegex = /\t(\w+):\s*\{([^}]+)\}/g; + const versionRegex = /version:\s*(\d+)/; + + let match; + while ((match = proposalBlockRegex.exec(content)) !== null) { + const [, name, block] = match; + const versionMatch = versionRegex.exec(block); + allApiProposals[name] = { + proposal: '', + version: versionMatch ? parseInt(versionMatch[1]) : undefined + }; + } + + return allApiProposals; +} + +export function checkExtensionCompatibility( + productVersion: string, + productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>, + manifest: IExtensionManifest +): { compatible: boolean; errors: string[] } { + const errors: string[] = []; + + // Check engine compatibility + const engineResult = isEngineCompatible(productVersion, manifest.engines.vscode); + if (!engineResult.compatible) { + errors.push(engineResult.error!); + } + + // Check API proposals compatibility + if (manifest.enabledApiProposals?.length) { + const apiResult = areApiProposalsCompatible(manifest.enabledApiProposals, productApiProposals); + if (!apiResult.compatible) { + errors.push(...apiResult.errors); + } + } + + return { compatible: errors.length === 0, errors }; +} + +if (import.meta.main) { + console.log('Running version compatibility tests...\n'); + + // isEngineCompatible tests + console.log('Testing isEngineCompatible...'); + + // Wildcard + assert.strictEqual(isEngineCompatible('1.50.0', '*').compatible, true); + + // Invalid engine version + assert.strictEqual(isEngineCompatible('1.50.0', 'invalid').compatible, false); + + // Invalid product version + assert.strictEqual(isEngineCompatible('invalid', '1.50.0').compatible, false); + + // >= prefix + assert.strictEqual(isEngineCompatible('1.50.0', '>=1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.50.1', '>=1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.51.0', '>=1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('2.0.0', '>=1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.49.0', '>=1.50.0').compatible, false); + assert.strictEqual(isEngineCompatible('1.50.0', '>=1.50.1').compatible, false); + assert.strictEqual(isEngineCompatible('0.50.0', '>=1.50.0').compatible, false); + + // ^ prefix (caret) + assert.strictEqual(isEngineCompatible('1.50.0', '^1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.50.1', '^1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.51.0', '^1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.49.0', '^1.50.0').compatible, false); + assert.strictEqual(isEngineCompatible('1.50.0', '^1.50.1').compatible, false); + assert.strictEqual(isEngineCompatible('2.0.0', '^1.50.0').compatible, false); + + // Exact/default (no prefix) + assert.strictEqual(isEngineCompatible('1.50.0', '1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.50.1', '1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.51.0', '1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.49.0', '1.50.0').compatible, false); + assert.strictEqual(isEngineCompatible('1.50.0', '1.50.1').compatible, false); + assert.strictEqual(isEngineCompatible('2.0.0', '1.50.0').compatible, false); + + console.log(' ✓ isEngineCompatible tests passed\n'); + + // parseApiProposals tests + console.log('Testing parseApiProposals...'); + + assert.deepStrictEqual(parseApiProposals([]), []); + assert.deepStrictEqual(parseApiProposals(['proposalA']), [{ proposalName: 'proposalA', version: undefined }]); + assert.deepStrictEqual(parseApiProposals(['proposalA@1']), [{ proposalName: 'proposalA', version: 1 }]); + assert.deepStrictEqual(parseApiProposals(['proposalA@1', 'proposalB', 'proposalC@3']), [ + { proposalName: 'proposalA', version: 1 }, + { proposalName: 'proposalB', version: undefined }, + { proposalName: 'proposalC', version: 3 } + ]); + + console.log(' ✓ parseApiProposals tests passed\n'); + + // areApiProposalsCompatible tests + console.log('Testing areApiProposalsCompatible...'); + + const productProposals = { + proposalA: { proposal: '', version: 1 }, + proposalB: { proposal: '', version: 2 }, + proposalC: { proposal: '' } // unversioned + }; + + // Empty proposals + assert.strictEqual(areApiProposalsCompatible([], productProposals).compatible, true); + + // Unversioned extension proposals (always compatible) + assert.strictEqual(areApiProposalsCompatible(['proposalA', 'proposalB'], productProposals).compatible, true); + assert.strictEqual(areApiProposalsCompatible(['unknownProposal'], productProposals).compatible, true); + + // Versioned proposals - matching + assert.strictEqual(areApiProposalsCompatible(['proposalA@1'], productProposals).compatible, true); + assert.strictEqual(areApiProposalsCompatible(['proposalA@1', 'proposalB@2'], productProposals).compatible, true); + + // Versioned proposals - version mismatch + assert.strictEqual(areApiProposalsCompatible(['proposalA@2'], productProposals).compatible, false); + assert.strictEqual(areApiProposalsCompatible(['proposalB@1'], productProposals).compatible, false); + + // Versioned proposals - missing proposal + assert.strictEqual(areApiProposalsCompatible(['unknownProposal@1'], productProposals).compatible, false); + + // Versioned proposals - product has unversioned + assert.strictEqual(areApiProposalsCompatible(['proposalC@1'], productProposals).compatible, false); + + // Mixed versioned and unversioned + assert.strictEqual(areApiProposalsCompatible(['proposalA@1', 'proposalB'], productProposals).compatible, true); + assert.strictEqual(areApiProposalsCompatible(['proposalA@2', 'proposalB'], productProposals).compatible, false); + + console.log(' ✓ areApiProposalsCompatible tests passed\n'); + + // parseApiProposalsFromSource tests + console.log('Testing parseApiProposalsFromSource...'); + + const sampleSource = ` +export const allApiProposals = { + authSession: { + proposal: 'vscode.proposed.authSession.d.ts', + }, + chatParticipant: { + proposal: 'vscode.proposed.chatParticipant.d.ts', + version: 2 + }, + testProposal: { + proposal: 'vscode.proposed.testProposal.d.ts', + version: 15 + } +}; +`; + + const parsedSource = parseApiProposalsFromSource(sampleSource); + assert.strictEqual(Object.keys(parsedSource).length, 3); + assert.strictEqual(parsedSource['authSession']?.version, undefined); + assert.strictEqual(parsedSource['chatParticipant']?.version, 2); + assert.strictEqual(parsedSource['testProposal']?.version, 15); + + // Empty source + assert.strictEqual(Object.keys(parseApiProposalsFromSource('')).length, 0); + + console.log(' ✓ parseApiProposalsFromSource tests passed\n'); + + // checkExtensionCompatibility tests + console.log('Testing checkExtensionCompatibility...'); + + const testApiProposals = { + authSession: { proposal: '', version: undefined }, + chatParticipant: { proposal: '', version: 2 }, + testProposal: { proposal: '', version: 15 } + }; + + // Compatible extension - matching engine and proposals + assert.strictEqual(checkExtensionCompatibility('1.90.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['chatParticipant@2'] + }).compatible, true); + + // Compatible - no API proposals + assert.strictEqual(checkExtensionCompatibility('1.90.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' } + }).compatible, true); + + // Compatible - unversioned API proposals + assert.strictEqual(checkExtensionCompatibility('1.90.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['authSession', 'chatParticipant'] + }).compatible, true); + + // Incompatible - engine version too new + assert.strictEqual(checkExtensionCompatibility('1.89.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['chatParticipant@2'] + }).compatible, false); + + // Incompatible - API proposal version mismatch + assert.strictEqual(checkExtensionCompatibility('1.90.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['chatParticipant@3'] + }).compatible, false); + + // Incompatible - missing API proposal + assert.strictEqual(checkExtensionCompatibility('1.90.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['unknownProposal@1'] + }).compatible, false); + + // Incompatible - both engine and API proposal issues + assert.strictEqual(checkExtensionCompatibility('1.89.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['chatParticipant@3'] + }).compatible, false); + + console.log(' ✓ checkExtensionCompatibility tests passed\n'); + + console.log('All tests passed! ✓'); +} diff --git a/build/gulpfile.cli.ts b/build/gulpfile.cli.ts index e746a00e2bb..974cf892e4f 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 cb1a0a5fd69..27149338d9f 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 b4007983cac..00000000000 --- 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 f1354b858c9..e4d01e143c9 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/extensions/git/src/git.ts b/extensions/git/src/git.ts index 5b2ca69d087..fb431257d19 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -2958,13 +2958,10 @@ export class Repository { } private async getWorktreesFS(): Promise { - try { - // List all worktree folder names - const mainRepositoryPath = this.dotGit.commonPath ?? this.dotGit.path; - const worktreesPath = path.join(mainRepositoryPath, 'worktrees'); - const dirents = await fs.readdir(worktreesPath, { withFileTypes: true }); - const result: Worktree[] = []; + const result: Worktree[] = []; + const mainRepositoryPath = this.dotGit.commonPath ?? this.dotGit.path; + try { if (!this.dotGit.isBare) { // Add main worktree for a non-bare repository const headPath = path.join(mainRepositoryPath, 'HEAD'); @@ -2981,6 +2978,10 @@ export class Repository { } satisfies Worktree); } + // List all worktree folder names + const worktreesPath = path.join(mainRepositoryPath, 'worktrees'); + const dirents = await fs.readdir(worktreesPath, { withFileTypes: true }); + for (const dirent of dirents) { if (!dirent.isDirectory()) { continue; @@ -3016,7 +3017,7 @@ export class Repository { } catch (err) { if (/ENOENT/.test(err.message) || /ENOTDIR/.test(err.message)) { - return []; + return result; } throw err; diff --git a/package-lock.json b/package-lock.json index 2b2332b3383..74e77c2e608 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 44e6740de43..b76374ed584 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 74cb106fd3c..00859ca7344 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 de412e896a2..2a26c187d73 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 d94ff704768..917aeb8c02a 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 32cbebafb60..94004152e7d 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)); @@ -1966,6 +1966,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 b4830b6c683..9ab8b4c1e98 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 112faf74966..12fe6f307c7 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 64b4e08aa4d..f40f0e4477d 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 0e3aaac540b..761fca70dbb 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 + /** 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, + 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 { - const itemHandle = this._itemPool++; - if (!this._items.has(handle)) { - this._items.set(handle, new Map()); + private _clearProviderItems(handle: number): void { + const itemHandles = this._providerItems.get(handle); + if (itemHandles) { + for (const itemHandle of itemHandles) { + this._globalItems.delete(itemHandle); + } + itemHandles.clear(); } - this._items.get(handle)!.set(itemHandle, item); + } + + private _addTrackedItem(providerHandle: number, item: vscode.ChatContextItem): number { + const itemHandle = this._itemPool++; + this._globalItems.set(itemHandle, item); + if (!this._providerItems.has(providerHandle)) { + this._providerItems.set(providerHandle, new Set()); + } + 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 b8d92947c99..03dc0c22075 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 105b13734bc..ef844667ecc 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/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index 064dc540ab2..87222cab944 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -124,6 +124,11 @@ class BrowserNavigationBar extends Disposable { } } })); + + // Select all URL bar text when the URL bar receives focus (like in regular browsers) + this._register(addDisposableListener(this._urlInput, EventType.FOCUS, () => { + this._urlInput.select(); + })); } /** diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index 8054da42f32..d5ab8f1a9aa 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 eac7f983360..48aee8ff887 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 5a4815ff15d..cc939a7ff93 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 bc89fbfdf76..ad76b78f988 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/contextContrib/chatContextService.ts b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts index 5538c84c4b1..57850cea5ca 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 00000000000..b193910212f --- /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 f36daedcf89..5cbb6604e91 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -80,7 +80,7 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd import { InlineChatConfigKeys } from '../../../../inlineChat/common/inlineChat.js'; import { IChatViewTitleActionContext } from '../../../common/actions/chatActions.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.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 { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; import { IChatFollowup, IChatService } from '../../../common/chatService/chatService.js'; import { IChatSessionFileChange, IChatSessionProviderOptionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.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); } @@ -2289,6 +2289,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.clearNode(this.chatEditingSessionWidgetContainer); this._chatEditsDisposables.clear(); this._chatEditList = undefined; + + this._onDidChangeHeight.fire(); } }); } diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index 54452201d19..c45c4899abc 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 854d415e208..6973240728d 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/promptSyntax/chatPromptContentStore.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptContentStore.ts new file mode 100644 index 00000000000..30de1ac7e0c --- /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 a5352e3e7b0..6952e85834f 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 ccf1a4c42f4..f84cc15db38 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 eac1705a2d0..47b297427bb 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/promptSyntax/chatPromptContentProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/chatPromptContentProvider.test.ts new file mode 100644 index 00000000000..6323d234eba --- /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/promptSyntax/chatPromptContentStore.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/chatPromptContentStore.test.ts new file mode 100644 index 00000000000..3d3236124b9 --- /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/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts index 6de2cb80cc3..9c146f6a13a 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/vscode-dts/vscode.proposed.chatContextProvider.d.ts b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts index e2309591824..94681edf174 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 { @@ -53,7 +58,11 @@ declare module 'vscode' { onDidChangeWorkspaceChatContext?: Event; /** - * Provide a list of chat context items to be included as workspace context for all chat sessions. + * TODO @API: should this be a separate provider interface? + * + * Provide a list of chat context items to be included as workspace context for all chat requests. + * This should be used very sparingly to avoid providing useless context and to avoid using up the context window. + * A good example use case is to provide information about which branch the user is working on in a source control context. * * @param token A cancellation token. */ diff --git a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts index 8a901755807..b0da5fe1321 100644 --- a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts +++ b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts @@ -7,25 +7,76 @@ 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 class InstructionsChatResource { + /** + * The instructions resource descriptor. + */ + readonly resource: ChatResourceDescriptor; + + /** + * Creates a new instructions resource from the specified resource. + * @param resource The chat resource descriptor. + */ + constructor(resource: ChatResourceDescriptor); + } + + /** + * Represents a prompt file resource (e.g., .prompt.md). + */ + export class PromptFileChatResource { + /** + * The prompt file resource descriptor. + */ + readonly resource: ChatResourceDescriptor; + + /** + * Creates a new prompt file resource from the specified resource. + * @param resource The chat resource descriptor. + */ + constructor(resource: ChatResourceDescriptor); + } + + // #endregion + + // #region Providers + + /** + * Options for querying custom agents. */ export type CustomAgentContext = object; @@ -47,28 +98,12 @@ declare module 'vscode' { * 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. + * @returns An array of custom agents or a promise that resolves to such. */ - provideCustomAgents(context: CustomAgentContext, token: CancellationToken): ProviderResult; - } - - // #endregion - - // #region InstructionsProvider - - /** - * Represents an instructions resource file available for a repository. - */ - export interface InstructionsResource { - /** - * The URI to the instructions resource file. - */ - readonly uri: Uri; - - /** - * Indicates whether the instructions are editable. Defaults to false. - */ - 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. + * @returns An array of instructions 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. - */ - 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