From b59040ff183e70828ffc741932a16ed88062560d Mon Sep 17 00:00:00 2001
From: kalac2232 <35256979+kalac2232@users.noreply.github.com>
Date: Wed, 1 Apr 2026 14:49:58 +0800
Subject: [PATCH] feat: escape HTML in user messages (#782)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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:
now displays as <div>
- Agent mentions render correctly as clickable links in Markdown mode
Co-authored-by: 郭皓楠
---
.../chat/message/parts/UserTextPart.tsx | 25 +++++++++++++++----
1 file changed, 20 insertions(+), 5 deletions(-)
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(() => {