mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-25 16:46:17 +00:00
feat(web): render slash-command skill activations as a labeled chip
A user turn whose origin metadata marks it as a skill_activation (trigger user-slash) no longer dumps the raw XML block into the chat: the turn shows only the user-provided args plus an "activated skill" label with the skill name.
This commit is contained in:
parent
719ee4ac76
commit
890db95567
6 changed files with 90 additions and 6 deletions
|
|
@ -278,8 +278,16 @@ function turnBlocks(turn: ChatTurn): TurnBlock[] {
|
|||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<!-- Skill activation card (replaces raw XML) -->
|
||||
<div v-if="turn.skillActivation" class="skill-act">
|
||||
<div class="skill-act-head">
|
||||
<span class="skill-act-arrow">▶</span>
|
||||
<span>{{ t('conversation.activatedSkill', { name: turn.skillActivation.name }) }}</span>
|
||||
</div>
|
||||
<div v-if="turn.skillActivation.args" class="skill-act-args">{{ turn.skillActivation.args }}</div>
|
||||
</div>
|
||||
<!-- User input renders verbatim (pre-wrap), never through Markdown -->
|
||||
<div class="u-text">{{ turn.text }}</div>
|
||||
<div v-else class="u-text">{{ turn.text }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Compaction divider — prior turns stay untouched; summary opens in
|
||||
|
|
@ -400,7 +408,16 @@ function turnBlocks(turn: ChatTurn): TurnBlock[] {
|
|||
</div>
|
||||
|
||||
<!-- User input renders verbatim (pre-wrap), never through Markdown -->
|
||||
<div v-if="turn.role === 'user'" class="u-text">{{ turn.text }}</div>
|
||||
<div v-if="turn.role === 'user'" class="u-text">
|
||||
<div v-if="turn.skillActivation" class="skill-act">
|
||||
<div class="skill-act-head">
|
||||
<span class="skill-act-arrow">▶</span>
|
||||
<span>{{ t('conversation.activatedSkill', { name: turn.skillActivation.name }) }}</span>
|
||||
</div>
|
||||
<div v-if="turn.skillActivation.args" class="skill-act-args">{{ turn.skillActivation.args }}</div>
|
||||
</div>
|
||||
<template v-else>{{ turn.text }}</template>
|
||||
</div>
|
||||
|
||||
<!-- Thinking + message text + tool cards, interleaved in original call order. -->
|
||||
<template v-else>
|
||||
|
|
@ -730,6 +747,32 @@ function turnBlocks(turn: ChatTurn): TurnBlock[] {
|
|||
padding-top: 2px;
|
||||
}
|
||||
|
||||
/* Skill activation card (replaces raw <kimi-skill-loaded> XML) */
|
||||
.skill-act {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.skill-act-head {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--blue2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.skill-act-arrow {
|
||||
color: var(--blue);
|
||||
font-size: 11px;
|
||||
}
|
||||
.skill-act-args {
|
||||
font-size: 12.5px;
|
||||
color: var(--muted);
|
||||
padding-left: 17px;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Mobile font bump (+2px) */
|
||||
@media (max-width: 640px) {
|
||||
.u-bub .u-text,
|
||||
|
|
|
|||
|
|
@ -454,10 +454,25 @@ export function messagesToTurns(
|
|||
// Hide system-injected user turns (TUI parity) — they end the previous
|
||||
// assistant turn but aren't rendered as a user bubble.
|
||||
if (!isDisplayableUserMessage(msg)) continue;
|
||||
|
||||
const origin = msg.metadata?.['origin'] as
|
||||
| { kind?: string; skillName?: string; skillArgs?: string; trigger?: string }
|
||||
| undefined;
|
||||
const isSkillActivation =
|
||||
origin?.kind === 'skill_activation' && origin?.trigger === 'user-slash';
|
||||
|
||||
const textParts: string[] = [];
|
||||
const images: { url: string; alt?: string }[] = [];
|
||||
for (const c of msg.content) {
|
||||
if (c.type === 'text') textParts.push(c.text);
|
||||
if (c.type === 'text') {
|
||||
if (isSkillActivation) {
|
||||
// Skill activation messages carry the raw XML block; we strip it and
|
||||
// surface only the user-provided args as the "user input" text.
|
||||
textParts.push(origin.skillArgs ?? '');
|
||||
} else {
|
||||
textParts.push(c.text);
|
||||
}
|
||||
}
|
||||
const url = resolveImageUrl(c);
|
||||
if (url) images.push({ url, alt: c.type === 'file' ? c.name : undefined });
|
||||
}
|
||||
|
|
@ -467,6 +482,9 @@ export function messagesToTurns(
|
|||
no: no++,
|
||||
text: textParts.join('\n'),
|
||||
images: images.length > 0 ? images : undefined,
|
||||
skillActivation: isSkillActivation
|
||||
? { name: origin.skillName!, args: origin.skillArgs }
|
||||
: undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ export default {
|
|||
viewSummary: 'View summary',
|
||||
summaryTitle: 'Compaction summary',
|
||||
manuallyAborted: 'Manually stopped',
|
||||
activatedSkill: 'Activated skill: {name}',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ export default {
|
|||
viewSummary: '查看摘要',
|
||||
summaryTitle: '压缩摘要',
|
||||
manuallyAborted: '您已手动终止',
|
||||
activatedSkill: '已激活技能: {name}',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -144,6 +144,9 @@ export interface ChatTurn {
|
|||
prior turns and renders this as a separator line; `text` holds the
|
||||
LLM-generated summary, opened in the right-side panel on click. */
|
||||
compaction?: { trigger?: 'manual' | 'auto'; tokensBefore?: number; tokensAfter?: number };
|
||||
/** Skill activation metadata: when a user turn was triggered by a slash
|
||||
command (/skill), this holds the skill name and args for display. */
|
||||
skillActivation?: { name: string; args?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -36,13 +36,31 @@ describe('user message origin filtering (TUI parity)', () => {
|
|||
it('shows user-typed slash commands, hides model/nested skill activations', () => {
|
||||
expect(
|
||||
shownTexts([
|
||||
userMsg('/compact', { kind: 'skill_activation', trigger: 'user-slash' }),
|
||||
userMsg('skill body', { kind: 'skill_activation', trigger: 'model-tool' }),
|
||||
userMsg('nested', { kind: 'skill_activation', trigger: 'nested-skill' }),
|
||||
userMsg('body', { kind: 'skill_activation', trigger: 'user-slash', skillName: 'compact', skillArgs: '/compact' }),
|
||||
userMsg('skill body', { kind: 'skill_activation', trigger: 'model-tool', skillName: 'review' }),
|
||||
userMsg('nested', { kind: 'skill_activation', trigger: 'nested-skill', skillName: 'brainstorm' }),
|
||||
]),
|
||||
).toEqual(['/compact']);
|
||||
});
|
||||
|
||||
it('strips XML body and surfaces skillActivation metadata for slash skills', () => {
|
||||
const turns = messagesToTurns(
|
||||
[
|
||||
userMsg('User activated the skill "review". Follow the loaded skill instructions.\n\n<kimi-skill-loaded name="review" trigger="user-slash" source="project" args="src/app.ts">\nbody\n</kimi-skill-loaded>', {
|
||||
kind: 'skill_activation',
|
||||
trigger: 'user-slash',
|
||||
skillName: 'review',
|
||||
skillArgs: 'src/app.ts',
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
expect(turns).toHaveLength(1);
|
||||
expect(turns[0]!.role).toBe('user');
|
||||
expect(turns[0]!.text).toBe('src/app.ts');
|
||||
expect(turns[0]!.skillActivation).toEqual({ name: 'review', args: 'src/app.ts' });
|
||||
});
|
||||
|
||||
it.each([
|
||||
['compaction_summary'],
|
||||
['injection'],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue