feat(vscode): expose /skills as slash command with secondary picker (#2548)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run

* feat(vscode): expose /skills as slash command with secondary picker

Add a secondary completion picker for the /skills slash command in the
VSCode IDE companion, allowing users to browse and select skills from
a dropdown before sending.

Changes:
- CLI: add 'skills' to ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE whitelist
- CLI: send available_skills_update via ACP with skill names/descriptions
- Extension: handle available_skills_update in session update handler
- Webview: implement secondary picker that triggers after selecting /skills
- Webview: allow spaces in completion trigger for /skills sub-queries

Closes #1562

Made-with: Cursor

* feat(vscode-ide-companion): embed skills in commands update metadata

- Move available skills from separate session update to _meta field of
  available_commands_update for more efficient delivery
- Simplify skill data to just skill names (string array)
- Add skillsCompletion utility for secondary picker logic
- Cache available skills in WebViewProvider for replay on webview ready
- Update all related types and handlers to support the new structure

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(vscode-ide-companion): simplify skills picker flow

* refactor(vscode-ide-companion): extract skills completion utils to shared module

Move `isSkillsSecondaryQuery`, `shouldOpenSkillsSecondaryPicker`, and
`SKILL_ITEM_ID_PREFIX` from App.tsx and useCompletionTrigger.ts into a
shared `completionUtils.ts` file to eliminate duplication.

* fix(vscode-ide-companion): restore skills picker state on reload

Cache and replay available skills when the webview becomes ready again.

Clear stale skills when commands metadata does not include availableSkills.

* fix(vscode-ide-companion): replay slash commands after webview reload

Cache available commands in the webview provider.

Replay them on webviewReady so slash command state survives reloads.

* fix(vscode-ide-companion): import AvailableCommand from ACP SDK

* fix(vscode-ide-companion): fallback /skills to direct command

* test(vscode-ide-companion): cover skills secondary picker flow

* test(vscode-ide-companion): guard App mock initialization

* fix(vscode-ide-companion): remove duplicate AvailableCommand import

The auto-merge introduced a duplicate AvailableCommand in the
@agentclientprotocol/sdk import block, causing TS2300.

* fix(vscode-ide-companion): remove duplicate availableCommands replay in handleWebviewReady

The handleWebviewReady method was sending cachedAvailableCommands twice
on every webview-ready handshake, causing an unnecessary extra state
update in the webview.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
易良 2026-04-24 23:28:53 +08:00 committed by GitHub
parent 3a2ee4ac1d
commit 202be6ec7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1006 additions and 98 deletions

View file

@ -246,6 +246,43 @@ describe('Session', () => {
});
});
it('attaches available skills to available_commands_update metadata', async () => {
getAvailableCommandsSpy.mockResolvedValueOnce([
{
name: 'init',
description: 'Initialize project context',
},
]);
mockConfig.getSkillManager = vi.fn().mockReturnValue({
listSkills: vi
.fn()
.mockResolvedValue([
{ name: 'code-review-expert' },
{ name: 'verification-pack' },
]),
});
await session.sendAvailableCommandsUpdate();
expect(mockClient.sessionUpdate).toHaveBeenCalledTimes(1);
expect(mockClient.sessionUpdate).toHaveBeenCalledWith({
sessionId: 'test-session-id',
update: {
sessionUpdate: 'available_commands_update',
availableCommands: [
{
name: 'init',
description: 'Initialize project context',
input: null,
},
],
_meta: {
availableSkills: ['code-review-expert', 'verification-pack'],
},
},
});
});
it('swallows errors and does not throw', async () => {
getAvailableCommandsSpy.mockRejectedValueOnce(
new Error('Command discovery failed'),

View file

@ -985,9 +985,27 @@ export class Session implements SessionContext {
}),
);
let availableSkills: string[] | undefined;
try {
const skillManager = this.config.getSkillManager();
if (skillManager) {
const skills = await skillManager.listSkills();
availableSkills = skills.map((skill) => skill.name);
}
} catch (error) {
debugLogger.error('Error loading available skills:', error);
}
const update: SessionUpdate = {
sessionUpdate: 'available_commands_update',
availableCommands,
...(availableSkills
? {
_meta: {
availableSkills,
},
}
: {}),
};
await this.sendUpdate(update);

View file

@ -24,7 +24,7 @@ export const skillsCommand: SlashCommand = {
return t('List available skills.');
},
kind: CommandKind.BUILT_IN,
supportedModes: ['interactive'] as const,
supportedModes: ['interactive', 'acp'] as const,
action: async (context: CommandContext, args?: string) => {
const rawArgs = args?.trim() ?? '';
const [skillName = ''] = rawArgs.split(/\s+/);