Skip to content

Commit 1b6a188

Browse files
authored
Agents - fix isolation mode regression (#326973) (#326977)
1 parent e5381e4 commit 1b6a188

3 files changed

Lines changed: 90 additions & 3 deletions

File tree

src/vs/platform/agentHost/node/agentSideEffects.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,15 @@ export class AgentSideEffects extends Disposable {
12941294
if (values) {
12951295
this._persistSessionFlag(channel, 'configValues', JSON.stringify(values));
12961296
}
1297+
if (this._worktree && sessionState?.lifecycle === SessionLifecycle.Creating) {
1298+
const sessionId = AgentSession.id(channel);
1299+
const isolation = values?.[SessionConfigKey.Isolation];
1300+
if (isolation === 'worktree') {
1301+
this._worktree.notePending(sessionId);
1302+
} else if (isolation === 'folder') {
1303+
this._worktree.clearPending(sessionId);
1304+
}
1305+
}
12971306
// This case is reached only for client-dispatched config changes
12981307
// (a user picker edit); internal server-side writes use
12991308
// `dispatchServerAction` and never land here. So the provider can

src/vs/platform/agentHost/node/shared/worktreeIsolation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,8 @@ export class WorktreeIsolation extends Disposable {
245245

246246
/**
247247
* Marks a fresh worktree-isolation session as pending — its worktree is
248-
* deferred to the first send. Called by the host from `createSession` when
249-
* the resolved session config selects `worktree` isolation.
248+
* deferred to the first send. Called by the host while a creating session's
249+
* resolved config selects `worktree` isolation.
250250
*/
251251
notePending(sessionId: string): void {
252252
this._pending.add(sessionId);

src/vs/platform/agentHost/test/node/agentService.test.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { hasKey } from '../../../../base/common/types.js';
2222
import { NullLogService } from '../../../log/common/log.js';
2323
import { FileService } from '../../../files/common/fileService.js';
2424
import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js';
25-
import { AgentSession, GITHUB_COPILOT_PROTECTED_RESOURCE, IRestoredSubagentSession, SubagentChatSignal, type IAgent, type IAgentChatDataChange, type IAgentChats, type IAgentCreateChatForkSource, type IAgentCreateChatOptions, type IAgentCreateChatResult, type IAgentCreateSessionResult, type IAgentLegacyChat, type IAgentSessionMetadata, type IAgentSpawnChatEvent } from '../../common/agentService.js';
25+
import { AgentSession, GITHUB_COPILOT_PROTECTED_RESOURCE, IRestoredSubagentSession, SubagentChatSignal, type IAgent, type IAgentChatDataChange, type IAgentChats, type IAgentCreateChatForkSource, type IAgentCreateChatOptions, type IAgentCreateChatResult, type IAgentCreateSessionConfig, type IAgentCreateSessionResult, type IAgentLegacyChat, type IAgentSessionMetadata, type IAgentSpawnChatEvent } from '../../common/agentService.js';
2626
import { ISessionDatabase, ISessionDataService } from '../../common/sessionDataService.js';
2727
import { SessionConfigKey } from '../../common/sessionConfigKeys.js';
2828
import { SessionDatabase } from '../../node/sessionDatabase.js';
@@ -427,6 +427,84 @@ suite('AgentService (node dispatcher)', () => {
427427
});
428428
});
429429

430+
test('reconciles pending worktree isolation when creating session config changes', async () => {
431+
const gitService = createNoopGitService();
432+
const sessionDataService = createSessionDataService(new TestSessionDatabase());
433+
const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService));
434+
const isolation = disposables.add(new WorktreeIsolation(
435+
{ generateBranchName: async () => 'agents/test' },
436+
gitService,
437+
new TestCopilotApiService(),
438+
sessionDataService,
439+
new NullLogService(),
440+
));
441+
localService.setWorktreeIsolation(isolation);
442+
443+
class ProvisionalAgent extends MockAgent {
444+
override async createSession(config?: IAgentCreateSessionConfig): Promise<IAgentCreateSessionResult> {
445+
return { ...await super.createSession(config), provisional: true };
446+
}
447+
}
448+
449+
const provisionalAgent = new ProvisionalAgent('codex');
450+
const readyAgent = new MockAgent('copilot');
451+
disposables.add(toDisposable(() => provisionalAgent.dispose()));
452+
disposables.add(toDisposable(() => readyAgent.dispose()));
453+
localService.registerProvider(provisionalAgent);
454+
localService.registerProvider(readyAgent);
455+
456+
const creatingSession = await localService.createSession({
457+
provider: 'codex',
458+
workingDirectory: URI.file('/workspace/repo'),
459+
config: { [SessionConfigKey.Isolation]: 'folder' },
460+
});
461+
const readySession = await localService.createSession({
462+
provider: 'copilot',
463+
workingDirectory: URI.file('/workspace/repo'),
464+
config: { [SessionConfigKey.Isolation]: 'folder' },
465+
});
466+
const creatingInitially = localService.configurationService.isWorkingDirectoryPending(creatingSession.toString());
467+
const readyInitially = localService.configurationService.isWorkingDirectoryPending(readySession.toString());
468+
const creatingLifecycle = localService.stateManager.getSessionState(creatingSession.toString())?.lifecycle;
469+
const readyLifecycle = localService.stateManager.getSessionState(readySession.toString())?.lifecycle;
470+
471+
localService.dispatchAction(creatingSession.toString(), {
472+
type: ActionType.SessionConfigChanged,
473+
config: { [SessionConfigKey.Isolation]: 'worktree' },
474+
}, 'test-client', 1);
475+
const creatingAfterWorktree = localService.configurationService.isWorkingDirectoryPending(creatingSession.toString());
476+
477+
localService.dispatchAction(creatingSession.toString(), {
478+
type: ActionType.SessionConfigChanged,
479+
config: { [SessionConfigKey.Isolation]: 'folder' },
480+
}, 'test-client', 2);
481+
const creatingAfterFolder = localService.configurationService.isWorkingDirectoryPending(creatingSession.toString());
482+
483+
localService.dispatchAction(readySession.toString(), {
484+
type: ActionType.SessionConfigChanged,
485+
config: { [SessionConfigKey.Isolation]: 'worktree' },
486+
}, 'test-client', 3);
487+
const readyAfterWorktree = localService.configurationService.isWorkingDirectoryPending(readySession.toString());
488+
489+
assert.deepStrictEqual({
490+
creatingInitially,
491+
readyInitially,
492+
creatingLifecycle,
493+
readyLifecycle,
494+
creatingAfterWorktree,
495+
creatingAfterFolder,
496+
readyAfterWorktree,
497+
}, {
498+
creatingInitially: false,
499+
readyInitially: false,
500+
creatingLifecycle: SessionLifecycle.Creating,
501+
readyLifecycle: SessionLifecycle.Ready,
502+
creatingAfterWorktree: true,
503+
creatingAfterFolder: false,
504+
readyAfterWorktree: false,
505+
});
506+
});
507+
430508
suite('resourceRead', () => {
431509

432510
test('maps missing files to NotFound', async () => {

0 commit comments

Comments
 (0)