Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -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;
});
}

/**
Expand Down Expand Up @@ -1532,6 +1539,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>();

// Collect all skills with their metadata for sorting
Expand Down Expand Up @@ -1566,6 +1574,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,65 @@ 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);

// 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));

const pluginPath = `${rootFolder}/my-plugin`;
const skillUri = URI.file(`${pluginPath}/skills/sdd-init/SKILL.md`);

// '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, [
{
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);

// 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);
});

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