From 8ae4e4ba02836529790f47109b8785e8b42843a7 Mon Sep 17 00:00:00 2001 From: Jeff Ramnani Date: Fri, 28 Aug 2026 20:09:13 +0000 Subject: [PATCH] fix(ui): render untagged fenced code blocks as proper code blocks (#11653) --- .../src/components/MarkdownContent.test.tsx | 35 +++++++++++++++++++ ui/desktop/src/components/MarkdownContent.tsx | 11 ++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/ui/desktop/src/components/MarkdownContent.test.tsx b/ui/desktop/src/components/MarkdownContent.test.tsx index d8c9c36e9..552089016 100644 --- a/ui/desktop/src/components/MarkdownContent.test.tsx +++ b/ui/desktop/src/components/MarkdownContent.test.tsx @@ -172,6 +172,41 @@ console.log('Hello, World!'); expect(screen.getByText('console.log()')).toBeInTheDocument(); }); }); + + it('renders untagged fenced code blocks (no language) through the same CodeBlock component as tagged blocks', async () => { + const plainTextBlock = [ + 'first line of plain text', + 'second line of plain text', + '', + 'line after a blank line', + ].join('\n'); + const content = ['```', plainTextBlock, '```'].join('\n'); + + const { container } = renderWithIntl(); + + await waitFor(() => { + expect(screen.getByText(/first line of plain text/)).toBeInTheDocument(); + }); + + // There should be exactly one
 element wrapping exactly one 
+      // element that contains the entire multi-line block as a single node,
+      // rather than one /
 pairing per line.
+      const preElements = container.querySelectorAll('pre');
+      expect(preElements).toHaveLength(1);
+
+      const codeElementsInsidePre = preElements[0].querySelectorAll('code');
+      expect(codeElementsInsidePre).toHaveLength(1);
+      expect(codeElementsInsidePre[0].textContent).toContain('first line of plain text');
+      expect(codeElementsInsidePre[0].textContent).toContain('line after a blank line');
+
+      // The block should NOT use the small single-line "inline code" badge
+      // style - that would indicate the bug regressed.
+      expect(codeElementsInsidePre[0].className).not.toContain('bg-inline-code');
+
+      // It should get the same hover "copy" button that tagged code blocks
+      // get, since it now renders through the shared CodeBlock component.
+      expect(preElements[0].querySelector('[data-testid="copy-icon"]')).toBeInTheDocument();
+    });
   });
 
   describe('Markdown Features', () => {
diff --git a/ui/desktop/src/components/MarkdownContent.tsx b/ui/desktop/src/components/MarkdownContent.tsx
index 91afe79a7..e8d924f9e 100644
--- a/ui/desktop/src/components/MarkdownContent.tsx
+++ b/ui/desktop/src/components/MarkdownContent.tsx
@@ -152,8 +152,15 @@ const MarkdownCode = memo(
     ref: React.Ref
   ) {
     const match = /language-(\w+)/.exec(className || '');
-    return !inline && match ? (
-      {String(children).replace(/\n$/, '')}
+    const codeContent = String(children ?? '');
+
+    // react-markdown gives untagged fenced blocks no language-xxx className,
+    // so they look like inline code here. Block-level content always ends with
+    // a trailing newline, which inline code spans can never contain.
+    const isBlockLevelCode = !inline && codeContent.endsWith('\n');
+
+    return isBlockLevelCode ? (
+      {codeContent.replace(/\n$/, '')}
     ) : (
       
         {children}