diff --git a/packages/ui/src/components/chat/MarkdownContent.test.tsx b/packages/ui/src/components/chat/MarkdownContent.test.tsx index 9b5b3fc..c1a5654 100644 --- a/packages/ui/src/components/chat/MarkdownContent.test.tsx +++ b/packages/ui/src/components/chat/MarkdownContent.test.tsx @@ -34,6 +34,19 @@ describe('MarkdownContent', () => { expect(container.querySelector('pre code')?.textContent).toContain('{"ok":true}'); }); + it('renders untyped fenced code as a block rather than inline code', async () => { + const container = document.createElement('div'); + const root = createRoot(container); + + await act(async () => { + root.render(); + }); + + expect(container.querySelector('pre code')?.className).toContain('font-mono'); + expect(container.querySelector('pre code')?.className).not.toContain('rounded-[4px]'); + expect(container.querySelector('p code')?.className).toContain('rounded-[4px]'); + }); + it('renders GFM tables and removes unsafe links', async () => { const container = document.createElement('div'); const root = createRoot(container); diff --git a/packages/ui/src/components/chat/MarkdownContent.tsx b/packages/ui/src/components/chat/MarkdownContent.tsx index 0c01317..07e9613 100644 --- a/packages/ui/src/components/chat/MarkdownContent.tsx +++ b/packages/ui/src/components/chat/MarkdownContent.tsx @@ -3,6 +3,7 @@ import ReactMarkdown, { type Components } from 'react-markdown'; import remarkGfm from 'remark-gfm'; const SAFE_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']); +const CodeBlockContext = React.createContext(false); /** * Keep agent-authored URLs useful without allowing navigation to executable @@ -79,6 +80,22 @@ function useStreamingContent(content: string, streaming: boolean): string { * is intentional. GFM adds the table, task-list, strike-through, and URL * behaviours people expect from a research answer. */ +const MarkdownCode: NonNullable = ({ className, children, ...props }) => { + const isBlock = React.useContext(CodeBlockContext) || Boolean(className?.includes('language-')); + return ( + + {isBlock ? String(children).replace(/\n$/, '') : children} + + ); +}; + const components: Components = { h1: ({ children }) => (

{children}

@@ -116,25 +133,13 @@ const components: Components = { ); }, - code: ({ className, children, ...props }) => { - const isBlock = Boolean(className?.includes('language-')); - return ( - - {isBlock ? String(children).replace(/\n$/, '') : children} - - ); - }, + code: MarkdownCode, pre: ({ children }) => ( -
-      {children}
-    
+ +
+        {children}
+      
+
), table: ({ children }) => (