mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat: prioritize built-in skill slash commands (#480)
This commit is contained in:
parent
2af19e29b9
commit
f555c89de7
10 changed files with 103 additions and 26 deletions
5
.changeset/builtin-skills-slash-commands.md
Normal file
5
.changeset/builtin-skills-slash-commands.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": minor
|
||||
---
|
||||
|
||||
Show built-in skills as direct slash commands and group them ahead of external skill commands.
|
||||
|
|
@ -18,10 +18,22 @@ export function isUserActivatableSkill(skill: SkillSummary): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function compareSkillSlashCommands(a: SkillSummary, b: SkillSummary): number {
|
||||
return (
|
||||
getSkillSlashCommandGroup(a.source) - getSkillSlashCommandGroup(b.source) ||
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
}
|
||||
|
||||
function getSkillSlashCommandGroup(source: SkillSummary['source']): number {
|
||||
return source === 'builtin' ? 0 : 1;
|
||||
}
|
||||
|
||||
export function buildSkillSlashCommands(skills: readonly SkillSummary[]): SkillSlashCommands {
|
||||
const commandMap = new Map<string, string>();
|
||||
const commands = skills.filter(isUserActivatableSkill).map((skill) => {
|
||||
const commandName = `skill:${skill.name}`;
|
||||
const sortedSkills = [...skills].toSorted(compareSkillSlashCommands);
|
||||
const commands = sortedSkills.filter(isUserActivatableSkill).map((skill) => {
|
||||
const commandName = skill.source === 'builtin' ? skill.name : `skill:${skill.name}`;
|
||||
commandMap.set(commandName, skill.name);
|
||||
return {
|
||||
name: commandName,
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export class HelpPanelComponent extends Container implements Focusable {
|
|||
|
||||
const shortcuts = this.opts.shortcuts ?? DEFAULT_KEYBOARD_SHORTCUTS;
|
||||
const kbdWidth = Math.max(8, ...shortcuts.map((s) => s.keys.length));
|
||||
const sortedCmds = [...this.opts.commands].toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
const sortedCmds = [...this.opts.commands].toSorted(compareSlashCommandsForDisplay);
|
||||
const cmdLabels = sortedCmds.map((c) => {
|
||||
const aliases = c.aliases.length > 0 ? ` (${c.aliases.map((a) => '/' + a).join(', ')})` : '';
|
||||
return `/${c.name}${aliases}`;
|
||||
|
|
@ -146,3 +146,14 @@ export class HelpPanelComponent extends Container implements Focusable {
|
|||
return lines.map((line) => truncateToWidth(line, width));
|
||||
}
|
||||
}
|
||||
|
||||
function compareSlashCommandsForDisplay(a: HelpPanelCommand, b: HelpPanelCommand): number {
|
||||
return (
|
||||
getSlashCommandDisplayGroup(a.name) - getSlashCommandDisplayGroup(b.name) ||
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
}
|
||||
|
||||
function getSlashCommandDisplayGroup(name: string): number {
|
||||
return name.startsWith('skill:') ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,6 +171,22 @@ describe('resolveSlashCommandInput', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('resolves unprefixed built-in skill commands and blocks them while busy', () => {
|
||||
const skillCommandMap = new Map([['mcp-config', 'mcp-config']]);
|
||||
|
||||
expect(resolve('/mcp-config', { skillCommandMap })).toEqual({
|
||||
kind: 'skill',
|
||||
commandName: 'mcp-config',
|
||||
skillName: 'mcp-config',
|
||||
args: '',
|
||||
});
|
||||
expect(resolve('/mcp-config', { skillCommandMap, isCompacting: true })).toEqual({
|
||||
kind: 'blocked',
|
||||
commandName: 'mcp-config',
|
||||
reason: 'compacting',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns message for unknown slash input', () => {
|
||||
expect(resolve('/does-not-exist arg')).toEqual({
|
||||
kind: 'message',
|
||||
|
|
@ -228,10 +244,14 @@ describe('goal command resolution', () => {
|
|||
|
||||
describe('slash command busy helpers', () => {
|
||||
it('resolves skill command aliases with and without skill prefix', () => {
|
||||
const map = new Map([['skill:review', 'review']]);
|
||||
const map = new Map([
|
||||
['skill:review', 'review'],
|
||||
['mcp-config', 'mcp-config'],
|
||||
]);
|
||||
|
||||
expect(resolveSkillCommand(map, 'skill:review')).toBe('review');
|
||||
expect(resolveSkillCommand(map, 'review')).toBe('review');
|
||||
expect(resolveSkillCommand(map, 'mcp-config')).toBe('mcp-config');
|
||||
});
|
||||
|
||||
it('formats busy messages', () => {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ describe('skill slash commands', () => {
|
|||
expect(isUserActivatableSkill(skill('agent', 'agent'))).toBe(false);
|
||||
});
|
||||
|
||||
it('builds slash commands and command map entries with skill prefixes', () => {
|
||||
it('builds slash commands and command map entries with skill prefixes for non-built-in skills', () => {
|
||||
const built = buildSkillSlashCommands([
|
||||
skill('review', 'prompt'),
|
||||
skill('nested-review', 'prompt', {
|
||||
|
|
@ -39,14 +39,14 @@ describe('skill slash commands', () => {
|
|||
]);
|
||||
|
||||
expect(built.commands.map((command) => command.name)).toEqual([
|
||||
'skill:review',
|
||||
'skill:nested-review',
|
||||
'skill:commit',
|
||||
'skill:nested-review',
|
||||
'skill:review',
|
||||
]);
|
||||
expect(built.commands[0]).toMatchObject({
|
||||
name: 'skill:review',
|
||||
name: 'skill:commit',
|
||||
aliases: [],
|
||||
description: 'review skill',
|
||||
description: 'commit skill',
|
||||
});
|
||||
expect(built.commands[1]).toMatchObject({
|
||||
name: 'skill:nested-review',
|
||||
|
|
@ -54,9 +54,31 @@ describe('skill slash commands', () => {
|
|||
description: 'Nested review skill',
|
||||
});
|
||||
expect([...built.commandMap.entries()]).toEqual([
|
||||
['skill:review', 'review'],
|
||||
['skill:nested-review', 'nested-review'],
|
||||
['skill:commit', 'commit'],
|
||||
['skill:nested-review', 'nested-review'],
|
||||
['skill:review', 'review'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts built-in skill slash commands before external skill commands', () => {
|
||||
const built = buildSkillSlashCommands([
|
||||
skill('zeta', 'prompt', { source: 'user' }),
|
||||
skill('alpha', 'prompt', { source: 'project' }),
|
||||
skill('update-config', 'inline', { source: 'builtin' }),
|
||||
skill('mcp-config', 'inline', { source: 'builtin' }),
|
||||
]);
|
||||
|
||||
expect(built.commands.map((command) => command.name)).toEqual([
|
||||
'mcp-config',
|
||||
'update-config',
|
||||
'skill:alpha',
|
||||
'skill:zeta',
|
||||
]);
|
||||
expect([...built.commandMap.entries()]).toEqual([
|
||||
['mcp-config', 'mcp-config'],
|
||||
['update-config', 'update-config'],
|
||||
['skill:alpha', 'alpha'],
|
||||
['skill:zeta', 'zeta'],
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -65,7 +87,7 @@ describe('skill slash commands', () => {
|
|||
skill('mcp-config', 'inline', { disableModelInvocation: true, source: 'builtin' }),
|
||||
]);
|
||||
|
||||
expect(built.commands.map((command) => command.name)).toEqual(['skill:mcp-config']);
|
||||
expect(built.commandMap.get('skill:mcp-config')).toBe('mcp-config');
|
||||
expect(built.commands.map((command) => command.name)).toEqual(['mcp-config']);
|
||||
expect(built.commandMap.get('mcp-config')).toBe('mcp-config');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,19 +34,26 @@ describe('HelpPanelComponent', () => {
|
|||
expect(out).toMatch(/Exit/);
|
||||
});
|
||||
|
||||
it('sorts slash commands by name', () => {
|
||||
it('sorts unprefixed commands before skill commands and by name within each group', () => {
|
||||
const panel = new HelpPanelComponent({
|
||||
commands: [cmd('zebra', 'Z'), cmd('alpha', 'A'), cmd('mango', 'M')],
|
||||
commands: [
|
||||
cmd('zebra', 'Z'),
|
||||
cmd('skill:bravo', 'B'),
|
||||
cmd('alpha', 'A'),
|
||||
cmd('mcp-config', 'M'),
|
||||
],
|
||||
colors: darkColors,
|
||||
onClose: () => {},
|
||||
});
|
||||
const out = strip(panel.render(80).join('\n'));
|
||||
const alphaIdx = out.indexOf('/alpha');
|
||||
const mangoIdx = out.indexOf('/mango');
|
||||
const mcpConfigIdx = out.indexOf('/mcp-config');
|
||||
const zebraIdx = out.indexOf('/zebra');
|
||||
const skillBravoIdx = out.indexOf('/skill:bravo');
|
||||
expect(alphaIdx).toBeGreaterThan(-1);
|
||||
expect(alphaIdx).toBeLessThan(mangoIdx);
|
||||
expect(mangoIdx).toBeLessThan(zebraIdx);
|
||||
expect(alphaIdx).toBeLessThan(mcpConfigIdx);
|
||||
expect(mcpConfigIdx).toBeLessThan(zebraIdx);
|
||||
expect(zebraIdx).toBeLessThan(skillBravoIdx);
|
||||
});
|
||||
|
||||
it('Escape fires onClose', () => {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ After pasting, the input box shows a placeholder that you can edit like normal t
|
|||
|
||||
Anything starting with `/` is treated as a slash command. Typing `/` opens a completion menu that filters in real time as you keep typing; press `Esc` to close the menu. If nothing matches, the input is sent to the agent as a regular message.
|
||||
|
||||
Active [Agent Skills](../customization/skills.md) are automatically registered as slash commands and invoked with `/skill:<name>`. If a skill name does not conflict with a built-in command, you can also drop the `skill:` prefix and type `/<name>` directly.
|
||||
Active [Agent Skills](../customization/skills.md) are automatically registered as slash commands: external Skills are invoked with `/skill:<name>`, while built-in Skills appear directly as `/<name>` in the slash command panel. If an external skill name does not conflict with a system slash command, you can also drop the `skill:` prefix and type `/<name>` directly.
|
||||
|
||||
Some commands are only available when the agent is idle — you need to press `Esc` to interrupt streaming output or context compression before using them. Mode-toggle and query commands like `/yolo`, `/plan`, `/help`, and `/btw` are always available. For the full list, see [Slash commands reference](../reference/slash-commands.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ Prompt mode exits with code `0` when the goal completes, `3` when it blocks, and
|
|||
|
||||
## Skill Dynamic Commands
|
||||
|
||||
Activated Skills are automatically registered as slash commands, all prefixed with the `skill:` namespace:
|
||||
Activated external Skills are automatically registered as slash commands with the `skill:` namespace prefix:
|
||||
|
||||
```
|
||||
/skill:<name> [extra text]
|
||||
|
|
@ -132,9 +132,9 @@ Activated Skills are automatically registered as slash commands, all prefixed wi
|
|||
|
||||
For example, `/skill:code-style` loads the Skill named `code-style` and sends it to the Agent; any text appended after the command is concatenated to the Skill prompt.
|
||||
|
||||
For convenience, Skill commands also support a shorthand form that omits the `skill:` prefix — `/<name>` — as long as the name is not taken by a built-in command. That is, `/code-style` falls back to matching `/skill:code-style`.
|
||||
For convenience, external Skill commands also support a shorthand form that omits the `skill:` prefix — `/<name>` — as long as the name is not taken by a system slash command. That is, `/code-style` falls back to matching `/skill:code-style`.
|
||||
|
||||
Kimi Code CLI ships a built-in `mcp-config` Skill for configuring MCP servers and handling MCP OAuth login; invoke it directly with `/mcp-config`.
|
||||
Built-in Skills shipped with Kimi Code CLI, such as `mcp-config`, appear directly as `/<name>` in the slash command panel for cases like configuring MCP servers and handling MCP OAuth login.
|
||||
|
||||
::: info
|
||||
All Skill commands are only available in the idle state. `flow`-type Skills are also exposed via `/skill:<name>` — there is no separate `/flow:` namespace.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Kimi Code CLI 支持在输入框中直接粘贴图片和视频,让 AI 结合
|
|||
|
||||
以 `/` 开头的内容会被识别为斜杠命令。输入 `/` 后弹出补全菜单,随后续字符实时过滤;按 `Esc` 关闭菜单,匹配失败时内容会作为普通消息发送给 Agent。
|
||||
|
||||
已激活的 [Agent Skills](../customization/skills.md) 会自动注册为斜杠命令,以 `/skill:<name>` 调用;若 Skill 名称与内置命令不冲突,也可以省略 `skill:` 前缀直接输入 `/<name>`。
|
||||
已激活的 [Agent Skills](../customization/skills.md) 会自动注册为斜杠命令:外部 Skill 以 `/skill:<name>` 调用,内置 Skill 直接以 `/<name>` 出现在斜杠命令面板中;若外部 Skill 名称与系统斜杠命令不冲突,也可以省略 `skill:` 前缀直接输入 `/<name>`。
|
||||
|
||||
部分命令仅在 Agent 空闲时可用,流式输出或上下文压缩期间需先按 `Esc` 中断。`/yolo`、`/plan`、`/help`、`/btw` 等模式切换和查询类命令则始终可用。全部命令说明见[斜杠命令参考](../reference/slash-commands.md)。
|
||||
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ Prompt 模式在目标完成时以退出码 `0` 退出,在目标阻塞时以 `
|
|||
|
||||
## Skill 动态命令
|
||||
|
||||
已激活的 Skill 会自动注册为斜杠命令,统一以 `skill:` 作为命名空间前缀:
|
||||
已激活的外部 Skill 会自动注册为斜杠命令,并以 `skill:` 作为命名空间前缀:
|
||||
|
||||
```
|
||||
/skill:<name> [附加文本]
|
||||
|
|
@ -130,9 +130,9 @@ Prompt 模式在目标完成时以退出码 `0` 退出,在目标阻塞时以 `
|
|||
|
||||
例如 `/skill:code-style` 加载名为 `code-style` 的 Skill 并发送给 Agent;命令后附带的文本拼接到 Skill 提示词之后。
|
||||
|
||||
为方便输入,Skill 命令同时支持省略 `skill:` 前缀的简写形式 `/<name>`,前提是该名称未被内置命令占用——即 `/code-style` 会回退匹配到 `/skill:code-style`。
|
||||
为方便输入,外部 Skill 命令同时支持省略 `skill:` 前缀的简写形式 `/<name>`,前提是该名称未被系统斜杠命令占用——即 `/code-style` 会回退匹配到 `/skill:code-style`。
|
||||
|
||||
Kimi Code CLI 随包内置了 `mcp-config` Skill,用于配置 MCP server 和处理 MCP OAuth 登录;可直接输入 `/mcp-config` 调用。
|
||||
Kimi Code CLI 随包内置的 Skill(例如 `mcp-config`)会直接以 `/<name>` 形式出现在斜杠命令面板中,用于配置 MCP server 和处理 MCP OAuth 登录等场景。
|
||||
|
||||
::: info 说明
|
||||
所有 Skill 命令仅在空闲状态下可用。`flow` 类型的 Skill 同样通过 `/skill:<name>` 暴露,没有独立的 `/flow:` 命名空间。
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue