diff --git a/apps/web/src/components/ai/shared/chat/tool-calls/RichContentRenderer.tsx b/apps/web/src/components/ai/shared/chat/tool-calls/RichContentRenderer.tsx index 14edb295f..af41a6169 100644 --- a/apps/web/src/components/ai/shared/chat/tool-calls/RichContentRenderer.tsx +++ b/apps/web/src/components/ai/shared/chat/tool-calls/RichContentRenderer.tsx @@ -68,16 +68,23 @@ export const RichContentRenderer: React.FC = memo(func // Strip line numbers if present const rawContent = stripLineNumbers(content); - // Convert markdown to HTML if needed - const html = isMarkdown ? markdownToHtml(rawContent) : rawContent; + // Check if content already looks like HTML (has actual HTML tags) + // This detects content from TipTap editor which outputs HTML + const contentIsHtml = /<[a-z][\s\S]*>/i.test(rawContent); - // Check if content looks like HTML - const isHtml = /<[a-z][\s\S]*>/i.test(html); + // Convert to HTML: + // - If content is already HTML, preserve it (markdownToHtml would escape the tags) + // - If content is not HTML (markdown or plain text), convert markdown to HTML + // - isMarkdown prop can force markdown conversion for edge cases + const html = (contentIsHtml && !isMarkdown) ? rawContent : markdownToHtml(rawContent); + + // After conversion, check if we have HTML to render + const hasHtml = /<[a-z][\s\S]*>/i.test(html); // Sanitize HTML content using allowlist approach - const sanitized = isHtml ? sanitizeHtmlAllowlist(html) : html; + const sanitized = hasHtml ? sanitizeHtmlAllowlist(html) : html; - return { processedHtml: sanitized, hasHtmlContent: isHtml }; + return { processedHtml: sanitized, hasHtmlContent: hasHtml }; }, [content, isMarkdown]); const handleNavigate = () => {