diff --git a/patches/11-update-use-github-release.patch b/patches/11-update-use-github-release.patch index a314895..3e17460 100644 --- a/patches/11-update-use-github-release.patch +++ b/patches/11-update-use-github-release.patch @@ -269,14 +269,20 @@ index 2be53f61..8776a7f6 100644 + this.setState(State.Idle(UpdateType.Archive, message)); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts -index 222db559..0037e002 100644 +index 1911ac0a6bc..8fe1d1e1cf0 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts -@@ -14,3 +14,2 @@ import { CancellationToken, CancellationTokenSource } from '../../../base/common +@@ -12,7 +12,6 @@ import { Delayer, ProcessTimeRunOnceScheduler, timeout } from '../../../base/com + import { VSBuffer } from '../../../base/common/buffer.js'; + import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { memoize } from '../../../base/common/decorators.js'; -import { hash } from '../../../base/common/hash.js'; import * as path from '../../../base/common/path.js'; -@@ -32,7 +31,7 @@ import { INativeHostMainService } from '../../native/electron-main/nativeHostMai + import { basename } from '../../../base/common/path.js'; + import { transform } from '../../../base/common/stream.js'; +@@ -29,11 +28,11 @@ import { ILogService } from '../../log/common/log.js'; + import { IMeteredConnectionService } from '../../meteredConnection/common/meteredConnection.js'; + import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js'; import { IProductService } from '../../product/common/productService.js'; -import { asJson, IRequestService } from '../../request/common/request.js'; +import { IRequestService, NO_FETCH_TELEMETRY } from '../../request/common/request.js'; @@ -287,7 +293,11 @@ index 222db559..0037e002 100644 +import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, Target, UpdateType } from '../common/update.js'; +import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions } from './abstractUpdateService.js'; -@@ -50,5 +49,9 @@ function getUpdateType(): UpdateType { + interface IAvailableUpdate { + packagePath: string; +@@ -47,9 +46,13 @@ interface IAvailableUpdate { + let _updateType: UpdateType | undefined = undefined; + function getUpdateType(): UpdateType { if (typeof _updateType === 'undefined') { - _updateType = existsSync(path.join(path.dirname(process.execPath), 'unins000.exe')) - ? UpdateType.Setup @@ -300,12 +310,20 @@ index 222db559..0037e002 100644 + _updateType = UpdateType.Archive; + } } -@@ -158,3 +161,3 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + + return _updateType; +@@ -168,7 +171,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + // updatingVersionPath will be deleted by inno setup. + } } else { - const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); + const fastUpdatesEnabled = getUpdateType() === UpdateType.Setup && this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); // GC for background updates in system setup happens via inno_setup since it requires -@@ -178,12 +181,22 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + // elevated permissions. + if (fastUpdatesEnabled && this.productService.target === 'user' && this.productService.commit) { +@@ -187,16 +190,26 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + } + } - protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined { - let platform = `win32-${process.arch}`; @@ -336,14 +354,20 @@ index 222db559..0037e002 100644 - return createUpdateURL(this.productService.updateUrl!, platform, quality, commit, options); + return createUpdateURL(this.productService, quality, process.platform, process.arch, target); } -@@ -195,6 +208,2 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + + protected doCheckForUpdates(explicit: boolean, pendingCommit?: string): void { +@@ -204,22 +217,22 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + return; + } - const internalOrg = this.getInternalOrg(); - const background = !explicit && !internalOrg; - const url = this.buildUpdateFeedUrl(this.quality, pendingCommit ?? this.productService.commit!, { background, internalOrg }); - // Only set CheckingForUpdates if we're not already in Overwriting state -@@ -204,9 +213,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + if (this.state.type !== StateType.Overwriting) { + this.setState(State.CheckingForUpdates(explicit)); + } - const headers = getUpdateRequestHeaders(this.productService.version); - this.requestService.request({ url, headers, callSite: 'updateService.win32.checkForUpdates' }, CancellationToken.None) @@ -362,7 +386,11 @@ index 222db559..0037e002 100644 - if (!update || !update.url || !update.version || !update.productVersion) { + if(!result) { // If we were checking for an overwrite update and found nothing newer, -@@ -222,2 +235,9 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + // restore the Ready state with the pending update + if (this.state.type === StateType.Overwriting) { +@@ -231,6 +244,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + return Promise.resolve(null); + } + const { lastest, update } = result; + @@ -372,12 +400,20 @@ index 222db559..0037e002 100644 + } + if (updateType === UpdateType.Archive) { -@@ -247,3 +267,3 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + this.setState(State.AvailableForDownload(update)); + return Promise.resolve(null); +@@ -256,7 +276,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + + const downloadPath = `${updatePackagePath}.tmp`; - return this.requestService.request({ url: update.url, callSite: 'updateService.win32.downloadUpdate' }, CancellationToken.None) + return this.requestService.request({ url: update.url, callSite: NO_FETCH_TELEMETRY }, CancellationToken.None) .then(context => { -@@ -292,8 +312,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + // Get total size from Content-Length header + const contentLengthHeader = context.res.headers['content-length']; +@@ -301,12 +321,11 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + }); + }); }) - .then(undefined, err => { - this.telemetryService.publicLog2<{ messageHash: string }, UpdateErrorClassification>('update:error', { messageHash: String(hash(String(err))) }); @@ -389,52 +425,57 @@ index 222db559..0037e002 100644 - const message: string | undefined = explicit ? (err.message || err) : undefined; + const message: string | undefined = explicit ? (error.message || error) : undefined; -@@ -357,20 +376,31 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun - await pfs.Promises.writeFile(this.availableUpdate.updateFilePath, 'flag'); -- const child = spawn(this.availableUpdate.packagePath, -- [ -- '/verysilent', -- '/log', -- `/update="${this.availableUpdate.updateFilePath}"`, -- `/progress="${progressFilePath}"`, -- `/sessionend="${sessionEndFlagPath}"`, -- `/cancel="${cancelFilePath}"`, -- '/nocloseapplications', -- '/mergetasks=runcode,!desktopicon,!quicklaunchicon' -- ], -- { -+ -+ let child: ChildProcess + // If we were checking for an overwrite update and it failed, + // restore the Ready state with the pending update +@@ -373,24 +392,34 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun + await this.unlink(progressFilePath); + await pfs.Promises.writeFile(this.availableUpdate.updateFilePath, 'flag'); + +- const child = spawn(this.availableUpdate.packagePath, +- [ +- '/verysilent', +- '/log', +- `/update="${this.availableUpdate.updateFilePath}"`, +- `/progress="${progressFilePath}"`, +- `/sessionend="${sessionEndFlagPath}"`, +- `/cancel="${cancelFilePath}"`, +- '/nocloseapplications', +- '/mergetasks=runcode,!desktopicon,!quicklaunchicon' +- ], +- { ++ let child: ChildProcess + -+ const type = getUpdateType(); -+ if (type == UpdateType.WindowsInstaller) { -+ child = spawn('msiexec.exe', ['/i', this.availableUpdate.packagePath], { - detached: true, -- stdio: ['ignore', 'ignore', 'ignore'], -- windowsVerbatimArguments: true, -- env: { ...process.env, __COMPAT_LAYER: 'RunAsInvoker' } -- } -- ); -+ stdio: ['ignore', 'ignore', 'ignore'] -+ }); -+ } else { -+ child = spawn(this.availableUpdate.packagePath, -+ [ -+ '/verysilent', -+ '/log', -+ `/update="${this.availableUpdate.updateFilePath}"`, -+ `/progress="${progressFilePath}"`, -+ `/sessionend="${sessionEndFlagPath}"`, -+ `/cancel="${cancelFilePath}"`, -+ '/nocloseapplications', -+ '/mergetasks=runcode,!desktopicon,!quicklaunchicon' -+ ], -+ { -+ detached: true, -+ stdio: ['ignore', 'ignore', 'ignore'], -+ windowsVerbatimArguments: true, -+ env: { ...process.env, __COMPAT_LAYER: 'RunAsInvoker' } -+ } -+ ); -+ } ++ const type = getUpdateType(); ++ if (type == UpdateType.WindowsInstaller) { ++ child = spawn('msiexec.exe', ['/i', this.availableUpdate.packagePath], { + detached: true, +- stdio: ['ignore', 'ignore', 'ignore'], +- windowsVerbatimArguments: true, +- env: { ...process.env, __COMPAT_LAYER: 'RunAsInvoker' } +- } +- ); ++ stdio: ['ignore', 'ignore', 'ignore'] ++ }); ++ } else { ++ child = spawn(this.availableUpdate.packagePath, ++ [ ++ '/verysilent', ++ '/log', ++ `/update="${this.availableUpdate.updateFilePath}"`, ++ `/progress="${progressFilePath}"`, ++ `/sessionend="${sessionEndFlagPath}"`, ++ `/cancel="${cancelFilePath}"`, ++ '/nocloseapplications', ++ '/mergetasks=runcode,!desktopicon,!quicklaunchicon' ++ ], ++ { ++ detached: true, ++ stdio: ['ignore', 'ignore', 'ignore'], ++ windowsVerbatimArguments: true, ++ env: { ...process.env, __COMPAT_LAYER: 'RunAsInvoker' } ++ } ++ ); ++ } + // Track the process so we can cancel it if needed + this.availableUpdate.updateProcess = child; diff --git a/patches/20-keymap-use-custom-lib.patch b/patches/20-keymap-use-custom-lib.patch index 480333b..ecd1331 100644 --- a/patches/20-keymap-use-custom-lib.patch +++ b/patches/20-keymap-use-custom-lib.patch @@ -1,8 +1,8 @@ diff --git a/.npmrc b/.npmrc -index 025e4b04..5e409d87 100644 +index 8c21e58ef14..25a93f408ad 100644 --- a/.npmrc +++ b/.npmrc -@@ -4,5 +4,6 @@ ms_build_id="13870025" +@@ -4,5 +4,6 @@ ms_build_id="14159160" runtime="electron" ignore-scripts=false build_from_source="true" @@ -10,7 +10,7 @@ index 025e4b04..5e409d87 100644 legacy-peer-deps="true" timeout=180000 diff --git a/build/.moduleignore b/build/.moduleignore -index 00779f06..4e809334 100644 +index 90780acffa9..b226373f632 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -63,11 +63,11 @@ fsevents/test/** @@ -31,10 +31,10 @@ index 00779f06..4e809334 100644 native-is-elevated/binding.gyp native-is-elevated/build/** diff --git a/eslint.config.js b/eslint.config.js -index 7ff8a911..5080ac33 100644 +index fda37713fee..a2150a119a6 100644 --- a/eslint.config.js +++ b/eslint.config.js -@@ -1516,7 +1516,7 @@ export default tseslint.config( +@@ -1530,7 +1530,7 @@ export default defineConfig( 'inspector', 'minimist', 'node:module', @@ -44,10 +44,10 @@ index 7ff8a911..5080ac33 100644 'node-pty', 'os', diff --git a/package-lock.json b/package-lock.json -index c8cb435d..f5b21ba4 100644 +index 38d50ffa4de..cad1ae14f2c 100644 --- a/package-lock.json +++ b/package-lock.json -@@ -39,6 +39,7 @@ +@@ -41,6 +41,7 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.7.0", "@vscode/windows-registry": "^1.2.0", @@ -55,15 +55,15 @@ index c8cb435d..f5b21ba4 100644 "@xterm/addon-clipboard": "^0.3.0-beta.220", "@xterm/addon-image": "^0.10.0-beta.220", "@xterm/addon-ligatures": "^0.11.0-beta.220", -@@ -57,7 +58,6 @@ +@@ -59,7 +60,6 @@ "kerberos": "2.1.1", "minimist": "^1.2.8", "native-is-elevated": "0.9.0", - "native-keymap": "^3.3.5", + "node-addon-api": "^6.0.0", "node-pty": "^1.2.0-beta.13", "open": "^10.1.2", - "playwright-core": "1.59.1", -@@ -4122,6 +4122,13 @@ +@@ -4466,6 +4466,13 @@ "hasInstallScript": true, "license": "MIT" }, @@ -77,7 +77,7 @@ index c8cb435d..f5b21ba4 100644 "node_modules/@webgpu/types": { "version": "0.1.66", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz", -@@ -13843,9 +13850,10 @@ +@@ -14114,9 +14121,10 @@ } }, "node_modules/napi-build-utils": { @@ -91,7 +91,7 @@ index c8cb435d..f5b21ba4 100644 }, "node_modules/native-is-elevated": { "version": "0.9.0", -@@ -13854,13 +13862,6 @@ +@@ -14125,13 +14133,6 @@ "hasInstallScript": true, "license": "MIT" }, @@ -105,7 +105,7 @@ index c8cb435d..f5b21ba4 100644 "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", -@@ -15257,16 +15258,17 @@ +@@ -15526,16 +15527,17 @@ "license": "ISC" }, "node_modules/prebuild-install": { @@ -128,20 +128,20 @@ index c8cb435d..f5b21ba4 100644 "pump": "^3.0.0", "rc": "^1.2.7", diff --git a/package.json b/package.json -index 0db7d834..b904799d 100644 +index a43d079633a..508acdfb4dd 100644 --- a/package.json +++ b/package.json -@@ -134,7 +134,7 @@ +@@ -141,7 +141,7 @@ "kerberos": "2.1.1", "minimist": "^1.2.8", "native-is-elevated": "0.9.0", - "native-keymap": "^3.3.5", + "@vscodium/native-keymap": "3.3.7-258424", + "node-addon-api": "^6.0.0", "node-pty": "^1.2.0-beta.13", "open": "^10.1.2", - "playwright-core": "1.59.1", diff --git a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts -index 50360a79..93df77af 100644 +index 9b11a712c25..975c312747a 100644 --- a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts +++ b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts @@ -42,12 +42,12 @@ flakySuite('Native Modules (all platforms)', () => { @@ -162,7 +162,7 @@ index 50360a79..93df77af 100644 test('@vscode/native-watchdog', async () => { diff --git a/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts b/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts -index 8950ce21..f31cea62 100644 +index 8950ce2184a..f31cea623bc 100644 --- a/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts +++ b/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts @@ -3,7 +3,7 @@ diff --git a/patches/51-ext-copilot-remove-it.patch b/patches/51-ext-copilot-remove-it.patch index 7130449..45ca345 100644 --- a/patches/51-ext-copilot-remove-it.patch +++ b/patches/51-ext-copilot-remove-it.patch @@ -1,5 +1,5 @@ diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts -index beb7ba84..3b0be1b7 100644 +index beb7ba848fd..3b0be1b7692 100644 --- a/build/gulpfile.reh.ts +++ b/build/gulpfile.reh.ts @@ -23,13 +23,12 @@ import glob from 'glob'; @@ -63,7 +63,7 @@ index beb7ba84..3b0be1b7 100644 minified ? minifyTask : bundleTask, serverTaskCI diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts -index 386bbb0b..396d6948 100644 +index 190ba87eb51..8476c4568fd 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -26,9 +26,8 @@ import { config } from './lib/electron.ts'; @@ -138,7 +138,7 @@ index 386bbb0b..396d6948 100644 minified ? minifyVSCodeTask : bundleVSCodeTask, )); diff --git a/build/npm/dirs.ts b/build/npm/dirs.ts -index 289a4697..8c6df6f7 100644 +index 289a469754a..8c6df6f7ede 100644 --- a/build/npm/dirs.ts +++ b/build/npm/dirs.ts @@ -15,7 +15,6 @@ export const dirs = [ @@ -150,7 +150,7 @@ index 289a4697..8c6df6f7 100644 'extensions/css-language-features/server', 'extensions/debug-auto-launch', diff --git a/build/npm/postinstall.ts b/build/npm/postinstall.ts -index 0d00ac39..23e23c05 100644 +index 0d00ac39261..23e23c051a7 100644 --- a/build/npm/postinstall.ts +++ b/build/npm/postinstall.ts @@ -318,37 +318,6 @@ async function main() { @@ -192,26 +192,26 @@ index 0d00ac39..23e23c05 100644 main().catch(err => { diff --git a/package-lock.json b/package-lock.json -index 3a191a0f..3aa27c91 100644 +index 8ffdf12aa94..a4ae22d6b31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,6 @@ "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.82.0", -- "@github/copilot": "1.0.55-3", -- "@github/copilot-sdk": "1.0.0-beta.8", +- "@github/copilot": "^1.0.57", +- "@github/copilot-sdk": "^1.0.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/dev-tunnels-connections": "^1.3.41", -@@ -1104,329 +1102,6 @@ +@@ -1106,179 +1104,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@github/copilot": { -- "version": "1.0.55-3", -- "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.55-3.tgz", -- "integrity": "sha512-rtaUFobDVbS6oTvGvQLZBeb6nbsp5lZEaL3bcsuENv7Wv87lL4C6GnAsaGWsm/b/bF3eSqFCOTk9rKsDcyLFmQ==", +- "version": "1.0.57", +- "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.57.tgz", +- "integrity": "sha512-7dpOu9/qiodmFohZVpTxYmTcjbcXfstWeHof0Ka5RkhguKMkbS3c+sW23a7TTjtlViTV73z+IZFfFW1ru621kw==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "detect-libc": "^2.1.2" @@ -220,20 +220,20 @@ index 3a191a0f..3aa27c91 100644 - "copilot": "npm-loader.js" - }, - "optionalDependencies": { -- "@github/copilot-darwin-arm64": "1.0.55-3", -- "@github/copilot-darwin-x64": "1.0.55-3", -- "@github/copilot-linux-arm64": "1.0.55-3", -- "@github/copilot-linux-x64": "1.0.55-3", -- "@github/copilot-linuxmusl-arm64": "1.0.55-3", -- "@github/copilot-linuxmusl-x64": "1.0.55-3", -- "@github/copilot-win32-arm64": "1.0.55-3", -- "@github/copilot-win32-x64": "1.0.55-3" +- "@github/copilot-darwin-arm64": "1.0.57", +- "@github/copilot-darwin-x64": "1.0.57", +- "@github/copilot-linux-arm64": "1.0.57", +- "@github/copilot-linux-x64": "1.0.57", +- "@github/copilot-linuxmusl-arm64": "1.0.57", +- "@github/copilot-linuxmusl-x64": "1.0.57", +- "@github/copilot-win32-arm64": "1.0.57", +- "@github/copilot-win32-x64": "1.0.57" - } - }, - "node_modules/@github/copilot-darwin-arm64": { -- "version": "1.0.55-3", -- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.55-3.tgz", -- "integrity": "sha512-L1qsdnLtStLc5/1BrKA4m7eumlZbcuwtNc9Q7epafLWfB6nYwXoKmtWdyByhNfyC7Hm9TERvjwKjmxI4fZS2TA==", +- "version": "1.0.57", +- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.57.tgz", +- "integrity": "sha512-ZmsojZbitPSRfgw3W9wBrHGLRDsBvMCjGsGnJ7xXOU6qxeF/IyWHADxEv1WKfDw8BdCM+LE5yITPXB8bcvCdqQ==", - "cpu": [ - "arm64" - ], @@ -247,9 +247,9 @@ index 3a191a0f..3aa27c91 100644 - } - }, - "node_modules/@github/copilot-darwin-x64": { -- "version": "1.0.55-3", -- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.55-3.tgz", -- "integrity": "sha512-iQLYiKEohJU46IWifwCjBxUp/h1tnnfOeibEM6axB9Rd5noRz5Ka0M+YWJYzh6bhQ/mpuWFXVRINZIHk2muTyQ==", +- "version": "1.0.57", +- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.57.tgz", +- "integrity": "sha512-F4TFDOdORy4oSHJS4DE+3sTk09uk1lohOloe0jfvoEVxJSU6jdQcJLNGoo+BQljcG7a1HEBrmB04iAWG1UXVfA==", - "cpu": [ - "x64" - ], @@ -263,9 +263,9 @@ index 3a191a0f..3aa27c91 100644 - } - }, - "node_modules/@github/copilot-linux-arm64": { -- "version": "1.0.55-3", -- "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.55-3.tgz", -- "integrity": "sha512-SGRhLxAW7uSzU6TUmGV/ge+b57zq9UAj9oSrVqbjB3kdssCkh5PuzkAN1aX2InVuF+k/Eu0eoeX0FmLtGuQODw==", +- "version": "1.0.57", +- "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.57.tgz", +- "integrity": "sha512-6apNY/v7CMxKk45CctUZLzQnddBpIG9keSendFKYN+kBIEBSdy//s/Cz/4YQX1iERnklpgZRP7FvcwaKs0/7YA==", - "cpu": [ - "arm64" - ], @@ -279,9 +279,9 @@ index 3a191a0f..3aa27c91 100644 - } - }, - "node_modules/@github/copilot-linux-x64": { -- "version": "1.0.55-3", -- "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.55-3.tgz", -- "integrity": "sha512-ri3r1gk4/FHNQVS7icYIGz+e89pVaa/yiMKee+svkmIedG1c5Ae94gFN7+7zNcnnLmiD/US+WH1QHwCRrp/gbQ==", +- "version": "1.0.57", +- "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.57.tgz", +- "integrity": "sha512-EOOnU4Y+vZHfxVl8eBAP7JtSTmu5d4ZDUC9wCGpAA5k703lEnpu8UOv04mTHRn8KTzb8gj+ijNhxDWe3Xljbaw==", - "cpu": [ - "x64" - ], @@ -295,9 +295,9 @@ index 3a191a0f..3aa27c91 100644 - } - }, - "node_modules/@github/copilot-linuxmusl-arm64": { -- "version": "1.0.55-3", -- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.55-3.tgz", -- "integrity": "sha512-A0bCRkNMEONw2HIcIx+dirI1VLR8onBU7Z9VSZz1uMaNixfWKuYXzc5Rw4G/AVI+cNLntR3HJtvXrdvLlFYn0A==", +- "version": "1.0.57", +- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.57.tgz", +- "integrity": "sha512-FCAaaJLX5T2ZpMeS1TCNnhQuGqyH9WVZndFdN1VOEnN/iWeSSaVF3lM4TPyRHHnWDVxzZtB+VLqOSjINZntD6g==", - "cpu": [ - "arm64" - ], @@ -311,9 +311,9 @@ index 3a191a0f..3aa27c91 100644 - } - }, - "node_modules/@github/copilot-linuxmusl-x64": { -- "version": "1.0.55-3", -- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.55-3.tgz", -- "integrity": "sha512-i3mji5ZZfiRM1k54qCS49/zGUirxkWVckRFNdZUoVs9Mwb4UQIf75VRjk3MtL5OuKZSsUc27CiKOAgmYIQ5dsg==", +- "version": "1.0.57", +- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.57.tgz", +- "integrity": "sha512-AMIBN830yOvNcrj2Q0tGMImqat/V24wZS/4m5BaUssELM7r7KrT9ZBnBs+nWDZYeQaRoblFWL3f4AfxE3t94lQ==", - "cpu": [ - "x64" - ], @@ -327,12 +327,12 @@ index 3a191a0f..3aa27c91 100644 - } - }, - "node_modules/@github/copilot-sdk": { -- "version": "1.0.0-beta.8", -- "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.0-beta.8.tgz", -- "integrity": "sha512-lAuBfH6E5PUaSj8P/0FVMxzvwwBUs02tlvQ56PoJFtuc47KPqzGpf9BS7+h2eEr1UmjoLNJ/yqDiVApH9Oo1Fg==", +- "version": "1.0.0", +- "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.0.tgz", +- "integrity": "sha512-OKjmJMDM+GB2uHr8UA6O0FNs1Gfw/tkoE5vUNlYmKbydc9Yjf6pvuBdseGjAVvzc6f9HIbB5eZKLUrxbOTw+yA==", - "license": "MIT", - "dependencies": { -- "@github/copilot": "^1.0.55-1", +- "@github/copilot": "^1.0.57", - "vscode-jsonrpc": "^8.2.1", - "zod": "^4.3.6" - }, @@ -340,169 +340,19 @@ index 3a191a0f..3aa27c91 100644 - "node": ">=20.0.0" - } - }, -- "node_modules/@github/copilot-sdk/node_modules/@github/copilot": { -- "version": "1.0.55-7", -- "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.55-7.tgz", -- "integrity": "sha512-TczFrIaHH2sel6FM007H4FzT+Ipkj++I5u8Vx2ECWz9u24H7WOx/RpWcp6ExnSY1KSK1MtXaGcniAuqVi8Khaw==", -- "license": "SEE LICENSE IN LICENSE.md", -- "dependencies": { -- "detect-libc": "^2.1.2" -- }, -- "bin": { -- "copilot": "npm-loader.js" -- }, -- "optionalDependencies": { -- "@github/copilot-darwin-arm64": "1.0.55-7", -- "@github/copilot-darwin-x64": "1.0.55-7", -- "@github/copilot-linux-arm64": "1.0.55-7", -- "@github/copilot-linux-x64": "1.0.55-7", -- "@github/copilot-linuxmusl-arm64": "1.0.55-7", -- "@github/copilot-linuxmusl-x64": "1.0.55-7", -- "@github/copilot-win32-arm64": "1.0.55-7", -- "@github/copilot-win32-x64": "1.0.55-7" -- } -- }, -- "node_modules/@github/copilot-sdk/node_modules/@github/copilot-darwin-arm64": { -- "version": "1.0.55-7", -- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.55-7.tgz", -- "integrity": "sha512-QReU4F5+W0x/Nuc6qO+xYPeNnRjuHIIAeMBc1S+RFQ0T+YWynxRzNHGs9ZkUiIcLJ1F/y8GDq6sq7760Cn+onQ==", -- "cpu": [ -- "arm64" -- ], -- "license": "SEE LICENSE IN LICENSE.md", -- "optional": true, -- "os": [ -- "darwin" -- ], -- "bin": { -- "copilot-darwin-arm64": "copilot" -- } -- }, -- "node_modules/@github/copilot-sdk/node_modules/@github/copilot-darwin-x64": { -- "version": "1.0.55-7", -- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.55-7.tgz", -- "integrity": "sha512-qQ0d+XyvIPbNiaIydHBSCTQfWK5s0x1XnlrUKSzadgOnsFobGeldLSKtB159zJEiz0F/in5ythiUGJjWoAQVrA==", -- "cpu": [ -- "x64" -- ], -- "license": "SEE LICENSE IN LICENSE.md", -- "optional": true, -- "os": [ -- "darwin" -- ], -- "bin": { -- "copilot-darwin-x64": "copilot" -- } -- }, -- "node_modules/@github/copilot-sdk/node_modules/@github/copilot-linux-arm64": { -- "version": "1.0.55-7", -- "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.55-7.tgz", -- "integrity": "sha512-+2zlHahK3fUfkrnlHqbdQsZMPZwRfchoTxDZd9UHbEhQF7eNLzYN+7frWs6AZujU+h/1i92+mcLT18AQXI3KxQ==", -- "cpu": [ -- "arm64" -- ], -- "license": "SEE LICENSE IN LICENSE.md", -- "optional": true, -- "os": [ -- "linux" -- ], -- "bin": { -- "copilot-linux-arm64": "copilot" -- } -- }, -- "node_modules/@github/copilot-sdk/node_modules/@github/copilot-linux-x64": { -- "version": "1.0.55-7", -- "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.55-7.tgz", -- "integrity": "sha512-SGmvWcJHIKDIsjYZdFQloGw3Re6r2N1Zv1VuB1yV1ClVqfG5i5pTvai6vzX8d3WgGgRzrkLksDrzZKR27zJZ7A==", -- "cpu": [ -- "x64" -- ], -- "license": "SEE LICENSE IN LICENSE.md", -- "optional": true, -- "os": [ -- "linux" -- ], -- "bin": { -- "copilot-linux-x64": "copilot" -- } -- }, -- "node_modules/@github/copilot-sdk/node_modules/@github/copilot-linuxmusl-arm64": { -- "version": "1.0.55-7", -- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.55-7.tgz", -- "integrity": "sha512-rJkZLvz4KeGoLgyX6gcONgTNfFxeoQvN4jaAXlbD1nFP3hJbLTuY0CB4fBHmZWktrPkRL/j5aDGxrcIcl+Xg3A==", -- "cpu": [ -- "arm64" -- ], -- "license": "SEE LICENSE IN LICENSE.md", -- "optional": true, -- "os": [ -- "linux" -- ], -- "bin": { -- "copilot-linuxmusl-arm64": "copilot" -- } -- }, -- "node_modules/@github/copilot-sdk/node_modules/@github/copilot-linuxmusl-x64": { -- "version": "1.0.55-7", -- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.55-7.tgz", -- "integrity": "sha512-uPb08qgJHY1QW2YhA1OBJ9PB0CDwCvtuttWbeZ+AW+qfFVsvBpARU1cdEl/xT4IXMhBFoJiePv3BnLGjVZtoWA==", -- "cpu": [ -- "x64" -- ], -- "license": "SEE LICENSE IN LICENSE.md", -- "optional": true, -- "os": [ -- "linux" -- ], -- "bin": { -- "copilot-linuxmusl-x64": "copilot" -- } -- }, -- "node_modules/@github/copilot-sdk/node_modules/@github/copilot-win32-arm64": { -- "version": "1.0.55-7", -- "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.55-7.tgz", -- "integrity": "sha512-mb4Sg2sJjmK9Rq8XCRuhoIOjUScB5p2Ct9ZtTbC3ipvONWMOMjYPbLvC8K9GAHcYcHLdv98hvzv3+qjBhb5tZQ==", -- "cpu": [ -- "arm64" -- ], -- "license": "SEE LICENSE IN LICENSE.md", -- "optional": true, -- "os": [ -- "win32" -- ], -- "bin": { -- "copilot-win32-arm64": "copilot.exe" -- } -- }, -- "node_modules/@github/copilot-sdk/node_modules/@github/copilot-win32-x64": { -- "version": "1.0.55-7", -- "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.55-7.tgz", -- "integrity": "sha512-GL9jAtkn2Kx4IO9ZfTiMC3LFd539KuuOx3uOIKciWKMuCvcfct0rdVkXlDr+EnrmPzu1A4PavcJ0RScpI39jUQ==", -- "cpu": [ -- "x64" -- ], -- "license": "SEE LICENSE IN LICENSE.md", -- "optional": true, -- "os": [ -- "win32" -- ], -- "bin": { -- "copilot-win32-x64": "copilot.exe" -- } -- }, - "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==", +- "version": "4.4.3", +- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", +- "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@github/copilot-win32-arm64": { -- "version": "1.0.55-3", -- "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.55-3.tgz", -- "integrity": "sha512-uw88Yrfz/2aueonvN+HXHGMK1oeYB4vN0d1/NVMvSJJXr0dMuunjq8aOkIdyoGYfxIUYEZqj97YS7SmgcW5UIg==", +- "version": "1.0.57", +- "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.57.tgz", +- "integrity": "sha512-3TL2bd1/p/sYbNgDIqbnjES//zlXP5b0sPEXKQRrpVF9ZLN3vjQ1tmBWx8Qx7zn2J3oywH2dG7qKjuxWTJRXKA==", - "cpu": [ - "arm64" - ], @@ -516,9 +366,9 @@ index 3a191a0f..3aa27c91 100644 - } - }, - "node_modules/@github/copilot-win32-x64": { -- "version": "1.0.55-3", -- "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.55-3.tgz", -- "integrity": "sha512-w3vDZr3SbrA8lnV0bdElKsigGEqy/Fzr0p6FNvjlN814VLb67wNcDast5sLC4rpwfcjV/VrI4jTaNruePvbrbQ==", +- "version": "1.0.57", +- "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.57.tgz", +- "integrity": "sha512-zuKqRn0pIF+ZvuiMXbZkYK1AMlrV21kFTpyf5l7gdI1dzJuwHNI0Qfe0gzaZYaU1B4htbzMk9MhEbjR1PQcoJg==", - "cpu": [ - "x64" - ], @@ -534,43 +384,11 @@ index 3a191a0f..3aa27c91 100644 "node_modules/@gulp-sourcemaps/identity-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", -@@ -16913,6 +16588,7 @@ - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", -+ "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" -@@ -19526,15 +19202,6 @@ - "node": ">=18" - } - }, -- "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": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", -@@ -20068,6 +19735,7 @@ - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", -+ "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json -index 942259cc..11cf1a96 100644 +index a7bc41dcbfc..5d2243257a9 100644 --- a/package.json +++ b/package.json -@@ -40,9 +40,6 @@ +@@ -43,9 +43,6 @@ "watch-extensions": "npm run gulp watch-extensions watch-extension-media", "watch-extensionsd": "deemon npm run watch-extensions", "kill-watch-extensionsd": "deemon --kill npm run watch-extensions", @@ -580,7 +398,7 @@ index 942259cc..11cf1a96 100644 "precommit": "node --experimental-strip-types build/hygiene.ts", "gulp": "node --experimental-strip-types --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js", "electron": "node build/lib/electron.ts", -@@ -81,8 +78,6 @@ +@@ -84,8 +81,6 @@ "perf": "node scripts/code-perf.js", "perf:chat": "node scripts/chat-simulation/test-chat-perf-regression.js", "perf:chat-leak": "node scripts/chat-simulation/test-chat-mem-leaks.js", @@ -589,17 +407,35 @@ index 942259cc..11cf1a96 100644 "update-build-ts-version": "npm install -D typescript@next && npm install -D @typescript/native-preview && (cd build && npm run typecheck)", "install-local-component-explorer": "npm install ../vscode-packages/js-component-explorer/dist/vscode-component-explorer-0.1.0.tgz ../vscode-packages/js-component-explorer/dist/vscode-component-explorer-cli-0.1.0.tgz --no-save && cd build/rspack && npm install ../../../vscode-packages/js-component-explorer/dist/vscode-component-explorer-webpack-plugin-0.1.0.tgz --no-save && cd ../vite && npm install ../../../vscode-packages/js-component-explorer/dist/vscode-component-explorer-vite-plugin-0.1.0.tgz --no-save", "symlink-local-component-explorer": "npm install ../vscode-packages/js-component-explorer/packages/explorer ../vscode-packages/js-component-explorer/packages/cli --no-save && cd build/rspack && npm install ../../../vscode-packages/js-component-explorer/packages/webpack-plugin ../../../vscode-packages/js-component-explorer/packages/explorer --no-save && cd ../vite && npm install ../../../vscode-packages/js-component-explorer/packages/vite-plugin ../../../vscode-packages/js-component-explorer/packages/explorer --no-save", -@@ -90,8 +85,6 @@ +@@ -93,8 +88,6 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.82.0", -- "@github/copilot": "1.0.55-3", -- "@github/copilot-sdk": "1.0.0-beta.8", +- "@github/copilot": "^1.0.57", +- "@github/copilot-sdk": "^1.0.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/dev-tunnels-connections": "^1.3.41", +diff --git a/src/vs/platform/agentHost/common/otel/agentHostOTelService.ts b/src/vs/platform/agentHost/common/otel/agentHostOTelService.ts +index 667e3bc8c6d..51af8b2ce19 100644 +--- a/src/vs/platform/agentHost/common/otel/agentHostOTelService.ts ++++ b/src/vs/platform/agentHost/common/otel/agentHostOTelService.ts +@@ -3,7 +3,12 @@ + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +-import type { TelemetryConfig } from '@github/copilot-sdk'; ++// `TelemetryConfig` was originally imported from '@github/copilot-sdk'; the SDK ++// is removed in BradfordCode. This service still exposes a config shape that ++// mirrors the SDK's `TelemetryConfig`; keep the type symbol available at ++// compile time without depending on the removed package. ++// eslint-disable-next-line @typescript-eslint/no-explicit-any ++type TelemetryConfig = any; + import type { URI } from '../../../../base/common/uri.js'; + import { createDecorator } from '../../../instantiation/common/instantiation.js'; + diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts -index 3bbd6447..6888a6b4 100644 +index 59c5257d573..5b854613420 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -20,7 +20,6 @@ import { AgentService } from './agentService.js'; @@ -610,7 +446,7 @@ index 3bbd6447..6888a6b4 100644 import { CopilotApiService, ICopilotApiService } from './shared/copilotApiService.js'; import { ClaudeAgent } from './claude/claudeAgent.js'; import { ClaudeAgentSdkService, IClaudeAgentSdkService } from './claude/claudeAgentSdkService.js'; -@@ -164,7 +163,6 @@ async function startAgentHost(): Promise { +@@ -169,7 +168,6 @@ async function startAgentHost(): Promise { diServices.set(IAgentHostTerminalManager, agentService.terminalManager); diServices.set(IAgentConfigurationService, agentService.configurationService); diServices.set(IAgentHostCompletions, agentService.completionsService); @@ -619,7 +455,7 @@ index 3bbd6447..6888a6b4 100644 // `chat.agentHost.claudeAgent.path` workbench setting being non-empty, // forwarded by the agent host starters as `VSCODE_AGENT_HOST_CLAUDE_SDK_PATH`. diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts -index 5371e667..fb0ca1da 100644 +index 52a291518b6..6197da16587 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -32,7 +32,6 @@ import product from '../../product/common/product.js'; @@ -630,8 +466,8 @@ index 5371e667..fb0ca1da 100644 import { CopilotApiService, ICopilotApiService } from './shared/copilotApiService.js'; import { ClaudeAgent } from './claude/claudeAgent.js'; import { ClaudeAgentSdkService, IClaudeAgentSdkService } from './claude/claudeAgentSdkService.js'; -@@ -242,9 +241,6 @@ async function main(): Promise { - diServices.set(IClaudeAgentSdkService, claudeAgentSdkService); +@@ -251,9 +250,6 @@ async function main(): Promise { + diServices.set(ICodexProxyService, codexProxyService); const agentHostOTelService = disposables.add(instantiationService.createInstance(AgentHostOTelService)); diServices.set(IAgentHostOTelService, agentHostOTelService); - const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent)); @@ -642,23 +478,24 @@ index 5371e667..fb0ca1da 100644 // so make sure it is set even if the path was provided via CLI flag. 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 6426974f..00000000 +index 23d008d0f91..00000000000 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ /dev/null -@@ -1,2392 +0,0 @@ +@@ -1,2554 +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, ResumeSessionConfig, RuntimeConnection, type CopilotClientOptions, type SessionConfig } from '@github/copilot-sdk'; +-import { CopilotClient, RuntimeConnection, type CopilotClientOptions } from '@github/copilot-sdk'; -import * as fs from 'fs/promises'; --import { Limiter, SequencerByKey } from '../../../../base/common/async.js'; +-import { Limiter, SequencerByKey, Throttler } from '../../../../base/common/async.js'; +-import { CancellationTokenSource, type CancellationToken } from '../../../../base/common/cancellation.js'; -import { rgDiskPath } from '../../../../base/node/ripgrep.js'; -import { CancellationError } from '../../../../base/common/errors.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { appendEscapedMarkdownInlineCode } from '../../../../base/common/htmlContent.js'; --import { Disposable, DisposableMap, toDisposable } from '../../../../base/common/lifecycle.js'; +-import { Disposable, DisposableMap, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { FileAccess } from '../../../../base/common/network.js'; -import { equals } from '../../../../base/common/objects.js'; @@ -668,7 +505,7 @@ index 6426974f..00000000 -import { URI } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; -import { localize } from '../../../../nls.js'; --import { IParsedPlugin, parseAgentFile, parsePlugin } from '../../../agentPlugins/common/pluginParsers.js'; +-import { IParsedPlugin, parseAgentFile, parsePlugin, parseSkillFile } 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'; @@ -682,18 +519,17 @@ index 6426974f..00000000 -import { ProtectedResourceMetadata, type ChildCustomizationType, type ConfigSchema, type ModelSelection, type AgentSelection, type ToolDefinition } from '../../common/state/protocol/state.js'; -import { ActionType, type SessionAction } from '../../common/state/sessionActions.js'; -import { AHP_AUTH_REQUIRED, ProtocolError } from '../../common/state/sessionProtocol.js'; --import { CustomizationLoadStatus, CustomizationType, ResponsePartKind, SessionInputResponseKind, customizationId, parseSubagentSessionUri, type ChildCustomization, type ClientPluginCustomization, type Customization, type DirectoryCustomization, type MessageAttachment, type PendingMessage, type PolicyState, type ResponsePart, type SessionInputAnswer, type ToolCallResult, type Turn } from '../../common/state/sessionState.js'; +-import { CustomizationLoadStatus, CustomizationType, ResponsePartKind, SessionInputResponseKind, customizationId, parseSubagentSessionUri, type ChildCustomization, type ClientPluginCustomization, type Customization, type DirectoryCustomization, type MessageAttachment, type PendingMessage, type PluginCustomization, type PolicyState, type ResponsePart, type SessionInputAnswer, type ToolCallResult, type Turn } from '../../common/state/sessionState.js'; -import { IAgentConfigurationService } from '../agentConfigurationService.js'; -import { IAgentHostOTelService } from '../../common/otel/agentHostOTelService.js'; -import { IAgentHostCompletions } from '../agentHostCompletions.js'; -import { IAgentHostGitService, META_DIFF_BASE_BRANCH } from '../agentHostGitService.js'; -import { IAgentHostCheckpointService } from '../../common/agentHostCheckpointService.js'; --import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js'; --import { CopilotAgentSession, SessionWrapperFactory, type CopilotSdkMode, type IActiveClientSnapshot } from './copilotAgentSession.js'; +-import { CopilotAgentSession, type CopilotSdkMode } from './copilotAgentSession.js'; -import { ICopilotSessionContext, projectFromCopilotContext } from './copilotGitProject.js'; --import { parsedPluginsEqual, toChildCustomizations, toSdkCustomAgents, toSdkHooks, toSdkInstructionDirectories, toSdkMcpServers, toSdkSkillDirectories } from './copilotPluginConverters.js'; --import { CopilotSessionWrapper } from './copilotSessionWrapper.js'; --import { ShellManager, createShellTools } from './copilotShellTools.js'; +-import { parsedPluginsEqual, toChildCustomizations } from './copilotPluginConverters.js'; +-import { ShellManager } from './copilotShellTools.js'; +-import { CopilotSessionLauncher, ThinkingLevelConfigKey, getCopilotReasoningEffort, type CopilotSessionLaunchPlan, type IActiveClientSnapshot } from './copilotSessionLauncher.js'; -import { DiscoveredType, SessionCustomizationDiscovery, type IDiscoveredDirectory } from './sessionCustomizationDiscovery.js'; -import { SessionPluginBundler } from '../shared/sessionPluginBundler.js'; -import { CopilotSlashCommandCompletionProvider } from './copilotSlashCommandCompletionProvider.js'; @@ -704,6 +540,8 @@ index 6426974f..00000000 - readonly worktree: URI; -} - +-export type ICopilotPluginInfo = IParsedPlugin & { readonly pluginDir?: URI }; +- -/** - * A session that has been requested by a client but has not yet been - * materialized into a real Copilot SDK session, worktree, or persisted @@ -740,19 +578,7 @@ index 6426974f..00000000 - readonly project: IAgentSessionProjectInfo | undefined; -} - --const ThinkingLevelConfigKey = 'thinkingLevel'; --const ReasoningEfforts = ['low', 'medium', 'high', 'xhigh'] as const; --type ReasoningEffort = NonNullable; -- --export const COPILOT_AGENT_HOST_SYSTEM_MESSAGE = { -- mode: 'customize', -- sections: { -- identity: { -- action: 'replace', -- content: 'You are an AI assistant using Copilot CLI runtime in VS Code. You help users with software engineering tasks. When asked about your identity, you must state that you are an AI assistant using Copilot CLI runtime in VS Code.', -- }, -- }, --} satisfies NonNullable; +-export { COPILOT_AGENT_HOST_SYSTEM_MESSAGE } from './copilotSessionLauncher.js'; - -interface ISerializedModelSelection { - id?: unknown; @@ -793,10 +619,6 @@ index 6426974f..00000000 - readonly feedback?: string; -} - --function isReasoningEffort(value: string | undefined): value is ReasoningEffort { -- return ReasoningEfforts.some(reasoningEffort => reasoningEffort === value); --} -- -export function getCopilotWorktreesRoot(repositoryRoot: URI): URI { - return URI.joinPath(repositoryRoot, '..', `${basename(repositoryRoot.fsPath)}.worktrees`); -} @@ -926,6 +748,7 @@ index 6426974f..00000000 - private readonly _sessionSequencer = new SequencerByKey(); - private _shutdownPromise: Promise | undefined; - private readonly _plugins: PluginController; +- private readonly _sessionLauncher: CopilotSessionLauncher; - readonly onDidCustomizationsChange: Event; - /** Per-session active client state for tools + plugin snapshot tracking. */ - private readonly _activeClients = new ResourceMap(); @@ -933,10 +756,8 @@ index 6426974f..00000000 - constructor( - @ILogService private readonly _logService: ILogService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, -- @IFileService private readonly _fileService: IFileService, - @ISessionDataService private readonly _sessionDataService: ISessionDataService, - @IAgentHostGitService private readonly _gitService: IAgentHostGitService, -- @IAgentHostTerminalManager private readonly _terminalManager: IAgentHostTerminalManager, - @IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService, - @IAgentHostOTelService private readonly _otelService: IAgentHostOTelService, - @IAgentHostCompletions completions: IAgentHostCompletions, @@ -944,33 +765,59 @@ index 6426974f..00000000 - ) { - super(); - this._plugins = this._register(this._instantiationService.createInstance(PluginController)); +- this._sessionLauncher = this._instantiationService.createInstance(CopilotSessionLauncher); - this.onDidCustomizationsChange = this._plugins.onDidChange; - this._register(completions.registerProvider(new CopilotSlashCommandCompletionProvider(this.id, { - hasHistory: (sessionId) => !this._provisionalSessions.has(sessionId) && this._sessions.has(sessionId), +- isRubberDuckEnabled: () => this._isRubberDuckEnabled(), - }))); - -- // Restart the CLI client when session sync setting changes (only if idle) +- // Restart the CLI client when a setting baked into the client/subprocess at +- // startup changes, disposing any active sessions. Both session sync (a client +- // option) and the rubber duck flag (a subprocess env var) are applied in +- // `_ensureClient`, so they only take effect on the next client start. - this._register(this._configurationService.onDidRootConfigChange(() => { -- this._restartClientIfSessionSyncChanged().catch(err => -- this._logService.error('[Copilot] Failed to restart client after session sync change', err) +- this._restartClientIfStartupConfigChanged().catch(err => +- this._logService.error('[Copilot] Failed to restart client after config change', err) - ); - })); - } - - private _lastSessionSyncEnabled: boolean = this._isSessionSyncEnabled(); +- private _lastRubberDuckEnabled: boolean = this._isRubberDuckEnabled(); - - private _isSessionSyncEnabled(): boolean { - return this._configurationService.getRootValue(platformRootSchema, AgentHostSessionSyncEnabledConfigKey) === true; - } - -- private async _restartClientIfSessionSyncChanged(): Promise { -- const current = this._isSessionSyncEnabled(); -- if (this._lastSessionSyncEnabled === current) { +- private _isRubberDuckEnabled(): boolean { +- return this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.RubberDuck) === true; +- } +- +- /** +- * Restarts the CLI client when a config value that is only read at client +- * startup ({@link _isSessionSyncEnabled} client option, {@link _isRubberDuckEnabled} +- * subprocess env var) has changed. Any active sessions are disposed before +- * the client is stopped; the latest values are picked up the next time +- * {@link _ensureClient} runs. If the client is still starting up, the +- * in-flight start detects the change against {@link _lastSessionSyncEnabled} / +- * {@link _lastRubberDuckEnabled} and aborts so it never comes up stale. +- */ +- private async _restartClientIfStartupConfigChanged(): Promise { +- const sessionSync = this._isSessionSyncEnabled(); +- const rubberDuck = this._isRubberDuckEnabled(); +- if (this._lastSessionSyncEnabled === sessionSync && this._lastRubberDuckEnabled === rubberDuck) { - return; - } -- this._lastSessionSyncEnabled = current; -- if (this._client && this._sessions.size === 0) { -- this._logService.info(`[Copilot] Session sync changed to ${current}, restarting CopilotClient`); +- const changed = [ +- this._lastSessionSyncEnabled !== sessionSync ? `sessionSync=${sessionSync}` : undefined, +- this._lastRubberDuckEnabled !== rubberDuck ? `rubberDuck=${rubberDuck}` : undefined, +- ].filter((v): v is string => v !== undefined).join(', '); +- this._lastSessionSyncEnabled = sessionSync; +- this._lastRubberDuckEnabled = rubberDuck; +- if (this._client) { +- this._logService.info(`[Copilot] Startup config changed (${changed}), restarting CopilotClient`); +- this._sessions.clearAndDisposeAll(); - await this._stopClient(); - } - } @@ -998,7 +845,9 @@ index 6426974f..00000000 - } - - async getSessionCustomizations(session: URI): Promise { -- return this._plugins.getSessionCustomizationsSettled(await this._getSessionCustomizationDirectory(session)); +- const directory = await this._getSessionCustomizationDirectory(session); +- const activeClient = this._getOrCreateActiveClient(session, directory); +- return activeClient.pluginController.getCustomizationsSettled(); - } - - private async _getSessionCustomizationDirectory(session: URI): Promise { @@ -1091,21 +940,6 @@ index 6426974f..00000000 - } - return originalSendRequest(method, params); - }; -- -- // Handle the inbound `exitPlanMode.request` RPC the CLI dispatches -- // when the model invokes `exit_plan_mode`. Routing by `sessionId` -- // hands the request off to the matching {@link CopilotAgentSession}, -- // which surfaces it as a {@link SessionInputRequest} and resolves -- // this promise with the user's choice. -- const handlerDisposable = connection.onRequest('exitPlanMode.request', async (params: IExitPlanModeRequestParams): Promise => { -- const session = this._sessions.get(params.sessionId); -- if (!session) { -- this._logService.warn(`[Copilot] exitPlanMode.request for unknown session ${params.sessionId}`); -- return { approved: false }; -- } -- return session.handleExitPlanModeRequest(params); -- }); -- this._register(toDisposable(() => handlerDisposable.dispose())); - } - - // ---- client lifecycle --------------------------------------------------- @@ -1121,6 +955,11 @@ index 6426974f..00000000 - if (this._clientStarting) { - return this._clientStarting; - } +- // Snapshot the startup config so we can detect a change that lands while the +- // client is still starting and abort the stale start (the values are baked +- // into the client options / subprocess env below). +- const sessionSyncAtStartup = this._isSessionSyncEnabled(); +- const rubberDuckAtStartup = this._isRubberDuckEnabled(); - const clientStarting = (async () => { - this._logService.info('[Copilot] Starting CopilotClient... (with token)'); - @@ -1142,6 +981,15 @@ index 6426974f..00000000 - env['COPILOT_CLI_RUN_AS_NODE'] = '1'; - env['USE_BUILTIN_RIPGREP'] = 'false'; - +- // Enable the rubber duck critic subagent in the CLI when the agent host +- // config opts in. `RUBBER_DUCK_AGENT` is the SDK's required interface for +- // gating this experimental feature +- if (this._isRubberDuckEnabled()) { +- env['RUBBER_DUCK_AGENT'] = 'true'; +- } else { +- delete env['RUBBER_DUCK_AGENT']; +- } +- - // 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. @@ -1173,6 +1021,10 @@ index 6426974f..00000000 - await client.stop(); - throw new Error('Copilot authentication changed while the client was starting'); - } +- if (this._isSessionSyncEnabled() !== sessionSyncAtStartup || this._isRubberDuckEnabled() !== rubberDuckAtStartup) { +- await client.stop(); +- throw new Error('Copilot startup config changed while the client was starting'); +- } - this._logService.info('[Copilot] CopilotClient started successfully'); - this._enablePlanModeOnClient(client); - this._client = client; @@ -1218,11 +1070,6 @@ index 6426974f..00000000 - }; - } - -- private _getReasoningEffort(model: ModelSelection | undefined): SessionConfig['reasoningEffort'] { -- const thinkingLevel = model?.config?.[ThinkingLevelConfigKey]; -- return isReasoningEffort(thinkingLevel) ? thinkingLevel : undefined; -- } -- - private _serializeModelSelection(model: ModelSelection): string { - return JSON.stringify(model); - } @@ -1494,10 +1341,14 @@ index 6426974f..00000000 - // runs identically for provisional and real sessions; the SDK side - // of activeClient state isn't engaged until materialization. - if (config.activeClient) { -- const ac = this._getOrCreateActiveClient(sessionUri); +- const ac = this._getOrCreateActiveClient(sessionUri, config.workingDirectory); - ac.updateTools(config.activeClient.clientId, config.activeClient.tools); - if (config.activeClient.customizations !== undefined) { -- await this._plugins.sync(config.activeClient.clientId, config.activeClient.customizations, config.workingDirectory); +- // Provisional eager-create: no session-state listener is +- // hooked up yet, so suppress action events. The session +- // reads the final view via its initial snapshot once it +- // materializes. +- await ac.pluginController.sync(config.activeClient.clientId, config.activeClient.customizations, { quiet: true }); - } - } - @@ -1562,29 +1413,26 @@ index 6426974f..00000000 - // Always create an ActiveClient so the snapshot includes host + - // session-discovered customizations, even when no client has called - // `setClientCustomizations` / `setClientTools` yet. -- const activeClient = this._getOrCreateActiveClient(sessionUri); -- const snapshot = await activeClient.snapshot(customizationDirectory); +- const activeClient = this._getOrCreateActiveClient(sessionUri, customizationDirectory); +- const snapshot = await activeClient.snapshot(); - const workingDirectory = await this._resolveSessionWorkingDirectory(materializedConfig, sessionId, prompt); - const shellManager = this._instantiationService.createInstance(ShellManager, sessionUri, workingDirectory); -- const sessionConfigBuilder = this._buildSessionConfig(snapshot, shellManager); -- -- const factory: SessionWrapperFactory = async callbacks => { -- const resolvedAgentName = provisional.agent ? await this._resolveAgentName(provisional.sessionUri, snapshot, provisional.agent) : undefined; -- const raw = await client.createSession({ -- model: provisional.model?.id, -- reasoningEffort: this._getReasoningEffort(provisional.model), -- ...(resolvedAgentName ? { agent: resolvedAgentName } : {}), -- sessionId, -- streaming: true, -- workingDirectory: workingDirectory?.fsPath, -- ...await sessionConfigBuilder(callbacks), -- }); -- return new CopilotSessionWrapper(raw); -- }; - - let agentSession: CopilotAgentSession | undefined; - try { -- agentSession = this._createAgentSession(factory, sessionId, shellManager, workingDirectory, customizationDirectory, snapshot); +- const resolvedAgentName = provisional.agent ? await this._resolveAgentName(provisional.sessionUri, snapshot, provisional.agent) : undefined; +- const launchPlan: CopilotSessionLaunchPlan = { +- kind: 'create', +- client, +- sessionId, +- workingDirectory, +- resolvedAgentName, +- snapshot, +- shellManager, +- githubToken: this._githubToken, +- model: provisional.model, +- }; +- agentSession = this._createAgentSession(launchPlan, customizationDirectory); - await agentSession.initializeSession(); - this._registerInitializedSession(sessionId, agentSession); - } catch (error) { @@ -1690,14 +1538,13 @@ index 6426974f..00000000 - - async setClientCustomizations(session: URI, clientId: string, customizations: ClientPluginCustomization[]): Promise { - const directory = await this._getSessionCustomizationDirectory(session); -- return this._plugins.sync(clientId, customizations, directory, action => { -- this._onDidSessionProgress.fire({ kind: 'action', session, action }); -- }); +- const activeClient = this._getOrCreateActiveClient(session, directory); +- return activeClient.pluginController.sync(clientId, customizations); - } - - setClientTools(session: URI, clientId: string, tools: ToolDefinition[]): void { - const sessionId = AgentSession.id(session); -- const activeClient = this._getOrCreateActiveClient(session); +- const activeClient = this._getOrCreateActiveClient(session, undefined); - const hasCachedEntry = this._sessions.has(sessionId); - this._logService.info(`[Copilot:${sessionId}] setClientTools: clientId=${clientId}, tools=[${tools.map(t => t.name).join(', ') || '(none)'}], hasCachedSdkSession=${hasCachedEntry}`); - activeClient.updateTools(clientId, tools); @@ -1717,12 +1564,18 @@ index 6426974f..00000000 - } - - setCustomizationEnabled(uri: string, enabled: boolean): void { -- this._plugins.setEnabled(uri, enabled); +- // Enablement is per-session: fan out to every existing session +- // controller (provisional + materialized). New sessions start with +- // the default value baked into their customizations. +- for (const activeClient of this._activeClients.values()) { +- activeClient.pluginController.setEnabled(uri, enabled); +- } - } - - async sendMessage(session: URI, prompt: string, attachments?: readonly MessageAttachment[], turnId?: string): Promise { - const sessionId = AgentSession.id(session); - await this._sessionSequencer.queue(sessionId, async () => { +- await this._activeClients.get(session)?.pluginController.retryFailedClientSyncIfNeeded(); - - // First message on a provisional session: materialize the SDK - // session, worktree, and on-disk metadata before continuing. The @@ -1740,7 +1593,7 @@ index 6426974f..00000000 - const activeClient = this._activeClients.get(session); - const hadCachedEntry = !!entry; - this._logService.info(`[Copilot:${sessionId}] sendMessage: cachedEntry=${hadCachedEntry}, hasActiveClient=${!!activeClient}, activeClientId=${activeClient ? '(set)' : '(none)'}`); -- if (entry && activeClient && await activeClient.isOutdated(entry.appliedSnapshot, entry.customizationDirectory)) { +- if (entry && activeClient && await activeClient.isOutdated(entry.appliedSnapshot)) { - this._logService.info(`[Copilot:${sessionId}] Session config changed (isOutdated=true), refreshing session. snapshotClientId=${entry.appliedSnapshot.clientId}`); - this._sessions.deleteAndDispose(sessionId); - entry = undefined; @@ -2025,7 +1878,7 @@ index 6426974f..00000000 - } - const entry = this._sessions.get(sessionId); - if (entry) { -- await entry.setModel(model.id, this._getReasoningEffort(model)); +- await entry.setModel(model.id, getCopilotReasoningEffort(model)); - } - await this._storeSessionMetadata(session, model, undefined, undefined, undefined); - } @@ -2089,11 +1942,14 @@ index 6426974f..00000000 - - // ---- helpers ------------------------------------------------------------ - -- private _getOrCreateActiveClient(session: URI): ActiveClient { +- private _getOrCreateActiveClient(session: URI, directory: URI | undefined): ActiveClient { - let client = this._activeClients.get(session); - if (!client) { -- client = new ActiveClient(directory => this._plugins.getAppliedPlugins(directory)); +- const pluginController = this._plugins.createSessionController(directory); +- client = new ActiveClient(session, pluginController, this._onDidSessionProgress); - this._activeClients.set(session, client); +- } else if (directory) { +- client.pluginController.setDirectory(directory); - } - return client; - } @@ -2106,20 +1962,21 @@ index 6426974f..00000000 - * {@link _resumeSession} for the same id cannot dispose this entry mid-init - * via {@link DisposableMap.set}. - */ -- private _createAgentSession(wrapperFactory: SessionWrapperFactory, sessionId: string, shellManager: ShellManager, workingDirectory: URI | undefined, customizationDirectory: URI | undefined, snapshot?: IActiveClientSnapshot): CopilotAgentSession { -- const sessionUri = AgentSession.uri(this.id, sessionId); +- private _createAgentSession(launchPlan: CopilotSessionLaunchPlan, customizationDirectory: URI | undefined): CopilotAgentSession { +- const sessionUri = AgentSession.uri(this.id, launchPlan.sessionId); - - const agentSession = this._instantiationService.createInstance( - CopilotAgentSession, - { - sessionUri, -- rawSessionId: sessionId, +- rawSessionId: launchPlan.sessionId, - onDidSessionProgress: this._onDidSessionProgress, -- wrapperFactory, -- shellManager, -- workingDirectory, +- sessionLauncher: this._sessionLauncher, +- launchPlan, +- shellManager: launchPlan.shellManager, +- workingDirectory: launchPlan.workingDirectory, - customizationDirectory, -- clientSnapshot: snapshot, +- clientSnapshot: launchPlan.snapshot, - }, - ); - @@ -2153,10 +2010,12 @@ index 6426974f..00000000 - const provisional = this._provisionalSessions.get(sessionId); - if (provisional) { - this._provisionalSessions.delete(sessionId); +- this._activeClients.get(provisional.sessionUri)?.dispose(); - this._activeClients.delete(provisional.sessionUri); - return; - } - const entry = this._sessions.get(sessionId); +- const sessionUri = AgentSession.uri(this.id, sessionId); - if (entry) { - try { - await entry.destroySession(); @@ -2165,53 +2024,11 @@ index 6426974f..00000000 - } - } - this._sessions.deleteAndDispose(sessionId); +- this._activeClients.get(sessionUri)?.dispose(); +- this._activeClients.delete(sessionUri); - await this._removeCreatedWorktree(sessionId); - } - -- /** -- * 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(snapshot: IActiveClientSnapshot, shellManager: ShellManager): (args: Parameters[0]) => Promise { -- const plugins = snapshot.plugins; -- -- return async (callbacks: Parameters[0]) => { -- const disableCustomTerminalTool = this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.DisableCustomTerminalTool) === true; -- const shellTools = disableCustomTerminalTool ? [] : await createShellTools(shellManager, this._terminalManager, this._logService, callbacks.requestUnsandboxedCommandConfirmation); -- const customAgents = await toSdkCustomAgents(plugins.flatMap(p => p.agents), this._fileService); -- return { -- onPermissionRequest: callbacks.onPermissionRequest, -- onUserInputRequest: callbacks.onUserInputRequest, -- onElicitationRequest: callbacks.onElicitationRequest, -- hooks: toSdkHooks(plugins.flatMap(p => p.hooks), callbacks.hooks), -- mcpServers: toSdkMcpServers(plugins.flatMap(p => p.mcpServers)), -- customAgents, -- skillDirectories: toSdkSkillDirectories(plugins.flatMap(p => p.skills)), -- instructionDirectories: toSdkInstructionDirectories(plugins.flatMap(p => p.instructions)), -- systemMessage: COPILOT_AGENT_HOST_SYSTEM_MESSAGE, -- tools: [...shellTools, ...callbacks.clientTools], -- // Pass the GitHub token at the session level. The SDK's -- // client-level `gitHubToken` authenticates the CLI process, -- // but each session also needs its own token resolved into a -- // GitHub identity (login, Copilot plan, endpoints) to drive -- // model routing and quota — without this the session -- // errors with "Session was not created with authentication -- // info or custom provider" on first send. See #318693. -- gitHubToken: this._githubToken, -- // Enable infinite sessions so the SDK provisions a workspace -- // directory (containing `plan.md`, `checkpoints/`, `files/`). -- // The workspace is required for plan mode to work — without -- // it, `rpc.plan.read()` returns `path: null` and the SDK -- // never emits `exit_plan_mode.requested`. -- infiniteSessions: { enabled: true }, -- }; -- }; -- } -- - protected _resumeSession(sessionId: string): Promise { - const existing = this._resumingSessions.get(sessionId); - if (existing) { @@ -2238,8 +2055,8 @@ index 6426974f..00000000 - // Always create an ActiveClient so the snapshot includes host + - // session-discovered customizations, even when no client has called - // `setClientCustomizations` / `setClientTools` yet. -- const activeClient = this._getOrCreateActiveClient(sessionUri); -- const snapshot = await activeClient.snapshot(customizationDirectory); +- const activeClient = this._getOrCreateActiveClient(sessionUri, customizationDirectory); +- const snapshot = await activeClient.snapshot(); - const sessionMetadata = await client.getSessionMetadata(sessionId).catch(err => { - this._logService.warn(`[Copilot:${sessionId}] getSessionMetadata failed`, err); - return undefined; @@ -2250,48 +2067,22 @@ index 6426974f..00000000 - } - - const shellManager = this._instantiationService.createInstance(ShellManager, sessionUri, workingDirectory); -- const sessionConfig = this._buildSessionConfig(snapshot, shellManager); -- -- const factory: SessionWrapperFactory = async callbacks => { -- const config = await sessionConfig(callbacks); -- const resolvedAgentName = storedMetadata.agent ? await this._resolveAgentName(sessionUri, snapshot, storedMetadata.agent) : undefined; -- try { -- this._logService.info(`[Copilot:${sessionId}] Calling SDK resumeSession...`); -- const raw = await client.resumeSession(sessionId, { -- ...config, -- workingDirectory: workingDirectory?.fsPath, -- ...(resolvedAgentName ? { agent: resolvedAgentName } : {}), -- }); -- this._logService.info(`[Copilot:${sessionId}] SDK resumeSession succeeded`); -- return new CopilotSessionWrapper(raw); -- } catch (err) { -- const errCode = (err as { code?: number })?.code; -- const errMsg = err instanceof Error ? err.message : String(err); -- this._logService.warn(`[Copilot:${sessionId}] SDK resumeSession failed: code=${errCode}, message=${errMsg}`); -- // 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 || errCode !== -32603) { -- throw err; -- } -- -- this._logService.warn(`[Copilot:${sessionId}] Resume failed (code=-32603), falling back to createSession with same ID`); -- const raw = await client.createSession({ -- ...config, -- sessionId, -- streaming: true, -- model: storedMetadata.model?.id, -- reasoningEffort: this._getReasoningEffort(storedMetadata.model), -- ...(resolvedAgentName ? { agent: resolvedAgentName } : {}), -- workingDirectory: workingDirectory?.fsPath, -- }); -- this._logService.info(`[Copilot:${sessionId}] Fallback createSession succeeded`); -- -- return new CopilotSessionWrapper(raw); -- } +- const resolvedAgentName = storedMetadata.agent ? await this._resolveAgentName(sessionUri, snapshot, storedMetadata.agent) : undefined; +- const launchPlan: CopilotSessionLaunchPlan = { +- kind: 'resume', +- client, +- sessionId, +- workingDirectory, +- resolvedAgentName, +- snapshot, +- shellManager, +- githubToken: this._githubToken, +- fallback: { +- model: storedMetadata.model, +- }, - }; - -- const agentSession = this._createAgentSession(factory, sessionId, shellManager, workingDirectory, customizationDirectory, snapshot); +- const agentSession = this._createAgentSession(launchPlan, customizationDirectory); - try { - await agentSession.initializeSession(); - } catch (err) { @@ -2546,6 +2337,10 @@ index 6426974f..00000000 - } - - override dispose(): void { +- for (const ac of this._activeClients.values()) { +- ac.dispose(); +- } +- this._activeClients.clear(); - this.shutdown().catch(err => { - this._logService.warn('[Copilot] Shutdown failed during dispose', err); - }).finally(() => super.dispose()); @@ -2553,9 +2348,16 @@ index 6426974f..00000000 -} - -interface IResolvedCustomization { -- readonly customization: Customization; +- readonly customization: PluginCustomization; - readonly pluginDir?: URI; - readonly plugin?: IParsedPlugin; +- /** +- * The original client-published input. Retained so a later +- * {@link SessionPluginController.retryFailedClientSyncIfNeeded} can +- * re-issue the sync without needing the caller to re-supply it (in +- * particular, the opaque `nonce` is preserved). +- */ +- readonly input?: ClientPluginCustomization; -} - -/** @@ -2576,6 +2378,8 @@ index 6426974f..00000000 - - private readonly _discovery: SessionCustomizationDiscovery; - private readonly _bundler: SessionPluginBundler; +- private readonly _refreshThrottler = this._register(new Throttler()); +- private _refreshCancellationSource: CancellationTokenSource | undefined; - - private _customizations: readonly DirectoryCustomization[] = []; - private _plugin: IParsedPlugin | undefined; @@ -2594,12 +2398,18 @@ index 6426974f..00000000 - this._discovery = this._register(instantiationService.createInstance(SessionCustomizationDiscovery, workingDirectory, userHome)); - this._bundler = this._register(instantiationService.createInstance(SessionPluginBundler, workingDirectory)); - this._fileService = instantiationService.invokeFunction(accessor => accessor.get(IFileService)); -- this._settled = this._refresh(); +- this._settled = this._queueRefresh(false); - this._register(this._discovery.onDidChange(() => { -- this._settled = this._refresh().finally(() => this._onDidRefresh()); +- this._settled = this._queueRefresh(true); - })); - } - +- override dispose(): void { +- this._refreshCancellationSource?.dispose(true); +- this._refreshCancellationSource = undefined; +- super.dispose(); +- } +- - whenSettled(): Promise { - return this._settled; - } @@ -2612,22 +2422,64 @@ index 6426974f..00000000 - return this._plugin; - } - -- private async _refresh(): Promise { +- private _queueRefresh(notify: boolean): Promise { +- this._refreshCancellationSource?.cancel(); +- return this._refreshThrottler.queue(async throttlerToken => { +- const refreshCancellationSource = new CancellationTokenSource(throttlerToken); +- this._refreshCancellationSource = refreshCancellationSource; +- try { +- const didRefresh = await this._refresh(refreshCancellationSource.token); +- if (didRefresh && notify && !refreshCancellationSource.token.isCancellationRequested) { +- this._onDidRefresh(); +- } +- } finally { +- if (this._refreshCancellationSource === refreshCancellationSource) { +- this._refreshCancellationSource = undefined; +- } +- refreshCancellationSource.dispose(); +- } +- }); +- } +- +- private async _refresh(token: CancellationToken): Promise { - try { - const directories = await this._discovery.directories(); -- this._customizations = await toDiscoveredDirectoryCustomizations(directories, this._fileService); -- this._plugin = undefined; +- if (token.isCancellationRequested) { +- return false; +- } +- +- const customizations = await toDiscoveredDirectoryCustomizations(directories, this._fileService); +- if (token.isCancellationRequested) { +- return false; +- } +- +- const bundleResult = await this._bundler.bundle(directories, token); +- if (token.isCancellationRequested) { +- return false; +- } - -- const bundleResult = await this._bundler.bundle(directories); +- // Don't update `_customizations` / `_plugin` when cancelled. +- // Otherwise a cancelled refresh could temporarily clear them and cause callers to see empty customizations. - if (!bundleResult) { -- return; +- this._customizations = customizations; +- this._plugin = undefined; +- } else { +- const pluginDir = URI.parse(bundleResult.ref.uri); +- const plugin = await this._resolvePlugin(pluginDir); +- this._customizations = customizations; +- this._plugin = plugin; - } -- const pluginDir = URI.parse(bundleResult.ref.uri); -- this._plugin = await this._resolvePlugin(pluginDir); +- return true; - } catch (err) { +- // Don't update `_customizations` / `_plugin` when cancelled. +- // Otherwise a cancelled refresh could temporarily clear them and cause callers to see empty customizations. +- if (token.isCancellationRequested) { +- return false; +- } - this._logService.warn(`[Copilot:SessionDiscoveredEntry] Discovery/bundle failed: ${err instanceof Error ? err.message : String(err)}`); - this._customizations = []; - this._plugin = undefined; +- return true; - } - } -} @@ -2656,6 +2508,7 @@ index 6426974f..00000000 - case DiscoveredType.Skill: - return CustomizationType.Skill; - case DiscoveredType.Instruction: +- case DiscoveredType.AgentInstruction: - return CustomizationType.Rule; - } -} @@ -2670,50 +2523,71 @@ index 6426974f..00000000 - id, - uri, - name: agentInfo.name, +- ...(agentInfo.description ? { description: agentInfo.description } : {}), - }; - } - if (type === DiscoveredType.Skill) { +- const skillInfo = await parseSkillFile(file, fileService); - return { - type: CustomizationType.Skill, - id, - uri, - name: resourceBasename(resourceDirname(file)), +- ...(skillInfo.description ? { description: skillInfo.description } : {}), +- }; +- } +- if (type === DiscoveredType.Instruction) { +- return { +- type: CustomizationType.Rule, +- id, +- uri, +- name: resourceBasename(file), - }; - } +- // agent instruction - return { - type: CustomizationType.Rule, +- alwaysApply: true, - id, - uri, - name: resourceBasename(file), - }; -} - +-/** +- * Process-wide plugin state shared across all sessions. +- * +- * Owns: +- * - host-configured customizations (read from root config, watched, parsed) +- * - the {@link IAgentPluginManager} that materializes plugin source URIs +- * into a nonce-deduped on-disk cache (one shared directory for all +- * sessions and clients) +- * - parsing + resolution helpers used by both host- and client-side +- * customizations +- * +- * Per-session state (client-published customizations, on-disk +- * customization discovery for the session's working directory, +- * enablement overrides) lives on {@link SessionPluginController}, +- * one per {@link CopilotAgentSession}. Each session controller holds +- * a reference back to this shared controller for the resolve/sync +- * helpers it needs. +- */ -class PluginController extends Disposable { - private readonly _onDidChange = this._register(new Emitter()); +- /** Fires when host customizations change. Session controllers forward this. */ - readonly onDidChange = this._onDidChange.event; - -- private readonly _enablement = new Map(); -- private _clientCustomizations: readonly IResolvedCustomization[] = []; - private _hostCustomizations: readonly IResolvedCustomization[] = []; -- private _clientSync: Promise = Promise.resolve([]); - private _hostSync: Promise = Promise.resolve([]); -- private _clientRevision = 0; - private _hostRevision = 0; - private _lastAppliedRefs: readonly Customization[] = []; - -- /** -- * Per-working-directory bundles built from on-disk discovery -- * (workspace + user-home conventions). Lazily created on first access -- * by {@link _getOrCreateSessionEntry}; lifetime tied to this controller. -- */ -- private readonly _sessionDiscovered = new Map(); -- - constructor( -- @IAgentPluginManager private readonly _pluginManager: IAgentPluginManager, -- @ILogService private readonly _logService: ILogService, -- @IFileService private readonly _fileService: IFileService, +- @IAgentPluginManager public readonly pluginManager: IAgentPluginManager, +- @ILogService public readonly logService: ILogService, +- @IFileService public readonly fileService: IFileService, - @IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService, -- @IInstantiationService private readonly _instantiationService: IInstantiationService, +- @IInstantiationService public readonly instantiationService: IInstantiationService, - ) { - super(); - @@ -2724,110 +2598,35 @@ index 6426974f..00000000 - })); - } - -- override dispose(): void { -- for (const entry of this._sessionDiscovered.values()) { -- entry.dispose(); -- } -- this._sessionDiscovered.clear(); -- super.dispose(); -- } -- - public getConfiguredHostCustomizations(): readonly Customization[] { - return this._hostCustomizations.map(item => item.customization); - } - -- public getSessionCustomizations(directory: URI | undefined): readonly Customization[] { -- const result: Customization[] = [ -- ...this._hostCustomizations.map(item => this._applyEnablement(item.customization)), -- ...this._clientCustomizations.map(item => this._applyEnablement(item.customization)), -- ]; -- const entry = directory ? this._getOrCreateSessionEntry(directory) : undefined; -- const discovered = entry?.currentCustomizations() ?? []; -- for (const customization of discovered) { -- result.push(this._applyEnablement(customization)); -- } -- return result; -- } -- - /** -- * Settled variant of {@link getSessionCustomizations}: awaits the -- * in-flight host sync, the in-flight client sync, and (when a directory -- * is supplied) the session-discovered entry's initial scan + parse -- * before snapshotting the customization list. -- * -- * Callers that publish customizations into session state at session -- * creation time MUST use this — the synchronous variant can return an -- * empty list for a brand-new working directory because -- * {@link SessionDiscoveredEntry} kicks off its `_refresh()` in its -- * constructor without anyone awaiting it. +- * Snapshot the resolved host customizations (loading or loaded). Used by +- * {@link SessionPluginController} to compose its per-session view. - */ -- public async getSessionCustomizationsSettled(directory: URI | undefined): Promise { -- const entry = directory ? this._getOrCreateSessionEntry(directory) : undefined; -- await Promise.all([ -- this._hostSync.catch(err => { -- this._logService.warn('[Copilot:PluginController] Host customization update failed', err); -- }), -- this._clientSync.catch(err => { -- this._logService.warn('[Copilot:PluginController] Customization sync failed', err); -- }), -- entry?.whenSettled(), -- ]); -- return this.getSessionCustomizations(directory); +- public hostCustomizations(): readonly IResolvedCustomization[] { +- return this._hostCustomizations; - } - -- /** -- * Returns the current parsed plugins, awaiting any pending sync. -- */ -- public async getAppliedPlugins(directory: URI | undefined): Promise { -- const entry = directory ? this._getOrCreateSessionEntry(directory) : undefined; -- const [host, client] = await Promise.all([ -- this._hostSync.catch(err => { -- this._logService.warn('[Copilot:PluginController] Host customization update failed', err); -- return this._hostCustomizations; -- }), -- this._clientSync.catch(err => { -- this._logService.warn('[Copilot:PluginController] Customization sync failed', err); -- return this._clientCustomizations; -- }), -- entry?.whenSettled(), -- ]); -- -- const discovered = entry?.currentCustomizations() ?? []; -- const sessionPlugin = discovered.some(customization => this._isEnabled(customization)) ? entry?.currentPlugin() : undefined; -- const sessionPlugins: IParsedPlugin[] = sessionPlugin ? [sessionPlugin] : []; -- -- return [ -- ...host.filter(item => -- !!item.plugin -- && this._isEnabled(item.customization) -- ).map(item => item.plugin!), -- ...client.filter(item => -- !!item.plugin -- && this._isEnabled(item.customization) -- ).map(item => item.plugin!), -- ...sessionPlugins, -- ]; +- /** In-flight host sync; awaited by `getCustomizationsSettled` consumers. */ +- public hostSync(): Promise { +- return this._hostSync; - } - -- private _getOrCreateSessionEntry(directory: URI): SessionDiscoveredEntry { -- const key = directory.toString(); -- let entry = this._sessionDiscovered.get(key); -- if (!entry) { -- entry = new SessionDiscoveredEntry( -- directory, -- URI.file(this._getUserHome()), -- uri => this._tryParsePlugin(uri), -- () => this._onDidChange.fire(), -- this._logService, -- this._instantiationService, -- ); -- this._sessionDiscovered.set(key, entry); -- } -- return entry; +- public getUserHome(): string { +- return process.env['HOME'] ?? process.env['USERPROFILE'] ?? ''; - } - -- public setEnabled(pluginProtocolUri: string, enabled: boolean) { -- this._enablement.set(pluginProtocolUri, enabled); +- /** +- * Construct a per-session controller bound to the given customization +- * directory. The returned controller is a {@link Disposable} owned by +- * the caller; disposing it releases the session's disk-discovery +- * watchers and detaches from this controller's change event. +- */ +- public createSessionController(directory: URI | undefined): SessionPluginController { +- return new SessionPluginController(this, directory); - } - - /** @@ -2851,7 +2650,7 @@ index 6426974f..00000000 - }, - })); - this._onDidChange.fire(); -- this._hostSync = Promise.all(customizations.map(customization => this._resolveConfiguredCustomization(customization))).then(resolved => { +- this._hostSync = Promise.all(customizations.map(customization => this.resolveConfiguredCustomization(customization))).then(resolved => { - if (revision === this._hostRevision) { - this._hostCustomizations = resolved; - } @@ -2863,7 +2662,201 @@ index 6426974f..00000000 - }); - } - -- public sync(clientId: string, customizations: ClientPluginCustomization[], directory: URI | undefined, publish?: (action: SessionAction) => void) { +- public async resolveConfiguredCustomization(customization: PluginCustomization): Promise { +- const pluginDir = URI.parse(customization.uri); +- const parsed = await this.tryParsePlugin(pluginDir); +- if (!parsed) { +- return { +- customization: { +- ...customization, +- load: { kind: CustomizationLoadStatus.Error, message: localize('copilotAgent.pluginParseError', "Error parsing plugin.") }, +- }, +- }; +- } +- +- return { +- customization: { +- ...customization, +- load: { kind: CustomizationLoadStatus.Loaded }, +- children: toChildCustomizations([parsed]), +- }, +- pluginDir, +- plugin: parsed, +- }; +- } +- +- public async resolveSyncedCustomization(item: ISyncedCustomization, clientId: string, input: ClientPluginCustomization | undefined): Promise { +- const baseCustomization: PluginCustomization = { ...item.customization, clientId }; +- if (!item.pluginDir) { +- return { customization: baseCustomization, input }; +- } +- +- const parsed = await this.tryParsePlugin(item.pluginDir); +- if (!parsed) { +- return { +- customization: { +- ...baseCustomization, +- load: { kind: CustomizationLoadStatus.Error, message: localize('copilotAgent.pluginParseError', "Error parsing plugin.") }, +- }, +- input, +- }; +- } +- +- return { +- customization: { +- ...baseCustomization, +- children: toChildCustomizations([parsed]), +- }, +- pluginDir: item.pluginDir, +- plugin: parsed, +- input, +- }; +- } +- +- public async tryParsePlugin(pluginDir: URI): Promise { +- try { +- return await parsePlugin(pluginDir, this.fileService, undefined, this.getUserHome()); +- } catch (error) { +- this.logService.warn(`[Copilot:PluginController] Error parsing plugin '${pluginDir.toString()}': ${error instanceof Error ? error.message : String(error)}`); +- return undefined; +- } +- } +-} +- +-/** +- * Per-session view over {@link PluginController}. +- * +- * Owns the session-scoped slice of plugin state — published client +- * customizations, on-disk-discovered customizations under the session's +- * customization directory, and the user's per-session enablement +- * overrides — and exposes a {@link onDidPublish} stream of +- * {@link SessionAction}s targeted at *this* session (no cross-session +- * routing). +- * +- * Created via {@link PluginController.createSessionController}. The +- * caller owns the returned disposable and disposes it when the session +- * (provisional or materialized) is torn down. +- */ +-class SessionPluginController extends Disposable { +- private readonly _onDidPublish = this._register(new Emitter()); +- /** Per-session action stream (reset + per-item updates). */ +- readonly onDidPublish = this._onDidPublish.event; +- +- private readonly _enablement = new Map(); +- private _clientCustomizations: readonly IResolvedCustomization[] = []; +- private _clientSync: Promise = Promise.resolve([]); +- private _clientRevision = 0; +- /** Last clientId seen via {@link sync}; used by {@link retryFailedClientSyncIfNeeded}. */ +- private _clientId: string | undefined; +- +- private readonly _sessionDiscovered: MutableDisposable = this._register(new MutableDisposable()); +- +- constructor( +- private readonly _parent: PluginController, +- private _directory: URI | undefined, +- ) { +- super(); +- } +- +- public get directory(): URI | undefined { +- return this._directory; +- } +- +- /** +- * Anchor (or re-anchor) the session's customization directory. +- * Only ever transitions from `undefined` → set; once a directory has +- * been bound the discovered entry is pinned to it for the remainder +- * of the session. +- */ +- public setDirectory(directory: URI | undefined): void { +- if (this._directory || !directory) { +- return; +- } +- this._directory = directory; +- } +- +- public getCustomizations(): readonly Customization[] { +- const result: Customization[] = [ +- ...this._parent.hostCustomizations().map(item => this._applyEnablement(item.customization)), +- ...this._clientCustomizations.map(item => this._applyEnablement(item.customization)), +- ]; +- const entry = this._discoveredEntry(); +- const discovered = entry?.currentCustomizations() ?? []; +- for (const customization of discovered) { +- result.push(this._applyEnablement(customization)); +- } +- return result; +- } +- +- /** +- * Settled variant of {@link getCustomizations}: awaits the in-flight +- * host sync, the in-flight client sync, and the discovered entry's +- * initial scan + parse before snapshotting the list. Callers that +- * publish customizations into session state at session creation time +- * MUST use this — the synchronous variant can return an empty list +- * for a brand-new working directory because {@link SessionDiscoveredEntry} +- * kicks off its `_refresh()` without anyone awaiting it. +- */ +- public async getCustomizationsSettled(): Promise { +- const entry = this._discoveredEntry(); +- await Promise.all([ +- this._parent.hostSync().catch(err => this._parent.logService.warn('[Copilot:SessionPluginController] Host customization update failed', err)), +- this._clientSync.catch(err => this._parent.logService.warn('[Copilot:SessionPluginController] Client customization sync failed', err)), +- entry?.whenSettled(), +- ]); +- return this.getCustomizations(); +- } +- +- /** Returns the parsed plugins currently enabled for this session, awaiting any pending sync. */ +- public async getAppliedPlugins(): Promise { +- const entry = this._discoveredEntry(); +- const [host, client] = await Promise.all([ +- this._parent.hostSync().catch(err => { +- this._parent.logService.warn('[Copilot:SessionPluginController] Host customization update failed', err); +- return this._parent.hostCustomizations(); +- }), +- this._clientSync.catch(err => { +- this._parent.logService.warn('[Copilot:SessionPluginController] Client customization sync failed', err); +- return this._clientCustomizations; +- }), +- entry?.whenSettled(), +- ]); +- +- const discovered = entry?.currentCustomizations() ?? []; +- const sessionPlugin = discovered.some(customization => this._isEnabled(customization)) ? entry?.currentPlugin() : undefined; +- const sessionPlugins: IParsedPlugin[] = sessionPlugin ? [sessionPlugin] : []; +- +- return [ +- ...host.filter(item => !!item.plugin && this._isEnabled(item.customization)) +- .map(item => ({ ...item.plugin!, pluginDir: item.pluginDir })), +- ...client.filter(item => !!item.plugin && this._isEnabled(item.customization)) +- .map(item => ({ ...item.plugin!, pluginDir: item.pluginDir })), +- ...sessionPlugins, +- ]; +- } +- +- /** +- * Set per-session enablement for a customization (by protocol URI). +- */ +- public setEnabled(pluginProtocolUri: string, enabled: boolean): void { +- const prev = this._enablement.get(pluginProtocolUri); +- if (prev === enabled) { +- return; +- } +- this._enablement.set(pluginProtocolUri, enabled); +- } +- +- /** +- * Sync the published client customizations for this session. +- * +- * @param quiet when `true`, suppress {@link onDidPublish} events for +- * this sync. Used during eager-create paths where there is no +- * session listener yet; the session-state snapshot picks up the +- * final view directly when the session materializes. +- */ +- public sync(clientId: string, customizations: ClientPluginCustomization[], options?: { quiet?: boolean }) { +- const quiet = options?.quiet === true; +- this._clientId = clientId; - const revision = ++this._clientRevision; - this._clientCustomizations = customizations.map(customization => ({ - customization: { @@ -2871,11 +2864,14 @@ index 6426974f..00000000 - clientId, - load: { kind: CustomizationLoadStatus.Loading }, - }, +- input: customization, - })); -- publish?.({ -- type: ActionType.SessionCustomizationsChanged, -- customizations: [...this.getSessionCustomizations(directory)], -- }); +- if (!quiet) { +- this._onDidPublish.fire({ +- type: ActionType.SessionCustomizationsChanged, +- customizations: [...this.getCustomizations()], +- }); +- } - const published = new Map(); - for (const customization of this._clientCustomizations) { - const enabled = this._applyEnablement(customization.customization); @@ -2887,137 +2883,139 @@ index 6426974f..00000000 - return; - } - published.set(customization.uri, customization); -- publish?.({ -- type: ActionType.SessionCustomizationUpdated, -- customization, -- }); +- if (!quiet) { +- this._onDidPublish.fire({ +- type: ActionType.SessionCustomizationUpdated, +- customization, +- }); +- } - }; - - const prev = this._clientSync; - const promise = this._clientSync = prev.catch(err => { -- this._logService.warn('[Copilot:PluginController] Previous customization sync failed', err); +- this._parent.logService.warn('[Copilot:SessionPluginController] Previous customization sync failed', err); - }).then(async () => { -- const result = await this._pluginManager.syncCustomizations(clientId, customizations, status => { +- const inputByUri = new Map(customizations.map(c => [c.uri, c])); +- const result = await this._parent.pluginManager.syncCustomizations(clientId, customizations, status => { - if (revision !== this._clientRevision) { - return; - } -- -- publishUpdate({ customization: { ...status, clientId } }); +- publishUpdate({ +- customization: { ...status, clientId }, +- input: inputByUri.get(status.uri), +- }); - }); - -- const resolved = await Promise.all(result.map(item => this._resolveSyncedCustomization(item, clientId))); -- if (revision === this._clientRevision) { -- this._clientCustomizations = resolved; -- for (const item of resolved) { -- publishUpdate(item); -- } -- } -- return resolved; -- }); -- -- return promise.then(results => results.map(item => ({ -- customization: this._applyEnablement(item.customization), -- ...(item.pluginDir ? { pluginDir: item.pluginDir } : {}), -- }))); -- } -- -- private _isEnabled(customization: Customization): boolean { -- return this._enablement.get(customization.uri) ?? customization.enabled; -- } -- -- private _applyEnablement(customization: Customization): Customization { -- const enabled = this._isEnabled(customization); -- return customization.enabled === enabled ? customization : { ...customization, enabled }; -- } -- -- private async _resolveConfiguredCustomization(customization: Customization): Promise { -- const pluginDir = URI.parse(customization.uri); -- const parsed = await this._tryParsePlugin(pluginDir); -- if (!parsed) { -- return { -- customization: { -- ...customization, -- load: { kind: CustomizationLoadStatus.Error, message: localize('copilotAgent.pluginParseError', "Error parsing plugin.") }, -- }, -- }; -- } -- -- return { -- customization: { -- ...customization, -- load: { kind: CustomizationLoadStatus.Loaded }, -- children: toChildCustomizations([parsed]), -- }, -- pluginDir, -- plugin: parsed, -- }; +- const resolved = await Promise.all(result.map(item => this._parent.resolveSyncedCustomization(item, clientId, inputByUri.get(item.customization.uri)))); +- if (revision === this._clientRevision) { +- this._clientCustomizations = resolved; +- for (const item of resolved) { +- publishUpdate(item); +- } +- } +- return resolved; +- }); +- +- return promise.then(results => results.map(item => ({ +- customization: this._applyEnablement(item.customization), +- ...(item.pluginDir ? { pluginDir: item.pluginDir } : {}), +- }))); - } - -- private async _resolveSyncedCustomization(item: ISyncedCustomization, clientId: string): Promise { -- const baseCustomization: Customization = { ...item.customization, clientId }; -- if (!item.pluginDir) { -- return { customization: baseCustomization }; +- /** +- * Re-issue the last client sync if any previously-synced customization +- * is currently in an error state. Used to recover from transient +- * sync failures (e.g. a `vscode-agent-host://` connection drop during +- * reconnection) at message boundaries. Re-syncs **only** the errored +- * items and always non-quiet so listeners observe recovery. +- */ +- public async retryFailedClientSyncIfNeeded(): Promise { +- await this._clientSync.catch(() => { }); +- if (!this._clientId) { +- return; - } -- -- const parsed = await this._tryParsePlugin(item.pluginDir); -- if (!parsed) { -- return { -- customization: { -- ...baseCustomization, -- load: { kind: CustomizationLoadStatus.Error, message: localize('copilotAgent.pluginParseError', "Error parsing plugin.") }, -- }, -- }; +- const errored = this._clientCustomizations.filter(item => +- item.customization.load?.kind === CustomizationLoadStatus.Error +- && item.input !== undefined +- ); +- if (errored.length === 0) { +- return; - } -- -- return { -- customization: { -- ...baseCustomization, -- children: toChildCustomizations([parsed]), -- }, -- pluginDir: item.pluginDir, -- plugin: parsed, -- }; +- const inputs = errored.map(item => item.input!); +- this._parent.logService.info(`[Copilot:SessionPluginController] Retrying ${inputs.length} previously-failed client customization(s)`); +- await this.sync(this._clientId, inputs).catch(err => { +- this._parent.logService.warn('[Copilot:SessionPluginController] Retried client customization sync failed', err); +- }); - } - -- private async _tryParsePlugin(pluginDir: URI): Promise { -- try { -- return await parsePlugin(pluginDir, this._fileService, undefined, this._getUserHome()); -- } catch (error) { -- this._logService.warn(`[Copilot:PluginController] Error parsing plugin '${pluginDir.toString()}': ${error instanceof Error ? error.message : String(error)}`); +- private _discoveredEntry(): SessionDiscoveredEntry | undefined { +- if (!this._directory) { - return undefined; - } +- if (!this._sessionDiscovered.value) { +- this._sessionDiscovered.value = new SessionDiscoveredEntry( +- this._directory, +- URI.file(this._parent.getUserHome()), +- uri => this._parent.tryParsePlugin(uri), +- () => this._onDidPublish.fire({ +- type: ActionType.SessionCustomizationsChanged, +- customizations: [...this.getCustomizations()], +- }), +- this._parent.logService, +- this._parent.instantiationService, +- ); +- } +- return this._sessionDiscovered.value; - } - -- private _getUserHome(): string { -- return process.env['HOME'] ?? process.env['USERPROFILE'] ?? ''; +- private _isEnabled(customization: Customization): boolean { +- return this._enablement.get(customization.uri) ?? customization.enabled; +- } +- +- private _applyEnablement(customization: T): T { +- const enabled = this._isEnabled(customization); +- return customization.enabled === enabled ? customization : { ...customization, enabled }; - } -} - -/** - * Tracks per-session active client contributions (tools and plugins). -- * The {@link snapshot} captures the state at session creation time, and -- * {@link isOutdated} detects when the session needs to be refreshed. +- * Owns the session's {@link SessionPluginController}, which is the +- * authoritative source for both the plugin snapshot (host + client + +- * session-discovered) and per-session action events. Disposing this +- * tears down the controller and any disk watchers it created. - */ --class ActiveClient { +-class ActiveClient extends Disposable { - private _tools: readonly ToolDefinition[] = []; - private _clientId = ''; - +- public readonly pluginController: SessionPluginController; +- - constructor( -- /** Resolves the current set of applied plugins. May block while a sync is in progress. */ -- private readonly _resolvePlugins: (directory: URI | undefined) => Promise, -- ) { } +- private readonly _sessionUri: URI, +- pluginController: SessionPluginController, +- onDidSessionProgress: Emitter, +- ) { +- super(); +- this.pluginController = this._register(pluginController); +- // Forward per-session publish events into the agent's progress +- // stream. This replaces the previous clientId-based routing. +- this._register(this.pluginController.onDidPublish(action => { +- onDidSessionProgress.fire({ kind: 'action', session: this._sessionUri, action }); +- })); +- } - - updateTools(clientId: string, tools: readonly ToolDefinition[]): void { - this._clientId = clientId; - this._tools = tools; - } - -- async snapshot(directory: URI | undefined): Promise { -- return { clientId: this._clientId, tools: this._tools, plugins: await this._resolvePlugins(directory) }; +- async snapshot(): Promise { +- return { clientId: this._clientId, tools: this._tools, plugins: await this.pluginController.getAppliedPlugins() }; - } - -- async isOutdated(snap: IActiveClientSnapshot, directory: URI | undefined): Promise { -- const plugins = await this._resolvePlugins(directory); +- async isOutdated(snap: IActiveClientSnapshot): Promise { +- const plugins = await this.pluginController.getAppliedPlugins(); - if (!parsedPluginsEqual(snap.plugins, plugins)) { - return true; - } @@ -3040,16 +3038,16 @@ index 6426974f..00000000 -} 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 f04a7cd1..00000000 +index 770817327b0..00000000000 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ /dev/null -@@ -1,2413 +0,0 @@ +@@ -1,2389 +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 { MessageOptions, PermissionRequestResult, SessionConfig, Tool, ToolResultObject } from '@github/copilot-sdk'; +-import type { ExitPlanModeRequest, MessageOptions, PermissionRequestResult, SessionConfig, Tool, ToolResultObject } from '@github/copilot-sdk'; -import { DeferredPromise } from '../../../../base/common/async.js'; -import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js'; -import { Emitter } from '../../../../base/common/event.js'; @@ -3063,7 +3061,6 @@ index f04a7cd1..00000000 -import { URI } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; -import { localize } from '../../../../nls.js'; --import type { IParsedPlugin } from '../../../agentPlugins/common/pluginParsers.js'; -import { INativeEnvironmentService } from '../../../environment/common/environment.js'; -import { IFileService } from '../../../files/common/files.js'; -import { IInstantiationService } from '../../../instantiation/common/instantiation.js'; @@ -3076,12 +3073,13 @@ index f04a7cd1..00000000 -import type { LanguageModelToolInvokedClassification, LanguageModelToolInvokedEvent } from '../../../telemetry/common/languageModelToolTelemetry.js'; -import { SessionConfigKey } from '../../common/sessionConfigKeys.js'; -import { ISessionDatabase, ISessionDataService, SESSION_ATTACHMENTS_DIRNAME } from '../../common/sessionDataService.js'; --import { MessageAttachmentKind, type FileEdit, type MessageAttachment, type ToolDefinition } from '../../common/state/protocol/state.js'; +-import { MessageAttachmentKind, ToolCallContributorKind, type FileEdit, type MessageAttachment } from '../../common/state/protocol/state.js'; -import { ActionType, type SessionAction } from '../../common/state/sessionActions.js'; -import { MessageKind, ResponsePartKind, SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, type PendingMessage, type SessionInputAnswer, type SessionInputOption, type SessionInputQuestion, type SessionInputRequest, type ToolCallResult, type ToolResultContent, type Turn, type UsageInfo } from '../../common/state/sessionState.js'; -import { IAgentConfigurationService } from '../agentConfigurationService.js'; --import type { IExitPlanModeRequestParams, IExitPlanModeResponse } from './copilotAgent.js'; +-import type { IExitPlanModeResponse } from './copilotAgent.js'; -import { CopilotSessionWrapper } from './copilotSessionWrapper.js'; +-import type { CopilotSessionLaunchPlan, IActiveClientSnapshot, ICopilotSessionLauncher, ICopilotSessionRuntime } from './copilotSessionLauncher.js'; -import { buildCopilotSystemNotification } from './copilotSystemNotification.js'; -import { parseLeadingSlashCommand } from './copilotSlashCommandCompletionProvider.js'; -import type { IUnsandboxedCommandConfirmationRequest, ShellManager } from './copilotShellTools.js'; @@ -3294,45 +3292,14 @@ index f04a7cd1..00000000 -} - -/** -- * Immutable snapshot of the active client's contributions at session creation -- * time. Used to detect when the session needs to be refreshed. -- */ --export interface IActiveClientSnapshot { -- readonly clientId: string; -- readonly tools: readonly ToolDefinition[]; -- readonly plugins: readonly IParsedPlugin[]; --} -- --/** -- * 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: ITypedPermissionRequest) => Promise; -- readonly onUserInputRequest: (request: UserInputRequest, invocation: { sessionId: string }) => Promise; -- readonly onElicitationRequest: (context: ElicitationContext) => Promise; -- readonly requestUnsandboxedCommandConfirmation: (request: IUnsandboxedCommandConfirmationRequest) => Promise; -- readonly hooks: { -- readonly onPreToolUse: (input: PreToolUseHookInput) => Promise; -- readonly onPostToolUse: (input: PostToolUseHookInput) => Promise; -- }; -- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- readonly clientTools: Tool[]; --}) => Promise; -- --/** - * Options for constructing a {@link CopilotAgentSession}. - */ -export interface ICopilotAgentSessionOptions { - readonly sessionUri: URI; - readonly rawSessionId: string; - readonly onDidSessionProgress: Emitter; -- readonly wrapperFactory: SessionWrapperFactory; +- readonly sessionLauncher: ICopilotSessionLauncher; +- readonly launchPlan: CopilotSessionLaunchPlan; - readonly shellManager: ShellManager | undefined; - /** Working directory associated with the session, used to strip redundant `cd` prefixes from shell commands. */ - readonly workingDirectory?: URI; @@ -3424,7 +3391,8 @@ index f04a7cd1..00000000 - private readonly _pendingEditContentUris = new Map(); - - private readonly _onDidSessionProgress: Emitter; -- private readonly _wrapperFactory: SessionWrapperFactory; +- private readonly _sessionLauncher: ICopilotSessionLauncher; +- private readonly _launchPlan: CopilotSessionLaunchPlan; - private readonly _shellManager: ShellManager | undefined; - private readonly _workingDirectory: URI | undefined; - private readonly _customizationDirectory: URI | undefined; @@ -3455,7 +3423,8 @@ index f04a7cd1..00000000 - this.sessionId = options.rawSessionId; - this.sessionUri = options.sessionUri; - this._onDidSessionProgress = options.onDidSessionProgress; -- this._wrapperFactory = options.wrapperFactory; +- this._sessionLauncher = options.sessionLauncher; +- this._launchPlan = options.launchPlan; - this._shellManager = options.shellManager; - this._workingDirectory = options.workingDirectory; - this._customizationDirectory = options.customizationDirectory; @@ -3747,7 +3716,7 @@ index f04a7cd1..00000000 - * for the client to dispatch `session/toolCallComplete`. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- createClientSdkTools(): Tool[] { +- private _createClientSdkTools(): Tool[] { - const tools = this._appliedSnapshot.tools; - if (tools.length === 0) { - return []; @@ -3811,24 +3780,14 @@ index f04a7cd1..00000000 - } - - /** -- * Creates (or resumes) the SDK session via the injected factory and +- * Creates (or resumes) the SDK session via the injected launcher and - * wires up all event listeners. Must be called exactly once after - * construction before using the session. - */ - async initializeSession(): Promise { -- const wrapper = await this._wrapperFactory({ -- onPermissionRequest: request => this.handlePermissionRequest(request), -- onUserInputRequest: (request, invocation) => this.handleUserInputRequest(request, invocation), -- onElicitationRequest: context => this.handleElicitationRequest(context), -- requestUnsandboxedCommandConfirmation: request => this.requestUnsandboxedCommandConfirmation(request), -- clientTools: this.createClientSdkTools(), -- hooks: { -- onPreToolUse: input => this._handlePreToolUse(input), -- onPostToolUse: input => this._handlePostToolUse(input), -- }, -- }); +- const wrapper = await this._sessionLauncher.launch(this._launchPlan, this._createRuntimeAdapter()); - // The session may have been disposed while we were awaiting the -- // wrapper factory. If so, dispose the freshly-created wrapper and +- // launcher. If so, dispose the freshly-created wrapper and - // skip subscribing — registering on a disposed store would leak. - if (this._store.isDisposed) { - wrapper.dispose(); @@ -3839,6 +3798,19 @@ index f04a7cd1..00000000 - this._subscribeForLogging(); - } - +- private _createRuntimeAdapter(): ICopilotSessionRuntime { +- return { +- handlePermissionRequest: request => this._handlePermissionRequest(request), +- handleExitPlanModeRequest: (request, invocation) => this._handleExitPlanModeRequest(request, invocation), +- handleUserInputRequest: (request, invocation) => this._handleUserInputRequest(request, invocation), +- handleElicitationRequest: context => this._handleElicitationRequest(context), +- requestUnsandboxedCommandConfirmation: request => this._requestUnsandboxedCommandConfirmation(request), +- createClientSdkTools: () => this._createClientSdkTools(), +- handlePreToolUse: input => this._handlePreToolUse(input), +- handlePostToolUse: input => this._handlePostToolUse(input), +- }; +- } +- - // ---- session operations ------------------------------------------------- - - async send(prompt: string, attachments?: readonly MessageAttachment[], turnId?: string, mode?: CopilotSdkMode): Promise { @@ -3865,7 +3837,7 @@ index f04a7cd1..00000000 - prompt = slashCommand.rest; - } - if (slashCommand?.command === 'rubber-duck') { -- if (!process.env['RUBBER_DUCK_AGENT']) { +- if (this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.RubberDuck) !== true) { - // Feature not enabled — pass the remaining text through as a plain - // message rather than injecting agent instructions for an unavailable agent. - prompt = slashCommand.rest; @@ -4077,7 +4049,7 @@ index f04a7cd1..00000000 - * (which transitions the tool to PendingConfirmation) and waiting for the - * side-effects layer to respond via {@link respondToPermissionRequest}. - */ -- async handlePermissionRequest( +- private async _handlePermissionRequest( - request: ITypedPermissionRequest, - ): Promise { - this._logService.info(`[Copilot:${this.sessionId}] Permission request: kind=${request.kind}`); @@ -4236,14 +4208,15 @@ index f04a7cd1..00000000 - * SDK's pre-call permission prompt is redundant in that case. - * - * Returns false when shell tools are not registered (the SDK's built-in -- * terminal runs unsandboxed via `AgentHostConfigKey.DisableCustomTerminalTool`) +- * terminal runs unsandboxed unless `AgentHostConfigKey.EnableCustomTerminalTool` +- * is set) - * so the standard confirmation flow is preserved. - */ - private async _isShellSandboxedByDefault(): Promise { - if (!this._shellManager) { - return false; - } -- if (this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.DisableCustomTerminalTool) === true) { +- if (this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.EnableCustomTerminalTool) !== true) { - return false; - } - return this._shellManager.getOrCreateSandboxEngine().isEnabled(); @@ -4322,7 +4295,7 @@ index f04a7cd1..00000000 - return false; - } - -- async requestUnsandboxedCommandConfirmation(request: IUnsandboxedCommandConfirmationRequest): Promise { +- private async _requestUnsandboxedCommandConfirmation(request: IUnsandboxedCommandConfirmationRequest): Promise { - const deferred = new DeferredPromise(); - this._pendingPermissions.set(request.toolCallId, deferred); - @@ -4368,7 +4341,7 @@ index f04a7cd1..00000000 - * `user_input_request` progress event and waiting for the renderer to - * respond via {@link respondToUserInputRequest}. - */ -- async handleUserInputRequest( +- private async _handleUserInputRequest( - request: UserInputRequest, - _invocation: { sessionId: string }, - ): Promise { @@ -4459,7 +4432,7 @@ index f04a7cd1..00000000 - * available to fill in a form, and accepting with empty content would - * be misleading to the MCP server. - */ -- async handleElicitationRequest(context: ElicitationContext): Promise { +- private async _handleElicitationRequest(context: ElicitationContext): Promise { - const isAutopilot = this._configurationService.getEffectiveValue(this.sessionUri.toString(), platformSessionSchema, SessionConfigKey.AutoApprove) === 'autopilot'; - if (isAutopilot) { - return { action: 'cancel' }; @@ -4800,6 +4773,7 @@ index f04a7cd1..00000000 - const toolKind = getToolKind(e.data.toolName); - const subagentMeta = toolKind === 'subagent' ? getSubagentMetadata(parameters) : undefined; - const toolClientId = this._clientToolNames.has(e.data.toolName) ? this._appliedSnapshot.clientId : undefined; +- const contributor = toolClientId ? { kind: ToolCallContributorKind.Client, clientId: toolClientId } as const : undefined; - - // A new tool call invalidates the current markdown and reasoning - // parts so the next text/reasoning delta after the tool call @@ -4832,7 +4806,7 @@ index f04a7cd1..00000000 - toolCallId: e.data.toolCallId, - toolName: e.data.toolName, - displayName, -- toolClientId, +- contributor, - _meta: meta, - }, parentToolCallId); - @@ -5073,7 +5047,7 @@ index f04a7cd1..00000000 - * paused `exit_plan_mode` tool call and (on accept) updates the SDK's - * `currentMode` so the model can continue with implementation. - */ -- async handleExitPlanModeRequest(data: IExitPlanModeRequestParams): Promise { +- private async _handleExitPlanModeRequest(data: ExitPlanModeRequest, _invocation: { sessionId: string }): Promise { - const requestId = generateUuid(); - const questionId = generateUuid(); - this._logService.info(`[Copilot:${this.sessionId}] exitPlanMode.request: rpcId=${requestId}, actions=[${data.actions.join(',')}], recommended=${data.recommendedAction}`); @@ -5409,7 +5383,7 @@ index f04a7cd1..00000000 - * `autoApproveEdits: true` is set whenever the chosen action is one of the - * autopilot variants, mirroring the CLI behavior. - */ --function autoApproveExitPlanMode(data: IExitPlanModeRequestParams): IExitPlanModeResponse { +-function autoApproveExitPlanMode(data: ExitPlanModeRequest): IExitPlanModeResponse { - const choices = data.actions ?? []; - const isAutopilotAction = (action: string) => action === 'autopilot' || action === 'autopilot_fleet'; - @@ -5459,10 +5433,10 @@ index f04a7cd1..00000000 -} 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 af849e86..00000000 +index 6ac611adf64..00000000000 --- a/src/vs/platform/agentHost/node/copilot/copilotPluginConverters.ts +++ /dev/null -@@ -1,411 +0,0 @@ +@@ -1,418 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. @@ -5557,9 +5531,16 @@ index af849e86..00000000 - const description = md?.getStringValue('description'); - const tools = md?.getStringArrayValue('tools'); - const prompt = md?.body ?? raw; +- let model: string | undefined = md?.getStringValue('model') ?? undefined; +- const models = md?.getStringArrayValue('model') ?? undefined; +- if (!model && models && Array.isArray(models) && models.length > 0) { +- model = models[0]; +- } +- - configs.push({ - name, - ...(description ? { description } : {}), +- ...(model ? { model } : {}), - tools: tools && tools.length > 0 ? tools : null, - prompt, - }); @@ -5793,90 +5774,372 @@ index af849e86..00000000 - }; - } - -- // User-prompt-submitted handler -- const promptCommands = commandsByKey.get('onUserPromptSubmitted'); -- if (promptCommands?.length) { -- hooks.onUserPromptSubmitted = async (input: UserPromptSubmittedHookInput) => { -- const stdin = JSON.stringify(input); -- for (const cmd of promptCommands) { -- 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: UserPromptSubmittedHookInput) => { +- 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: SessionStartHookInput) => { +- 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: SessionEndHookInput) => { +- 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: ErrorOccurredHookInput) => { +- 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 })), +- instructions: p.instructions.map(i => ({ uri: i.uri.toString(), name: i.name })), +- }))); +- }; +- return serialize(a) === serialize(b); +-} +diff --git a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts +deleted file mode 100644 +index 043c025c8f6..00000000000 +--- a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts ++++ /dev/null +@@ -1,276 +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 { CopilotClient, ExitPlanModeRequest, ExitPlanModeResult, PermissionRequestResult, ResumeSessionConfig, SessionConfig, Tool } from '@github/copilot-sdk'; +-import { coalesce } from '../../../../base/common/arrays.js'; +-import { Schemas } from '../../../../base/common/network.js'; +-import { URI } from '../../../../base/common/uri.js'; +-import { IFileService } from '../../../files/common/files.js'; +-import { ILogService } from '../../../log/common/log.js'; +-import { AgentHostConfigKey, agentHostCustomizationConfigSchema } from '../../common/agentHostCustomizationConfig.js'; +-import { AgentHostSessionSyncEnabledConfigKey, platformRootSchema } from '../../common/agentHostSchema.js'; +-import { IAgentConfigurationService } from '../agentConfigurationService.js'; +-import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js'; +-import type { ModelSelection, ToolDefinition } from '../../common/state/protocol/state.js'; +-import { CopilotSessionWrapper } from './copilotSessionWrapper.js'; +-import { ShellManager, createShellTools, type IUnsandboxedCommandConfirmationRequest } from './copilotShellTools.js'; +-import { toSdkCustomAgents, toSdkHooks, toSdkInstructionDirectories, toSdkMcpServers, toSdkSkillDirectories } from './copilotPluginConverters.js'; +-import type { ITypedPermissionRequest } from './copilotToolDisplay.js'; +-import type { ICopilotPluginInfo } from './copilotAgent.js'; +- +-export const ThinkingLevelConfigKey = 'thinkingLevel'; +- +-const ReasoningEfforts = ['low', 'medium', 'high', 'xhigh'] as const; +-type ReasoningEffort = NonNullable; +- +-type UserInputHandler = NonNullable; +-type UserInputRequest = Parameters[0]; +-type UserInputInvocation = Parameters[1]; +-type UserInputResponse = Awaited>; +-type ElicitationHandler = NonNullable; +-type ElicitationContext = Parameters[0]; +-type ElicitationResult = Awaited>; +-type SessionHooks = NonNullable; +-type PreToolUseHookInput = Parameters>[0]; +-type PostToolUseHookInput = Parameters>[0]; +-type CopilotSessionLaunchConfig = ResumeSessionConfig & { +- readonly pluginDirectories?: string[]; +- readonly remoteSession?: 'export'; +-}; +- +-export const COPILOT_AGENT_HOST_SYSTEM_MESSAGE = { +- mode: 'customize', +- sections: { +- identity: { +- action: 'replace', +- content: 'You are an AI assistant using Copilot CLI runtime in VS Code. You help users with software engineering tasks. When asked about your identity, you must state that you are an AI assistant using Copilot CLI runtime in VS Code.', +- }, +- }, +-} satisfies NonNullable; +- +-/** +- * Immutable snapshot of the active client's contributions at session creation +- * time. Used to detect when the session needs to be refreshed. +- */ +-export interface IActiveClientSnapshot { +- readonly clientId: string; +- readonly tools: readonly ToolDefinition[]; +- readonly plugins: readonly ICopilotPluginInfo[]; +-} +- +-export interface ICopilotSessionRuntime { +- handlePermissionRequest(request: ITypedPermissionRequest): Promise; +- handleExitPlanModeRequest(request: ExitPlanModeRequest, invocation: { sessionId: string }): Promise; +- handleUserInputRequest(request: UserInputRequest, invocation: UserInputInvocation): Promise; +- handleElicitationRequest(context: ElicitationContext): Promise; +- requestUnsandboxedCommandConfirmation(request: IUnsandboxedCommandConfirmationRequest): Promise; +- handlePreToolUse(input: PreToolUseHookInput): Promise; +- handlePostToolUse(input: PostToolUseHookInput): Promise; +- // eslint-disable-next-line @typescript-eslint/no-explicit-any +- createClientSdkTools(): Tool[]; +-} +- +-export interface ICopilotSessionLauncher { +- /** +- * Creates an unowned SDK session wrapper. The caller is responsible for +- * registering or disposing the returned wrapper. +- */ +- launch(plan: CopilotSessionLaunchPlan, runtime: ICopilotSessionRuntime): Promise; +-} +- +-type CopilotSessionClient = Pick; +- +-interface ICopilotSessionLaunchBase { +- readonly client: CopilotSessionClient; +- readonly sessionId: string; +- readonly workingDirectory: URI | undefined; +- readonly resolvedAgentName: string | undefined; +- readonly snapshot: IActiveClientSnapshot; +- readonly shellManager: ShellManager | undefined; +- readonly githubToken: string | undefined; +-} +- +-export interface ICopilotCreateSessionLaunchPlan extends ICopilotSessionLaunchBase { +- readonly kind: 'create'; +- readonly model: ModelSelection | undefined; +-} +- +-export interface ICopilotResumeSessionLaunchPlan extends ICopilotSessionLaunchBase { +- readonly kind: 'resume'; +- readonly workingDirectory: URI; +- readonly fallback: { +- readonly model: ModelSelection | undefined; +- }; +-} +- +-export type CopilotSessionLaunchPlan = ICopilotCreateSessionLaunchPlan | ICopilotResumeSessionLaunchPlan; +- +-function isReasoningEffort(value: string | undefined): value is ReasoningEffort { +- return ReasoningEfforts.some(reasoningEffort => reasoningEffort === value); +-} +- +-function getCopilotSdkErrorCode(err: unknown): number | undefined { +- if (typeof err !== 'object' || err === null) { +- return undefined; +- } +- const code = Object.getOwnPropertyDescriptor(err, 'code')?.value; +- return typeof code === 'number' ? code : undefined; +-} +- +-function getErrorMessage(err: unknown): string { +- if (err instanceof Error) { +- return err.message; +- } +- if (typeof err === 'object' && err !== null) { +- const message = Object.getOwnPropertyDescriptor(err, 'message')?.value; +- if (typeof message === 'string') { +- return message; +- } +- } +- return String(err); +-} +- +-/** +- * Decide whether a Copilot SDK `resumeSession` failure should fall back to +- * `createSession({ sessionId })`. We want to preserve the original +- * recovery for empty / truncated sessions (e.g. after the user invoked +- * "Start Over", which calls `truncateSession` and leaves the on-disk +- * session with zero events - the SDK then refuses to resume it), but we +- * must NOT silently swallow corruption / schema-validation / parse +- * failures: those should surface so the user sees the real error and the +- * original session contents are not masked by a fresh empty session. +- * +- * Heuristic: any `-32603` Internal Error is treated as the empty-session +- * case UNLESS the message clearly indicates corruption, schema +- * validation, parse failure, or malformed input. +- */ +-function shouldCreateEmptySessionAfterResumeError(err: unknown): boolean { +- if (getCopilotSdkErrorCode(err) !== -32603) { +- return false; +- } +- +- const message = getErrorMessage(err); +- return !/\b(corrupt|corrupted|invalid|validation|schema|must be|parse|malformed|unexpected token)\b/i.test(message); +-} - -- // Session-start handler -- const startCommands = commandsByKey.get('onSessionStart'); -- if (startCommands?.length) { -- hooks.onSessionStart = async (input: SessionStartHookInput) => { -- const stdin = JSON.stringify(input); -- for (const cmd of startCommands) { -- try { -- await executeHookCommand(cmd, stdin); -- } catch { -- // Hook failures are non-fatal -- } +-export function getCopilotReasoningEffort(model: ModelSelection | undefined): SessionConfig['reasoningEffort'] { +- const thinkingLevel = model?.config?.[ThinkingLevelConfigKey]; +- return isReasoningEffort(thinkingLevel) ? thinkingLevel : undefined; +-} +- +-export class CopilotSessionLauncher implements ICopilotSessionLauncher { +- +- constructor( +- @IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService, +- @IAgentHostTerminalManager private readonly _terminalManager: IAgentHostTerminalManager, +- @ILogService private readonly _logService: ILogService, +- @IFileService private readonly _fileService: IFileService, +- ) { } +- +- async launch(plan: CopilotSessionLaunchPlan, runtime: ICopilotSessionRuntime): Promise { +- const config = await this._buildSessionConfig(plan, runtime); +- if (plan.kind === 'create') { +- return this._createSession(plan, config); +- } +- +- try { +- this._logService.info(`[Copilot:${plan.sessionId}] Calling SDK resumeSession...`); +- const raw = await plan.client.resumeSession(plan.sessionId, { +- ...config, +- workingDirectory: plan.workingDirectory.fsPath, +- ...(plan.resolvedAgentName ? { agent: plan.resolvedAgentName } : {}), +- }); +- this._logService.info(`[Copilot:${plan.sessionId}] SDK resumeSession succeeded`); +- return new CopilotSessionWrapper(raw); +- } catch (err) { +- const errCode = getCopilotSdkErrorCode(err); +- const errMsg = getErrorMessage(err); +- this._logService.warn(`[Copilot:${plan.sessionId}] SDK resumeSession failed: code=${errCode}, message=${errMsg}`); +- // 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 (!shouldCreateEmptySessionAfterResumeError(err)) { +- throw err; - } -- }; +- +- this._logService.warn(`[Copilot:${plan.sessionId}] Resume failed (code=-32603), falling back to createSession with same ID`); +- const wrapper = await this._createSession({ +- ...plan, +- kind: 'create', +- model: plan.fallback.model, +- }, config); +- this._logService.info(`[Copilot:${plan.sessionId}] Fallback createSession succeeded`); +- return wrapper; +- } - } - -- // Session-end handler -- const endCommands = commandsByKey.get('onSessionEnd'); -- if (endCommands?.length) { -- hooks.onSessionEnd = async (input: SessionEndHookInput) => { -- const stdin = JSON.stringify(input); -- for (const cmd of endCommands) { -- try { -- await executeHookCommand(cmd, stdin); -- } catch { -- // Hook failures are non-fatal -- } -- } -- }; +- private async _createSession(plan: ICopilotCreateSessionLaunchPlan, config: CopilotSessionLaunchConfig): Promise { +- const raw = await plan.client.createSession({ +- ...config, +- sessionId: plan.sessionId, +- streaming: true, +- model: plan.model?.id, +- reasoningEffort: getCopilotReasoningEffort(plan.model), +- ...(plan.resolvedAgentName ? { agent: plan.resolvedAgentName } : {}), +- workingDirectory: plan.workingDirectory?.fsPath, +- }); +- return new CopilotSessionWrapper(raw); - } - -- // Error-occurred handler -- const errorCommands = commandsByKey.get('onErrorOccurred'); -- if (errorCommands?.length) { -- hooks.onErrorOccurred = async (input: ErrorOccurredHookInput) => { -- const stdin = JSON.stringify(input); -- for (const cmd of errorCommands) { -- try { -- await executeHookCommand(cmd, stdin); -- } catch { -- // Hook failures are non-fatal -- } +- private async _buildSessionConfig(plan: CopilotSessionLaunchPlan, runtime: ICopilotSessionRuntime): Promise { +- const plugins = plan.snapshot.plugins; +- const enableCustomTerminalTool = this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.EnableCustomTerminalTool) === true; +- let shellTools: Awaited> = []; +- if (enableCustomTerminalTool) { +- if (!plan.shellManager) { +- throw new Error(`ShellManager is required to launch Copilot session '${plan.sessionId}'`); - } +- shellTools = await createShellTools(plan.shellManager, this._terminalManager, this._logService, request => runtime.requestUnsandboxedCommandConfirmation(request)); +- } +- // Rely on SDK to find all agents/skills & the like from the plugins instead of us feeding them. +- // Else we could end up with duplicates or the like. +- const pluginsWithoutDirs = plugins.filter(p => !p.pluginDir || p.pluginDir.scheme !== Schemas.file); +- const customAgents = await toSdkCustomAgents(pluginsWithoutDirs.flatMap(p => p.agents), this._fileService); +- return { +- clientName: 'vscode', +- onPermissionRequest: request => runtime.handlePermissionRequest(request), +- onUserInputRequest: (request, invocation) => runtime.handleUserInputRequest(request, invocation), +- onElicitationRequest: context => runtime.handleElicitationRequest(context), +- hooks: toSdkHooks(pluginsWithoutDirs.flatMap(p => p.hooks), { +- onPreToolUse: input => runtime.handlePreToolUse(input), +- onPostToolUse: input => runtime.handlePostToolUse(input), +- }), +- mcpServers: toSdkMcpServers(pluginsWithoutDirs.flatMap(p => p.mcpServers)), +- onExitPlanModeRequest: (request, invocation) => runtime.handleExitPlanModeRequest(request, invocation), +- workingDirectory: plan.workingDirectory?.fsPath, +- customAgents, +- skillDirectories: toSdkSkillDirectories(pluginsWithoutDirs.flatMap(p => p.skills)), +- instructionDirectories: toSdkInstructionDirectories(plugins.flatMap(p => p.instructions)), +- systemMessage: COPILOT_AGENT_HOST_SYSTEM_MESSAGE, +- pluginDirectories: coalesce(plugins.map(p => p.pluginDir)) +- .filter(d => d.scheme === Schemas.file).map(d => d.fsPath), +- tools: [...shellTools, ...runtime.createClientSdkTools()], +- // Pass the GitHub token at the session level. The SDK's +- // client-level `gitHubToken` authenticates the CLI process, +- // but each session also needs its own token resolved into a +- // GitHub identity (login, Copilot plan, endpoints) to drive +- // model routing and quota — without this the session +- // errors with "Session was not created with authentication +- // info or custom provider" on first send. See #318693. +- gitHubToken: plan.githubToken, +- // Enable infinite sessions so the SDK provisions a workspace +- // directory (containing `plan.md`, `checkpoints/`, `files/`). +- // The workspace is required for plan mode to work — without +- // it, `rpc.plan.read()` returns `path: null` and the SDK +- // never emits `exit_plan_mode.requested`. +- infiniteSessions: { enabled: true }, +- // Per-session remote export: the client-level `--remote` flag +- // (enableRemoteSessions) enables the CLI capability, but each +- // session must opt in via `remoteSession` to actually export +- // events. Without this, sessions default to "off". +- remoteSession: this._configurationService.getRootValue(platformRootSchema, AgentHostSessionSyncEnabledConfigKey) === true ? 'export' : undefined, - }; - } -- -- 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 })), -- instructions: p.instructions.map(i => ({ uri: i.uri.toString(), name: i.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 0890c64e..00000000 +index 0890c64e4b1..00000000000 --- a/src/vs/platform/agentHost/node/copilot/copilotSessionWrapper.ts +++ /dev/null @@ -1,227 +0,0 @@ @@ -6109,7 +6372,7 @@ index 0890c64e..00000000 -} 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 14004b8a..00000000 +index 14004b8afb1..00000000000 --- a/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts +++ /dev/null @@ -1,1093 +0,0 @@ @@ -7208,10 +7471,10 @@ index 14004b8a..00000000 -} diff --git a/src/vs/platform/agentHost/node/copilot/copilotSlashCommandCompletionProvider.ts b/src/vs/platform/agentHost/node/copilot/copilotSlashCommandCompletionProvider.ts deleted file mode 100644 -index 5e0c5baf..00000000 +index 4943986a0e1..00000000000 --- a/src/vs/platform/agentHost/node/copilot/copilotSlashCommandCompletionProvider.ts +++ /dev/null -@@ -1,126 +0,0 @@ +@@ -1,132 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. @@ -7249,6 +7512,11 @@ index 5e0c5baf..00000000 -export interface ICopilotSlashCommandSessionInfo { - /** `sessionId` is the raw id (URI path without the leading slash). */ - hasHistory(sessionId: string): boolean; +- /** +- * Whether the experimental rubber duck critic subagent is enabled via +- * the agent host config. When absent or `false`, `/rubber-duck` is hidden. +- */ +- isRubberDuckEnabled?(): boolean; -} - -/** @@ -7311,6 +7579,7 @@ index 5e0c5baf..00000000 - - // `/abc` → typed = 'abc'; empty after just '/' → typed = ''. - const typed = leading.typed; +- const rubberDuckEnabled = this._sessionInfo?.isRubberDuckEnabled?.() ?? false; - const items: CompletionItem[] = []; - for (const command of COMMANDS) { - if (typed.length > 0 && !command.startsWith(typed)) { @@ -7321,7 +7590,7 @@ index 5e0c5baf..00000000 - continue; - } - // `/rubber-duck` is only available when the feature is enabled. -- if (command === 'rubber-duck' && !process.env['RUBBER_DUCK_AGENT']) { +- if (command === 'rubber-duck' && !rubberDuckEnabled) { - continue; - } - items.push({ @@ -7340,7 +7609,7 @@ index 5e0c5baf..00000000 -} diff --git a/src/vs/platform/agentHost/node/copilot/copilotSystemNotification.ts b/src/vs/platform/agentHost/node/copilot/copilotSystemNotification.ts deleted file mode 100644 -index a5d5c783..00000000 +index a5d5c7836ef..00000000000 --- a/src/vs/platform/agentHost/node/copilot/copilotSystemNotification.ts +++ /dev/null @@ -1,52 +0,0 @@ @@ -7397,7 +7666,7 @@ index a5d5c783..00000000 - return (match?.[1] ?? trimmed).trim(); -} diff --git a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts -index bf2d438e..967739e3 100644 +index bf2d438e32b..967739e3723 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts @@ -3,14 +3,11 @@ @@ -7569,7 +7838,7 @@ index bf2d438e..967739e3 100644 - } -} diff --git a/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts b/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts -index 632c2994..e25e78b5 100644 +index 632c2994430..e25e78b5ca2 100644 --- a/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts +++ b/src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts @@ -3,7 +3,21 @@ @@ -7596,7 +7865,7 @@ index 632c2994..e25e78b5 100644 import { basename } from '../../../../base/common/path.js'; import { isString } from '../../../../base/common/types.js'; diff --git a/src/vs/platform/agentHost/node/otel/agentHostOTelService.ts b/src/vs/platform/agentHost/node/otel/agentHostOTelService.ts -index 61cf2cf7..e7b0e069 100644 +index 61cf2cf76c4..e7b0e069468 100644 --- a/src/vs/platform/agentHost/node/otel/agentHostOTelService.ts +++ b/src/vs/platform/agentHost/node/otel/agentHostOTelService.ts @@ -5,7 +5,12 @@ @@ -7615,10 +7884,10 @@ index 61cf2cf7..e7b0e069 100644 import { INativeEnvironmentService } from '../../../environment/common/environment.js'; diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts deleted file mode 100644 -index 0d0e1d22..00000000 +index 6bb478dbb36..00000000000 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ /dev/null -@@ -1,1514 +0,0 @@ +@@ -1,1746 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. @@ -7649,10 +7918,10 @@ index 0d0e1d22..00000000 -import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js'; -import { AgentHostConfigKey } from '../../common/agentHostCustomizationConfig.js'; -import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPluginManager.js'; --import { AgentSession, type AgentSignal, type IAgentActionSignal, type IAgentSessionMetadata } from '../../common/agentService.js'; +-import { AgentSession, GITHUB_COPILOT_PROTECTED_RESOURCE, type AgentSignal, type IAgentActionSignal, type IAgentSessionMetadata } from '../../common/agentService.js'; -import { ISessionDataService } from '../../common/sessionDataService.js'; -import { AHP_AUTH_REQUIRED, ProtocolError } from '../../common/state/sessionProtocol.js'; --import { buildSubagentSessionUri, CustomizationLoadStatus, MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, TurnState, customizationId, type ClientPluginCustomization, type Customization, type MarkdownResponsePart, type ToolCallResult, type Turn } from '../../common/state/sessionState.js'; +-import { buildSubagentSessionUri, CustomizationLoadStatus, MessageKind, ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, TurnState, customizationId, type ClientPluginCustomization, type MarkdownResponsePart, type PluginCustomization, type ToolCallResult, type Turn, RuleCustomization } from '../../common/state/sessionState.js'; -import { CustomizationType } from '../../common/state/protocol/state.js'; -import { ActionType, type IDeltaAction, type SessionAction } from '../../common/state/sessionActions.js'; - @@ -7664,8 +7933,8 @@ index 0d0e1d22..00000000 -import { AgentHostCompletions, IAgentHostCompletions } from '../../node/agentHostCompletions.js'; -import { COPILOT_AGENT_HOST_SYSTEM_MESSAGE, CopilotAgent, getCopilotBranchNameHintFromMessage, getCopilotWorktreeBranchName, getCopilotWorktreeName, getCopilotWorktreesRoot } from '../../node/copilot/copilotAgent.js'; -import { NULL_CHECKPOINT_SERVICE } from '../../common/agentHostCheckpointService.js'; --import { CopilotAgentSession, type SessionWrapperFactory } from '../../node/copilot/copilotAgentSession.js'; --import { CopilotSessionWrapper } from '../../node/copilot/copilotSessionWrapper.js'; +-import { CopilotAgentSession } from '../../node/copilot/copilotAgentSession.js'; +-import type { CopilotSessionLaunchPlan } from '../../node/copilot/copilotSessionLauncher.js'; -import { ShellManager } from '../../node/copilot/copilotShellTools.js'; -import { SessionDatabase } from '../../node/sessionDatabase.js'; -import { createNullSessionDataService } from '../common/sessionTestHelpers.js'; @@ -7675,7 +7944,7 @@ index 0d0e1d22..00000000 - - readonly basePath = URI.from({ scheme: 'inmemory', path: '/agentPlugins' }); - -- async syncCustomizations(_clientId: string, _customizations: ClientPluginCustomization[], _progress?: (status: Customization) => void): Promise { +- async syncCustomizations(_clientId: string, _customizations: ClientPluginCustomization[], _progress?: (status: PluginCustomization) => void): Promise { - return []; - } -} @@ -7712,6 +7981,9 @@ index 0d0e1d22..00000000 - async hasUncommittedChanges(workingDirectory: URI): Promise { - return this.dirtyWorkingDirectories.has(workingDirectory.fsPath); - } +- async commitAll(): Promise { } +- async hasUpstream(): Promise { return false; } +- async pushBranch(): Promise { } - async getSessionGitState(): Promise { return undefined; } - async computeSessionFileDiffs(): Promise { return undefined; } - async showBlob(): Promise { return undefined; } @@ -7858,6 +8130,12 @@ index 0d0e1d22..00000000 - async disconnect(): Promise { } -} - +-class TestSdkError extends Error { +- constructor(message: string, readonly code: number) { +- super(message); +- } +-} +- -class MockAgentHostOTelService implements IAgentHostOTelService { - readonly _serviceBrand: undefined; - @@ -7872,6 +8150,25 @@ index 0d0e1d22..00000000 - } -} - +-class ResumePathCopilotAgent extends CopilotAgent { +- constructor( +- private readonly _copilotClient: ITestCopilotClient, +- @ILogService logService: ILogService, +- @IInstantiationService instantiationService: IInstantiationService, +- @ISessionDataService sessionDataService: ISessionDataService, +- @IAgentHostGitService gitService: IAgentHostGitService, +- @IAgentConfigurationService configurationService: IAgentConfigurationService, +- @IAgentHostCompletions completions: IAgentHostCompletions, +- ) { +- super(logService, instantiationService, sessionDataService, gitService, configurationService, new MockAgentHostOTelService(), completions, NULL_CHECKPOINT_SERVICE); +- this._enablePlanModeOnClient(this._copilotClient as CopilotClient); +- } +- +- protected override _createCopilotClient(): CopilotClient { +- return this._copilotClient as CopilotClient; +- } +-} +- -class TestableCopilotAgent extends CopilotAgent { - private readonly _fakeSessions = new Map(); - readonly resumeCalls: string[] = []; @@ -7880,14 +8177,12 @@ index 0d0e1d22..00000000 - private readonly _copilotClient: ITestCopilotClient, - @ILogService logService: ILogService, - @IInstantiationService instantiationService: IInstantiationService, -- @IFileService fileService: IFileService, - @ISessionDataService sessionDataService: ISessionDataService, - @IAgentHostGitService gitService: IAgentHostGitService, -- @IAgentHostTerminalManager terminalManager: IAgentHostTerminalManager, - @IAgentConfigurationService configurationService: IAgentConfigurationService, - @IAgentHostCompletions completions: IAgentHostCompletions, - ) { -- super(logService, instantiationService, fileService, sessionDataService, gitService, terminalManager, configurationService, new MockAgentHostOTelService(), completions, NULL_CHECKPOINT_SERVICE); +- super(logService, instantiationService, sessionDataService, gitService, configurationService, new MockAgentHostOTelService(), completions, NULL_CHECKPOINT_SERVICE); - this._enablePlanModeOnClient(this._copilotClient as CopilotClient); - } - @@ -7937,7 +8232,7 @@ index 0d0e1d22..00000000 - } -} - --function createTestAgentContext(disposables: Pick, options?: { sessionDataService?: ISessionDataService; copilotClient?: ITestCopilotClient; gitService?: TestAgentHostGitService; environmentServiceRegistration?: 'native' | 'none'; pluginManager?: IAgentPluginManager; fileService?: FileService }): { agent: CopilotAgent; instantiationService: IInstantiationService; configurationService: IAgentConfigurationService; fileService: FileService } { +-function createTestAgentContext(disposables: Pick, options?: { sessionDataService?: ISessionDataService; copilotClient?: ITestCopilotClient; useRealResumePath?: boolean; gitService?: TestAgentHostGitService; environmentServiceRegistration?: 'native' | 'none'; pluginManager?: IAgentPluginManager; fileService?: FileService }): { agent: CopilotAgent; instantiationService: IInstantiationService; configurationService: IAgentConfigurationService; fileService: FileService } { - const services = new ServiceCollection(); - const logService = new NullLogService(); - const fileService = options?.fileService ?? disposables.add(new FileService(logService)); @@ -7968,22 +8263,42 @@ index 0d0e1d22..00000000 - const instantiationService: IInstantiationService = disposables.add(new InstantiationService(services)); - services.set(IInstantiationService, instantiationService); - const agent = options?.copilotClient -- ? instantiationService.createInstance(TestableCopilotAgent, options.copilotClient) +- ? instantiationService.createInstance(options.useRealResumePath ? ResumePathCopilotAgent : TestableCopilotAgent, options.copilotClient) - : instantiationService.createInstance(CopilotAgent); - return { agent, instantiationService, configurationService: configService, fileService }; -} - --function createTestAgent(disposables: Pick, options?: { sessionDataService?: ISessionDataService; copilotClient?: ITestCopilotClient; gitService?: TestAgentHostGitService; environmentServiceRegistration?: 'native' | 'none'; pluginManager?: IAgentPluginManager }): CopilotAgent { +-function createTestAgent(disposables: Pick, options?: { sessionDataService?: ISessionDataService; copilotClient?: ITestCopilotClient; useRealResumePath?: boolean; gitService?: TestAgentHostGitService; environmentServiceRegistration?: 'native' | 'none'; pluginManager?: IAgentPluginManager }): CopilotAgent { - return createTestAgentContext(disposables, options).agent; -} - --function createAgentSessionThroughAgent(agent: CopilotAgent, instantiationService: IInstantiationService): CopilotAgentSession { +-type CopilotCreateSessionOptions = Parameters[0]; +- +-function createAgentSessionThroughAgent(agent: CopilotAgent, instantiationService: IInstantiationService): { readonly session: CopilotAgentSession; readonly createOptions: () => CopilotCreateSessionOptions | undefined } { - const sessionUri = AgentSession.uri('copilotcli', 'test-session-1'); - const shellManager = instantiationService.createInstance(ShellManager, sessionUri, undefined); -- const wrapperFactory: SessionWrapperFactory = async () => new CopilotSessionWrapper(new MockCopilotSession() as unknown as CopilotSession); -- return (agent as unknown as { -- _createAgentSession: (wrapperFactory: SessionWrapperFactory, sessionId: string, shellManager: ShellManager) => CopilotAgentSession; -- })._createAgentSession(wrapperFactory, 'test-session-1', shellManager); +- let createOptions: CopilotCreateSessionOptions | undefined; +- const launchPlan: CopilotSessionLaunchPlan = { +- kind: 'create', +- client: { +- createSession: async options => { +- createOptions = options; +- return new MockCopilotSession() as unknown as CopilotSession; +- }, +- resumeSession: async () => new MockCopilotSession() as unknown as CopilotSession, +- }, +- sessionId: 'test-session-1', +- workingDirectory: undefined, +- resolvedAgentName: undefined, +- snapshot: { clientId: '', tools: [], plugins: [] }, +- shellManager, +- githubToken: 'token', +- model: undefined, +- }; +- const session = (agent as unknown as { +- _createAgentSession: (launchPlan: CopilotSessionLaunchPlan, customizationDirectory: URI | undefined) => CopilotAgentSession; +- })._createAgentSession(launchPlan, undefined); +- return { session, createOptions: () => createOptions }; -} - -function withoutUndefinedProperties(metadata: IAgentSessionMetadata): Record { @@ -8074,6 +8389,71 @@ index 0d0e1d22..00000000 - } - }); - +- suite('restart on startup config change', () => { +- +- class StopCountingClient extends TestCopilotClient { +- stopCount = 0; +- override async stop(): ReturnType { +- this.stopCount++; +- return super.stop(); +- } +- } +- +- test('restarts the idle client when the rubber duck config changes', async () => { +- const client = new StopCountingClient([]); +- const { agent, configurationService } = createTestAgentContext(disposables, { copilotClient: client }); +- try { +- await agent.authenticate('https://api.github.com', 'token'); +- // Force the client to start so a subsequent config change has something to restart. +- await agent.listSessions(); +- +- configurationService.updateRootConfig({ [AgentHostConfigKey.RubberDuck]: true }); +- await Promise.resolve(); +- +- assert.strictEqual(client.stopCount, 1); +- } finally { +- await disposeAgent(agent); +- } +- }); +- +- test('restarts and disposes active sessions when the config changes', async () => { +- const client = new StopCountingClient([]); +- const { agent, configurationService } = createTestAgentContext(disposables, { copilotClient: client }); +- try { +- await agent.authenticate('https://api.github.com', 'token'); +- await agent.listSessions(); +- +- let disposed = false; +- const sessions = (agent as unknown as { _sessions: { set(k: string, v: { dispose(): void }): void } })._sessions; +- sessions.set('active', { dispose() { disposed = true; } }); +- +- configurationService.updateRootConfig({ [AgentHostConfigKey.RubberDuck]: true }); +- await Promise.resolve(); +- +- assert.strictEqual(client.stopCount, 1); +- assert.strictEqual(disposed, true); +- } finally { +- await disposeAgent(agent); +- } +- }); +- +- test('does not restart when an unrelated config key changes', async () => { +- const client = new StopCountingClient([]); +- const { agent, configurationService } = createTestAgentContext(disposables, { copilotClient: client }); +- try { +- await agent.authenticate('https://api.github.com', 'token'); +- await agent.listSessions(); +- +- configurationService.updateRootConfig({ [AgentHostConfigKey.EnableCustomTerminalTool]: true }); +- await Promise.resolve(); +- +- assert.strictEqual(client.stopCount, 0); +- } finally { +- await disposeAgent(agent); +- } +- }); +- }); +- - test('models include billing multiplier metadata when SDK provides it', async () => { - const agent = createTestAgent(disposables, { - copilotClient: new TestCopilotClient([], [{ @@ -8111,14 +8491,18 @@ index 0d0e1d22..00000000 - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - delete process.env['XDG_STATE_HOME']; - try { -- const agentSession = disposables.add(createAgentSessionThroughAgent(agent, instantiationService)); +- const createdSession = createAgentSessionThroughAgent(agent, instantiationService); +- const agentSession = disposables.add(createdSession.session); - await agentSession.initializeSession(); +- const onPermissionRequest = createdSession.createOptions()?.onPermissionRequest; +- assert.ok(onPermissionRequest); - -- const result = await agentSession.handlePermissionRequest({ +- const result = await onPermissionRequest({ - kind: 'read', +- intention: 'read plan', - path: URI.file('/mock-home/.copilot/session-state/test-session-1/plan.md').fsPath, - toolCallId: 'tc-read-plan-agent-composition', -- }); +- }, { sessionId: 'test-session-1' }); - - assert.strictEqual(result.kind, 'approve-once'); - } finally { @@ -8234,7 +8618,7 @@ index 0d0e1d22..00000000 - class SpyingPluginManager extends TestAgentPluginManager { - public readonly calls: { clientId: string; customizations: ClientPluginCustomization[] }[] = []; - -- override async syncCustomizations(clientId: string, customizations: ClientPluginCustomization[], _progress?: (status: Customization) => void): Promise { +- override async syncCustomizations(clientId: string, customizations: ClientPluginCustomization[], _progress?: (status: PluginCustomization) => void): Promise { - this.calls.push({ clientId, customizations: [...customizations] }); - return []; - } @@ -8337,10 +8721,10 @@ index 0d0e1d22..00000000 - const updatesWithChildren = actions - .filter(a => a.type === ActionType.SessionCustomizationUpdated) - .filter((a): a is Extract => true) -- .filter(a => a.customization.children !== undefined); +- .filter(a => (a.customization as PluginCustomization).children !== undefined); - - assert.strictEqual(updatesWithChildren.length > 0, true, 'expected SessionCustomizationUpdated to carry parsed children'); -- const agentChildren = updatesWithChildren.at(-1)!.customization.children!.filter(c => c.type === CustomizationType.Agent); +- const agentChildren = (updatesWithChildren.at(-1)!.customization as PluginCustomization).children!.filter(c => c.type === CustomizationType.Agent); - assert.deepStrictEqual(agentChildren, [{ - type: CustomizationType.Agent, - id: customizationId(URI.joinPath(pluginDir, 'agents', 'helper.md').toString()), @@ -8364,6 +8748,8 @@ index 0d0e1d22..00000000 - const instructionFile = URI.joinPath(workspace, '.github', 'instructions', 'team', 'nested.instructions.md'); - await fileService.writeFile(agentFile, VSBuffer.fromString('agent body')); - await fileService.writeFile(instructionFile, VSBuffer.fromString('instruction body')); +- const agentsMdFile = URI.joinPath(workspace, 'AGENTS.md'); +- await fileService.writeFile(agentsMdFile, VSBuffer.fromString('agents md body')); - - const sessionDataService = disposables.add(new TestSessionDataService()); - const client = new TestCopilotClient([]); @@ -8381,8 +8767,9 @@ index 0d0e1d22..00000000 - const customizations = await agent.getSessionCustomizations(session); - const discoveredDirectories = customizations.filter(customization => customization.type === CustomizationType.Directory); - -- assert.strictEqual(discoveredDirectories.length, 2); +- assert.strictEqual(discoveredDirectories.length, 3); - assert.deepStrictEqual(discoveredDirectories.map(customization => customization.uri).sort(), [ +- workspace.toString(), - URI.joinPath(workspace, '.github', 'agents').toString(), - URI.joinPath(workspace, '.github', 'instructions').toString(), - ].sort()); @@ -8406,6 +8793,55 @@ index 0d0e1d22..00000000 - uri: instructionFile.toString(), - name: 'nested.instructions.md', - }]); +- +- const agentInstructionsDirectory = discoveredDirectories.find(customization => customization.uri === workspace.toString()); +- assert.ok(agentInstructionsDirectory); +- assert.strictEqual(agentInstructionsDirectory.contents, CustomizationType.Rule); +- assert.deepStrictEqual(agentInstructionsDirectory.children, [{ +- type: CustomizationType.Rule, +- id: customizationId(agentsMdFile.toString()), +- uri: agentsMdFile.toString(), +- name: 'AGENTS.md', +- alwaysApply: true, +- } satisfies RuleCustomization]); +- } finally { +- await disposeAgent(agent); +- } +- }); +- +- test('getSessionCustomizations clears discovered files when the root disappears', async () => { +- const fileService = disposables.add(new FileService(new NullLogService())); +- disposables.add(fileService.registerProvider(Schemas.inMemory, disposables.add(new InMemoryFileSystemProvider()))); +- +- const workspace = URI.from({ scheme: Schemas.inMemory, path: '/workspace' }); +- const agentsRoot = URI.joinPath(workspace, '.github', 'agents'); +- await fileService.createFolder(agentsRoot); +- await fileService.writeFile(URI.joinPath(agentsRoot, 'helper.agent.md'), VSBuffer.fromString('agent body')); +- +- const sessionDataService = disposables.add(new TestSessionDataService()); +- const client = new TestCopilotClient([]); +- const { agent } = createTestAgentContext(disposables, { sessionDataService, copilotClient: client, fileService }); +- +- try { +- await agent.authenticate('https://api.github.com', 'token'); +- +- const session = AgentSession.uri('copilotcli', 'session-discovery-cleared'); +- await agent.createSession({ +- session, +- workingDirectory: workspace, +- }); +- +- const before = await agent.getSessionCustomizations(session); +- assert.deepStrictEqual(before.filter(customization => customization.type === CustomizationType.Directory).map(customization => customization.uri), [agentsRoot.toString()]); +- +- await fileService.del(agentsRoot, { recursive: true }); +- +- let after = await agent.getSessionCustomizations(session); +- for (let i = 0; i < 20 && after.filter(customization => customization.type === CustomizationType.Directory).length > 0; i++) { +- await new Promise(resolve => setTimeout(resolve, 50)); +- after = await agent.getSessionCustomizations(session); +- } +- assert.deepStrictEqual(after.filter(customization => customization.type === CustomizationType.Directory).map(customization => customization.uri), []); - } finally { - await disposeAgent(agent); - } @@ -8549,7 +8985,7 @@ index 0d0e1d22..00000000 - const { agent, configurationService } = createTestAgentContext(disposables, { sessionDataService, copilotClient: client }); - try { - await agent.authenticate('https://api.github.com', 'token'); -- configurationService.updateRootConfig({ [AgentHostConfigKey.DisableCustomTerminalTool]: true }); +- configurationService.updateRootConfig({ [AgentHostConfigKey.EnableCustomTerminalTool]: false }); - - const result = await agent.createSession({ - session: AgentSession.uri('copilotcli', 'sdk-terminal-defaults'), @@ -8782,6 +9218,71 @@ index 0d0e1d22..00000000 - }); - }); - +- suite('_resumeSession fallback', () => { +- type AgentInternals = { +- _resumeSession: (id: string) => Promise; +- }; +- +- function createResumeFailingClient(message: string, code = -32603): { readonly client: TestCopilotClient; readonly getCreateSessionCalls: () => number } { +- let createSessionCalls = 0; +- const client = new TestCopilotClient([sdkSession('s1', '/workspace')]); +- client.resumeSession = async () => { +- throw new TestSdkError(message, code); +- }; +- client.createSession = async () => { +- createSessionCalls++; +- return new MockCopilotSession() as unknown as CopilotSession; +- }; +- return { client, getCreateSessionCalls: () => createSessionCalls }; +- } +- +- test('falls back to createSession after a Start Over truncate leaves the session empty', async () => { +- // Simulates the post-`truncateSession`/"Start Over" case: the on-disk +- // session has zero events, so the SDK's resumeSession refuses to +- // resume it. The exact wording varies across SDK versions, so we +- // assert on the general -32603 + "no events" shape. +- const { client, getCreateSessionCalls } = createResumeFailingClient(`Request session.resume failed with message: LocalRpcSession: 'session.getMessages' returned no events for session s1`); +- const agent = createTestAgent(disposables, { copilotClient: client, useRealResumePath: true, sessionDataService: disposables.add(new TestSessionDataService()) }); +- const internals = agent as unknown as AgentInternals; +- try { +- await agent.authenticate(GITHUB_COPILOT_PROTECTED_RESOURCE.resource, 'token'); +- await internals._resumeSession('s1'); +- assert.strictEqual(getCreateSessionCalls(), 1); +- } finally { +- await disposeAgent(agent); +- } +- }); +- +- test('falls back to createSession for an unknown -32603 from resumeSession', async () => { +- // Defensive: if the SDK starts emitting some other generic +- // "cannot resume this session" message, we should still recover +- // rather than leaving the user with an unopenable session. +- const { client, getCreateSessionCalls } = createResumeFailingClient('Request session.resume failed: something went wrong'); +- const agent = createTestAgent(disposables, { copilotClient: client, useRealResumePath: true, sessionDataService: disposables.add(new TestSessionDataService()) }); +- const internals = agent as unknown as AgentInternals; +- try { +- await agent.authenticate(GITHUB_COPILOT_PROTECTED_RESOURCE.resource, 'token'); +- await internals._resumeSession('s1'); +- assert.strictEqual(getCreateSessionCalls(), 1); +- } finally { +- await disposeAgent(agent); +- } +- }); +- +- test('does not replace a corrupted session file with an empty session', async () => { +- const { client, getCreateSessionCalls } = createResumeFailingClient('Request session.resume failed with message: Session file is corrupted (line 19567: data.compactionTokensUsed.copilotUsage.tokenDetails.0.batchSize: Number must be greater than 0)'); +- const agent = createTestAgent(disposables, { copilotClient: client, useRealResumePath: true, sessionDataService: disposables.add(new TestSessionDataService()) }); +- const internals = agent as unknown as AgentInternals; +- try { +- await agent.authenticate(GITHUB_COPILOT_PROTECTED_RESOURCE.resource, 'token'); +- await assert.rejects(() => internals._resumeSession('s1'), /Session file is corrupted/); +- assert.strictEqual(getCreateSessionCalls(), 0); +- } finally { +- await disposeAgent(agent); +- } +- }); +- }); +- - suite('worktree announcement', () => { - // Drives a real session through worktree creation (calling the - // agent's _resolveSessionWorkingDirectory via a test seam so we don't @@ -9135,10 +9636,10 @@ index 0d0e1d22..00000000 -}); 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 ce67d5b0..00000000 +index 3ba908cae5d..00000000000 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ /dev/null -@@ -1,3054 +0,0 @@ +@@ -1,3076 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. @@ -9165,8 +9666,9 @@ index ce67d5b0..00000000 -import { IDiffComputeService } from '../../common/diffComputeService.js'; -import { ISessionDataService } from '../../common/sessionDataService.js'; -import { ActionType, type SessionDeltaAction, type SessionErrorAction, type SessionInputRequestedAction, type SessionResponsePartAction, type SessionToolCallCompleteAction, type SessionToolCallReadyAction, type SessionToolCallStartAction } from '../../common/state/sessionActions.js'; --import { MessageAttachmentKind, MessageKind, ResponsePartKind, SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, ToolCallStatus, ToolResultContentType, type ToolResultFileEditContent } from '../../common/state/sessionState.js'; --import { CopilotAgentSession, IActiveClientSnapshot, SessionWrapperFactory } from '../../node/copilot/copilotAgentSession.js'; +-import { MessageAttachmentKind, MessageKind, ResponsePartKind, SessionInputAnswerState, SessionInputAnswerValueKind, SessionInputQuestionKind, SessionInputResponseKind, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, type ToolResultFileEditContent } from '../../common/state/sessionState.js'; +-import { CopilotAgentSession } from '../../node/copilot/copilotAgentSession.js'; +-import { type CopilotSessionLaunchPlan, type IActiveClientSnapshot, type ICopilotSessionLauncher, type ICopilotSessionRuntime } from '../../node/copilot/copilotSessionLauncher.js'; -import { CopilotSessionWrapper } from '../../node/copilot/copilotSessionWrapper.js'; -import { buildCopilotSystemNotification } from '../../node/copilot/copilotSystemNotification.js'; -import { IAgentConfigurationService } from '../../node/agentConfigurationService.js'; @@ -9329,7 +9831,7 @@ index ce67d5b0..00000000 - environmentServiceRegistration?: 'native' | 'none'; - logService?: ILogService; - telemetryService?: ITelemetryService; -- captureWrapperCallbacks?: { current?: Parameters[0] }; +- captureRuntime?: { current?: ICopilotSessionRuntime }; - workingDirectory?: URI; - /** Per-key effective config values returned by the fake configuration service. */ - configValues?: Record; @@ -9337,6 +9839,7 @@ index ce67d5b0..00000000 - fileReadErrors?: readonly string[]; -}): Promise<{ - session: CopilotAgentSession; +- runtime: ICopilotSessionRuntime; - mockSession: MockCopilotSession; - signals: AgentSignal[]; - waitForSignal: (predicate: (signal: AgentSignal) => boolean) => Promise; @@ -9370,11 +9873,29 @@ index ce67d5b0..00000000 - const sessionUri = AgentSession.uri('copilot', 'test-session-1'); - const mockSession = new MockCopilotSession(); - -- const factory: SessionWrapperFactory = async callbacks => { -- if (options?.captureWrapperCallbacks) { -- options.captureWrapperCallbacks.current = callbacks; +- const launchPlan: CopilotSessionLaunchPlan = { +- kind: 'create', +- client: { +- createSession: async () => mockSession as unknown as CopilotSession, +- resumeSession: async () => mockSession as unknown as CopilotSession, +- }, +- sessionId: 'test-session-1', +- workingDirectory: options?.workingDirectory, +- resolvedAgentName: undefined, +- snapshot: options?.clientSnapshot ?? { clientId: '', tools: [], plugins: [] }, +- shellManager: undefined, +- githubToken: undefined, +- model: undefined, +- }; +- let launchedRuntime: ICopilotSessionRuntime | undefined; +- const sessionLauncher: ICopilotSessionLauncher = { +- launch: async (_plan, runtime) => { +- launchedRuntime = runtime; +- if (options?.captureRuntime) { +- options.captureRuntime.current = runtime; +- } +- return new CopilotSessionWrapper(mockSession as unknown as CopilotSession); - } -- return new CopilotSessionWrapper(mockSession as unknown as CopilotSession); - }; - - const services = new ServiceCollection(); @@ -9425,7 +9946,8 @@ index ce67d5b0..00000000 - sessionUri, - rawSessionId: 'test-session-1', - onDidSessionProgress: progressEmitter, -- wrapperFactory: factory, +- sessionLauncher, +- launchPlan, - shellManager: undefined, - clientSnapshot: options?.clientSnapshot, - workingDirectory: options?.workingDirectory, @@ -9433,8 +9955,9 @@ index ce67d5b0..00000000 - )); - - await session.initializeSession(); +- assert.ok(launchedRuntime); - -- return { session, mockSession, signals, waitForSignal, sessionConfigUpdates }; +- return { session, runtime: launchedRuntime, mockSession, signals, waitForSignal, sessionConfigUpdates }; -} - -// ---- Tests ------------------------------------------------------------------ @@ -9690,8 +10213,8 @@ index ce67d5b0..00000000 - suite('permission handling', () => { - - test('read permission fires tool_ready (deferred to side effects)', async () => { -- const { session, signals, waitForSignal } = await createAgentSession(disposables); -- const resultPromise = session.handlePermissionRequest({ +- const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'read', - path: '/workspace/src/file.ts', - toolCallId: 'tc-1', @@ -9709,8 +10232,8 @@ index ce67d5b0..00000000 - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - process.env['XDG_STATE_HOME'] = '/mock-state-home'; - try { -- const { session, signals } = await createAgentSession(disposables); -- const result = await session.handlePermissionRequest({ +- const { runtime, signals } = await createAgentSession(disposables); +- const result = await runtime.handlePermissionRequest({ - kind: 'read', - path: join('/mock-state-home', '.copilot', 'session-state', 'test-session-1', 'plan.md'), - toolCallId: 'tc-read-plan', @@ -9731,8 +10254,8 @@ index ce67d5b0..00000000 - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - delete process.env['XDG_STATE_HOME']; - try { -- const { session, signals } = await createAgentSession(disposables, { environmentServiceRegistration: 'native' }); -- const result = await session.handlePermissionRequest({ +- const { runtime, signals } = await createAgentSession(disposables, { environmentServiceRegistration: 'native' }); +- const result = await runtime.handlePermissionRequest({ - kind: 'read', - path: join('/mock-home', '.copilot', 'session-state', 'test-session-1', 'plan.md'), - toolCallId: 'tc-read-plan-native-env', @@ -9754,13 +10277,13 @@ index ce67d5b0..00000000 - delete process.env['XDG_STATE_HOME']; - const logService = new CapturingLogService(); - try { -- const { session } = await createAgentSession(disposables, { +- const { runtime } = await createAgentSession(disposables, { - environmentServiceRegistration: 'none', - logService, - }); - - await assert.rejects( -- session.handlePermissionRequest({ +- runtime.handlePermissionRequest({ - kind: 'read', - path: join('/mock-home', '.copilot', 'session-state', 'test-session-1', 'plan.md'), - toolCallId: 'tc-read-plan-missing-env', @@ -9781,8 +10304,8 @@ index ce67d5b0..00000000 - }); - - test('write permission fires tool_ready (deferred to side effects)', async () => { -- const { session, signals, waitForSignal } = await createAgentSession(disposables); -- const resultPromise = session.handlePermissionRequest({ +- const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'write', - fileName: '/workspace/src/file.ts', - toolCallId: 'tc-1', @@ -9800,8 +10323,8 @@ index ce67d5b0..00000000 - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - process.env['XDG_STATE_HOME'] = '/mock-state-home'; - try { -- const { session, signals } = await createAgentSession(disposables); -- const result = await session.handlePermissionRequest({ +- const { runtime, signals } = await createAgentSession(disposables); +- const result = await runtime.handlePermissionRequest({ - kind: 'write', - fileName: join('/mock-state-home', '.copilot', 'session-state', 'test-session-1', 'plan.md'), - toolCallId: 'tc-write-plan', @@ -9822,8 +10345,8 @@ index ce67d5b0..00000000 - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - process.env['XDG_STATE_HOME'] = '/mock-state-home'; - try { -- const { session, signals, waitForSignal } = await createAgentSession(disposables); -- const resultPromise = session.handlePermissionRequest({ +- const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'write', - fileName: join('/mock-state-home', '.copilot', 'session-state', 'different-session', 'plan.md'), - toolCallId: 'tc-write-other-plan', @@ -9848,9 +10371,9 @@ index ce67d5b0..00000000 - const previousXdgStateHome = process.env['XDG_STATE_HOME']; - process.env['XDG_STATE_HOME'] = '/mock-state-home'; - try { -- const { session, signals, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); - const sessionDir = join('/mock-state-home', '.copilot', 'session-state', 'test-session-1'); -- const resultPromise = session.handlePermissionRequest({ +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'write', - fileName: `${sessionDir}${sep}..${sep}outside.md`, - toolCallId: 'tc-write-traversal', @@ -9872,10 +10395,10 @@ index ce67d5b0..00000000 - }); - - test('auto-approves read of Copilot SDK large-tool-output temp files', async () => { -- const { session, signals } = await createAgentSession(disposables); +- const { runtime, signals } = await createAgentSession(disposables); - - // Layout 1: -copilot-tool-output-.txt -- const result1 = await session.handlePermissionRequest({ +- const result1 = await runtime.handlePermissionRequest({ - kind: 'read', - path: join('/mock-tmp', '1730000000000-copilot-tool-output-abc123.txt'), - toolCallId: 'tc-tool-output-1', @@ -9883,7 +10406,7 @@ index ce67d5b0..00000000 - assert.strictEqual(result1.kind, 'approve-once'); - - // Layout 2: copilot-tool-output--.txt -- const result2 = await session.handlePermissionRequest({ +- const result2 = await runtime.handlePermissionRequest({ - kind: 'read', - path: join('/mock-tmp', 'copilot-tool-output-1730000000000-abc123.txt'), - toolCallId: 'tc-tool-output-2', @@ -9894,8 +10417,8 @@ index ce67d5b0..00000000 - }); - - test('does not auto-approve tool-output-named files outside tmpdir', async () => { -- const { session, signals, waitForSignal } = await createAgentSession(disposables); -- const resultPromise = session.handlePermissionRequest({ +- const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'read', - path: join('/some/other/dir', 'copilot-tool-output-1730000000000-abc123.txt'), - toolCallId: 'tc-tool-output-outside', @@ -9910,8 +10433,8 @@ index ce67d5b0..00000000 - }); - - test('does not auto-approve unrelated files inside tmpdir', async () => { -- const { session, signals, waitForSignal } = await createAgentSession(disposables); -- const resultPromise = session.handlePermissionRequest({ +- const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'read', - path: join('/mock-tmp', 'something-else.txt'), - toolCallId: 'tc-tmp-other', @@ -9926,8 +10449,8 @@ index ce67d5b0..00000000 - }); - - test('does not auto-approve write to a tool-output temp path', async () => { -- const { session, signals, waitForSignal } = await createAgentSession(disposables); -- const resultPromise = session.handlePermissionRequest({ +- const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'write', - fileName: join('/mock-tmp', 'copilot-tool-output-1730000000000-abc123.txt'), - toolCallId: 'tc-tool-output-write', @@ -9942,9 +10465,9 @@ index ce67d5b0..00000000 - }); - - test('write permission outside working directory fires tool_ready', async () => { -- const { session, signals, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); - -- const resultPromise = session.handlePermissionRequest({ +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'write', - fileName: '/other/file.ts', - toolCallId: 'tc-write-outside', @@ -9959,10 +10482,10 @@ index ce67d5b0..00000000 - }); - - test('read permission outside working directory fires tool_ready', async () => { -- const { session, signals, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); - - // Kick off permission request but don't await — it will block -- const resultPromise = session.handlePermissionRequest({ +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'read', - path: '/other/file.ts', - toolCallId: 'tc-2', @@ -9979,14 +10502,14 @@ index ce67d5b0..00000000 - }); - - test('denies permission when no toolCallId', async () => { -- const { session } = await createAgentSession(disposables); -- const result = await session.handlePermissionRequest({ kind: 'write' }); +- const { runtime } = await createAgentSession(disposables); +- const result = await runtime.handlePermissionRequest({ kind: 'write' }); - assert.strictEqual(result.kind, 'reject'); - }); - - test('denied-interactively when user denies', async () => { -- const { session, signals, waitForSignal } = await createAgentSession(disposables); -- const resultPromise = session.handlePermissionRequest({ +- const { session, runtime, signals, waitForSignal } = await createAgentSession(disposables); +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'shell', - toolCallId: 'tc-3', - }); @@ -9999,8 +10522,8 @@ index ce67d5b0..00000000 - }); - - test('pending permissions are denied on dispose', async () => { -- const { session } = await createAgentSession(disposables); -- const resultPromise = session.handlePermissionRequest({ +- const { session, runtime } = await createAgentSession(disposables); +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'write', - toolCallId: 'tc-4', - }); @@ -10011,8 +10534,8 @@ index ce67d5b0..00000000 - }); - - test('pending permissions are denied on abort', async () => { -- const { session } = await createAgentSession(disposables); -- const resultPromise = session.handlePermissionRequest({ +- const { session, runtime } = await createAgentSession(disposables); +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'write', - toolCallId: 'tc-5', - }); @@ -10516,10 +11039,10 @@ index ce67d5b0..00000000 - }); - - test('edit hooks resolve relative apply_patch file paths against workingDirectory', async () => { -- const capturedCallbacks: { current?: Parameters[0] } = {}; +- const capturedRuntime: { current?: ICopilotSessionRuntime } = {}; - const workingDirectory = URI.file('/repo/project'); - const absolutePath = URI.file('/tmp/absolute.ts').fsPath; -- const { session } = await createAgentSession(disposables, { workingDirectory, captureWrapperCallbacks: capturedCallbacks }); +- const { session } = await createAgentSession(disposables, { workingDirectory, captureRuntime: capturedRuntime }); - const sessionInternals = session as unknown as ISessionInternalsForTest; - const started: string[] = []; - const completed: string[] = []; @@ -10539,14 +11062,14 @@ index ce67d5b0..00000000 - '*** End Patch', - ].join('\n'); - -- await capturedCallbacks.current!.hooks.onPreToolUse({ +- await capturedRuntime.current!.handlePreToolUse({ - sessionId: 'test-session-1', - timestamp: new Date(0), - workingDirectory: '/repo/project', - toolName: 'apply_patch', - toolArgs: patch, - }); -- await capturedCallbacks.current!.hooks.onPostToolUse({ +- await capturedRuntime.current!.handlePostToolUse({ - sessionId: 'test-session-1', - timestamp: new Date(0), - workingDirectory: '/repo/project', @@ -11116,10 +11639,10 @@ index ce67d5b0..00000000 - suite('user input handling', () => { - - test('handleUserInputRequest fires user_input_request progress event', async () => { -- const { session, signals } = await createAgentSession(disposables); +- const { session, runtime, signals } = await createAgentSession(disposables); - - // Start the request (don't await — it blocks waiting for response) -- const resultPromise = session.handleUserInputRequest( +- const resultPromise = runtime.handleUserInputRequest( - { question: 'What is your name?' }, - { sessionId: 'test-session-1' } - ); @@ -11146,9 +11669,9 @@ index ce67d5b0..00000000 - }); - - test('handleUserInputRequest with choices generates SingleSelect question', async () => { -- const { session, signals } = await createAgentSession(disposables); +- const { session, runtime, signals } = await createAgentSession(disposables); - -- const resultPromise = session.handleUserInputRequest( +- const resultPromise = runtime.handleUserInputRequest( - { question: 'Pick a color', choices: ['red', 'blue', 'green'] }, - { sessionId: 'test-session-1' } - ); @@ -11178,9 +11701,9 @@ index ce67d5b0..00000000 - }); - - test('handleUserInputRequest returns empty answer on cancel', async () => { -- const { session, signals } = await createAgentSession(disposables); +- const { session, runtime, signals } = await createAgentSession(disposables); - -- const resultPromise = session.handleUserInputRequest( +- const resultPromise = runtime.handleUserInputRequest( - { question: 'Cancel me' }, - { sessionId: 'test-session-1' } - ); @@ -11199,9 +11722,9 @@ index ce67d5b0..00000000 - }); - - test('handleUserInputRequest returns empty answer on skipped question', async () => { -- const { session, signals } = await createAgentSession(disposables); +- const { session, runtime, signals } = await createAgentSession(disposables); - -- const resultPromise = session.handleUserInputRequest( +- const resultPromise = runtime.handleUserInputRequest( - { question: 'Skip me' }, - { sessionId: 'test-session-1' } - ); @@ -11220,9 +11743,9 @@ index ce67d5b0..00000000 - }); - - test('pending user inputs are cancelled on dispose', async () => { -- const { session } = await createAgentSession(disposables); +- const { session, runtime } = await createAgentSession(disposables); - -- const resultPromise = session.handleUserInputRequest( +- const resultPromise = runtime.handleUserInputRequest( - { question: 'Will be cancelled' }, - { sessionId: 'test-session-1' } - ); @@ -11234,11 +11757,11 @@ index ce67d5b0..00000000 - }); - - test('autopilot auto-answers a free-form question without firing a progress event', async () => { -- const { session, signals } = await createAgentSession(disposables, { +- const { runtime, signals } = await createAgentSession(disposables, { - configValues: { [SessionConfigKey.AutoApprove]: 'autopilot' }, - }); - -- const result = await session.handleUserInputRequest( +- const result = await runtime.handleUserInputRequest( - { question: 'Pick a color', choices: ['red', 'blue', 'green'] }, - { sessionId: 'test-session-1' } - ); @@ -11254,11 +11777,11 @@ index ce67d5b0..00000000 - test('autopilot does not auto-answer when autoApprove is not "autopilot"', async () => { - // Sanity check: with autoApprove=default the question must - // still be surfaced as a progress event (the existing behavior). -- const { session, signals } = await createAgentSession(disposables, { +- const { runtime, signals } = await createAgentSession(disposables, { - configValues: { [SessionConfigKey.AutoApprove]: 'default' }, - }); - -- session.handleUserInputRequest( +- runtime.handleUserInputRequest( - { question: 'Need user input' }, - { sessionId: 'test-session-1' } - ); @@ -11276,9 +11799,9 @@ index ce67d5b0..00000000 - suite('elicitation handling', () => { - - test('form-mode request projects schema fields to questions and accept round-trips content', async () => { -- const { session, signals } = await createAgentSession(disposables); +- const { session, runtime, signals } = await createAgentSession(disposables); - -- const resultPromise = session.handleElicitationRequest({ +- const resultPromise = runtime.handleElicitationRequest({ - sessionId: 'test-session-1', - message: 'Configure deployment', - mode: 'form', @@ -11336,9 +11859,9 @@ index ce67d5b0..00000000 - }); - - test('skipped and missing answers are omitted from accept content', async () => { -- const { session, signals } = await createAgentSession(disposables); +- const { session, runtime, signals } = await createAgentSession(disposables); - -- const resultPromise = session.handleElicitationRequest({ +- const resultPromise = runtime.handleElicitationRequest({ - sessionId: 'test-session-1', - message: 'Partial form', - mode: 'form', @@ -11361,9 +11884,9 @@ index ce67d5b0..00000000 - }); - - test('url-mode request surfaces url and accept returns no content', async () => { -- const { session, signals } = await createAgentSession(disposables); +- const { session, runtime, signals } = await createAgentSession(disposables); - -- const resultPromise = session.handleElicitationRequest({ +- const resultPromise = runtime.handleElicitationRequest({ - sessionId: 'test-session-1', - message: 'Open this link', - mode: 'url', @@ -11379,9 +11902,9 @@ index ce67d5b0..00000000 - }); - - test('free-form request (no schema) returns submitted text as content.answer', async () => { -- const { session, signals } = await createAgentSession(disposables); +- const { session, runtime, signals } = await createAgentSession(disposables); - -- const resultPromise = session.handleElicitationRequest({ +- const resultPromise = runtime.handleElicitationRequest({ - sessionId: 'test-session-1', - message: 'What is your favorite color?', - mode: 'form', @@ -11399,9 +11922,9 @@ index ce67d5b0..00000000 - }); - - test('decline response maps to action=decline', async () => { -- const { session, signals } = await createAgentSession(disposables); +- const { session, runtime, signals } = await createAgentSession(disposables); - -- const resultPromise = session.handleElicitationRequest({ +- const resultPromise = runtime.handleElicitationRequest({ - sessionId: 'test-session-1', - message: 'Please confirm', - mode: 'form', @@ -11414,9 +11937,9 @@ index ce67d5b0..00000000 - }); - - test('cancel response maps to action=cancel', async () => { -- const { session, signals } = await createAgentSession(disposables); +- const { session, runtime, signals } = await createAgentSession(disposables); - -- const resultPromise = session.handleElicitationRequest({ +- const resultPromise = runtime.handleElicitationRequest({ - sessionId: 'test-session-1', - message: 'Please confirm', - mode: 'form', @@ -11429,11 +11952,11 @@ index ce67d5b0..00000000 - }); - - test('autopilot auto-cancels without firing a progress event', async () => { -- const { session, signals } = await createAgentSession(disposables, { +- const { runtime, signals } = await createAgentSession(disposables, { - configValues: { [SessionConfigKey.AutoApprove]: 'autopilot' }, - }); - -- const result = await session.handleElicitationRequest({ +- const result = await runtime.handleElicitationRequest({ - sessionId: 'test-session-1', - message: 'Need input', - mode: 'form', @@ -11445,9 +11968,9 @@ index ce67d5b0..00000000 - }); - - test('pending elicitations are cancelled on dispose', async () => { -- const { session } = await createAgentSession(disposables); +- const { session, runtime } = await createAgentSession(disposables); - -- const resultPromise = session.handleElicitationRequest({ +- const resultPromise = runtime.handleElicitationRequest({ - sessionId: 'test-session-1', - message: 'Will be cancelled', - mode: 'form', @@ -11463,14 +11986,14 @@ index ce67d5b0..00000000 - - test('logs and rethrows user input callback failures', async () => { - const logService = new CapturingLogService(); -- const { session } = await createAgentSession(disposables, { logService }); +- const { session, runtime } = await createAgentSession(disposables, { logService }); - const sessionInternals = session as unknown as ISessionInternalsForTest; - sessionInternals._onDidSessionProgress.fire = () => { - throw new Error('user input boom'); - }; - - await assert.rejects( -- session.handleUserInputRequest( +- runtime.handleUserInputRequest( - { question: 'Need input' }, - { sessionId: 'test-session-1' }, - ), @@ -11486,15 +12009,15 @@ index ce67d5b0..00000000 - - test('logs and rethrows onPreToolUse failures', async () => { - const logService = new CapturingLogService(); -- const capturedCallbacks: { current?: Parameters[0] } = {}; -- const { session } = await createAgentSession(disposables, { logService, captureWrapperCallbacks: capturedCallbacks }); +- const capturedRuntime: { current?: ICopilotSessionRuntime } = {}; +- const { session } = await createAgentSession(disposables, { logService, captureRuntime: capturedRuntime }); - const sessionInternals = session as unknown as ISessionInternalsForTest; - sessionInternals._editTracker.trackEditStart = async () => { - throw new Error('pre tool boom'); - }; - - await assert.rejects( -- capturedCallbacks.current!.hooks.onPreToolUse({ +- capturedRuntime.current!.handlePreToolUse({ - sessionId: 'test-session-1', - timestamp: new Date(0), - workingDirectory: '/tmp', @@ -11513,15 +12036,15 @@ index ce67d5b0..00000000 - - test('logs and rethrows onPostToolUse failures', async () => { - const logService = new CapturingLogService(); -- const capturedCallbacks: { current?: Parameters[0] } = {}; -- const { session } = await createAgentSession(disposables, { logService, captureWrapperCallbacks: capturedCallbacks }); +- const capturedRuntime: { current?: ICopilotSessionRuntime } = {}; +- const { session } = await createAgentSession(disposables, { logService, captureRuntime: capturedRuntime }); - const sessionInternals = session as unknown as ISessionInternalsForTest; - sessionInternals._editTracker.completeEdit = async () => { - throw new Error('post tool boom'); - }; - - await assert.rejects( -- capturedCallbacks.current!.hooks.onPostToolUse({ +- capturedRuntime.current!.handlePostToolUse({ - sessionId: 'test-session-1', - timestamp: new Date(0), - workingDirectory: '/tmp', @@ -11555,7 +12078,7 @@ index ce67d5b0..00000000 - }; - - test('client tool handler waits for completion without emitting tool_ready', async () => { -- const { session, mockSession, signals } = await createAgentSession(disposables, { clientSnapshot: snapshot }); +- const { session, runtime, mockSession, signals } = await createAgentSession(disposables, { clientSnapshot: snapshot }); - - // SDK emits tool.execution_start — tool_start fires immediately - mockSession.fire('tool.execution_start', { @@ -11569,12 +12092,12 @@ index ce67d5b0..00000000 - const startSignal = signals.find(s => isAction(s, ActionType.SessionToolCallStart)); - assert.ok(startSignal && isAction(startSignal, ActionType.SessionToolCallStart)); - if (isAction(startSignal!, ActionType.SessionToolCallStart)) { -- assert.strictEqual((startSignal.action as SessionToolCallStartAction).toolClientId, 'test-client'); +- assert.deepStrictEqual((startSignal.action as SessionToolCallStartAction).contributor, { kind: ToolCallContributorKind.Client, clientId: 'test-client' }); - } - - // SDK invokes the handler — it creates a deferred and waits, - // but does NOT fire tool_ready (that comes from the permission flow). -- const tools = session.createClientSdkTools(); +- const tools = runtime.createClientSdkTools(); - const handlerPromise = invokeClientToolHandler(tools[0], 'tc-client-1', { file: 'test.ts' }); - - // No pending_confirmation or tool_ready should have been emitted by the handler @@ -11593,7 +12116,7 @@ index ce67d5b0..00000000 - }); - - test('client tool handler does not emit tool_ready (permission flow owns it)', async () => { -- const { session, mockSession, signals, waitForSignal } = await createAgentSession(disposables, { clientSnapshot: snapshot }); +- const { session, runtime, mockSession, signals, waitForSignal } = await createAgentSession(disposables, { clientSnapshot: snapshot }); - - // SDK emits tool.execution_start — tool_start fires immediately - mockSession.fire('tool.execution_start', { @@ -11607,7 +12130,7 @@ index ce67d5b0..00000000 - assert.strictEqual(signals.filter(s => s.kind === 'pending_confirmation').length, 0); - - // Permission request fires — pending_confirmation from permission flow. -- const resultPromise = session.handlePermissionRequest({ +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'custom-tool', - toolCallId: 'tc-client-perm', - toolName: 'my_tool', @@ -11620,7 +12143,7 @@ index ce67d5b0..00000000 - assert.strictEqual(permSignals[0].state.toolCallId, 'tc-client-perm'); - assert.ok(permSignals[0].state.confirmationTitle); - -- const tools = session.createClientSdkTools(); +- const tools = runtime.createClientSdkTools(); - const handlerPromise = invokeClientToolHandler(tools[0], 'tc-client-perm'); - - // The handler should NOT emit its own pending_confirmation — only the @@ -11646,7 +12169,7 @@ index ce67d5b0..00000000 - // SessionToolCallReady to the subagent session and emits a - // stray ready against the parent session (no preceding - // SessionToolCallStart). -- const { session, mockSession, signals, waitForSignal } = await createAgentSession(disposables, { clientSnapshot: snapshot }); +- const { session, runtime, mockSession, signals, waitForSignal } = await createAgentSession(disposables, { clientSnapshot: snapshot }); - - mockSession.fire('subagent.started', { - toolCallId: 'tc-parent-subagent', @@ -11661,7 +12184,7 @@ index ce67d5b0..00000000 - arguments: {}, - } as SessionEventPayload<'tool.execution_start'>['data'], { agentId: 'agent-client-tool' }); - -- const resultPromise = session.handlePermissionRequest({ +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'custom-tool', - toolCallId: 'tc-sub-client', - toolName: 'my_tool', @@ -11677,7 +12200,7 @@ index ce67d5b0..00000000 - }); - - test('handleClientToolCallComplete pre-completes when no handler is waiting yet', async () => { -- const { session } = await createAgentSession(disposables, { clientSnapshot: snapshot }); +- const { session, runtime } = await createAgentSession(disposables, { clientSnapshot: snapshot }); - - // Completion arrives before handler — pre-creates deferred - session.handleClientToolCallComplete('tc-unknown', { @@ -11686,15 +12209,15 @@ index ce67d5b0..00000000 - }); - - // Handler picks up the pre-completed result -- const tools = session.createClientSdkTools(); +- const tools = runtime.createClientSdkTools(); - const result = await invokeClientToolHandler(tools[0], 'tc-unknown'); - assert.strictEqual(result.resultType, 'success'); - }); - - test('handleClientToolCallComplete with failure result', async () => { -- const { session } = await createAgentSession(disposables, { clientSnapshot: snapshot }); +- const { session, runtime } = await createAgentSession(disposables, { clientSnapshot: snapshot }); - -- const tools = session.createClientSdkTools(); +- const tools = runtime.createClientSdkTools(); - const handlerPromise = invokeClientToolHandler(tools[0], 'tc-client-3'); - - session.handleClientToolCallComplete('tc-client-3', { @@ -11709,9 +12232,9 @@ index ce67d5b0..00000000 - }); - - test('pending client tool calls are cancelled on dispose', async () => { -- const { session } = await createAgentSession(disposables, { clientSnapshot: snapshot }); +- const { session, runtime } = await createAgentSession(disposables, { clientSnapshot: snapshot }); - -- const tools = session.createClientSdkTools(); +- const tools = runtime.createClientSdkTools(); - const handlerPromise = invokeClientToolHandler(tools[0], 'tc-client-4'); - - session.dispose(); @@ -11721,9 +12244,9 @@ index ce67d5b0..00000000 - }); - - test('multiple concurrent client tool calls resolve independently', async () => { -- const { session } = await createAgentSession(disposables, { clientSnapshot: snapshot }); +- const { session, runtime } = await createAgentSession(disposables, { clientSnapshot: snapshot }); - -- const tools = session.createClientSdkTools(); +- const tools = runtime.createClientSdkTools(); - const promise1 = invokeClientToolHandler(tools[0], 'tc-multi-1'); - const promise2 = invokeClientToolHandler(tools[0], 'tc-multi-2'); - @@ -11745,9 +12268,9 @@ index ce67d5b0..00000000 - }); - - test('handler cleans up deferred after consuming result', async () => { -- const { session } = await createAgentSession(disposables, { clientSnapshot: snapshot }); +- const { session, runtime } = await createAgentSession(disposables, { clientSnapshot: snapshot }); - -- const tools = session.createClientSdkTools(); +- const tools = runtime.createClientSdkTools(); - const handlerPromise = invokeClientToolHandler(tools[0], 'tc-cleanup'); - - session.handleClientToolCallComplete('tc-cleanup', { @@ -11768,8 +12291,8 @@ index ce67d5b0..00000000 - - test('client tool handler logs and rethrows failures', async () => { - const logService = new CapturingLogService(); -- const { session } = await createAgentSession(disposables, { clientSnapshot: snapshot, logService }); -- const tools = session.createClientSdkTools(); +- const { session, runtime } = await createAgentSession(disposables, { clientSnapshot: snapshot, logService }); +- const tools = runtime.createClientSdkTools(); - const sessionInternals = session as unknown as ISessionInternalsForTest; - sessionInternals._pendingClientToolCalls.get = () => { - throw new Error('client tool boom'); @@ -11788,7 +12311,7 @@ index ce67d5b0..00000000 - }); - - test('permission request before client tool handler emits only confirmation ready', async () => { -- const { session, mockSession, signals, waitForSignal } = await createAgentSession(disposables, { clientSnapshot: snapshot }); +- const { session, runtime, mockSession, signals, waitForSignal } = await createAgentSession(disposables, { clientSnapshot: snapshot }); - - mockSession.fire('tool.execution_start', { - toolCallId: 'tc-ready-data', @@ -11801,7 +12324,7 @@ index ce67d5b0..00000000 - - // Permission before the handler should produce only the confirmation - // pending_confirmation, not a synthetic auto-ready. -- const resultPromise = session.handlePermissionRequest({ +- const resultPromise = runtime.handlePermissionRequest({ - kind: 'custom-tool', - toolCallId: 'tc-ready-data', - toolName: 'my_tool', @@ -11817,9 +12340,9 @@ index ce67d5b0..00000000 - }); - - test('handleClientToolCallComplete with content containing embedded resources', async () => { -- const { session } = await createAgentSession(disposables, { clientSnapshot: snapshot }); +- const { session, runtime } = await createAgentSession(disposables, { clientSnapshot: snapshot }); - -- const tools = session.createClientSdkTools(); +- const tools = runtime.createClientSdkTools(); - const handlerPromise = invokeClientToolHandler(tools[0], 'tc-embedded'); - - session.handleClientToolCallComplete('tc-embedded', { @@ -11875,11 +12398,11 @@ index ce67d5b0..00000000 - }); - - test('handleExitPlanModeRequest produces a single-select input request with options and recommended', async () => { -- const { session, mockSession, signals, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, mockSession, signals, waitForSignal } = await createAgentSession(disposables); - - mockSession.planReadResult = { exists: true, content: '## Plan', path: '/sessions/abc/plan.md' }; - -- const responsePromise = session.handleExitPlanModeRequest(planRequestParams()); +- const responsePromise = runtime.handleExitPlanModeRequest(planRequestParams(), { sessionId: 'test-session-1' }); - - const signal = await waitForSignal(s => isAction(s, ActionType.SessionInputRequested)); - const request = getInputRequest(signal); @@ -11916,9 +12439,9 @@ index ce67d5b0..00000000 - }); - - test('completing the input request with autopilot resolves with approved + autopilot + autoApproveEdits', async () => { -- const { session, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, waitForSignal } = await createAgentSession(disposables); - -- const responsePromise = session.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive'], recommendedAction: 'autopilot' })); +- const responsePromise = runtime.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive'], recommendedAction: 'autopilot' }), { sessionId: 'test-session-1' }); - const signal = await waitForSignal(s => isAction(s, ActionType.SessionInputRequested)); - const request = getInputRequest(signal); - const requestId = request.id; @@ -11935,9 +12458,9 @@ index ce67d5b0..00000000 - }); - - test('completing the input request with interactive resolves with approved + interactive (no autoApprove)', async () => { -- const { session, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, waitForSignal } = await createAgentSession(disposables); - -- const responsePromise = session.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive'], recommendedAction: 'interactive' })); +- const responsePromise = runtime.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive'], recommendedAction: 'interactive' }), { sessionId: 'test-session-1' }); - const signal = await waitForSignal(s => isAction(s, ActionType.SessionInputRequested)); - const request = getInputRequest(signal); - const requestId = request.id; @@ -11954,9 +12477,9 @@ index ce67d5b0..00000000 - }); - - test('declining the input request resolves with approved=false', async () => { -- const { session, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, waitForSignal } = await createAgentSession(disposables); - -- const responsePromise = session.handleExitPlanModeRequest(planRequestParams()); +- const responsePromise = runtime.handleExitPlanModeRequest(planRequestParams(), { sessionId: 'test-session-1' }); - const signal = await waitForSignal(s => isAction(s, ActionType.SessionInputRequested)); - - session.respondToUserInputRequest(getInputRequest(signal).id, SessionInputResponseKind.Decline); @@ -11965,9 +12488,9 @@ index ce67d5b0..00000000 - }); - - test('exit_only resolves as approved + interactive without autoApproveEdits', async () => { -- const { session, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, waitForSignal } = await createAgentSession(disposables); - -- const responsePromise = session.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive', 'exit_only'], recommendedAction: 'exit_only' })); +- const responsePromise = runtime.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive', 'exit_only'], recommendedAction: 'exit_only' }), { sessionId: 'test-session-1' }); - const signal = await waitForSignal(s => isAction(s, ActionType.SessionInputRequested)); - const request = getInputRequest(signal); - const requestId = request.id; @@ -11984,9 +12507,9 @@ index ce67d5b0..00000000 - }); - - test('freeform feedback alongside a selected action becomes a revision request', async () => { -- const { session, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, waitForSignal } = await createAgentSession(disposables); - -- const responsePromise = session.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive'], recommendedAction: 'interactive' })); +- const responsePromise = runtime.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive'], recommendedAction: 'interactive' }), { sessionId: 'test-session-1' }); - const signal = await waitForSignal(s => isAction(s, ActionType.SessionInputRequested)); - const request = getInputRequest(signal); - const requestId = request.id; @@ -12011,9 +12534,9 @@ index ce67d5b0..00000000 - }); - - test('selectedAction not in offered actions falls back to recommendedAction', async () => { -- const { session, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, waitForSignal } = await createAgentSession(disposables); - -- const responsePromise = session.handleExitPlanModeRequest(planRequestParams({ actions: ['interactive', 'exit_only'], recommendedAction: 'interactive' })); +- const responsePromise = runtime.handleExitPlanModeRequest(planRequestParams({ actions: ['interactive', 'exit_only'], recommendedAction: 'interactive' }), { sessionId: 'test-session-1' }); - const signal = await waitForSignal(s => isAction(s, ActionType.SessionInputRequested)); - const request = getInputRequest(signal); - const requestId = request.id; @@ -12034,12 +12557,12 @@ index ce67d5b0..00000000 - }); - - test('selectedAction not in offered actions and no fallback resolves to approved=false', async () => { -- const { session, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, waitForSignal } = await createAgentSession(disposables); - - // SDK offered `exit_only` only and recommended a value not in - // the offered set. The client picked something invalid. With - // no usable selectedAction and no feedback, decline. -- const responsePromise = session.handleExitPlanModeRequest(planRequestParams({ actions: ['exit_only'], recommendedAction: 'autopilot' })); +- const responsePromise = runtime.handleExitPlanModeRequest(planRequestParams({ actions: ['exit_only'], recommendedAction: 'autopilot' }), { sessionId: 'test-session-1' }); - const signal = await waitForSignal(s => isAction(s, ActionType.SessionInputRequested)); - const request = getInputRequest(signal); - const requestId = request.id; @@ -12056,9 +12579,9 @@ index ce67d5b0..00000000 - }); - - test('text answer with feedback becomes a revision request without selectedAction', async () => { -- const { session, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, waitForSignal } = await createAgentSession(disposables); - -- const responsePromise = session.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive'], recommendedAction: 'interactive' })); +- const responsePromise = runtime.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive'], recommendedAction: 'interactive' }), { sessionId: 'test-session-1' }); - const signal = await waitForSignal(s => isAction(s, ActionType.SessionInputRequested)); - const request = getInputRequest(signal); - const requestId = request.id; @@ -12083,9 +12606,9 @@ index ce67d5b0..00000000 - }); - - test('whitespace-only freeform feedback is ignored', async () => { -- const { session, waitForSignal } = await createAgentSession(disposables); +- const { session, runtime, waitForSignal } = await createAgentSession(disposables); - -- const responsePromise = session.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive'], recommendedAction: 'interactive' })); +- const responsePromise = runtime.handleExitPlanModeRequest(planRequestParams({ actions: ['autopilot', 'interactive'], recommendedAction: 'interactive' }), { sessionId: 'test-session-1' }); - const signal = await waitForSignal(s => isAction(s, ActionType.SessionInputRequested)); - const request = getInputRequest(signal); - const requestId = request.id; @@ -12149,14 +12672,14 @@ index ce67d5b0..00000000 - // ---- autopilot fast-path ------------------------------------------- - - test('handleExitPlanModeRequest auto-accepts when autoApprove=autopilot (recommended action)', async () => { -- const { session, signals } = await createAgentSession(disposables, { +- const { runtime, signals } = await createAgentSession(disposables, { - configValues: { [SessionConfigKey.AutoApprove]: 'autopilot' }, - }); - -- const response = await session.handleExitPlanModeRequest(planRequestParams({ +- const response = await runtime.handleExitPlanModeRequest(planRequestParams({ - actions: ['autopilot', 'interactive', 'exit_only'], - recommendedAction: 'autopilot', -- })); +- }), { sessionId: 'test-session-1' }); - - assert.deepStrictEqual(response, { approved: true, selectedAction: 'autopilot', autoApproveEdits: true }); - // User-input request should NOT be surfaced to the client. @@ -12164,27 +12687,27 @@ index ce67d5b0..00000000 - }); - - test('handleExitPlanModeRequest auto-accepts with priority order when no recommended action available', async () => { -- const { session } = await createAgentSession(disposables, { +- const { runtime } = await createAgentSession(disposables, { - configValues: { [SessionConfigKey.AutoApprove]: 'autopilot' }, - }); - - // SDK proposes a recommended action that's NOT in the offered set — - // fall back to the priority order (autopilot > autopilot_fleet > - // interactive > exit_only). -- const response = await session.handleExitPlanModeRequest(planRequestParams({ +- const response = await runtime.handleExitPlanModeRequest(planRequestParams({ - actions: ['interactive', 'exit_only'], - recommendedAction: 'autopilot_fleet', -- })); +- }), { sessionId: 'test-session-1' }); - - assert.deepStrictEqual(response, { approved: true, selectedAction: 'interactive' }); - }); - - test('handleExitPlanModeRequest does NOT auto-accept when autoApprove=default', async () => { -- const { session, waitForSignal } = await createAgentSession(disposables, { +- const { session, runtime, waitForSignal } = await createAgentSession(disposables, { - configValues: { [SessionConfigKey.AutoApprove]: 'default' }, - }); - -- const responsePromise = session.handleExitPlanModeRequest(planRequestParams()); +- const responsePromise = runtime.handleExitPlanModeRequest(planRequestParams(), { sessionId: 'test-session-1' }); - - // The user-input request fires — the user must respond. - const signal = await waitForSignal(s => isAction(s, ActionType.SessionInputRequested)); @@ -12195,7 +12718,7 @@ index ce67d5b0..00000000 -}); 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 b64fea2e..00000000 +index 6c51573d1ff..00000000000 --- a/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts +++ /dev/null @@ -1,448 +0,0 @@ @@ -12218,10 +12741,10 @@ index b64fea2e..00000000 -import { McpServerType } from '../../../mcp/common/mcpPlatformTypes.js'; -import { toSdkInstructionDirectories, toSdkMcpServers, toSdkCustomAgents, toSdkSkillDirectories, parsedPluginsEqual, toSdkHooks } from '../../node/copilot/copilotPluginConverters.js'; -import type { IMcpServerDefinition, INamedPluginResource, IParsedHookGroup, IParsedPlugin, IParsedSkill } from '../../../agentPlugins/common/pluginParsers.js'; --import { CustomizationType, type HookCustomization, type McpServerCustomization, type SkillCustomization } from '../../common/state/protocol/state.js'; +-import { CustomizationType, McpServerStatus, type HookCustomization, type McpServerCustomization, type SkillCustomization } from '../../common/state/protocol/state.js'; - -function stubMcpCustomization(name = 'test'): McpServerCustomization { -- return { type: CustomizationType.McpServer, id: `mcp:${name}`, uri: 'file:///plugin', name }; +- return { type: CustomizationType.McpServer, id: `mcp:${name}`, uri: 'file:///plugin', name, enabled: true, state: { kind: McpServerStatus.Starting } }; -} -function stubHookCustomization(type: string): HookCustomization { - return { type: CustomizationType.Hook, id: `hook:${type}`, uri: 'file:///plugin/hooks.json', name: 'hooks.json' }; @@ -12649,7 +13172,7 @@ index b64fea2e..00000000 -}); diff --git a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts b/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts deleted file mode 100644 -index 93f0cfbe..00000000 +index 93f0cfbe1e4..00000000000 --- a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts +++ /dev/null @@ -1,978 +0,0 @@ @@ -13633,10 +14156,10 @@ index 93f0cfbe..00000000 -}); diff --git a/src/vs/platform/agentHost/test/node/copilotSlashCommandCompletionProvider.test.ts b/src/vs/platform/agentHost/test/node/copilotSlashCommandCompletionProvider.test.ts deleted file mode 100644 -index 61f7e809..00000000 +index 7578ef4d294..00000000000 --- a/src/vs/platform/agentHost/test/node/copilotSlashCommandCompletionProvider.test.ts +++ /dev/null -@@ -1,216 +0,0 @@ +@@ -1,198 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. @@ -13651,20 +14174,6 @@ index 61f7e809..00000000 - -suite('CopilotSlashCommandCompletionProvider', () => { - -- let _savedRubberDuckEnv: string | undefined; -- suiteSetup(() => { -- _savedRubberDuckEnv = process.env['RUBBER_DUCK_AGENT']; -- process.env['RUBBER_DUCK_AGENT'] = 'true'; -- }); -- -- suiteTeardown(() => { -- if (_savedRubberDuckEnv === undefined) { -- delete process.env['RUBBER_DUCK_AGENT']; -- } else { -- process.env['RUBBER_DUCK_AGENT'] = _savedRubberDuckEnv; -- } -- }); -- - ensureNoDisposablesAreLeakedInTestSuite(); - - suite('parseLeadingSlashCommand', () => { @@ -13726,7 +14235,7 @@ index 61f7e809..00000000 - }); - - suite('provideCompletionItems', () => { -- const provider = new CopilotSlashCommandCompletionProvider('copilotcli'); +- const provider = new CopilotSlashCommandCompletionProvider('copilotcli', { hasHistory: () => true, isRubberDuckEnabled: () => true }); - const session = 'copilotcli:/abc'; - - async function run(text: string, offset = text.length) { @@ -13821,30 +14330,26 @@ index 61f7e809..00000000 - }); - - test('omits /compact when session has no history', async () => { -- const gated = new CopilotSlashCommandCompletionProvider('copilotcli', { hasHistory: () => false }); +- const gated = new CopilotSlashCommandCompletionProvider('copilotcli', { hasHistory: () => false, isRubberDuckEnabled: () => true }); - const items = await gated.provideCompletionItems({ - kind: CompletionItemKind.UserMessage, channel: session, text: '/', offset: 1, - }, CancellationToken.None); - assert.deepStrictEqual(items.map(i => i.insertText), ['/plan ', '/research ', '/rubber-duck ']); - }); - -- test('omits /rubber-duck when env var is unset', async () => { -- const saved = process.env['RUBBER_DUCK_AGENT']; -- delete process.env['RUBBER_DUCK_AGENT']; -- try { -- const items = await run('/'); -- assert.deepStrictEqual(items.map(i => i.insertText), ['/plan ', '/compact', '/research ']); -- } finally { -- if (saved !== undefined) { -- process.env['RUBBER_DUCK_AGENT'] = saved; -- } -- } +- test('omits /rubber-duck when not enabled', async () => { +- const gated = new CopilotSlashCommandCompletionProvider('copilotcli', { hasHistory: () => true, isRubberDuckEnabled: () => false }); +- const items = await gated.provideCompletionItems({ +- kind: CompletionItemKind.UserMessage, channel: session, text: '/', offset: 1, +- }, CancellationToken.None); +- assert.deepStrictEqual(items.map(i => i.insertText), ['/plan ', '/compact', '/research ']); - }); - - test('passes raw session id (no scheme/slash) to hasHistory', async () => { - let seen: string | undefined; - const gated = new CopilotSlashCommandCompletionProvider('copilotcli', { - hasHistory: (id: string) => { seen = id; return true; }, +- isRubberDuckEnabled: () => true, - }); - await gated.provideCompletionItems({ - kind: CompletionItemKind.UserMessage, channel: 'copilotcli:/abc', text: '/', offset: 1, @@ -13855,7 +14360,7 @@ index 61f7e809..00000000 -}); diff --git a/src/vs/platform/agentHost/test/node/copilotToolDisplay.test.ts b/src/vs/platform/agentHost/test/node/copilotToolDisplay.test.ts deleted file mode 100644 -index d7a36fb8..00000000 +index d7a36fb8625..00000000000 --- a/src/vs/platform/agentHost/test/node/copilotToolDisplay.test.ts +++ /dev/null @@ -1,612 +0,0 @@ diff --git a/upstream/stable.json b/upstream/stable.json index a5d1a2a..10485b7 100644 --- a/upstream/stable.json +++ b/upstream/stable.json @@ -1,4 +1,4 @@ { - "tag": "1.123.0", - "commit": "6a44c352bd24569c417e530095901b649960f9f8" + "tag": "1.124.0", + "commit": "1b50d58d73426c9171299ec4037d01365d995b78" }