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(
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}