Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as l10n from '@vscode/l10n';
7
import * as vscode from 'vscode';
8
import { ICustomInstructionsService } from '../../../../platform/customInstructions/common/customInstructionsService';
9
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
10
import { ILogService } from '../../../../platform/log/common/logService';
11
import { IPromptsService } from '../../../../platform/promptFiles/common/promptsService';
12
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
13
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
14
import { isCancellationError } from '../../../../util/vs/base/common/errors';
15
import { Emitter } from '../../../../util/vs/base/common/event';
16
import { Disposable } from '../../../../util/vs/base/common/lifecycle';
17
import { basename } from '../../../../util/vs/base/common/resources';
18
import { URI } from '../../../../util/vs/base/common/uri';
19
import { ICopilotCLIAgents, isEnabledForCopilotCLI } from '../../copilotcli/node/copilotCli';
20
21
export class CopilotCLICustomizationProvider extends Disposable implements vscode.ChatSessionCustomizationProvider {
22
23
private readonly _onDidChange = this._register(new Emitter<void>());
24
readonly onDidChange = this._onDidChange.event;
25
26
static get metadata(): vscode.ChatSessionCustomizationProviderMetadata {
27
return {
28
label: 'Copilot CLI',
29
iconId: 'copilot',
30
supportedTypes: [
31
vscode.ChatSessionCustomizationType.Agent,
32
vscode.ChatSessionCustomizationType.Skill,
33
vscode.ChatSessionCustomizationType.Instructions,
34
vscode.ChatSessionCustomizationType.Hook,
35
vscode.ChatSessionCustomizationType.Plugins,
36
].filter((t): t is vscode.ChatSessionCustomizationType => t !== undefined),
37
};
38
}
39
40
constructor(
41
@ICopilotCLIAgents private readonly copilotCLIAgents: ICopilotCLIAgents,
42
@ICustomInstructionsService private readonly customInstructionsService: ICustomInstructionsService,
43
@IPromptsService private readonly promptsService: IPromptsService,
44
@ILogService private readonly logService: ILogService,
45
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
46
@IFileSystemService private readonly fileSystemService: IFileSystemService,
47
) {
48
super();
49
50
this._register(this.promptsService.onDidChangeCustomAgents(() => this._onDidChange.fire()));
51
this._register(this.promptsService.onDidChangeInstructions(() => this._onDidChange.fire()));
52
this._register(this.promptsService.onDidChangeSkills(() => this._onDidChange.fire()));
53
this._register(this.promptsService.onDidChangeHooks(() => this._onDidChange.fire()));
54
this._register(this.promptsService.onDidChangePlugins(() => this._onDidChange.fire()));
55
this._register(this.copilotCLIAgents.onDidChangeAgents(() => this._onDidChange.fire()));
56
}
57
58
async provideChatSessionCustomizations(token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {
59
const [agents, instructions, skills, hooks, plugins] = await Promise.all([
60
this.getAgentItems(token),
61
this.getInstructionItems(token),
62
this.getSkillItems(token),
63
this.getHookItems(token),
64
this.getPluginItems(token),
65
].map(p => p.catch(err => {
66
if (isCancellationError(err) || token.isCancellationRequested) {
67
throw err;
68
}
69
this.logService.error(`[CopilotCLICustomizationProvider] failed to get customizations: ${err}`);
70
return [];
71
})));
72
73
this.logService.debug(`[CopilotCLICustomizationProvider] agents (${agents.length}): ${agents.map(a => a.name).join(', ') || '(none)'}`);
74
this.logService.debug(`[CopilotCLICustomizationProvider] instructions (${instructions.length}): ${instructions.map(i => i.name).join(', ') || '(none)'}`);
75
this.logService.debug(`[CopilotCLICustomizationProvider] skills (${skills.length}): ${skills.map(s => s.name).join(', ') || '(none)'}`);
76
this.logService.debug(`[CopilotCLICustomizationProvider] hooks (${hooks.length}): ${hooks.map(h => h.name).join(', ') || '(none)'}`);
77
78
this.logService.debug(`[CopilotCLICustomizationProvider] plugins (${plugins.length}): ${plugins.map(p => p.name).join(', ') || '(none)'}`);
79
80
const items = [...agents, ...instructions, ...skills, ...hooks, ...plugins];
81
this.logService.debug(`[CopilotCLICustomizationProvider] total: ${items.length} items`);
82
return items;
83
}
84
85
/**
86
* Builds agent items from ICopilotCLIAgents, which already merges SDK
87
* and prompt-file agents with source URIs.
88
*/
89
private async getAgentItems(_token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {
90
const agentInfos = await this.copilotCLIAgents.getAgents();
91
return agentInfos.map(({ agent, sourceUri, pluginUri, extensionId }) => ({
92
uri: sourceUri,
93
type: vscode.ChatSessionCustomizationType.Agent,
94
name: agent.displayName || agent.name,
95
description: agent.description,
96
extensionId,
97
pluginUri
98
}));
99
}
100
101
/**
102
* Collects all instruction items from the prompt file service,
103
* categorizing them with groupKeys and badges matching the core
104
* implementation:
105
* - agent-instructions: AGENTS.md, CLAUDE.md, copilot-instructions.md
106
* - context-instructions: files with an applyTo pattern (badge = pattern)
107
* - on-demand-instructions: files without an applyTo pattern
108
*/
109
private async getInstructionItems(token: CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {
110
// Collect agent instruction URIs from customInstructionsService
111
// (copilot-instructions.md) plus workspace-root AGENTS.md and CLAUDE.md
112
const agentInstructionUriList = await this.customInstructionsService.getAgentInstructions();
113
const rootFileNames = ['AGENTS.md', 'CLAUDE.md'];
114
for (const folder of this.workspaceService.getWorkspaceFolders()) {
115
for (const fileName of rootFileNames) {
116
const uri = URI.joinPath(folder, fileName);
117
try {
118
await this.fileSystemService.stat(uri);
119
agentInstructionUriList.push(uri);
120
} catch {
121
// file doesn't exist
122
}
123
}
124
}
125
126
const items: vscode.ChatSessionCustomizationItem[] = [];
127
const seenUris = new Set<string>();
128
129
// Emit agent instruction files (AGENTS.md, CLAUDE.md, copilot-instructions.md)
130
// that come from customInstructionsService but may not appear in
131
// promptsService.getInstructions().
132
for (const uri of agentInstructionUriList) {
133
seenUris.add(uri.toString());
134
items.push({
135
uri,
136
type: vscode.ChatSessionCustomizationType.Instructions,
137
name: basename(uri),
138
description: undefined,
139
groupKey: 'agent-instructions',
140
extensionId: undefined,
141
pluginUri: undefined
142
});
143
}
144
145
for (const instruction of await this.promptsService.getInstructions(token)) {
146
const uri = instruction.uri;
147
if (!isEnabledForCopilotCLI(instruction)) {
148
continue; // only include instructions that are relevant for copilotcli
149
}
150
151
if (seenUris.has(uri.toString())) {
152
continue; // already emitted as agent instruction
153
}
154
155
const name = instruction.name;
156
const pattern = instruction.pattern;
157
const description = instruction.description;
158
159
if (pattern !== undefined) {
160
const badge = pattern === '**'
161
? l10n.t('always added')
162
: pattern;
163
const badgeTooltip = pattern === '**'
164
? l10n.t('This instruction is automatically included in every interaction.')
165
: l10n.t('This instruction is automatically included when files matching \'{0}\' are in context.', pattern);
166
items.push({
167
uri,
168
type: vscode.ChatSessionCustomizationType.Instructions,
169
name,
170
description,
171
groupKey: 'context-instructions',
172
badge,
173
badgeTooltip,
174
extensionId: instruction.extensionId,
175
pluginUri: instruction.pluginUri
176
});
177
} else {
178
items.push({
179
uri,
180
type: vscode.ChatSessionCustomizationType.Instructions,
181
name,
182
description,
183
groupKey: 'on-demand-instructions',
184
extensionId: instruction.extensionId,
185
pluginUri: instruction.pluginUri
186
});
187
}
188
}
189
190
return items;
191
}
192
193
/**
194
* Collects all skill items from the prompt file service.
195
*/
196
private async getSkillItems(token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {
197
return (await this.promptsService.getSkills(token)).filter(isEnabledForCopilotCLI).map(s => ({
198
uri: s.uri,
199
type: vscode.ChatSessionCustomizationType.Skill,
200
name: s.name,
201
description: s.description,
202
extensionId: s.extensionId,
203
pluginUri: s.pluginUri,
204
}));
205
}
206
207
/**
208
* Collects all hook items from the prompt file service.
209
* Each item is a hook configuration file (JSON).
210
*/
211
private async getHookItems(token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {
212
return (await this.promptsService.getHooks(token)).filter(isEnabledForCopilotCLI).map(h => ({
213
uri: h.uri,
214
type: vscode.ChatSessionCustomizationType.Hook,
215
name: basename(h.uri).replace(/\.json$/i, ''),
216
description: undefined,
217
extensionId: h.extensionId,
218
pluginUri: h.pluginUri,
219
}));
220
}
221
222
/**
223
* Collects all plugin items from the prompt file service.
224
*/
225
private async getPluginItems(token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {
226
return (await this.promptsService.getPlugins(token)).filter(isEnabledForCopilotCLI).map(p => ({
227
uri: p.uri,
228
type: vscode.ChatSessionCustomizationType.Plugins,
229
name: basename(p.uri),
230
description: undefined,
231
extensionId: undefined,
232
pluginUri: undefined,
233
}));
234
}
235
}
236
237