From c0adfd4a67ca1db66ae13724d2d1286b66e4147f Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 6 Jul 2026 02:20:48 -0700 Subject: [PATCH] feat(desktop): render Mermaid code blocks in markdown file preview Salvaged from #40531; surgically reapplied onto current main (i18n'd preview-file.tsx). mermaid dep already present on main. Co-authored-by: liuhao1024 --- .../src/app/chat/right-rail/preview-file.tsx | 16 +++- .../src/components/chat/mermaid-block.tsx | 84 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/components/chat/mermaid-block.tsx 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 ( +
+ ) +}