Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,7 @@ export class PromptsService extends Disposable implements IPromptsService {
private async computeSkillDiscoveryInfo(token: CancellationToken): Promise<IPromptFileDiscoveryResult[]> {
const files: IPromptFileDiscoveryResult[] = [];
const seenNames = new Set<string>();
const seenUris = new ResourceSet();
const nameToUri = new Map<string, URI>();
Comment on lines 1539 to 1543
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This URI de-duplication is added in computeSkillDiscoveryInfo, which is used by findAgentSkills()/cachedSkills. However, the slash command list is built via getPromptSlashCommands()computeSlashCommandDiscoveryInfo()listPromptFiles(PromptsType.skill), and that path still merges file-locator + plugin skills without URI de-duplication. If the user-facing duplication was in slash commands (per PR description), this change likely won’t address it; consider adding URI de-duplication in computeListPromptFiles (for PromptsType.skill) or in computeSlashCommandDiscoveryInfo, and update tests accordingly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 88f9527. Added URI deduplication (ResourceSet) to computeListPromptFiles — the function that feeds both computeSlashCommandDiscoveryInfo and all other listPromptFiles callers. Also fixed the test: the previous version used an absolute path for SKILLS_LOCATION_KEY which is rejected by isValidPromptFolderPath, so the file locator never actually found the skill. The updated test uses a workspace-relative path, causing the file locator to discover the same URI the plugin registers — and both findAgentSkills and getPromptSlashCommands now assert exactly 1 result.


// Collect all skills with their metadata for sorting
Expand Down Expand Up @@ -1566,6 +1567,11 @@ export class PromptsService extends Disposable implements IPromptsService {
const uri = skill.uri;
const promptPath = skill;

if (seenUris.has(uri)) {
continue;
}
seenUris.add(uri);

try {
const parsedFile = await this.parseNew(uri, token);
const folderName = getSkillFolderName(uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<readonly IAgentPluginSkill[]>('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, {});
Expand Down
Loading