diff --git a/.changeset/builtin-skills-slash-commands.md b/.changeset/builtin-skills-slash-commands.md new file mode 100644 index 000000000..5c59c42e6 --- /dev/null +++ b/.changeset/builtin-skills-slash-commands.md @@ -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. diff --git a/apps/kimi-code/src/tui/commands/skills.ts b/apps/kimi-code/src/tui/commands/skills.ts index 08dfafc7e..8ddfbda0e 100644 --- a/apps/kimi-code/src/tui/commands/skills.ts +++ b/apps/kimi-code/src/tui/commands/skills.ts @@ -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(); - 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, diff --git a/apps/kimi-code/src/tui/components/dialogs/help-panel.ts b/apps/kimi-code/src/tui/components/dialogs/help-panel.ts index f096e11c4..9b219a0c4 100644 --- a/apps/kimi-code/src/tui/components/dialogs/help-panel.ts +++ b/apps/kimi-code/src/tui/components/dialogs/help-panel.ts @@ -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; +} diff --git a/apps/kimi-code/test/tui/commands/resolve.test.ts b/apps/kimi-code/test/tui/commands/resolve.test.ts index fb54f7d9d..b98619773 100644 --- a/apps/kimi-code/test/tui/commands/resolve.test.ts +++ b/apps/kimi-code/test/tui/commands/resolve.test.ts @@ -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', () => { diff --git a/apps/kimi-code/test/tui/commands/skills.test.ts b/apps/kimi-code/test/tui/commands/skills.test.ts index f2059be97..cdb5c8a51 100644 --- a/apps/kimi-code/test/tui/commands/skills.test.ts +++ b/apps/kimi-code/test/tui/commands/skills.test.ts @@ -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'); }); }); diff --git a/apps/kimi-code/test/tui/components/panels/help-panel.test.ts b/apps/kimi-code/test/tui/components/panels/help-panel.test.ts index 329932812..d9692366a 100644 --- a/apps/kimi-code/test/tui/components/panels/help-panel.test.ts +++ b/apps/kimi-code/test/tui/components/panels/help-panel.test.ts @@ -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', () => { diff --git a/docs/en/guides/interaction.md b/docs/en/guides/interaction.md index 3ee0e35f6..92907d2c5 100644 --- a/docs/en/guides/interaction.md +++ b/docs/en/guides/interaction.md @@ -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:`. If a skill name does not conflict with a built-in command, you can also drop the `skill:` prefix and type `/` directly. +Active [Agent Skills](../customization/skills.md) are automatically registered as slash commands: external Skills are invoked with `/skill:`, while built-in Skills appear directly as `/` 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 `/` 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). diff --git a/docs/en/reference/slash-commands.md b/docs/en/reference/slash-commands.md index 4f32251f6..ad55c7862 100644 --- a/docs/en/reference/slash-commands.md +++ b/docs/en/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: [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 — `/` — 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 — `/` — 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 `/` 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:` — there is no separate `/flow:` namespace. diff --git a/docs/zh/guides/interaction.md b/docs/zh/guides/interaction.md index 50692edd9..ca61d1f62 100644 --- a/docs/zh/guides/interaction.md +++ b/docs/zh/guides/interaction.md @@ -25,7 +25,7 @@ Kimi Code CLI 支持在输入框中直接粘贴图片和视频,让 AI 结合 以 `/` 开头的内容会被识别为斜杠命令。输入 `/` 后弹出补全菜单,随后续字符实时过滤;按 `Esc` 关闭菜单,匹配失败时内容会作为普通消息发送给 Agent。 -已激活的 [Agent Skills](../customization/skills.md) 会自动注册为斜杠命令,以 `/skill:` 调用;若 Skill 名称与内置命令不冲突,也可以省略 `skill:` 前缀直接输入 `/`。 +已激活的 [Agent Skills](../customization/skills.md) 会自动注册为斜杠命令:外部 Skill 以 `/skill:` 调用,内置 Skill 直接以 `/` 出现在斜杠命令面板中;若外部 Skill 名称与系统斜杠命令不冲突,也可以省略 `skill:` 前缀直接输入 `/`。 部分命令仅在 Agent 空闲时可用,流式输出或上下文压缩期间需先按 `Esc` 中断。`/yolo`、`/plan`、`/help`、`/btw` 等模式切换和查询类命令则始终可用。全部命令说明见[斜杠命令参考](../reference/slash-commands.md)。 diff --git a/docs/zh/reference/slash-commands.md b/docs/zh/reference/slash-commands.md index 030f1d341..b62e44d4f 100644 --- a/docs/zh/reference/slash-commands.md +++ b/docs/zh/reference/slash-commands.md @@ -122,7 +122,7 @@ Prompt 模式在目标完成时以退出码 `0` 退出,在目标阻塞时以 ` ## Skill 动态命令 -已激活的 Skill 会自动注册为斜杠命令,统一以 `skill:` 作为命名空间前缀: +已激活的外部 Skill 会自动注册为斜杠命令,并以 `skill:` 作为命名空间前缀: ``` /skill: [附加文本] @@ -130,9 +130,9 @@ Prompt 模式在目标完成时以退出码 `0` 退出,在目标阻塞时以 ` 例如 `/skill:code-style` 加载名为 `code-style` 的 Skill 并发送给 Agent;命令后附带的文本拼接到 Skill 提示词之后。 -为方便输入,Skill 命令同时支持省略 `skill:` 前缀的简写形式 `/`,前提是该名称未被内置命令占用——即 `/code-style` 会回退匹配到 `/skill:code-style`。 +为方便输入,外部 Skill 命令同时支持省略 `skill:` 前缀的简写形式 `/`,前提是该名称未被系统斜杠命令占用——即 `/code-style` 会回退匹配到 `/skill:code-style`。 -Kimi Code CLI 随包内置了 `mcp-config` Skill,用于配置 MCP server 和处理 MCP OAuth 登录;可直接输入 `/mcp-config` 调用。 +Kimi Code CLI 随包内置的 Skill(例如 `mcp-config`)会直接以 `/` 形式出现在斜杠命令面板中,用于配置 MCP server 和处理 MCP OAuth 登录等场景。 ::: info 说明 所有 Skill 命令仅在空闲状态下可用。`flow` 类型的 Skill 同样通过 `/skill:` 暴露,没有独立的 `/flow:` 命名空间。