authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-05-07 17:43:16-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-07 14:43:16-07:00
log11f40556de8a37c7f16486c23fb6bce058275557
tree2a9fa318406a0ae2148c4bddd9ed385c0feaf33c
parent1bcfc88f589051c92882151788217c1410141ef7
signature Signed by PGP key B5690EEEBB952194

feat(web): render skill calls as inline chips (#2572)


6 files changed, 134 insertions(+), 8 deletions(-)

apps/web/src/components/ChatMarkdown.tsx+18-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { DiffsHighlighter, getSharedHighlighter, SupportedLanguages } from "@pierre/diffs";1import { DiffsHighlighter, getSharedHighlighter, SupportedLanguages } from "@pierre/diffs";
2import { CheckIcon, CopyIcon } from "lucide-react";2import { CheckIcon, CopyIcon } from "lucide-react";
3import type { ServerProviderSkill } from "@t3tools/contracts";
3import React, {4import React, {
4 Children,5 Children,
5 Suspense,6 Suspense,
...@@ -19,6 +20,7 @@ import ReactMarkdown from "react-markdown";...@@ -19,6 +20,7 @@ import ReactMarkdown from "react-markdown";
19import { defaultUrlTransform } from "react-markdown";20import { defaultUrlTransform } from "react-markdown";
20import remarkGfm from "remark-gfm";21import remarkGfm from "remark-gfm";
21import { VscodeEntryIcon } from "./chat/VscodeEntryIcon";22import { VscodeEntryIcon } from "./chat/VscodeEntryIcon";
23import { renderSkillInlineMarkdownChildren } from "./chat/SkillInlineText";
22import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";24import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
23import { stackedThreadToast, toastManager } from "./ui/toast";25import { stackedThreadToast, toastManager } from "./ui/toast";
24import { openInPreferredEditor } from "../editorPreferences";26import { openInPreferredEditor } from "../editorPreferences";
...@@ -59,8 +61,11 @@ interface ChatMarkdownProps {...@@ -59,8 +61,11 @@ interface ChatMarkdownProps {
59 text: string;61 text: string;
60 cwd: string | undefined;62 cwd: string | undefined;
61 isStreaming?: boolean;63 isStreaming?: boolean;
64 skills?: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;
62}65}
6366
67const EMPTY_MARKDOWN_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">> = [];
68
64const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/;69const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/;
65const MAX_HIGHLIGHT_CACHE_ENTRIES = 500;70const MAX_HIGHLIGHT_CACHE_ENTRIES = 500;
66const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024;71const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024;
...@@ -507,7 +512,12 @@ function areMarkdownFileLinkPropsEqual(...@@ -507,7 +512,12 @@ function areMarkdownFileLinkPropsEqual(
507 );512 );
508}513}
509514
510function ChatMarkdown({ text, cwd, isStreaming = false }: ChatMarkdownProps) {515function ChatMarkdown({
516 text,
517 cwd,
518 isStreaming = false,
519 skills = EMPTY_MARKDOWN_SKILLS,
520}: ChatMarkdownProps) {
511 const { resolvedTheme } = useTheme();521 const { resolvedTheme } = useTheme();
512 const diffThemeName = resolveDiffThemeName(resolvedTheme);522 const diffThemeName = resolveDiffThemeName(resolvedTheme);
513 const markdownFileLinkMetaByHref = useMemo(() => {523 const markdownFileLinkMetaByHref = useMemo(() => {
...@@ -534,6 +544,12 @@ function ChatMarkdown({ text, cwd, isStreaming = false }: ChatMarkdownProps) {...@@ -534,6 +544,12 @@ function ChatMarkdown({ text, cwd, isStreaming = false }: ChatMarkdownProps) {
534 }, []);544 }, []);
535 const markdownComponents = useMemo<Components>(545 const markdownComponents = useMemo<Components>(
536 () => ({546 () => ({
547 p({ node: _node, children, ...props }) {
548 return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}</p>;
549 },
550 li({ node: _node, children, ...props }) {
551 return <li {...props}>{renderSkillInlineMarkdownChildren(children, skills)}</li>;
552 },
537 a({ node: _node, href, ...props }) {553 a({ node: _node, href, ...props }) {
538 const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";554 const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";
539 const fileLinkMeta = normalizedHref ? markdownFileLinkMetaByHref.get(normalizedHref) : null;555 const fileLinkMeta = normalizedHref ? markdownFileLinkMetaByHref.get(normalizedHref) : null;
...@@ -592,6 +608,7 @@ function ChatMarkdown({ text, cwd, isStreaming = false }: ChatMarkdownProps) {...@@ -592,6 +608,7 @@ function ChatMarkdown({ text, cwd, isStreaming = false }: ChatMarkdownProps) {
592 isStreaming,608 isStreaming,
593 markdownFileLinkMetaByHref,609 markdownFileLinkMetaByHref,
594 resolvedTheme,610 resolvedTheme,
611 skills,
595 ],612 ],
596 );613 );
597614
apps/web/src/components/ChatView.tsx+2
...@@ -195,6 +195,7 @@ const IMAGE_ONLY_BOOTSTRAP_PROMPT =...@@ -195,6 +195,7 @@ const IMAGE_ONLY_BOOTSTRAP_PROMPT =
195const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = [];195const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = [];
196const EMPTY_PROPOSED_PLANS: Thread["proposedPlans"] = [];196const EMPTY_PROPOSED_PLANS: Thread["proposedPlans"] = [];
197const EMPTY_PROVIDERS: ServerProvider[] = [];197const EMPTY_PROVIDERS: ServerProvider[] = [];
198const EMPTY_PROVIDER_SKILLS: ServerProvider["skills"] = [];
198const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {};199const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {};
199type EnvironmentUnavailableState = {200type EnvironmentUnavailableState = {
200 readonly environmentId: EnvironmentId;201 readonly environmentId: EnvironmentId;
...@@ -3574,6 +3575,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -3574,6 +3575,7 @@ export default function ChatView(props: ChatViewProps) {
3574 resolvedTheme={resolvedTheme}3575 resolvedTheme={resolvedTheme}
3575 timestampFormat={timestampFormat}3576 timestampFormat={timestampFormat}
3576 workspaceRoot={activeWorkspaceRoot}3577 workspaceRoot={activeWorkspaceRoot}
3578 skills={activeProviderStatus?.skills ?? EMPTY_PROVIDER_SKILLS}
3577 onIsAtEndChange={onIsAtEndChange}3579 onIsAtEndChange={onIsAtEndChange}
3578 />3580 />
35793581
apps/web/src/components/ComposerPromptEditor.tsx+1-2
...@@ -72,6 +72,7 @@ import {...@@ -72,6 +72,7 @@ import {
72 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,72 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
73 COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME,73 COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME,
74 COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME,74 COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME,
75 SKILL_CHIP_ICON_SVG,
75} from "./composerInlineChip";76} from "./composerInlineChip";
76import { ComposerPendingTerminalContextChip } from "./chat/ComposerPendingTerminalContexts";77import { ComposerPendingTerminalContextChip } from "./chat/ComposerPendingTerminalContexts";
77import { formatProviderSkillDisplayName } from "~/providerSkillPresentation";78import { formatProviderSkillDisplayName } from "~/providerSkillPresentation";
...@@ -216,8 +217,6 @@ function $createComposerMentionNode(path: string): ComposerMentionNode {...@@ -216,8 +217,6 @@ function $createComposerMentionNode(path: string): ComposerMentionNode {
216 return $applyNodeReplacement(new ComposerMentionNode(path));217 return $applyNodeReplacement(new ComposerMentionNode(path));
217}218}
218219
219const SKILL_CHIP_ICON_SVG = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>`;
220
221function resolveSkillDescription(220function resolveSkillDescription(
222 skill: Pick<ServerProviderSkill, "shortDescription" | "description">,221 skill: Pick<ServerProviderSkill, "shortDescription" | "description">,
223): string | null {222): string | null {
apps/web/src/components/chat/MessagesTimeline.tsx+24-5
...@@ -1,4 +1,9 @@...@@ -1,4 +1,9 @@
1import { type EnvironmentId, type MessageId, type TurnId } from "@t3tools/contracts";1import {
2 type EnvironmentId,
3 type MessageId,
4 type ServerProviderSkill,
5 type TurnId,
6} from "@t3tools/contracts";
2import {7import {
3 createContext,8 createContext,
4 memo,9 memo,
...@@ -60,6 +65,7 @@ import {...@@ -60,6 +65,7 @@ import {
60 formatInlineTerminalContextLabel,65 formatInlineTerminalContextLabel,
61 textContainsInlineTerminalContextLabels,66 textContainsInlineTerminalContextLabels,
62} from "./userMessageTerminalContexts";67} from "./userMessageTerminalContexts";
68import { SkillInlineText } from "./SkillInlineText";
63import { formatWorkspaceRelativePath } from "../../filePathDisplay";69import { formatWorkspaceRelativePath } from "../../filePathDisplay";
6470
65// ---------------------------------------------------------------------------71// ---------------------------------------------------------------------------
...@@ -75,6 +81,7 @@ interface TimelineRowSharedState {...@@ -75,6 +81,7 @@ interface TimelineRowSharedState {
75 markdownCwd: string | undefined;81 markdownCwd: string | undefined;
76 resolvedTheme: "light" | "dark";82 resolvedTheme: "light" | "dark";
77 workspaceRoot: string | undefined;83 workspaceRoot: string | undefined;
84 skills: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;
78 activeThreadEnvironmentId: EnvironmentId;85 activeThreadEnvironmentId: EnvironmentId;
79 onRevertUserMessage: (messageId: MessageId) => void;86 onRevertUserMessage: (messageId: MessageId) => void;
80 onImageExpand: (preview: ExpandedImagePreview) => void;87 onImageExpand: (preview: ExpandedImagePreview) => void;
...@@ -93,6 +100,7 @@ const TimelineRowCtx = createContext<TimelineRowSharedState>(null!);...@@ -93,6 +100,7 @@ const TimelineRowCtx = createContext<TimelineRowSharedState>(null!);
93const TimelineRowActivityCtx = createContext<TimelineRowActivityState>(null!);100const TimelineRowActivityCtx = createContext<TimelineRowActivityState>(null!);
94const TIMELINE_LIST_HEADER = <div className="h-3 sm:h-4" />;101const TIMELINE_LIST_HEADER = <div className="h-3 sm:h-4" />;
95const TIMELINE_LIST_FOOTER = <div className="h-3 sm:h-4" />;102const TIMELINE_LIST_FOOTER = <div className="h-3 sm:h-4" />;
103const EMPTY_TIMELINE_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">> = [];
96104
97// ---------------------------------------------------------------------------105// ---------------------------------------------------------------------------
98// Props (public API)106// Props (public API)
...@@ -119,6 +127,7 @@ interface MessagesTimelineProps {...@@ -119,6 +127,7 @@ interface MessagesTimelineProps {
119 resolvedTheme: "light" | "dark";127 resolvedTheme: "light" | "dark";
120 timestampFormat: TimestampFormat;128 timestampFormat: TimestampFormat;
121 workspaceRoot: string | undefined;129 workspaceRoot: string | undefined;
130 skills?: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;
122 onIsAtEndChange: (isAtEnd: boolean) => void;131 onIsAtEndChange: (isAtEnd: boolean) => void;
123}132}
124133
...@@ -147,6 +156,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({...@@ -147,6 +156,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
147 resolvedTheme,156 resolvedTheme,
148 timestampFormat,157 timestampFormat,
149 workspaceRoot,158 workspaceRoot,
159 skills = EMPTY_TIMELINE_SKILLS,
150 onIsAtEndChange,160 onIsAtEndChange,
151}: MessagesTimelineProps) {161}: MessagesTimelineProps) {
152 const rawRows = useMemo(162 const rawRows = useMemo(
...@@ -202,6 +212,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({...@@ -202,6 +212,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
202 markdownCwd,212 markdownCwd,
203 resolvedTheme,213 resolvedTheme,
204 workspaceRoot,214 workspaceRoot,
215 skills,
205 activeThreadEnvironmentId,216 activeThreadEnvironmentId,
206 onRevertUserMessage,217 onRevertUserMessage,
207 onImageExpand,218 onImageExpand,
...@@ -213,6 +224,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({...@@ -213,6 +224,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
213 markdownCwd,224 markdownCwd,
214 resolvedTheme,225 resolvedTheme,
215 workspaceRoot,226 workspaceRoot,
227 skills,
216 activeThreadEnvironmentId,228 activeThreadEnvironmentId,
217 onRevertUserMessage,229 onRevertUserMessage,
218 onImageExpand,230 onImageExpand,
...@@ -357,6 +369,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"...@@ -357,6 +369,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
357 <UserMessageBody369 <UserMessageBody
358 text={displayedUserMessage.visibleText}370 text={displayedUserMessage.visibleText}
359 terminalContexts={terminalContexts}371 terminalContexts={terminalContexts}
372 skills={ctx.skills}
360 />373 />
361 )}374 )}
362 <div className="mt-1.5 flex items-center justify-end gap-2">375 <div className="mt-1.5 flex items-center justify-end gap-2">
...@@ -405,6 +418,7 @@ function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "mess...@@ -405,6 +418,7 @@ function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "mess
405 text={messageText}418 text={messageText}
406 cwd={ctx.markdownCwd}419 cwd={ctx.markdownCwd}
407 isStreaming={Boolean(row.message.streaming)}420 isStreaming={Boolean(row.message.streaming)}
421 skills={ctx.skills}
408 />422 />
409 <AssistantChangedFilesSection423 <AssistantChangedFilesSection
410 turnSummary={row.assistantTurnDiffSummary}424 turnSummary={row.assistantTurnDiffSummary}
...@@ -744,6 +758,7 @@ const UserMessageTerminalContextInlineLabel = memo(...@@ -744,6 +758,7 @@ const UserMessageTerminalContextInlineLabel = memo(
744const UserMessageBody = memo(function UserMessageBody(props: {758const UserMessageBody = memo(function UserMessageBody(props: {
745 text: string;759 text: string;
746 terminalContexts: ParsedTerminalContextEntry[];760 terminalContexts: ParsedTerminalContextEntry[];
761 skills: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;
747}) {762}) {
748 if (props.terminalContexts.length > 0) {763 if (props.terminalContexts.length > 0) {
749 const hasEmbeddedInlineLabels = textContainsInlineTerminalContextLabels(764 const hasEmbeddedInlineLabels = textContainsInlineTerminalContextLabels(
...@@ -766,7 +781,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {...@@ -766,7 +781,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
766 if (matchIndex > cursor) {781 if (matchIndex > cursor) {
767 inlineNodes.push(782 inlineNodes.push(
768 <span key={`user-terminal-context-inline-before:${context.header}:${cursor}`}>783 <span key={`user-terminal-context-inline-before:${context.header}:${cursor}`}>
769 {props.text.slice(cursor, matchIndex)}784 <SkillInlineText text={props.text.slice(cursor, matchIndex)} skills={props.skills} />
770 </span>,785 </span>,
771 );786 );
772 }787 }
...@@ -783,7 +798,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {...@@ -783,7 +798,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
783 if (cursor < props.text.length) {798 if (cursor < props.text.length) {
784 inlineNodes.push(799 inlineNodes.push(
785 <span key={`user-message-terminal-context-inline-rest:${cursor}`}>800 <span key={`user-message-terminal-context-inline-rest:${cursor}`}>
786 {props.text.slice(cursor)}801 <SkillInlineText text={props.text.slice(cursor)} skills={props.skills} />
787 </span>,802 </span>,
788 );803 );
789 }804 }
...@@ -811,7 +826,11 @@ const UserMessageBody = memo(function UserMessageBody(props: {...@@ -811,7 +826,11 @@ const UserMessageBody = memo(function UserMessageBody(props: {
811 }826 }
812827
813 if (props.text.length > 0) {828 if (props.text.length > 0) {
814 inlineNodes.push(<span key="user-message-terminal-context-inline-text">{props.text}</span>);829 inlineNodes.push(
830 <span key="user-message-terminal-context-inline-text">
831 <SkillInlineText text={props.text} skills={props.skills} />
832 </span>,
833 );
815 } else if (inlinePrefix.length === 0) {834 } else if (inlinePrefix.length === 0) {
816 return null;835 return null;
817 }836 }
...@@ -829,7 +848,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {...@@ -829,7 +848,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
829848
830 return (849 return (
831 <div className="whitespace-pre-wrap wrap-break-word text-sm leading-relaxed text-foreground">850 <div className="whitespace-pre-wrap wrap-break-word text-sm leading-relaxed text-foreground">
832 {props.text}851 <SkillInlineText text={props.text} skills={props.skills} />
833 </div>852 </div>
834 );853 );
835});854});
apps/web/src/components/chat/SkillInlineText.tsx created+87
...@@ -0,0 +1,87 @@
1import { Children, cloneElement, isValidElement, type ReactNode } from "react";
2import type { ServerProviderSkill } from "@t3tools/contracts";
3
4import { formatProviderSkillDisplayName } from "../../providerSkillPresentation";
5import {
6 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
7 COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME,
8 COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME,
9 SKILL_CHIP_ICON_SVG,
10} from "../composerInlineChip";
11
12const SKILL_TOKEN_REGEX = /(^|\s)\$([a-zA-Z][a-zA-Z0-9:_-]*)(?=\s|$)/g;
13
14type InlineSkill = Pick<ServerProviderSkill, "name" | "displayName">;
15
16export function SkillInlineText(props: { text: string; skills: ReadonlyArray<InlineSkill> }) {
17 const nodes: ReactNode[] = [];
18 let cursor = 0;
19
20 for (const match of props.text.matchAll(SKILL_TOKEN_REGEX)) {
21 const prefix = match[1] ?? "";
22 const name = match[2] ?? "";
23 const start = (match.index ?? 0) + prefix.length;
24 const rawText = `$${name}`;
25 const skill = props.skills.find((candidate) => candidate.name === name);
26 if (!skill) {
27 continue;
28 }
29
30 if (start > cursor) {
31 nodes.push(props.text.slice(cursor, start));
32 }
33 nodes.push(<SkillChip key={`${start}:${name}`} skill={skill} rawText={rawText} />);
34 cursor = start + rawText.length;
35 }
36
37 if (cursor === 0) {
38 return <>{props.text}</>;
39 }
40 if (cursor < props.text.length) {
41 nodes.push(props.text.slice(cursor));
42 }
43 return <>{nodes}</>;
44}
45
46export function renderSkillInlineMarkdownChildren(
47 children: ReactNode,
48 skills: ReadonlyArray<InlineSkill>,
49): ReactNode {
50 return Children.map(children, (child) => {
51 if (typeof child === "string") {
52 return <SkillInlineText text={child} skills={skills} />;
53 }
54 if (!isValidElement<{ children?: ReactNode }>(child)) {
55 return child;
56 }
57 if (child.type === "code" || child.type === "a") {
58 return child;
59 }
60 if (!("children" in child.props)) {
61 return child;
62 }
63 return cloneElement(
64 child,
65 undefined,
66 renderSkillInlineMarkdownChildren(child.props.children, skills),
67 );
68 });
69}
70
71function SkillChip(props: { skill: InlineSkill; rawText: string }) {
72 return (
73 <span className="inline-flex align-middle leading-none">
74 <span className="sr-only">{props.rawText}</span>
75 <span aria-hidden="true" className={COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME}>
76 <span
77 aria-hidden="true"
78 className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME}
79 dangerouslySetInnerHTML={{ __html: SKILL_CHIP_ICON_SVG }}
80 />
81 <span className={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}>
82 {formatProviderSkillDisplayName(props.skill)}
83 </span>
84 </span>
85 </span>
86 );
87}
apps/web/src/components/composerInlineChip.ts+2
...@@ -8,5 +8,7 @@ export const COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME = "truncate select-none leadi...@@ -8,5 +8,7 @@ export const COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME = "truncate select-none leadi
8export const COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME =8export const COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME =
9 "inline-flex max-w-full select-none items-center gap-1 rounded-md border border-fuchsia-500/25 bg-fuchsia-500/12 px-1.5 py-px font-medium text-[12px] leading-[1.1] text-fuchsia-700 align-middle dark:text-fuchsia-300";9 "inline-flex max-w-full select-none items-center gap-1 rounded-md border border-fuchsia-500/25 bg-fuchsia-500/12 px-1.5 py-px font-medium text-[12px] leading-[1.1] text-fuchsia-700 align-middle dark:text-fuchsia-300";
1010
11export const SKILL_CHIP_ICON_SVG = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>`;
12
11export const COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME =13export const COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME =
12 "ml-0.5 inline-flex size-3.5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground/72 transition-colors hover:bg-foreground/6 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";14 "ml-0.5 inline-flex size-3.5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground/72 transition-colors hover:bg-foreground/6 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";