Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/vscode-node/claudeCustomizationProvider.ts
13399 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 vscode from 'vscode';
7
import { INativeEnvService } from '../../../platform/env/common/envService';
8
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
9
import { ILogService } from '../../../platform/log/common/logService';
10
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
11
import { Emitter } from '../../../util/vs/base/common/event';
12
import { Disposable } from '../../../util/vs/base/common/lifecycle';
13
import { basename } from '../../../util/vs/base/common/resources';
14
import { URI } from '../../../util/vs/base/common/uri';
15
import { IClaudeRuntimeDataService } from '../claude/common/claudeRuntimeDataService';
16
import { ClaudeSessionUri } from '../claude/common/claudeSessionUri';
17
import { IPromptsService } from '../../../platform/promptFiles/common/promptsService';
18
19
// TODO: Consider reporting Claude slash commands (from Query.supportedCommands()) when appropriate
20
// TODO: Report MCP servers when ChatSessionCustomizationType.Mcp is available (use Query.mcpServerStatus())
21
22
/**
23
* Hard-coded CLAUDE.md instruction file names that Claude recognizes.
24
* Per workspace folder: CLAUDE.md, CLAUDE.local.md, .claude/CLAUDE.md, .claude/CLAUDE.local.md
25
* User home: ~/.claude/CLAUDE.md
26
*/
27
const WORKSPACE_INSTRUCTION_PATHS = [
28
'CLAUDE.md',
29
'CLAUDE.local.md',
30
['.claude', 'CLAUDE.md'] as const,
31
['.claude', 'CLAUDE.local.md'] as const,
32
] as const;
33
34
const HOME_INSTRUCTION_PATHS = [
35
['.claude', 'CLAUDE.md'] as const,
36
] as const;
37
38
/**
39
* Hook event IDs that Claude supports, matching the HookEvent types from
40
* the Claude Agent SDK. Used to discover hooks from .claude/settings.json.
41
*/
42
const HOOK_EVENT_IDS = [
43
'PreToolUse', 'PostToolUse', 'PostToolUseFailure', 'PermissionRequest',
44
'UserPromptSubmit', 'Stop', 'SubagentStart', 'SubagentStop',
45
'PreCompact', 'SessionStart', 'SessionEnd', 'Notification',
46
] as const;
47
48
interface HookConfig {
49
readonly type: string;
50
readonly command: string;
51
}
52
53
interface MatcherConfig {
54
readonly matcher: string;
55
readonly hooks: HookConfig[];
56
}
57
58
interface HooksSettings {
59
readonly hooks?: Partial<Record<string, MatcherConfig[]>>;
60
}
61
62
export class ClaudeCustomizationProvider extends Disposable implements vscode.ChatSessionCustomizationProvider {
63
64
private readonly _onDidChange = this._register(new Emitter<void>());
65
readonly onDidChange = this._onDidChange.event;
66
67
static get metadata(): vscode.ChatSessionCustomizationProviderMetadata {
68
return {
69
label: 'Claude',
70
iconId: 'claude',
71
supportedTypes: [
72
vscode.ChatSessionCustomizationType.Agent,
73
vscode.ChatSessionCustomizationType.Skill,
74
vscode.ChatSessionCustomizationType.Instructions,
75
vscode.ChatSessionCustomizationType.Hook,
76
],
77
};
78
}
79
80
constructor(
81
@IPromptsService private readonly promptsService: IPromptsService,
82
@IClaudeRuntimeDataService private readonly runtimeDataService: IClaudeRuntimeDataService,
83
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
84
@IFileSystemService private readonly fileSystemService: IFileSystemService,
85
@INativeEnvService private readonly envService: INativeEnvService,
86
@ILogService private readonly logService: ILogService,
87
) {
88
super();
89
90
this._register(this.runtimeDataService.onDidChange(() => this._onDidChange.fire()));
91
this._register(this.promptsService.onDidChangeCustomAgents(() => this._onDidChange.fire()));
92
this._register(this.promptsService.onDidChangeSkills(() => this._onDidChange.fire()));
93
this._register(this.workspaceService.onDidChangeWorkspaceFolders(() => this._onDidChange.fire()));
94
}
95
96
async provideChatSessionCustomizations(token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {
97
const items: vscode.ChatSessionCustomizationItem[] = [];
98
99
// Agents: hybrid approach — file-based .claude/ agents merged with SDK-provided agents.
100
// File-based agents are available immediately; SDK agents appear once a session starts.
101
const sdkAgents = this.runtimeDataService.getAgents();
102
const sdkAgentNames = new Set(sdkAgents.map(a => a.name.toLowerCase()));
103
104
// SDK agents (built-in subagents like "Explore") — preferred when available
105
for (const agent of sdkAgents) {
106
items.push({
107
uri: URI.from({ scheme: ClaudeSessionUri.scheme, path: `/agents/${agent.name}` }),
108
type: vscode.ChatSessionCustomizationType.Agent,
109
name: agent.name,
110
description: agent.description,
111
extensionId: undefined,
112
pluginUri: undefined,
113
// No groupKey — vscode infers Built-in from non-file: scheme
114
});
115
}
116
117
// File-based agents from .claude/ paths — shown pre-session, deduplicated with SDK
118
for (const agent of await this.promptsService.getCustomAgents(token)) {
119
if (agent.enabled && isEnabledForClaudeCode(agent) && this.isClaudePath(agent.uri)) {
120
const name = agent.name;
121
if (!sdkAgentNames.has(name.toLowerCase())) {
122
items.push({
123
uri: agent.uri,
124
type: vscode.ChatSessionCustomizationType.Agent,
125
name,
126
description: agent.description,
127
extensionId: agent.extensionId,
128
pluginUri: agent.pluginUri,
129
});
130
}
131
}
132
}
133
134
const agentItems = items.filter(i => i.type === vscode.ChatSessionCustomizationType.Agent);
135
this.logService.debug(`[ClaudeCustomizationProvider] agents (${agentItems.length}): ${agentItems.map(a => a.name).join(', ') || '(none)'}${sdkAgents.length ? ' [sdk]' : ' [files-only, no session]'}`);
136
137
// Instructions from hard-coded CLAUDE.md paths (checked for existence)
138
const instructionItems = await this.discoverInstructions();
139
items.push(...instructionItems);
140
this.logService.debug(`[ClaudeCustomizationProvider] instructions (${instructionItems.length}): ${instructionItems.map(i => i.name).join(', ') || '(none)'}`);
141
142
// Skills from .claude/skills/ directories (user-defined SKILL.md files)
143
const skillItems: vscode.ChatSessionCustomizationItem[] = [];
144
for (const skill of await this.promptsService.getSkills(token)) {
145
if (this.isClaudePath(skill.uri)) {
146
const item: vscode.ChatSessionCustomizationItem = {
147
uri: skill.uri,
148
type: vscode.ChatSessionCustomizationType.Skill,
149
name: skill.name,
150
description: skill.description,
151
extensionId: skill.extensionId,
152
pluginUri: skill.pluginUri,
153
};
154
skillItems.push(item);
155
}
156
}
157
items.push(...skillItems);
158
this.logService.debug(`[ClaudeCustomizationProvider] skills (${skillItems.length}): ${skillItems.map(s => s.name).join(', ') || '(none)'}`);
159
160
// Hooks from .claude/settings.json files
161
const hookItems = await this.discoverHooks();
162
items.push(...hookItems);
163
this.logService.debug(`[ClaudeCustomizationProvider] hooks (${hookItems.length}): ${hookItems.map(h => h.name).join(', ') || '(none)'}`);
164
165
this.logService.debug(`[ClaudeCustomizationProvider] total: ${items.length} items`);
166
return items;
167
}
168
169
private async discoverInstructions(): Promise<vscode.ChatSessionCustomizationItem[]> {
170
const items: vscode.ChatSessionCustomizationItem[] = [];
171
const candidates: URI[] = [];
172
173
for (const folder of this.workspaceService.getWorkspaceFolders()) {
174
for (const entry of WORKSPACE_INSTRUCTION_PATHS) {
175
if (typeof entry === 'string') {
176
candidates.push(URI.joinPath(folder, entry));
177
} else {
178
candidates.push(URI.joinPath(folder, ...entry));
179
}
180
}
181
}
182
183
for (const entry of HOME_INSTRUCTION_PATHS) {
184
candidates.push(URI.joinPath(this.envService.userHome, ...entry));
185
}
186
187
for (const uri of candidates) {
188
if (await this.fileExists(uri)) {
189
const name = basename(uri).replace(/\.md$/i, '');
190
items.push({
191
uri,
192
type: vscode.ChatSessionCustomizationType.Instructions,
193
name,
194
description: undefined,
195
extensionId: undefined,
196
pluginUri: undefined,
197
});
198
}
199
}
200
201
return items;
202
}
203
204
private async fileExists(uri: URI): Promise<boolean> {
205
try {
206
await this.fileSystemService.stat(uri);
207
return true;
208
} catch {
209
return false;
210
}
211
}
212
213
private async discoverHooks(): Promise<vscode.ChatSessionCustomizationItem[]> {
214
const items: vscode.ChatSessionCustomizationItem[] = [];
215
const settingsPaths = this.getSettingsFilePaths();
216
217
for (const settingsUri of settingsPaths) {
218
try {
219
const content = await this.fileSystemService.readFile(settingsUri);
220
const settings: HooksSettings = JSON.parse(new TextDecoder().decode(content));
221
if (!settings.hooks) {
222
continue;
223
}
224
225
for (const eventId of HOOK_EVENT_IDS) {
226
const matchers = settings.hooks[eventId];
227
if (!matchers || matchers.length === 0) {
228
continue;
229
}
230
231
for (const matcher of matchers) {
232
for (const hook of matcher.hooks) {
233
const matcherLabel = matcher.matcher === '*' ? '' : ` (${matcher.matcher})`;
234
items.push({
235
uri: settingsUri,
236
type: vscode.ChatSessionCustomizationType.Hook,
237
name: `${eventId}${matcherLabel}`,
238
description: hook.command,
239
extensionId: undefined,
240
pluginUri: undefined,
241
});
242
}
243
}
244
}
245
} catch {
246
// Settings file doesn't exist or is invalid — skip
247
}
248
}
249
250
return items;
251
}
252
253
private getSettingsFilePaths(): URI[] {
254
const paths: URI[] = [];
255
256
for (const folder of this.workspaceService.getWorkspaceFolders()) {
257
paths.push(URI.joinPath(folder, '.claude', 'settings.json'));
258
paths.push(URI.joinPath(folder, '.claude', 'settings.local.json'));
259
}
260
261
paths.push(URI.joinPath(this.envService.userHome, '.claude', 'settings.json'));
262
return paths;
263
}
264
265
private isClaudePath(uri: URI): boolean {
266
const folders = this.workspaceService.getWorkspaceFolders();
267
for (const folder of folders) {
268
const folderPath = folder.path.endsWith('/') ? folder.path : folder.path + '/';
269
if (uri.path.startsWith(folderPath)) {
270
const relative = uri.path.slice(folderPath.length);
271
if (relative.startsWith('.claude/')) {
272
return true;
273
}
274
}
275
}
276
277
// Also check user home .claude/ directory
278
const homePath = this.envService.userHome.path;
279
const homePrefix = homePath.endsWith('/') ? homePath : homePath + '/';
280
if (uri.path.startsWith(homePrefix)) {
281
const relative = uri.path.slice(homePrefix.length);
282
if (relative.startsWith('.claude/')) {
283
return true;
284
}
285
}
286
287
return false;
288
}
289
}
290
291
export function isEnabledForClaudeCode(customization: { sessionTypes?: readonly string[] }): boolean {
292
const sessionTypes = customization.sessionTypes;
293
return sessionTypes === undefined || sessionTypes.includes('claude-code') || false;
294
}
295
296