From f7c5965a70eb6e051ac549808d1871487cbe9e92 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 11 Feb 2026 15:08:59 -0600 Subject: [PATCH] feat: $ skills mention --- src/lib/components/chat/Chat.svelte | 35 ++++++ src/lib/components/chat/MessageInput.svelte | 14 ++- .../MessageInput/CommandSuggestionList.svelte | 22 ++++ .../chat/MessageInput/Commands/Skills.svelte | 100 ++++++++++++++++++ .../components/chat/Messages/Markdown.svelte | 2 +- .../MarkdownInlineTokens/MentionToken.svelte | 18 +++- src/lib/components/icons/KeyframePlus.svelte | 20 ++++ src/lib/components/icons/Keyframes.svelte | 21 ++++ 8 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 src/lib/components/chat/MessageInput/Commands/Skills.svelte create mode 100644 src/lib/components/icons/KeyframePlus.svelte create mode 100644 src/lib/components/icons/Keyframes.svelte diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index e144885bb..1bd004fc3 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -2023,6 +2023,40 @@ } } + // Parse skill mentions (<$skillId|label>) from user messages + const skillMentionRegex = /<\$([^|>]+)\|?[^>]*>/g; + const skillIds = []; + for (const message of messages) { + const content = typeof message.content === 'string' ? message.content : message.content?.[0]?.text ?? ''; + for (const match of content.matchAll(skillMentionRegex)) { + if (!skillIds.includes(match[1])) { + skillIds.push(match[1]); + } + } + } + + // Strip skill mentions from message content + if (skillIds.length > 0) { + messages = messages.map((message) => { + if (typeof message.content === 'string') { + return { + ...message, + content: message.content.replace(/<\$[^>]+>/g, '').trim() + }; + } else if (Array.isArray(message.content)) { + return { + ...message, + content: message.content.map((part) => + part.type === 'text' + ? { ...part, text: part.text.replace(/<\$[^>]+>/g, '').trim() } + : part + ) + }; + } + return message; + }); + } + const res = await generateOpenAIChatCompletion( localStorage.token, { @@ -2044,6 +2078,7 @@ filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined, tool_ids: toolIds.length > 0 ? toolIds : undefined, + skill_ids: skillIds.length > 0 ? skillIds : undefined, tool_servers: ($toolServers ?? []).filter( (server, idx) => toolServerIds.includes(idx) || toolServerIds.includes(server?.id) ), diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 1f16526c4..2955d83ec 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -390,7 +390,7 @@ let command = ''; export let showCommands = false; - $: showCommands = ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2); + $: showCommands = ['/', '#', '@', '$'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2); let suggestions = null; let showTools = false; @@ -946,6 +946,18 @@ } } }) + }, + { + char: '$', + render: getSuggestionRenderer(CommandSuggestionList, { + i18n, + onSelect: (e) => { + document.getElementById('chat-input')?.focus(); + }, + + insertTextHandler: insertTextAtCursor, + onUpload: () => {} + }) } ]; loaded = true; diff --git a/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte b/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte index f4bd5b83f..1a3a83689 100644 --- a/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte +++ b/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte @@ -7,6 +7,7 @@ import Prompts from './Commands/Prompts.svelte'; import Knowledge from './Commands/Knowledge.svelte'; import Models from './Commands/Models.svelte'; + import Skills from './Commands/Skills.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; import { onMount } from 'svelte'; @@ -138,6 +139,27 @@ } }} /> + {:else if char === '$'} + { + const { type, data } = e; + + if (type === 'skill') { + command({ + id: `${data.id}|${data.name}`, + label: data.name + }); + + onSelect({ + type: 'skill', + data: data + }); + } + }} + /> {/if} {:else}
diff --git a/src/lib/components/chat/MessageInput/Commands/Skills.svelte b/src/lib/components/chat/MessageInput/Commands/Skills.svelte new file mode 100644 index 000000000..9bac71959 --- /dev/null +++ b/src/lib/components/chat/MessageInput/Commands/Skills.svelte @@ -0,0 +1,100 @@ + + +
+ {$i18n.t('Skills')} +
+ +{#if filteredItems.length > 0} + {#each filteredItems as skill, skillIdx} + + + + {/each} +{/if} diff --git a/src/lib/components/chat/Messages/Markdown.svelte b/src/lib/components/chat/Messages/Markdown.svelte index 459b4b6b5..67698648d 100644 --- a/src/lib/components/chat/Messages/Markdown.svelte +++ b/src/lib/components/chat/Messages/Markdown.svelte @@ -46,7 +46,7 @@ marked.use(footnoteExtension(options)); marked.use(disableSingleTilde); marked.use({ - extensions: [mentionExtension({ triggerChar: '@' }), mentionExtension({ triggerChar: '#' })] + extensions: [mentionExtension({ triggerChar: '@' }), mentionExtension({ triggerChar: '#' }), mentionExtension({ triggerChar: '$' })] }); $: (async () => { diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte index 19f23b2aa..9497444be 100644 --- a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte +++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte @@ -25,8 +25,23 @@ const init = () => { const _id = token?.id; - // split by : and take first part as idType and second part as id + triggerChar = token?.triggerChar ?? '@'; + if (triggerChar === '$') { + // Skill mention: id format is "skillId|label" + const pipeIdx = _id?.indexOf('|') ?? -1; + if (pipeIdx > 0) { + id = _id.substring(0, pipeIdx); + label = _id.substring(pipeIdx + 1); + } else { + id = _id; + label = _id; + } + idType = null; + return; + } + + // split by : and take first part as idType and second part as id const parts = _id?.split(':'); if (parts) { idType = parts[0]; @@ -37,7 +52,6 @@ } label = token?.label ?? id; - triggerChar = token?.triggerChar ?? '@'; if (triggerChar === '#') { if (idType === 'C') { diff --git a/src/lib/components/icons/KeyframePlus.svelte b/src/lib/components/icons/KeyframePlus.svelte new file mode 100644 index 000000000..349fcf89f --- /dev/null +++ b/src/lib/components/icons/KeyframePlus.svelte @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/lib/components/icons/Keyframes.svelte b/src/lib/components/icons/Keyframes.svelte new file mode 100644 index 000000000..20e3531e1 --- /dev/null +++ b/src/lib/components/icons/Keyframes.svelte @@ -0,0 +1,21 @@ + + + + + + +