diff --git a/packages/ui/src/components/chat/message/parts/UserTextPart.tsx b/packages/ui/src/components/chat/message/parts/UserTextPart.tsx index 19623ec6..41f9d473 100644 --- a/packages/ui/src/components/chat/message/parts/UserTextPart.tsx +++ b/packages/ui/src/components/chat/message/parts/UserTextPart.tsx @@ -19,6 +19,15 @@ const buildMentionUrl = (name: string): string => { return `https://opencode.ai/docs/agents/#${encoded}`; }; +const escapeHtml = (text: string): string => { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +}; + const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain' => { return mode === 'markdown' ? 'markdown' : 'plain'; }; @@ -99,12 +108,18 @@ const UserTextPart: React.FC = ({ part, messageId, agentMenti }, [collapseZoneHeight, hasActiveSelectionInElement, isExpanded, isTruncated]); const processedMarkdownContent = React.useMemo(() => { - if (!agentMention?.token || !textContent.includes(agentMention.token)) { - return textContent; + let content = textContent; + + // Step 1: First escape HTML to protect against XSS and ensure HTML tags display as text + content = escapeHtml(content); + + // Step 2: Then insert agent mention links (after escaping, so tags won't be escaped) + if (agentMention?.token && content.includes(agentMention.token)) { + const mentionHtml = `${agentMention.token}`; + content = content.replace(agentMention.token, mentionHtml); } - - const mentionHtml = `${agentMention.token}`; - return textContent.replace(agentMention.token, mentionHtml); + + return content; }, [agentMention, textContent]); const plainTextContent = React.useMemo(() => {