feat: escape HTML in user messages (#782)

- Add HTML escaping for user messages in Markdown mode
- Fix agent mention link rendering order issue
- Plain mode remains unchanged (uses React elements)

Tested:
- User messages: <div> now displays as &lt;div&gt;
- Agent mentions render correctly as clickable links in Markdown mode

Co-authored-by: 郭皓楠 <guohaonan@MacBook-Neo.local>
This commit is contained in:
kalac2232
2026-04-01 09:49:58 +03:00
committed by GitHub
co-authored by 郭皓楠
parent 086cf26cd7
commit b59040ff18
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
};
const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain' => {
return mode === 'markdown' ? 'markdown' : 'plain';
};
@@ -99,12 +108,18 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ 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 <a> tags won't be escaped)
if (agentMention?.token && content.includes(agentMention.token)) {
const mentionHtml = `<a href="${buildMentionUrl(agentMention.name)}" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">${agentMention.token}</a>`;
content = content.replace(agentMention.token, mentionHtml);
}
const mentionHtml = `<a href="${buildMentionUrl(agentMention.name)}" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">${agentMention.token}</a>`;
return textContent.replace(agentMention.token, mentionHtml);
return content;
}, [agentMention, textContent]);
const plainTextContent = React.useMemo(() => {