diff --git a/ui/desktop/src/components/MarkdownContent.test.tsx b/ui/desktop/src/components/MarkdownContent.test.tsx
new file mode 100644
index 000000000000..ca4a4324864a
--- /dev/null
+++ b/ui/desktop/src/components/MarkdownContent.test.tsx
@@ -0,0 +1,542 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/dom';
+import MarkdownContent from './MarkdownContent';
+
+// Mock the icons to avoid import issues
+vi.mock('./icons', () => ({
+ Check: () =>
✓
,
+ Copy: () => 📋
,
+}));
+
+describe('MarkdownContent', () => {
+ describe('HTML Security Integration', () => {
+ it('renders safe markdown content normally', async () => {
+ const content = `# Test Title
+
+Visit for more info.
+
+Contact for support.
+
+Use \`Array\` for generics.`;
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
+ expect(screen.getByText(/Visit/)).toBeInTheDocument();
+ expect(screen.getByText(/for more info/)).toBeInTheDocument();
+ expect(screen.getByText(/Contact/)).toBeInTheDocument();
+ expect(screen.getByText(/for support/)).toBeInTheDocument();
+ });
+
+ // Should not create extra code blocks for safe content
+ const codeBlocks = screen.queryAllByText(/```html/);
+ expect(codeBlocks).toHaveLength(0);
+ });
+
+ it('wraps dangerous HTML in code blocks', async () => {
+ const content = `# Security Test
+
+This is safe text.
+
+
+
+More safe text.`;
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Security Test')).toBeInTheDocument();
+ expect(screen.getByText('This is safe text.')).toBeInTheDocument();
+ expect(screen.getByText('More safe text.')).toBeInTheDocument();
+ });
+
+ // The script tag should be in a code block, not executed
+ const scriptElements = document.querySelectorAll('script');
+ expect(scriptElements).toHaveLength(0); // No actual script tags should be created
+
+ // Should find the script content in a code block (text may be split across spans)
+ await waitFor(() => {
+ expect(screen.getByText(/alert/)).toBeInTheDocument();
+ expect(screen.getByText(/xss/)).toBeInTheDocument();
+ });
+ });
+
+ it('handles HTML comments securely', async () => {
+ const content = `# Comment Test
+
+
+
+Normal text continues.`;
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Comment Test')).toBeInTheDocument();
+ expect(screen.getByText('Normal text continues.')).toBeInTheDocument();
+ });
+
+ // Comment should be in a code block
+ await waitFor(() => {
+ expect(screen.getByText(/This is a malicious comment/)).toBeInTheDocument();
+ });
+ });
+
+ it('preserves existing code blocks', async () => {
+ const content = `# Code Block Test
+
+\`\`\`javascript
+const html = "This is safe in a code block
";
+console.log(html);
+\`\`\`
+
+This should be wrapped
`;
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Code Block Test')).toBeInTheDocument();
+ });
+
+ // Should preserve the original JavaScript code block (text may be split)
+ await waitFor(() => {
+ expect(screen.getByText(/const/)).toBeInTheDocument();
+ expect(screen.getAllByText(/html/)).toHaveLength(2); // Variable name and function parameter
+ });
+
+ // The div outside the code block should be wrapped
+ await waitFor(() => {
+ expect(screen.getByText(/This should be wrapped/)).toBeInTheDocument();
+ });
+ });
+
+ it('handles mixed safe and unsafe content', async () => {
+ const content = `# Mixed Content Test
+
+1. Auto-link:
+2. Inline code: \`const x = Array();\`
+3. Real markup:
+4. Placeholder path: /src`;
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Mixed Content Test')).toBeInTheDocument();
+ expect(screen.getByText(/Auto-link/)).toBeInTheDocument();
+ expect(screen.getByText(/Inline code/)).toBeInTheDocument();
+ expect(screen.getByText(/Real markup/)).toBeInTheDocument();
+ expect(screen.getByText(/Placeholder path/)).toBeInTheDocument();
+ });
+
+ // Only the input tag should be wrapped
+ await waitFor(() => {
+ expect(screen.getByText(/input/)).toBeInTheDocument();
+ expect(screen.getByText(/type/)).toBeInTheDocument();
+ expect(screen.getByText(/disabled/)).toBeInTheDocument();
+ });
+
+ // Should not have actual input elements in the DOM
+ const inputElements = document.querySelectorAll('input');
+ expect(inputElements).toHaveLength(0);
+ });
+ });
+
+ describe('Code Block Functionality', () => {
+ it('renders code blocks with syntax highlighting', async () => {
+ const content = `\`\`\`javascript
+console.log('Hello, World!');
+\`\`\``;
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText(/console/)).toBeInTheDocument();
+ expect(screen.getByText(/log/)).toBeInTheDocument();
+ expect(screen.getByText(/Hello, World!/)).toBeInTheDocument();
+ });
+ });
+
+ it('renders inline code', async () => {
+ const content = 'Use `console.log()` to debug.';
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText(/Use/)).toBeInTheDocument();
+ expect(screen.getByText(/to debug/)).toBeInTheDocument();
+ expect(screen.getByText('console.log()')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Markdown Features', () => {
+ it('renders headers correctly', async () => {
+ const content = `# H1 Header
+## H2 Header
+### H3 Header`;
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByRole('heading', { level: 1, name: 'H1 Header' })).toBeInTheDocument();
+ expect(screen.getByRole('heading', { level: 2, name: 'H2 Header' })).toBeInTheDocument();
+ expect(screen.getByRole('heading', { level: 3, name: 'H3 Header' })).toBeInTheDocument();
+ });
+ });
+
+ it('renders lists correctly', async () => {
+ const content = `- Item 1
+- Item 2
+- Item 3
+
+1. Numbered 1
+2. Numbered 2`;
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
+ expect(screen.getByText('Item 3')).toBeInTheDocument();
+ expect(screen.getByText('Numbered 1')).toBeInTheDocument();
+ expect(screen.getByText('Numbered 2')).toBeInTheDocument();
+ });
+ });
+
+ it('renders links with correct attributes', async () => {
+ const content = '[Visit Block](https://block.dev)';
+
+ render();
+
+ await waitFor(() => {
+ const link = screen.getByRole('link', { name: 'Visit Block' });
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute('href', 'https://block.dev');
+ expect(link).toHaveAttribute('target', '_blank');
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer');
+ });
+ });
+
+ it('renders tables correctly', async () => {
+ const content = `| Name | Value |
+|------|-------|
+| Test | 123 |
+| Demo | 456 |`;
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Name')).toBeInTheDocument();
+ expect(screen.getByText('Value')).toBeInTheDocument();
+ expect(screen.getByText('Test')).toBeInTheDocument();
+ expect(screen.getByText('123')).toBeInTheDocument();
+ expect(screen.getByText('Demo')).toBeInTheDocument();
+ expect(screen.getByText('456')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('handles empty content gracefully', async () => {
+ render();
+
+ // Should not throw and should render the component
+ const container = document.querySelector('.w-full.overflow-x-hidden');
+ expect(container).toBeInTheDocument();
+ });
+
+ it('handles malformed markdown gracefully', async () => {
+ const content = `# Unclosed header
+[Unclosed link(https://example.com
+\`\`\`
+Unclosed code block`;
+
+ render();
+
+ await waitFor(() => {
+ // Should still render what it can
+ expect(screen.getByText('Unclosed header')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Line Break Functionality', () => {
+ it('preserves single line breaks with remark-breaks plugin', async () => {
+ const content = `First line
+Second line
+Third line`;
+
+ const { container } = render();
+
+ await waitFor(() => {
+ // Check that all text content is present (text may be split by
tags)
+ expect(container).toHaveTextContent('First line');
+ expect(container).toHaveTextContent('Second line');
+ expect(container).toHaveTextContent('Third line');
+ });
+
+ // Check that line breaks are preserved (rendered as
tags)
+ const brElements = container.querySelectorAll('br');
+ expect(brElements.length).toBeGreaterThan(0);
+ });
+
+ it('handles mixed content with line breaks', async () => {
+ const content = `# Header
+Paragraph with
+line breaks.
+
+- List item 1
+- List item 2`;
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(screen.getByRole('heading', { level: 1, name: 'Header' })).toBeInTheDocument();
+
+ // Check that text content is present (text may be split by
tags)
+ expect(container).toHaveTextContent('Paragraph with');
+ expect(container).toHaveTextContent('line breaks.');
+ expect(screen.getByText('List item 1')).toBeInTheDocument();
+ expect(screen.getByText('List item 2')).toBeInTheDocument();
+ });
+ });
+
+ it('maintains existing markdown features with line breaks', async () => {
+ const content = `**Bold text**
+with line break
+
+\`code\` and
+more text`;
+
+ const { container } = render();
+
+ await waitFor(() => {
+ // Bold text should still work
+ const boldElement = container.querySelector('strong');
+ expect(boldElement).toBeInTheDocument();
+ expect(boldElement).toHaveTextContent('Bold text');
+
+ // Code should still work
+ expect(screen.getByText('code')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('URL Overflow Handling', () => {
+ it('handles very long URLs without overflow', async () => {
+ const longUrl =
+ 'https://example-docs.com/document/d/1oruk3lcrnhoOXMFzBJB8X6qQ5AtQTmj4XXxXk3xK-3g/edit?usp=sharing&mode=edit&version=1';
+ const content = `Check out this document: ${longUrl}
+
+Another very long URL: https://www.example.com/very/long/path/with/many/segments/and/parameters?param1=value1¶m2=value2¶m3=value3¶m4=value4¶m5=value5`;
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(screen.getByText(/Check out this document/)).toBeInTheDocument();
+ expect(screen.getByText(/Another very long URL/)).toBeInTheDocument();
+ });
+
+ // Check that URLs are rendered as links
+ const links = container.querySelectorAll('a');
+ expect(links.length).toBeGreaterThan(0);
+
+ // Check that links have proper CSS classes for word breaking
+ links.forEach((link) => {
+ // The CSS should allow the text to break
+ expect(link).toBeInTheDocument();
+ });
+ });
+
+ it('handles markdown links with long URLs', async () => {
+ const longUrl =
+ 'https://example-docs.com/document/d/1oruk3lcrnhoOXMFzBJB8X6qQ5AtQTmj4XXxXk3xK-3g/edit?usp=sharing&mode=edit&version=1';
+ const content = `[Click here for the document](${longUrl})`;
+
+ render();
+
+ await waitFor(() => {
+ const link = screen.getByRole('link', { name: 'Click here for the document' });
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute('href', longUrl);
+ });
+ });
+
+ it('handles multiple long URLs in the same message', async () => {
+ const content = `Here are some long URLs:
+
+1. Example Doc: https://example-docs.com/document/d/1oruk3lcrnhoOXMFzBJB8X6qQ5AtQTmj4XXxXk3xK-3g/edit?usp=sharing&mode=edit&version=1
+2. Another long URL: https://www.example.com/very/long/path/with/many/segments/and/parameters?param1=value1¶m2=value2¶m3=value3
+3. Third URL: https://api.example.com/v1/users/12345/documents/67890/attachments/abcdef123456789?format=json&include=metadata&sort=created_at`;
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText(/Here are some long URLs/)).toBeInTheDocument();
+ expect(screen.getByText(/Example Doc/)).toBeInTheDocument();
+ expect(screen.getByText(/Another long URL/)).toBeInTheDocument();
+ expect(screen.getByText(/Third URL/)).toBeInTheDocument();
+ });
+ });
+
+ it('applies word-break CSS classes to the container', () => {
+ const content = 'Test content';
+ render();
+
+ const markdownContainer = document.querySelector('.prose');
+ expect(markdownContainer).toBeInTheDocument();
+ expect(markdownContainer).toHaveClass('prose-a:break-all');
+ expect(markdownContainer).toHaveClass('prose-a:overflow-wrap-anywhere');
+ });
+ });
+
+ describe('LaTeX/KaTeX Escaping', () => {
+ it('escapes underscores in plain text to prevent subscript rendering', async () => {
+ const content = 'This is a variable_name and another_variable_name in text.';
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(container).toHaveTextContent('This is a variable_name and another_variable_name in text.');
+ });
+
+ // Should not have any subscript elements
+ const subscripts = container.querySelectorAll('sub');
+ expect(subscripts).toHaveLength(0);
+ });
+
+ it('escapes dollar signs in shell commands to prevent math mode', async () => {
+ const content = 'Run this command: cmd "$FOO" --opt="$BAR"';
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(container).toHaveTextContent('Run this command: cmd "$FOO" --opt="$BAR"');
+ });
+
+ // Should not have any KaTeX math elements
+ const mathElements = container.querySelectorAll('.katex');
+ expect(mathElements).toHaveLength(0);
+ });
+
+ it('preserves underscores inside inline code', async () => {
+ const content = 'Use `variable_name` and `another_variable` in your code.';
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(screen.getByText('variable_name')).toBeInTheDocument();
+ expect(screen.getByText('another_variable')).toBeInTheDocument();
+ });
+
+ // Underscores in code should be preserved
+ const codeElements = container.querySelectorAll('code');
+ expect(codeElements.length).toBeGreaterThan(0);
+ });
+
+ it('preserves dollar signs inside code blocks', async () => {
+ const content = `\`\`\`bash
+cmd "$FOO" --opt="$BAR"
+echo "$HOME"
+\`\`\``;
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(container).toHaveTextContent('cmd "$FOO" --opt="$BAR"');
+ expect(container).toHaveTextContent('echo "$HOME"');
+ });
+
+ // Should not have any KaTeX math elements
+ const mathElements = container.querySelectorAll('.katex');
+ expect(mathElements).toHaveLength(0);
+ });
+
+ it('handles mixed content with underscores in text and code', async () => {
+ const content = `This is some_variable in text.
+
+\`\`\`python
+def some_function():
+ return some_variable
+\`\`\`
+
+And more text_with_underscores here.`;
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(container).toHaveTextContent('This is some_variable in text.');
+ expect(container).toHaveTextContent('def some_function():');
+ expect(container).toHaveTextContent('And more text_with_underscores here.');
+ });
+
+ // Should not have any subscript elements
+ const subscripts = container.querySelectorAll('sub');
+ expect(subscripts).toHaveLength(0);
+ });
+
+ it('allows actual LaTeX math when explicitly written', async () => {
+ const content = 'The equation is: $x^2 + y^2 = z^2$';
+
+ const { container } = render();
+
+ await waitFor(() => {
+ // The math should be rendered (or at least attempted)
+ // Since we're escaping $ signs, this will NOT render as math
+ // This test verifies our escaping is working
+ expect(container).toHaveTextContent('The equation is: $x^2 + y^2 = z^2$');
+ });
+ });
+
+ it('handles file paths with underscores', async () => {
+ const content = 'Check the file at /path/to/my_file_name.txt and /another/path_to/file.log';
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(container).toHaveTextContent('/path/to/my_file_name.txt');
+ expect(container).toHaveTextContent('/another/path_to/file.log');
+ });
+
+ // Should not have any subscript elements
+ const subscripts = container.querySelectorAll('sub');
+ expect(subscripts).toHaveLength(0);
+ });
+
+ it('handles environment variables in text', async () => {
+ const content = 'Set $HOME and $PATH variables, also check $USER.';
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(container).toHaveTextContent('Set $HOME and $PATH variables, also check $USER.');
+ });
+
+ // Should not have any KaTeX math elements
+ const mathElements = container.querySelectorAll('.katex');
+ expect(mathElements).toHaveLength(0);
+ });
+
+ it('handles log output with underscores and dollar signs', async () => {
+ const content = `Command output:
+test_function_name called with $arg1="value" and $arg2="other_value"
+Error in module_name at line_number 42`;
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(container).toHaveTextContent('test_function_name');
+ expect(container).toHaveTextContent('$arg1="value"');
+ expect(container).toHaveTextContent('other_value');
+ expect(container).toHaveTextContent('module_name');
+ });
+
+ // Should not have subscripts or math elements
+ const subscripts = container.querySelectorAll('sub');
+ const mathElements = container.querySelectorAll('.katex');
+ expect(subscripts).toHaveLength(0);
+ expect(mathElements).toHaveLength(0);
+ });
+ });
+});
diff --git a/ui/desktop/src/components/MarkdownContent.tsx b/ui/desktop/src/components/MarkdownContent.tsx
new file mode 100644
index 000000000000..a665f1239acb
--- /dev/null
+++ b/ui/desktop/src/components/MarkdownContent.tsx
@@ -0,0 +1,258 @@
+import React, { useState, useEffect, useRef, memo, useMemo } from 'react';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import remarkBreaks from 'remark-breaks';
+import remarkMath from 'remark-math';
+import rehypeKatex from 'rehype-katex';
+import 'katex/dist/katex.min.css';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
+// Improved oneDark theme for better comment contrast and readability
+const customOneDarkTheme = {
+ ...oneDark,
+ 'code[class*="language-"]': {
+ ...oneDark['code[class*="language-"]'],
+ color: '#e6e6e6',
+ fontSize: '14px',
+ },
+ 'pre[class*="language-"]': {
+ ...oneDark['pre[class*="language-"]'],
+ color: '#e6e6e6',
+ fontSize: '14px',
+ },
+ comment: { ...oneDark.comment, color: '#a0a0a0', fontStyle: 'italic' },
+ prolog: { ...oneDark.prolog, color: '#a0a0a0' },
+ doctype: { ...oneDark.doctype, color: '#a0a0a0' },
+ cdata: { ...oneDark.cdata, color: '#a0a0a0' },
+};
+
+import { Check, Copy } from './icons';
+import { wrapHTMLInCodeBlock } from '../utils/htmlSecurity';
+
+interface CodeProps extends React.ClassAttributes, React.HTMLAttributes {
+ inline?: boolean;
+}
+
+interface MarkdownContentProps {
+ content: string;
+ className?: string;
+}
+
+// Memoized CodeBlock component to prevent re-rendering when props haven't changed
+const CodeBlock = memo(function CodeBlock({
+ language,
+ children,
+}: {
+ language: string;
+ children: string;
+}) {
+ const [copied, setCopied] = useState(false);
+ const timeoutRef = useRef(null);
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(children);
+ setCopied(true);
+
+ if (timeoutRef.current) {
+ window.clearTimeout(timeoutRef.current);
+ }
+
+ timeoutRef.current = window.setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error('Failed to copy text: ', err);
+ }
+ };
+
+ useEffect(() => {
+ return () => {
+ if (timeoutRef.current) {
+ window.clearTimeout(timeoutRef.current);
+ }
+ };
+ }, []);
+
+ // Memoize the SyntaxHighlighter component to prevent re-rendering
+ // Only re-render if language or children change
+ const memoizedSyntaxHighlighter = useMemo(() => {
+ // For very large code blocks, consider truncating or lazy loading
+ const isLargeCodeBlock = children.length > 10000; // 10KB threshold
+
+ if (isLargeCodeBlock) {
+ console.log(`Large code block detected (${children.length} chars), consider optimization`);
+ }
+
+ return (
+
+ {children}
+
+ );
+ }, [language, children]);
+
+ return (
+
+
+
{memoizedSyntaxHighlighter}
+
+ );
+});
+
+const MarkdownCode = memo(
+ React.forwardRef(function MarkdownCode(
+ { inline, className, children, ...props }: CodeProps,
+ ref: React.Ref
+ ) {
+ const match = /language-(\w+)/.exec(className || '');
+ return !inline && match ? (
+ {String(children).replace(/\n$/, '')}
+ ) : (
+
+ {children}
+
+ );
+ })
+);
+
+/**
+ * Escapes special characters that would be interpreted as LaTeX/KaTeX syntax,
+ * but only outside of code blocks and inline code.
+ * This prevents issues like:
+ * - `_` being interpreted as subscript
+ * - `$...$` being interpreted as inline math (e.g., in shell commands like `cmd "$FOO"`)
+ */
+function escapeLatexOutsideCode(content: string): string {
+ const parts: string[] = [];
+ let lastIndex = 0;
+
+ // Match code blocks (```...```) and inline code (`...`)
+ // This regex captures:
+ // - Fenced code blocks: ```lang\n...\n```
+ // - Inline code: `...`
+ const codePattern = /(```[\s\S]*?```|`[^`\n]+?`)/g;
+
+ let match;
+ while ((match = codePattern.exec(content)) !== null) {
+ // Add the text before this code block/inline code (with escaping)
+ const textBefore = content.slice(lastIndex, match.index);
+ parts.push(escapeLatexChars(textBefore));
+
+ // Add the code block/inline code as-is (no escaping)
+ parts.push(match[0]);
+
+ lastIndex = match.index + match[0].length;
+ }
+
+ // Add any remaining text after the last code block (with escaping)
+ if (lastIndex < content.length) {
+ parts.push(escapeLatexChars(content.slice(lastIndex)));
+ }
+
+ return parts.join('');
+}
+
+/**
+ * Escapes LaTeX special characters in plain text.
+ * Escapes:
+ * - `_` to `\_` (prevents subscript interpretation)
+ * - `$` to `\$` (prevents math mode interpretation)
+ */
+function escapeLatexChars(text: string): string {
+ return text
+ .replace(/\\/g, '\\\\') // Escape backslashes first
+ .replace(/_/g, '\\_') // Escape underscores
+ .replace(/\$/g, '\\$'); // Escape dollar signs
+}
+
+const MarkdownContent = memo(function MarkdownContent({
+ content,
+ className = '',
+}: MarkdownContentProps) {
+ const [processedContent, setProcessedContent] = useState(content);
+
+ useEffect(() => {
+ try {
+ // First wrap HTML in code blocks for security
+ let processed = wrapHTMLInCodeBlock(content);
+ // Then escape LaTeX special characters outside of code blocks
+ processed = escapeLatexOutsideCode(processed);
+ setProcessedContent(processed);
+ } catch (error) {
+ console.error('Error processing content:', error);
+ // Fallback to original content if processing fails
+ setProcessedContent(content);
+ }
+ }, [content]);
+
+ return (
+
+
,
+ code: MarkdownCode,
+ }}
+ >
+ {processedContent}
+
+
+ );
+});
+
+export default MarkdownContent;