diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 355094d8011ec..571a669c5761f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -53,7 +53,7 @@ import { ILanguageModelToolsService } from '../common/tools/languageModelToolsSe import { agentPluginDiscoveryRegistry, IAgentPluginService } from '../common/plugins/agentPluginService.js'; import { ChatPromptFilesExtensionPointHandler } from '../common/promptSyntax/chatPromptFilesContribution.js'; import { PromptsConfig } from '../common/promptSyntax/config/config.js'; -import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, DEFAULT_SKILL_SOURCE_FOLDERS, AGENTS_SOURCE_FOLDER, AGENT_FILE_EXTENSION, SKILL_FILENAME, CLAUDE_AGENTS_SOURCE_FOLDER, DEFAULT_HOOK_FILE_PATHS, DEFAULT_INSTRUCTIONS_SOURCE_FOLDERS, PromptFileSource } from '../common/promptSyntax/config/promptFileLocations.js'; +import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, DEFAULT_SKILL_SOURCE_FOLDERS, AGENTS_SOURCE_FOLDER, AGENT_FILE_EXTENSION, SKILL_FILENAME, CLAUDE_AGENTS_SOURCE_FOLDER, DEFAULT_HOOK_FILE_PATHS, DEFAULT_INSTRUCTIONS_SOURCE_FOLDERS, PromptFileSource, COPILOT_USER_AGENTS_SOURCE_FOLDER } from '../common/promptSyntax/config/promptFileLocations.js'; import { PromptLanguageFeaturesProvider } from '../common/promptSyntax/promptFileContributions.js'; import { AGENT_DOCUMENTATION_URL, INSTRUCTIONS_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL, SKILL_DOCUMENTATION_URL, HOOK_DOCUMENTATION_URL, PromptsType } from '../common/promptSyntax/promptTypes.js'; import { hookFileSchema, HOOK_SCHEMA_URI } from '../common/promptSyntax/hookSchema.js'; @@ -884,6 +884,7 @@ configurationRegistry.registerConfiguration({ default: { [AGENTS_SOURCE_FOLDER]: true, [CLAUDE_AGENTS_SOURCE_FOLDER]: true, + [COPILOT_USER_AGENTS_SOURCE_FOLDER]: true, }, additionalProperties: { type: 'boolean' }, propertyNames: { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts index 70de863c8f815..779974e1e6310 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts @@ -86,6 +86,11 @@ export const AGENTS_SOURCE_FOLDER = '.github/agents'; */ export const CLAUDE_AGENTS_SOURCE_FOLDER = '.claude/agents'; +/** + * Copilot user agents folder. + */ +export const COPILOT_USER_AGENTS_SOURCE_FOLDER = '~/.copilot/agents'; + /** * Claude rules folder. */ @@ -186,6 +191,7 @@ export const DEFAULT_AGENT_SOURCE_FOLDERS: readonly IPromptSourceFolder[] = [ { path: AGENTS_SOURCE_FOLDER, source: PromptFileSource.GitHubWorkspace, storage: PromptsStorage.local }, { path: CLAUDE_AGENTS_SOURCE_FOLDER, source: PromptFileSource.ClaudeWorkspace, storage: PromptsStorage.local }, { path: '~/' + CLAUDE_AGENTS_SOURCE_FOLDER, source: PromptFileSource.ClaudePersonal, storage: PromptsStorage.user }, + { path: COPILOT_USER_AGENTS_SOURCE_FOLDER, source: PromptFileSource.CopilotPersonal, storage: PromptsStorage.user }, ]; /** @@ -204,7 +210,7 @@ export const DEFAULT_HOOK_FILE_PATHS: readonly IPromptSourceFolder[] = [ */ function isInAgentsFolder(fileUri: URI): boolean { const dir = dirname(fileUri.path); - return dir.endsWith('/' + AGENTS_SOURCE_FOLDER) || dir.endsWith('/' + CLAUDE_AGENTS_SOURCE_FOLDER); + return dir.endsWith('/' + AGENTS_SOURCE_FOLDER) || dir.endsWith('/' + CLAUDE_AGENTS_SOURCE_FOLDER) || isInCopilotAgentsFolder(fileUri); } /** @@ -215,6 +221,14 @@ export function isInClaudeAgentsFolder(fileUri: URI): boolean { return dir.endsWith('/' + CLAUDE_AGENTS_SOURCE_FOLDER); } +/** + * Helper function to check if a file is directly in the ~/.copilot/agents/ folder. + */ +export function isInCopilotAgentsFolder(fileUri: URI): boolean { + const dir = dirname(fileUri.path); + return dir.endsWith(COPILOT_USER_AGENTS_SOURCE_FOLDER.substring(1)); +} + /** * Helper function to check if a file is inside the .claude/rules/ folder (including subfolders). * Claude rules files (.md) in this folder are treated as instruction files. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index a6058e2c47ec7..7d05dd9ff61fd 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -23,10 +23,9 @@ import { ResourceMap } from '../../../../../../base/common/map.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; import { IPromptsService, Target } from '../service/promptsService.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; -import { AGENTS_SOURCE_FOLDER, CLAUDE_AGENTS_SOURCE_FOLDER, isInClaudeRulesFolder, LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; +import { AGENTS_SOURCE_FOLDER, isInClaudeAgentsFolder, isInClaudeRulesFolder, isInCopilotAgentsFolder, LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { dirname } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; export const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; @@ -1023,10 +1022,12 @@ export function isVSCodeOrDefaultTarget(target: Target): boolean { export function getTarget(promptType: PromptsType, header: PromptHeader | URI): Target { const uri = header instanceof URI ? header : header.uri; if (promptType === PromptsType.agent) { - const parentDir = dirname(uri); - if (parentDir.path.endsWith(`/${CLAUDE_AGENTS_SOURCE_FOLDER}`)) { + if (isInClaudeAgentsFolder(uri)) { return Target.Claude; } + if (isInCopilotAgentsFolder(uri)) { + return Target.GitHubCopilot; + } if (!(header instanceof URI)) { const target = header.target; if (target === Target.GitHubCopilot || target === Target.VSCode) { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/promptFileLocations.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/promptFileLocations.test.ts index f6d859e9f2736..f7e22613dee3b 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/promptFileLocations.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/promptFileLocations.test.ts @@ -48,6 +48,36 @@ suite('promptFileLocations', function () { assert.strictEqual(getPromptFileType(uri), undefined); }); + test('.md files in .claude/agents/ subfolder should NOT be recognized as agent files', () => { + const uri = URI.file('/workspace/.claude/agents/subfolder/test.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + + test('.md files in ~/.copilot/agents/ subfolder should NOT be recognized as agent files', () => { + const uri = URI.file('/home/user/.copilot/agents/subfolder/test.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + + test('.md files in .claude/agents/ folder should be recognized as agent files', () => { + const uri = URI.file('/workspace/.claude/agents/demonstrate.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.agent); + }); + + test('README.md in .claude/agents/ should NOT be recognized as agent file', () => { + const uri = URI.file('/workspace/.claude/agents/README.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + + test('.md files in ~/.copilot/agents/ folder should be recognized as agent files', () => { + const uri = URI.file('/home/user/.copilot/agents/my-agent.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.agent); + }); + + test('README.md in ~/.copilot/agents/ should NOT be recognized as agent file', () => { + const uri = URI.file('/home/user/.copilot/agents/README.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + test('.md files outside .github/agents/ should not be recognized as agent files', () => { const uri = URI.file('/workspace/test/foo.md'); assert.strictEqual(getPromptFileType(uri), undefined); @@ -130,6 +160,16 @@ suite('promptFileLocations', function () { assert.strictEqual(getCleanPromptName(uri), 'demonstrate'); }); + test('removes .md extension for files in .claude/agents/', () => { + const uri = URI.file('/workspace/.claude/agents/claude-agent.md'); + assert.strictEqual(getCleanPromptName(uri), 'claude-agent'); + }); + + test('removes .md extension for files in ~/.copilot/agents/', () => { + const uri = URI.file('/home/user/.copilot/agents/my-agent.md'); + assert.strictEqual(getCleanPromptName(uri), 'my-agent'); + }); + test('README.md in .github/agents/ should keep .md extension', () => { const uri = URI.file('/workspace/.github/agents/README.md'); assert.strictEqual(getCleanPromptName(uri), 'README.md'); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 90e6605a00dd3..0f3e39807dbe9 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -1177,6 +1177,56 @@ suite('PromptsService', () => { ); }); + test('copilot user agents from ~/.copilot/agents/ should have GitHubCopilot target', async () => { + const rootFolderName = 'copilot-user-agents'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + // Copilot user agent in ~/.copilot/agents/ (resolved from /home/user/.copilot/agents/) + path: '/home/user/.copilot/agents/copilot-user-agent.md', + contents: [ + '---', + 'description: \'Copilot user agent from home folder.\'', + 'tools: [ read ]', + '---', + 'I am a Copilot user agent.', + ] + }, + ]); + + const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const expected: ICustomAgent[] = [ + { + name: 'copilot-user-agent', + description: 'Copilot user agent from home folder.', + target: Target.GitHubCopilot, + tools: ['read'], + agentInstructions: { + content: 'I am a Copilot user agent.', + toolReferences: [], + metadata: undefined + }, + handOffs: undefined, + model: undefined, + argumentHint: undefined, + visibility: { userInvocable: true, agentInvocable: true }, + agents: undefined, + uri: URI.file('/home/user/.copilot/agents/copilot-user-agent.md'), + source: { storage: PromptsStorage.user } + }, + ]; + + assert.deepEqual( + result, + expected, + 'Agents from ~/.copilot/agents/ must have Target.GitHubCopilot.', + ); + }); + test('agents with .md extension should be recognized, except README.md', async () => { const rootFolderName = 'custom-agents-md-extension'; const rootFolder = `/${rootFolderName}`;