Skip to content

Commit 0295ec3

Browse files
authored
Filter Copilot CLI sessions per workspace (#2313)
* Filter Copilot CLI sessions per workspace * Update review comments
1 parent a4ed029 commit 0295ec3

File tree

4 files changed

+88
-9
lines changed

4 files changed

+88
-9
lines changed

src/extension/agents/copilotcli/common/delegationSummaryService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface IChatDelegationSummaryService {
2727

2828
export class ChatDelegationSummaryService implements IChatDelegationSummaryService {
2929
declare _serviceBrand: undefined;
30-
private readonly _mementoUpdator = new Sequencer();
30+
private readonly _mementoUpdater = new Sequencer();
3131
private readonly _summaries = new ResourceMap<string>();
3232
public readonly scheme = SummaryFileScheme;
3333
constructor(
@@ -44,7 +44,7 @@ export class ChatDelegationSummaryService implements IChatDelegationSummaryServi
4444
return;
4545
}
4646
summary = summary.substring(0, 100);
47-
await this._mementoUpdator.queue(async () => {
47+
await this._mementoUpdater.queue(async () => {
4848
const details = this.context.globalState.get<Record<string, { summary: string; createdDateTime: number }>>(DelegationSummaryMementoKey, {});
4949

5050
details[sessionId] = { summary, createdDateTime: Date.now() };

src/extension/agents/copilotcli/node/copilotcliSessionService.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import type { Session, SessionEvent, SessionOptions, SweCustomAgent, internal } from '@github/copilot/sdk';
6+
import type { internal, Session, SessionEvent, SessionOptions, SweCustomAgent } from '@github/copilot/sdk';
77
import type { CancellationToken, ChatRequest, Uri } from 'vscode';
88
import { INativeEnvService } from '../../../../platform/env/common/envService';
9+
import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';
910
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
1011
import { RelativePattern } from '../../../../platform/filesystem/common/fileTypes';
1112
import { ILogService } from '../../../../platform/log/common/logService';
13+
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
1214
import { createServiceIdentifier } from '../../../../util/common/services';
1315
import { coalesce } from '../../../../util/vs/base/common/arrays';
14-
import { disposableTimeout, raceCancellation, raceCancellationError } from '../../../../util/vs/base/common/async';
16+
import { disposableTimeout, raceCancellation, raceCancellationError, Sequencer } from '../../../../util/vs/base/common/async';
1517
import { Emitter, Event } from '../../../../util/vs/base/common/event';
1618
import { Lazy } from '../../../../util/vs/base/common/lazy';
1719
import { Disposable, DisposableMap, IDisposable, IReference, RefCountedDisposable, toDisposable } from '../../../../util/vs/base/common/lifecycle';
@@ -24,6 +26,8 @@ import { CopilotCLISession, ICopilotCLISession } from './copilotcliSession';
2426
import { getCopilotLogger } from './logger';
2527
import { ICopilotCLIMCPHandler } from './mcpHandler';
2628

29+
const COPILOT_CLI_WORKSPACE_SPECIFIC_SESSIONS_KEY = 'github.copilot.cli.workspaceSpecificSessions';
30+
2731
export interface ICopilotCLISessionItem {
2832
readonly id: string;
2933
readonly label: string;
@@ -67,6 +71,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
6771

6872
private sessionMutexForGetSession = new Map<string, Mutex>();
6973

74+
private readonly _mementoUpdator = new Sequencer();
7075
constructor(
7176
@ILogService protected readonly logService: ILogService,
7277
@ICopilotCLISDK private readonly copilotCLISDK: ICopilotCLISDK,
@@ -75,6 +80,8 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
7580
@IFileSystemService private readonly fileSystem: IFileSystemService,
7681
@ICopilotCLIMCPHandler private readonly mcpHandler: ICopilotCLIMCPHandler,
7782
@ICopilotCLIAgents private readonly agents: ICopilotCLIAgents,
83+
@IVSCodeExtensionContext private readonly context: IVSCodeExtensionContext,
84+
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
7885
) {
7986
super();
8087
this.monitorSessionFiles();
@@ -111,12 +118,18 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
111118

112119
async _getAllSessions(token: CancellationToken): Promise<readonly ICopilotCLISessionItem[]> {
113120
try {
121+
const mementoUpdateCompleted = this._mementoUpdator.queue(async () => Promise.resolve());
114122
const sessionManager = await raceCancellationError(this.getSessionManager(), token);
115123
const sessionMetadataList = await raceCancellationError(sessionManager.listSessions(), token);
116124

125+
// Wait for any pending memento updates to complete before filtering sessions.
126+
await mementoUpdateCompleted;
117127
// Convert SessionMetadata to ICopilotCLISession
118128
const diskSessions: ICopilotCLISessionItem[] = coalesce(await Promise.all(
119129
sessionMetadataList.map(async (metadata) => {
130+
if (this.shouldExcludeSession(metadata.sessionId)) {
131+
return;
132+
}
120133
const id = metadata.sessionId;
121134
const startTime = metadata.startTime.getTime();
122135
const endTime = metadata.modifiedTime.getTime();
@@ -195,7 +208,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
195208
const sessionManager = await raceCancellationError(this.getSessionManager(), token);
196209
const sdkSession = await sessionManager.createSession(options.toSessionOptions());
197210
this.logService.trace(`[CopilotCLISession] Created new CopilotCLI session ${sdkSession.sessionId}.`);
198-
211+
void this.updateSessionInWorkspace(sdkSession.sessionId, 'add');
199212

200213
return this.createCopilotSession(sdkSession, options, sessionManager);
201214
}
@@ -280,6 +293,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
280293
}
281294

282295
public async deleteSession(sessionId: string): Promise<void> {
296+
void this.updateSessionInWorkspace(sessionId, 'delete');
283297
try {
284298
{
285299
const session = this._sessionWrappers.get(sessionId);
@@ -301,6 +315,37 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
301315
this._onDidChangeSessions.fire();
302316
}
303317
}
318+
319+
private async updateSessionInWorkspace(sessionId: string, operation: 'add' | 'delete'): Promise<void> {
320+
// If we're not in a workspace, do not track sessions as these are global sessions.
321+
if (this.workspaceService.getWorkspaceFolders().length === 0) {
322+
return;
323+
}
324+
this._mementoUpdator.queue(async () => {
325+
let trackedSessions = this.context.workspaceState.get<Record<string, { createdDateTime: number }>>(COPILOT_CLI_WORKSPACE_SPECIFIC_SESSIONS_KEY, {});
326+
if (operation === 'add') {
327+
trackedSessions[sessionId] = { createdDateTime: Date.now() };
328+
} else {
329+
delete trackedSessions[sessionId];
330+
}
331+
332+
// If we have 100 entries or more, sort by created time and keep the recent 100 and drop the rest.
333+
if (Object.keys(trackedSessions).length >= 100) {
334+
const sortedSessions = Object.entries(trackedSessions).sort((a, b) => b[1].createdDateTime - a[1].createdDateTime);
335+
trackedSessions = Object.fromEntries(sortedSessions.slice(0, 100));
336+
}
337+
await this.context.workspaceState.update(COPILOT_CLI_WORKSPACE_SPECIFIC_SESSIONS_KEY, trackedSessions);
338+
});
339+
}
340+
341+
private shouldExcludeSession(sessionId: string): boolean {
342+
if (this.workspaceService.getWorkspaceFolders().length === 0) {
343+
return false;
344+
}
345+
const trackedSessions = this.context.workspaceState.get<Record<string, { createdDateTime: number }>>(COPILOT_CLI_WORKSPACE_SPECIFIC_SESSIONS_KEY, {});
346+
return !(sessionId in trackedSessions);
347+
}
348+
304349
}
305350

306351
function labelFromPrompt(prompt: string): string {

src/extension/agents/copilotcli/node/test/copilotCliSessionService.spec.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55

66
import type { SessionOptions, SweCustomAgent } from '@github/copilot/sdk';
77
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8-
import type { ChatContext } from 'vscode';
8+
import type { ChatContext, Memento } from 'vscode';
99
import { CancellationToken } from 'vscode-languageserver-protocol';
1010
import { IAuthenticationService } from '../../../../../platform/authentication/common/authentication';
1111
import { IConfigurationService } from '../../../../../platform/configuration/common/configurationService';
1212
import { NullNativeEnvService } from '../../../../../platform/env/common/nullEnvService';
13+
import { IVSCodeExtensionContext } from '../../../../../platform/extContext/common/extensionContext';
1314
import { MockFileSystemService } from '../../../../../platform/filesystem/node/test/mockFileSystemService';
1415
import { IGitService } from '../../../../../platform/git/common/gitService';
1516
import { ILogService } from '../../../../../platform/log/common/logService';
@@ -120,9 +121,25 @@ describe('CopilotCLISessionService', () => {
120121
return disposables.add(new CopilotCLISession(options, sdkSession, gitService, logService, workspaceService, sdk, instantiationService, delegationService));
121122
}
122123
} as unknown as IInstantiationService;
123-
124+
const state: Record<string, unknown> = {};
125+
const workspaceState: Memento = {
126+
keys: () => Object.keys(state),
127+
get: <T>(key: string, defaultValue?: T): T => {
128+
if (key in state) {
129+
return state[key] as T;
130+
}
131+
state[key] = defaultValue;
132+
return defaultValue as T;
133+
},
134+
update: async (key: string, value: unknown) => {
135+
state[key] = value;
136+
}
137+
};
138+
const context: IVSCodeExtensionContext = new class extends mock<IVSCodeExtensionContext>() {
139+
override workspaceState: Memento = workspaceState;
140+
}();
124141
const configurationService = accessor.get(IConfigurationService);
125-
service = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), new MockFileSystemService(), new CopilotCLIMCPHandler(logService, new TestWorkspaceService(), authService, configurationService), cliAgents));
142+
service = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), new MockFileSystemService(), new CopilotCLIMCPHandler(logService, new TestWorkspaceService(), authService, configurationService), cliAgents, context, new TestWorkspaceService()));
126143
manager = await service.getSessionManager() as unknown as MockCliSdkSessionManager;
127144
});
128145

src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,24 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => {
177177
return disposables.add(session);
178178
}
179179
} as unknown as IInstantiationService;
180-
sessionService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), new MockFileSystemService(), mcpHandler, new NullCopilotCLIAgents()));
180+
const state: Record<string, unknown> = {};
181+
const workspaceState: vscode.Memento = {
182+
keys: () => Object.keys(state),
183+
get: <T>(key: string, defaultValue?: T): T => {
184+
if (key in state) {
185+
return state[key] as T;
186+
}
187+
state[key] = defaultValue;
188+
return defaultValue as T;
189+
},
190+
update: async (key: string, value: unknown) => {
191+
state[key] = value;
192+
}
193+
};
194+
const context: IVSCodeExtensionContext = new class extends mock<IVSCodeExtensionContext>() {
195+
override workspaceState: vscode.Memento = workspaceState;
196+
}();
197+
sessionService = disposables.add(new CopilotCLISessionService(logService, sdk, instantiationService, new NullNativeEnvService(), new MockFileSystemService(), mcpHandler, new NullCopilotCLIAgents(), context, workspaceService));
181198

182199
manager = await sessionService.getSessionManager() as unknown as MockCliSdkSessionManager;
183200

0 commit comments

Comments
 (0)