This commit is contained in:
Timothy Jaeryang Baek 2026-09-12 17:11:37 -04:00
parent 2a44c9d384
commit 0edd731c74
5 changed files with 32 additions and 12 deletions

View file

@ -2240,7 +2240,7 @@ def sanitize_tool_pairs(messages: list[dict]) -> list[dict]:
return sanitized
# Ids are validated as [a-z0-9_-]+ on create; matching that keeps ordinary "<$..." text intact.
# Match candidate mentions using the same ID characters allowed on skill creation.
SKILL_MENTION_RE = re.compile(r'<(?:\$([a-z0-9_-]+)(?:\|[^>]*)?|/([a-z0-9_-]+)\|[^>]*)>')
@ -2263,25 +2263,27 @@ def extract_skill_ids_from_messages(messages: list[dict]) -> set[str]:
return ids
SKILL_MENTION_STRIP_RE = re.compile(r'<(?:\$[a-z0-9_-]+(?:\|([^>]*))?|/[a-z0-9_-]+\|([^>]*))>')
SKILL_MENTION_STRIP_RE = re.compile(r'<(?:\$([a-z0-9_-]+)(?:\|([^>]*))?|/([a-z0-9_-]+)\|([^>]*))>')
def strip_skill_mentions(messages: list[dict]) -> None:
"""Replace <$skillId|label> and </skillId|label> mention tags with the label in-place."""
def strip_skill_mentions(messages: list[dict], skill_ids: set[str]) -> None:
"""Replace mentions of resolved skills with their label, preserving all other text."""
def label(match):
return match.group(1) or match.group(2) or ''
if (match.group(1) or match.group(3)) not in skill_ids:
return match.group(0)
return match.group(2) or match.group(4) or ''
for message in messages:
content = message.get('content')
if isinstance(content, str) and SKILL_MENTION_STRIP_RE.search(content):
message['content'] = SKILL_MENTION_STRIP_RE.sub(label, content).strip()
message['content'] = SKILL_MENTION_STRIP_RE.sub(label, content)
elif isinstance(content, list):
for part in content:
if isinstance(part, dict) and part.get('type') == 'text':
text = part.get('text', '')
if SKILL_MENTION_STRIP_RE.search(text):
part['text'] = SKILL_MENTION_STRIP_RE.sub(label, text).strip()
part['text'] = SKILL_MENTION_STRIP_RE.sub(label, text)
async def connect_mcp_server(
@ -2776,8 +2778,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
append=True,
)
# Strip <$skillId|label> mention tags so the model doesn't see raw markup.
strip_skill_mentions(form_data.get('messages', []))
# Strip only resolved skill mentions; ordinary text such as Perl's <$fh> stays intact.
strip_skill_mentions(form_data.get('messages', []), {s.id for s in available_skills})
prompt = get_last_user_message(form_data['messages'])

View file

@ -9,6 +9,7 @@
const i18n = getContext('i18n');
import { WEBUI_BASE_URL } from '$lib/constants';
import { skills } from '$lib/stores';
import { copyToClipboard, safeLinkUrl, unescapeHtml } from '$lib/utils';
import Image from '$lib/components/common/Image.svelte';
@ -128,7 +129,11 @@
}}
></iframe>
{:else if token.type === 'mention'}
<MentionToken {token} />
{#if token.triggerChar === '$' && !$skills?.some((skill) => skill.id === token.id && skill.is_active)}
{token.raw}
{:else}
<MentionToken {token} />
{/if}
{:else if token.type === 'footnote'}
{@html DOMPurify.sanitize(
`<sup class="footnote-ref footnote-ref-text">${token.escapedText}</sup>`

View file

@ -2,6 +2,7 @@
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import equal from 'fast-deep-equal';
import { skills } from '$lib/stores';
marked.use({
breaks: true,
@ -522,9 +523,15 @@
// Now replace the escaped mention patterns back into real spans
const withMentions = escaped.replace(
/&lt;([@#$])([^|&\s]+)(?:\|([^&]*?))?&gt;|&lt;\/([\w.\-:/]+)\|([^&]*?)&gt;/g,
(_, ch, id, label, slashSkillId, slashSkillLabel) => {
(match, ch, id, label, slashSkillId, slashSkillLabel) => {
const mentionChar = ch || '$';
const mentionId = id || slashSkillId;
if (
mentionChar === '$' &&
!$skills?.some((skill) => skill.id === mentionId && skill.is_active)
) {
return match;
}
const display = (label || slashSkillLabel)?.length
? label || slashSkillLabel
: mentionId;

View file

@ -69,7 +69,7 @@ export const models: Writable<Model[]> = writable([]);
export const knowledge: Writable<null | Document[]> = writable(null);
export const tools = writable(null);
export const skills = writable(null);
export const skills: Writable<null | any[]> = writable(null);
export const functions = writable(null);
export type WorkspaceSection = 'models' | 'knowledge' | 'prompts' | 'skills' | 'tools';

View file

@ -1,4 +1,7 @@
// mention-extension.ts
import { get } from 'svelte/store';
import { skills } from '$lib/stores';
type MentionOptions = {
triggerChar?: string; // default "@"
className?: string; // default "mention"
@ -20,6 +23,9 @@ function mentionStart(src: string) {
function mentionRenderer(token: any, options: MentionOptions = {}) {
const trigger = options.triggerChar ?? '@';
if (trigger === '$' && !get(skills)?.some((skill) => skill.id === token.id && skill.is_active)) {
return escapeHtml(token.raw);
}
const cls = options.className ?? 'mention';
const extra = options.extraAttrs ?? {};