Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.

Commit 92cc0a3

Browse files
authored
feat: wire plugins through CopilotCLI customization provider (#4962)
* feat: wire plugins through CopilotCLI customization provider Consume the new chat.plugins API and plumb it through to the CopilotCLI customization provider: - Mirror updated chatPromptFiles d.ts with ChatPluginResource - Add plugins/onDidChangePlugins to IChatPromptFileService interface - Wire vscode.chat.plugins in ChatPromptFileService implementation - Add Plugins to CopilotCLI provider supportedTypes (unhides section) - Add getPluginItems() to CopilotCLI provider - Subscribe to onDidChangePlugins for provider invalidation - Fix all test mocks for new interface member - Add plugin tests (type, name, combined items, onDidChange) Depends on: microsoft/vscode josh/plugins-chatpromptfiles-api * chore: update vscodeCommit pointer via vscode-dts:update
1 parent 3c6aa87 commit 92cc0a3

9 files changed

Lines changed: 89 additions & 5 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6409,5 +6409,5 @@
64096409
"node-gyp": "npm:node-gyp@10.3.1",
64106410
"zod": "3.25.76"
64116411
},
6412-
"vscodeCommit": "d0c21ae98319ccc4f796b6557b945faa9b72a3af"
6412+
"vscodeCommit": "20b24833bc088c84b778ee89be4c17f15660441a"
64136413
}

src/extension/chatSessions/common/chatPromptFileService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,14 @@ export interface IChatPromptFileService extends IDisposable {
6262
* (workspace, user, and extension-provided).
6363
*/
6464
readonly hooks: readonly ChatResource[];
65+
66+
/**
67+
* An event that fires when the list of {@link plugins plugins} changes.
68+
*/
69+
readonly onDidChangePlugins: Event<void>;
70+
71+
/**
72+
* The list of currently installed agent plugins.
73+
*/
74+
readonly plugins: readonly ChatResource[];
6575
}

src/extension/chatSessions/copilotcli/node/test/copilotCLISkills.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ class TestChatPromptFileService extends Disposable implements IChatPromptFileSer
3535
readonly onDidChangeInstructions: Event<void> = Event.None;
3636
readonly onDidChangeSkills: Event<void> = Event.None;
3737
readonly onDidChangeHooks: Event<void> = Event.None;
38+
readonly onDidChangePlugins: Event<void> = Event.None;
3839
readonly customAgents: readonly ChatResource[] = [];
3940
readonly customAgentPromptFiles: readonly ParsedPromptFile[] = [];
4041
readonly instructions: readonly ChatResource[] = [];
4142
skills: readonly ChatResource[] = [];
4243
readonly hooks: readonly ChatResource[] = [];
44+
readonly plugins: readonly ChatResource[] = [];
4345
}
4446

4547
function createWorkspaceService(folders: URI[] = [URI.file('/workspace')]): IWorkspaceService {

src/extension/chatSessions/copilotcli/node/test/copilotCliAgents.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ class TestChatPromptFileService extends Disposable implements IChatPromptFileSer
5050
readonly onDidChangeInstructions: Event<void> = Event.None;
5151
readonly onDidChangeSkills: Event<void> = Event.None;
5252
readonly onDidChangeHooks: Event<void> = Event.None;
53+
readonly onDidChangePlugins: Event<void> = Event.None;
5354
readonly customAgents: readonly import('vscode').ChatResource[] = [];
5455
readonly instructions: readonly import('vscode').ChatResource[] = [];
5556
readonly skills: readonly import('vscode').ChatResource[] = [];
5657
readonly hooks: readonly import('vscode').ChatResource[] = [];
58+
readonly plugins: readonly import('vscode').ChatResource[] = [];
5759

5860
constructor(private _customAgentPromptFiles: ParsedPromptFile[] = []) {
5961
super();

src/extension/chatSessions/copilotcli/vscode-node/test/testHelpers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,12 @@ export class MockChatPromptFileService extends Disposable implements IChatPrompt
238238
instructions: ChatResource[] = [];
239239
skills: ChatResource[] = [];
240240
hooks: ChatResource[] = [];
241+
plugins: ChatResource[] = [];
241242
private readonly _onDidChangeCustomAgents = this._register(new Emitter<void>());
242243
private readonly _onDidChangeInstructions = this._register(new Emitter<void>());
243244
private readonly _onDidChangeSkills = this._register(new Emitter<void>());
244245
private readonly _onDidChangeHooks = this._register(new Emitter<void>());
246+
private readonly _onDidChangePlugins = this._register(new Emitter<void>());
245247

246248
get onDidChangeCustomAgents() {
247249
return this._onDidChangeCustomAgents.event;
@@ -258,6 +260,10 @@ export class MockChatPromptFileService extends Disposable implements IChatPrompt
258260
get onDidChangeHooks() {
259261
return this._onDidChangeHooks.event;
260262
}
263+
264+
get onDidChangePlugins() {
265+
return this._onDidChangePlugins.event;
266+
}
261267
get customAgentPromptFiles() {
262268
return [];
263269
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export class ChatPromptFileService extends Disposable implements IChatPromptFile
2424
readonly onDidChangeSkills: Event<void> = this._onDidChangeSkills.event;
2525
private readonly _onDidChangeHooks = this._register(new Emitter<void>());
2626
readonly onDidChangeHooks: Event<void> = this._onDidChangeHooks.event;
27+
private readonly _onDidChangePlugins = this._register(new Emitter<void>());
28+
readonly onDidChangePlugins: Event<void> = this._onDidChangePlugins.event;
2729

2830
private _customAgents: ParsedPromptFile[] = [];
2931
private refreshCts: CancellationTokenSource | undefined;
@@ -49,6 +51,10 @@ export class ChatPromptFileService extends Disposable implements IChatPromptFile
4951
this._register(vscode.chat.onDidChangeHooks(() => {
5052
this._onDidChangeHooks.fire();
5153
}));
54+
55+
this._register(vscode.chat.onDidChangePlugins(() => {
56+
this._onDidChangePlugins.fire();
57+
}));
5258
this.triggerRefreshCustomAgents();
5359
}
5460

@@ -72,6 +78,10 @@ export class ChatPromptFileService extends Disposable implements IChatPromptFile
7278
return vscode.chat.hooks;
7379
}
7480

81+
get plugins(): readonly vscode.ChatResource[] {
82+
return vscode.chat.plugins;
83+
}
84+
7585
override dispose(): void {
7686
this.refreshCts?.dispose(true);
7787
this.refreshCts = undefined;

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod
2626
vscode.ChatSessionCustomizationType.Skill,
2727
vscode.ChatSessionCustomizationType.Instructions,
2828
vscode.ChatSessionCustomizationType.Hook,
29+
vscode.ChatSessionCustomizationType.Plugins,
2930
],
3031
};
3132
}
@@ -41,6 +42,7 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod
4142
this._register(this.chatPromptFileService.onDidChangeInstructions(() => this._onDidChange.fire()));
4243
this._register(this.chatPromptFileService.onDidChangeSkills(() => this._onDidChange.fire()));
4344
this._register(this.chatPromptFileService.onDidChangeHooks(() => this._onDidChange.fire()));
45+
this._register(this.chatPromptFileService.onDidChangePlugins(() => this._onDidChange.fire()));
4446
this._register(this.copilotCLIAgents.onDidChangeAgents(() => this._onDidChange.fire()));
4547
}
4648

@@ -49,13 +51,16 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod
4951
const instructions = this.getInstructionItems();
5052
const skills = this.getSkillItems();
5153
const hooks = this.getHookItems();
54+
const plugins = this.getPluginItems();
5255

5356
this.logService.debug(`[CopilotCLICustomizationProvider] agents (${agents.length}): ${agents.map(a => a.name).join(', ') || '(none)'}`);
5457
this.logService.debug(`[CopilotCLICustomizationProvider] instructions (${instructions.length}): ${instructions.map(i => i.name).join(', ') || '(none)'}`);
5558
this.logService.debug(`[CopilotCLICustomizationProvider] skills (${skills.length}): ${skills.map(s => s.name).join(', ') || '(none)'}`);
5659
this.logService.debug(`[CopilotCLICustomizationProvider] hooks (${hooks.length}): ${hooks.map(h => h.name).join(', ') || '(none)'}`);
5760

58-
const items = [...agents, ...instructions, ...skills, ...hooks];
61+
this.logService.debug(`[CopilotCLICustomizationProvider] plugins (${plugins.length}): ${plugins.map(p => p.name).join(', ') || '(none)'}`);
62+
63+
const items = [...agents, ...instructions, ...skills, ...hooks, ...plugins];
5964
this.logService.debug(`[CopilotCLICustomizationProvider] total: ${items.length} items`);
6065
return items;
6166
}
@@ -107,6 +112,16 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod
107112
name: basename(h.uri).replace(/\.json$/i, ''),
108113
}));
109114
}
115+
116+
/** * Collects all plugin items from the prompt file service.
117+
*/
118+
private getPluginItems(): vscode.ChatSessionCustomizationItem[] {
119+
return this.chatPromptFileService.plugins.map(p => ({
120+
uri: p.uri,
121+
type: vscode.ChatSessionCustomizationType.Plugins,
122+
name: basename(p.uri),
123+
}));
124+
}
110125
}
111126

112127
function deriveNameFromUri(uri: vscode.Uri, extensionOrFilename: string): string {

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

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class FakeChatSessionCustomizationType {
2121
static readonly Instructions = new FakeChatSessionCustomizationType('instructions');
2222
static readonly Prompt = new FakeChatSessionCustomizationType('prompt');
2323
static readonly Hook = new FakeChatSessionCustomizationType('hook');
24+
static readonly Plugins = new FakeChatSessionCustomizationType('plugins');
2425
constructor(readonly id: string) { }
2526
}
2627

@@ -60,32 +61,39 @@ class MockChatPromptFileService extends mock<IChatPromptFileService>() {
6061
override readonly onDidChangeSkills = this._onDidChangeSkills.event;
6162
private readonly _onDidChangeHooks = new Emitter<void>();
6263
override readonly onDidChangeHooks = this._onDidChangeHooks.event;
64+
private readonly _onDidChangePlugins = new Emitter<void>();
65+
override readonly onDidChangePlugins = this._onDidChangePlugins.event;
6366

6467
private _customAgents: vscode.ChatResource[] = [];
6568
private _instructions: vscode.ChatResource[] = [];
6669
private _skills: vscode.ChatResource[] = [];
6770
private _hooks: vscode.ChatResource[] = [];
71+
private _plugins: vscode.ChatResource[] = [];
6872

6973
override get customAgents(): readonly vscode.ChatResource[] { return this._customAgents; }
7074
override get instructions(): readonly vscode.ChatResource[] { return this._instructions; }
7175
override get skills(): readonly vscode.ChatResource[] { return this._skills; }
7276
override get hooks(): readonly vscode.ChatResource[] { return this._hooks; }
77+
override get plugins(): readonly vscode.ChatResource[] { return this._plugins; }
7378

7479
setCustomAgents(agents: vscode.ChatResource[]) { this._customAgents = agents; }
7580
setInstructions(instructions: vscode.ChatResource[]) { this._instructions = instructions; }
7681
setSkills(skills: vscode.ChatResource[]) { this._skills = skills; }
7782
setHooks(hooks: vscode.ChatResource[]) { this._hooks = hooks; }
83+
setPlugins(plugins: vscode.ChatResource[]) { this._plugins = plugins; }
7884

7985
fireCustomAgentsChanged() { this._onDidChangeCustomAgents.fire(); }
8086
fireInstructionsChanged() { this._onDidChangeInstructions.fire(); }
8187
fireSkillsChanged() { this._onDidChangeSkills.fire(); }
8288
fireHooksChanged() { this._onDidChangeHooks.fire(); }
89+
firePluginsChanged() { this._onDidChangePlugins.fire(); }
8390

8491
override dispose() {
8592
this._onDidChangeCustomAgents.dispose();
8693
this._onDidChangeInstructions.dispose();
8794
this._onDidChangeSkills.dispose();
8895
this._onDidChangeHooks.dispose();
96+
this._onDidChangePlugins.dispose();
8997
}
9098
}
9199

@@ -137,14 +145,15 @@ describe('CopilotCLICustomizationProvider', () => {
137145
expect(CopilotCLICustomizationProvider.metadata.iconId).toBe('worktree');
138146
});
139147

140-
it('supports Agent, Skill, Instructions, and Hook types', () => {
148+
it('supports Agent, Skill, Instructions, Hook, and Plugins types', () => {
141149
const supported = CopilotCLICustomizationProvider.metadata.supportedTypes;
142150
expect(supported).toBeDefined();
143-
expect(supported).toHaveLength(4);
151+
expect(supported).toHaveLength(5);
144152
expect(supported).toContain(FakeChatSessionCustomizationType.Agent);
145153
expect(supported).toContain(FakeChatSessionCustomizationType.Skill);
146154
expect(supported).toContain(FakeChatSessionCustomizationType.Instructions);
147155
expect(supported).toContain(FakeChatSessionCustomizationType.Hook);
156+
expect(supported).toContain(FakeChatSessionCustomizationType.Plugins);
148157
});
149158

150159
it('only returns items whose type is in supportedTypes', async () => {
@@ -249,9 +258,10 @@ describe('CopilotCLICustomizationProvider', () => {
249258
mockPromptFileService.setInstructions([{ uri: URI.file('/workspace/.github/b.instructions.md') }]);
250259
mockPromptFileService.setSkills([{ uri: URI.file('/workspace/.github/skills/c/SKILL.md') }]);
251260
mockPromptFileService.setHooks([{ uri: URI.file('/workspace/.copilot/hooks/pre-commit.json') }]);
261+
mockPromptFileService.setPlugins([{ uri: URI.file('/workspace/.copilot/plugins/my-plugin') }]);
252262

253263
const items = await provider.provideChatSessionCustomizations(undefined!);
254-
expect(items).toHaveLength(4);
264+
expect(items).toHaveLength(5);
255265
});
256266

257267
it('returns hooks with correct type and name', async () => {
@@ -282,6 +292,17 @@ describe('CopilotCLICustomizationProvider', () => {
282292
const hookItems = items.filter((i: vscode.ChatSessionCustomizationItem) => i.type === FakeChatSessionCustomizationType.Hook);
283293
expect(hookItems).toHaveLength(2);
284294
});
295+
296+
it('returns plugins with correct type and name derived from URI', async () => {
297+
const uri = URI.file('/workspace/.copilot/plugins/lint-rules');
298+
mockPromptFileService.setPlugins([{ uri }]);
299+
300+
const items = await provider.provideChatSessionCustomizations(undefined!);
301+
expect(items).toHaveLength(1);
302+
expect(items[0].uri).toEqual(uri);
303+
expect(items[0].type).toBe(FakeChatSessionCustomizationType.Plugins);
304+
expect(items[0].name).toBe('lint-rules');
305+
});
285306
});
286307

287308
describe('onDidChange', () => {
@@ -317,6 +338,14 @@ describe('CopilotCLICustomizationProvider', () => {
317338
expect(fired).toBe(true);
318339
});
319340

341+
it('fires when plugins change', () => {
342+
let fired = false;
343+
disposables.add(provider.onDidChange(() => { fired = true; }));
344+
345+
mockPromptFileService.firePluginsChanged();
346+
expect(fired).toBe(true);
347+
});
348+
320349
it('fires when ICopilotCLIAgents agents change', () => {
321350
let fired = false;
322351
disposables.add(provider.onDidChange(() => { fired = true; }));

src/extension/vscode.proposed.chatPromptFiles.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,16 @@ declare module 'vscode' {
170170
*/
171171
export const hooks: readonly ChatResource[];
172172

173+
/**
174+
* An event that fires when the list of {@link plugins plugins} changes.
175+
*/
176+
export const onDidChangePlugins: Event<void>;
177+
178+
/**
179+
* The list of currently installed agent plugins.
180+
*/
181+
export const plugins: readonly ChatResource[];
182+
173183
/**
174184
* Register a provider for custom agents.
175185
* @param provider The custom agent provider.

0 commit comments

Comments
 (0)