diff --git a/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.test.tsx b/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.test.tsx
index d333bf3fa0..d4ce73065c 100644
--- a/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.test.tsx
+++ b/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.test.tsx
@@ -46,4 +46,22 @@ describe('MermaidRenderer', () => {
])
expect(decodeURIComponent(images[0]?.src.split(',')[1] ?? '')).toContain('marker-end="url(#arrow)"')
})
+
+ it('repairs a label mermaid returned with an open
, so the image stays loadable', async () => {
+ // `A[x
y]` comes back from mermaid as `
` — malformed XML, so the
+ // data: URI fails to decode and the message shows a broken image with the
+ // alt text instead of the diagram (#133089).
+ renderMermaid.mockResolvedValueOnce({
+ svg: ''
+ })
+
+ const { container } = render(
')
+ expect(new DOMParser().parseFromString(src, 'image/svg+xml').documentElement.tagName).toBe('svg')
+ })
})
diff --git a/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.tsx b/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.tsx
index a7514c5426..2dc7a93491 100644
--- a/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.tsx
+++ b/apps/desktop/src/components/assistant-ui/embeds/mermaid-embed.tsx
@@ -4,7 +4,7 @@ import type { Mermaid as MermaidApi } from 'mermaid'
import { useEffect, useState } from 'react'
import { Zoomable } from '@/components/ui/zoomable'
-import { copySvgAsPng, normalizeSvgSize } from '@/lib/svg-image'
+import { copySvgAsPng, normalizeSvgSize, xmlWellFormedSvg } from '@/lib/svg-image'
import { cn } from '@/lib/utils'
import { createMermaidRenderCache, createRetryableLoader, nextPaint } from './mermaid-render-cache'
@@ -36,7 +36,10 @@ const renderCache = createMermaidRenderCache({
const id = `mmd-${Math.random().toString(36).slice(2)}`
const result = await mermaid.render(id, code)
- return normalizeSvgSize(result.svg)
+ // Mermaid's markup targets inline HTML, so a `
` in a label reaches us
+ // as an open `
`; close void tags before the size pass and before the
+ // data: URI below, or the diagram decodes to a broken image (#133089).
+ return normalizeSvgSize(xmlWellFormedSvg(result.svg))
},
// Defer until the source fallback has had a frame to paint, so the mermaid
// runtime import and parse/layout cannot contend with first paint of the
diff --git a/apps/desktop/src/lib/svg-image.test.ts b/apps/desktop/src/lib/svg-image.test.ts
index 2e81a5958c..4197f39290 100644
--- a/apps/desktop/src/lib/svg-image.test.ts
+++ b/apps/desktop/src/lib/svg-image.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
-import { normalizeSvgSize, svgSize } from './svg-image'
+import { normalizeSvgSize, svgSize, xmlWellFormedSvg } from './svg-image'
// Real mermaid 11.16 render output shape (verified against the installed
// package): width="100%" + inline style="max-width: Npx" + viewBox.
@@ -64,3 +64,41 @@ describe('normalizeSvgSize', () => {
expect(normalizeSvgSize(svg)).toBe(svg)
})
})
+
+// A mermaid label keeps HTML void tags open: `
` in the source comes back
+// from `mermaid.render()` as `
`. That makes the string malformed XML, so
+// every strict `image/svg+xml` parse below degrades — and Blink fails the same
+// way when the string is handed to an as a data: URI, which is how a
+// diagram silently stops rendering (broken image, no error anywhere).
+describe('xmlWellFormedSvg', () => {
+ const rootOf = (svg: string) => new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement.tagName
+
+ it('closes a void tag mermaid left open inside a label', () => {
+ expect(xmlWellFormedSvg('
x
x
')).toBe('
')
+ })
+
+ it('leaves already-closed void tags untouched', () => {
+ const svg = '
' + + expect(xmlWellFormedSvg(svg)).toBe(svg) + }) + + it('makes a label withx y