Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/vscode-node/slashCommands/agentsCommand.ts
13406 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 { createDirectoryIfNotExists, IFileSystemService } from '../../../../../platform/filesystem/common/fileSystemService';
9
import { ILogService } from '../../../../../platform/log/common/logService';
10
import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';
11
import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
12
import { DisposableStore } from '../../../../../util/vs/base/common/lifecycle';
13
import { URI } from '../../../../../util/vs/base/common/uri';
14
import { IClaudeSlashCommandHandler, registerClaudeSlashCommand } from './claudeSlashCommandRegistry';
15
16
/**
17
* AGENTS CONFIGURATION WIZARD
18
* ===========================
19
*
20
* This wizard allows users to create and manage specialized Claude subagents that can be
21
* delegated to for specific tasks. Each agent has its own system prompt, tools, and model.
22
*
23
* ## MAIN MENU
24
* Shows a list of all agents with options to create new or manage existing ones.
25
*
26
* - Create new agent (always available)
27
* - Project agents (.claude/agents/) - listed with model
28
* - User/Personal agents (~/.claude/agents/) - listed with model
29
*
30
* ## CREATE FLOW
31
* 1. Choose location (Project .claude/agents/ or Personal ~/.claude/agents/)
32
* 2. Choose creation method:
33
* - Generate with Claude (recommended): Describe what the agent should do
34
* - Manual configuration: Enter type, system prompt, description manually
35
* 3. For "Generate with Claude":
36
* a. Enter description of what agent should do
37
* b. Wait for generation
38
* c. Select tools (with advanced options for individual tools/MCP)
39
* d. Select model
40
* e. File is saved and opened
41
* 4. For "Manual configuration":
42
* a. Enter agent type identifier (e.g., "test-runner")
43
* b. Enter system prompt
44
* c. Enter description (when Claude should use this agent)
45
* d. Select tools
46
* e. Select model
47
* f. File is saved and opened
48
*
49
* ## EDIT FLOW (selecting existing agent)
50
* 1. Choose action:
51
* - View agent (opens file)
52
* - Edit agent (shows edit menu)
53
* - Delete agent (with confirmation)
54
* - Back (returns to main menu)
55
* 2. Edit menu:
56
* - Open in editor
57
* - Edit tools (tool picker)
58
* - Edit model (model picker)
59
*
60
* ## AGENT FILE FORMAT
61
* Agents are stored as markdown files with YAML frontmatter:
62
* ```
63
* ---
64
* name: agent-name
65
* description: "When Claude should use this agent..."
66
* model: sonnet
67
* allowedTools:
68
* - Read
69
* - Grep
70
* - Glob
71
* ---
72
*
73
* System prompt content here...
74
* ```
75
*/
76
77
/**
78
* Agent location type
79
*/
80
type AgentLocationType = 'project' | 'user';
81
82
/**
83
* Agent file location
84
*/
85
interface AgentLocation {
86
type: AgentLocationType;
87
label: string;
88
agentsDir: URI;
89
workspaceFolder?: URI;
90
}
91
92
/**
93
* Parsed agent configuration
94
*/
95
interface AgentConfig {
96
name: string;
97
description: string;
98
model: string;
99
allowedTools?: string[];
100
systemPrompt: string;
101
}
102
103
/**
104
* Agent with its source location
105
*/
106
interface AgentWithSource {
107
config: AgentConfig;
108
location: AgentLocation;
109
filePath: URI;
110
}
111
112
/**
113
* Available models for agents
114
*/
115
const AGENT_MODELS = [
116
{
117
id: 'sonnet',
118
label: 'Sonnet',
119
description: 'Balanced performance - best for most agents',
120
isDefault: true,
121
},
122
{
123
id: 'opus',
124
label: 'Opus',
125
description: 'Most capable for complex reasoning tasks',
126
},
127
{
128
id: 'haiku',
129
label: 'Haiku',
130
description: 'Fast and efficient for simple tasks',
131
},
132
{
133
id: 'inherit',
134
label: 'Inherit from parent',
135
description: 'Use the same model as the main conversation',
136
},
137
] as const;
138
139
/**
140
* Tool categories for selection
141
*/
142
const TOOL_CATEGORIES = [
143
{ id: 'readonly', label: 'Read-only tools', tools: ['Read', 'Glob', 'Grep', 'WebFetch', 'WebSearch'] },
144
{ id: 'edit', label: 'Edit tools', tools: ['Edit', 'Write', 'NotebookEdit'] },
145
{ id: 'execution', label: 'Execution tools', tools: ['Bash'] },
146
{ id: 'mcp', label: 'MCP tools', tools: [] }, // Populated dynamically
147
{ id: 'other', label: 'Other tools', tools: ['Skill', 'Agent', 'Task', 'TodoWrite'] },
148
] as const;
149
150
/**
151
* All individual tools
152
*/
153
const ALL_TOOLS = [
154
'Bash',
155
'Glob',
156
'Grep',
157
'Read',
158
'Edit',
159
'Write',
160
'NotebookEdit',
161
'WebFetch',
162
'WebSearch',
163
'Skill',
164
'Agent',
165
'Task',
166
'TodoWrite',
167
] as const;
168
169
/**
170
* Slash command handler for managing Claude agents.
171
* Launches a QuickPick wizard to create, view, edit, and delete agents.
172
*/
173
export class AgentsSlashCommand implements IClaudeSlashCommandHandler {
174
readonly commandName = 'agents';
175
readonly description = 'Create and manage specialized Claude agents';
176
readonly commandId = 'copilot.claude.agents';
177
178
constructor(
179
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
180
@IFileSystemService private readonly fileSystemService: IFileSystemService,
181
@INativeEnvService private readonly envService: INativeEnvService,
182
@ILogService private readonly logService: ILogService,
183
) { }
184
185
async handle(
186
_args: string,
187
stream: vscode.ChatResponseStream | undefined,
188
_token: CancellationToken
189
): Promise<vscode.ChatResult> {
190
stream?.markdown(vscode.l10n.t('Opening agents configuration...'));
191
192
// Fire and forget - wizard runs in background
193
this._runWizard().catch(error => {
194
this.logService.error('[AgentsSlashCommand] Error running agents wizard:', error);
195
vscode.window.showErrorMessage(
196
vscode.l10n.t('Error configuring agent: {0}', error instanceof Error ? error.message : String(error))
197
);
198
});
199
200
return {};
201
}
202
203
private async _runWizard(): Promise<void> {
204
// Main menu - show agents list
205
const result = await this._showMainMenu();
206
if (!result) {
207
return;
208
}
209
210
if (result.action === 'create') {
211
await this._runCreateFlow();
212
} else if (result.action === 'select' && result.agent) {
213
await this._runAgentActionMenu(result.agent);
214
}
215
}
216
217
/**
218
* Shows the main agents list menu.
219
*/
220
private async _showMainMenu(): Promise<{ action: 'create' | 'select'; agent?: AgentWithSource } | undefined> {
221
const projectAgents = await this._loadProjectAgents();
222
const userAgents = await this._loadUserAgents();
223
224
type AgentMenuItem = vscode.QuickPickItem & {
225
action: 'create' | 'select';
226
agent?: AgentWithSource;
227
};
228
229
const items: (AgentMenuItem | vscode.QuickPickItem)[] = [];
230
231
// Create new agent option
232
items.push({
233
label: '$(add) ' + vscode.l10n.t('Create new agent'),
234
action: 'create',
235
});
236
237
// Project agents section
238
if (projectAgents.length > 0) {
239
items.push({
240
label: vscode.l10n.t('Project agents'),
241
kind: vscode.QuickPickItemKind.Separator,
242
});
243
244
for (const agent of projectAgents) {
245
items.push({
246
label: agent.config.name,
247
description: `· ${agent.config.model}`,
248
action: 'select',
249
agent,
250
});
251
}
252
}
253
254
// User/Personal agents section
255
if (userAgents.length > 0) {
256
items.push({
257
label: vscode.l10n.t('Personal agents'),
258
kind: vscode.QuickPickItemKind.Separator,
259
});
260
261
for (const agent of userAgents) {
262
items.push({
263
label: agent.config.name,
264
description: `· ${agent.config.model}`,
265
action: 'select',
266
agent,
267
});
268
}
269
}
270
271
// Show placeholder text if no custom agents
272
const placeholderText = projectAgents.length === 0 && userAgents.length === 0
273
? vscode.l10n.t('No agents found. Create specialized subagents that Claude can delegate to.')
274
: vscode.l10n.t('Select an agent to view, edit, or delete');
275
276
const selected = await vscode.window.showQuickPick(items, {
277
title: vscode.l10n.t('Agents'),
278
placeHolder: placeholderText,
279
ignoreFocusOut: true,
280
}) as AgentMenuItem | undefined;
281
282
if (!selected) {
283
return undefined;
284
}
285
286
return { action: selected.action, agent: selected.agent };
287
}
288
289
/**
290
* Runs the create agent flow.
291
*/
292
private async _runCreateFlow(): Promise<void> {
293
// Step 1: Choose location
294
const location = await this._selectLocation();
295
if (!location) {
296
return;
297
}
298
299
// Step 2: Choose creation method
300
const method = await this._selectCreationMethod();
301
if (!method) {
302
return;
303
}
304
305
if (method === 'generate') {
306
await this._runGenerateFlow(location);
307
} else {
308
await this._runManualFlow(location);
309
}
310
}
311
312
/**
313
* Step 1: Select where to save the agent.
314
*/
315
private async _selectLocation(): Promise<AgentLocation | undefined> {
316
type LocationItem = vscode.QuickPickItem & { location: AgentLocation };
317
318
const items: LocationItem[] = [];
319
320
// Project location (first workspace folder)
321
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
322
if (workspaceFolders.length > 0) {
323
const firstFolder = workspaceFolders[0];
324
items.push({
325
label: vscode.l10n.t('1. Project (.claude/agents/)'),
326
location: {
327
type: 'project',
328
label: vscode.l10n.t('Project'),
329
agentsDir: URI.joinPath(firstFolder, '.claude', 'agents'),
330
workspaceFolder: firstFolder,
331
},
332
});
333
}
334
335
// Personal location
336
items.push({
337
label: vscode.l10n.t('2. Personal (~/.claude/agents/)'),
338
location: {
339
type: 'user',
340
label: vscode.l10n.t('Personal'),
341
agentsDir: URI.joinPath(this.envService.userHome, '.claude', 'agents'),
342
},
343
});
344
345
const selected = await vscode.window.showQuickPick(items, {
346
title: vscode.l10n.t('Create new agent'),
347
placeHolder: vscode.l10n.t('Choose location'),
348
ignoreFocusOut: true,
349
});
350
351
return selected?.location;
352
}
353
354
/**
355
* Step 2: Select creation method.
356
*/
357
private async _selectCreationMethod(): Promise<'generate' | 'manual' | undefined> {
358
const items: (vscode.QuickPickItem & { method: 'generate' | 'manual' })[] = [
359
{
360
label: vscode.l10n.t('1. Generate with Claude (recommended)'),
361
method: 'generate',
362
},
363
{
364
label: vscode.l10n.t('2. Manual configuration'),
365
method: 'manual',
366
},
367
];
368
369
const selected = await vscode.window.showQuickPick(items, {
370
title: vscode.l10n.t('Create new agent'),
371
placeHolder: vscode.l10n.t('Creation method'),
372
ignoreFocusOut: true,
373
});
374
375
return selected?.method;
376
}
377
378
/**
379
* Generate flow: describe agent, generate, select tools, select model.
380
*/
381
private async _runGenerateFlow(location: AgentLocation): Promise<void> {
382
// Step 3: Enter description
383
const description = await vscode.window.showInputBox({
384
title: vscode.l10n.t('Create new agent'),
385
prompt: vscode.l10n.t('Describe what this agent should do and when it should be used (be comprehensive for best results)'),
386
placeHolder: vscode.l10n.t('e.g., Help me write unit tests for my code...'),
387
ignoreFocusOut: true,
388
});
389
390
if (!description) {
391
return;
392
}
393
394
// Step 4: Generate agent with Claude
395
const generated = await vscode.window.withProgress({
396
location: vscode.ProgressLocation.Notification,
397
title: vscode.l10n.t('Generating agent from description...'),
398
cancellable: true,
399
}, async (_progress, token) => {
400
return this._generateAgentConfig(description, token);
401
});
402
403
if (!generated) {
404
return;
405
}
406
407
// Step 5: Select tools
408
const tools = await this._selectTools();
409
if (!tools) {
410
return;
411
}
412
413
// Step 6: Select model
414
const model = await this._selectModel();
415
if (!model) {
416
return;
417
}
418
419
// Build final config
420
const config: AgentConfig = {
421
name: generated.name,
422
description: generated.description,
423
model,
424
allowedTools: tools.length > 0 && !tools.includes('*') ? tools : undefined,
425
systemPrompt: generated.systemPrompt,
426
};
427
428
// Save and open
429
const filePath = URI.joinPath(location.agentsDir, `${config.name}.md`);
430
await this._saveAgent(filePath, config);
431
await this._openAgentFile(filePath);
432
}
433
434
/**
435
* Manual flow: enter type, system prompt, description, select tools, select model.
436
*/
437
private async _runManualFlow(location: AgentLocation): Promise<void> {
438
// Step 3: Enter agent type (identifier)
439
const name = await vscode.window.showInputBox({
440
title: vscode.l10n.t('Create new agent'),
441
prompt: vscode.l10n.t('Enter a unique identifier for your agent:'),
442
placeHolder: vscode.l10n.t('e.g., test-runner, tech-lead, etc'),
443
ignoreFocusOut: true,
444
validateInput: value => {
445
if (!value) {
446
return vscode.l10n.t('Agent name is required');
447
}
448
if (!/^[a-z0-9-]+$/.test(value)) {
449
return vscode.l10n.t('Use lowercase letters, numbers, and hyphens only');
450
}
451
return null;
452
},
453
});
454
455
if (!name) {
456
return;
457
}
458
459
// Step 4: Enter system prompt
460
const systemPrompt = await vscode.window.showInputBox({
461
title: vscode.l10n.t('Create new agent'),
462
prompt: vscode.l10n.t('Enter the system prompt for your agent:') + '\n' + vscode.l10n.t('Be comprehensive for best results'),
463
placeHolder: vscode.l10n.t('You are a helpful code reviewer who...'),
464
ignoreFocusOut: true,
465
});
466
467
if (!systemPrompt) {
468
return;
469
}
470
471
// Step 5: Enter description
472
const description = await vscode.window.showInputBox({
473
title: vscode.l10n.t('Create new agent'),
474
prompt: vscode.l10n.t('When should Claude use this agent?'),
475
placeHolder: vscode.l10n.t("e.g., use this agent after you're done writing code..."),
476
ignoreFocusOut: true,
477
});
478
479
if (!description) {
480
return;
481
}
482
483
// Step 6: Select tools
484
const tools = await this._selectTools();
485
if (!tools) {
486
return;
487
}
488
489
// Step 7: Select model
490
const model = await this._selectModel();
491
if (!model) {
492
return;
493
}
494
495
// Build config
496
const config: AgentConfig = {
497
name,
498
description,
499
model,
500
allowedTools: tools.length > 0 && !tools.includes('*') ? tools : undefined,
501
systemPrompt,
502
};
503
504
// Save and open
505
const filePath = URI.joinPath(location.agentsDir, `${config.name}.md`);
506
await this._saveAgent(filePath, config);
507
await this._openAgentFile(filePath);
508
}
509
510
/**
511
* Generate agent config using Claude.
512
*/
513
private async _generateAgentConfig(
514
description: string,
515
token: vscode.CancellationToken
516
): Promise<{ name: string; description: string; systemPrompt: string } | undefined> {
517
try {
518
const prompt = `Based on the following description, generate a Claude agent configuration.
519
520
Description: ${description}
521
522
Respond with a JSON object containing:
523
1. "name": A short, kebab-case identifier (e.g., "test-runner", "code-reviewer")
524
2. "description": A detailed description of when Claude should use this agent (include examples)
525
3. "systemPrompt": A comprehensive system prompt that defines the agent's behavior, expertise, and guidelines
526
527
Keep the systemPrompt focused but thorough. Include specific instructions for how the agent should approach tasks.
528
529
Respond ONLY with the JSON object, no markdown code blocks or other text.`;
530
531
// Use claude-sonnet-4.5 for agent generation (fast and efficient for structured output)
532
let models = await vscode.lm.selectChatModels({ family: 'claude-sonnet-4.5', vendor: 'copilot' });
533
if (models.length === 0) {
534
// Fallback to any available model
535
models = await vscode.lm.selectChatModels({ vendor: 'copilot' });
536
// Get latest claude-sonnet- model
537
models = models
538
.filter(model => model.family.startsWith('claude-sonnet-'))
539
.sort((a, b) => b.family.localeCompare(a.family));
540
if (models.length === 0) {
541
vscode.window.showErrorMessage(vscode.l10n.t('No language model available for agent generation'));
542
return undefined;
543
}
544
}
545
546
const response = await models[0].sendRequest(
547
[vscode.LanguageModelChatMessage.User(prompt)],
548
{},
549
token
550
);
551
552
let responseText = '';
553
for await (const chunk of response.stream) {
554
if (chunk instanceof vscode.LanguageModelTextPart) {
555
responseText += chunk.value;
556
}
557
}
558
559
// Strip markdown code blocks if present
560
let jsonText = responseText.trim();
561
const codeBlockMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)```/);
562
if (codeBlockMatch) {
563
jsonText = codeBlockMatch[1].trim();
564
}
565
566
// Parse JSON response
567
const parsed = JSON.parse(jsonText);
568
return {
569
name: parsed.name,
570
description: parsed.description,
571
systemPrompt: parsed.systemPrompt,
572
};
573
} catch (error) {
574
this.logService.error('[AgentsSlashCommand] Failed to generate agent:', error);
575
vscode.window.showErrorMessage(
576
vscode.l10n.t('Failed to generate agent: {0}', error instanceof Error ? error.message : String(error))
577
);
578
return undefined;
579
}
580
}
581
582
/**
583
* Select tools for the agent (multi-select with categories).
584
*/
585
private async _selectTools(): Promise<string[] | undefined> {
586
type ToolPickItem = vscode.QuickPickItem & {
587
categoryId?: string;
588
toolId?: string;
589
};
590
591
// Toggle button for advanced options
592
const showAdvancedButton: vscode.QuickInputButton = {
593
iconPath: new vscode.ThemeIcon('chevron-down'),
594
tooltip: vscode.l10n.t('Show advanced options'),
595
};
596
const hideAdvancedButton: vscode.QuickInputButton = {
597
iconPath: new vscode.ThemeIcon('chevron-up'),
598
tooltip: vscode.l10n.t('Hide advanced options'),
599
};
600
601
let showAdvanced = false;
602
let resolved = false;
603
604
return new Promise<string[] | undefined>((resolve) => {
605
const disposables = new DisposableStore();
606
const quickPick = vscode.window.createQuickPick<ToolPickItem>();
607
disposables.add(quickPick);
608
quickPick.title = vscode.l10n.t('Create new agent');
609
quickPick.placeholder = vscode.l10n.t('Select tools');
610
quickPick.canSelectMany = true;
611
quickPick.ignoreFocusOut = true;
612
quickPick.buttons = [showAdvancedButton];
613
614
const updateItems = () => {
615
const items: ToolPickItem[] = [];
616
617
// Tool categories
618
for (const cat of TOOL_CATEGORIES) {
619
items.push({
620
label: cat.label,
621
categoryId: cat.id,
622
});
623
}
624
625
// Advanced: individual tools
626
if (showAdvanced) {
627
items.push({
628
label: vscode.l10n.t('Individual Tools'),
629
kind: vscode.QuickPickItemKind.Separator,
630
});
631
632
for (const tool of ALL_TOOLS) {
633
items.push({
634
label: tool,
635
toolId: tool,
636
});
637
}
638
}
639
640
// Preserve selection when updating items
641
const previouslySelectedIds = new Set(
642
quickPick.selectedItems.map(item => item.categoryId || item.toolId)
643
);
644
645
quickPick.items = items;
646
647
// Restore selection
648
quickPick.selectedItems = items.filter(item => {
649
const id = item.categoryId || item.toolId;
650
return id && previouslySelectedIds.has(id);
651
});
652
};
653
654
// Initialize with all categories selected
655
updateItems();
656
quickPick.selectedItems = quickPick.items.filter(item => item.categoryId);
657
658
disposables.add(quickPick.onDidTriggerButton((button) => {
659
if (button === showAdvancedButton || button === hideAdvancedButton) {
660
showAdvanced = !showAdvanced;
661
quickPick.buttons = [showAdvanced ? hideAdvancedButton : showAdvancedButton];
662
updateItems();
663
}
664
}));
665
666
disposables.add(quickPick.onDidAccept(() => {
667
if (resolved) {
668
return;
669
}
670
resolved = true;
671
672
const selectedItems = quickPick.selectedItems;
673
disposables.dispose();
674
675
// Check if all categories are selected - treat as "all tools"
676
const selectedCategoryIds = new Set(
677
selectedItems.filter(item => item.categoryId).map(item => item.categoryId)
678
);
679
const allCategoriesSelected = TOOL_CATEGORIES.every(cat => selectedCategoryIds.has(cat.id));
680
if (allCategoriesSelected) {
681
resolve(['*']);
682
return;
683
}
684
685
// Collect selected tools from categories and individual tools
686
const tools = new Set<string>();
687
for (const item of selectedItems) {
688
if (item.categoryId) {
689
const cat = TOOL_CATEGORIES.find(c => c.id === item.categoryId);
690
if (cat) {
691
for (const tool of cat.tools) {
692
tools.add(tool);
693
}
694
}
695
} else if (item.toolId) {
696
tools.add(item.toolId);
697
}
698
}
699
700
resolve(Array.from(tools));
701
}));
702
703
disposables.add(quickPick.onDidHide(() => {
704
disposables.dispose();
705
if (!resolved) {
706
resolved = true;
707
resolve(undefined);
708
}
709
}));
710
711
quickPick.show();
712
});
713
}
714
715
/**
716
* Select model for the agent.
717
*/
718
private async _selectModel(): Promise<string | undefined> {
719
const items = AGENT_MODELS.map((model, index) => ({
720
label: `${index + 1}. ${model.label}${'isDefault' in model && model.isDefault ? ' $(check)' : ''}`,
721
description: model.description,
722
modelId: model.id,
723
}));
724
725
const selected = await vscode.window.showQuickPick(items, {
726
title: vscode.l10n.t('Create new agent'),
727
placeHolder: vscode.l10n.t('Select model') + '\n' + vscode.l10n.t("Model determines the agent's reasoning capabilities and speed."),
728
ignoreFocusOut: true,
729
});
730
731
return selected?.modelId;
732
}
733
734
/**
735
* Shows the action menu for a selected agent.
736
*/
737
private async _runAgentActionMenu(agent: AgentWithSource): Promise<void> {
738
type ActionItem = vscode.QuickPickItem & { action: 'view' | 'edit' | 'delete' | 'back' };
739
740
const items: ActionItem[] = [
741
{ label: vscode.l10n.t('1. View agent'), action: 'view' },
742
{ label: vscode.l10n.t('2. Edit agent'), action: 'edit' },
743
{ label: vscode.l10n.t('3. Delete agent'), action: 'delete' },
744
{ label: vscode.l10n.t('4. Back'), action: 'back' },
745
];
746
747
const selected = await vscode.window.showQuickPick(items, {
748
title: agent.config.name,
749
placeHolder: vscode.l10n.t('Choose an action'),
750
ignoreFocusOut: true,
751
});
752
753
if (!selected) {
754
return;
755
}
756
757
switch (selected.action) {
758
case 'view':
759
await this._openAgentFile(agent.filePath);
760
break;
761
case 'edit':
762
await this._runEditMenu(agent);
763
break;
764
case 'delete':
765
await this._deleteAgent(agent);
766
break;
767
case 'back':
768
await this._runWizard();
769
break;
770
}
771
}
772
773
/**
774
* Shows the edit menu for an agent.
775
*/
776
private async _runEditMenu(agent: AgentWithSource): Promise<void> {
777
type EditItem = vscode.QuickPickItem & { action: 'open' | 'tools' | 'model' };
778
779
const items: EditItem[] = [
780
{ label: '$(edit) ' + vscode.l10n.t('Open in editor'), action: 'open' },
781
{ label: '$(tools) ' + vscode.l10n.t('Edit tools'), action: 'tools' },
782
{ label: '$(symbol-misc) ' + vscode.l10n.t('Edit model'), action: 'model' },
783
];
784
785
const selected = await vscode.window.showQuickPick(items, {
786
title: vscode.l10n.t('Edit agent: {0}', agent.config.name),
787
placeHolder: vscode.l10n.t('Source: {0}', agent.location.label),
788
ignoreFocusOut: true,
789
});
790
791
if (!selected) {
792
return;
793
}
794
795
switch (selected.action) {
796
case 'open':
797
await this._openAgentFile(agent.filePath);
798
break;
799
case 'tools': {
800
const tools = await this._selectTools();
801
if (tools) {
802
const updatedConfig = {
803
...agent.config,
804
allowedTools: tools.includes('*') ? undefined : tools,
805
};
806
await this._saveAgent(agent.filePath, updatedConfig);
807
await this._openAgentFile(agent.filePath);
808
}
809
break;
810
}
811
case 'model': {
812
const model = await this._selectModel();
813
if (model) {
814
const updatedConfig = {
815
...agent.config,
816
model,
817
};
818
await this._saveAgent(agent.filePath, updatedConfig);
819
await this._openAgentFile(agent.filePath);
820
}
821
break;
822
}
823
}
824
}
825
826
/**
827
* Delete an agent with confirmation.
828
*/
829
private async _deleteAgent(agent: AgentWithSource): Promise<void> {
830
const confirm = await vscode.window.showWarningMessage(
831
vscode.l10n.t('Are you sure you want to delete the agent "{0}"?', agent.config.name),
832
{ modal: true },
833
vscode.l10n.t('Delete')
834
);
835
836
if (confirm === vscode.l10n.t('Delete')) {
837
await this.fileSystemService.delete(agent.filePath);
838
vscode.window.showInformationMessage(vscode.l10n.t('Agent "{0}" deleted', agent.config.name));
839
// Return to main menu
840
await this._runWizard();
841
}
842
}
843
844
/**
845
* Load all project agents from .claude/agents/ directories.
846
*/
847
private async _loadProjectAgents(): Promise<AgentWithSource[]> {
848
const agents: AgentWithSource[] = [];
849
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
850
851
for (const folder of workspaceFolders) {
852
const agentsDir = URI.joinPath(folder, '.claude', 'agents');
853
const location: AgentLocation = {
854
type: 'project',
855
label: vscode.l10n.t('Project'),
856
agentsDir,
857
workspaceFolder: folder,
858
};
859
860
const loaded = await this._loadAgentsFromDirectory(agentsDir, location);
861
agents.push(...loaded);
862
}
863
864
return agents;
865
}
866
867
/**
868
* Load all user/personal agents from ~/.claude/agents/.
869
*/
870
private async _loadUserAgents(): Promise<AgentWithSource[]> {
871
const agentsDir = URI.joinPath(this.envService.userHome, '.claude', 'agents');
872
const location: AgentLocation = {
873
type: 'user',
874
label: vscode.l10n.t('Personal'),
875
agentsDir,
876
};
877
878
return this._loadAgentsFromDirectory(agentsDir, location);
879
}
880
881
/**
882
* Load agents from a specific directory.
883
*/
884
private async _loadAgentsFromDirectory(dir: URI, location: AgentLocation): Promise<AgentWithSource[]> {
885
const agents: AgentWithSource[] = [];
886
887
try {
888
const entries = await this.fileSystemService.readDirectory(dir);
889
890
for (const [name, type] of entries) {
891
if (type === vscode.FileType.File && name.endsWith('.md')) {
892
const filePath = URI.joinPath(dir, name);
893
try {
894
const config = await this._parseAgentFile(filePath);
895
if (config) {
896
agents.push({ config, location, filePath });
897
}
898
} catch (error) {
899
this.logService.warn(`[AgentsSlashCommand] Failed to parse agent file ${filePath.fsPath}: ${error}`);
900
}
901
}
902
}
903
} catch {
904
// Directory doesn't exist or can't be read
905
}
906
907
return agents;
908
}
909
910
/**
911
* Parse an agent markdown file with YAML frontmatter.
912
*/
913
private async _parseAgentFile(filePath: URI): Promise<AgentConfig | undefined> {
914
try {
915
const content = await this.fileSystemService.readFile(filePath);
916
const text = new TextDecoder().decode(content);
917
918
// Parse YAML frontmatter
919
const frontmatterMatch = text.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
920
if (!frontmatterMatch) {
921
return undefined;
922
}
923
924
const frontmatter = frontmatterMatch[1];
925
const systemPrompt = frontmatterMatch[2].trim();
926
927
// Simple YAML parsing for the fields we need
928
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
929
const descMatch = frontmatter.match(/^description:\s*["']?([\s\S]*?)["']?$/m);
930
const modelMatch = frontmatter.match(/^model:\s*(.+)$/m);
931
932
// Parse allowedTools if present
933
const toolsMatch = frontmatter.match(/^allowedTools:\s*\n((?:\s+-\s+.+\n?)+)/m);
934
let allowedTools: string[] | undefined;
935
if (toolsMatch) {
936
allowedTools = toolsMatch[1]
937
.split('\n')
938
.map(line => line.match(/^\s+-\s+(.+)$/)?.[1])
939
.filter((t): t is string => !!t);
940
}
941
942
if (!nameMatch || !modelMatch) {
943
return undefined;
944
}
945
946
return {
947
name: nameMatch[1].trim(),
948
description: descMatch ? descMatch[1].trim() : '',
949
model: modelMatch[1].trim(),
950
allowedTools,
951
systemPrompt,
952
};
953
} catch {
954
return undefined;
955
}
956
}
957
958
/**
959
* Save an agent to a markdown file.
960
*/
961
private async _saveAgent(filePath: URI, config: AgentConfig): Promise<void> {
962
// Ensure directory exists
963
const dir = URI.joinPath(filePath, '..');
964
await createDirectoryIfNotExists(this.fileSystemService, dir);
965
966
// Build the file content
967
let content = `---\nname: ${config.name}\ndescription: "${config.description.replace(/"/g, '\\"')}"\nmodel: ${config.model}\n`;
968
969
if (config.allowedTools && config.allowedTools.length > 0) {
970
content += 'allowedTools:\n';
971
for (const tool of config.allowedTools) {
972
content += ` - ${tool}\n`;
973
}
974
}
975
976
content += `---\n\n${config.systemPrompt}\n`;
977
978
await this.fileSystemService.writeFile(filePath, new TextEncoder().encode(content));
979
}
980
981
/**
982
* Open an agent file in the editor.
983
*/
984
private async _openAgentFile(filePath: URI): Promise<void> {
985
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath.fsPath));
986
await vscode.window.showTextDocument(doc);
987
}
988
}
989
990
// Self-register the agents command
991
registerClaudeSlashCommand(AgentsSlashCommand);
992
993