{content}
@@ -112,5 +114,10 @@ export default function MermaidBlock({
)
}
- return
+ return (
+
+ )
}
diff --git a/src/renderer/src/components/native-chat/NativeChatMarkdown.mermaid.test.tsx b/src/renderer/src/components/native-chat/NativeChatMarkdown.mermaid.test.tsx
new file mode 100644
index 00000000000..8a9eda3b245
--- /dev/null
+++ b/src/renderer/src/components/native-chat/NativeChatMarkdown.mermaid.test.tsx
@@ -0,0 +1,70 @@
+// @vitest-environment happy-dom
+import '@testing-library/jest-dom/vitest'
+import { cleanup, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mermaid = vi.hoisted(() => ({ initialize: vi.fn(), render: vi.fn() }))
+vi.mock('mermaid', () => ({ default: mermaid }))
+vi.mock('dompurify', () => ({ default: { sanitize: (svg: string) => svg } }))
+vi.mock('@/store', () => ({
+ useAppStore: (selector: (state: { settings: { theme: string } }) => unknown) =>
+ selector({ settings: { theme: 'light' } })
+}))
+
+import { NativeChatMarkdown } from './NativeChatMarkdown'
+import { NativeChatCodeBlock } from './NativeChatCodeBlock'
+import { TooltipProvider } from '@/components/ui/tooltip'
+
+const partial = 'flowchart TD\n A["Sync with main '
+const complete = `${partial}once"]`
+
+function reply(source: string, streaming: boolean): React.JSX.Element {
+ return (
+
+
+
+ )
+}
+
+beforeEach(() => {
+ mermaid.render.mockReset()
+ mermaid.render.mockResolvedValue({ svg: '' })
+})
+afterEach(cleanup)
+
+describe('native chat Mermaid fences', () => {
+ it('shows partial source while streaming and renders the diagram when the reply finishes', async () => {
+ const { container, rerender } = render(reply(partial, true))
+ expect(container.querySelector('[data-native-chat-code-content]')?.textContent).toContain(
+ partial
+ )
+ expect(container.querySelector('[data-code-language="mermaid"]')).toBeInTheDocument()
+ expect(container.querySelector('.mermaid-block')).toBeNull()
+ expect(mermaid.render).not.toHaveBeenCalled()
+
+ rerender(reply(complete, true))
+ expect(container.querySelector('pre')?.textContent).toContain(complete)
+ expect(mermaid.render).not.toHaveBeenCalled()
+
+ rerender(reply(complete, false))
+ await waitFor(() =>
+ expect(container.querySelector('svg')).toHaveTextContent('Sync with main once')
+ )
+ expect(mermaid.render).toHaveBeenCalledWith(expect.any(String), complete)
+ expect(container.querySelector('pre')).toBeNull()
+ expect(screen.queryByText(/Diagram error:/)).not.toBeInTheDocument()
+ })
+
+ it('still shows the existing error and source for an invalid finished reply', async () => {
+ mermaid.render.mockRejectedValue(new Error('Invalid mermaid syntax'))
+ const { container } = render(reply(partial, false))
+ await screen.findByText(/Diagram error: Invalid mermaid syntax/)
+ expect(container.querySelector('pre')?.textContent).toBe(partial.trimEnd())
+ expect(container.querySelector('svg')).toBeNull()
+ })
+})
diff --git a/src/renderer/src/components/native-chat/NativeChatMarkdown.tsx b/src/renderer/src/components/native-chat/NativeChatMarkdown.tsx
index 4033b51fc62..d93b2110787 100644
--- a/src/renderer/src/components/native-chat/NativeChatMarkdown.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatMarkdown.tsx
@@ -27,6 +27,7 @@ export function NativeChatMarkdown({
extension && streaming ? withoutPendingNativeChatVisualDirectiveTail(content) : content
}
extension={extension}
+ renderMermaid={!streaming}
className={cn('native-chat-markdown', className)}
/>
)
diff --git a/src/renderer/src/components/sidebar/CommentMarkdown.tsx b/src/renderer/src/components/sidebar/CommentMarkdown.tsx
index 190d93a768d..8ea88eb578b 100644
--- a/src/renderer/src/components/sidebar/CommentMarkdown.tsx
+++ b/src/renderer/src/components/sidebar/CommentMarkdown.tsx
@@ -220,6 +220,7 @@ type CommentMarkdownProps = React.ComponentPropsWithoutRef<'div'> & {
linkifyFilePaths?: boolean
expandImages?: boolean
renderCodeBlock?: DocumentCodeBlockRenderer
+ renderMermaid?: boolean
extension?: CommentMarkdownExtension
}
@@ -238,6 +239,7 @@ const CommentMarkdown = React.memo(
linkifyFilePaths = false,
expandImages = false,
renderCodeBlock,
+ renderMermaid = true,
extension,
...rest
},
@@ -246,17 +248,17 @@ const CommentMarkdown = React.memo(
const baseComponents = React.useMemo(() => {
if (!onLinkClick) {
return variant === 'document'
- ? renderCodeBlock
- ? createDocumentCommentMarkdownComponents(undefined, renderCodeBlock)
+ ? renderCodeBlock || !renderMermaid
+ ? createDocumentCommentMarkdownComponents(undefined, renderCodeBlock, renderMermaid)
: documentCommentMarkdownComponents
: expandImages
? createCompactCommentMarkdownComponents(undefined, true)
: compactCommentMarkdownComponents
}
return variant === 'document'
- ? createDocumentCommentMarkdownComponents(onLinkClick, renderCodeBlock)
+ ? createDocumentCommentMarkdownComponents(onLinkClick, renderCodeBlock, renderMermaid)
: createCompactCommentMarkdownComponents(onLinkClick, expandImages)
- }, [expandImages, renderCodeBlock, variant, onLinkClick])
+ }, [expandImages, renderCodeBlock, renderMermaid, variant, onLinkClick])
const components = React.useMemo(
() => (extension ? { ...baseComponents, ...extension.components } : baseComponents),
[baseComponents, extension]
diff --git a/src/renderer/src/components/sidebar/comment-markdown-element-renderers.tsx b/src/renderer/src/components/sidebar/comment-markdown-element-renderers.tsx
index bfcfedfa92a..9ca2a24271b 100644
--- a/src/renderer/src/components/sidebar/comment-markdown-element-renderers.tsx
+++ b/src/renderer/src/components/sidebar/comment-markdown-element-renderers.tsx
@@ -250,7 +250,8 @@ export function createCompactCommentMarkdownComponents(
export function createDocumentCommentMarkdownComponents(
onLinkClick?: CommentMarkdownLinkClickHandler,
- renderCodeBlock?: DocumentCodeBlockRenderer
+ renderCodeBlock?: DocumentCodeBlockRenderer,
+ renderMermaid = true
): Components {
return {
p: ({ children }) => {children}
,
@@ -272,7 +273,7 @@ export function createDocumentCommentMarkdownComponents(
),
code: ({ className, children }) =>
- isMermaidFence(className) ? (
+ renderMermaid && isMermaidFence(className) ? (
renderMermaidFence(
children,
'my-3 min-w-0 max-w-full overflow-x-auto rounded-md border border-border/60 p-3 [&_.mermaid-block]:min-w-0 [&_.mermaid-block_pre]:my-0 [&_.mermaid-block_pre]:max-h-80 [&_.mermaid-block_pre]:max-w-full [&_.mermaid-block_pre]:overflow-x-auto [&_.mermaid-block_pre]:rounded-md [&_.mermaid-block_pre]:bg-accent [&_.mermaid-block_pre]:p-3 [&_.mermaid-block_pre]:font-mono [&_.mermaid-block_pre]:text-[12px]'
@@ -284,7 +285,7 @@ export function createDocumentCommentMarkdownComponents(
),
// Mermaid fences render a , which is invalid inside , so unwrap them.
pre: ({ children }) =>
- isMermaidPre(children) ? (
+ renderMermaid && isMermaidPre(children) ? (
<>{children}>
) : renderCodeBlock ? (
renderCodeBlock({ children, language: extractCodeFenceLanguage(children) })