2026-07-21 18:42:25 +08:00
|
|
|
import { Fragment } from "react";
|
2026-08-03 16:03:31 +08:00
|
|
|
import { useTranslation } from "react-i18next";
|
2026-07-21 18:42:25 +08:00
|
|
|
|
|
|
|
|
import {
|
2026-08-04 00:38:40 +08:00
|
|
|
CapabilityMentionToken,
|
2026-08-18 09:53:55 +08:00
|
|
|
SessionReferenceToken,
|
2026-07-21 18:42:25 +08:00
|
|
|
splitCapabilityMentionSegments,
|
2026-08-18 09:53:55 +08:00
|
|
|
splitSessionReferenceSegments,
|
2026-07-21 18:42:25 +08:00
|
|
|
type CapabilityMentionSegment,
|
2026-08-18 09:53:55 +08:00
|
|
|
type SessionReferenceSegment,
|
2026-07-21 18:42:25 +08:00
|
|
|
} from "@/components/CliAppMentionText";
|
2026-07-21 18:29:57 +08:00
|
|
|
import {
|
|
|
|
|
INLINE_TOKEN_HIGHLIGHT_COLOR,
|
|
|
|
|
InlineTokenHighlight,
|
|
|
|
|
} from "@/components/InlineTokenHighlight";
|
2026-08-18 09:53:55 +08:00
|
|
|
import type {
|
|
|
|
|
CliAppInfo,
|
|
|
|
|
McpPresetInfo,
|
|
|
|
|
SessionHandle,
|
|
|
|
|
SessionMention,
|
|
|
|
|
UICliAppAttachment,
|
|
|
|
|
UIMcpPresetAttachment,
|
|
|
|
|
} from "@/lib/types";
|
2026-07-21 18:29:57 +08:00
|
|
|
|
|
|
|
|
type SkillReferenceSegment =
|
|
|
|
|
| { kind: "text"; text: string }
|
2026-07-21 18:42:25 +08:00
|
|
|
| { kind: "skill"; text: string; name: string };
|
2026-07-21 18:29:57 +08:00
|
|
|
|
2026-07-21 18:42:25 +08:00
|
|
|
type UserMessageSegment =
|
|
|
|
|
| CapabilityMentionSegment
|
2026-08-18 09:53:55 +08:00
|
|
|
| SessionReferenceSegment
|
2026-07-21 18:42:25 +08:00
|
|
|
| { kind: "skill"; text: string; name: string };
|
2026-07-21 18:29:57 +08:00
|
|
|
|
2026-07-21 18:42:25 +08:00
|
|
|
function splitSkillReferenceSegments(value: string): SkillReferenceSegment[] {
|
|
|
|
|
if (!value) return [];
|
2026-07-21 18:29:57 +08:00
|
|
|
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] ?? "";
|
|
|
|
|
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),
|
2026-07-21 18:42:25 +08:00
|
|
|
name,
|
2026-07-21 18:29:57 +08:00
|
|
|
});
|
|
|
|
|
cursor = referenceRe.lastIndex;
|
|
|
|
|
}
|
|
|
|
|
if (cursor < value.length) {
|
|
|
|
|
segments.push({ kind: "text", text: value.slice(cursor) });
|
|
|
|
|
}
|
|
|
|
|
return segments.length ? segments : [{ kind: "text", text: value }];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 18:42:25 +08:00
|
|
|
function splitUserMessageSegments(
|
|
|
|
|
value: string,
|
|
|
|
|
cliApps: CliAppInfo[],
|
|
|
|
|
mcpPresets: McpPresetInfo[],
|
2026-08-02 22:04:59 +08:00
|
|
|
sessionMentions: SessionMention[],
|
2026-08-18 09:53:55 +08:00
|
|
|
sessionHandles: SessionHandle[],
|
|
|
|
|
attachedCliApps: UICliAppAttachment[],
|
|
|
|
|
attachedMcpPresets: UIMcpPresetAttachment[],
|
2026-07-21 18:42:25 +08:00
|
|
|
): UserMessageSegment[] {
|
|
|
|
|
const segments: UserMessageSegment[] = [];
|
2026-08-18 09:53:55 +08:00
|
|
|
const structuredAtNamespaces = new Map<string, "handle" | "cli" | "mcp">();
|
|
|
|
|
sessionHandles.forEach((handle) => {
|
|
|
|
|
structuredAtNamespaces.set(handle.name.toLowerCase(), "handle");
|
|
|
|
|
});
|
|
|
|
|
attachedCliApps.forEach((app) => {
|
|
|
|
|
const name = app.name.toLowerCase();
|
|
|
|
|
if (!structuredAtNamespaces.has(name)) structuredAtNamespaces.set(name, "cli");
|
|
|
|
|
});
|
|
|
|
|
attachedMcpPresets.forEach((preset) => {
|
|
|
|
|
const name = preset.name.toLowerCase();
|
|
|
|
|
if (!structuredAtNamespaces.has(name)) structuredAtNamespaces.set(name, "mcp");
|
|
|
|
|
});
|
|
|
|
|
const structuredAtNames = new Set(structuredAtNamespaces.keys());
|
|
|
|
|
const replayCliApps = cliApps.filter((app) => {
|
|
|
|
|
const owner = structuredAtNamespaces.get(app.name.toLowerCase());
|
|
|
|
|
return owner === undefined || owner === "cli";
|
|
|
|
|
});
|
|
|
|
|
const replayMcpPresets = mcpPresets.filter((preset) => {
|
|
|
|
|
const owner = structuredAtNamespaces.get(preset.name.toLowerCase());
|
|
|
|
|
return owner === undefined || owner === "mcp";
|
|
|
|
|
});
|
|
|
|
|
const replaySessionHandles = sessionHandles.filter((handle) => (
|
|
|
|
|
structuredAtNamespaces.get(handle.name.toLowerCase()) === "handle"
|
|
|
|
|
));
|
|
|
|
|
const hashSegments = splitSessionReferenceSegments(value, sessionMentions);
|
|
|
|
|
const hashSessionKeys = new Set(hashSegments.flatMap((segment) => (
|
|
|
|
|
segment.kind === "session" ? [segment.mention.session_key] : []
|
|
|
|
|
)));
|
|
|
|
|
const legacySessionMentions = sessionMentions.filter((mention) => (
|
|
|
|
|
!hashSessionKeys.has(mention.session_key)
|
|
|
|
|
&& !structuredAtNames.has(mention.name.toLowerCase())
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
const appendCapabilitiesAndSkills = (text: string) => {
|
|
|
|
|
for (const capability of splitCapabilityMentionSegments(
|
|
|
|
|
text,
|
|
|
|
|
replayCliApps,
|
|
|
|
|
replayMcpPresets,
|
|
|
|
|
replaySessionHandles,
|
|
|
|
|
)) {
|
|
|
|
|
if (capability.kind === "text") {
|
|
|
|
|
segments.push(...splitSkillReferenceSegments(capability.text));
|
|
|
|
|
} else {
|
|
|
|
|
segments.push(capability);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const hashSegment of hashSegments) {
|
|
|
|
|
if (hashSegment.kind === "session") {
|
|
|
|
|
segments.push(hashSegment);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
for (const legacySegment of splitSessionReferenceSegments(
|
|
|
|
|
hashSegment.text,
|
|
|
|
|
legacySessionMentions,
|
|
|
|
|
undefined,
|
|
|
|
|
true,
|
|
|
|
|
)) {
|
|
|
|
|
if (legacySegment.kind === "session") {
|
|
|
|
|
segments.push(legacySegment);
|
|
|
|
|
} else {
|
|
|
|
|
appendCapabilitiesAndSkills(legacySegment.text);
|
|
|
|
|
}
|
2026-07-21 18:42:25 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return segments;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 18:29:57 +08:00
|
|
|
export function UserMessageText({
|
|
|
|
|
text,
|
|
|
|
|
cliApps,
|
|
|
|
|
mcpPresets,
|
2026-08-02 22:04:59 +08:00
|
|
|
sessionMentions = [],
|
2026-08-18 09:53:55 +08:00
|
|
|
sessionHandles = [],
|
|
|
|
|
attachedCliApps = [],
|
|
|
|
|
attachedMcpPresets = [],
|
2026-07-21 18:29:57 +08:00
|
|
|
}: {
|
|
|
|
|
text: string;
|
|
|
|
|
cliApps: CliAppInfo[];
|
|
|
|
|
mcpPresets: McpPresetInfo[];
|
2026-08-02 22:04:59 +08:00
|
|
|
sessionMentions?: SessionMention[];
|
2026-08-18 09:53:55 +08:00
|
|
|
sessionHandles?: SessionHandle[];
|
|
|
|
|
attachedCliApps?: UICliAppAttachment[];
|
|
|
|
|
attachedMcpPresets?: UIMcpPresetAttachment[];
|
2026-07-21 18:29:57 +08:00
|
|
|
}) {
|
2026-08-03 16:03:31 +08:00
|
|
|
const { t } = useTranslation();
|
2026-08-18 09:53:55 +08:00
|
|
|
const segments = splitUserMessageSegments(
|
|
|
|
|
text,
|
|
|
|
|
cliApps,
|
|
|
|
|
mcpPresets,
|
|
|
|
|
sessionMentions,
|
|
|
|
|
sessionHandles,
|
|
|
|
|
attachedCliApps,
|
|
|
|
|
attachedMcpPresets,
|
|
|
|
|
);
|
2026-07-21 18:29:57 +08:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{segments.map((segment, index) => {
|
|
|
|
|
if (segment.kind === "text") {
|
2026-07-21 18:42:25 +08:00
|
|
|
return <Fragment key={`text-${index}`}>{segment.text}</Fragment>;
|
2026-07-21 18:29:57 +08:00
|
|
|
}
|
2026-07-21 18:42:25 +08:00
|
|
|
if (segment.kind === "skill") return (
|
2026-07-21 18:29:57 +08:00
|
|
|
<InlineTokenHighlight
|
2026-07-21 18:42:25 +08:00
|
|
|
key={`skill-${segment.name}-${index}`}
|
|
|
|
|
testId={`message-skill-reference-${segment.name.toLowerCase()}`}
|
2026-08-03 16:03:31 +08:00
|
|
|
title={t("message.skill", { name: segment.name })}
|
2026-07-21 18:29:57 +08:00
|
|
|
color={INLINE_TOKEN_HIGHLIGHT_COLOR}
|
|
|
|
|
>
|
2026-08-04 16:40:41 +08:00
|
|
|
{segment.name}
|
2026-07-21 18:29:57 +08:00
|
|
|
</InlineTokenHighlight>
|
|
|
|
|
);
|
2026-08-18 09:53:55 +08:00
|
|
|
if (segment.kind === "session") return (
|
|
|
|
|
<SessionReferenceToken
|
|
|
|
|
key={`session-${segment.mention.session_key}-${index}`}
|
|
|
|
|
mention={segment.mention}
|
|
|
|
|
label={segment.text}
|
|
|
|
|
variant="message"
|
|
|
|
|
/>
|
|
|
|
|
);
|
2026-08-02 22:04:59 +08:00
|
|
|
return (
|
2026-08-04 00:38:40 +08:00
|
|
|
<CapabilityMentionToken
|
|
|
|
|
key={`${segment.kind}-${index}`}
|
|
|
|
|
segment={segment}
|
2026-08-02 22:04:59 +08:00
|
|
|
variant="message"
|
|
|
|
|
/>
|
|
|
|
|
);
|
2026-07-21 18:29:57 +08:00
|
|
|
})}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|