From 19e3a29e71ca09f9a76c7b785057f36d4c9a3b21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:08:01 +0000 Subject: [PATCH 1/4] Initial plan From 1ee5b6fdf6b1d74395df7457f1b6f7cbee31b96c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:17:03 +0000 Subject: [PATCH 2/4] fix: deduplicate skills by URI in computeSkillDiscoveryInfo When 'Agent Skills Location' overlaps with 'Plugin Locations', the same skill URI can appear in both discoveredSkills and pluginSkills sources. Add URI-based deduplication (seenUris Set) before processing each skill to prevent the same file from being included multiple times. Also add a test verifying that skills are not duplicated when the same URI appears via both SKILLS_LOCATION_KEY and plugin sources. Agent-Logs-Url: https://github.com/microsoft/vscode/sessions/93df8c49-237a-434e-938e-5ba4e004cfd9 Co-authored-by: connor4312 <2230985+connor4312@users.noreply.github.com> --- .../service/promptsServiceImpl.ts | 8 +++ .../service/promptsService.test.ts | 49 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 10f09d2e435bd..6a3b305811082 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -1532,6 +1532,7 @@ export class PromptsService extends Disposable implements IPromptsService { private async computeSkillDiscoveryInfo(token: CancellationToken): Promise { const files: IPromptFileDiscoveryResult[] = []; const seenNames = new Set(); + const seenUris = new Set(); const nameToUri = new Map(); // Collect all skills with their metadata for sorting @@ -1566,6 +1567,13 @@ export class PromptsService extends Disposable implements IPromptsService { const uri = skill.uri; const promptPath = skill; + const uriKey = uri.toString(); + if (seenUris.has(uriKey)) { + this.logger.debug(`[computeSkillDiscoveryInfo] Skipping duplicate agent skill URI: ${uri}`); + continue; + } + seenUris.add(uriKey); + try { const parsedFile = await this.parseNew(uri, token); const folderName = getSkillFolderName(uri); 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 44dda985f4900..515568452cbf5 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 @@ -2927,6 +2927,55 @@ suite('PromptsService', () => { registered.dispose(); }); + test('should deduplicate skills by URI when Agent Skills Location overlaps with Plugin Locations', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + + const pluginPath = '/plugins/my-plugin'; + const skillUri = URI.file(`${pluginPath}/skills/sdd-init/SKILL.md`); + + // Configure SKILLS_LOCATION_KEY to point to the plugin's skills folder (overlapping with plugin location) + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { [`${pluginPath}/skills`]: true }); + + workspaceContextService.setWorkspace(testWorkspace(URI.file('/workspace'))); + + await mockFiles(fileService, [ + { + path: skillUri.path, + contents: [ + '---', + 'name: "sdd-init"', + 'description: "Initialize Spec-Driven Development"', + '---', + 'Skill content', + ], + }, + ]); + + const enablement = observableValue('testPluginEnablement', 2 /* ContributionEnablementState.EnabledProfile */); + const plugin: IAgentPlugin = { + uri: URI.file(pluginPath), + label: 'my-plugin', + enablement, + remove: () => { }, + hooks: observableValue('testPluginHooks', []), + commands: observableValue('testPluginCommands', []), + skills: observableValue('testPluginSkills', [{ uri: skillUri, name: 'sdd-init' }]), + agents: observableValue('testPluginAgents', []), + instructions: observableValue('testPluginInstructions', []), + mcpServerDefinitions: observableValue('testPluginMcpServerDefinitions', []), + }; + + testPluginsObservable.set([plugin], undefined); + + const allResult = await service.findAgentSkills(CancellationToken.None); + + assert.ok(allResult, 'Should return results'); + assert.strictEqual(allResult.length, 1, 'Should find exactly 1 skill (not duplicated) when Agent Skills Location overlaps with Plugin Locations'); + assert.strictEqual(allResult[0].name, 'sdd-init'); + + testPluginsObservable.set([], undefined); + }); + test('should include contributed skill files in findAgentSkills', async () => { testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); From 86c2a151bc2a560fd0712965b5ab36a3b60c5c11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:32:53 +0000 Subject: [PATCH 3/4] fix: use ResourceSet for URI deduplication and drop debug log Agent-Logs-Url: https://github.com/microsoft/vscode/sessions/790cd72e-c8ea-4c81-80c1-e203e1a29ab8 Co-authored-by: connor4312 <2230985+connor4312@users.noreply.github.com> --- .../common/promptSyntax/service/promptsServiceImpl.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 6a3b305811082..10a0daf8820de 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -1532,7 +1532,7 @@ export class PromptsService extends Disposable implements IPromptsService { private async computeSkillDiscoveryInfo(token: CancellationToken): Promise { const files: IPromptFileDiscoveryResult[] = []; const seenNames = new Set(); - const seenUris = new Set(); + const seenUris = new ResourceSet(); const nameToUri = new Map(); // Collect all skills with their metadata for sorting @@ -1567,12 +1567,10 @@ export class PromptsService extends Disposable implements IPromptsService { const uri = skill.uri; const promptPath = skill; - const uriKey = uri.toString(); - if (seenUris.has(uriKey)) { - this.logger.debug(`[computeSkillDiscoveryInfo] Skipping duplicate agent skill URI: ${uri}`); + if (seenUris.has(uri)) { continue; } - seenUris.add(uriKey); + seenUris.add(uri); try { const parsedFile = await this.parseNew(uri, token); From 88f9527e467e1653cf113f0c731cc5ce6696daa0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:55:56 +0000 Subject: [PATCH 4/4] fix: deduplicate URIs in computeListPromptFiles and fix test setup Agent-Logs-Url: https://github.com/microsoft/vscode/sessions/71dad59b-e7d7-4655-8141-a34b5072d905 Co-authored-by: connor4312 <2230985+connor4312@users.noreply.github.com> --- .../service/promptsServiceImpl.ts | 9 +++++++- .../service/promptsService.test.ts | 22 ++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 10a0daf8820de..aed42571afcc5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -366,7 +366,14 @@ export class PromptsService extends Disposable implements IPromptsService { this._pluginPromptFilesByType.get(type) ?? [], ]); - return prompts.flat(); + const seen = new ResourceSet(); + return prompts.flat().filter(p => { + if (seen.has(p.uri)) { + return false; + } + seen.add(p.uri); + return true; + }); } /** 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 515568452cbf5..b82846072b3b8 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 @@ -2930,13 +2930,18 @@ suite('PromptsService', () => { test('should deduplicate skills by URI when Agent Skills Location overlaps with Plugin Locations', async () => { testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); - const pluginPath = '/plugins/my-plugin'; - const skillUri = URI.file(`${pluginPath}/skills/sdd-init/SKILL.md`); + // Workspace contains the plugin, so a relative SKILLS_LOCATION_KEY path + // resolves to the same directory the plugin registers its skills from. + const rootFolder = '/workspace'; + const rootFolderUri = URI.file(rootFolder); + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - // Configure SKILLS_LOCATION_KEY to point to the plugin's skills folder (overlapping with plugin location) - testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { [`${pluginPath}/skills`]: true }); + const pluginPath = `${rootFolder}/my-plugin`; + const skillUri = URI.file(`${pluginPath}/skills/sdd-init/SKILL.md`); - workspaceContextService.setWorkspace(testWorkspace(URI.file('/workspace'))); + // 'my-plugin/skills' is a workspace-relative path that resolves to the + // same directory as the plugin's skill URIs → overlap scenario. + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { 'my-plugin/skills': true }); await mockFiles(fileService, [ { @@ -2967,12 +2972,17 @@ suite('PromptsService', () => { testPluginsObservable.set([plugin], undefined); + // Check deduplication via findAgentSkills const allResult = await service.findAgentSkills(CancellationToken.None); - assert.ok(allResult, 'Should return results'); assert.strictEqual(allResult.length, 1, 'Should find exactly 1 skill (not duplicated) when Agent Skills Location overlaps with Plugin Locations'); assert.strictEqual(allResult[0].name, 'sdd-init'); + // Check deduplication via getPromptSlashCommands (the user-facing slash command list) + const slashCommands = await service.getPromptSlashCommands(CancellationToken.None); + const skillCommands = slashCommands.filter(cmd => cmd.type === PromptsType.skill && cmd.name === 'sdd-init'); + assert.strictEqual(skillCommands.length, 1, 'Should have exactly 1 slash command for the skill (not duplicated) when Agent Skills Location overlaps with Plugin Locations'); + testPluginsObservable.set([], undefined); });