diff --git a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts index 4d5cbf8eff6..67bfb2e955f 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts @@ -510,7 +510,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements }; const commands = observeComponent('commands', d => this._readMarkdownComponents(d)); - const skills = observeComponent('skills', d => this._readSkills(d)); + const skills = observeComponent('skills', d => this._readSkills(uri, d)); const agents = observeComponent('agents', d => this._readMarkdownComponents(d)); const instructions = observeComponent('rules', d => this._readRules(d)); const hooks = observeComponent( @@ -699,7 +699,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements } } - private async _readSkills(dirs: readonly URI[]): Promise { + private async _readSkills(pluginRoot: URI, dirs: readonly URI[]): Promise { const seen = new Set(); const skills: IAgentPluginSkill[] = []; @@ -738,6 +738,14 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements } } + // Fallback: support single-skill plugins with SKILL.md at the plugin root + if (skills.length === 0) { + const rootSkillMd = URI.joinPath(pluginRoot, 'SKILL.md'); + if (await this._pathExists(rootSkillMd)) { + addSkill(basename(pluginRoot), rootSkillMd); + } + } + skills.sort((a, b) => a.name.localeCompare(b.name)); return skills; } diff --git a/src/vs/workbench/contrib/chat/test/common/plugins/agentPluginFormatDetection.test.ts b/src/vs/workbench/contrib/chat/test/common/plugins/agentPluginFormatDetection.test.ts index 9e79f04f26b..52623081371 100644 --- a/src/vs/workbench/contrib/chat/test/common/plugins/agentPluginFormatDetection.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/plugins/agentPluginFormatDetection.test.ts @@ -242,6 +242,45 @@ suite('AgentPlugin format detection', () => { assert.deepStrictEqual(skillNames, ['deploy', 'lint']); })); + test('reads root-level SKILL.md as a fallback skill', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const uri = pluginUri('/plugins/root-skill'); + await writeFile('/plugins/root-skill/.plugin/plugin.json', JSON.stringify({ name: 'root-skill' })); + await writeFile('/plugins/root-skill/SKILL.md', '# Visual Explainer'); + + const discovery = createDiscovery(); + discovery.start(mockEnablementModel); + await discovery.setSourcesAndRefresh([uri]); + + const plugins = discovery.plugins.get(); + assert.strictEqual(plugins.length, 1); + + await waitForState(plugins[0].skills, s => s.length > 0); + assert.deepStrictEqual( + plugins[0].skills.get().map(s => s.name), + ['root-skill'], + ); + })); + + test('root-level SKILL.md is ignored when skills/ has content', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const uri = pluginUri('/plugins/root-skill-ignored'); + await writeFile('/plugins/root-skill-ignored/.plugin/plugin.json', JSON.stringify({ name: 'root-skill-ignored' })); + await writeFile('/plugins/root-skill-ignored/SKILL.md', '# Root skill'); + await writeFile('/plugins/root-skill-ignored/skills/real/SKILL.md', '# Real skill'); + + const discovery = createDiscovery(); + discovery.start(mockEnablementModel); + await discovery.setSourcesAndRefresh([uri]); + + const plugins = discovery.plugins.get(); + assert.strictEqual(plugins.length, 1); + + await waitForState(plugins[0].skills, s => s.length > 0); + assert.deepStrictEqual( + plugins[0].skills.get().map(s => s.name), + ['real'], + ); + })); + test('reads agents from agents/ directory', () => runWithFakedTimers({ useFakeTimers: true }, async () => { const uri = pluginUri('/plugins/agents-plugin'); await writeFile('/plugins/agents-plugin/.plugin/plugin.json', JSON.stringify({ name: 'agents-plugin' }));