diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index ae8fe89745..23394805a6 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -48,6 +48,7 @@ "react-select": "^5.10.2", "react-syntax-highlighter": "^15.6.1", "react-toastify": "^11.0.5", + "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", "split-type": "^0.3.4", "swr": "^2.3.6", @@ -13417,6 +13418,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-newline-to-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-find-and-replace": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-phrasing": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", @@ -16386,6 +16401,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remark-breaks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-newline-to-break": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index b8dd418f4f..91465afcd1 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -78,6 +78,7 @@ "react-select": "^5.10.2", "react-syntax-highlighter": "^15.6.1", "react-toastify": "^11.0.5", + "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", "split-type": "^0.3.4", "swr": "^2.3.6", diff --git a/ui/desktop/src/components/MarkdownContent.test.tsx b/ui/desktop/src/components/MarkdownContent.test.tsx index 163ecb2afd..889fb62496 100644 --- a/ui/desktop/src/components/MarkdownContent.test.tsx +++ b/ui/desktop/src/components/MarkdownContent.test.tsx @@ -260,4 +260,66 @@ Unclosed code block`; }); }); }); + + 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(); + }); + }); + }); }); diff --git a/ui/desktop/src/components/MarkdownContent.tsx b/ui/desktop/src/components/MarkdownContent.tsx index 734af41716..68d05d8849 100644 --- a/ui/desktop/src/components/MarkdownContent.tsx +++ b/ui/desktop/src/components/MarkdownContent.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; +import remarkBreaks from 'remark-breaks'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { Check, Copy } from './icons'; @@ -126,7 +127,7 @@ export default function MarkdownContent({ content, className = '' }: MarkdownCon prose-li:m-0prose-li:font-sans ${className}`} > , code: MarkdownCode,