Skip to content

Commit 180255f

Browse files
authored
Support monitoring agent files for changes (#2837)
1 parent bd7b518 commit 180255f

File tree

3 files changed

+59
-14
lines changed

3 files changed

+59
-14
lines changed

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

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@
55

66
import type { SessionOptions, SweCustomAgent } from '@github/copilot/sdk';
77
import type { Uri } from 'vscode';
8+
import { RelativePattern } from '../../../../platform/filesystem/common/fileTypes';
89
import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';
910
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
1011
import { IEnvService } from '../../../../platform/env/common/envService';
1112
import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';
13+
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
1214
import { ILogService } from '../../../../platform/log/common/logService';
1315
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
1416
import { createServiceIdentifier } from '../../../../util/common/services';
17+
import { Delayer } from '../../../../util/vs/base/common/async';
18+
import { Emitter, Event } from '../../../../util/vs/base/common/event';
1519
import { Lazy } from '../../../../util/vs/base/common/lazy';
16-
import { IDisposable, toDisposable } from '../../../../util/vs/base/common/lifecycle';
20+
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../util/vs/base/common/lifecycle';
1721
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
1822
import { getCopilotLogger } from './logger';
1923
import { ensureNodePtyShim } from './nodePtyShim';
@@ -154,6 +158,7 @@ export class CopilotCLIModels implements ICopilotCLIModels {
154158

155159
export interface ICopilotCLIAgents {
156160
readonly _serviceBrand: undefined;
161+
readonly onDidChangeAgents: Event<void>;
157162
getDefaultAgent(): Promise<string>;
158163
resolveAgent(agentId: string): Promise<SweCustomAgent | undefined>;
159164
setDefaultAgent(agent: string | undefined): Promise<void>;
@@ -164,16 +169,51 @@ export interface ICopilotCLIAgents {
164169

165170
export const ICopilotCLIAgents = createServiceIdentifier<ICopilotCLIAgents>('ICopilotCLIAgents');
166171

167-
export class CopilotCLIAgents implements ICopilotCLIAgents {
172+
export class CopilotCLIAgents extends Disposable implements ICopilotCLIAgents {
168173
declare _serviceBrand: undefined;
169174
private sessionAgents: Record<string, { agentId?: string; createdDateTime: number }> = {};
170-
private _agents?: Readonly<SweCustomAgent>[];
175+
private _agentsPromise?: Promise<Readonly<SweCustomAgent>[]>;
176+
private readonly _onDidChangeAgents = this._register(new Emitter<void>());
177+
readonly onDidChangeAgents: Event<void> = this._onDidChangeAgents.event;
178+
private readonly _fileWatchers = this._register(new DisposableStore());
171179
constructor(
172180
@ICopilotCLISDK private readonly copilotCLISDK: ICopilotCLISDK,
173181
@IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext,
174182
@ILogService private readonly logService: ILogService,
175183
@IConfigurationService private readonly configurationService: IConfigurationService,
176-
) { }
184+
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
185+
@IFileSystemService private readonly fileSystemService: IFileSystemService,
186+
) {
187+
super();
188+
this.setupFileWatchers();
189+
void this.getAgents();
190+
this._register(this.workspaceService.onDidChangeWorkspaceFolders(() => {
191+
this.setupFileWatchers();
192+
this._refreshAgents();
193+
}));
194+
}
195+
196+
protected setupFileWatchers(): void {
197+
this._fileWatchers.clear();
198+
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
199+
const refresher = this._fileWatchers.add(new Delayer(500));
200+
for (const folder of workspaceFolders) {
201+
const pattern = new RelativePattern(folder, '.github/agents/*.agent.md');
202+
const watcher = this._fileWatchers.add(this.fileSystemService.createFileSystemWatcher(pattern));
203+
this._fileWatchers.add(watcher.onDidCreate(() => refresher.trigger(() => this._refreshAgents())));
204+
this._fileWatchers.add(watcher.onDidChange(() => refresher.trigger(() => this._refreshAgents())));
205+
this._fileWatchers.add(watcher.onDidDelete(() => refresher.trigger(() => this._refreshAgents())));
206+
}
207+
}
208+
209+
private _refreshAgents(): void {
210+
this._agentsPromise = undefined;
211+
this.getAgents().catch((error) => {
212+
this.logService.error('[CopilotCLIAgents] Failed to refresh agents', error);
213+
});
214+
this._onDidChangeAgents.fire();
215+
}
216+
177217
async trackSessionAgent(sessionId: string, agent: string | undefined): Promise<void> {
178218
const details = Object.keys(this.sessionAgents).length ? this.sessionAgents : this.extensionContext.workspaceState.get<Record<string, { agentId?: string; createdDateTime: number }>>(COPILOT_CLI_SESSION_AGENTS_MEMENTO_KEY, this.sessionAgents);
179219

@@ -227,17 +267,16 @@ export class CopilotCLIAgents implements ICopilotCLIAgents {
227267
}
228268

229269
async getAgents(): Promise<Readonly<SweCustomAgent>[]> {
230-
// Fetching agents from the SDK can be slow, cache the result while allowing background refreshes.
231-
const agents = this._agents;
232-
const promise = this.getAgentsImpl();
233-
234-
promise.then(fetchedAgents => {
235-
this._agents = fetchedAgents;
236-
}).catch((error) => {
237-
this.logService.error('[CopilotCLISession] Failed to fetch custom agents', error);
238-
});
270+
// Cache the promise to avoid concurrent fetches
271+
if (!this._agentsPromise) {
272+
this._agentsPromise = this.getAgentsImpl().catch((error) => {
273+
this.logService.error('[CopilotCLIAgents] Failed to fetch custom agents', error);
274+
this._agentsPromise = undefined;
275+
return [];
276+
});
277+
}
239278

240-
return agents ?? promise;
279+
return this._agentsPromise;
241280
}
242281

243282
async getAgentsImpl(): Promise<Readonly<SweCustomAgent>[]> {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { NullRequestLogger } from '../../../../../platform/requestLogger/node/nu
1616
import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService';
1717
import { NullWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';
1818
import { mock } from '../../../../../util/common/test/simpleMock';
19+
import { Event } from '../../../../../util/vs/base/common/event';
1920
import { DisposableStore, IReference, toDisposable } from '../../../../../util/vs/base/common/lifecycle';
2021
import { URI } from '../../../../../util/vs/base/common/uri';
2122
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
@@ -66,6 +67,7 @@ export class MockCliSdkSessionManager {
6667

6768
export class NullCopilotCLIAgents implements ICopilotCLIAgents {
6869
_serviceBrand: undefined;
70+
readonly onDidChangeAgents: Event<void> = Event.None;
6971
async getAgents(): Promise<SweCustomAgent[]> {
7072
return [];
7173
}

src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,10 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
242242
this._onDidChangeChatSessionProviderOptions.fire();
243243
}
244244
}));
245+
246+
this._register(this.copilotCLIAgents.onDidChangeAgents(() => {
247+
this._onDidChangeChatSessionProviderOptions.fire();
248+
}));
245249
}
246250

247251
public notifySessionOptionsChange(resource: vscode.Uri, updates: ReadonlyArray<{ optionId: string; value: string | vscode.ChatSessionProviderOptionItem }>): void {

0 commit comments

Comments
 (0)