Files
vscodium/patches/feat-remove-copilot.patch
2026-04-22 01:54:37 +02:00

5263 lines
200 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts
index 2c44fc94..d85dadfe 100644
--- a/build/gulpfile.reh.ts
+++ b/build/gulpfile.reh.ts
@@ -30,3 +30,3 @@ import rceditCallback from 'rcedit';
import { compileBuildWithManglingTask } from './gulpfile.compile.ts';
-import { cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileExtensionMediaBuildTask, compileCopilotExtensionBuildTask } from './gulpfile.extensions.ts';
+import { cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileExtensionMediaBuildTask } from './gulpfile.extensions.ts';
import { vscodeWebResourceIncludes, createVSCodeWebFileContentMapper } from './gulpfile.vscode.web.ts';
@@ -36,3 +36,2 @@ import buildfile from './buildfile.ts';
import { fetchUrls, fetchGithub } from './lib/fetch.ts';
-import { getCopilotExcludeFilter, copyCopilotNativeDeps, prepareBuiltInCopilotExtensionShims } from './lib/copilot.ts';
import jsonEditor from 'gulp-json-editor';
@@ -346,3 +345,2 @@ function packageTask(type: string, platform: string, arch: string, sourceFolderN
.pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`)))
- .pipe(filter(getCopilotExcludeFilter(platform, arch)))
.pipe(jsFilter)
@@ -465,13 +463,2 @@ function patchWin32DependenciesTask(destinationFolderName: string) {
-function copyCopilotNativeDepsTaskREH(platform: string, arch: string, destinationFolderName: string) {
- return async () => {
- const outputDir = path.join(BUILD_ROOT, destinationFolderName);
- const nodeModulesDir = path.join(outputDir, 'node_modules');
- copyCopilotNativeDeps(platform, arch, nodeModulesDir);
-
- const builtInCopilotExtensionDir = path.join(outputDir, 'extensions', 'copilot');
- prepareBuiltInCopilotExtensionShims(platform, arch, builtInCopilotExtensionDir, nodeModulesDir);
- };
-}
-
/**
@@ -525,3 +512,2 @@ function tweakProductForServerWeb(product: typeof import('../product.json')) {
packageTask(type, platform, arch, sourceFolderName, destinationFolderName),
- copyCopilotNativeDepsTaskREH(platform, arch, destinationFolderName)
];
@@ -539,3 +525,2 @@ function tweakProductForServerWeb(product: typeof import('../product.json')) {
compileNonNativeExtensionsBuildTask,
- compileCopilotExtensionBuildTask,
compileExtensionMediaBuildTask,
diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts
index 93b5f711..d8084ab1 100644
--- a/build/gulpfile.vscode.ts
+++ b/build/gulpfile.vscode.ts
@@ -31,5 +31,4 @@ import minimist from 'minimist';
import { compileBuildWithoutManglingTask } from './gulpfile.compile.ts';
-import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask, compileCopilotExtensionBuildTask } from './gulpfile.extensions.ts';
+import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask } from './gulpfile.extensions.ts';
import { copyCodiconsTask } from './lib/compilation.ts';
-import { getCopilotExcludeFilter, copyCopilotNativeDeps, prepareBuiltInCopilotExtensionShims } from './lib/copilot.ts';
import type { EmbeddedProductInfo } from './lib/embeddedType.ts';
@@ -446,3 +445,2 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d
.pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`)))
- .pipe(filter(getCopilotExcludeFilter(platform, arch)))
.pipe(jsFilter)
@@ -453,3 +451,2 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d
'**/@vscode/ripgrep/bin/*',
- '**/@github/copilot-*/**',
'**/node-pty/build/Release/*',
@@ -691,20 +688,2 @@ function patchWin32DependenciesTask(destinationFolderName: string) {
-function copyCopilotNativeDepsTask(platform: string, arch: string, destinationFolderName: string) {
- const outputDir = path.join(path.dirname(root), destinationFolderName);
-
- return async () => {
- // On Windows with win32VersionedUpdate, app resources live under a
- // commit-hash prefix: {output}/{commitHash}/resources/app/
- const versionedResourcesFolder = util.getVersionedResourcesFolder(platform, commit!);
- const appBase = platform === 'darwin'
- ? path.join(outputDir, `${product.nameLong}.app`, 'Contents', 'Resources', 'app')
- : path.join(outputDir, versionedResourcesFolder, 'resources', 'app');
- const appNodeModulesDir = path.join(appBase, 'node_modules');
- copyCopilotNativeDeps(platform, arch, appNodeModulesDir);
-
- const builtInCopilotExtensionDir = path.join(appBase, 'extensions', 'copilot');
- prepareBuiltInCopilotExtensionShims(platform, arch, builtInCopilotExtensionDir, appNodeModulesDir);
- };
-}
-
const buildRoot = path.dirname(root);
@@ -734,3 +713,2 @@ BUILD_TARGETS.forEach(buildTarget => {
packageTask(platform, arch, sourceFolderName, destinationFolderName, opts),
- copyCopilotNativeDepsTask(platform, arch, destinationFolderName)
];
@@ -761,3 +739,2 @@ BUILD_TARGETS.forEach(buildTarget => {
compileNonNativeExtensionsBuildTask,
- compileCopilotExtensionBuildTask,
compileExtensionMediaBuildTask,
@@ -782,3 +759,2 @@ BUILD_TARGETS.forEach(buildTarget => {
compileNonNativeExtensionsBuildTask,
- compileCopilotExtensionBuildTask,
compileExtensionMediaBuildTask,
diff --git a/build/npm/dirs.ts b/build/npm/dirs.ts
index 74b5422b..db8df90a 100644
--- a/build/npm/dirs.ts
+++ b/build/npm/dirs.ts
@@ -17,3 +17,2 @@ export const dirs = [
'extensions/configuration-editing',
- 'extensions/copilot',
'extensions/css-language-features',
diff --git a/build/npm/postinstall.ts b/build/npm/postinstall.ts
index db659fa7..ee169a6f 100644
--- a/build/npm/postinstall.ts
+++ b/build/npm/postinstall.ts
@@ -316,33 +316,2 @@ async function main() {
fs.writeFileSync(stateContentsFile, JSON.stringify(computeContents()));
-
- // Symlink .claude/ files to their canonical locations to test Claude agent harness
- const claudeDir = path.join(root, '.claude');
- fs.mkdirSync(claudeDir, { recursive: true });
-
- const claudeMdLink = path.join(claudeDir, 'CLAUDE.md');
- const claudeMdLinkType = ensureAgentHarnessLink(path.join('..', '.github', 'copilot-instructions.md'), claudeMdLink);
- if (claudeMdLinkType !== 'existing') {
- log('.', `Created ${claudeMdLinkType} .claude/CLAUDE.md -> .github/copilot-instructions.md`);
- }
-
- const claudeSkillsLink = path.join(claudeDir, 'skills');
- const claudeSkillsLinkType = ensureAgentHarnessLink(path.join('..', '.agents', 'skills'), claudeSkillsLink);
- if (claudeSkillsLinkType !== 'existing') {
- log('.', `Created ${claudeSkillsLinkType} .claude/skills -> .agents/skills`);
- }
-
- // Temporary: patch @github/copilot-sdk session.js to fix ESM import
- // (missing .js extension on vscode-jsonrpc/node). Fixed upstream in v0.1.32.
- // TODO: Remove once @github/copilot-sdk is updated to >=0.1.32
- for (const dir of ['', 'remote']) {
- const sessionFile = path.join(root, dir, 'node_modules', '@github', 'copilot-sdk', 'dist', 'session.js');
- if (fs.existsSync(sessionFile)) {
- const content = fs.readFileSync(sessionFile, 'utf8');
- const patched = content.replace(/from "vscode-jsonrpc\/node"/g, 'from "vscode-jsonrpc/node.js"');
- if (content !== patched) {
- fs.writeFileSync(sessionFile, patched);
- log(dir || '.', 'Patched @github/copilot-sdk session.js (vscode-jsonrpc ESM import fix)');
- }
- }
- }
}
diff --git a/package-lock.json b/package-lock.json
index 75b98719..86572201 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,5 +12,2 @@
"dependencies": {
- "@anthropic-ai/sandbox-runtime": "0.0.42",
- "@github/copilot": "^1.0.24",
- "@github/copilot-sdk": "^0.2.2",
"@microsoft/1ds-core-js": "^3.2.13",
@@ -188,31 +185,2 @@
},
- "node_modules/@anthropic-ai/sandbox-runtime": {
- "version": "0.0.42",
- "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.42.tgz",
- "integrity": "sha512-kJpuhU4hHMumeygIkKvNhscEsTtQK1sat1kZwhb6HLYBznwjMGOdnuBI/RM9HeFwxArn71/ciD2WJbxttXBMHw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@pondwader/socks5-server": "^1.0.10",
- "@types/lodash-es": "^4.17.12",
- "commander": "^12.1.0",
- "lodash-es": "^4.17.23",
- "shell-quote": "^1.8.3",
- "zod": "^3.24.1"
- },
- "bin": {
- "srt": "dist/cli.js"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@anthropic-ai/sandbox-runtime/node_modules/commander": {
- "version": "12.1.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
- "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/@appium/logger": {
@@ -1073,138 +1041,2 @@
},
- "node_modules/@github/copilot": {
- "version": "1.0.24",
- "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.24.tgz",
- "integrity": "sha512-/nZ2GwhaGq0HeI3W+6LE0JGw25/bipC6tYVa+oQ5tIvAafBazuNt10CXkeaor+u9oBWLZtPbdTyAzE2tjy9NpQ==",
- "license": "SEE LICENSE IN LICENSE.md",
- "bin": {
- "copilot": "npm-loader.js"
- },
- "optionalDependencies": {
- "@github/copilot-darwin-arm64": "1.0.24",
- "@github/copilot-darwin-x64": "1.0.24",
- "@github/copilot-linux-arm64": "1.0.24",
- "@github/copilot-linux-x64": "1.0.24",
- "@github/copilot-win32-arm64": "1.0.24",
- "@github/copilot-win32-x64": "1.0.24"
- }
- },
- "node_modules/@github/copilot-darwin-arm64": {
- "version": "1.0.24",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.24.tgz",
- "integrity": "sha512-lejn6KV+09rZEICX3nRx9a58DQFQ2kK3NJ3EICfVLngUCWIUmwn1BLezjeTQc9YNasHltA1hulvfsWqX+VjlMw==",
- "cpu": [
- "arm64"
- ],
- "license": "SEE LICENSE IN LICENSE.md",
- "optional": true,
- "os": [
- "darwin"
- ],
- "bin": {
- "copilot-darwin-arm64": "copilot"
- }
- },
- "node_modules/@github/copilot-darwin-x64": {
- "version": "1.0.24",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.24.tgz",
- "integrity": "sha512-r2F3keTvr4Bunz3V+waRAvsHgqsVQGyIZFBebsNPWxBX1eh3IXgtBqxCR7vXTFyZonQ8VaiJH3YYEfAhyKsk9g==",
- "cpu": [
- "x64"
- ],
- "license": "SEE LICENSE IN LICENSE.md",
- "optional": true,
- "os": [
- "darwin"
- ],
- "bin": {
- "copilot-darwin-x64": "copilot"
- }
- },
- "node_modules/@github/copilot-linux-arm64": {
- "version": "1.0.24",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.24.tgz",
- "integrity": "sha512-B3oANXKKKLhnKYozXa/W+DxfCQAHJCs0QKR5rBwNrwJbf656twNgALSxWTSJk+1rEP6MrHCswUAcwjwZL7Q+FQ==",
- "cpu": [
- "arm64"
- ],
- "license": "SEE LICENSE IN LICENSE.md",
- "optional": true,
- "os": [
- "linux"
- ],
- "bin": {
- "copilot-linux-arm64": "copilot"
- }
- },
- "node_modules/@github/copilot-linux-x64": {
- "version": "1.0.24",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.24.tgz",
- "integrity": "sha512-NGTldizY54B+4Sfhu/GWoEQNMwqqUNgMwbSgBshFv+Hqy1EwuvNWKVov1Y0Vzhp4qAHc6ZxBk/OPIW8Ato9FUg==",
- "cpu": [
- "x64"
- ],
- "license": "SEE LICENSE IN LICENSE.md",
- "optional": true,
- "os": [
- "linux"
- ],
- "bin": {
- "copilot-linux-x64": "copilot"
- }
- },
- "node_modules/@github/copilot-sdk": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.2.2.tgz",
- "integrity": "sha512-VZCqS08YlUM90bUKJ7VLeIxgTTEHtfXBo84T1IUMNvXRREX2csjPH6Z+CPw3S2468RcCLvzBXcc9LtJJTLIWFw==",
- "license": "MIT",
- "dependencies": {
- "@github/copilot": "^1.0.21",
- "vscode-jsonrpc": "^8.2.1",
- "zod": "^4.3.6"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@github/copilot-sdk/node_modules/zod": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
- "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/colinhacks"
- }
- },
- "node_modules/@github/copilot-win32-arm64": {
- "version": "1.0.24",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.24.tgz",
- "integrity": "sha512-/pd/kgef7/HIIg1SQq4q8fext39pDSC44jHB10KkhfgG1WaDFhQbc/aSSMQfxeldkRbQh6VANp8WtGQdwtMCBA==",
- "cpu": [
- "arm64"
- ],
- "license": "SEE LICENSE IN LICENSE.md",
- "optional": true,
- "os": [
- "win32"
- ],
- "bin": {
- "copilot-win32-arm64": "copilot.exe"
- }
- },
- "node_modules/@github/copilot-win32-x64": {
- "version": "1.0.24",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.24.tgz",
- "integrity": "sha512-RDvOiSvyEJwELqErwANJTrdRuMIHkwPE4QK7Le7WsmaSKxiuS4H1Pa8Yxnd2FWrMsCHEbase23GJlymbnGYLXQ==",
- "cpu": [
- "x64"
- ],
- "license": "SEE LICENSE IN LICENSE.md",
- "optional": true,
- "os": [
- "win32"
- ],
- "bin": {
- "copilot-win32-x64": "copilot.exe"
- }
- },
"node_modules/@gulp-sourcemaps/identity-map": {
@@ -2450,8 +2282,2 @@
},
- "node_modules/@pondwader/socks5-server": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz",
- "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==",
- "license": "MIT"
- },
"node_modules/@promptbook/utils": {
@@ -2778,17 +2604,2 @@
},
- "node_modules/@types/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
- "license": "MIT"
- },
- "node_modules/@types/lodash-es": {
- "version": "4.17.12",
- "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
- "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
- "license": "MIT",
- "dependencies": {
- "@types/lodash": "*"
- }
- },
"node_modules/@types/minimatch": {
@@ -13284,8 +13095,2 @@
},
- "node_modules/lodash-es": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
- "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
- "license": "MIT"
- },
"node_modules/lodash.camelcase": {
@@ -17223,2 +17028,3 @@
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "dev": true,
"license": "MIT",
@@ -19610,11 +19416,2 @@
},
- "node_modules/vscode-jsonrpc": {
- "version": "8.2.1",
- "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz",
- "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==",
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/vscode-oniguruma": {
@@ -20328,2 +20125,3 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "dev": true,
"license": "MIT",
diff --git a/package.json b/package.json
index b925a690..86fdd1f6 100644
--- a/package.json
+++ b/package.json
@@ -23,3 +23,3 @@
"compile-check-ts-native": "tsgo --project ./src/tsconfig.json --noEmit --skipLibCheck",
- "watch": "npm-run-all2 -lp watch-client-transpile watch-client watch-extensions watch-copilot",
+ "watch": "npm-run-all2 -lp watch-client-transpile watch-client watch-extensions",
"watchd": "deemon npm run watch",
@@ -40,5 +40,2 @@
"kill-watch-extensionsd": "deemon --kill npm run watch-extensions",
- "watch-copilot": "npm --prefix extensions/copilot run watch",
- "watch-copilotd": "deemon npm run watch-copilot",
- "kill-watch-copilotd": "deemon --kill npm run watch-copilot",
"precommit": "node --experimental-strip-types build/hygiene.ts",
@@ -81,4 +78,2 @@
"perf": "node scripts/code-perf.js",
- "copilot:setup": "npm --prefix extensions/copilot run setup",
- "copilot:get_token": "npm --prefix extensions/copilot run get_token",
"update-build-ts-version": "npm install -D typescript@next && npm install -D @typescript/native-preview && (cd build && npm run typecheck)",
@@ -89,5 +84,2 @@
"dependencies": {
- "@anthropic-ai/sandbox-runtime": "0.0.42",
- "@github/copilot": "^1.0.24",
- "@github/copilot-sdk": "^0.2.2",
"@microsoft/1ds-core-js": "^3.2.13",
diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts
index d6b6d42f..c891d99c 100644
--- a/src/vs/platform/agentHost/node/agentHostMain.ts
+++ b/src/vs/platform/agentHost/node/agentHostMain.ts
@@ -16,3 +16,2 @@ import { AgentService } from './agentService.js';
import { IAgentHostTerminalManager } from './agentHostTerminalManager.js';
-import { CopilotAgent } from './copilot/copilotAgent.js';
import { ProtocolServerHandler } from './protocolServerHandler.js';
@@ -33,3 +32,2 @@ import { DiskFileSystemProvider } from '../../files/node/diskFileSystemProvider.
import { Schemas } from '../../../base/common/network.js';
-import { InstantiationService } from '../../instantiation/common/instantiationService.js';
import { ServiceCollection } from '../../instantiation/common/serviceCollection.js';
@@ -93,4 +91,2 @@ function startAgentHost(): void {
diServices.set(IAgentHostTerminalManager, agentService.terminalManager);
- const instantiationService = new InstantiationService(diServices);
- agentService.registerProvider(instantiationService.createInstance(CopilotAgent));
} catch (err) {
diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts
index 2234c1de..767b4267 100644
--- a/src/vs/platform/agentHost/node/agentHostServerMain.ts
+++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts
@@ -29,5 +29,3 @@ import product from '../../product/common/product.js';
import { IProductService } from '../../product/common/productService.js';
-import { InstantiationService } from '../../instantiation/common/instantiationService.js';
import { ServiceCollection } from '../../instantiation/common/serviceCollection.js';
-import { CopilotAgent } from './copilot/copilotAgent.js';
import { AgentService } from './agentService.js';
@@ -175,6 +173,2 @@ async function main(): Promise<void> {
diServices.set(IAgentHostTerminalManager, agentService.terminalManager);
- const instantiationService = new InstantiationService(diServices);
- const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent));
- agentService.registerProvider(copilotAgent);
- log('CopilotAgent registered');
}
diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts
index 13561b5e..d5b5517e 100644
--- a/src/vs/platform/agentHost/node/agentService.ts
+++ b/src/vs/platform/agentHost/node/agentService.ts
@@ -33,3 +33,3 @@ import { AgentHostStateManager } from './agentHostStateManager.js';
*/
-function extractSubagentMeta(start: IAgentToolStartEvent | undefined): { subagentDescription?: string; subagentAgentName?: string } {
+function extractSubagentMeta(start: IAgentToolStartEvent | undefined): { subagentDescription?: string; subagentAgentName?: string; } {
if (!start?.toolKind || start.toolKind !== 'subagent' || !start.toolArguments) {
@@ -558,3 +558,3 @@ export class AgentService extends Disposable implements IAgentService {
id: string;
- userMessage: { text: string };
+ userMessage: { text: string; };
responseParts: IResponsePart[];
diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts
deleted file mode 100644
index 565e4160..00000000
--- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts
+++ /dev/null
@@ -1,696 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import { CopilotClient } from '@github/copilot-sdk';
-import { rgPath } from '@vscode/ripgrep';
-import { Limiter, SequencerByKey } from '../../../../base/common/async.js';
-import { Emitter } from '../../../../base/common/event.js';
-import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js';
-import { FileAccess } from '../../../../base/common/network.js';
-import { delimiter, dirname } from '../../../../base/common/path.js';
-import { URI } from '../../../../base/common/uri.js';
-import { generateUuid } from '../../../../base/common/uuid.js';
-import { IParsedPlugin, parsePlugin } from '../../../agentPlugins/common/pluginParsers.js';
-import { IFileService } from '../../../files/common/files.js';
-import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
-import { ILogService } from '../../../log/common/log.js';
-import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPluginManager.js';
-import { AgentSession, IAgent, IAgentAttachment, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDescriptor, IAgentMessageEvent, IAgentModelInfo, IAgentProgressEvent, IAgentSessionMetadata, IAgentSessionProjectInfo, IAgentSubagentStartedEvent, IAgentToolCompleteEvent, IAgentToolStartEvent } from '../../common/agentService.js';
-import { ISessionDataService } from '../../common/sessionDataService.js';
-import { CustomizationStatus, ICustomizationRef, SessionInputResponseKind, type ISessionInputAnswer, type IPendingMessage, type PolicyState } from '../../common/state/sessionState.js';
-import { CopilotAgentSession, SessionWrapperFactory } from './copilotAgentSession.js';
-import { parsedPluginsEqual, toSdkCustomAgents, toSdkHooks, toSdkMcpServers, toSdkSkillDirectories } from './copilotPluginConverters.js';
-import { CopilotSessionWrapper } from './copilotSessionWrapper.js';
-import { forkCopilotSessionOnDisk, getCopilotDataDir, truncateCopilotSessionOnDisk } from './copilotAgentForking.js';
-import { IProtectedResourceMetadata } from '../../common/state/protocol/state.js';
-import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js';
-import { ICopilotSessionContext, projectFromCopilotContext } from './copilotGitProject.js';
-import { createShellTools, ShellManager } from './copilotShellTools.js';
-
-/**
- * Agent provider backed by the Copilot SDK {@link CopilotClient}.
- */
-export class CopilotAgent extends Disposable implements IAgent {
- readonly id = 'copilot' as const;
-
- private readonly _onDidSessionProgress = this._register(new Emitter<IAgentProgressEvent>());
- readonly onDidSessionProgress = this._onDidSessionProgress.event;
-
- private _client: CopilotClient | undefined;
- private _clientStarting: Promise<CopilotClient> | undefined;
- private _githubToken: string | undefined;
- private readonly _sessions = this._register(new DisposableMap<string, CopilotAgentSession>());
- private readonly _sessionSequencer = new SequencerByKey<string>();
- private readonly _plugins: PluginController;
-
- constructor(
- @ILogService private readonly _logService: ILogService,
- @IInstantiationService private readonly _instantiationService: IInstantiationService,
- @IFileService private readonly _fileService: IFileService,
- @ISessionDataService private readonly _sessionDataService: ISessionDataService,
- @IAgentHostTerminalManager private readonly _terminalManager: IAgentHostTerminalManager,
- ) {
- super();
- this._plugins = this._instantiationService.createInstance(PluginController);
- }
-
- // ---- auth ---------------------------------------------------------------
-
- getDescriptor(): IAgentDescriptor {
- return {
- provider: 'copilot',
- displayName: 'Copilot',
- description: 'Copilot SDK agent running in a dedicated process',
- };
- }
-
- getProtectedResources(): IProtectedResourceMetadata[] {
- return [{
- resource: 'https://api.github.com',
- resource_name: 'GitHub Copilot',
- authorization_servers: ['https://github.com/login/oauth'],
- scopes_supported: ['read:user', 'user:email'],
- required: true,
- }];
- }
-
- async authenticate(resource: string, token: string): Promise<boolean> {
- if (resource !== 'https://api.github.com') {
- return false;
- }
- const tokenChanged = this._githubToken !== token;
- this._githubToken = token;
- this._logService.info(`[Copilot] Auth token ${tokenChanged ? 'updated' : 'unchanged'}`);
- if (tokenChanged && this._client && this._sessions.size === 0) {
- this._logService.info('[Copilot] Restarting CopilotClient with new token');
- const client = this._client;
- this._client = undefined;
- this._clientStarting = undefined;
- await client.stop();
- }
- return true;
- }
-
- // ---- client lifecycle ---------------------------------------------------
-
- private async _ensureClient(): Promise<CopilotClient> {
- if (this._client) {
- return this._client;
- }
- if (this._clientStarting) {
- return this._clientStarting;
- }
- this._clientStarting = (async () => {
- this._logService.info(`[Copilot] Starting CopilotClient... ${this._githubToken ? '(with token)' : '(no token)'}`);
-
- // Build a clean env for the CLI subprocess, stripping Electron/VS Code vars
- // that can interfere with the Node.js process the SDK spawns.
- const env: Record<string, string | undefined> = Object.assign({}, process.env, { ELECTRON_RUN_AS_NODE: '1' });
- delete env['NODE_OPTIONS'];
- delete env['VSCODE_INSPECTOR_OPTIONS'];
- delete env['VSCODE_ESM_ENTRYPOINT'];
- delete env['VSCODE_HANDLES_UNCAUGHT_ERRORS'];
- for (const key of Object.keys(env)) {
- if (key === 'ELECTRON_RUN_AS_NODE') {
- continue;
- }
- if (key.startsWith('VSCODE_') || key.startsWith('ELECTRON_')) {
- delete env[key];
- }
- }
- env['COPILOT_CLI_RUN_AS_NODE'] = '1';
- env['USE_BUILTIN_RIPGREP'] = 'false';
-
- // Resolve the CLI entry point from node_modules. We can't use require.resolve()
- // because @github/copilot's exports map blocks direct subpath access.
- // FileAccess.asFileUri('') points to the `out/` directory; node_modules is one level up.
- const cliPath = URI.joinPath(FileAccess.asFileUri(''), '..', 'node_modules', '@github', 'copilot', 'index.js').fsPath;
-
- // Add VS Code's built-in ripgrep to PATH so the CLI subprocess can find it.
- // If @vscode/ripgrep is in an .asar file, the binary is unpacked.
- const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked');
- const rgDir = dirname(rgDiskPath);
- // On Windows the env key is typically "Path" (not "PATH"). Since we copied
- // process.env into a plain (case-sensitive) object, we must find the actual key.
- const pathKey = Object.keys(env).find(k => k.toUpperCase() === 'PATH') ?? 'PATH';
- const currentPath = env[pathKey];
- env[pathKey] = currentPath ? `${currentPath}${delimiter}${rgDir}` : rgDir;
- this._logService.info(`[Copilot] Resolved CLI path: ${cliPath}`);
-
- const client = new CopilotClient({
- githubToken: this._githubToken,
- useLoggedInUser: !this._githubToken,
- useStdio: true,
- autoStart: true,
- env,
- cliPath,
- });
- await client.start();
- this._logService.info('[Copilot] CopilotClient started successfully');
- this._client = client;
- this._clientStarting = undefined;
- return client;
- })();
- return this._clientStarting;
- }
-
- // ---- session management -------------------------------------------------
-
- async listSessions(): Promise<IAgentSessionMetadata[]> {
- this._logService.info('[Copilot] Listing sessions...');
- const client = await this._ensureClient();
- const sessions = await client.listSessions();
- const projectLimiter = new Limiter<IAgentSessionProjectInfo | undefined>(4);
- const projectByContext = new Map<string, Promise<IAgentSessionProjectInfo | undefined>>();
- const result: IAgentSessionMetadata[] = await Promise.all(sessions.map(async s => {
- const session = AgentSession.uri(this.id, s.sessionId);
- let { project, resolved } = await this._readSessionProject(session);
- if (!resolved) {
- project = await this._resolveSessionProject(s.context, projectLimiter, projectByContext);
- this._storeSessionProjectResolution(session, project);
- }
- return {
- session,
- startTime: s.startTime.getTime(),
- modifiedTime: s.modifiedTime.getTime(),
- ...(project ? { project } : {}),
- summary: s.summary,
- workingDirectory: typeof s.context?.cwd === 'string' ? URI.file(s.context.cwd) : undefined,
- };
- }));
- this._logService.info(`[Copilot] Found ${result.length} sessions`);
- return result;
- }
-
- async listModels(): Promise<IAgentModelInfo[]> {
- this._logService.info('[Copilot] Listing models...');
- const client = await this._ensureClient();
- const models = await client.listModels();
- const result = models.map(m => ({
- provider: this.id,
- id: m.id,
- name: m.name,
- maxContextWindow: m.capabilities.limits.max_context_window_tokens,
- supportsVision: m.capabilities.supports.vision,
- supportsReasoningEffort: m.capabilities.supports.reasoningEffort,
- supportedReasoningEfforts: m.supportedReasoningEfforts,
- defaultReasoningEffort: m.defaultReasoningEffort,
- policyState: m.policy?.state as PolicyState | undefined,
- billingMultiplier: m.billing?.multiplier,
- }));
- this._logService.info(`[Copilot] Found ${result.length} models`);
- return result;
- }
-
- async createSession(config?: IAgentCreateSessionConfig): Promise<IAgentCreateSessionResult> {
- this._logService.info(`[Copilot] Creating session... ${config?.model ? `model=${config.model}` : ''}`);
- const client = await this._ensureClient();
- const parsedPlugins = await this._plugins.getAppliedPlugins();
-
- // When forking, we manipulate the CLI's on-disk data and then resume
- // instead of creating a fresh session via the SDK.
- if (config?.fork) {
- const sourceSessionId = AgentSession.id(config.fork.session);
- const newSessionId = config.session ? AgentSession.id(config.session) : generateUuid();
-
- // Serialize against the source session to prevent concurrent
- // modifications while we read its on-disk data.
- return this._sessionSequencer.queue(sourceSessionId, async () => {
- this._logService.info(`[Copilot] Forking session ${sourceSessionId} at index ${config.fork!.turnIndex} → ${newSessionId}`);
-
- // Ensure the source session is loaded so on-disk data is available
- if (!this._sessions.has(sourceSessionId)) {
- await this._resumeSession(sourceSessionId);
- }
-
- const copilotDataDir = getCopilotDataDir();
- await forkCopilotSessionOnDisk(copilotDataDir, sourceSessionId, newSessionId, config.fork!.turnIndex);
-
- // Resume the forked session so the SDK loads the forked history
- const agentSession = await this._resumeSession(newSessionId);
- const session = agentSession.sessionUri;
- this._logService.info(`[Copilot] Forked session created: ${session.toString()}`);
- const project = await projectFromCopilotContext({ cwd: config.workingDirectory?.fsPath });
- this._storeSessionMetadata(session, undefined, config.workingDirectory, project, true);
- return { session, ...(project ? { project } : {}) };
- });
- }
-
- const sessionId = config?.session ? AgentSession.id(config.session) : generateUuid();
- const sessionUri = AgentSession.uri(this.id, sessionId);
- const shellManager = this._instantiationService.createInstance(ShellManager, sessionUri);
- const sessionConfig = this._buildSessionConfig(parsedPlugins, shellManager);
-
- const factory: SessionWrapperFactory = async callbacks => {
- const raw = await client.createSession({
- model: config?.model,
- sessionId,
- streaming: true,
- workingDirectory: config?.workingDirectory?.fsPath,
- ...await sessionConfig(callbacks),
- });
- return new CopilotSessionWrapper(raw);
- };
-
- const agentSession = this._createAgentSession(factory, config?.workingDirectory, sessionId, shellManager);
- this._plugins.setAppliedPlugins(agentSession, parsedPlugins);
- await agentSession.initializeSession();
-
- const session = agentSession.sessionUri;
- this._logService.info(`[Copilot] Session created: ${session.toString()}`);
- const project = await projectFromCopilotContext({ cwd: config?.workingDirectory?.fsPath });
- // Persist model, working directory, and project so we can recreate the
- // session if the SDK loses it and avoid rediscovering git metadata.
- this._storeSessionMetadata(agentSession.sessionUri, config?.model, config?.workingDirectory, project, true);
- return { session, ...(project ? { project } : {}) };
- }
-
- async setClientCustomizations(clientId: string, customizations: ICustomizationRef[], progress?: (results: ISyncedCustomization[]) => void): Promise<ISyncedCustomization[]> {
- return this._plugins.sync(clientId, customizations, progress);
- }
-
- setCustomizationEnabled(uri: string, enabled: boolean): void {
- this._plugins.setEnabled(uri, enabled);
- }
-
- async sendMessage(session: URI, prompt: string, attachments?: IAgentAttachment[], turnId?: string): Promise<void> {
- const sessionId = AgentSession.id(session);
- await this._sessionSequencer.queue(sessionId, async () => {
-
- // If plugin config changed, dispose this session so it gets resumed
- // with the updated plugin primitives.
- let entry = this._sessions.get(sessionId);
- if (entry && await this._plugins.needsSessionRefresh(entry)) {
- this._logService.info(`[Copilot:${sessionId}] Plugin config changed, refreshing session`);
- this._sessions.deleteAndDispose(sessionId);
- entry = undefined;
- }
-
- entry ??= await this._resumeSession(sessionId);
- await entry.send(prompt, attachments, turnId);
- });
- }
-
- setPendingMessages(session: URI, steeringMessage: IPendingMessage | undefined, _queuedMessages: readonly IPendingMessage[]): void {
- const sessionId = AgentSession.id(session);
- const entry = this._sessions.get(sessionId);
- if (!entry) {
- this._logService.warn(`[Copilot:${sessionId}] setPendingMessages: session not found`);
- return;
- }
-
- // Steering: send with mode 'immediate' so the SDK injects it mid-turn
- if (steeringMessage) {
- entry.sendSteering(steeringMessage);
- }
-
- // Queued messages are consumed by the server (AgentSideEffects)
- // which dispatches SessionTurnStarted and calls sendMessage directly.
- // No SDK-level enqueue is needed.
- }
-
- async getSessionMessages(session: URI): Promise<(IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[]> {
- const sessionId = AgentSession.id(session);
- const entry = this._sessions.get(sessionId) ?? await this._resumeSession(sessionId).catch(() => undefined);
- if (!entry) {
- return [];
- }
- return entry.getMessages();
- }
-
- async disposeSession(session: URI): Promise<void> {
- const sessionId = AgentSession.id(session);
- await this._sessionSequencer.queue(sessionId, async () => {
- this._sessions.deleteAndDispose(sessionId);
- });
- }
-
- async abortSession(session: URI): Promise<void> {
- const sessionId = AgentSession.id(session);
- await this._sessionSequencer.queue(sessionId, async () => {
- const entry = this._sessions.get(sessionId);
- if (entry) {
- await entry.abort();
- }
- });
- }
-
- async truncateSession(session: URI, turnIndex?: number): Promise<void> {
- const sessionId = AgentSession.id(session);
- await this._sessionSequencer.queue(sessionId, async () => {
- this._logService.info(`[Copilot:${sessionId}] Truncating session${turnIndex !== undefined ? ` at index ${turnIndex}` : ' (all turns)'}`);
-
- const keepUpToTurnIndex = turnIndex ?? -1;
-
- // Destroy the SDK session first and wait for cleanup to complete,
- // ensuring on-disk data (events.jsonl, locks) is released before
- // we modify it. Then dispose the wrapper.
- const entry = this._sessions.get(sessionId);
- if (entry) {
- await entry.destroySession();
- }
- this._sessions.deleteAndDispose(sessionId);
-
- const copilotDataDir = getCopilotDataDir();
- await truncateCopilotSessionOnDisk(copilotDataDir, sessionId, keepUpToTurnIndex);
-
- // Resume the session from the modified on-disk data
- await this._resumeSession(sessionId);
- this._logService.info(`[Copilot:${sessionId}] Session truncated and resumed`);
- });
- }
-
- async forkSession(sourceSession: URI, newSessionId: string, turnIndex: number): Promise<void> {
- const sourceSessionId = AgentSession.id(sourceSession);
- await this._sessionSequencer.queue(sourceSessionId, async () => {
- this._logService.info(`[Copilot] Forking session ${sourceSessionId} at index ${turnIndex} → ${newSessionId}`);
-
- const copilotDataDir = getCopilotDataDir();
- await forkCopilotSessionOnDisk(copilotDataDir, sourceSessionId, newSessionId, turnIndex);
- this._logService.info(`[Copilot] Forked session ${newSessionId} created on disk`);
- });
- }
-
- async changeModel(session: URI, model: string): Promise<void> {
- const sessionId = AgentSession.id(session);
- const entry = this._sessions.get(sessionId);
- if (entry) {
- await entry.setModel(model);
- }
- this._storeSessionMetadata(session, model, undefined, undefined);
- }
-
- async shutdown(): Promise<void> {
- this._logService.info('[Copilot] Shutting down...');
- this._sessions.clearAndDisposeAll();
- await this._client?.stop();
- this._client = undefined;
- }
-
- respondToPermissionRequest(requestId: string, approved: boolean): void {
- for (const [, session] of this._sessions) {
- if (session.respondToPermissionRequest(requestId, approved)) {
- return;
- }
- }
- }
-
- respondToUserInputRequest(requestId: string, response: SessionInputResponseKind, answers?: Record<string, ISessionInputAnswer>): void {
- for (const [, session] of this._sessions) {
- if (session.respondToUserInputRequest(requestId, response, answers)) {
- return;
- }
- }
- }
-
- /**
- * Returns true if this provider owns the given session ID.
- */
- hasSession(session: URI): boolean {
- return this._sessions.has(AgentSession.id(session));
- }
-
- // ---- helpers ------------------------------------------------------------
-
- /**
- * Creates a {@link CopilotAgentSession}, registers it in the sessions map,
- * and returns it. The caller must call {@link CopilotAgentSession.initializeSession}
- * to wire up the SDK session.
- */
- private _createAgentSession(wrapperFactory: SessionWrapperFactory, workingDirectory: URI | undefined, sessionId: string, shellManager: ShellManager): CopilotAgentSession {
- const sessionUri = AgentSession.uri(this.id, sessionId);
-
- const agentSession = this._instantiationService.createInstance(
- CopilotAgentSession,
- sessionUri,
- sessionId,
- workingDirectory,
- this._onDidSessionProgress,
- wrapperFactory,
- shellManager,
- );
-
- this._sessions.set(sessionId, agentSession);
- return agentSession;
- }
-
- /**
- * Builds the common session configuration (plugins + shell tools) shared
- * by both {@link createSession} and {@link _resumeSession}.
- *
- * Returns an async function that resolves the final config given the
- * session's permission/hook callbacks, so it can be called lazily
- * inside the {@link SessionWrapperFactory}.
- */
- private _buildSessionConfig(parsedPlugins: readonly IParsedPlugin[], shellManager: ShellManager) {
- const shellTools = createShellTools(shellManager, this._terminalManager, this._logService);
-
- return async (callbacks: Parameters<SessionWrapperFactory>[0]) => {
- const customAgents = await toSdkCustomAgents(parsedPlugins.flatMap(p => p.agents), this._fileService);
- return {
- onPermissionRequest: callbacks.onPermissionRequest,
- onUserInputRequest: callbacks.onUserInputRequest,
- hooks: toSdkHooks(parsedPlugins.flatMap(p => p.hooks), callbacks.hooks),
- mcpServers: toSdkMcpServers(parsedPlugins.flatMap(p => p.mcpServers)),
- customAgents,
- skillDirectories: toSdkSkillDirectories(parsedPlugins.flatMap(p => p.skills)),
- tools: shellTools,
- };
- };
- }
-
- private async _resumeSession(sessionId: string): Promise<CopilotAgentSession> {
- this._logService.info(`[Copilot:${sessionId}] Session not in memory, resuming...`);
- const client = await this._ensureClient();
- const parsedPlugins = await this._plugins.getAppliedPlugins();
-
- const sessionUri = AgentSession.uri(this.id, sessionId);
- const sessionMetadata = await client.getSessionMetadata(sessionId).catch(err => {
- this._logService.warn(`[Copilot:${sessionId}] getSessionMetadata failed`, err);
- return undefined;
- });
- const workingDirectory = typeof sessionMetadata?.context?.cwd === 'string' ? URI.file(sessionMetadata.context.cwd) : undefined;
- const shellManager = this._instantiationService.createInstance(ShellManager, sessionUri);
- const sessionConfig = this._buildSessionConfig(parsedPlugins, shellManager);
-
- const factory: SessionWrapperFactory = async callbacks => {
- const config = await sessionConfig(callbacks);
- try {
- const raw = await client.resumeSession(sessionId, {
- ...config,
- workingDirectory: workingDirectory?.fsPath,
- });
- return new CopilotSessionWrapper(raw);
- } catch (err) {
- // The SDK fails to resume sessions that have no messages.
- // Fall back to creating a new session with the same ID,
- // seeding model & working directory from stored metadata.
- if (!err || (err as { code?: number }).code !== -32603) {
- throw err;
- }
-
- this._logService.warn(`[Copilot:${sessionId}] Resume failed (session not found in SDK), recreating`);
- const metadata = await this._readSessionMetadata(sessionUri);
- const raw = await client.createSession({
- ...config,
- sessionId,
- streaming: true,
- model: metadata.model,
- workingDirectory: metadata.workingDirectory?.fsPath,
- });
-
- return new CopilotSessionWrapper(raw);
- }
- };
-
- const agentSession = this._createAgentSession(factory, workingDirectory, sessionId, shellManager);
- this._plugins.setAppliedPlugins(agentSession, parsedPlugins);
- await agentSession.initializeSession();
-
- return agentSession;
- }
-
- // ---- session metadata persistence --------------------------------------
-
- private static readonly _META_MODEL = 'copilot.model';
- private static readonly _META_CWD = 'copilot.workingDirectory';
- private static readonly _META_PROJECT_RESOLVED = 'copilot.project.resolved';
- private static readonly _META_PROJECT_URI = 'copilot.project.uri';
- private static readonly _META_PROJECT_DISPLAY_NAME = 'copilot.project.displayName';
-
- private _storeSessionMetadata(session: URI, model: string | undefined, workingDirectory: URI | undefined, project: IAgentSessionProjectInfo | undefined, projectResolved = project !== undefined): void {
- const dbRef = this._sessionDataService.tryOpenDatabase(session);
- dbRef?.then(ref => {
- if (!ref) {
- return;
- }
- const db = ref.object;
- const work: Promise<void>[] = [];
- if (model) {
- work.push(db.setMetadata(CopilotAgent._META_MODEL, model));
- }
- if (workingDirectory) {
- work.push(db.setMetadata(CopilotAgent._META_CWD, workingDirectory.toString()));
- }
- if (projectResolved) {
- work.push(db.setMetadata(CopilotAgent._META_PROJECT_RESOLVED, 'true'));
- }
- if (project) {
- work.push(db.setMetadata(CopilotAgent._META_PROJECT_URI, project.uri.toString()));
- work.push(db.setMetadata(CopilotAgent._META_PROJECT_DISPLAY_NAME, project.displayName));
- }
- Promise.all(work).finally(() => ref.dispose());
- });
- }
-
- private async _readSessionMetadata(session: URI): Promise<{ model?: string; workingDirectory?: URI }> {
- const ref = await this._sessionDataService.tryOpenDatabase(session);
- if (!ref) {
- return {};
- }
- try {
- const [model, cwd] = await Promise.all([
- ref.object.getMetadata(CopilotAgent._META_MODEL),
- ref.object.getMetadata(CopilotAgent._META_CWD),
- ]);
- return {
- model,
- workingDirectory: cwd ? URI.parse(cwd) : undefined,
- };
- } finally {
- ref.dispose();
- }
- }
-
- private async _readSessionProject(session: URI): Promise<{ project?: IAgentSessionProjectInfo; resolved: boolean }> {
- const ref = await this._sessionDataService.tryOpenDatabase(session);
- if (!ref) {
- return { resolved: false };
- }
- try {
- const [resolved, uri, displayName] = await Promise.all([
- ref.object.getMetadata(CopilotAgent._META_PROJECT_RESOLVED),
- ref.object.getMetadata(CopilotAgent._META_PROJECT_URI),
- ref.object.getMetadata(CopilotAgent._META_PROJECT_DISPLAY_NAME),
- ]);
- const project = uri && displayName ? { uri: URI.parse(uri), displayName } : undefined;
- return { project, resolved: resolved === 'true' || project !== undefined };
- } finally {
- ref.dispose();
- }
- }
-
- private _storeSessionProjectResolution(session: URI, project: IAgentSessionProjectInfo | undefined): void {
- this._storeSessionMetadata(session, undefined, undefined, project, true);
- }
-
- private _resolveSessionProject(context: ICopilotSessionContext | undefined, limiter: Limiter<IAgentSessionProjectInfo | undefined>, projectByContext: Map<string, Promise<IAgentSessionProjectInfo | undefined>>): Promise<IAgentSessionProjectInfo | undefined> {
- const key = this._projectContextKey(context);
- if (!key) {
- return Promise.resolve(undefined);
- }
-
- let project = projectByContext.get(key);
- if (!project) {
- project = limiter.queue(() => projectFromCopilotContext(context));
- projectByContext.set(key, project);
- }
- return project;
- }
-
- private _projectContextKey(context: ICopilotSessionContext | undefined): string | undefined {
- if (context?.cwd) {
- return `cwd:${context.cwd}`;
- }
- if (context?.gitRoot) {
- return `gitRoot:${context.gitRoot}`;
- }
- if (context?.repository) {
- return `repository:${context.repository}`;
- }
- return undefined;
- }
-
- override dispose(): void {
- this._client?.stop().catch(() => { /* best-effort */ });
- super.dispose();
- }
-}
-
-class PluginController {
- private readonly _enablement = new Map<string, boolean>();
- private _lastSynced: Promise<{ synced: ISyncedCustomization[]; parsed: IParsedPlugin[] }> = Promise.resolve({ synced: [], parsed: [] });
-
- /** Parsed plugin contents from the most recently applied sync. */
- private _appliedParsed = new WeakMap<CopilotAgentSession, readonly IParsedPlugin[]>();
-
- constructor(
- @IAgentPluginManager private readonly _pluginManager: IAgentPluginManager,
- @ILogService private readonly _logService: ILogService,
- @IFileService private readonly _fileService: IFileService,
- ) { }
-
- /**
- * Returns true if the plugin configuration has changed since the last
- * time sessions were created/resumed. Used by {@link CopilotAgent.sendMessage}
- * to decide whether a session needs to be refreshed.
- */
- public async needsSessionRefresh(session: CopilotAgentSession): Promise<boolean> {
- const { parsed } = await this._lastSynced;
- return !parsedPluginsEqual(this._appliedParsed.get(session) || [], parsed);
- }
-
- /**
- * Returns the current parsed plugins filtered by enablement,
- * then marks them as applied so {@link needsSessionRefresh} returns
- * false until the next change.
- */
- public async getAppliedPlugins(): Promise<readonly IParsedPlugin[]> {
- const { parsed } = await this._lastSynced;
- return parsed;
- }
-
- public setAppliedPlugins(session: CopilotAgentSession, plugins: readonly IParsedPlugin[]) {
- this._appliedParsed.set(session, plugins);
- }
-
- public setEnabled(pluginProtocolUri: string, enabled: boolean) {
- this._enablement.set(pluginProtocolUri, enabled);
- }
-
- public sync(clientId: string, customizations: ICustomizationRef[], progress?: (results: ISyncedCustomization[]) => void) {
- const prev = this._lastSynced;
- const promise = this._lastSynced = prev.catch(() => []).then(async () => {
- const result = await this._pluginManager.syncCustomizations(clientId, customizations, status => {
- progress?.(status.map(c => ({ customization: c })));
- });
-
-
- const parsed: IParsedPlugin[] = [];
- const synced: ISyncedCustomization[] = [];
- for (const dir of result) {
- if (dir.pluginDir) {
- try {
- parsed.push(await parsePlugin(dir.pluginDir, this._fileService, undefined, this._getUserHome()));
- synced.push(dir);
- } catch (e) {
- this._logService.warn(`[Copilot:PluginController] Error parsing plugin: ${e}`);
- synced.push({ customization: { ...dir.customization, status: CustomizationStatus.Error, statusMessage: `Error parsing plugin: ${e}` } });
- }
- } else {
- synced.push(dir);
- }
- }
-
- return { synced, parsed };
- });
-
- return promise.then(p => p.synced);
- }
-
- private _getUserHome(): string {
- return process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
- }
-}
diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentForking.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentForking.ts
deleted file mode 100644
index 0631ba51..00000000
--- a/src/vs/platform/agentHost/node/copilot/copilotAgentForking.ts
+++ /dev/null
@@ -1,599 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import * as fs from 'fs';
-import * as os from 'os';
-import type { Database } from '@vscode/sqlite3';
-import { generateUuid } from '../../../../base/common/uuid.js';
-import * as path from '../../../../base/common/path.js';
-
-// ---- Types ------------------------------------------------------------------
-
-/**
- * A single event entry from a Copilot CLI `events.jsonl` file.
- * The Copilot CLI stores session history as a newline-delimited JSON log
- * where events form a linked list via `parentId`.
- */
-export interface ICopilotEventLogEntry {
- readonly type: string;
- readonly data: Record<string, unknown>;
- readonly id: string;
- readonly timestamp: string;
- readonly parentId: string | null;
-}
-
-// ---- Promise wrappers around callback-based @vscode/sqlite3 API -----------
-
-function dbExec(db: Database, sql: string): Promise<void> {
- return new Promise((resolve, reject) => {
- db.exec(sql, err => err ? reject(err) : resolve());
- });
-}
-
-function dbRun(db: Database, sql: string, params: unknown[]): Promise<void> {
- return new Promise((resolve, reject) => {
- db.run(sql, params, function (err: Error | null) {
- if (err) {
- return reject(err);
- }
- resolve();
- });
- });
-}
-
-function dbAll(db: Database, sql: string, params: unknown[]): Promise<Record<string, unknown>[]> {
- return new Promise((resolve, reject) => {
- db.all(sql, params, (err: Error | null, rows: Record<string, unknown>[]) => {
- if (err) {
- return reject(err);
- }
- resolve(rows);
- });
- });
-}
-
-function dbClose(db: Database): Promise<void> {
- return new Promise((resolve, reject) => {
- db.close(err => err ? reject(err) : resolve());
- });
-}
-
-function dbOpen(dbPath: string): Promise<Database> {
- return new Promise((resolve, reject) => {
- import('@vscode/sqlite3').then(sqlite3 => {
- const db = new sqlite3.default.Database(dbPath, (err: Error | null) => {
- if (err) {
- return reject(err);
- }
- resolve(db);
- });
- }, reject);
- });
-}
-
-// ---- Pure functions (testable, no I/O) ------------------------------------
-
-/**
- * Parses a JSONL string into an array of event log entries.
- */
-export function parseEventLog(content: string): ICopilotEventLogEntry[] {
- const entries: ICopilotEventLogEntry[] = [];
- for (const line of content.split('\n')) {
- const trimmed = line.trim();
- if (trimmed.length === 0) {
- continue;
- }
- entries.push(JSON.parse(trimmed));
- }
- return entries;
-}
-
-/**
- * Serializes an array of event log entries back into a JSONL string.
- */
-export function serializeEventLog(entries: readonly ICopilotEventLogEntry[]): string {
- return entries.map(e => JSON.stringify(e)).join('\n') + '\n';
-}
-
-/**
- * Finds the index of the last event that belongs to the given turn (0-based).
- *
- * A "turn" corresponds to one `user.message` event and all subsequent events
- * up to (and including) the `assistant.turn_end` that closes that interaction,
- * or the `session.shutdown` that ends the session.
- *
- * @returns The inclusive index of the last event in the specified turn,
- * or `-1` if the turn is not found.
- */
-export function findTurnBoundaryInEventLog(entries: readonly ICopilotEventLogEntry[], turnIndex: number): number {
- let userMessageCount = -1;
- let lastEventForTurn = -1;
-
- for (let i = 0; i < entries.length; i++) {
- const entry = entries[i];
-
- if (entry.type === 'user.message') {
- userMessageCount++;
- if (userMessageCount > turnIndex) {
- // We've entered the next turn — stop
- return lastEventForTurn;
- }
- }
-
- if (userMessageCount === turnIndex) {
- lastEventForTurn = i;
- }
- }
-
- // If we scanned everything and the target turn was found, return its last event
- return lastEventForTurn;
-}
-
-/**
- * Builds a forked event log from the source session's events.
- *
- * - Keeps events up to and including the specified fork turn (0-based).
- * - Rewrites `session.start` with the new session ID.
- * - Generates fresh UUIDs for all events.
- * - Re-chains `parentId` links via an old→new ID map.
- * - Strips `session.shutdown` and `session.resume` lifecycle events.
- */
-export function buildForkedEventLog(
- entries: readonly ICopilotEventLogEntry[],
- forkTurnIndex: number,
- newSessionId: string,
-): ICopilotEventLogEntry[] {
- const boundary = findTurnBoundaryInEventLog(entries, forkTurnIndex);
- if (boundary < 0) {
- throw new Error(`Fork turn index ${forkTurnIndex} not found in event log`);
- }
-
- // Keep events up to boundary, filtering out lifecycle events
- const kept = entries
- .slice(0, boundary + 1)
- .filter(e => e.type !== 'session.shutdown' && e.type !== 'session.resume');
-
- // Build UUID remap and re-chain
- const idMap = new Map<string, string>();
- const result: ICopilotEventLogEntry[] = [];
-
- for (const entry of kept) {
- const newId = generateUuid();
- idMap.set(entry.id, newId);
-
- let data = entry.data;
- if (entry.type === 'session.start') {
- data = { ...data, sessionId: newSessionId };
- }
-
- const newParentId = entry.parentId !== null
- ? (idMap.get(entry.parentId) ?? result[result.length - 1]?.id ?? null)
- : null;
-
- result.push({
- type: entry.type,
- data,
- id: newId,
- timestamp: entry.timestamp,
- parentId: newParentId,
- });
- }
-
- return result;
-}
-
-/**
- * Builds a truncated event log from the source session's events.
- *
- * - Keeps events up to and including the specified turn (0-based).
- * - Prepends a new `session.start` event using the original start data.
- * - Re-chains `parentId` links for remaining events.
- */
-export function buildTruncatedEventLog(
- entries: readonly ICopilotEventLogEntry[],
- keepUpToTurnIndex: number,
-): ICopilotEventLogEntry[] {
- const boundary = findTurnBoundaryInEventLog(entries, keepUpToTurnIndex);
- if (boundary < 0) {
- throw new Error(`Turn index ${keepUpToTurnIndex} not found in event log`);
- }
-
- // Find the original session.start for its metadata
- const originalStart = entries.find(e => e.type === 'session.start');
- if (!originalStart) {
- throw new Error('No session.start event found in event log');
- }
-
- // Keep events from after session start up to boundary, stripping lifecycle events
- const kept = entries
- .slice(0, boundary + 1)
- .filter(e => e.type !== 'session.start' && e.type !== 'session.shutdown' && e.type !== 'session.resume');
-
- // Build new start event
- const newStartId = generateUuid();
- const newStart: ICopilotEventLogEntry = {
- type: 'session.start',
- data: { ...originalStart.data, startTime: new Date().toISOString() },
- id: newStartId,
- timestamp: new Date().toISOString(),
- parentId: null,
- };
-
- // Re-chain: first remaining event points to the new start
- const idMap = new Map<string, string>();
- idMap.set(originalStart.id, newStartId);
-
- const result: ICopilotEventLogEntry[] = [newStart];
- let lastId = newStartId;
-
- for (const entry of kept) {
- const newId = generateUuid();
- idMap.set(entry.id, newId);
-
- const newParentId = entry.parentId !== null
- ? (idMap.get(entry.parentId) ?? lastId)
- : lastId;
-
- result.push({
- type: entry.type,
- data: entry.data,
- id: newId,
- timestamp: entry.timestamp,
- parentId: newParentId,
- });
- lastId = newId;
- }
-
- return result;
-}
-
-/**
- * Generates a `workspace.yaml` file content for a Copilot CLI session.
- */
-export function buildWorkspaceYaml(sessionId: string, cwd: string, summary: string): string {
- const now = new Date().toISOString();
- return [
- `id: ${sessionId}`,
- `cwd: ${cwd}`,
- `summary_count: 0`,
- `created_at: ${now}`,
- `updated_at: ${now}`,
- `summary: ${summary}`,
- '',
- ].join('\n');
-}
-
-// ---- SQLite operations (Copilot CLI session-store.db) ---------------------
-
-/**
- * Forks a session record in the Copilot CLI's `session-store.db`.
- *
- * Copies the source session's metadata, turns (up to `forkTurnIndex`),
- * session files, search index entries, and checkpoints into a new session.
- */
-export async function forkSessionInDb(
- db: Database,
- sourceSessionId: string,
- newSessionId: string,
- forkTurnIndex: number,
-): Promise<void> {
- await dbExec(db, 'PRAGMA foreign_keys = ON');
- await dbExec(db, 'BEGIN TRANSACTION');
- try {
- const now = new Date().toISOString();
-
- // Copy session row
- await dbRun(db,
- `INSERT INTO sessions (id, cwd, repository, branch, summary, created_at, updated_at, host_type)
- SELECT ?, cwd, repository, branch, summary, ?, ?, host_type
- FROM sessions WHERE id = ?`,
- [newSessionId, now, now, sourceSessionId],
- );
-
- // Copy turns up to fork point (turn_index is 0-based)
- await dbRun(db,
- `INSERT INTO turns (session_id, turn_index, user_message, assistant_response, timestamp)
- SELECT ?, turn_index, user_message, assistant_response, timestamp
- FROM turns
- WHERE session_id = ? AND turn_index <= ?`,
- [newSessionId, sourceSessionId, forkTurnIndex],
- );
-
- // Copy session files that were first seen at or before the fork point
- await dbRun(db,
- `INSERT INTO session_files (session_id, file_path, tool_name, turn_index, first_seen_at)
- SELECT ?, file_path, tool_name, turn_index, first_seen_at
- FROM session_files
- WHERE session_id = ? AND turn_index <= ?`,
- [newSessionId, sourceSessionId, forkTurnIndex],
- );
-
- // Copy search index entries for kept turns only.
- // source_id format is "<session_id>:turn:<turn_index>"; filter by
- // parsing the turn index so we don't leak content from later turns.
- await dbAll(db,
- `SELECT content, source_type, source_id
- FROM search_index
- WHERE session_id = ? AND source_type = 'turn'`,
- [sourceSessionId],
- ).then(async rows => {
- const prefix = `${sourceSessionId}:turn:`;
- for (const row of rows) {
- const sourceId = row.source_id as string;
- if (sourceId.startsWith(prefix)) {
- const turnIdx = parseInt(sourceId.substring(prefix.length), 10);
- if (!isNaN(turnIdx) && turnIdx <= forkTurnIndex) {
- const newSourceId = sourceId.replace(sourceSessionId, newSessionId);
- await dbRun(db,
- `INSERT INTO search_index (content, session_id, source_type, source_id)
- VALUES (?, ?, ?, ?)`,
- [row.content, newSessionId, row.source_type, newSourceId],
- );
- }
- }
- }
- });
-
- // Copy checkpoints at or before the fork point.
- // checkpoint_number is 1-based and correlates to turns, so we keep
- // only those where checkpoint_number <= forkTurnIndex + 1.
- await dbRun(db,
- `INSERT INTO checkpoints (session_id, checkpoint_number, title, overview, history, work_done, technical_details, important_files, next_steps, created_at)
- SELECT ?, checkpoint_number, title, overview, history, work_done, technical_details, important_files, next_steps, created_at
- FROM checkpoints
- WHERE session_id = ? AND checkpoint_number <= ?`,
- [newSessionId, sourceSessionId, forkTurnIndex + 1],
- );
-
- await dbExec(db, 'COMMIT');
- } catch (err) {
- await dbExec(db, 'ROLLBACK');
- throw err;
- }
-}
-
-/**
- * Truncates a session in the Copilot CLI's `session-store.db`.
- *
- * Removes all turns after `keepUpToTurnIndex` and updates session metadata.
- */
-export async function truncateSessionInDb(
- db: Database,
- sessionId: string,
- keepUpToTurnIndex: number,
-): Promise<void> {
- await dbExec(db, 'PRAGMA foreign_keys = ON');
- await dbExec(db, 'BEGIN TRANSACTION');
- try {
- const now = new Date().toISOString();
-
- // Delete turns after the truncation point
- await dbRun(db,
- `DELETE FROM turns WHERE session_id = ? AND turn_index > ?`,
- [sessionId, keepUpToTurnIndex],
- );
-
- // Update session timestamp
- await dbRun(db,
- `UPDATE sessions SET updated_at = ? WHERE id = ?`,
- [now, sessionId],
- );
-
- // Remove search index entries for removed turns
- // source_id format is "<session_id>:turn:<turn_index>"
- await dbAll(db,
- `SELECT source_id FROM search_index
- WHERE session_id = ? AND source_type = 'turn'`,
- [sessionId],
- ).then(async rows => {
- const prefix = `${sessionId}:turn:`;
- for (const row of rows) {
- const sourceId = row.source_id as string;
- if (sourceId.startsWith(prefix)) {
- const turnIdx = parseInt(sourceId.substring(prefix.length), 10);
- if (!isNaN(turnIdx) && turnIdx > keepUpToTurnIndex) {
- await dbRun(db,
- `DELETE FROM search_index WHERE source_id = ? AND session_id = ?`,
- [sourceId, sessionId],
- );
- }
- }
- }
- });
-
- await dbExec(db, 'COMMIT');
- } catch (err) {
- await dbExec(db, 'ROLLBACK');
- throw err;
- }
-}
-
-// ---- File system operations -----------------------------------------------
-
-/**
- * Resolves the Copilot CLI data directory.
- * The Copilot CLI stores its data in `~/.copilot/` by default, or in the
- * directory specified by `COPILOT_CONFIG_DIR`.
- */
-export function getCopilotDataDir(): string {
- return process.env['COPILOT_CONFIG_DIR'] ?? path.join(os.homedir(), '.copilot');
-}
-
-/**
- * Forks a Copilot CLI session on disk.
- *
- * 1. Reads the source session's `events.jsonl`
- * 2. Builds a forked event log
- * 3. Creates the new session folder with all required files/directories
- * 4. Updates the `session-store.db`
- *
- * @param copilotDataDir Path to the `.copilot` directory
- * @param sourceSessionId UUID of the source session to fork from
- * @param newSessionId UUID for the new forked session
- * @param forkTurnIndex 0-based turn index to fork at (inclusive)
- */
-export async function forkCopilotSessionOnDisk(
- copilotDataDir: string,
- sourceSessionId: string,
- newSessionId: string,
- forkTurnIndex: number,
-): Promise<void> {
- const sessionStateDir = path.join(copilotDataDir, 'session-state');
-
- // Read source events
- const sourceEventsPath = path.join(sessionStateDir, sourceSessionId, 'events.jsonl');
- const sourceContent = await fs.promises.readFile(sourceEventsPath, 'utf-8');
- const sourceEntries = parseEventLog(sourceContent);
-
- // Build forked event log
- const forkedEntries = buildForkedEventLog(sourceEntries, forkTurnIndex, newSessionId);
-
- // Read source workspace.yaml for cwd/summary
- let cwd = '';
- let summary = '';
- try {
- const workspaceYamlPath = path.join(sessionStateDir, sourceSessionId, 'workspace.yaml');
- const yamlContent = await fs.promises.readFile(workspaceYamlPath, 'utf-8');
- const cwdMatch = yamlContent.match(/^cwd:\s*(.+)$/m);
- const summaryMatch = yamlContent.match(/^summary:\s*(.+)$/m);
- if (cwdMatch) {
- cwd = cwdMatch[1].trim();
- }
- if (summaryMatch) {
- summary = summaryMatch[1].trim();
- }
- } catch {
- // Fall back to session.start data
- const startEvent = sourceEntries.find(e => e.type === 'session.start');
- if (startEvent) {
- const ctx = startEvent.data.context as Record<string, string> | undefined;
- cwd = ctx?.cwd ?? '';
- }
- }
-
- // Create new session folder structure
- const newSessionDir = path.join(sessionStateDir, newSessionId);
- await fs.promises.mkdir(path.join(newSessionDir, 'checkpoints'), { recursive: true });
- await fs.promises.mkdir(path.join(newSessionDir, 'files'), { recursive: true });
- await fs.promises.mkdir(path.join(newSessionDir, 'research'), { recursive: true });
-
- // Write events.jsonl
- await fs.promises.writeFile(
- path.join(newSessionDir, 'events.jsonl'),
- serializeEventLog(forkedEntries),
- 'utf-8',
- );
-
- // Write workspace.yaml
- await fs.promises.writeFile(
- path.join(newSessionDir, 'workspace.yaml'),
- buildWorkspaceYaml(newSessionId, cwd, summary),
- 'utf-8',
- );
-
- // Write empty vscode.metadata.json
- await fs.promises.writeFile(
- path.join(newSessionDir, 'vscode.metadata.json'),
- '{}',
- 'utf-8',
- );
-
- // Write empty checkpoints index
- await fs.promises.writeFile(
- path.join(newSessionDir, 'checkpoints', 'index.md'),
- '',
- 'utf-8',
- );
-
- // Update session-store.db
- const dbPath = path.join(copilotDataDir, 'session-store.db');
- const db = await dbOpen(dbPath);
- try {
- await forkSessionInDb(db, sourceSessionId, newSessionId, forkTurnIndex);
- } finally {
- await dbClose(db);
- }
-}
-
-/**
- * Truncates a Copilot CLI session on disk.
- *
- * 1. Reads the session's `events.jsonl`
- * 2. Builds a truncated event log
- * 3. Overwrites `events.jsonl` and updates `workspace.yaml`
- * 4. Updates the `session-store.db`
- *
- * @param copilotDataDir Path to the `.copilot` directory
- * @param sessionId UUID of the session to truncate
- * @param keepUpToTurnIndex 0-based turn index to keep up to (inclusive)
- */
-export async function truncateCopilotSessionOnDisk(
- copilotDataDir: string,
- sessionId: string,
- keepUpToTurnIndex: number,
-): Promise<void> {
- const sessionStateDir = path.join(copilotDataDir, 'session-state');
- const sessionDir = path.join(sessionStateDir, sessionId);
-
- // Read and truncate events
- const eventsPath = path.join(sessionDir, 'events.jsonl');
- const content = await fs.promises.readFile(eventsPath, 'utf-8');
- const entries = parseEventLog(content);
-
- let truncatedEntries: ICopilotEventLogEntry[];
- if (keepUpToTurnIndex < 0) {
- // Truncate all turns: keep only a fresh session.start event
- const originalStart = entries.find(e => e.type === 'session.start');
- if (!originalStart) {
- throw new Error('No session.start event found in event log');
- }
- truncatedEntries = [{
- type: 'session.start',
- data: { ...originalStart.data, startTime: new Date().toISOString() },
- id: generateUuid(),
- timestamp: new Date().toISOString(),
- parentId: null,
- }];
- } else {
- truncatedEntries = buildTruncatedEventLog(entries, keepUpToTurnIndex);
- }
-
- // Overwrite events.jsonl
- await fs.promises.writeFile(eventsPath, serializeEventLog(truncatedEntries), 'utf-8');
-
- // Update workspace.yaml timestamp
- try {
- const yamlPath = path.join(sessionDir, 'workspace.yaml');
- let yaml = await fs.promises.readFile(yamlPath, 'utf-8');
- yaml = yaml.replace(/^updated_at:\s*.+$/m, `updated_at: ${new Date().toISOString()}`);
- await fs.promises.writeFile(yamlPath, yaml, 'utf-8');
- } catch {
- // workspace.yaml may not exist (old format)
- }
-
- // Update session-store.db
- const dbPath = path.join(copilotDataDir, 'session-store.db');
- const db = await dbOpen(dbPath);
- try {
- await truncateSessionInDb(db, sessionId, keepUpToTurnIndex);
- } finally {
- await dbClose(db);
- }
-}
-
-/**
- * Maps a protocol turn ID to a 0-based turn index by finding the turn's
- * position within the session's event log.
- *
- * The protocol state assigns arbitrary string IDs to turns, but the Copilot
- * CLI's `events.jsonl` uses sequential `user.message` events. To bridge the
- * two, we match turns by their position in the sequence.
- *
- * @returns The 0-based turn index, or `-1` if the turn ID is not found in the
- * `turnIds` array.
- */
-export function turnIdToIndex(turnIds: readonly string[], turnId: string): number {
- return turnIds.indexOf(turnId);
-}
diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts
deleted file mode 100644
index e4e4203d..00000000
--- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts
+++ /dev/null
@@ -1,754 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import type { PermissionRequest, PermissionRequestResult } from '@github/copilot-sdk';
-import { DeferredPromise } from '../../../../base/common/async.js';
-import { Emitter } from '../../../../base/common/event.js';
-import { Disposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js';
-import { URI } from '../../../../base/common/uri.js';
-import { extUriBiasedIgnorePathCase, normalizePath } from '../../../../base/common/resources.js';
-import { generateUuid } from '../../../../base/common/uuid.js';
-import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
-import { ILogService } from '../../../log/common/log.js';
-import { localize } from '../../../../nls.js';
-import { IAgentAttachment, IAgentMessageEvent, IAgentProgressEvent, IAgentSubagentStartedEvent, IAgentToolCompleteEvent, IAgentToolStartEvent } from '../../common/agentService.js';
-import { ISessionDatabase, ISessionDataService } from '../../common/sessionDataService.js';
-import { SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, ToolResultContentType, type ISessionInputAnswer, type ISessionInputRequest, type IPendingMessage, type IToolResultContent } from '../../common/state/sessionState.js';
-import { CopilotSessionWrapper } from './copilotSessionWrapper.js';
-import { getEditFilePath, getInvocationMessage, getPastTenseMessage, getShellLanguage, getToolDisplayName, getToolInputString, getToolKind, isEditTool, isHiddenTool, isShellTool } from './copilotToolDisplay.js';
-import { FileEditTracker } from './fileEditTracker.js';
-import { mapSessionEvents } from './mapSessionEvents.js';
-import type { ShellManager } from './copilotShellTools.js';
-
-/**
- * Factory function that produces a {@link CopilotSessionWrapper}.
- * Called by {@link CopilotAgentSession.initializeSession} with the
- * session's permission handler and edit-tracking hooks so the factory
- * can wire them into the SDK session it creates.
- *
- * In production, the factory calls `CopilotClient.createSession()` or
- * `resumeSession()`. In tests, it returns a mock wrapper directly.
- */
-export type SessionWrapperFactory = (callbacks: {
- readonly onPermissionRequest: (request: PermissionRequest) => Promise<PermissionRequestResult>;
- readonly onUserInputRequest: (request: IUserInputRequest, invocation: { sessionId: string }) => Promise<IUserInputResponse>;
- readonly hooks: {
- readonly onPreToolUse: (input: { toolName: string; toolArgs: unknown }) => Promise<void>;
- readonly onPostToolUse: (input: { toolName: string; toolArgs: unknown }) => Promise<void>;
- };
-}) => Promise<CopilotSessionWrapper>;
-
-/** Matches the SDK's `UserInputRequest` which is not re-exported from the package entry point. */
-interface IUserInputRequest {
- question: string;
- choices?: string[];
- allowFreeform?: boolean;
-}
-
-/** Matches the SDK's `UserInputResponse` which is not re-exported from the package entry point. */
-interface IUserInputResponse {
- answer: string;
- wasFreeform: boolean;
-}
-
-function tryStringify(value: unknown): string | undefined {
- try {
- return JSON.stringify(value);
- } catch {
- return undefined;
- }
-}
-
-/**
- * Derives display fields from a permission request for the tool confirmation UI.
- */
-function getPermissionDisplay(request: { kind: string;[key: string]: unknown }): {
- confirmationTitle: string;
- invocationMessage: string;
- toolInput?: string;
- /** Normalized permission kind for auto-approval routing. */
- permissionKind: string;
-} {
- const path = typeof request.path === 'string' ? request.path : (typeof request.fileName === 'string' ? request.fileName : undefined);
- const fullCommandText = typeof request.fullCommandText === 'string' ? request.fullCommandText : undefined;
- const intention = typeof request.intention === 'string' ? request.intention : undefined;
- const serverName = typeof request.serverName === 'string' ? request.serverName : undefined;
- const toolName = typeof request.toolName === 'string' ? request.toolName : undefined;
-
- switch (request.kind) {
- case 'shell':
- return {
- confirmationTitle: localize('copilot.permission.shell.title', "Run in terminal"),
- invocationMessage: intention ?? localize('copilot.permission.shell.message', "Run command"),
- toolInput: fullCommandText,
- permissionKind: 'shell',
- };
- case 'custom-tool': {
- // Custom tool overrides (e.g. our shell tool). Extract the actual
- // tool args from the SDK's wrapper envelope.
- const args = typeof request.args === 'object' && request.args !== null ? request.args as Record<string, unknown> : undefined;
- const command = typeof args?.command === 'string' ? args.command : undefined;
- const sdkToolName = typeof request.toolName === 'string' ? request.toolName : undefined;
- if (command && sdkToolName && isShellTool(sdkToolName)) {
- return {
- confirmationTitle: localize('copilot.permission.shell.title', "Run in terminal"),
- invocationMessage: localize('copilot.permission.shell.message', "Run command"),
- toolInput: command,
- permissionKind: 'shell',
- };
- }
- return {
- confirmationTitle: toolName ?? localize('copilot.permission.default.title', "Permission request"),
- invocationMessage: localize('copilot.permission.default.message', "Permission request"),
- toolInput: args ? tryStringify(args) : tryStringify(request),
- permissionKind: request.kind,
- };
- }
- case 'write':
- return {
- confirmationTitle: localize('copilot.permission.write.title', "Write file"),
- invocationMessage: path ? localize('copilot.permission.write.message', "Edit {0}", path) : localize('copilot.permission.write.messageGeneric', "Edit file"),
- toolInput: tryStringify(path ? { path } : request) ?? undefined,
- permissionKind: 'write',
- };
- case 'mcp': {
- const title = toolName ?? localize('copilot.permission.mcp.defaultTool', "MCP Tool");
- return {
- confirmationTitle: serverName ? `${serverName}: ${title}` : title,
- invocationMessage: serverName ? `${serverName}: ${title}` : title,
- toolInput: tryStringify({ serverName, toolName }) ?? undefined,
- permissionKind: 'mcp',
- };
- }
- case 'read':
- return {
- confirmationTitle: localize('copilot.permission.read.title', "Read file"),
- invocationMessage: intention ?? localize('copilot.permission.read.message', "Read file"),
- toolInput: tryStringify(path ? { path, intention } : request) ?? undefined,
- permissionKind: 'read',
- };
- default:
- return {
- confirmationTitle: localize('copilot.permission.default.title', "Permission request"),
- invocationMessage: localize('copilot.permission.default.message', "Permission request"),
- toolInput: tryStringify(request) ?? undefined,
- permissionKind: request.kind,
- };
- }
-}
-
-/**
- * Encapsulates a single Copilot SDK session and all its associated bookkeeping.
- *
- * Created by {@link CopilotAgent}, one instance per active session. Disposing
- * this class tears down all per-session resources (SDK wrapper, edit tracker,
- * database reference, pending permissions).
- */
-export class CopilotAgentSession extends Disposable {
- readonly sessionId: string;
- readonly sessionUri: URI;
-
- /** Tracks active tool invocations so we can produce past-tense messages on completion. */
- private readonly _activeToolCalls = new Map<string, { toolName: string; displayName: string; parameters: Record<string, unknown> | undefined }>();
- /** Pending permission requests awaiting a renderer-side decision. */
- private readonly _pendingPermissions = new Map<string, DeferredPromise<boolean>>();
- /** Pending user input requests awaiting a renderer-side answer. */
- private readonly _pendingUserInputs = new Map<string, { deferred: DeferredPromise<{ response: SessionInputResponseKind; answers?: Record<string, ISessionInputAnswer> }>; questionId: string }>();
- /** File edit tracker for this session. */
- private readonly _editTracker: FileEditTracker;
- /** Session database reference. */
- private readonly _databaseRef: IReference<ISessionDatabase>;
- /** Protocol turn ID set by {@link send}, used for file edit tracking. */
- private _turnId = '';
- /** SDK session wrapper, set by {@link initializeSession}. */
- private _wrapper!: CopilotSessionWrapper;
-
- private readonly _workingDirectory: URI | undefined;
-
- constructor(
- sessionUri: URI,
- rawSessionId: string,
- workingDirectory: URI | undefined,
- private readonly _onDidSessionProgress: Emitter<IAgentProgressEvent>,
- private readonly _wrapperFactory: SessionWrapperFactory,
- private readonly _shellManager: ShellManager | undefined,
- @IInstantiationService private readonly _instantiationService: IInstantiationService,
- @ILogService private readonly _logService: ILogService,
- @ISessionDataService sessionDataService: ISessionDataService,
- ) {
- super();
- this.sessionId = rawSessionId;
- this.sessionUri = sessionUri;
- this._workingDirectory = workingDirectory;
-
- this._databaseRef = sessionDataService.openDatabase(sessionUri);
- this._register(toDisposable(() => this._databaseRef.dispose()));
-
- this._editTracker = this._instantiationService.createInstance(FileEditTracker, sessionUri.toString(), this._databaseRef.object);
-
- this._register(toDisposable(() => this._denyPendingPermissions()));
- this._register(toDisposable(() => this._shellManager?.dispose()));
- this._register(toDisposable(() => this._cancelPendingUserInputs()));
- }
-
- /**
- * Creates (or resumes) the SDK session via the injected factory and
- * wires up all event listeners. Must be called exactly once after
- * construction before using the session.
- */
- async initializeSession(): Promise<void> {
- this._wrapper = this._register(await this._wrapperFactory({
- onPermissionRequest: request => this.handlePermissionRequest(request),
- onUserInputRequest: (request, invocation) => this.handleUserInputRequest(request, invocation),
- hooks: {
- onPreToolUse: async input => {
- if (isEditTool(input.toolName)) {
- const filePath = getEditFilePath(input.toolArgs);
- if (filePath) {
- await this._editTracker.trackEditStart(filePath);
- }
- }
- },
- onPostToolUse: async input => {
- if (isEditTool(input.toolName)) {
- const filePath = getEditFilePath(input.toolArgs);
- if (filePath) {
- await this._editTracker.completeEdit(filePath);
- }
- }
- },
- },
- }));
- this._subscribeToEvents();
- this._subscribeForLogging();
- }
-
- // ---- session operations -------------------------------------------------
-
- async send(prompt: string, attachments?: IAgentAttachment[], turnId?: string): Promise<void> {
- if (turnId) {
- this._turnId = turnId;
- }
- this._logService.info(`[Copilot:${this.sessionId}] sendMessage called: "${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}" (${attachments?.length ?? 0} attachments)`);
-
- const sdkAttachments = attachments?.map(a => {
- if (a.type === 'selection') {
- return { type: 'selection' as const, filePath: a.path, displayName: a.displayName ?? a.path, text: a.text, selection: a.selection };
- }
- return { type: a.type, path: a.path, displayName: a.displayName };
- });
- if (sdkAttachments?.length) {
- this._logService.trace(`[Copilot:${this.sessionId}] Attachments: ${JSON.stringify(sdkAttachments.map(a => ({ type: a.type, path: a.type === 'selection' ? a.filePath : a.path })))}`);
- }
-
- await this._wrapper.session.send({ prompt, attachments: sdkAttachments });
- this._logService.info(`[Copilot:${this.sessionId}] session.send() returned`);
- }
-
- async sendSteering(steeringMessage: IPendingMessage): Promise<void> {
- this._logService.info(`[Copilot:${this.sessionId}] Sending steering message: "${steeringMessage.userMessage.text.substring(0, 100)}"`);
- try {
- await this._wrapper.session.send({
- prompt: steeringMessage.userMessage.text,
- mode: 'immediate',
- });
- this._onDidSessionProgress.fire({
- session: this.sessionUri,
- type: 'steering_consumed',
- id: steeringMessage.id,
- });
- } catch (err) {
- this._logService.error(`[Copilot:${this.sessionId}] Steering message failed`, err);
- }
- }
-
- async getMessages(): Promise<(IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[]> {
- const events = await this._wrapper.session.getMessages();
- let db: ISessionDatabase | undefined;
- try {
- db = this._databaseRef.object;
- } catch {
- // Database may not exist yet — that's fine
- }
- return mapSessionEvents(this.sessionUri, db, events);
- }
-
- async abort(): Promise<void> {
- this._logService.info(`[Copilot:${this.sessionId}] Aborting session...`);
- this._denyPendingPermissions();
- await this._wrapper.session.abort();
- }
-
- /**
- * Explicitly destroys the underlying SDK session and waits for cleanup
- * to complete. Call this before {@link dispose} when you need to ensure
- * the session's on-disk data is no longer locked (e.g. before
- * truncation or fork operations that modify the session files).
- */
- async destroySession(): Promise<void> {
- await this._wrapper.session.destroy();
- }
-
- async setModel(model: string): Promise<void> {
- this._logService.info(`[Copilot:${this.sessionId}] Changing model to: ${model}`);
- await this._wrapper.session.setModel(model);
- }
-
- // ---- permission handling ------------------------------------------------
-
- /**
- * Handles a permission request from the SDK by firing a `tool_ready` event
- * (which transitions the tool to PendingConfirmation) and waiting for the
- * side-effects layer to respond via {@link respondToPermissionRequest}.
- */
- async handlePermissionRequest(
- request: PermissionRequest,
- ): Promise<PermissionRequestResult> {
- this._logService.info(`[Copilot:${this.sessionId}] Permission request: kind=${request.kind}`);
-
- // Auto-approve reads inside the working directory
- if (request.kind === 'read') {
- const requestPath = typeof request.path === 'string' ? request.path : undefined;
- if (requestPath && this._workingDirectory && extUriBiasedIgnorePathCase.isEqualOrParent(normalizePath(URI.file(requestPath)), this._workingDirectory)) {
- this._logService.trace(`[Copilot:${this.sessionId}] Auto-approving read inside working directory: ${requestPath}`);
- return { kind: 'approved' };
- }
- }
-
- const toolCallId = request.toolCallId;
- if (!toolCallId) {
- // TODO: handle permission requests without a toolCallId by creating a synthetic tool call
- this._logService.warn(`[Copilot:${this.sessionId}] Permission request without toolCallId, auto-denying: kind=${request.kind}`);
- return { kind: 'denied-interactively-by-user' };
- }
-
- this._logService.info(`[Copilot:${this.sessionId}] Requesting confirmation for tool call: ${toolCallId}`);
-
- const deferred = new DeferredPromise<boolean>();
- this._pendingPermissions.set(toolCallId, deferred);
-
- // Derive display information from the permission request kind
- const { confirmationTitle, invocationMessage, toolInput, permissionKind } = getPermissionDisplay(request);
-
- // Fire a tool_ready event to transition the tool to PendingConfirmation
- this._onDidSessionProgress.fire({
- session: this.sessionUri,
- type: 'tool_ready',
- toolCallId,
- invocationMessage,
- toolInput,
- confirmationTitle,
- permissionKind,
- permissionPath: typeof request.path === 'string' ? request.path : (typeof request.fileName === 'string' ? request.fileName : undefined),
- });
-
- const approved = await deferred.p;
- this._logService.info(`[Copilot:${this.sessionId}] Permission response: toolCallId=${toolCallId}, approved=${approved}`);
- return { kind: approved ? 'approved' : 'denied-interactively-by-user' };
- }
-
- respondToPermissionRequest(requestId: string, approved: boolean): boolean {
- const deferred = this._pendingPermissions.get(requestId);
- if (deferred) {
- this._pendingPermissions.delete(requestId);
- deferred.complete(approved);
- return true;
- }
- return false;
- }
-
- // ---- user input handling ------------------------------------------------
-
- /**
- * Handles a user input request from the SDK (ask_user tool) by firing a
- * `user_input_request` progress event and waiting for the renderer to
- * respond via {@link respondToUserInputRequest}.
- */
- async handleUserInputRequest(
- request: IUserInputRequest,
- _invocation: { sessionId: string },
- ): Promise<IUserInputResponse> {
- const requestId = generateUuid();
- const questionId = generateUuid();
- this._logService.info(`[Copilot:${this.sessionId}] User input request: requestId=${requestId}, question="${request.question.substring(0, 100)}"`);
-
- const deferred = new DeferredPromise<{ response: SessionInputResponseKind; answers?: Record<string, ISessionInputAnswer> }>();
- this._pendingUserInputs.set(requestId, { deferred, questionId });
-
- // Build the protocol ISessionInputRequest from the SDK's simple format
- const inputRequest: ISessionInputRequest = {
- id: requestId,
- message: request.question,
- questions: [request.choices && request.choices.length > 0
- ? {
- kind: SessionInputQuestionKind.SingleSelect,
- id: questionId,
- message: request.question,
- required: true,
- options: request.choices.map(c => ({ id: c, label: c })),
- allowFreeformInput: request.allowFreeform ?? true,
- }
- : {
- kind: SessionInputQuestionKind.Text,
- id: questionId,
- message: request.question,
- required: true,
- },
- ],
- };
-
- this._onDidSessionProgress.fire({
- session: this.sessionUri,
- type: 'user_input_request',
- request: inputRequest,
- });
-
- const result = await deferred.p;
- this._logService.info(`[Copilot:${this.sessionId}] User input response: requestId=${requestId}, response=${result.response}`);
-
- if (result.response !== SessionInputResponseKind.Accept || !result.answers) {
- return { answer: '', wasFreeform: true };
- }
-
- // Extract the answer for our single question
- const answer = result.answers[questionId];
- if (!answer || answer.state === SessionInputAnswerState.Skipped) {
- return { answer: '', wasFreeform: true };
- }
-
- const { value: val } = answer;
- if (val.kind === SessionInputAnswerValueKind.Text) {
- return { answer: val.value, wasFreeform: true };
- } else if (val.kind === SessionInputAnswerValueKind.Selected) {
- const wasFreeform = !request.choices?.includes(val.value);
- return { answer: val.value, wasFreeform };
- }
-
- return { answer: '', wasFreeform: true };
- }
-
- respondToUserInputRequest(requestId: string, response: SessionInputResponseKind, answers?: Record<string, ISessionInputAnswer>): boolean {
- const pending = this._pendingUserInputs.get(requestId);
- if (pending) {
- this._pendingUserInputs.delete(requestId);
- pending.deferred.complete({ response, answers });
- return true;
- }
- return false;
- }
-
- // ---- event wiring -------------------------------------------------------
-
- private _subscribeToEvents(): void {
- const wrapper = this._wrapper;
- const sessionId = this.sessionId;
- const session = this.sessionUri;
-
- this._register(wrapper.onMessageDelta(e => {
- this._logService.trace(`[Copilot:${sessionId}] delta: ${e.data.deltaContent}`);
- this._onDidSessionProgress.fire({
- session,
- type: 'delta',
- messageId: e.data.messageId,
- content: e.data.deltaContent,
- parentToolCallId: e.data.parentToolCallId,
- });
- }));
-
- this._register(wrapper.onMessage(e => {
- this._logService.info(`[Copilot:${sessionId}] Full message received: ${e.data.content.length} chars`);
- this._onDidSessionProgress.fire({
- session,
- type: 'message',
- role: 'assistant',
- messageId: e.data.messageId,
- content: e.data.content,
- toolRequests: e.data.toolRequests?.map(tr => ({
- toolCallId: tr.toolCallId,
- name: tr.name,
- arguments: tr.arguments !== undefined ? tryStringify(tr.arguments) : undefined,
- type: tr.type,
- })),
- reasoningOpaque: e.data.reasoningOpaque,
- reasoningText: e.data.reasoningText,
- encryptedContent: e.data.encryptedContent,
- parentToolCallId: e.data.parentToolCallId,
- });
- }));
-
- this._register(wrapper.onToolStart(e => {
- if (isHiddenTool(e.data.toolName)) {
- this._logService.trace(`[Copilot:${sessionId}] Tool started (hidden): ${e.data.toolName}`);
- return;
- }
- this._logService.info(`[Copilot:${sessionId}] Tool started: ${e.data.toolName}`);
- const toolArgs = e.data.arguments !== undefined ? tryStringify(e.data.arguments) : undefined;
- let parameters: Record<string, unknown> | undefined;
- if (toolArgs) {
- try { parameters = JSON.parse(toolArgs) as Record<string, unknown>; } catch { /* ignore */ }
- }
- const displayName = getToolDisplayName(e.data.toolName);
- this._activeToolCalls.set(e.data.toolCallId, { toolName: e.data.toolName, displayName, parameters });
- const toolKind = getToolKind(e.data.toolName);
-
- this._onDidSessionProgress.fire({
- session,
- type: 'tool_start',
- toolCallId: e.data.toolCallId,
- toolName: e.data.toolName,
- displayName,
- invocationMessage: getInvocationMessage(e.data.toolName, displayName, parameters),
- toolInput: getToolInputString(e.data.toolName, parameters, toolArgs),
- toolKind,
- language: toolKind === 'terminal' ? getShellLanguage(e.data.toolName) : undefined,
- toolArguments: toolArgs,
- mcpServerName: e.data.mcpServerName,
- mcpToolName: e.data.mcpToolName,
- parentToolCallId: e.data.parentToolCallId,
- });
- }));
-
- this._register(wrapper.onToolComplete(async e => {
- const tracked = this._activeToolCalls.get(e.data.toolCallId);
- if (!tracked) {
- return;
- }
- this._logService.info(`[Copilot:${sessionId}] Tool completed: ${e.data.toolCallId}`);
- this._activeToolCalls.delete(e.data.toolCallId);
- const displayName = tracked.displayName;
- const toolOutput = e.data.error?.message ?? e.data.result?.content;
-
- const content: IToolResultContent[] = [];
- if (toolOutput !== undefined) {
- content.push({ type: ToolResultContentType.Text, text: toolOutput });
- }
-
- const filePath = isEditTool(tracked.toolName) ? getEditFilePath(tracked.parameters) : undefined;
- if (filePath) {
- try {
- const fileEdit = await this._editTracker.takeCompletedEdit(this._turnId, e.data.toolCallId, filePath);
- if (fileEdit) {
- content.push(fileEdit);
- }
- } catch (err) {
- this._logService.warn(`[Copilot:${sessionId}] Failed to take completed edit`, err);
- }
- }
-
- // Add terminal content reference for shell tools
- if (isShellTool(tracked.toolName) && this._shellManager) {
- const terminalUri = this._shellManager.getTerminalUriForToolCall(e.data.toolCallId);
- if (terminalUri) {
- content.push({
- type: ToolResultContentType.Terminal,
- resource: terminalUri,
- title: tracked.displayName,
- });
- }
- }
-
- this._onDidSessionProgress.fire({
- session,
- type: 'tool_complete',
- toolCallId: e.data.toolCallId,
- result: {
- success: e.data.success,
- pastTenseMessage: getPastTenseMessage(tracked.toolName, displayName, tracked.parameters, e.data.success),
- content: content.length > 0 ? content : undefined,
- error: e.data.error,
- },
- isUserRequested: e.data.isUserRequested,
- toolTelemetry: e.data.toolTelemetry !== undefined ? tryStringify(e.data.toolTelemetry) : undefined,
- parentToolCallId: e.data.parentToolCallId,
- });
- }));
-
- this._register(wrapper.onIdle(() => {
- this._logService.info(`[Copilot:${sessionId}] Session idle`);
- this._onDidSessionProgress.fire({ session, type: 'idle' });
- }));
-
- this._register(wrapper.onSubagentStarted(e => {
- this._logService.info(`[Copilot:${sessionId}] Subagent started: toolCallId=${e.data.toolCallId}, agent=${e.data.agentName}`);
- this._onDidSessionProgress.fire({
- session,
- type: 'subagent_started',
- toolCallId: e.data.toolCallId,
- agentName: e.data.agentName,
- agentDisplayName: e.data.agentDisplayName,
- agentDescription: e.data.agentDescription,
- });
- }));
-
- this._register(wrapper.onSessionError(e => {
- this._logService.error(`[Copilot:${sessionId}] Session error: ${e.data.errorType} - ${e.data.message}`);
- this._onDidSessionProgress.fire({
- session,
- type: 'error',
- errorType: e.data.errorType,
- message: e.data.message,
- stack: e.data.stack,
- });
- }));
-
- this._register(wrapper.onUsage(e => {
- this._logService.trace(`[Copilot:${sessionId}] Usage: model=${e.data.model}, in=${e.data.inputTokens ?? '?'}, out=${e.data.outputTokens ?? '?'}, cacheRead=${e.data.cacheReadTokens ?? '?'}`);
- this._onDidSessionProgress.fire({
- session,
- type: 'usage',
- inputTokens: e.data.inputTokens,
- outputTokens: e.data.outputTokens,
- model: e.data.model,
- cacheReadTokens: e.data.cacheReadTokens,
- });
- }));
-
- this._register(wrapper.onReasoningDelta(e => {
- this._logService.trace(`[Copilot:${sessionId}] Reasoning delta: ${e.data.deltaContent.length} chars`);
- this._onDidSessionProgress.fire({
- session,
- type: 'reasoning',
- content: e.data.deltaContent,
- });
- }));
- }
-
- private _subscribeForLogging(): void {
- const wrapper = this._wrapper;
- const sessionId = this.sessionId;
-
- this._register(wrapper.onSessionStart(e => {
- this._logService.trace(`[Copilot:${sessionId}] Session started: model=${e.data.selectedModel ?? 'default'}, producer=${e.data.producer}`);
- }));
-
- this._register(wrapper.onSessionResume(e => {
- this._logService.trace(`[Copilot:${sessionId}] Session resumed: eventCount=${e.data.eventCount}`);
- }));
-
- this._register(wrapper.onSessionInfo(e => {
- this._logService.trace(`[Copilot:${sessionId}] Session info [${e.data.infoType}]: ${e.data.message}`);
- }));
-
- this._register(wrapper.onSessionModelChange(e => {
- this._logService.trace(`[Copilot:${sessionId}] Model changed: ${e.data.previousModel ?? '(none)'} -> ${e.data.newModel}`);
- }));
-
- this._register(wrapper.onSessionHandoff(e => {
- this._logService.trace(`[Copilot:${sessionId}] Session handoff: sourceType=${e.data.sourceType}, remoteSessionId=${e.data.remoteSessionId ?? '(none)'}`);
- }));
-
- this._register(wrapper.onSessionTruncation(e => {
- this._logService.trace(`[Copilot:${sessionId}] Session truncation: removed ${e.data.tokensRemovedDuringTruncation} tokens, ${e.data.messagesRemovedDuringTruncation} messages`);
- }));
-
- this._register(wrapper.onSessionSnapshotRewind(e => {
- this._logService.trace(`[Copilot:${sessionId}] Snapshot rewind: upTo=${e.data.upToEventId}, eventsRemoved=${e.data.eventsRemoved}`);
- }));
-
- this._register(wrapper.onSessionShutdown(e => {
- this._logService.trace(`[Copilot:${sessionId}] Session shutdown: type=${e.data.shutdownType}, premiumRequests=${e.data.totalPremiumRequests}, apiDuration=${e.data.totalApiDurationMs}ms`);
- }));
-
- this._register(wrapper.onSessionUsageInfo(e => {
- this._logService.trace(`[Copilot:${sessionId}] Usage info: ${e.data.currentTokens}/${e.data.tokenLimit} tokens, ${e.data.messagesLength} messages`);
- }));
-
- this._register(wrapper.onSessionCompactionStart(() => {
- this._logService.trace(`[Copilot:${sessionId}] Compaction started`);
- }));
-
- this._register(wrapper.onSessionCompactionComplete(e => {
- this._logService.trace(`[Copilot:${sessionId}] Compaction complete: success=${e.data.success}, tokensRemoved=${e.data.tokensRemoved ?? '?'}`);
- }));
-
- this._register(wrapper.onUserMessage(e => {
- this._logService.trace(`[Copilot:${sessionId}] User message: ${e.data.content.length} chars, ${e.data.attachments?.length ?? 0} attachments`);
- }));
-
- this._register(wrapper.onPendingMessagesModified(() => {
- this._logService.trace(`[Copilot:${sessionId}] Pending messages modified`);
- }));
-
- this._register(wrapper.onTurnStart(e => {
- this._logService.trace(`[Copilot:${sessionId}] Turn started: ${e.data.turnId}`);
- }));
-
- this._register(wrapper.onIntent(e => {
- this._logService.trace(`[Copilot:${sessionId}] Intent: ${e.data.intent}`);
- }));
-
- this._register(wrapper.onReasoning(e => {
- this._logService.trace(`[Copilot:${sessionId}] Reasoning: ${e.data.content.length} chars`);
- }));
-
- this._register(wrapper.onTurnEnd(e => {
- this._logService.trace(`[Copilot:${sessionId}] Turn ended: ${e.data.turnId}`);
- }));
-
- this._register(wrapper.onAbort(e => {
- this._logService.trace(`[Copilot:${sessionId}] Aborted: ${e.data.reason}`);
- }));
-
- this._register(wrapper.onToolUserRequested(e => {
- this._logService.trace(`[Copilot:${sessionId}] Tool user-requested: ${e.data.toolName} (${e.data.toolCallId})`);
- }));
-
- this._register(wrapper.onToolPartialResult(e => {
- this._logService.trace(`[Copilot:${sessionId}] Tool partial result: ${e.data.toolCallId} (${e.data.partialOutput.length} chars)`);
- }));
-
- this._register(wrapper.onToolProgress(e => {
- this._logService.trace(`[Copilot:${sessionId}] Tool progress: ${e.data.toolCallId} - ${e.data.progressMessage}`);
- }));
-
- this._register(wrapper.onSkillInvoked(e => {
- this._logService.trace(`[Copilot:${sessionId}] Skill invoked: ${e.data.name} (${e.data.path})`);
- }));
-
- this._register(wrapper.onSubagentStarted(e => {
- this._logService.trace(`[Copilot:${sessionId}] Subagent started: ${e.data.agentName} (${e.data.agentDisplayName})`);
- }));
-
- this._register(wrapper.onSubagentCompleted(e => {
- this._logService.trace(`[Copilot:${sessionId}] Subagent completed: ${e.data.agentName}`);
- }));
-
- this._register(wrapper.onSubagentFailed(e => {
- this._logService.error(`[Copilot:${sessionId}] Subagent failed: ${e.data.agentName} - ${e.data.error}`);
- }));
-
- this._register(wrapper.onSubagentSelected(e => {
- this._logService.trace(`[Copilot:${sessionId}] Subagent selected: ${e.data.agentName}`);
- }));
-
- this._register(wrapper.onHookStart(e => {
- this._logService.trace(`[Copilot:${sessionId}] Hook started: ${e.data.hookType} (${e.data.hookInvocationId})`);
- }));
-
- this._register(wrapper.onHookEnd(e => {
- this._logService.trace(`[Copilot:${sessionId}] Hook ended: ${e.data.hookType} (${e.data.hookInvocationId}), success=${e.data.success}`);
- }));
-
- this._register(wrapper.onSystemMessage(e => {
- this._logService.trace(`[Copilot:${sessionId}] System message [${e.data.role}]: ${e.data.content.length} chars`);
- }));
- }
-
- // ---- cleanup ------------------------------------------------------------
-
- private _denyPendingPermissions(): void {
- for (const [, deferred] of this._pendingPermissions) {
- deferred.complete(false);
- }
- this._pendingPermissions.clear();
- }
-
- private _cancelPendingUserInputs(): void {
- for (const [, pending] of this._pendingUserInputs) {
- pending.deferred.complete({ response: SessionInputResponseKind.Cancel });
- }
- this._pendingUserInputs.clear();
- }
-}
diff --git a/src/vs/platform/agentHost/node/copilot/copilotGitProject.ts b/src/vs/platform/agentHost/node/copilot/copilotGitProject.ts
deleted file mode 100644
index c1e2d22f..00000000
--- a/src/vs/platform/agentHost/node/copilot/copilotGitProject.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import * as cp from 'child_process';
-import { Schemas } from '../../../../base/common/network.js';
-import { basename } from '../../../../base/common/path.js';
-import { URI } from '../../../../base/common/uri.js';
-import type { IAgentSessionProjectInfo } from '../../common/agentService.js';
-
-export interface ICopilotSessionContext {
- readonly cwd?: string;
- readonly gitRoot?: string;
- readonly repository?: string;
-}
-
-function execGit(cwd: string, args: string[]): Promise<string> {
- return new Promise((resolve, reject) => {
- cp.execFile('git', args, { cwd, encoding: 'utf8' }, (error, stdout) => {
- if (error) {
- reject(error);
- return;
- }
- resolve(stdout.trim());
- });
- });
-}
-
-export async function resolveGitProject(workingDirectory: URI | undefined): Promise<IAgentSessionProjectInfo | undefined> {
- if (!workingDirectory || workingDirectory.scheme !== Schemas.file) {
- return undefined;
- }
-
- const cwd = workingDirectory.fsPath;
- try {
- if ((await execGit(cwd, ['rev-parse', '--is-inside-work-tree'])) !== 'true') {
- return undefined;
- }
- } catch {
- return undefined;
- }
-
- let projectPath: string | undefined;
- try {
- const worktreeList = await execGit(cwd, ['worktree', 'list', '--porcelain']);
- projectPath = worktreeList.split(/\r?\n/).find(line => line.startsWith('worktree '))?.substring('worktree '.length);
- } catch {
- // Fall back to the current worktree root below.
- }
-
- if (!projectPath) {
- try {
- projectPath = await execGit(cwd, ['rev-parse', '--show-toplevel']);
- } catch {
- return undefined;
- }
- }
-
- const uri = URI.file(projectPath);
- return { uri, displayName: basename(uri.fsPath) || uri.toString() };
-}
-
-export function projectFromRepository(repository: string): IAgentSessionProjectInfo | undefined {
- const uri = repository.includes('://') ? URI.parse(repository) : URI.parse(`https://github.com/${repository}`);
- const rawDisplayName = basename(uri.path) || repository.split('/').filter(Boolean).pop() || repository;
- const displayName = rawDisplayName.endsWith('.git') ? rawDisplayName.slice(0, -'.git'.length) : rawDisplayName;
- return { uri, displayName };
-}
-
-export async function projectFromCopilotContext(context: ICopilotSessionContext | undefined): Promise<IAgentSessionProjectInfo | undefined> {
- const workingDirectory = typeof context?.cwd === 'string'
- ? URI.file(context.cwd)
- : typeof context?.gitRoot === 'string'
- ? URI.file(context.gitRoot)
- : undefined;
- const gitProject = await resolveGitProject(workingDirectory);
- if (gitProject) {
- return gitProject;
- }
-
- if (context?.repository) {
- return projectFromRepository(context.repository);
- }
-
- return undefined;
-}
diff --git a/src/vs/platform/agentHost/node/copilot/copilotPluginConverters.ts b/src/vs/platform/agentHost/node/copilot/copilotPluginConverters.ts
deleted file mode 100644
index c08d7e5f..00000000
--- a/src/vs/platform/agentHost/node/copilot/copilotPluginConverters.ts
+++ /dev/null
@@ -1,341 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import { spawn } from 'child_process';
-import type { CustomAgentConfig, MCPServerConfig, SessionConfig } from '@github/copilot-sdk';
-import { OperatingSystem, OS } from '../../../../base/common/platform.js';
-import { IFileService } from '../../../files/common/files.js';
-import { McpServerType } from '../../../mcp/common/mcpPlatformTypes.js';
-import type { IMcpServerDefinition, INamedPluginResource, IParsedHookCommand, IParsedHookGroup, IParsedPlugin } from '../../../agentPlugins/common/pluginParsers.js';
-import { dirname } from '../../../../base/common/path.js';
-
-type SessionHooks = NonNullable<SessionConfig['hooks']>;
-
-// ---------------------------------------------------------------------------
-// MCP servers
-// ---------------------------------------------------------------------------
-
-/**
- * Converts parsed MCP server definitions into the SDK's `mcpServers` config.
- */
-export function toSdkMcpServers(defs: readonly IMcpServerDefinition[]): Record<string, MCPServerConfig> {
- const result: Record<string, MCPServerConfig> = {};
- for (const def of defs) {
- const config = def.configuration;
- if (config.type === McpServerType.LOCAL) {
- result[def.name] = {
- type: 'local',
- command: config.command,
- args: config.args ? [...config.args] : [],
- tools: ['*'],
- ...(config.env && { env: toStringEnv(config.env) }),
- ...(config.cwd && { cwd: config.cwd }),
- };
- } else {
- result[def.name] = {
- type: 'http',
- url: config.url,
- tools: ['*'],
- ...(config.headers && { headers: { ...config.headers } }),
- };
- }
- }
- return result;
-}
-
-/**
- * Ensures all env values are strings (the SDK requires `Record<string, string>`).
- */
-function toStringEnv(env: Record<string, string | number | null>): Record<string, string> {
- const result: Record<string, string> = {};
- for (const [key, value] of Object.entries(env)) {
- if (value !== null) {
- result[key] = String(value);
- }
- }
- return result;
-}
-
-// ---------------------------------------------------------------------------
-// Custom agents
-// ---------------------------------------------------------------------------
-
-/**
- * Converts parsed plugin agents into the SDK's `customAgents` config.
- * Reads each agent's `.md` file to use as the prompt.
- */
-export async function toSdkCustomAgents(agents: readonly INamedPluginResource[], fileService: IFileService): Promise<CustomAgentConfig[]> {
- const configs: CustomAgentConfig[] = [];
- for (const agent of agents) {
- try {
- const content = await fileService.readFile(agent.uri);
- configs.push({
- name: agent.name,
- prompt: content.value.toString(),
- });
- } catch {
- // Skip agents whose file cannot be read
- }
- }
- return configs;
-}
-
-// ---------------------------------------------------------------------------
-// Skill directories
-// ---------------------------------------------------------------------------
-
-/**
- * Converts parsed plugin skills into the SDK's `skillDirectories` config.
- * The SDK expects directory paths; we extract the parent directory of each SKILL.md.
- */
-export function toSdkSkillDirectories(skills: readonly INamedPluginResource[]): string[] {
- const seen = new Set<string>();
- const result: string[] = [];
- for (const skill of skills) {
- // SKILL.md parent directory is the skill directory
- const dir = dirname(skill.uri.fsPath);
- if (!seen.has(dir)) {
- seen.add(dir);
- result.push(dir);
- }
- }
- return result;
-}
-
-// ---------------------------------------------------------------------------
-// Hooks
-// ---------------------------------------------------------------------------
-
-/**
- * Resolves the effective command for the current platform from a parsed hook command.
- */
-function resolveEffectiveCommand(hook: IParsedHookCommand, os: OperatingSystem): string | undefined {
- if (os === OperatingSystem.Windows && hook.windows) {
- return hook.windows;
- } else if (os === OperatingSystem.Macintosh && hook.osx) {
- return hook.osx;
- } else if (os === OperatingSystem.Linux && hook.linux) {
- return hook.linux;
- }
- return hook.command;
-}
-
-/**
- * Executes a hook command as a shell process. Returns the stdout on success,
- * or throws on non-zero exit code or timeout.
- */
-function executeHookCommand(hook: IParsedHookCommand, stdin?: string): Promise<string> {
- const command = resolveEffectiveCommand(hook, OS);
- if (!command) {
- return Promise.resolve('');
- }
-
- const timeout = (hook.timeout ?? 30) * 1000;
- const cwd = hook.cwd?.fsPath;
-
- return new Promise<string>((resolve, reject) => {
- const isWindows = OS === OperatingSystem.Windows;
- const shell = isWindows ? 'cmd.exe' : '/bin/sh';
- const shellArgs = isWindows ? ['/c', command] : ['-c', command];
-
- const child = spawn(shell, shellArgs, {
- cwd,
- env: { ...process.env, ...hook.env },
- stdio: ['pipe', 'pipe', 'pipe'],
- timeout,
- });
-
- let stdout = '';
- let stderr = '';
-
- child.stdout.on('data', (data: Buffer) => { stdout += data.toString(); });
- child.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
-
- if (stdin) {
- child.stdin.write(stdin);
- child.stdin.end();
- } else {
- child.stdin.end();
- }
-
- child.on('error', reject);
- child.on('close', (code) => {
- if (code === 0) {
- resolve(stdout);
- } else {
- reject(new Error(`Hook command exited with code ${code}: ${stderr || stdout}`));
- }
- });
- });
-}
-
-/**
- * Mapping from canonical hook type identifiers to SDK SessionHooks handler keys.
- */
-const HOOK_TYPE_TO_SDK_KEY: Record<string, keyof SessionHooks> = {
- 'PreToolUse': 'onPreToolUse',
- 'PostToolUse': 'onPostToolUse',
- 'UserPromptSubmit': 'onUserPromptSubmitted',
- 'SessionStart': 'onSessionStart',
- 'SessionEnd': 'onSessionEnd',
- 'ErrorOccurred': 'onErrorOccurred',
-};
-
-/**
- * Converts parsed plugin hooks into SDK {@link SessionHooks} handler functions.
- *
- * Each handler executes the hook's shell commands sequentially when invoked.
- * Hook types that don't map to SDK handler keys are silently ignored.
- *
- * The optional `editTrackingHooks` parameter provides internal edit-tracking
- * callbacks from {@link CopilotAgentSession} that are merged with plugin hooks.
- */
-export function toSdkHooks(
- hookGroups: readonly IParsedHookGroup[],
- editTrackingHooks?: {
- readonly onPreToolUse: (input: { toolName: string; toolArgs: unknown }) => Promise<void>;
- readonly onPostToolUse: (input: { toolName: string; toolArgs: unknown }) => Promise<void>;
- },
-): SessionHooks {
- // Group all commands by SDK handler key
- const commandsByKey = new Map<keyof SessionHooks, IParsedHookCommand[]>();
- for (const group of hookGroups) {
- const sdkKey = HOOK_TYPE_TO_SDK_KEY[group.type];
- if (!sdkKey) {
- continue;
- }
- const existing = commandsByKey.get(sdkKey) ?? [];
- existing.push(...group.commands);
- commandsByKey.set(sdkKey, existing);
- }
-
- const hooks: SessionHooks = {};
-
- // Pre-tool-use handler
- const preToolCommands = commandsByKey.get('onPreToolUse');
- if (preToolCommands?.length || editTrackingHooks) {
- hooks.onPreToolUse = async (input: { toolName: string; toolArgs: unknown }) => {
- await editTrackingHooks?.onPreToolUse(input);
- if (preToolCommands) {
- const stdin = JSON.stringify(input);
- for (const cmd of preToolCommands) {
- try {
- const output = await executeHookCommand(cmd, stdin);
- if (output.trim()) {
- try {
- const parsed = JSON.parse(output);
- if (parsed && typeof parsed === 'object') {
- return parsed;
- }
- } catch {
- // Non-JSON output is fine — no modification
- }
- }
- } catch {
- // Hook failures are non-fatal
- }
- }
- }
- };
- }
-
- // Post-tool-use handler
- const postToolCommands = commandsByKey.get('onPostToolUse');
- if (postToolCommands?.length || editTrackingHooks) {
- hooks.onPostToolUse = async (input: { toolName: string; toolArgs: unknown }) => {
- await editTrackingHooks?.onPostToolUse(input);
- if (postToolCommands) {
- const stdin = JSON.stringify(input);
- for (const cmd of postToolCommands) {
- try {
- await executeHookCommand(cmd, stdin);
- } catch {
- // Hook failures are non-fatal
- }
- }
- }
- };
- }
-
- // User-prompt-submitted handler
- const promptCommands = commandsByKey.get('onUserPromptSubmitted');
- if (promptCommands?.length) {
- hooks.onUserPromptSubmitted = async (input: { prompt: string }) => {
- const stdin = JSON.stringify(input);
- for (const cmd of promptCommands) {
- try {
- await executeHookCommand(cmd, stdin);
- } catch {
- // Hook failures are non-fatal
- }
- }
- };
- }
-
- // Session-start handler
- const startCommands = commandsByKey.get('onSessionStart');
- if (startCommands?.length) {
- hooks.onSessionStart = async (input: { source: string }) => {
- const stdin = JSON.stringify(input);
- for (const cmd of startCommands) {
- try {
- await executeHookCommand(cmd, stdin);
- } catch {
- // Hook failures are non-fatal
- }
- }
- };
- }
-
- // Session-end handler
- const endCommands = commandsByKey.get('onSessionEnd');
- if (endCommands?.length) {
- hooks.onSessionEnd = async (input: { reason: string }) => {
- const stdin = JSON.stringify(input);
- for (const cmd of endCommands) {
- try {
- await executeHookCommand(cmd, stdin);
- } catch {
- // Hook failures are non-fatal
- }
- }
- };
- }
-
- // Error-occurred handler
- const errorCommands = commandsByKey.get('onErrorOccurred');
- if (errorCommands?.length) {
- hooks.onErrorOccurred = async (input: { error: string }) => {
- const stdin = JSON.stringify(input);
- for (const cmd of errorCommands) {
- try {
- await executeHookCommand(cmd, stdin);
- } catch {
- // Hook failures are non-fatal
- }
- }
- };
- }
-
- return hooks;
-}
-
-/**
- * Checks whether two sets of parsed plugins produce equivalent SDK config.
- * Used to determine if a session needs to be refreshed.
- */
-export function parsedPluginsEqual(a: readonly IParsedPlugin[], b: readonly IParsedPlugin[]): boolean {
- // Simple structural comparison via JSON serialization.
- // We serialize only the essential fields, replacing URIs with strings.
- const serialize = (plugins: readonly IParsedPlugin[]) => {
- return JSON.stringify(plugins.map(p => ({
- hooks: p.hooks.map(h => ({ type: h.type, commands: h.commands.map(c => ({ command: c.command, windows: c.windows, linux: c.linux, osx: c.osx, cwd: c.cwd?.toString(), env: c.env, timeout: c.timeout })) })),
- mcpServers: p.mcpServers.map(m => ({ name: m.name, configuration: m.configuration })),
- skills: p.skills.map(s => ({ uri: s.uri.toString(), name: s.name })),
- agents: p.agents.map(a => ({ uri: a.uri.toString(), name: a.name })),
- })));
- };
- return serialize(a) === serialize(b);
-}
diff --git a/src/vs/platform/agentHost/node/copilot/copilotSessionWrapper.ts b/src/vs/platform/agentHost/node/copilot/copilotSessionWrapper.ts
deleted file mode 100644
index 36ad526d..00000000
--- a/src/vs/platform/agentHost/node/copilot/copilotSessionWrapper.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import { CopilotSession, SessionEventPayload, SessionEventType } from '@github/copilot-sdk';
-import { Emitter, Event } from '../../../../base/common/event.js';
-import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
-
-/**
- * Thin wrapper around {@link CopilotSession} that exposes each SDK event as a
- * proper VS Code `Event<T>`. All subscriptions and the underlying SDK session
- * are cleaned up on dispose.
- */
-export class CopilotSessionWrapper extends Disposable {
-
- constructor(readonly session: CopilotSession) {
- super();
- this._register(toDisposable(() => {
- session.destroy().catch(() => { /* best-effort */ });
- }));
- }
-
- get sessionId(): string { return this.session.sessionId; }
-
- private _onMessageDelta: Event<SessionEventPayload<'assistant.message_delta'>> | undefined;
- get onMessageDelta(): Event<SessionEventPayload<'assistant.message_delta'>> {
- return this._onMessageDelta ??= this._sdkEvent('assistant.message_delta');
- }
-
- private _onMessage: Event<SessionEventPayload<'assistant.message'>> | undefined;
- get onMessage(): Event<SessionEventPayload<'assistant.message'>> {
- return this._onMessage ??= this._sdkEvent('assistant.message');
- }
-
- private _onToolStart: Event<SessionEventPayload<'tool.execution_start'>> | undefined;
- get onToolStart(): Event<SessionEventPayload<'tool.execution_start'>> {
- return this._onToolStart ??= this._sdkEvent('tool.execution_start');
- }
-
- private _onToolComplete: Event<SessionEventPayload<'tool.execution_complete'>> | undefined;
- get onToolComplete(): Event<SessionEventPayload<'tool.execution_complete'>> {
- return this._onToolComplete ??= this._sdkEvent('tool.execution_complete');
- }
-
- private _onIdle: Event<SessionEventPayload<'session.idle'>> | undefined;
- get onIdle(): Event<SessionEventPayload<'session.idle'>> {
- return this._onIdle ??= this._sdkEvent('session.idle');
- }
-
- private _onSessionStart: Event<SessionEventPayload<'session.start'>> | undefined;
- get onSessionStart(): Event<SessionEventPayload<'session.start'>> {
- return this._onSessionStart ??= this._sdkEvent('session.start');
- }
-
- private _onSessionResume: Event<SessionEventPayload<'session.resume'>> | undefined;
- get onSessionResume(): Event<SessionEventPayload<'session.resume'>> {
- return this._onSessionResume ??= this._sdkEvent('session.resume');
- }
-
- private _onSessionError: Event<SessionEventPayload<'session.error'>> | undefined;
- get onSessionError(): Event<SessionEventPayload<'session.error'>> {
- return this._onSessionError ??= this._sdkEvent('session.error');
- }
-
- private _onSessionInfo: Event<SessionEventPayload<'session.info'>> | undefined;
- get onSessionInfo(): Event<SessionEventPayload<'session.info'>> {
- return this._onSessionInfo ??= this._sdkEvent('session.info');
- }
-
- private _onSessionModelChange: Event<SessionEventPayload<'session.model_change'>> | undefined;
- get onSessionModelChange(): Event<SessionEventPayload<'session.model_change'>> {
- return this._onSessionModelChange ??= this._sdkEvent('session.model_change');
- }
-
- private _onSessionHandoff: Event<SessionEventPayload<'session.handoff'>> | undefined;
- get onSessionHandoff(): Event<SessionEventPayload<'session.handoff'>> {
- return this._onSessionHandoff ??= this._sdkEvent('session.handoff');
- }
-
- private _onSessionTruncation: Event<SessionEventPayload<'session.truncation'>> | undefined;
- get onSessionTruncation(): Event<SessionEventPayload<'session.truncation'>> {
- return this._onSessionTruncation ??= this._sdkEvent('session.truncation');
- }
-
- private _onSessionSnapshotRewind: Event<SessionEventPayload<'session.snapshot_rewind'>> | undefined;
- get onSessionSnapshotRewind(): Event<SessionEventPayload<'session.snapshot_rewind'>> {
- return this._onSessionSnapshotRewind ??= this._sdkEvent('session.snapshot_rewind');
- }
-
- private _onSessionShutdown: Event<SessionEventPayload<'session.shutdown'>> | undefined;
- get onSessionShutdown(): Event<SessionEventPayload<'session.shutdown'>> {
- return this._onSessionShutdown ??= this._sdkEvent('session.shutdown');
- }
-
- private _onSessionUsageInfo: Event<SessionEventPayload<'session.usage_info'>> | undefined;
- get onSessionUsageInfo(): Event<SessionEventPayload<'session.usage_info'>> {
- return this._onSessionUsageInfo ??= this._sdkEvent('session.usage_info');
- }
-
- private _onSessionCompactionStart: Event<SessionEventPayload<'session.compaction_start'>> | undefined;
- get onSessionCompactionStart(): Event<SessionEventPayload<'session.compaction_start'>> {
- return this._onSessionCompactionStart ??= this._sdkEvent('session.compaction_start');
- }
-
- private _onSessionCompactionComplete: Event<SessionEventPayload<'session.compaction_complete'>> | undefined;
- get onSessionCompactionComplete(): Event<SessionEventPayload<'session.compaction_complete'>> {
- return this._onSessionCompactionComplete ??= this._sdkEvent('session.compaction_complete');
- }
-
- private _onUserMessage: Event<SessionEventPayload<'user.message'>> | undefined;
- get onUserMessage(): Event<SessionEventPayload<'user.message'>> {
- return this._onUserMessage ??= this._sdkEvent('user.message');
- }
-
- private _onPendingMessagesModified: Event<SessionEventPayload<'pending_messages.modified'>> | undefined;
- get onPendingMessagesModified(): Event<SessionEventPayload<'pending_messages.modified'>> {
- return this._onPendingMessagesModified ??= this._sdkEvent('pending_messages.modified');
- }
-
- private _onTurnStart: Event<SessionEventPayload<'assistant.turn_start'>> | undefined;
- get onTurnStart(): Event<SessionEventPayload<'assistant.turn_start'>> {
- return this._onTurnStart ??= this._sdkEvent('assistant.turn_start');
- }
-
- private _onIntent: Event<SessionEventPayload<'assistant.intent'>> | undefined;
- get onIntent(): Event<SessionEventPayload<'assistant.intent'>> {
- return this._onIntent ??= this._sdkEvent('assistant.intent');
- }
-
- private _onReasoning: Event<SessionEventPayload<'assistant.reasoning'>> | undefined;
- get onReasoning(): Event<SessionEventPayload<'assistant.reasoning'>> {
- return this._onReasoning ??= this._sdkEvent('assistant.reasoning');
- }
-
- private _onReasoningDelta: Event<SessionEventPayload<'assistant.reasoning_delta'>> | undefined;
- get onReasoningDelta(): Event<SessionEventPayload<'assistant.reasoning_delta'>> {
- return this._onReasoningDelta ??= this._sdkEvent('assistant.reasoning_delta');
- }
-
- private _onTurnEnd: Event<SessionEventPayload<'assistant.turn_end'>> | undefined;
- get onTurnEnd(): Event<SessionEventPayload<'assistant.turn_end'>> {
- return this._onTurnEnd ??= this._sdkEvent('assistant.turn_end');
- }
-
- private _onUsage: Event<SessionEventPayload<'assistant.usage'>> | undefined;
- get onUsage(): Event<SessionEventPayload<'assistant.usage'>> {
- return this._onUsage ??= this._sdkEvent('assistant.usage');
- }
-
- private _onAbort: Event<SessionEventPayload<'abort'>> | undefined;
- get onAbort(): Event<SessionEventPayload<'abort'>> {
- return this._onAbort ??= this._sdkEvent('abort');
- }
-
- private _onToolUserRequested: Event<SessionEventPayload<'tool.user_requested'>> | undefined;
- get onToolUserRequested(): Event<SessionEventPayload<'tool.user_requested'>> {
- return this._onToolUserRequested ??= this._sdkEvent('tool.user_requested');
- }
-
- private _onToolPartialResult: Event<SessionEventPayload<'tool.execution_partial_result'>> | undefined;
- get onToolPartialResult(): Event<SessionEventPayload<'tool.execution_partial_result'>> {
- return this._onToolPartialResult ??= this._sdkEvent('tool.execution_partial_result');
- }
-
- private _onToolProgress: Event<SessionEventPayload<'tool.execution_progress'>> | undefined;
- get onToolProgress(): Event<SessionEventPayload<'tool.execution_progress'>> {
- return this._onToolProgress ??= this._sdkEvent('tool.execution_progress');
- }
-
- private _onSkillInvoked: Event<SessionEventPayload<'skill.invoked'>> | undefined;
- get onSkillInvoked(): Event<SessionEventPayload<'skill.invoked'>> {
- return this._onSkillInvoked ??= this._sdkEvent('skill.invoked');
- }
-
- private _onSubagentStarted: Event<SessionEventPayload<'subagent.started'>> | undefined;
- get onSubagentStarted(): Event<SessionEventPayload<'subagent.started'>> {
- return this._onSubagentStarted ??= this._sdkEvent('subagent.started');
- }
-
- private _onSubagentCompleted: Event<SessionEventPayload<'subagent.completed'>> | undefined;
- get onSubagentCompleted(): Event<SessionEventPayload<'subagent.completed'>> {
- return this._onSubagentCompleted ??= this._sdkEvent('subagent.completed');
- }
-
- private _onSubagentFailed: Event<SessionEventPayload<'subagent.failed'>> | undefined;
- get onSubagentFailed(): Event<SessionEventPayload<'subagent.failed'>> {
- return this._onSubagentFailed ??= this._sdkEvent('subagent.failed');
- }
-
- private _onSubagentSelected: Event<SessionEventPayload<'subagent.selected'>> | undefined;
- get onSubagentSelected(): Event<SessionEventPayload<'subagent.selected'>> {
- return this._onSubagentSelected ??= this._sdkEvent('subagent.selected');
- }
-
- private _onHookStart: Event<SessionEventPayload<'hook.start'>> | undefined;
- get onHookStart(): Event<SessionEventPayload<'hook.start'>> {
- return this._onHookStart ??= this._sdkEvent('hook.start');
- }
-
- private _onHookEnd: Event<SessionEventPayload<'hook.end'>> | undefined;
- get onHookEnd(): Event<SessionEventPayload<'hook.end'>> {
- return this._onHookEnd ??= this._sdkEvent('hook.end');
- }
-
- private _onSystemMessage: Event<SessionEventPayload<'system.message'>> | undefined;
- get onSystemMessage(): Event<SessionEventPayload<'system.message'>> {
- return this._onSystemMessage ??= this._sdkEvent('system.message');
- }
-
- private _sdkEvent<K extends SessionEventType>(eventType: K): Event<SessionEventPayload<K>> {
- const emitter = this._register(new Emitter<SessionEventPayload<K>>());
- const unsubscribe = this.session.on(eventType, (data: SessionEventPayload<K>) => emitter.fire(data));
- this._register(toDisposable(unsubscribe));
- return emitter.event;
- }
-}
diff --git a/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts b/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts
deleted file mode 100644
index b40abea2..00000000
--- a/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts
+++ /dev/null
@@ -1,455 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import type { Tool, ToolResultObject } from '@github/copilot-sdk';
-import { generateUuid } from '../../../../base/common/uuid.js';
-import { URI } from '../../../../base/common/uri.js';
-import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
-import * as platform from '../../../../base/common/platform.js';
-import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
-import { ILogService } from '../../../log/common/log.js';
-import { TerminalClaimKind, type ITerminalSessionClaim } from '../../common/state/protocol/state.js';
-import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js';
-
-/**
- * Maximum scrollback content (in bytes) returned to the model in tool results.
- */
-const MAX_OUTPUT_BYTES = 80_000;
-
-/**
- * Default command timeout in milliseconds (120 seconds).
- */
-const DEFAULT_TIMEOUT_MS = 120_000;
-
-/**
- * The sentinel prefix used to detect command completion in terminal output.
- * The full sentinel format is: `<<<COPILOT_SENTINEL_<uuid>_EXIT_<code>>>`.
- */
-const SENTINEL_PREFIX = '<<<COPILOT_SENTINEL_';
-
-/**
- * Tracks a single persistent shell instance backed by a managed PTY terminal.
- */
-interface IManagedShell {
- readonly id: string;
- readonly terminalUri: string;
- readonly shellType: ShellType;
-}
-
-type ShellType = 'bash' | 'powershell';
-
-function getShellExecutable(shellType: ShellType): string {
- if (shellType === 'powershell') {
- return 'powershell.exe';
- }
- return process.env['SHELL'] || '/bin/bash';
-}
-
-// ---------------------------------------------------------------------------
-// ShellManager
-// ---------------------------------------------------------------------------
-
-/**
- * Per-session manager for persistent shell instances. Each shell is backed by
- * a {@link IAgentHostTerminalManager} terminal and participates in AHP terminal
- * claim semantics.
- *
- * Created via {@link IInstantiationService} once per session and disposed when
- * the session ends.
- */
-export class ShellManager {
-
- private readonly _shells = new Map<string, IManagedShell>();
- private readonly _toolCallShells = new Map<string, string>();
-
- constructor(
- private readonly _sessionUri: URI,
- @IAgentHostTerminalManager private readonly _terminalManager: IAgentHostTerminalManager,
- @ILogService private readonly _logService: ILogService,
- ) { }
-
- async getOrCreateShell(
- shellType: ShellType,
- turnId: string,
- toolCallId: string,
- cwd?: string,
- ): Promise<IManagedShell> {
- for (const shell of this._shells.values()) {
- if (shell.shellType === shellType && this._terminalManager.hasTerminal(shell.terminalUri)) {
- const exitCode = this._terminalManager.getExitCode(shell.terminalUri);
- if (exitCode === undefined) {
- this._trackToolCall(toolCallId, shell.id);
- return shell;
- }
- this._shells.delete(shell.id);
- }
- }
-
- const id = generateUuid();
- const terminalUri = `agenthost-terminal://shell/${id}`;
-
- const claim: ITerminalSessionClaim = {
- kind: TerminalClaimKind.Session,
- session: this._sessionUri.toString(),
- turnId,
- toolCallId,
- };
-
- const shellDisplayName = shellType === 'bash' ? 'Bash' : 'PowerShell';
-
- await this._terminalManager.createTerminal({
- terminal: terminalUri,
- claim,
- name: shellDisplayName,
- cwd,
- }, { shell: getShellExecutable(shellType) });
-
- const shell: IManagedShell = { id, terminalUri, shellType };
- this._shells.set(id, shell);
- this._trackToolCall(toolCallId, id);
- this._logService.info(`[ShellManager] Created ${shellType} shell ${id} (terminal=${terminalUri})`);
- return shell;
- }
-
- private _trackToolCall(toolCallId: string, shellId: string): void {
- this._toolCallShells.set(toolCallId, shellId);
- }
-
- getTerminalUriForToolCall(toolCallId: string): string | undefined {
- const shellId = this._toolCallShells.get(toolCallId);
- if (!shellId) {
- return undefined;
- }
- return this._shells.get(shellId)?.terminalUri;
- }
-
- getShell(id: string): IManagedShell | undefined {
- return this._shells.get(id);
- }
-
- listShells(): IManagedShell[] {
- const result: IManagedShell[] = [];
- for (const shell of this._shells.values()) {
- if (this._terminalManager.hasTerminal(shell.terminalUri)) {
- result.push(shell);
- }
- }
- return result;
- }
-
- shutdownShell(id: string): boolean {
- const shell = this._shells.get(id);
- if (!shell) {
- return false;
- }
- this._terminalManager.disposeTerminal(shell.terminalUri);
- this._shells.delete(id);
- this._logService.info(`[ShellManager] Shut down shell ${id}`);
- return true;
- }
-
- dispose(): void {
- for (const shell of this._shells.values()) {
- if (this._terminalManager.hasTerminal(shell.terminalUri)) {
- this._terminalManager.disposeTerminal(shell.terminalUri);
- }
- }
- this._shells.clear();
- this._toolCallShells.clear();
- }
-}
-
-// ---------------------------------------------------------------------------
-// Sentinel helpers
-// ---------------------------------------------------------------------------
-
-function makeSentinelId(): string {
- return generateUuid().replace(/-/g, '');
-}
-
-function buildSentinelCommand(sentinelId: string, shellType: ShellType): string {
- if (shellType === 'powershell') {
- return `Write-Output "${SENTINEL_PREFIX}${sentinelId}_EXIT_$LASTEXITCODE>>>"`;
- }
- return `echo "${SENTINEL_PREFIX}${sentinelId}_EXIT_$?>>>"`;
-}
-
-function parseSentinel(content: string, sentinelId: string): { found: boolean; exitCode: number; outputBeforeSentinel: string } {
- const marker = `${SENTINEL_PREFIX}${sentinelId}_EXIT_`;
- const idx = content.indexOf(marker);
- if (idx === -1) {
- return { found: false, exitCode: -1, outputBeforeSentinel: content };
- }
-
- const outputBeforeSentinel = content.substring(0, idx);
- const afterMarker = content.substring(idx + marker.length);
- const endIdx = afterMarker.indexOf('>>>');
- const exitCodeStr = endIdx >= 0 ? afterMarker.substring(0, endIdx) : afterMarker.trim();
- const exitCode = parseInt(exitCodeStr, 10);
- return {
- found: true,
- exitCode: isNaN(exitCode) ? -1 : exitCode,
- outputBeforeSentinel,
- };
-}
-
-function prepareOutputForModel(rawOutput: string): string {
- let text = removeAnsiEscapeCodes(rawOutput).trim();
- if (text.length > MAX_OUTPUT_BYTES) {
- text = text.substring(text.length - MAX_OUTPUT_BYTES);
- }
- return text;
-}
-
-// ---------------------------------------------------------------------------
-// Tool implementations
-// ---------------------------------------------------------------------------
-
-function makeSuccessResult(text: string): ToolResultObject {
- return { textResultForLlm: text, resultType: 'success' };
-}
-
-function makeFailureResult(text: string, error?: string): ToolResultObject {
- return { textResultForLlm: text, resultType: 'failure', error };
-}
-
-async function executeCommandInShell(
- shell: IManagedShell,
- command: string,
- timeoutMs: number,
- terminalManager: IAgentHostTerminalManager,
- logService: ILogService,
-): Promise<ToolResultObject> {
- const sentinelId = makeSentinelId();
- const sentinelCmd = buildSentinelCommand(sentinelId, shell.shellType);
- const disposables = new DisposableStore();
-
- const contentBefore = terminalManager.getContent(shell.terminalUri) ?? '';
- const offsetBefore = contentBefore.length;
-
- // PTY input uses \r for line endings — the PTY translates to \r\n
- const input = `${command}\r${sentinelCmd}\r`;
- terminalManager.writeInput(shell.terminalUri, input);
-
- return new Promise<ToolResultObject>(resolve => {
- let resolved = false;
- const finish = (result: ToolResultObject) => {
- if (resolved) {
- return;
- }
- resolved = true;
- disposables.dispose();
- resolve(result);
- };
-
- const checkForSentinel = () => {
- const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';
- // Clamp offset: the terminal manager trims content when it exceeds
- // 100k chars (slices to last 80k). If trimming happened after we
- // captured offsetBefore, scan from the start of the current buffer.
- const clampedOffset = Math.min(offsetBefore, fullContent.length);
- const newContent = fullContent.substring(clampedOffset);
- const parsed = parseSentinel(newContent, sentinelId);
- if (parsed.found) {
- const output = prepareOutputForModel(parsed.outputBeforeSentinel);
- logService.info(`[ShellTool] Command completed with exit code ${parsed.exitCode}`);
- if (parsed.exitCode === 0) {
- finish(makeSuccessResult(`Exit code: ${parsed.exitCode}\n${output}`));
- } else {
- finish(makeFailureResult(`Exit code: ${parsed.exitCode}\n${output}`));
- }
- }
- };
-
- disposables.add(terminalManager.onData(shell.terminalUri, () => {
- checkForSentinel();
- }));
-
- disposables.add(terminalManager.onExit(shell.terminalUri, (exitCode: number) => {
- logService.info(`[ShellTool] Shell exited unexpectedly with code ${exitCode}`);
- const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';
- const newContent = fullContent.substring(offsetBefore);
- const output = prepareOutputForModel(newContent);
- finish(makeFailureResult(`Shell exited with code ${exitCode}\n${output}`));
- }));
-
- disposables.add(terminalManager.onClaimChanged(shell.terminalUri, (claim) => {
- if (claim.kind === TerminalClaimKind.Session && !claim.toolCallId) {
- logService.info(`[ShellTool] Continuing in background (claim narrowed)`);
- finish(makeSuccessResult('The user chose to continue this command in the background. The terminal is still running.'));
- }
- }));
-
- const timer = setTimeout(() => {
- logService.warn(`[ShellTool] Command timed out after ${timeoutMs}ms`);
- const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';
- const newContent = fullContent.substring(offsetBefore);
- const output = prepareOutputForModel(newContent);
- finish(makeFailureResult(
- `Command timed out after ${Math.round(timeoutMs / 1000)}s. Partial output:\n${output}`,
- 'timeout',
- ));
- }, timeoutMs);
- disposables.add(toDisposable(() => clearTimeout(timer)));
-
- checkForSentinel();
- });
-}
-
-// ---------------------------------------------------------------------------
-// Public factory
-// ---------------------------------------------------------------------------
-
-interface IShellToolArgs {
- command: string;
- timeout?: number;
-}
-
-interface IWriteShellArgs {
- command: string;
-}
-
-interface IReadShellArgs {
- shell_id?: string;
-}
-
-interface IShutdownShellArgs {
- shell_id?: string;
-}
-
-/**
- * Creates the set of SDK {@link Tool} definitions that override the built-in
- * Copilot CLI shell tools with PTY-backed implementations.
- *
- * Returns tools for the platform-appropriate shell (bash or powershell),
- * including companion tools (read, write, shutdown, list).
- */
-export function createShellTools(
- shellManager: ShellManager,
- terminalManager: IAgentHostTerminalManager,
- logService: ILogService,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
-): Tool<any>[] {
- const shellType: ShellType = platform.isWindows ? 'powershell' : 'bash';
-
- const primaryTool: Tool<IShellToolArgs> = {
- name: shellType,
- description: `Execute a command in a persistent ${shellType} shell. The shell is reused across calls.`,
- parameters: {
- type: 'object',
- properties: {
- command: { type: 'string', description: 'The command to execute' },
- timeout: { type: 'number', description: 'Timeout in milliseconds (default 120000)' },
- },
- required: ['command'],
- },
- overridesBuiltInTool: true,
- handler: async (args, invocation) => {
- const shell = await shellManager.getOrCreateShell(
- shellType,
- invocation.toolCallId,
- invocation.toolCallId,
- );
- const timeoutMs = args.timeout ?? DEFAULT_TIMEOUT_MS;
- return executeCommandInShell(shell, args.command, timeoutMs, terminalManager, logService);
- },
- };
-
- const readTool: Tool<IReadShellArgs> = {
- name: `read_${shellType}`,
- description: `Read the latest output from a running ${shellType} shell.`,
- parameters: {
- type: 'object',
- properties: {
- shell_id: { type: 'string', description: 'Shell ID to read from (optional; uses latest shell if omitted)' },
- },
- },
- overridesBuiltInTool: true,
- handler: (args) => {
- const shells = shellManager.listShells();
- const shell = args.shell_id
- ? shellManager.getShell(args.shell_id)
- : shells[shells.length - 1];
- if (!shell) {
- return makeFailureResult('No active shell found.', 'no_shell');
- }
- const content = terminalManager.getContent(shell.terminalUri);
- if (!content) {
- return makeSuccessResult('(no output)');
- }
- return makeSuccessResult(prepareOutputForModel(content));
- },
- };
-
- const writeTool: Tool<IWriteShellArgs> = {
- name: `write_${shellType}`,
- description: `Send input to a running ${shellType} shell (e.g. answering a prompt, sending Ctrl+C).`,
- parameters: {
- type: 'object',
- properties: {
- command: { type: 'string', description: 'Text to write to the shell stdin' },
- },
- required: ['command'],
- },
- overridesBuiltInTool: true,
- handler: (args) => {
- const shells = shellManager.listShells();
- const shell = shells[shells.length - 1];
- if (!shell) {
- return makeFailureResult('No active shell found.', 'no_shell');
- }
- terminalManager.writeInput(shell.terminalUri, args.command);
- return makeSuccessResult('Input sent to shell.');
- },
- };
-
- const shutdownTool: Tool<IShutdownShellArgs> = {
- name: shellType === 'bash' ? 'bash_shutdown' : `${shellType}_shutdown`,
- description: `Stop a ${shellType} shell.`,
- parameters: {
- type: 'object',
- properties: {
- shell_id: { type: 'string', description: 'Shell ID to stop (optional; stops latest shell if omitted)' },
- },
- },
- overridesBuiltInTool: true,
- handler: (args) => {
- if (args.shell_id) {
- const success = shellManager.shutdownShell(args.shell_id);
- return success
- ? makeSuccessResult('Shell stopped.')
- : makeFailureResult('Shell not found.', 'not_found');
- }
- const shells = shellManager.listShells();
- const shell = shells[shells.length - 1];
- if (!shell) {
- return makeFailureResult('No active shell to stop.', 'no_shell');
- }
- shellManager.shutdownShell(shell.id);
- return makeSuccessResult('Shell stopped.');
- },
- };
-
- const listTool: Tool<Record<string, never>> = {
- name: `list_${shellType}`,
- description: `List active ${shellType} shell instances.`,
- parameters: { type: 'object', properties: {} },
- overridesBuiltInTool: true,
- handler: () => {
- const shells = shellManager.listShells();
- if (shells.length === 0) {
- return makeSuccessResult('No active shells.');
- }
- const descriptions = shells.map(s => {
- const exitCode = terminalManager.getExitCode(s.terminalUri);
- const status = exitCode !== undefined ? `exited (${exitCode})` : 'running';
- return `- ${s.id}: ${s.shellType} [${status}]`;
- });
- return makeSuccessResult(descriptions.join('\n'));
- },
- };
-
- return [primaryTool, readTool, writeTool, shutdownTool, listTool];
-}
diff --git a/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts b/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts
index 903158d8..31c74bc2 100644
--- a/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts
+++ b/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts
@@ -40,4 +40,4 @@ export interface ISessionEventToolComplete {
success: boolean;
- result?: { content?: string };
- error?: { message: string; code?: string };
+ result?: { content?: string; };
+ error?: { message: string; code?: string; };
isUserRequested?: boolean;
@@ -54,3 +54,3 @@ export interface ISessionEventMessage {
content?: string;
- toolRequests?: readonly { toolCallId: string; name: string; arguments?: unknown; type?: 'function' | 'custom' }[];
+ toolRequests?: readonly { toolCallId: string; name: string; arguments?: unknown; type?: 'function' | 'custom'; }[];
reasoningOpaque?: string;
@@ -63,3 +63,3 @@ export interface ISessionEventMessage {
/** Minimal event shape for session history mapping. */
-export type ISessionEvent = ISessionEventToolStart | ISessionEventToolComplete | ISessionEventMessage | ISessionEventSubagentStarted | { type: string; data?: unknown };
+export type ISessionEvent = ISessionEventToolStart | ISessionEventToolComplete | ISessionEventMessage | ISessionEventSubagentStarted | { type: string; data?: unknown; };
@@ -88,3 +88,3 @@ export async function mapSessionEvents(
const result: (IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[] = [];
- const toolInfoByCallId = new Map<string, { toolName: string; parameters: Record<string, unknown> | undefined }>();
+ const toolInfoByCallId = new Map<string, { toolName: string; parameters: Record<string, unknown> | undefined; }>();
diff --git a/src/vs/platform/agentHost/test/node/agentService.test.ts b/src/vs/platform/agentHost/test/node/agentService.test.ts
index 8533a728..ca1195b3 100644
--- a/src/vs/platform/agentHost/test/node/agentService.test.ts
+++ b/src/vs/platform/agentHost/test/node/agentService.test.ts
@@ -202,3 +202,3 @@ suite('AgentService (node dispatcher)', () => {
// Manually add the session to the mock
- (agent as unknown as { _sessions: Map<string, URI> })._sessions.set(sessionId, sessionUri);
+ (agent as unknown as { _sessions: Map<string, URI>; })._sessions.set(sessionId, sessionUri);
diff --git a/src/vs/platform/agentHost/test/node/copilotAgentForking.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentForking.test.ts
deleted file mode 100644
index d5577754..00000000
--- a/src/vs/platform/agentHost/test/node/copilotAgentForking.test.ts
+++ /dev/null
@@ -1,705 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import assert from 'assert';
-import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
-import {
- parseEventLog,
- serializeEventLog,
- findTurnBoundaryInEventLog,
- buildForkedEventLog,
- buildTruncatedEventLog,
- buildWorkspaceYaml,
- forkSessionInDb,
- truncateSessionInDb,
- type ICopilotEventLogEntry,
-} from '../../node/copilot/copilotAgentForking.js';
-
-// ---- Test helpers -----------------------------------------------------------
-
-function makeEntry(type: string, overrides?: Partial<ICopilotEventLogEntry>): ICopilotEventLogEntry {
- return {
- type,
- data: {},
- id: `id-${Math.random().toString(36).slice(2, 8)}`,
- timestamp: new Date().toISOString(),
- parentId: null,
- ...overrides,
- };
-}
-
-/**
- * Builds a minimal event log representing a multi-turn session.
- * Each turn = user.message → assistant.turn_start → assistant.message → assistant.turn_end.
- */
-function buildTestEventLog(turnCount: number): ICopilotEventLogEntry[] {
- const entries: ICopilotEventLogEntry[] = [];
- let lastId: string | null = null;
-
- const sessionStart = makeEntry('session.start', {
- id: 'session-start-id',
- data: { sessionId: 'source-session', context: { cwd: '/test' } },
- parentId: null,
- });
- entries.push(sessionStart);
- lastId = sessionStart.id;
-
- for (let turn = 0; turn < turnCount; turn++) {
- const userMsg = makeEntry('user.message', {
- id: `user-msg-${turn}`,
- data: { content: `Turn ${turn} message` },
- parentId: lastId,
- });
- entries.push(userMsg);
- lastId = userMsg.id;
-
- const turnStart = makeEntry('assistant.turn_start', {
- id: `turn-start-${turn}`,
- data: { turnId: String(turn) },
- parentId: lastId,
- });
- entries.push(turnStart);
- lastId = turnStart.id;
-
- const assistantMsg = makeEntry('assistant.message', {
- id: `assistant-msg-${turn}`,
- data: { content: `Response ${turn}` },
- parentId: lastId,
- });
- entries.push(assistantMsg);
- lastId = assistantMsg.id;
-
- const turnEnd = makeEntry('assistant.turn_end', {
- id: `turn-end-${turn}`,
- parentId: lastId,
- });
- entries.push(turnEnd);
- lastId = turnEnd.id;
- }
-
- return entries;
-}
-
-suite('CopilotAgentForking', () => {
-
- ensureNoDisposablesAreLeakedInTestSuite();
-
- // ---- parseEventLog / serializeEventLog ------------------------------
-
- suite('parseEventLog', () => {
-
- test('parses a single-line JSONL', () => {
- const entry = makeEntry('session.start');
- const jsonl = JSON.stringify(entry);
- const result = parseEventLog(jsonl);
- assert.strictEqual(result.length, 1);
- assert.strictEqual(result[0].type, 'session.start');
- });
-
- test('parses multi-line JSONL', () => {
- const entries = [makeEntry('session.start'), makeEntry('user.message')];
- const jsonl = entries.map(e => JSON.stringify(e)).join('\n');
- const result = parseEventLog(jsonl);
- assert.strictEqual(result.length, 2);
- });
-
- test('ignores empty lines', () => {
- const entry = makeEntry('session.start');
- const jsonl = '\n' + JSON.stringify(entry) + '\n\n';
- const result = parseEventLog(jsonl);
- assert.strictEqual(result.length, 1);
- });
-
- test('empty input returns empty array', () => {
- assert.deepStrictEqual(parseEventLog(''), []);
- assert.deepStrictEqual(parseEventLog('\n\n'), []);
- });
- });
-
- suite('serializeEventLog', () => {
-
- test('round-trips correctly', () => {
- const entries = buildTestEventLog(2);
- const serialized = serializeEventLog(entries);
- const parsed = parseEventLog(serialized);
- assert.strictEqual(parsed.length, entries.length);
- for (let i = 0; i < entries.length; i++) {
- assert.strictEqual(parsed[i].id, entries[i].id);
- assert.strictEqual(parsed[i].type, entries[i].type);
- }
- });
-
- test('ends with a newline', () => {
- const entries = [makeEntry('session.start')];
- const serialized = serializeEventLog(entries);
- assert.ok(serialized.endsWith('\n'));
- });
- });
-
- // ---- findTurnBoundaryInEventLog -------------------------------------
-
- suite('findTurnBoundaryInEventLog', () => {
-
- test('finds first turn boundary', () => {
- const entries = buildTestEventLog(3);
- const boundary = findTurnBoundaryInEventLog(entries, 0);
- // Turn 0: user.message(1) + turn_start(2) + assistant.message(3) + turn_end(4)
- // Index 4 = turn_end of turn 0
- assert.strictEqual(boundary, 4);
- assert.strictEqual(entries[boundary].type, 'assistant.turn_end');
- assert.strictEqual(entries[boundary].id, 'turn-end-0');
- });
-
- test('finds middle turn boundary', () => {
- const entries = buildTestEventLog(3);
- const boundary = findTurnBoundaryInEventLog(entries, 1);
- // Turn 1 ends at index 8
- assert.strictEqual(boundary, 8);
- assert.strictEqual(entries[boundary].type, 'assistant.turn_end');
- assert.strictEqual(entries[boundary].id, 'turn-end-1');
- });
-
- test('finds last turn boundary', () => {
- const entries = buildTestEventLog(3);
- const boundary = findTurnBoundaryInEventLog(entries, 2);
- assert.strictEqual(boundary, entries.length - 1);
- assert.strictEqual(entries[boundary].type, 'assistant.turn_end');
- assert.strictEqual(entries[boundary].id, 'turn-end-2');
- });
-
- test('returns -1 for non-existent turn', () => {
- const entries = buildTestEventLog(2);
- assert.strictEqual(findTurnBoundaryInEventLog(entries, 5), -1);
- });
-
- test('returns -1 for empty log', () => {
- assert.strictEqual(findTurnBoundaryInEventLog([], 0), -1);
- });
- });
-
- // ---- buildForkedEventLog --------------------------------------------
-
- suite('buildForkedEventLog', () => {
-
- test('forks at turn 0', () => {
- const entries = buildTestEventLog(3);
- const forked = buildForkedEventLog(entries, 0, 'new-session-id');
-
- // Should have session.start + turn 0 events (user.message, turn_start, assistant.message, turn_end)
- assert.strictEqual(forked.length, 5);
- assert.strictEqual(forked[0].type, 'session.start');
- assert.strictEqual((forked[0].data as Record<string, unknown>).sessionId, 'new-session-id');
- });
-
- test('forks at turn 1', () => {
- const entries = buildTestEventLog(3);
- const forked = buildForkedEventLog(entries, 1, 'new-session-id');
-
- // session.start + 2 turns × 4 events = 9 events
- assert.strictEqual(forked.length, 9);
- });
-
- test('generates unique UUIDs', () => {
- const entries = buildTestEventLog(2);
- const forked = buildForkedEventLog(entries, 0, 'new-session-id');
-
- const ids = new Set(forked.map(e => e.id));
- assert.strictEqual(ids.size, forked.length, 'All IDs should be unique');
-
- // None should match the original
- for (const entry of forked) {
- assert.ok(!entries.some(e => e.id === entry.id), 'Should not reuse original IDs');
- }
- });
-
- test('re-chains parentId links', () => {
- const entries = buildTestEventLog(2);
- const forked = buildForkedEventLog(entries, 0, 'new-session-id');
-
- // First event has no parent
- assert.strictEqual(forked[0].parentId, null);
-
- // Each subsequent event's parentId should be a valid ID in the forked log
- const idSet = new Set(forked.map(e => e.id));
- for (let i = 1; i < forked.length; i++) {
- assert.ok(
- forked[i].parentId !== null && idSet.has(forked[i].parentId!),
- `Event ${i} (${forked[i].type}) should have a valid parentId`,
- );
- }
- });
-
- test('strips session.shutdown and session.resume events', () => {
- const entries = buildTestEventLog(2);
- // Insert lifecycle events
- entries.splice(5, 0, makeEntry('session.shutdown', { parentId: entries[4].id }));
- entries.splice(6, 0, makeEntry('session.resume', { parentId: entries[5].id }));
-
- const forked = buildForkedEventLog(entries, 1, 'new-session-id');
- assert.ok(!forked.some(e => e.type === 'session.shutdown'));
- assert.ok(!forked.some(e => e.type === 'session.resume'));
- });
-
- test('throws for invalid turn index', () => {
- const entries = buildTestEventLog(1);
- assert.throws(() => buildForkedEventLog(entries, 5, 'new-session-id'));
- });
-
- test('falls back to last kept event when lifecycle event parent is stripped', () => {
- const entries = buildTestEventLog(2);
- // Insert shutdown event between turns, then make the next turn's
- // user.message point to the shutdown event (which will be stripped)
- const shutdownEntry = makeEntry('session.shutdown', {
- id: 'shutdown-1',
- parentId: entries[4].id, // turn-end-0
- });
- entries.splice(5, 0, shutdownEntry);
- // Next entry (user-msg-1) now points to the shutdown event
- entries[6] = { ...entries[6], parentId: 'shutdown-1' };
-
- const forked = buildForkedEventLog(entries, 1, 'new-session-id');
-
- // All parentIds should be valid
- const idSet = new Set(forked.map(e => e.id));
- for (let i = 1; i < forked.length; i++) {
- assert.ok(
- forked[i].parentId !== null && idSet.has(forked[i].parentId!),
- `Event ${i} (${forked[i].type}) should have a valid parentId, got ${forked[i].parentId}`,
- );
- }
- });
- });
-
- // ---- buildTruncatedEventLog -----------------------------------------
-
- suite('buildTruncatedEventLog', () => {
-
- test('truncates to turn 0', () => {
- const entries = buildTestEventLog(3);
- const truncated = buildTruncatedEventLog(entries, 0);
-
- // New session.start + turn 0 events (user.message, turn_start, assistant.message, turn_end)
- assert.strictEqual(truncated.length, 5);
- assert.strictEqual(truncated[0].type, 'session.start');
- });
-
- test('truncates to turn 1', () => {
- const entries = buildTestEventLog(3);
- const truncated = buildTruncatedEventLog(entries, 1);
-
- // New session.start + 2 turns × 4 events = 9 events
- assert.strictEqual(truncated.length, 9);
- });
-
- test('prepends fresh session.start', () => {
- const entries = buildTestEventLog(2);
- const truncated = buildTruncatedEventLog(entries, 0);
-
- assert.strictEqual(truncated[0].type, 'session.start');
- assert.strictEqual(truncated[0].parentId, null);
- // Should not reuse original session.start ID
- assert.notStrictEqual(truncated[0].id, entries[0].id);
- });
-
- test('re-chains parentId links', () => {
- const entries = buildTestEventLog(2);
- const truncated = buildTruncatedEventLog(entries, 0);
-
- const idSet = new Set(truncated.map(e => e.id));
- for (let i = 1; i < truncated.length; i++) {
- assert.ok(
- truncated[i].parentId !== null && idSet.has(truncated[i].parentId!),
- `Event ${i} (${truncated[i].type}) should have a valid parentId`,
- );
- }
- });
-
- test('strips lifecycle events', () => {
- const entries = buildTestEventLog(3);
- // Add lifecycle events between turns
- entries.splice(5, 0, makeEntry('session.shutdown'));
- entries.splice(6, 0, makeEntry('session.resume'));
-
- const truncated = buildTruncatedEventLog(entries, 2);
- const lifecycleEvents = truncated.filter(
- e => e.type === 'session.shutdown' || e.type === 'session.resume',
- );
- assert.strictEqual(lifecycleEvents.length, 0);
- });
-
- test('throws for invalid turn index', () => {
- const entries = buildTestEventLog(1);
- assert.throws(() => buildTruncatedEventLog(entries, 5));
- });
-
- test('throws when no session.start exists', () => {
- const entries = [makeEntry('user.message')];
- assert.throws(() => buildTruncatedEventLog(entries, 0));
- });
- });
-
- // ---- buildWorkspaceYaml ---------------------------------------------
-
- suite('buildWorkspaceYaml', () => {
-
- test('contains required fields', () => {
- const yaml = buildWorkspaceYaml('test-id', '/home/user/project', 'Test summary');
- assert.ok(yaml.includes('id: test-id'));
- assert.ok(yaml.includes('cwd: /home/user/project'));
- assert.ok(yaml.includes('summary: Test summary'));
- assert.ok(yaml.includes('summary_count: 0'));
- assert.ok(yaml.includes('created_at:'));
- assert.ok(yaml.includes('updated_at:'));
- });
- });
-
- // ---- SQLite operations (in-memory) ----------------------------------
-
- suite('forkSessionInDb', () => {
-
- async function openTestDb(): Promise<import('@vscode/sqlite3').Database> {
- const sqlite3 = await import('@vscode/sqlite3');
- return new Promise((resolve, reject) => {
- const db = new sqlite3.default.Database(':memory:', (err: Error | null) => {
- if (err) {
- return reject(err);
- }
- resolve(db);
- });
- });
- }
-
- function exec(db: import('@vscode/sqlite3').Database, sql: string): Promise<void> {
- return new Promise((resolve, reject) => {
- db.exec(sql, err => err ? reject(err) : resolve());
- });
- }
-
- function all(db: import('@vscode/sqlite3').Database, sql: string, params: unknown[] = []): Promise<Record<string, unknown>[]> {
- return new Promise((resolve, reject) => {
- db.all(sql, params, (err: Error | null, rows: Record<string, unknown>[]) => {
- if (err) {
- return reject(err);
- }
- resolve(rows);
- });
- });
- }
-
- function close(db: import('@vscode/sqlite3').Database): Promise<void> {
- return new Promise((resolve, reject) => {
- db.close(err => err ? reject(err) : resolve());
- });
- }
-
- async function setupSchema(db: import('@vscode/sqlite3').Database): Promise<void> {
- await exec(db, `
- CREATE TABLE sessions (
- id TEXT PRIMARY KEY,
- cwd TEXT,
- repository TEXT,
- branch TEXT,
- summary TEXT,
- created_at TEXT,
- updated_at TEXT,
- host_type TEXT
- );
- CREATE TABLE turns (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- session_id TEXT NOT NULL,
- turn_index INTEGER NOT NULL,
- user_message TEXT,
- assistant_response TEXT,
- timestamp TEXT,
- UNIQUE(session_id, turn_index)
- );
- CREATE TABLE session_files (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- session_id TEXT NOT NULL,
- file_path TEXT,
- tool_name TEXT,
- turn_index INTEGER,
- first_seen_at TEXT
- );
- CREATE VIRTUAL TABLE search_index USING fts5(
- content,
- session_id,
- source_type,
- source_id
- );
- CREATE TABLE checkpoints (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- session_id TEXT NOT NULL,
- checkpoint_number INTEGER,
- title TEXT,
- overview TEXT,
- history TEXT,
- work_done TEXT,
- technical_details TEXT,
- important_files TEXT,
- next_steps TEXT,
- created_at TEXT
- );
- `);
- }
-
- async function seedTestData(db: import('@vscode/sqlite3').Database, sessionId: string, turnCount: number): Promise<void> {
- await exec(db, `
- INSERT INTO sessions (id, cwd, repository, branch, summary, created_at, updated_at, host_type)
- VALUES ('${sessionId}', '/test', 'test-repo', 'main', 'Test session', '2026-01-01', '2026-01-01', 'github');
- `);
- for (let i = 0; i < turnCount; i++) {
- await exec(db, `
- INSERT INTO turns (session_id, turn_index, user_message, assistant_response, timestamp)
- VALUES ('${sessionId}', ${i}, 'msg ${i}', 'resp ${i}', '2026-01-01');
- `);
- await exec(db, `
- INSERT INTO session_files (session_id, file_path, tool_name, turn_index, first_seen_at)
- VALUES ('${sessionId}', 'file${i}.ts', 'edit', ${i}, '2026-01-01');
- `);
- }
- }
-
- test('copies session metadata', async () => {
- const db = await openTestDb();
- try {
- await setupSchema(db);
- await seedTestData(db, 'source', 3);
-
- await forkSessionInDb(db, 'source', 'forked', 1);
-
- const sessions = await all(db, 'SELECT * FROM sessions WHERE id = ?', ['forked']);
- assert.strictEqual(sessions.length, 1);
- assert.strictEqual(sessions[0].cwd, '/test');
- assert.strictEqual(sessions[0].repository, 'test-repo');
- } finally {
- await close(db);
- }
- });
-
- test('copies turns up to fork point', async () => {
- const db = await openTestDb();
- try {
- await setupSchema(db);
- await seedTestData(db, 'source', 3);
-
- await forkSessionInDb(db, 'source', 'forked', 1);
-
- const turns = await all(db, 'SELECT * FROM turns WHERE session_id = ? ORDER BY turn_index', ['forked']);
- assert.strictEqual(turns.length, 2); // turns 0 and 1
- assert.strictEqual(turns[0].turn_index, 0);
- assert.strictEqual(turns[1].turn_index, 1);
- } finally {
- await close(db);
- }
- });
-
- test('copies session files up to fork point', async () => {
- const db = await openTestDb();
- try {
- await setupSchema(db);
- await seedTestData(db, 'source', 3);
-
- await forkSessionInDb(db, 'source', 'forked', 1);
-
- const files = await all(db, 'SELECT * FROM session_files WHERE session_id = ?', ['forked']);
- assert.strictEqual(files.length, 2); // files from turns 0 and 1
- } finally {
- await close(db);
- }
- });
-
- test('does not affect source session', async () => {
- const db = await openTestDb();
- try {
- await setupSchema(db);
- await seedTestData(db, 'source', 3);
-
- await forkSessionInDb(db, 'source', 'forked', 1);
-
- const sourceTurns = await all(db, 'SELECT * FROM turns WHERE session_id = ?', ['source']);
- assert.strictEqual(sourceTurns.length, 3);
- } finally {
- await close(db);
- }
- });
- });
-
- suite('truncateSessionInDb', () => {
-
- async function openTestDb(): Promise<import('@vscode/sqlite3').Database> {
- const sqlite3 = await import('@vscode/sqlite3');
- return new Promise((resolve, reject) => {
- const db = new sqlite3.default.Database(':memory:', (err: Error | null) => {
- if (err) {
- return reject(err);
- }
- resolve(db);
- });
- });
- }
-
- function exec(db: import('@vscode/sqlite3').Database, sql: string): Promise<void> {
- return new Promise((resolve, reject) => {
- db.exec(sql, err => err ? reject(err) : resolve());
- });
- }
-
- function all(db: import('@vscode/sqlite3').Database, sql: string, params: unknown[] = []): Promise<Record<string, unknown>[]> {
- return new Promise((resolve, reject) => {
- db.all(sql, params, (err: Error | null, rows: Record<string, unknown>[]) => {
- if (err) {
- return reject(err);
- }
- resolve(rows);
- });
- });
- }
-
- function close(db: import('@vscode/sqlite3').Database): Promise<void> {
- return new Promise((resolve, reject) => {
- db.close(err => err ? reject(err) : resolve());
- });
- }
-
- async function setupSchema(db: import('@vscode/sqlite3').Database): Promise<void> {
- await exec(db, `
- CREATE TABLE sessions (
- id TEXT PRIMARY KEY,
- cwd TEXT,
- repository TEXT,
- branch TEXT,
- summary TEXT,
- created_at TEXT,
- updated_at TEXT,
- host_type TEXT
- );
- CREATE TABLE turns (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- session_id TEXT NOT NULL,
- turn_index INTEGER NOT NULL,
- user_message TEXT,
- assistant_response TEXT,
- timestamp TEXT,
- UNIQUE(session_id, turn_index)
- );
- CREATE VIRTUAL TABLE search_index USING fts5(
- content,
- session_id,
- source_type,
- source_id
- );
- `);
- }
-
- test('removes turns after truncation point', async () => {
- const db = await openTestDb();
- try {
- await setupSchema(db);
- await exec(db, `
- INSERT INTO sessions (id, cwd, summary, created_at, updated_at)
- VALUES ('sess', '/test', 'Test', '2026-01-01', '2026-01-01');
- `);
- for (let i = 0; i < 5; i++) {
- await exec(db, `
- INSERT INTO turns (session_id, turn_index, user_message, timestamp)
- VALUES ('sess', ${i}, 'msg ${i}', '2026-01-01');
- `);
- }
-
- await truncateSessionInDb(db, 'sess', 2);
-
- const turns = await all(db, 'SELECT * FROM turns WHERE session_id = ? ORDER BY turn_index', ['sess']);
- assert.strictEqual(turns.length, 3); // turns 0, 1, 2
- assert.strictEqual(turns[0].turn_index, 0);
- assert.strictEqual(turns[2].turn_index, 2);
- } finally {
- await close(db);
- }
- });
-
- test('updates session timestamp', async () => {
- const db = await openTestDb();
- try {
- await setupSchema(db);
- await exec(db, `
- INSERT INTO sessions (id, cwd, summary, created_at, updated_at)
- VALUES ('sess', '/test', 'Test', '2026-01-01', '2026-01-01');
- `);
- await exec(db, `
- INSERT INTO turns (session_id, turn_index, user_message, timestamp)
- VALUES ('sess', 0, 'msg 0', '2026-01-01');
- `);
-
- await truncateSessionInDb(db, 'sess', 0);
-
- const sessions = await all(db, 'SELECT updated_at FROM sessions WHERE id = ?', ['sess']);
- assert.notStrictEqual(sessions[0].updated_at, '2026-01-01');
- } finally {
- await close(db);
- }
- });
-
- test('removes search index entries for truncated turns', async () => {
- const db = await openTestDb();
- try {
- await setupSchema(db);
- await exec(db, `
- INSERT INTO sessions (id, cwd, summary, created_at, updated_at)
- VALUES ('sess', '/test', 'Test', '2026-01-01', '2026-01-01');
- `);
- for (let i = 0; i < 3; i++) {
- await exec(db, `
- INSERT INTO turns (session_id, turn_index, user_message, timestamp)
- VALUES ('sess', ${i}, 'msg ${i}', '2026-01-01');
- `);
- await exec(db, `
- INSERT INTO search_index (content, session_id, source_type, source_id)
- VALUES ('content ${i}', 'sess', 'turn', 'sess:turn:${i}');
- `);
- }
-
- await truncateSessionInDb(db, 'sess', 0);
-
- const searchEntries = await all(db, 'SELECT * FROM search_index WHERE session_id = ?', ['sess']);
- assert.strictEqual(searchEntries.length, 1);
- assert.strictEqual(searchEntries[0].source_id, 'sess:turn:0');
- } finally {
- await close(db);
- }
- });
-
- test('removes all turns when keepUpToTurnIndex is -1', async () => {
- const db = await openTestDb();
- try {
- await setupSchema(db);
- await exec(db, `
- INSERT INTO sessions (id, cwd, summary, created_at, updated_at)
- VALUES ('sess', '/test', 'Test', '2026-01-01', '2026-01-01');
- `);
- for (let i = 0; i < 3; i++) {
- await exec(db, `
- INSERT INTO turns (session_id, turn_index, user_message, timestamp)
- VALUES ('sess', ${i}, 'msg ${i}', '2026-01-01');
- `);
- await exec(db, `
- INSERT INTO search_index (content, session_id, source_type, source_id)
- VALUES ('content ${i}', 'sess', 'turn', 'sess:turn:${i}');
- `);
- }
-
- await truncateSessionInDb(db, 'sess', -1);
-
- const turns = await all(db, 'SELECT * FROM turns WHERE session_id = ?', ['sess']);
- assert.strictEqual(turns.length, 0, 'all turns should be removed');
-
- const searchEntries = await all(db, 'SELECT * FROM search_index WHERE session_id = ?', ['sess']);
- assert.strictEqual(searchEntries.length, 0, 'all search entries should be removed');
- } finally {
- await close(db);
- }
- });
- });
-});
diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts
deleted file mode 100644
index 3b02e51c..00000000
--- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts
+++ /dev/null
@@ -1,456 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import assert from 'assert';
-import type { CopilotSession, SessionEvent, SessionEventPayload, SessionEventType, TypedSessionEventHandler } from '@github/copilot-sdk';
-import { Emitter } from '../../../../base/common/event.js';
-import { DisposableStore } from '../../../../base/common/lifecycle.js';
-import { URI } from '../../../../base/common/uri.js';
-import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
-import { NullLogService, ILogService } from '../../../log/common/log.js';
-import { IFileService } from '../../../files/common/files.js';
-import { AgentSession, IAgentProgressEvent, IAgentUserInputRequestEvent } from '../../common/agentService.js';
-import { IDiffComputeService } from '../../common/diffComputeService.js';
-import { ISessionDataService } from '../../common/sessionDataService.js';
-import { SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind } from '../../common/state/sessionState.js';
-import { CopilotAgentSession, SessionWrapperFactory } from '../../node/copilot/copilotAgentSession.js';
-import { CopilotSessionWrapper } from '../../node/copilot/copilotSessionWrapper.js';
-import { InstantiationService } from '../../../instantiation/common/instantiationService.js';
-import { ServiceCollection } from '../../../instantiation/common/serviceCollection.js';
-import { createSessionDataService, createZeroDiffComputeService } from '../common/sessionTestHelpers.js';
-
-// ---- Mock CopilotSession (SDK level) ----------------------------------------
-
-/**
- * Minimal mock of the SDK's {@link CopilotSession}. Implements `on()` to
- * store typed handlers, and exposes `fire()` so tests can push events
- * through the real {@link CopilotSessionWrapper} event pipeline.
- */
-class MockCopilotSession {
- readonly sessionId = 'test-session-1';
-
- private readonly _handlers = new Map<string, Set<(event: SessionEvent) => void>>();
-
- on<K extends SessionEventType>(eventType: K, handler: TypedSessionEventHandler<K>): () => void {
- let set = this._handlers.get(eventType);
- if (!set) {
- set = new Set();
- this._handlers.set(eventType, set);
- }
- set.add(handler as (event: SessionEvent) => void);
- return () => { set.delete(handler as (event: SessionEvent) => void); };
- }
-
- /** Push an event through to all registered handlers of the given type. */
- fire<K extends SessionEventType>(type: K, data: SessionEventPayload<K>['data']): void {
- const event = { type, data, id: 'evt-1', timestamp: new Date().toISOString(), parentId: null } as SessionEventPayload<K>;
- const set = this._handlers.get(type);
- if (set) {
- for (const handler of set) {
- handler(event);
- }
- }
- }
-
- // Stubs for methods the wrapper / session class calls
- async send() { return ''; }
- async abort() { }
- async setModel() { }
- async getMessages() { return []; }
- async destroy() { }
-}
-
-// ---- Helpers ----------------------------------------------------------------
-
-async function createAgentSession(disposables: DisposableStore, options?: { workingDirectory?: URI }): Promise<{
- session: CopilotAgentSession;
- mockSession: MockCopilotSession;
- progressEvents: IAgentProgressEvent[];
-}> {
- const progressEmitter = disposables.add(new Emitter<IAgentProgressEvent>());
- const progressEvents: IAgentProgressEvent[] = [];
- disposables.add(progressEmitter.event(e => progressEvents.push(e)));
-
- const sessionUri = AgentSession.uri('copilot', 'test-session-1');
- const mockSession = new MockCopilotSession();
-
- const factory: SessionWrapperFactory = async () => new CopilotSessionWrapper(mockSession as unknown as CopilotSession);
-
- const services = new ServiceCollection();
- services.set(ILogService, new NullLogService());
- services.set(IFileService, { _serviceBrand: undefined } as IFileService);
- services.set(ISessionDataService, createSessionDataService());
- services.set(IDiffComputeService, createZeroDiffComputeService());
- const instantiationService = disposables.add(new InstantiationService(services));
-
- const session = disposables.add(instantiationService.createInstance(
- CopilotAgentSession,
- sessionUri,
- 'test-session-1',
- options?.workingDirectory,
- progressEmitter,
- factory,
- undefined, // shellManager
- ));
-
- await session.initializeSession();
-
- return { session, mockSession, progressEvents };
-}
-
-// ---- Tests ------------------------------------------------------------------
-
-suite('CopilotAgentSession', () => {
-
- const disposables = new DisposableStore();
-
- teardown(() => disposables.clear());
- ensureNoDisposablesAreLeakedInTestSuite();
-
- // ---- permission handling ----
-
- suite('permission handling', () => {
-
- test('auto-approves read inside working directory', async () => {
- const { session } = await createAgentSession(disposables, { workingDirectory: URI.file('/workspace') });
- const result = await session.handlePermissionRequest({
- kind: 'read',
- path: '/workspace/src/file.ts',
- toolCallId: 'tc-1',
- });
- assert.strictEqual(result.kind, 'approved');
- });
-
- test('does not auto-approve read outside working directory', async () => {
- const { session, progressEvents } = await createAgentSession(disposables, { workingDirectory: URI.file('/workspace') });
-
- // Kick off permission request but don't await — it will block
- const resultPromise = session.handlePermissionRequest({
- kind: 'read',
- path: '/other/file.ts',
- toolCallId: 'tc-2',
- });
-
- // Should have fired a tool_ready event
- assert.strictEqual(progressEvents.length, 1);
- assert.strictEqual(progressEvents[0].type, 'tool_ready');
-
- // Respond to it
- assert.ok(session.respondToPermissionRequest('tc-2', true));
- const result = await resultPromise;
- assert.strictEqual(result.kind, 'approved');
- });
-
- test('denies permission when no toolCallId', async () => {
- const { session } = await createAgentSession(disposables);
- const result = await session.handlePermissionRequest({ kind: 'write' });
- assert.strictEqual(result.kind, 'denied-interactively-by-user');
- });
-
- test('denied-interactively when user denies', async () => {
- const { session, progressEvents } = await createAgentSession(disposables);
- const resultPromise = session.handlePermissionRequest({
- kind: 'shell',
- toolCallId: 'tc-3',
- });
-
- assert.strictEqual(progressEvents.length, 1);
- session.respondToPermissionRequest('tc-3', false);
- const result = await resultPromise;
- assert.strictEqual(result.kind, 'denied-interactively-by-user');
- });
-
- test('pending permissions are denied on dispose', async () => {
- const { session } = await createAgentSession(disposables);
- const resultPromise = session.handlePermissionRequest({
- kind: 'write',
- toolCallId: 'tc-4',
- });
-
- session.dispose();
- const result = await resultPromise;
- assert.strictEqual(result.kind, 'denied-interactively-by-user');
- });
-
- test('pending permissions are denied on abort', async () => {
- const { session } = await createAgentSession(disposables);
- const resultPromise = session.handlePermissionRequest({
- kind: 'write',
- toolCallId: 'tc-5',
- });
-
- await session.abort();
- const result = await resultPromise;
- assert.strictEqual(result.kind, 'denied-interactively-by-user');
- });
-
- test('respondToPermissionRequest returns false for unknown id', async () => {
- const { session } = await createAgentSession(disposables);
- assert.strictEqual(session.respondToPermissionRequest('unknown-id', true), false);
- });
- });
-
- // ---- sendSteering ----
-
- suite('sendSteering', () => {
-
- test('fires steering_consumed after send resolves', async () => {
- const { session, progressEvents } = await createAgentSession(disposables);
-
- await session.sendSteering({ id: 'steer-1', userMessage: { text: 'focus on tests' } });
-
- const consumed = progressEvents.find(e => e.type === 'steering_consumed');
- assert.ok(consumed, 'should fire steering_consumed event');
- assert.strictEqual((consumed as { id: string }).id, 'steer-1');
- });
-
- test('does not fire steering_consumed when send fails', async () => {
- const { session, mockSession, progressEvents } = await createAgentSession(disposables);
-
- mockSession.send = async () => { throw new Error('send failed'); };
-
- await session.sendSteering({ id: 'steer-fail', userMessage: { text: 'will fail' } });
-
- const consumed = progressEvents.find(e => e.type === 'steering_consumed');
- assert.strictEqual(consumed, undefined, 'should not fire steering_consumed on failure');
- });
- });
-
- // ---- event mapping ----
-
- suite('event mapping', () => {
-
- test('tool_start event is mapped for non-hidden tools', async () => {
- const { mockSession, progressEvents } = await createAgentSession(disposables);
- mockSession.fire('tool.execution_start', {
- toolCallId: 'tc-10',
- toolName: 'bash',
- arguments: { command: 'echo hello' },
- } as SessionEventPayload<'tool.execution_start'>['data']);
-
- assert.strictEqual(progressEvents.length, 1);
- assert.strictEqual(progressEvents[0].type, 'tool_start');
- if (progressEvents[0].type === 'tool_start') {
- assert.strictEqual(progressEvents[0].toolCallId, 'tc-10');
- assert.strictEqual(progressEvents[0].toolName, 'bash');
- }
- });
-
- test('hidden tools are not emitted as tool_start', async () => {
- const { mockSession, progressEvents } = await createAgentSession(disposables);
- mockSession.fire('tool.execution_start', {
- toolCallId: 'tc-11',
- toolName: 'report_intent',
- } as SessionEventPayload<'tool.execution_start'>['data']);
-
- assert.strictEqual(progressEvents.length, 0);
- });
-
- test('tool_complete event produces past-tense message', async () => {
- const { mockSession, progressEvents } = await createAgentSession(disposables);
-
- // First fire tool_start so it's tracked
- mockSession.fire('tool.execution_start', {
- toolCallId: 'tc-12',
- toolName: 'bash',
- arguments: { command: 'ls' },
- } as SessionEventPayload<'tool.execution_start'>['data']);
-
- // Then fire complete
- mockSession.fire('tool.execution_complete', {
- toolCallId: 'tc-12',
- success: true,
- result: { content: 'file1.ts\nfile2.ts' },
- } as SessionEventPayload<'tool.execution_complete'>['data']);
-
- assert.strictEqual(progressEvents.length, 2);
- assert.strictEqual(progressEvents[1].type, 'tool_complete');
- if (progressEvents[1].type === 'tool_complete') {
- assert.strictEqual(progressEvents[1].toolCallId, 'tc-12');
- assert.ok(progressEvents[1].result.success);
- assert.ok(progressEvents[1].result.pastTenseMessage);
- }
- });
-
- test('tool_complete for untracked tool is ignored', async () => {
- const { mockSession, progressEvents } = await createAgentSession(disposables);
- mockSession.fire('tool.execution_complete', {
- toolCallId: 'tc-untracked',
- success: true,
- } as SessionEventPayload<'tool.execution_complete'>['data']);
-
- assert.strictEqual(progressEvents.length, 0);
- });
-
- test('idle event is forwarded', async () => {
- const { mockSession, progressEvents } = await createAgentSession(disposables);
- mockSession.fire('session.idle', {} as SessionEventPayload<'session.idle'>['data']);
-
- assert.strictEqual(progressEvents.length, 1);
- assert.strictEqual(progressEvents[0].type, 'idle');
- });
-
- test('error event is forwarded', async () => {
- const { mockSession, progressEvents } = await createAgentSession(disposables);
- mockSession.fire('session.error', {
- errorType: 'TestError',
- message: 'something went wrong',
- stack: 'Error: something went wrong',
- } as SessionEventPayload<'session.error'>['data']);
-
- assert.strictEqual(progressEvents.length, 1);
- assert.strictEqual(progressEvents[0].type, 'error');
- if (progressEvents[0].type === 'error') {
- assert.strictEqual(progressEvents[0].errorType, 'TestError');
- assert.strictEqual(progressEvents[0].message, 'something went wrong');
- }
- });
-
- test('message delta is forwarded', async () => {
- const { mockSession, progressEvents } = await createAgentSession(disposables);
- mockSession.fire('assistant.message_delta', {
- messageId: 'msg-1',
- deltaContent: 'Hello ',
- } as SessionEventPayload<'assistant.message_delta'>['data']);
-
- assert.strictEqual(progressEvents.length, 1);
- assert.strictEqual(progressEvents[0].type, 'delta');
- if (progressEvents[0].type === 'delta') {
- assert.strictEqual(progressEvents[0].content, 'Hello ');
- }
- });
-
- test('complete message with tool requests is forwarded', async () => {
- const { mockSession, progressEvents } = await createAgentSession(disposables);
- mockSession.fire('assistant.message', {
- messageId: 'msg-2',
- content: 'Let me help you.',
- toolRequests: [{
- toolCallId: 'tc-20',
- name: 'bash',
- arguments: { command: 'ls' },
- type: 'function',
- }],
- } as SessionEventPayload<'assistant.message'>['data']);
-
- assert.strictEqual(progressEvents.length, 1);
- assert.strictEqual(progressEvents[0].type, 'message');
- if (progressEvents[0].type === 'message') {
- assert.strictEqual(progressEvents[0].content, 'Let me help you.');
- assert.strictEqual(progressEvents[0].toolRequests?.length, 1);
- assert.strictEqual(progressEvents[0].toolRequests?.[0].toolCallId, 'tc-20');
- }
- });
- });
-
- // ---- user input handling ----
-
- suite('user input handling', () => {
-
- function assertUserInputEvent(event: IAgentProgressEvent): asserts event is IAgentUserInputRequestEvent {
- assert.strictEqual(event.type, 'user_input_request');
- }
-
- test('handleUserInputRequest fires user_input_request progress event', async () => {
- const { session, progressEvents } = await createAgentSession(disposables);
-
- // Start the request (don't await — it blocks waiting for response)
- const resultPromise = session.handleUserInputRequest(
- { question: 'What is your name?' },
- { sessionId: 'test-session-1' }
- );
-
- // Verify progress event was fired
- assert.strictEqual(progressEvents.length, 1);
- const event = progressEvents[0];
- assertUserInputEvent(event);
- assert.strictEqual(event.request.message, 'What is your name?');
- const requestId = event.request.id;
- assert.ok(event.request.questions);
- const questionId = event.request.questions[0].id;
-
- // Respond to unblock the promise
- session.respondToUserInputRequest(requestId, SessionInputResponseKind.Accept, {
- [questionId]: {
- state: SessionInputAnswerState.Submitted,
- value: { kind: SessionInputAnswerValueKind.Text, value: 'Alice' }
- }
- });
-
- const result = await resultPromise;
- assert.strictEqual(result.answer, 'Alice');
- assert.strictEqual(result.wasFreeform, true);
- });
-
- test('handleUserInputRequest with choices generates SingleSelect question', async () => {
- const { session, progressEvents } = await createAgentSession(disposables);
-
- const resultPromise = session.handleUserInputRequest(
- { question: 'Pick a color', choices: ['red', 'blue', 'green'] },
- { sessionId: 'test-session-1' }
- );
-
- assert.strictEqual(progressEvents.length, 1);
- const event = progressEvents[0];
- assertUserInputEvent(event);
- assert.ok(event.request.questions);
- assert.strictEqual(event.request.questions.length, 1);
- assert.strictEqual(event.request.questions[0].kind, SessionInputQuestionKind.SingleSelect);
- if (event.request.questions[0].kind === SessionInputQuestionKind.SingleSelect) {
- assert.strictEqual(event.request.questions[0].options.length, 3);
- assert.strictEqual(event.request.questions[0].options[0].label, 'red');
- }
-
- // Respond with a selected choice
- const questions = event.request.questions;
- session.respondToUserInputRequest(event.request.id, SessionInputResponseKind.Accept, {
- [questions[0].id]: {
- state: SessionInputAnswerState.Submitted,
- value: { kind: SessionInputAnswerValueKind.Selected, value: 'blue' }
- }
- });
-
- const result = await resultPromise;
- assert.strictEqual(result.answer, 'blue');
- assert.strictEqual(result.wasFreeform, false);
- });
-
- test('handleUserInputRequest returns empty answer on cancel', async () => {
- const { session, progressEvents } = await createAgentSession(disposables);
-
- const resultPromise = session.handleUserInputRequest(
- { question: 'Cancel me' },
- { sessionId: 'test-session-1' }
- );
-
- const event = progressEvents[0];
- assertUserInputEvent(event);
- session.respondToUserInputRequest(event.request.id, SessionInputResponseKind.Cancel);
-
- const result = await resultPromise;
- assert.strictEqual(result.answer, '');
- assert.strictEqual(result.wasFreeform, true);
- });
-
- test('respondToUserInputRequest returns false for unknown id', async () => {
- const { session } = await createAgentSession(disposables);
- assert.strictEqual(session.respondToUserInputRequest('unknown-id', SessionInputResponseKind.Accept), false);
- });
-
- test('pending user inputs are cancelled on dispose', async () => {
- const { session } = await createAgentSession(disposables);
-
- const resultPromise = session.handleUserInputRequest(
- { question: 'Will be cancelled' },
- { sessionId: 'test-session-1' }
- );
-
- session.dispose();
- const result = await resultPromise;
- assert.strictEqual(result.answer, '');
- assert.strictEqual(result.wasFreeform, true);
- });
- });
-});
diff --git a/src/vs/platform/agentHost/test/node/copilotGitProject.test.ts b/src/vs/platform/agentHost/test/node/copilotGitProject.test.ts
deleted file mode 100644
index 6ae1694c..00000000
--- a/src/vs/platform/agentHost/test/node/copilotGitProject.test.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import assert from 'assert';
-import * as cp from 'child_process';
-import * as fs from 'fs';
-import { tmpdir } from 'os';
-import { join } from '../../../../base/common/path.js';
-import { URI } from '../../../../base/common/uri.js';
-import { Promises } from '../../../../base/node/pfs.js';
-import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
-import { getRandomTestPath } from '../../../../base/test/node/testUtils.js';
-import { projectFromCopilotContext, projectFromRepository, resolveGitProject } from '../../node/copilot/copilotGitProject.js';
-
-function execGit(cwd: string, args: string[]): Promise<string> {
- return new Promise((resolve, reject) => {
- cp.execFile('git', args, { cwd, encoding: 'utf8' }, (error, stdout, stderr) => {
- if (error) {
- reject(new Error(stderr || error.message));
- return;
- }
- resolve(stdout.trim());
- });
- });
-}
-
-suite('Copilot Git Project', () => {
- ensureNoDisposablesAreLeakedInTestSuite();
-
- let testDir: string;
-
- setup(async () => {
- testDir = getRandomTestPath(tmpdir(), 'vsctests', 'copilot-git-project');
- await fs.promises.mkdir(testDir, { recursive: true });
- });
-
- teardown(async () => {
- await Promises.rm(testDir);
- });
-
- async function createRepository(name: string): Promise<string> {
- const repositoryPath = join(testDir, name);
- await fs.promises.mkdir(repositoryPath, { recursive: true });
- await execGit(repositoryPath, ['init']);
- await execGit(repositoryPath, ['config', 'user.email', 'test@example.com']);
- await execGit(repositoryPath, ['config', 'user.name', 'Test User']);
- await fs.promises.writeFile(join(repositoryPath, 'README.md'), '# Test\n');
- await execGit(repositoryPath, ['add', 'README.md']);
- await execGit(repositoryPath, ['commit', '-m', 'initial']);
- return repositoryPath;
- }
-
- test('resolves a repository project from a worktree working directory', async () => {
- const repositoryPath = await createRepository('source-repo');
- const canonicalRepositoryPath = await fs.promises.realpath(repositoryPath);
- const worktreePath = join(testDir, 'worktree-checkout');
- await execGit(repositoryPath, ['worktree', 'add', worktreePath]);
-
- const project = await resolveGitProject(URI.file(worktreePath));
-
- assert.deepStrictEqual({
- uri: project?.uri.toString(),
- displayName: project?.displayName,
- }, {
- uri: URI.file(canonicalRepositoryPath).toString(),
- displayName: 'source-repo',
- });
- });
-
- test('resolves the repository itself for a normal git working directory', async () => {
- const repositoryPath = await createRepository('normal-repo');
- const canonicalRepositoryPath = await fs.promises.realpath(repositoryPath);
-
- const project = await resolveGitProject(URI.file(repositoryPath));
-
- assert.deepStrictEqual({
- uri: project?.uri.toString(),
- displayName: project?.displayName,
- }, {
- uri: URI.file(canonicalRepositoryPath).toString(),
- displayName: 'normal-repo',
- });
- });
-
- test('returns undefined outside a git working tree', async () => {
- const folder = join(testDir, 'plain-folder');
- await fs.promises.mkdir(folder);
-
- assert.strictEqual(await resolveGitProject(URI.file(folder)), undefined);
- });
-
- test('falls back to repository context when no git project is available', async () => {
- const project = await projectFromCopilotContext({ repository: 'microsoft/vscode' });
-
- assert.deepStrictEqual({
- uri: project?.uri.toString(),
- displayName: project?.displayName,
- }, {
- uri: 'https://github.com/microsoft/vscode',
- displayName: 'vscode',
- });
- });
-
- test('parses repository URLs', () => {
- const project = projectFromRepository('https://github.com/microsoft/vscode.git');
-
- assert.deepStrictEqual({
- uri: project?.uri.toString(),
- displayName: project?.displayName,
- }, {
- uri: 'https://github.com/microsoft/vscode.git',
- displayName: 'vscode',
- });
- });
-});
diff --git a/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts b/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts
deleted file mode 100644
index 4a18ade2..00000000
--- a/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts
+++ /dev/null
@@ -1,242 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import assert from 'assert';
-import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
-import { DisposableStore } from '../../../../base/common/lifecycle.js';
-import { Schemas } from '../../../../base/common/network.js';
-import { URI } from '../../../../base/common/uri.js';
-import { VSBuffer } from '../../../../base/common/buffer.js';
-import { FileService } from '../../../files/common/fileService.js';
-import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js';
-import { NullLogService } from '../../../log/common/log.js';
-import { McpServerType } from '../../../mcp/common/mcpPlatformTypes.js';
-import { toSdkMcpServers, toSdkCustomAgents, toSdkSkillDirectories, parsedPluginsEqual } from '../../node/copilot/copilotPluginConverters.js';
-import type { IMcpServerDefinition, INamedPluginResource, IParsedPlugin } from '../../../agentPlugins/common/pluginParsers.js';
-
-suite('copilotPluginConverters', () => {
-
- const disposables = new DisposableStore();
- let fileService: FileService;
-
- setup(() => {
- fileService = disposables.add(new FileService(new NullLogService()));
- disposables.add(fileService.registerProvider(Schemas.inMemory, disposables.add(new InMemoryFileSystemProvider())));
- });
-
- teardown(() => disposables.clear());
- ensureNoDisposablesAreLeakedInTestSuite();
-
- // ---- toSdkMcpServers ------------------------------------------------
-
- suite('toSdkMcpServers', () => {
-
- test('converts local server definitions', () => {
- const defs: IMcpServerDefinition[] = [{
- name: 'test-server',
- uri: URI.file('/plugin'),
- configuration: {
- type: McpServerType.LOCAL,
- command: 'node',
- args: ['server.js', '--port', '3000'],
- env: { NODE_ENV: 'production', PORT: 3000 as unknown as string },
- cwd: '/workspace',
- },
- }];
-
- const result = toSdkMcpServers(defs);
- assert.deepStrictEqual(result, {
- 'test-server': {
- type: 'local',
- command: 'node',
- args: ['server.js', '--port', '3000'],
- tools: ['*'],
- env: { NODE_ENV: 'production', PORT: '3000' },
- cwd: '/workspace',
- },
- });
- });
-
- test('converts remote/http server definitions', () => {
- const defs: IMcpServerDefinition[] = [{
- name: 'remote-server',
- uri: URI.file('/plugin'),
- configuration: {
- type: McpServerType.REMOTE,
- url: 'https://example.com/mcp',
- headers: { 'Authorization': 'Bearer token' },
- },
- }];
-
- const result = toSdkMcpServers(defs);
- assert.deepStrictEqual(result, {
- 'remote-server': {
- type: 'http',
- url: 'https://example.com/mcp',
- tools: ['*'],
- headers: { 'Authorization': 'Bearer token' },
- },
- });
- });
-
- test('handles empty definitions', () => {
- const result = toSdkMcpServers([]);
- assert.deepStrictEqual(result, {});
- });
-
- test('omits optional fields when undefined', () => {
- const defs: IMcpServerDefinition[] = [{
- name: 'minimal',
- uri: URI.file('/plugin'),
- configuration: {
- type: McpServerType.LOCAL,
- command: 'echo',
- },
- }];
-
- const result = toSdkMcpServers(defs);
- assert.strictEqual(result['minimal'].type, 'local');
- assert.deepStrictEqual((result['minimal'] as { args?: string[] }).args, []);
- assert.strictEqual(Object.hasOwn(result['minimal'], 'env'), false);
- assert.strictEqual(Object.hasOwn(result['minimal'], 'cwd'), false);
- });
-
- test('filters null values from env', () => {
- const defs: IMcpServerDefinition[] = [{
- name: 'with-null-env',
- uri: URI.file('/plugin'),
- configuration: {
- type: McpServerType.LOCAL,
- command: 'test',
- env: { KEEP: 'value', DROP: null as unknown as string },
- },
- }];
-
- const result = toSdkMcpServers(defs);
- const env = (result['with-null-env'] as { env?: Record<string, string> }).env;
- assert.deepStrictEqual(env, { KEEP: 'value' });
- });
- });
-
- // ---- toSdkCustomAgents ----------------------------------------------
-
- suite('toSdkCustomAgents', () => {
-
- test('reads agent files and creates configs', async () => {
- const agentUri = URI.from({ scheme: Schemas.inMemory, path: '/agents/helper.md' });
- await fileService.writeFile(agentUri, VSBuffer.fromString('You are a helpful assistant'));
-
- const agents: INamedPluginResource[] = [{ uri: agentUri, name: 'helper' }];
- const result = await toSdkCustomAgents(agents, fileService);
-
- assert.deepStrictEqual(result, [{
- name: 'helper',
- prompt: 'You are a helpful assistant',
- }]);
- });
-
- test('skips agents whose files cannot be read', async () => {
- const agents: INamedPluginResource[] = [
- { uri: URI.from({ scheme: Schemas.inMemory, path: '/nonexistent/agent.md' }), name: 'missing' },
- ];
- const result = await toSdkCustomAgents(agents, fileService);
- assert.deepStrictEqual(result, []);
- });
-
- test('processes multiple agents, skipping failures', async () => {
- const goodUri = URI.from({ scheme: Schemas.inMemory, path: '/agents/good.md' });
- await fileService.writeFile(goodUri, VSBuffer.fromString('Good agent'));
-
- const agents: INamedPluginResource[] = [
- { uri: goodUri, name: 'good' },
- { uri: URI.from({ scheme: Schemas.inMemory, path: '/agents/bad.md' }), name: 'bad' },
- ];
- const result = await toSdkCustomAgents(agents, fileService);
- assert.strictEqual(result.length, 1);
- assert.strictEqual(result[0].name, 'good');
- });
- });
-
- // ---- toSdkSkillDirectories ------------------------------------------
-
- suite('toSdkSkillDirectories', () => {
-
- test('extracts parent directories of skill URIs', () => {
- const skills: INamedPluginResource[] = [
- { uri: URI.file('/plugins/skill-a/SKILL.md'), name: 'skill-a' },
- { uri: URI.file('/plugins/skill-b/SKILL.md'), name: 'skill-b' },
- ];
- const result = toSdkSkillDirectories(skills);
- assert.strictEqual(result.length, 2);
- });
-
- test('deduplicates directories', () => {
- const skills: INamedPluginResource[] = [
- { uri: URI.file('/plugins/shared/SKILL.md'), name: 'skill-a' },
- { uri: URI.file('/plugins/shared/SKILL.md'), name: 'skill-b' },
- ];
- const result = toSdkSkillDirectories(skills);
- assert.strictEqual(result.length, 1);
- });
-
- test('handles empty input', () => {
- const result = toSdkSkillDirectories([]);
- assert.deepStrictEqual(result, []);
- });
- });
-
- // ---- parsedPluginsEqual ---------------------------------------------
-
- suite('parsedPluginsEqual', () => {
-
- function makePlugin(overrides?: Partial<IParsedPlugin>): IParsedPlugin {
- return {
- hooks: [],
- mcpServers: [],
- skills: [],
- agents: [],
- ...overrides,
- };
- }
-
- test('returns true for identical empty plugins', () => {
- assert.strictEqual(parsedPluginsEqual([makePlugin()], [makePlugin()]), true);
- });
-
- test('returns true for same content', () => {
- const a = makePlugin({
- skills: [{ uri: URI.file('/a/SKILL.md'), name: 'a' }],
- mcpServers: [{
- name: 'server',
- uri: URI.file('/mcp'),
- configuration: { type: McpServerType.LOCAL, command: 'node' },
- }],
- });
- const b = makePlugin({
- skills: [{ uri: URI.file('/a/SKILL.md'), name: 'a' }],
- mcpServers: [{
- name: 'server',
- uri: URI.file('/mcp'),
- configuration: { type: McpServerType.LOCAL, command: 'node' },
- }],
- });
- assert.strictEqual(parsedPluginsEqual([a], [b]), true);
- });
-
- test('returns false for different content', () => {
- const a = makePlugin({ skills: [{ uri: URI.file('/a/SKILL.md'), name: 'a' }] });
- const b = makePlugin({ skills: [{ uri: URI.file('/b/SKILL.md'), name: 'b' }] });
- assert.strictEqual(parsedPluginsEqual([a], [b]), false);
- });
-
- test('returns false for different lengths', () => {
- assert.strictEqual(parsedPluginsEqual([makePlugin()], [makePlugin(), makePlugin()]), false);
- });
-
- test('returns true for empty arrays', () => {
- assert.strictEqual(parsedPluginsEqual([], []), true);
- });
- });
-});
diff --git a/src/vs/platform/agentHost/test/node/mapSessionEvents.test.ts b/src/vs/platform/agentHost/test/node/mapSessionEvents.test.ts
index 9872642c..f679eae4 100644
--- a/src/vs/platform/agentHost/test/node/mapSessionEvents.test.ts
+++ b/src/vs/platform/agentHost/test/node/mapSessionEvents.test.ts
@@ -49,3 +49,3 @@ suite('mapSessionEvents', () => {
assert.strictEqual(result[1].type, 'message');
- assert.strictEqual((result[1] as { role: string }).role, 'assistant');
+ assert.strictEqual((result[1] as { role: string; }).role, 'assistant');
});
@@ -69,3 +69,3 @@ suite('mapSessionEvents', () => {
- const complete = result[1] as { result: { content?: readonly { type: string; text?: string }[] } };
+ const complete = result[1] as { result: { content?: readonly { type: string; text?: string; }[]; }; };
assert.ok(complete.result.content);
@@ -126,3 +126,3 @@ suite('mapSessionEvents', () => {
- const content = (complete as { result: { content?: readonly Record<string, unknown>[] } }).result.content;
+ const content = (complete as { result: { content?: readonly Record<string, unknown>[]; }; }).result.content;
assert.ok(content);
@@ -134,3 +134,3 @@ suite('mapSessionEvents', () => {
// File edit URIs should be parseable
- const fileEdit = content[1] as { before: { uri: any; content: { uri: any } }; after: { uri: any; content: { uri: any } }; diff?: { added?: number; removed?: number } };
+ const fileEdit = content[1] as { before: { uri: any; content: { uri: any; }; }; after: { uri: any; content: { uri: any; }; }; diff?: { added?: number; removed?: number; }; };
const beforeFields = parseSessionDbUri(fileEdit.before.content.uri);
@@ -179,3 +179,3 @@ suite('mapSessionEvents', () => {
const result = await mapSessionEvents(session, db, events);
- const content = (result[1] as { result: { content?: readonly Record<string, unknown>[] } }).result.content;
+ const content = (result[1] as { result: { content?: readonly Record<string, unknown>[]; }; }).result.content;
assert.ok(content);
@@ -199,3 +199,3 @@ suite('mapSessionEvents', () => {
const result = await mapSessionEvents(session, undefined, events);
- const content = (result[1] as { result: { content?: readonly Record<string, unknown>[] } }).result.content;
+ const content = (result[1] as { result: { content?: readonly Record<string, unknown>[]; }; }).result.content;
assert.ok(content);
@@ -221,3 +221,3 @@ suite('mapSessionEvents', () => {
const result = await mapSessionEvents(session, db, events);
- const content = (result[1] as { result: { content?: readonly Record<string, unknown>[] } }).result.content;
+ const content = (result[1] as { result: { content?: readonly Record<string, unknown>[]; }; }).result.content;
assert.ok(content);
@@ -248,3 +248,3 @@ suite('mapSessionEvents', () => {
assert.strictEqual(result[0].type, 'subagent_started');
- const event = result[0] as { type: string; toolCallId: string; agentName: string; agentDisplayName: string };
+ const event = result[0] as { type: string; toolCallId: string; agentName: string; agentDisplayName: string; };
assert.strictEqual(event.toolCallId, 'tc-1');