Merge pull request #309237 from microsoft/roblou/agent-host-session-cwd

Fix agent host session working directories
This commit is contained in:
Rob Lourens
2026-04-12 10:29:39 -07:00
committed by GitHub
14 changed files with 255 additions and 85 deletions
+33 -33
View File
@@ -11,8 +11,8 @@
"license": "MIT",
"dependencies": {
"@anthropic-ai/sandbox-runtime": "0.0.42",
"@github/copilot": "^1.0.11",
"@github/copilot-sdk": "^0.2.0",
"@github/copilot": "^1.0.24",
"@github/copilot-sdk": "^0.2.2",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@microsoft/dev-tunnels-connections": "^1.3.41",
@@ -1072,26 +1072,26 @@
}
},
"node_modules/@github/copilot": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.11.tgz",
"integrity": "sha512-cptVopko/tNKEXyBP174yBjHQBEwg6CqaKN2S0M3J+5LEB8u31bLL75ioOPd+5vubqBrA0liyTdcHeZ8UTRbmg==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.24.tgz",
"integrity": "sha512-/nZ2GwhaGq0HeI3W+6LE0JGw25/bipC6tYVa+oQ5tIvAafBazuNt10CXkeaor+u9oBWLZtPbdTyAzE2tjy9NpQ==",
"license": "SEE LICENSE IN LICENSE.md",
"bin": {
"copilot": "npm-loader.js"
},
"optionalDependencies": {
"@github/copilot-darwin-arm64": "1.0.11",
"@github/copilot-darwin-x64": "1.0.11",
"@github/copilot-linux-arm64": "1.0.11",
"@github/copilot-linux-x64": "1.0.11",
"@github/copilot-win32-arm64": "1.0.11",
"@github/copilot-win32-x64": "1.0.11"
"@github/copilot-darwin-arm64": "1.0.24",
"@github/copilot-darwin-x64": "1.0.24",
"@github/copilot-linux-arm64": "1.0.24",
"@github/copilot-linux-x64": "1.0.24",
"@github/copilot-win32-arm64": "1.0.24",
"@github/copilot-win32-x64": "1.0.24"
}
},
"node_modules/@github/copilot-darwin-arm64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.11.tgz",
"integrity": "sha512-wdKimjtbsVeXqMqQSnGpGBPFEYHljxXNuWeH8EIJTNRgFpAsimcivsFgql3Twq4YOp0AxfsH36icG4IEen30mA==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.24.tgz",
"integrity": "sha512-lejn6KV+09rZEICX3nRx9a58DQFQ2kK3NJ3EICfVLngUCWIUmwn1BLezjeTQc9YNasHltA1hulvfsWqX+VjlMw==",
"cpu": [
"arm64"
],
@@ -1105,9 +1105,9 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.11.tgz",
"integrity": "sha512-VeuPv8rzBVGBB8uDwMEhcHBpldoKaq26yZ5YQm+G9Ka5QIF+1DMah8ZNRMVsTeNKkb1ji9G8vcuCsaPbnG3fKg==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.24.tgz",
"integrity": "sha512-r2F3keTvr4Bunz3V+waRAvsHgqsVQGyIZFBebsNPWxBX1eh3IXgtBqxCR7vXTFyZonQ8VaiJH3YYEfAhyKsk9g==",
"cpu": [
"x64"
],
@@ -1121,9 +1121,9 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.11.tgz",
"integrity": "sha512-/d8p6RlFYKj1Va2hekFIcYNMHWagcEkaxgcllUNXSyQLnmEtXUkaWtz62VKGWE+n/UMkEwCB6vI2xEwPTlUNBQ==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.24.tgz",
"integrity": "sha512-B3oANXKKKLhnKYozXa/W+DxfCQAHJCs0QKR5rBwNrwJbf656twNgALSxWTSJk+1rEP6MrHCswUAcwjwZL7Q+FQ==",
"cpu": [
"arm64"
],
@@ -1137,9 +1137,9 @@
}
},
"node_modules/@github/copilot-linux-x64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.11.tgz",
"integrity": "sha512-UujTRO3xkPFC1CybchBbCnaTEAG6JrH0etIst07JvfekMWgvRxbiCHQPpDPSzBCPiBcGu0gba0/IT+vUCORuIw==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.24.tgz",
"integrity": "sha512-NGTldizY54B+4Sfhu/GWoEQNMwqqUNgMwbSgBshFv+Hqy1EwuvNWKVov1Y0Vzhp4qAHc6ZxBk/OPIW8Ato9FUg==",
"cpu": [
"x64"
],
@@ -1153,12 +1153,12 @@
}
},
"node_modules/@github/copilot-sdk": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.2.0.tgz",
"integrity": "sha512-fCEpD9W9xqcaCAJmatyNQ1PkET9P9liK2P4Vk0raDFoMXcvpIdqewa5JQeKtWCBUsN/HCz7ExkkFP8peQuo+DA==",
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.2.2.tgz",
"integrity": "sha512-VZCqS08YlUM90bUKJ7VLeIxgTTEHtfXBo84T1IUMNvXRREX2csjPH6Z+CPw3S2468RcCLvzBXcc9LtJJTLIWFw==",
"license": "MIT",
"dependencies": {
"@github/copilot": "^1.0.10",
"@github/copilot": "^1.0.21",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
},
@@ -1176,9 +1176,9 @@
}
},
"node_modules/@github/copilot-win32-arm64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.11.tgz",
"integrity": "sha512-EOW8HUM+EmnHEZEa+iUMl4pP1+2eZUk2XCbynYiMehwX9sidc4BxEHp2RuxADSzFPTieQEWzgjQmHWrtet8pQg==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.24.tgz",
"integrity": "sha512-/pd/kgef7/HIIg1SQq4q8fext39pDSC44jHB10KkhfgG1WaDFhQbc/aSSMQfxeldkRbQh6VANp8WtGQdwtMCBA==",
"cpu": [
"arm64"
],
@@ -1192,9 +1192,9 @@
}
},
"node_modules/@github/copilot-win32-x64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.11.tgz",
"integrity": "sha512-fKGkSNamzs3h9AbmswNvPYJBORCb2Y8CbusijU3C7fT3ohvqnHJwKo5iHhJXLOKZNOpFZgq9YKha410u9sIs6Q==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.24.tgz",
"integrity": "sha512-RDvOiSvyEJwELqErwANJTrdRuMIHkwPE4QK7Le7WsmaSKxiuS4H1Pa8Yxnd2FWrMsCHEbase23GJlymbnGYLXQ==",
"cpu": [
"x64"
],
+2 -2
View File
@@ -88,8 +88,8 @@
},
"dependencies": {
"@anthropic-ai/sandbox-runtime": "0.0.42",
"@github/copilot": "^1.0.11",
"@github/copilot-sdk": "^0.2.0",
"@github/copilot": "^1.0.24",
"@github/copilot-sdk": "^0.2.2",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@microsoft/dev-tunnels-connections": "^1.3.41",
+33 -33
View File
@@ -9,8 +9,8 @@
"version": "0.0.0",
"dependencies": {
"@anthropic-ai/sandbox-runtime": "0.0.42",
"@github/copilot": "^1.0.11",
"@github/copilot-sdk": "^0.2.0",
"@github/copilot": "^1.0.24",
"@github/copilot-sdk": "^0.2.2",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@parcel/watcher": "^2.5.6",
@@ -83,26 +83,26 @@
}
},
"node_modules/@github/copilot": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.11.tgz",
"integrity": "sha512-cptVopko/tNKEXyBP174yBjHQBEwg6CqaKN2S0M3J+5LEB8u31bLL75ioOPd+5vubqBrA0liyTdcHeZ8UTRbmg==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.24.tgz",
"integrity": "sha512-/nZ2GwhaGq0HeI3W+6LE0JGw25/bipC6tYVa+oQ5tIvAafBazuNt10CXkeaor+u9oBWLZtPbdTyAzE2tjy9NpQ==",
"license": "SEE LICENSE IN LICENSE.md",
"bin": {
"copilot": "npm-loader.js"
},
"optionalDependencies": {
"@github/copilot-darwin-arm64": "1.0.11",
"@github/copilot-darwin-x64": "1.0.11",
"@github/copilot-linux-arm64": "1.0.11",
"@github/copilot-linux-x64": "1.0.11",
"@github/copilot-win32-arm64": "1.0.11",
"@github/copilot-win32-x64": "1.0.11"
"@github/copilot-darwin-arm64": "1.0.24",
"@github/copilot-darwin-x64": "1.0.24",
"@github/copilot-linux-arm64": "1.0.24",
"@github/copilot-linux-x64": "1.0.24",
"@github/copilot-win32-arm64": "1.0.24",
"@github/copilot-win32-x64": "1.0.24"
}
},
"node_modules/@github/copilot-darwin-arm64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.11.tgz",
"integrity": "sha512-wdKimjtbsVeXqMqQSnGpGBPFEYHljxXNuWeH8EIJTNRgFpAsimcivsFgql3Twq4YOp0AxfsH36icG4IEen30mA==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.24.tgz",
"integrity": "sha512-lejn6KV+09rZEICX3nRx9a58DQFQ2kK3NJ3EICfVLngUCWIUmwn1BLezjeTQc9YNasHltA1hulvfsWqX+VjlMw==",
"cpu": [
"arm64"
],
@@ -116,9 +116,9 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.11.tgz",
"integrity": "sha512-VeuPv8rzBVGBB8uDwMEhcHBpldoKaq26yZ5YQm+G9Ka5QIF+1DMah8ZNRMVsTeNKkb1ji9G8vcuCsaPbnG3fKg==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.24.tgz",
"integrity": "sha512-r2F3keTvr4Bunz3V+waRAvsHgqsVQGyIZFBebsNPWxBX1eh3IXgtBqxCR7vXTFyZonQ8VaiJH3YYEfAhyKsk9g==",
"cpu": [
"x64"
],
@@ -132,9 +132,9 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.11.tgz",
"integrity": "sha512-/d8p6RlFYKj1Va2hekFIcYNMHWagcEkaxgcllUNXSyQLnmEtXUkaWtz62VKGWE+n/UMkEwCB6vI2xEwPTlUNBQ==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.24.tgz",
"integrity": "sha512-B3oANXKKKLhnKYozXa/W+DxfCQAHJCs0QKR5rBwNrwJbf656twNgALSxWTSJk+1rEP6MrHCswUAcwjwZL7Q+FQ==",
"cpu": [
"arm64"
],
@@ -148,9 +148,9 @@
}
},
"node_modules/@github/copilot-linux-x64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.11.tgz",
"integrity": "sha512-UujTRO3xkPFC1CybchBbCnaTEAG6JrH0etIst07JvfekMWgvRxbiCHQPpDPSzBCPiBcGu0gba0/IT+vUCORuIw==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.24.tgz",
"integrity": "sha512-NGTldizY54B+4Sfhu/GWoEQNMwqqUNgMwbSgBshFv+Hqy1EwuvNWKVov1Y0Vzhp4qAHc6ZxBk/OPIW8Ato9FUg==",
"cpu": [
"x64"
],
@@ -164,12 +164,12 @@
}
},
"node_modules/@github/copilot-sdk": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.2.0.tgz",
"integrity": "sha512-fCEpD9W9xqcaCAJmatyNQ1PkET9P9liK2P4Vk0raDFoMXcvpIdqewa5JQeKtWCBUsN/HCz7ExkkFP8peQuo+DA==",
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.2.2.tgz",
"integrity": "sha512-VZCqS08YlUM90bUKJ7VLeIxgTTEHtfXBo84T1IUMNvXRREX2csjPH6Z+CPw3S2468RcCLvzBXcc9LtJJTLIWFw==",
"license": "MIT",
"dependencies": {
"@github/copilot": "^1.0.10",
"@github/copilot": "^1.0.21",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
},
@@ -187,9 +187,9 @@
}
},
"node_modules/@github/copilot-win32-arm64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.11.tgz",
"integrity": "sha512-EOW8HUM+EmnHEZEa+iUMl4pP1+2eZUk2XCbynYiMehwX9sidc4BxEHp2RuxADSzFPTieQEWzgjQmHWrtet8pQg==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.24.tgz",
"integrity": "sha512-/pd/kgef7/HIIg1SQq4q8fext39pDSC44jHB10KkhfgG1WaDFhQbc/aSSMQfxeldkRbQh6VANp8WtGQdwtMCBA==",
"cpu": [
"arm64"
],
@@ -203,9 +203,9 @@
}
},
"node_modules/@github/copilot-win32-x64": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.11.tgz",
"integrity": "sha512-fKGkSNamzs3h9AbmswNvPYJBORCb2Y8CbusijU3C7fT3ohvqnHJwKo5iHhJXLOKZNOpFZgq9YKha410u9sIs6Q==",
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.24.tgz",
"integrity": "sha512-RDvOiSvyEJwELqErwANJTrdRuMIHkwPE4QK7Le7WsmaSKxiuS4H1Pa8Yxnd2FWrMsCHEbase23GJlymbnGYLXQ==",
"cpu": [
"x64"
],
+2 -2
View File
@@ -4,8 +4,8 @@
"private": true,
"dependencies": {
"@anthropic-ai/sandbox-runtime": "0.0.42",
"@github/copilot": "^1.0.11",
"@github/copilot-sdk": "^0.2.0",
"@github/copilot": "^1.0.24",
"@github/copilot-sdk": "^0.2.2",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@parcel/watcher": "^2.5.6",
@@ -467,6 +467,11 @@ export class CopilotAgent extends Disposable implements IAgent {
const parsedPlugins = await this._plugins.getAppliedPlugins();
const sessionUri = AgentSession.uri(this.id, sessionId);
const sessionMetadata = await client.getSessionMetadata(sessionId).catch(err => {
this._logService.warn(`[Copilot:${sessionId}] getSessionMetadata failed`, err);
return undefined;
});
const workingDirectory = typeof sessionMetadata?.context?.cwd === 'string' ? URI.file(sessionMetadata.context.cwd) : undefined;
const shellManager = this._instantiationService.createInstance(ShellManager, sessionUri);
const sessionConfig = this._buildSessionConfig(parsedPlugins, shellManager);
@@ -475,6 +480,7 @@ export class CopilotAgent extends Disposable implements IAgent {
try {
const raw = await client.resumeSession(sessionId, {
...config,
workingDirectory: workingDirectory?.fsPath,
});
return new CopilotSessionWrapper(raw);
} catch (err) {
@@ -499,7 +505,7 @@ export class CopilotAgent extends Disposable implements IAgent {
}
};
const agentSession = this._createAgentSession(factory, undefined, sessionId, shellManager);
const agentSession = this._createAgentSession(factory, workingDirectory, sessionId, shellManager);
this._plugins.setAppliedPlugins(agentSession, parsedPlugins);
await agentSession.initializeSession();
@@ -8,6 +8,7 @@ import { AgentHostEnabledSettingId } from '../../../../platform/agentHost/common
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
import { IAgentHostSessionWorkingDirectoryResolver } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionWorkingDirectoryResolver.js';
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
import { LocalAgentHostSessionsProvider } from './localAgentHostSessionsProvider.js';
@@ -29,6 +30,7 @@ class LocalAgentHostContribution extends Disposable implements IWorkbenchContrib
@IConfigurationService configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@ISessionsProvidersService sessionsProvidersService: ISessionsProvidersService,
@IAgentHostSessionWorkingDirectoryResolver workingDirectoryResolver: IAgentHostSessionWorkingDirectoryResolver,
) {
super();
@@ -38,6 +40,12 @@ class LocalAgentHostContribution extends Disposable implements IWorkbenchContrib
const provider = this._register(instantiationService.createInstance(LocalAgentHostSessionsProvider));
this._register(sessionsProvidersService.registerProvider(provider));
for (const sessionType of provider.sessionTypes) {
this._register(workingDirectoryResolver.registerResolver(sessionType.id, sessionResource => {
const repository = provider.getSessionByResource(sessionResource)?.workspace.get()?.repositories[0];
return repository?.workingDirectory ?? repository?.uri;
}));
}
}
}
@@ -283,6 +283,25 @@ export class LocalAgentHostSessionsProvider extends Disposable implements ISessi
return sessions;
}
getSessionByResource(resource: URI): ISession | undefined {
if (this._currentNewSession?.resource.toString() === resource.toString()) {
return this._currentNewSession;
}
if (this._pendingSession?.resource.toString() === resource.toString()) {
return this._pendingSession;
}
this._ensureSessionCache();
for (const cached of this._sessionCache.values()) {
if (cached.resource.toString() === resource.toString()) {
return cached;
}
}
return undefined;
}
// -- Session Lifecycle --
createNewSession(workspace: ISessionWorkspace): ISession {
@@ -306,6 +306,29 @@ suite('LocalAgentHostSessionsProvider', () => {
assert.strictEqual(session.sessionType, provider.sessionTypes[0].id);
});
test('getSessionByResource resolves current new session without listing it', () => {
const provider = createProvider(disposables, agentHost);
const workspace = {
label: 'my-project',
icon: { id: 'folder' },
repositories: [{ uri: URI.parse('file:///home/user/project'), workingDirectory: undefined, detail: undefined, baseBranchName: undefined, baseBranchProtected: undefined }],
requiresWorkspaceTrust: true,
};
const session = provider.createNewSession(workspace);
const resolved = provider.getSessionByResource(session.resource);
assert.deepStrictEqual({
listedSessions: provider.getSessions().length,
resolvedResource: resolved?.resource.toString(),
resolvedWorkspaceLabel: resolved?.workspace.get()?.label,
}, {
listedSessions: 0,
resolvedResource: session.resource.toString(),
resolvedWorkspaceLabel: 'my-project',
});
});
test('createNewSession throws when no repository URI', () => {
const provider = createProvider(disposables, agentHost);
const workspace = { label: 'empty', icon: { id: 'folder' }, repositories: [], requiresWorkspaceTrust: false };
@@ -39,7 +39,6 @@ import { createRemoteAgentHarnessDescriptor, RemoteAgentCustomizationItemProvide
import { RemoteAgentHostSessionsProvider } from './remoteAgentHostSessionsProvider.js';
import { SyncedCustomizationBundler } from './syncedCustomizationBundler.js';
import { ISSHRemoteAgentHostService } from '../../../../platform/agentHost/common/sshRemoteAgentHost.js';
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
/** Per-connection state bundle, disposed when a connection is removed. */
class ConnectionState extends Disposable {
@@ -86,7 +85,6 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
@IDefaultAccountService private readonly _defaultAccountService: IDefaultAccountService,
@ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService,
@ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IAgentHostFileSystemService private readonly _agentHostFileSystemService: IAgentHostFileSystemService,
@@ -339,6 +337,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
connState.store.add(agentStore);
const sanitized = agentHostAuthority(address);
const providerId = `agenthost-${sanitized}`;
const sessionType = `remote-${sanitized}-${agent.provider}`;
const agentId = sessionType;
const vendor = sessionType;
@@ -349,17 +348,20 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
const sessionWorkingDirs = new Map<string, URI>();
agentStore.add(toDisposable(() => sessionWorkingDirs.clear()));
// Capture the working directory from the active session for new sessions
const resolveWorkingDirectory = (resourceKey: string): URI | undefined => {
// Capture the working directory from the session that is being created.
const resolveWorkingDirectory = (sessionResource: URI): URI | undefined => {
const resourceKey = sessionResource.toString();
const cached = sessionWorkingDirs.get(resourceKey);
if (cached) {
return cached;
}
const activeSession = this._sessionsManagementService.activeSession.get();
const repoUri = activeSession?.workspace.get()?.repositories[0]?.uri;
if (repoUri) {
sessionWorkingDirs.set(resourceKey, repoUri);
return repoUri;
const provider = this._sessionsProvidersService.getProvider<RemoteAgentHostSessionsProvider>(providerId);
const session = provider?.getSessionByResource(sessionResource);
const repository = session?.workspace.get()?.repositories[0];
const workingDirectory = repository?.workingDirectory ?? repository?.uri;
if (workingDirectory) {
sessionWorkingDirs.set(resourceKey, workingDirectory);
return workingDirectory;
}
return undefined;
};
@@ -370,6 +370,25 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess
return sessions;
}
getSessionByResource(resource: URI): ISession | undefined {
if (this._currentNewSession?.resource.toString() === resource.toString()) {
return this._chatToSession(this._currentNewSession);
}
if (this._pendingSession?.resource.toString() === resource.toString()) {
return this._pendingSession;
}
this._ensureSessionCache();
for (const cached of this._sessionCache.values()) {
if (cached.resource.toString() === resource.toString()) {
return this._chatToSession(cached);
}
}
return undefined;
}
// -- Session Lifecycle --
private _currentNewSession: IChatData | undefined;
@@ -369,6 +369,29 @@ suite('RemoteAgentHostSessionsProvider', () => {
assert.strictEqual(session.sessionType, provider.sessionTypes[0].id);
});
test('getSessionByResource resolves current new session without listing it', () => {
const provider = createProvider(disposables, connection);
const workspace = {
label: 'my-project',
icon: { id: 'remote' },
repositories: [{ uri: URI.parse('vscode-agent-host://auth/home/user/project'), workingDirectory: undefined, detail: undefined, baseBranchName: undefined, baseBranchProtected: undefined }],
requiresWorkspaceTrust: false,
};
const session = provider.createNewSession(workspace);
const resolved = provider.getSessionByResource(session.resource);
assert.deepStrictEqual({
listedSessions: provider.getSessions().length,
resolvedResource: resolved?.resource.toString(),
resolvedWorkspaceLabel: resolved?.workspace.get()?.label,
}, {
listedSessions: 0,
resolvedResource: session.resource.toString(),
resolvedWorkspaceLabel: 'my-project',
});
});
test('createNewSession throws when no repository URI', () => {
const provider = createProvider(disposables, connection);
const workspace = { label: 'empty', icon: { id: 'remote' }, repositories: [], requiresWorkspaceTrust: false };
@@ -37,6 +37,7 @@ import { IChatAgentData, IChatAgentImplementation, IChatAgentRequest, IChatAgent
import { ToolInvocationPresentation } from '../../../common/tools/languageModelToolsService.js';
import { getAgentHostIcon } from '../agentSessions.js';
import { AgentHostEditingSession } from './agentHostEditingSession.js';
import { IAgentHostSessionWorkingDirectoryResolver } from './agentHostSessionWorkingDirectoryResolver.js';
import { IAgentHostTerminalService } from '../../../../terminal/browser/agentHostTerminalService.js';
import { activeTurnToProgress, completedToolCallToEditParts, completedToolCallToSerialized, finalizeToolInvocation, getTerminalContentUri, toolCallStateToInvocation, turnsToHistory, updateRunningToolSpecificData, type IToolCallFileEdit } from './stateToProgressAdapter.js';
import { getToolKind } from '../../../../../../platform/agentHost/common/state/sessionReducers.js';
@@ -237,9 +238,10 @@ export interface IAgentHostSessionHandlerConfig {
readonly extensionDisplayName?: string;
/**
* Optional callback to resolve a working directory for a new session.
* If not provided, falls back to the first workspace folder.
* If not provided or unresolved, session resource resolvers are consulted before
* falling back to the first workspace folder.
*/
readonly resolveWorkingDirectory?: (resourceKey: string) => URI | undefined;
readonly resolveWorkingDirectory?: (sessionResource: URI) => URI | undefined;
/**
* Optional callback invoked when the server rejects an operation because
* authentication is required. Should trigger interactive authentication
@@ -286,6 +288,7 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ITerminalChatService private readonly _terminalChatService: ITerminalChatService,
@IAgentHostTerminalService private readonly _agentHostTerminalService: IAgentHostTerminalService,
@IAgentHostSessionWorkingDirectoryResolver private readonly _workingDirectoryResolver: IAgentHostSessionWorkingDirectoryResolver,
) {
super();
this._config = config;
@@ -1798,8 +1801,8 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC
/** Creates a new backend session and subscribes to its state. */
private async _createAndSubscribe(sessionResource: URI, modelId?: string, fork?: { session: URI; turnIndex: number }): Promise<URI> {
const rawModelId = this._extractRawModelId(modelId);
const resourceKey = sessionResource.path.substring(1);
const workingDirectory = this._config.resolveWorkingDirectory?.(resourceKey)
const workingDirectory = this._config.resolveWorkingDirectory?.(sessionResource)
?? this._workingDirectoryResolver.resolve(sessionResource)
?? this._workspaceContextService.getWorkspace().folders[0]?.uri;
this._logService.trace(`[AgentHost] Creating new session, model=${rawModelId ?? '(default)'}, provider=${this._config.provider}${fork ? `, fork from ${fork.session.toString()} at index ${fork.turnIndex}` : ''}`);
@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../../base/common/uri.js';
import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js';
import { InstantiationType, registerSingleton } from '../../../../../../platform/instantiation/common/extensions.js';
export const IAgentHostSessionWorkingDirectoryResolver = createDecorator<IAgentHostSessionWorkingDirectoryResolver>('agentHostSessionWorkingDirectoryResolver');
export interface IAgentHostSessionWorkingDirectoryResolver {
readonly _serviceBrand: undefined;
registerResolver(sessionType: string, resolver: (sessionResource: URI) => URI | undefined): IDisposable;
resolve(sessionResource: URI): URI | undefined;
}
class AgentHostSessionWorkingDirectoryResolver implements IAgentHostSessionWorkingDirectoryResolver {
declare readonly _serviceBrand: undefined;
private readonly _resolvers = new Map<string, (sessionResource: URI) => URI | undefined>();
registerResolver(sessionType: string, resolver: (sessionResource: URI) => URI | undefined): IDisposable {
this._resolvers.set(sessionType, resolver);
return toDisposable(() => {
if (this._resolvers.get(sessionType) === resolver) {
this._resolvers.delete(sessionType);
}
});
}
resolve(sessionResource: URI): URI | undefined {
return this._resolvers.get(sessionResource.scheme)?.(sessionResource);
}
}
registerSingleton(IAgentHostSessionWorkingDirectoryResolver, AgentHostSessionWorkingDirectoryResolver, InstantiationType.Delayed);
@@ -47,6 +47,7 @@ import { IStorageService, InMemoryStorageService } from '../../../../../../platf
import { IAgentSubscription } from '../../../../../../platform/agentHost/common/state/agentSubscription.js';
import { ITerminalChatService } from '../../../../terminal/browser/terminal.js';
import { IAgentHostTerminalService } from '../../../../terminal/browser/agentHostTerminalService.js';
import { IAgentHostSessionWorkingDirectoryResolver } from '../../../browser/agentSessions/agentHost/agentHostSessionWorkingDirectoryResolver.js';
// ---- Mock agent host service ------------------------------------------------
@@ -259,7 +260,7 @@ class MockChatAgentService extends mock<IChatAgentService>() {
// ---- Helpers ----------------------------------------------------------------
function createTestServices(disposables: DisposableStore) {
function createTestServices(disposables: DisposableStore, workingDirectoryResolver?: { resolve(sessionResource: URI): URI | undefined }) {
const instantiationService = disposables.add(new TestInstantiationService());
const agentHostService = new MockAgentHostService();
@@ -313,6 +314,10 @@ function createTestServices(disposables: DisposableStore) {
instantiationService.stub(IAgentHostTerminalService, {
reviveTerminal: async () => undefined!,
});
instantiationService.stub(IAgentHostSessionWorkingDirectoryResolver, {
registerResolver: () => toDisposable(() => { }),
resolve: sessionResource => workingDirectoryResolver?.resolve(sessionResource),
});
return { instantiationService, agentHostService, chatAgentService };
}
@@ -1605,6 +1610,30 @@ suite('AgentHostChatContribution', () => {
assert.strictEqual(agentHostService.createSessionCalls[0].workingDirectory?.toString(), URI.file('/custom/working/dir').toString());
}));
test('handler uses registered working directory resolver', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
const resolvedWorkingDirectory = URI.file('/resolved/working/dir');
const { instantiationService, agentHostService } = createTestServices(disposables, {
resolve: () => resolvedWorkingDirectory,
});
const handler = disposables.add(instantiationService.createInstance(AgentHostSessionHandler, {
provider: 'copilot' as const,
agentId: 'workdir-resolver-test',
sessionType: 'workdir-resolver-test',
fullName: 'Test',
description: 'test',
connection: agentHostService,
connectionAuthority: 'local',
}));
const { turnPromise, session, turnId, fire } = await startTurn(handler, agentHostService, disposables);
fire({ type: 'session/turnComplete', session, turnId } as ISessionAction);
await turnPromise;
assert.strictEqual(agentHostService.createSessionCalls.length, 1);
assert.strictEqual(agentHostService.createSessionCalls[0].workingDirectory?.toString(), resolvedWorkingDirectory.toString());
}));
test('handler passes vscode-agent-host URI as-is to createSession', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
const { instantiationService, agentHostService } = createTestServices(disposables);