feat(webui): highlight skill references in sent messages

This commit is contained in:
chengyongru
2026-07-21 22:56:04 +08:00
committed by chengyongru
parent 89d8c055a8
commit 79b89f4f4c
7 changed files with 216 additions and 6 deletions
+8 -3
View File
@@ -18,10 +18,10 @@ import {
import { useTranslation } from "react-i18next";
import { AttachmentTile } from "@/components/AttachmentTile";
import { CliAppMentionText } from "@/components/CliAppMentionText";
import { ImageLightbox } from "@/components/ImageLightbox";
import { MarkdownText, preloadMarkdownText } from "@/components/MarkdownText";
import { SlashCommandText } from "@/components/SlashCommandText";
import { UserMessageText } from "@/components/UserMessageText";
import {
Tooltip,
TooltipContent,
@@ -37,6 +37,7 @@ import type {
CliAppInfo,
McpPresetInfo,
SlashCommand,
SkillSummary,
UICliAppAttachment,
UIMcpPresetAttachment,
UIImage,
@@ -51,6 +52,7 @@ interface MessageBubbleProps {
cliApps?: CliAppInfo[];
mcpPresets?: McpPresetInfo[];
slashCommands?: SlashCommand[];
skills?: SkillSummary[];
onOpenFilePreview?: (path: string) => void;
onForkFromHere?: () => void;
}
@@ -143,6 +145,7 @@ export function MessageBubble({
cliApps = [],
mcpPresets = [],
slashCommands = [],
skills = [],
onOpenFilePreview,
onForkFromHere,
}: MessageBubbleProps) {
@@ -171,15 +174,17 @@ export function MessageBubble({
const messageText = slashCommand ? (
<>
<SlashCommandText command={slashCommand.command} />
<CliAppMentionText
<UserMessageText
text={message.content.slice(slashCommand.command.length)}
skills={skills}
cliApps={mentionCliApps}
mcpPresets={mentionMcpPresets}
/>
</>
) : (
<CliAppMentionText
<UserMessageText
text={message.content}
skills={skills}
cliApps={mentionCliApps}
mcpPresets={mentionMcpPresets}
/>
+91
View File
@@ -0,0 +1,91 @@
import { CliAppMentionText } from "@/components/CliAppMentionText";
import {
INLINE_TOKEN_HIGHLIGHT_COLOR,
InlineTokenHighlight,
} from "@/components/InlineTokenHighlight";
import type { CliAppInfo, McpPresetInfo, SkillSummary } from "@/lib/types";
type SkillReferenceSegment =
| { kind: "text"; text: string }
| { kind: "skill"; text: string; skill: SkillSummary };
function splitSkillReferenceSegments(
value: string,
skills: SkillSummary[],
): SkillReferenceSegment[] {
if (!value || skills.length === 0) {
return value ? [{ kind: "text", text: value }] : [];
}
const skillsByName = new Map(
skills
.filter((skill) => skill.available)
.map((skill) => [skill.name.toLowerCase(), skill]),
);
if (skillsByName.size === 0) return [{ kind: "text", text: value }];
const segments: SkillReferenceSegment[] = [];
const referenceRe = /\$([A-Za-z0-9_-]+)/g;
let cursor = 0;
let match: RegExpExecArray | null;
while ((match = referenceRe.exec(value)) !== null) {
const name = match[1] ?? "";
const skill = skillsByName.get(name.toLowerCase());
if (!skill) continue;
if (match.index > cursor) {
segments.push({ kind: "text", text: value.slice(cursor, match.index) });
}
segments.push({
kind: "skill",
text: value.slice(match.index, referenceRe.lastIndex),
skill,
});
cursor = referenceRe.lastIndex;
}
if (cursor < value.length) {
segments.push({ kind: "text", text: value.slice(cursor) });
}
return segments.length ? segments : [{ kind: "text", text: value }];
}
export function UserMessageText({
text,
skills,
cliApps,
mcpPresets,
}: {
text: string;
skills: SkillSummary[];
cliApps: CliAppInfo[];
mcpPresets: McpPresetInfo[];
}) {
const segments = splitSkillReferenceSegments(text, skills);
return (
<>
{segments.map((segment, index) => {
if (segment.kind === "text") {
return (
<CliAppMentionText
key={`text-${index}`}
text={segment.text}
cliApps={cliApps}
mcpPresets={mcpPresets}
/>
);
}
return (
<InlineTokenHighlight
key={`skill-${segment.skill.name}-${index}`}
testId={`message-skill-reference-${segment.skill.name}`}
title={`Skill: ${segment.skill.name}`}
color={INLINE_TOKEN_HIGHLIGHT_COLOR}
className="font-medium"
>
{segment.text}
</InlineTokenHighlight>
);
})}
</>
);
}
+10 -1
View File
@@ -3,7 +3,13 @@ import { useTranslation } from "react-i18next";
import { MessageBubble } from "@/components/MessageBubble";
import { AgentActivityCluster } from "@/components/thread/AgentActivityCluster";
import { normalizeActivityTimeline, type TurnUnit } from "@/lib/activity-timeline";
import type { CliAppInfo, McpPresetInfo, SlashCommand, UIMessage } from "@/lib/types";
import type {
CliAppInfo,
McpPresetInfo,
SlashCommand,
SkillSummary,
UIMessage,
} from "@/lib/types";
interface ThreadMessagesProps {
messages: UIMessage[];
@@ -13,6 +19,7 @@ interface ThreadMessagesProps {
cliApps?: CliAppInfo[];
mcpPresets?: McpPresetInfo[];
slashCommands?: SlashCommand[];
skills?: SkillSummary[];
forkBoundaryMessageCount?: number | null;
onOpenFilePreview?: (path: string) => void;
onForkFromMessage?: (beforeUserIndex: number) => void;
@@ -53,6 +60,7 @@ export function ThreadMessages({
cliApps = [],
mcpPresets = [],
slashCommands = [],
skills = [],
forkBoundaryMessageCount = null,
onOpenFilePreview,
onForkFromMessage,
@@ -115,6 +123,7 @@ export function ThreadMessages({
cliApps={cliApps}
mcpPresets={mcpPresets}
slashCommands={slashCommands}
skills={skills}
onOpenFilePreview={onOpenFilePreview}
onForkFromHere={
onForkFromMessage && forkIndex !== undefined
@@ -897,6 +897,7 @@ export function ThreadShell({
cliApps={cliApps}
mcpPresets={mcpPresets}
slashCommands={slashCommands}
skills={skills}
forkBoundaryMessageCount={forkBoundaryMessageCount}
hasMoreBefore={hasMoreBefore}
loadingOlder={loadingOlder}
+10 -1
View File
@@ -22,7 +22,13 @@ import {
promptTop,
} from "@/components/thread/promptNavigation";
import { cn } from "@/lib/utils";
import type { CliAppInfo, McpPresetInfo, SlashCommand, UIMessage } from "@/lib/types";
import type {
CliAppInfo,
McpPresetInfo,
SlashCommand,
SkillSummary,
UIMessage,
} from "@/lib/types";
export interface ThreadViewportHandle {
jumpToUserPrompt: (promptId: string) => void;
@@ -41,6 +47,7 @@ interface ThreadViewportProps {
cliApps?: CliAppInfo[];
mcpPresets?: McpPresetInfo[];
slashCommands?: SlashCommand[];
skills?: SkillSummary[];
forkBoundaryMessageCount?: number | null;
hasMoreBefore?: boolean;
loadingOlder?: boolean;
@@ -113,6 +120,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
cliApps = [],
mcpPresets = [],
slashCommands = [],
skills = [],
forkBoundaryMessageCount = null,
hasMoreBefore = false,
loadingOlder = false,
@@ -529,6 +537,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
cliApps={cliApps}
mcpPresets={mcpPresets}
slashCommands={slashCommands}
skills={skills}
forkBoundaryMessageCount={visibleForkBoundaryMessageCount}
onOpenFilePreview={onOpenFilePreview}
onForkFromMessage={onForkFromMessage}