diff --git a/apps/desktop/src/app/chat/right-rail/preview-file.tsx b/apps/desktop/src/app/chat/right-rail/preview-file.tsx
index 408e9d86b88..48e0649647a 100644
--- a/apps/desktop/src/app/chat/right-rail/preview-file.tsx
+++ b/apps/desktop/src/app/chat/right-rail/preview-file.tsx
@@ -6,7 +6,7 @@ import type {
MouseEvent as ReactMouseEvent,
ReactNode
} from 'react'
-import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { Fragment, lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import ShikiHighlighter from 'react-shiki'
import { Streamdown } from 'streamdown'
@@ -34,6 +34,10 @@ import { setPreviewDirty } from '@/store/preview-edit'
import { $currentCwd } from '@/store/session'
import { notifyWorkspaceChanged } from '@/store/workspace-events'
+const MermaidDiagram = lazy(() =>
+ import('@/components/chat/mermaid-block').then(m => ({ default: m.MermaidBlock }))
+)
+
const SHIKI_THEME = { dark: 'github-dark-default', light: 'github-light-default' } as const
const TEXT_PREVIEW_MAX_BYTES = 512 * 1024
const SOURCE_CHUNK_LINES = 200
@@ -291,6 +295,16 @@ function MarkdownCode({ className, children, ...props }: ComponentProps<'code'>)
)
}
+ if (language === 'mermaid') {
+ return (
+ Loading diagram…}
+ >
+
+
+ )
+ }
+
return (
| null = null
+
+function loadMermaid() {
+ if (!mermaidPromise) {
+ mermaidPromise = import('mermaid').then(async mod => {
+ const m = mod.default
+
+ m.initialize({
+ securityLevel: 'strict',
+ startOnLoad: false,
+ theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default'
+ })
+
+ return mod
+ })
+ }
+
+ return mermaidPromise
+}
+
+interface MermaidBlockProps {
+ chart: string
+}
+
+export function MermaidBlock({ chart }: MermaidBlockProps) {
+ const containerRef = useRef(null)
+ const [error, setError] = useState(null)
+ const [svg, setSvg] = useState(null)
+
+ useEffect(() => {
+ let cancelled = false
+
+ async function render() {
+ try {
+ const { default: mermaid } = await loadMermaid()
+ const id = `mermaid-${Math.random().toString(36).slice(2, 10)}`
+ const { svg: rendered } = await mermaid.render(id, chart)
+
+ if (!cancelled) {
+ setSvg(rendered)
+ setError(null)
+ }
+ } catch (err) {
+ if (!cancelled) {
+ setError(err instanceof Error ? err.message : String(err))
+ setSvg(null)
+ }
+ }
+ }
+
+ void render()
+
+ return () => {
+ cancelled = true
+ }
+ }, [chart])
+
+ if (error) {
+ return (
+
+
Mermaid diagram error
+
{error}
+
+ View source
+ {chart}
+
+
+ )
+ }
+
+ if (!svg) {
+ return Rendering diagram…
+ }
+
+ return (
+
+ )
+}