mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat(web): prefix skill slash commands with skill: to distinguish them from built-in commands (#1492)
This commit is contained in:
parent
0cc9831a2f
commit
b0809ddac8
5 changed files with 54 additions and 15 deletions
|
|
@ -39,6 +39,7 @@ import ServerAuthDialog from './components/ServerAuthDialog.vue';
|
|||
import { initServerAuth, onAuthRequired } from './api/daemon/serverAuth';
|
||||
import type { AppConfig, ThinkingLevel } from './api/types';
|
||||
import { coerceThinkingForModel, commitLevel, segmentsFor } from './lib/modelThinking';
|
||||
import { stripSkillPrefix } from './lib/slashCommands';
|
||||
import Button from './components/ui/Button.vue';
|
||||
import IconButton from './components/ui/IconButton.vue';
|
||||
import Icon from './components/ui/Icon.vue';
|
||||
|
|
@ -506,13 +507,15 @@ function handleCommand(cmd: string): void {
|
|||
break;
|
||||
default: {
|
||||
// Not a built-in command → treat it as a session skill activation
|
||||
// (the user picked `/<skill>` from the menu, or typed `/<skill> args`).
|
||||
// The daemon answers an unknown name with skill.not_found, surfaced as a
|
||||
// warning, so a stray slash is harmless. With no active session, create
|
||||
// one first (same path as the first prompt) so the activation isn't
|
||||
// silently dropped on the new-session screen.
|
||||
// (the user picked `/skill:<skill>` from the menu, or typed
|
||||
// `/<skill> args`). Strip the `skill:` display prefix — the REST API
|
||||
// takes the bare skill name. The daemon answers an unknown name with
|
||||
// skill.not_found, surfaced as a warning, so a stray slash is harmless.
|
||||
// With no active session, create one first (same path as the first
|
||||
// prompt) so the activation isn't silently dropped on the new-session
|
||||
// screen.
|
||||
const space = cmd.indexOf(' ');
|
||||
const name = (space === -1 ? cmd : cmd.slice(0, space)).slice(1);
|
||||
const name = stripSkillPrefix((space === -1 ? cmd : cmd.slice(0, space)).slice(1));
|
||||
const args = space === -1 ? undefined : cmd.slice(space + 1).trim() || undefined;
|
||||
if (!name) break;
|
||||
if (!client.activeSessionId.value && client.activeWorkspaceId.value) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
|||
import { useI18n } from 'vue-i18n';
|
||||
import SlashMenu from './SlashMenu.vue';
|
||||
import MentionMenu from './MentionMenu.vue';
|
||||
import { buildSlashItems, parseSlash } from '../../lib/slashCommands';
|
||||
import { buildSlashItems, parseSlash, SKILL_COMMAND_PREFIX } from '../../lib/slashCommands';
|
||||
import type { FileItem } from './MentionMenu.vue';
|
||||
import type { ActivationBadges, ConversationStatus, PermissionMode, QueuedPromptView } from '../../types';
|
||||
import type { AppModel, AppSkill, ThinkingLevel } from '../../api/types';
|
||||
|
|
@ -304,11 +304,14 @@ function handleSubmit(): void {
|
|||
// If it's a known slash command, keep the optional tail as command input
|
||||
// instead of submitting it as normal chat text. This covers `/goal <task>`,
|
||||
// `/swarm <task>`, `/btw <question>`, slash skills with args, and bare
|
||||
// commands such as `/model`.
|
||||
// commands such as `/model`. A hand-typed bare skill name (`/deploy`) also
|
||||
// resolves to its prefixed menu entry (`/skill:deploy`), mirroring the TUI.
|
||||
if (trimmed) {
|
||||
const parsed = parseSlash(trimmed);
|
||||
const known = parsed
|
||||
? buildSlashItems(props.skills).some((item) => item.name === parsed.cmd)
|
||||
? buildSlashItems(props.skills).some(
|
||||
(item) => item.name === parsed.cmd || item.name === `/${SKILL_COMMAND_PREFIX}${parsed.cmd.slice(1)}`,
|
||||
)
|
||||
: false;
|
||||
if (parsed && known) {
|
||||
text.value = '';
|
||||
|
|
|
|||
|
|
@ -65,16 +65,30 @@ export function parseSlash(input: string): { cmd: string; arg: string } | null {
|
|||
};
|
||||
}
|
||||
|
||||
/** The prefix marking a slash item as a skill activation (`/skill:<name>`). */
|
||||
export const SKILL_COMMAND_PREFIX = 'skill:';
|
||||
|
||||
/**
|
||||
* Strip the `skill:` prefix from a slash-command name (with or without the
|
||||
* leading `/`), returning the bare skill name. Non-prefixed input is returned
|
||||
* unchanged.
|
||||
*/
|
||||
export function stripSkillPrefix(name: string): string {
|
||||
return name.startsWith(SKILL_COMMAND_PREFIX) ? name.slice(SKILL_COMMAND_PREFIX.length) : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full slash-item list: built-in commands followed by the session's
|
||||
* skills (each shown as `/<skill-name>`). Skills carry their raw description and
|
||||
* skills. Non-builtin skills are shown as `/skill:<skill-name>` so the user can
|
||||
* tell them apart from built-in commands (mirroring the TUI); builtin-sourced
|
||||
* skills keep the bare `/<skill-name>`. Skills carry their raw description and
|
||||
* an `isSkill` flag so the caller knows to activate rather than run a command.
|
||||
*/
|
||||
export function buildSlashItems(
|
||||
skills: ReadonlyArray<{ name: string; description: string }> = [],
|
||||
skills: ReadonlyArray<{ name: string; description: string; source?: string }> = [],
|
||||
): SlashCommand[] {
|
||||
const skillItems: SlashCommand[] = skills.map((s) => ({
|
||||
name: `/${s.name}`,
|
||||
name: s.source === 'builtin' ? `/${s.name}` : `/${SKILL_COMMAND_PREFIX}${s.name}`,
|
||||
desc: s.description,
|
||||
isSkill: true,
|
||||
// Keep the selected skill in the composer so arguments can be appended.
|
||||
|
|
|
|||
|
|
@ -74,11 +74,25 @@ describe('useSlashMenu — update', () => {
|
|||
expect(slash.open.value).toBe(false);
|
||||
});
|
||||
|
||||
it('includes session skills as /<skill-name>', () => {
|
||||
const { slash } = setup('/', [{ name: 'deploy', description: 'deploy stuff' } as AppSkill]);
|
||||
it('includes session skills as /skill:<skill-name>', () => {
|
||||
const { slash } = setup('/', [{ name: 'deploy', description: 'deploy stuff', source: 'project' } as AppSkill]);
|
||||
slash.update();
|
||||
const names = slash.items.value.map((i) => i.name);
|
||||
expect(names).toContain('/deploy');
|
||||
expect(names).toContain('/skill:deploy');
|
||||
});
|
||||
|
||||
it('keeps builtin-sourced skills unprefixed', () => {
|
||||
const { slash } = setup('/', [{ name: 'update-config', description: 'edit config', source: 'builtin' } as AppSkill]);
|
||||
slash.update();
|
||||
const names = slash.items.value.map((i) => i.name);
|
||||
expect(names).toContain('/update-config');
|
||||
expect(names).not.toContain('/skill:update-config');
|
||||
});
|
||||
|
||||
it('matches a prefixed skill when filtering by its bare name', () => {
|
||||
const { slash } = setup('/depl', [{ name: 'deploy', description: 'deploy stuff', source: 'project' } as AppSkill]);
|
||||
slash.update();
|
||||
expect(slash.items.value.map((i) => i.name)).toContain('/skill:deploy');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue