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
5 changes: 5 additions & 0 deletions .changeset/add-skills-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add `/skills` slash command to display available skills in a grouped table.
7 changes: 7 additions & 0 deletions apps/kimi-code/src/tui/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ export const BUILTIN_SLASH_COMMANDS = [
priority: 60,
availability: 'always',
},
{
name: 'skills',
aliases: [],
description: 'Show available skills',
priority: 60,
availability: 'always',
},
{
name: 'compact',
aliases: [],
Expand Down
94 changes: 94 additions & 0 deletions apps/kimi-code/src/tui/components/messages/skills-panel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { SkillSummary } from '@moonshot-ai/kimi-code-sdk';
import chalk from 'chalk';

import type { ColorPalette } from '#/tui/theme/colors';

export interface SkillsReportOptions {
readonly colors: ColorPalette;
readonly skills: readonly SkillSummary[];
}

type Source = SkillSummary['source'];

const SOURCE_ORDER: readonly Source[] = ['builtin', 'project', 'user', 'extra'];

const SOURCE_LABEL: Record<Source, string> = {
builtin: 'Built-in',
project: 'Project',
user: 'User',
extra: 'Extra',
};

const SOURCE_PRIORITY: Record<Source, number> = {
builtin: 0,
project: 1,
user: 2,
extra: 3,
};

function sortSkills(skills: readonly SkillSummary[]): SkillSummary[] {
return skills.toSorted(
(a, b) =>
SOURCE_PRIORITY[a.source] - SOURCE_PRIORITY[b.source] ||
a.name.localeCompare(b.name),
);
}

function groupBySource(
skills: readonly SkillSummary[],
): Map<Source, SkillSummary[]> {
const groups = new Map<Source, SkillSummary[]>();
for (const skill of skills) {
const list = groups.get(skill.source) ?? [];
list.push(skill);
groups.set(skill.source, list);
}
return groups;
}

function buildSkillLines(
skills: readonly SkillSummary[],
value: (text: string) => string,
dim: (text: string) => string,
nameWidth: number,
typeWidth: number,
): string[] {
return skills.map((skill) => {
const name = skill.name.length > nameWidth ? skill.name.slice(0, nameWidth - 1) + '…' : skill.name;
const type = (skill.type ?? '-').padEnd(typeWidth).slice(0, typeWidth);
const desc = skill.description || '-';
return ` ${value(name.padEnd(nameWidth))} ${dim(type)} ${value(desc)}`;
});
}

export function buildSkillsReportLines(options: SkillsReportOptions): string[] {
const { colors, skills } = options;
const accent = chalk.hex(colors.primary).bold;
const value = chalk.hex(colors.text);
const muted = chalk.hex(colors.textDim);
const dim = chalk.hex(colors.textDim);

if (skills.length === 0) {
return [muted(' No skills available.')];
}

const sorted = sortSkills(skills);
const groups = groupBySource(sorted);

const nameWidth = Math.min(24, Math.max('Name'.length, ...skills.map((s) => s.name.length)));
const typeWidth = Math.min(12, Math.max('Type'.length, ...skills.map((s) => (s.type ?? '-').length)));

const lines: string[] = [
` ${muted('Name'.padEnd(nameWidth))} ${muted('Type'.padEnd(typeWidth))} ${muted('Description')}`,
];

for (const source of SOURCE_ORDER) {
const group = groups.get(source);
if (group === undefined || group.length === 0) continue;

lines.push(` ${accent(SOURCE_LABEL[source])}`);
lines.push(...buildSkillLines(group, value, dim, nameWidth, typeWidth));
}

return lines;
}
25 changes: 25 additions & 0 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import type {
Session,
SessionMetaUpdatedEvent,
SessionStatus,
SkillSummary,
SessionUsage,
SkillActivatedEvent,
SubagentCompletedEvent,
Expand Down Expand Up @@ -156,6 +157,7 @@ import { AgentGroupComponent } from './components/messages/agent-group';
import { AssistantMessageComponent } from './components/messages/assistant-message';
import { BackgroundAgentStatusComponent } from './components/messages/background-agent-status';
import { buildMcpStatusReportLines } from './components/messages/mcp-status-panel';
import { buildSkillsReportLines } from './components/messages/skills-panel';
import { ReadGroupComponent } from './components/messages/read-group';
import { SkillActivationComponent } from './components/messages/skill-activation';
import {
Expand Down Expand Up @@ -1411,6 +1413,9 @@ export class KimiTUI {
case 'mcp':
void this.showMcpServers();
return;
case 'skills':
void this.showSkills();
return;
case 'editor':
await this.handleEditorCommand(args, {});
return;
Expand Down Expand Up @@ -4754,6 +4759,26 @@ export class KimiTUI {
this.state.ui.requestRender();
}

// Loads and renders available skills.
private async showSkills(): Promise<void> {
let skills: readonly SkillSummary[];
try {
skills = await this.requireSession().listSkills();
} catch (error) {
this.showError(`Failed to load skills: ${formatErrorMessage(error)}`);
return;
}

const lines = buildSkillsReportLines({
colors: this.state.theme.colors,
skills,
});
const title = skills.length > 0 ? ` Skills (${skills.length}) ` : ' Skills ';
const panel = new UsagePanelComponent(lines, this.state.theme.colors.primary, title);
this.state.transcriptContainer.addChild(panel);
this.state.ui.requestRender();
}

// Loads per-session usage and captures displayable errors.
private async loadSessionUsageReport(): Promise<SessionUsageResult> {
try {
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/tui/commands/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ describe('built-in slash command registry', () => {
'plan',
'sessions',
'settings',
'skills',
'status',
'theme',
'title',
Expand Down
5 changes: 5 additions & 0 deletions apps/kimi-code/test/tui/commands/resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ describe('resolveSlashCommandInput', () => {
name: 'mcp',
args: '',
});
expect(resolve('/skills', { isStreaming: true })).toMatchObject({
kind: 'builtin',
name: 'skills',
args: '',
});
});

it('blocks plan clear while compacting because it is idle-only', () => {
Expand Down
73 changes: 73 additions & 0 deletions apps/kimi-code/test/tui/components/messages/skills-panel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';

import { buildSkillsReportLines } from '#/tui/components/messages/skills-panel';
import { darkColors } from '#/tui/theme/colors';

function strip(text: string): string {
return text.replaceAll(/\u001B\[[0-9;]*m/g, '');
}

const mockSkills = [
{ name: 'gen-changesets', description: 'Generate changesets', path: '/a', source: 'builtin' as const, type: 'inline' },
{ name: 'gen-docs', description: 'Update docs after code changes', path: '/b', source: 'builtin' as const, type: 'inline' },
{ name: 'my-project-skill', description: 'Custom project workflow', path: '/c', source: 'project' as const, type: 'flow' },
{ name: 'user-snippet', description: 'Personal snippet', path: '/d', source: 'user' as const },
{ name: 'extra-helper', description: 'Extra helper skill', path: '/e', source: 'extra' as const, type: 'prompt' },
];

describe('skills panel report lines', () => {
it('renders a grouped table with aligned columns', () => {
const lines = buildSkillsReportLines({
colors: darkColors,
skills: mockSkills,
}).map(strip);

const output = lines.join('\n');

// Global header
expect(output).toContain('Name');
expect(output).toContain('Type');
expect(output).toContain('Description');

// Group labels
expect(output).toContain('Built-in');
expect(output).toContain('Project');
expect(output).toContain('User');
expect(output).toContain('Extra');

// Skill rows
expect(output).toContain('gen-changesets');
expect(output).toContain('gen-docs');
expect(output).toContain('my-project-skill');
expect(output).toContain('user-snippet');
expect(output).toContain('extra-helper');

// Empty groups should be omitted — verify only one Project row exists
const projectMatches = output.split('Project').length - 1;
expect(projectMatches).toBe(1);
});

it('renders empty state when no skills are available', () => {
const lines = buildSkillsReportLines({
colors: darkColors,
skills: [],
}).map(strip);

expect(lines).toHaveLength(1);
expect(lines[0]).toContain('No skills available');
});

it('skips empty source groups', () => {
const lines = buildSkillsReportLines({
colors: darkColors,
skills: [{ name: 'only-one', description: 'Only skill', path: '/x', source: 'builtin' as const }],
}).map(strip);

const output = lines.join('\n');
expect(output).toContain('Built-in');
expect(output).toContain('only-one');
expect(output).not.toContain('Project');
expect(output).not.toContain('User');
expect(output).not.toContain('Extra');
});
});
1 change: 1 addition & 0 deletions docs/en/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Some commands are only available in the idle state. Running them while the sessi
| `/usage` | — | Show token usage, context consumption, and quota information. | Yes |
| `/status` | — | Show the current session runtime status, including version, model, working directory, and permission mode. | Yes |
| `/mcp` | — | List the MCP servers in the current session and their connection status. | Yes |
| `/skills` | — | Show all available skills grouped by source (built-in, project, user, extra). | Yes |
| `/version` | — | Show the Kimi Code CLI version number. | Yes |
| `/feedback` | — | Submit feedback to help improve Kimi Code CLI. | Yes |

Expand Down
1 change: 1 addition & 0 deletions docs/zh/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
| `/usage` | — | 显示 token 用量、上下文占用以及配额信息。 | 是 |
| `/status` | — | 显示当前会话运行时状态,包括版本、模型、工作目录和权限模式等。 | 是 |
| `/mcp` | — | 列出当前会话中的 MCP server 及其连接状态。 | 是 |
| `/skills` | — | 展示所有可用 Skill,按来源分组(内置、项目、用户、扩展)。 | 是 |
| `/version` | — | 显示 Kimi Code CLI 版本号。 | 是 |
| `/feedback` | — | 提交反馈以改进 Kimi Code CLI。 | 是 |

Expand Down