feat: render mermaid code blocks as interactive diagrams (#124)#128
feat: render mermaid code blocks as interactive diagrams (#124)#128
Conversation
Mermaid fenced code blocks in AI responses are now rendered as SVG diagrams instead of raw text. Includes code/diagram toggle, dark/light theme support, and error fallback for invalid syntax. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a significant enhancement to how code blocks are displayed, specifically enabling the interactive rendering of Mermaid diagrams. Previously, Mermaid syntax would appear as raw text, but now it transforms into dynamic SVG diagrams. This feature greatly improves the user experience by providing immediate visual feedback for diagrammatic code, complete with theme adaptation, a toggle to view the underlying code, and built-in error handling for malformed syntax. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Pull request overview
Adds first-class rendering for Mermaid fenced code blocks in chat markdown so diagrams display as SVG with a code/diagram toggle, theme support, and error fallback.
Changes:
- Introduces a new
MermaidViewerReact component that renders Mermaid code to SVG and provides a header UI (toggle + copy). - Updates both markdown renderers (
MarkdownViewerandcreateMarkdownComponents) to detectlanguage-mermaidcode fences and render them as diagrams, skipping the<pre>wrapper. - Adds the
mermaiddependency (plus lockfile updates) and exports the new viewer.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/renderer/components/chat/viewers/index.ts | Exports MermaidViewer from the viewers barrel. |
| src/renderer/components/chat/viewers/MermaidViewer.tsx | New diagram viewer: Mermaid init, theme watching, SVG rendering, toggle/error UI. |
| src/renderer/components/chat/viewers/MarkdownViewer.tsx | Detects language-mermaid blocks and bypasses <pre> wrapping for diagram output. |
| src/renderer/components/chat/markdownComponents.tsx | Same mermaid handling for the non-viewer markdown component factory. |
| package.json | Adds mermaid as a runtime dependency. |
| pnpm-lock.yaml | Locks Mermaid and its transitive dependencies. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let mermaidInitialized = false; | ||
|
|
||
| function ensureMermaidInit(isDark: boolean): void { | ||
| const theme = isDark ? 'dark' : 'default'; | ||
| mermaid.initialize({ | ||
| startOnLoad: false, | ||
| theme, | ||
| securityLevel: 'strict', | ||
| fontFamily: 'ui-sans-serif, system-ui, sans-serif', | ||
| }); | ||
| mermaidInitialized = true; |
There was a problem hiding this comment.
ensureMermaidInit always calls mermaid.initialize(...) and sets mermaidInitialized = true, but there’s no guard that uses mermaidInitialized to avoid repeated initialization. As written, every diagram render (and every theme flip) re-initializes the global mermaid singleton, and the follow-up effect that sets mermaidInitialized = false doesn’t change behavior. Consider tracking the last initialized theme (e.g. 'dark' | 'default') and only calling mermaid.initialize when it changes, or remove mermaidInitialized entirely if re-init on every render is intended.
| let mermaidInitialized = false; | |
| function ensureMermaidInit(isDark: boolean): void { | |
| const theme = isDark ? 'dark' : 'default'; | |
| mermaid.initialize({ | |
| startOnLoad: false, | |
| theme, | |
| securityLevel: 'strict', | |
| fontFamily: 'ui-sans-serif, system-ui, sans-serif', | |
| }); | |
| mermaidInitialized = true; | |
| let lastMermaidTheme: 'dark' | 'default' | null = null; | |
| function ensureMermaidInit(isDark: boolean): void { | |
| const theme: 'dark' | 'default' = isDark ? 'dark' : 'default'; | |
| // Avoid redundant global re-initialization when the theme hasn't changed. | |
| if (lastMermaidTheme === theme) { | |
| return; | |
| } | |
| mermaid.initialize({ | |
| startOnLoad: false, | |
| theme, | |
| securityLevel: 'strict', | |
| fontFamily: 'ui-sans-serif, system-ui, sans-serif', | |
| }); | |
| lastMermaidTheme = theme; |
| const [isDark, setIsDark] = useState( | ||
| () => | ||
| document.documentElement.classList.contains('dark') || | ||
| !document.documentElement.classList.contains('light') | ||
| ); | ||
|
|
||
| // Watch theme changes | ||
| useEffect(() => { | ||
| const observer = new MutationObserver(() => { | ||
| const dark = | ||
| document.documentElement.classList.contains('dark') || | ||
| !document.documentElement.classList.contains('light'); | ||
| setIsDark(dark); | ||
| }); | ||
| observer.observe(document.documentElement, { | ||
| attributes: true, | ||
| attributeFilter: ['class'], | ||
| }); | ||
| return () => observer.disconnect(); | ||
| }, []); |
There was a problem hiding this comment.
This component sets up a MutationObserver per Mermaid diagram instance to watch the <html> class for theme changes. The codebase already has a centralized useTheme() hook that resolves dark/light/system and applies the root class; using that hook here would avoid extra observers and keep theme logic consistent across the app.
| </div> | ||
| ) : svg ? ( | ||
| <div | ||
| ref={containerRef} |
There was a problem hiding this comment.
containerRef is created and attached to the rendered SVG container, but it’s never read. Either remove it to avoid dead code, or use it to apply post-render handling (e.g., if you need to run Mermaid’s returned binding function on the container for interactive behaviors).
| ref={containerRef} |
| if (isBlock) { | ||
| const lang = className?.replace('language-', '') ?? ''; | ||
| const text = content.replace(/\n$/, ''); | ||
|
|
||
| if (lang === 'mermaid') { | ||
| return <MermaidViewer code={text} />; | ||
| } |
There was a problem hiding this comment.
createMarkdownComponents now renders language-mermaid fenced code blocks via MermaidViewer, but there are existing tests that exercise createMarkdownComponents (e.g. markdown search renderer alignment) and none cover this new mermaid path. Add a renderer test case with a mermaid fenced block to ensure it returns the diagram viewer and doesn’t get wrapped in a <pre> (and that search highlighting/indexing isn’t inadvertently impacted).
There was a problem hiding this comment.
Code Review
The pull request introduces support for rendering Mermaid diagrams within markdown content by integrating the mermaid library and creating a new MermaidViewer component. This component dynamically renders Mermaid code into SVG, adapts to UI theme changes, and provides UI controls for viewing raw code and copying. Review feedback indicates an inefficiency in the MermaidViewer's initialization logic, recommending a revised approach to prevent redundant mermaid.initialize() calls, and suggests enhancing error logging to capture the full error object for improved debugging.
| let mermaidInitialized = false; | ||
|
|
||
| function ensureMermaidInit(isDark: boolean): void { | ||
| const theme = isDark ? 'dark' : 'default'; | ||
| mermaid.initialize({ | ||
| startOnLoad: false, | ||
| theme, | ||
| securityLevel: 'strict', | ||
| fontFamily: 'ui-sans-serif, system-ui, sans-serif', | ||
| }); | ||
| mermaidInitialized = true; | ||
| } |
There was a problem hiding this comment.
The current logic for initializing Mermaid causes mermaid.initialize() to be called on every component render. This is inefficient and goes against the library's best practice of calling it only when the configuration changes. This happens because the useEffect at lines 92-97 resets the mermaidInitialized flag after every render.
A better approach is to track the theme used for initialization at the module level and only re-initialize when the theme actually changes.
Please replace the mermaidInitialized flag and ensureMermaidInit function with the suggested code below. Also, you will need to remove the useEffect hook at lines 92-97, as it will no longer be necessary.
| let mermaidInitialized = false; | |
| function ensureMermaidInit(isDark: boolean): void { | |
| const theme = isDark ? 'dark' : 'default'; | |
| mermaid.initialize({ | |
| startOnLoad: false, | |
| theme, | |
| securityLevel: 'strict', | |
| fontFamily: 'ui-sans-serif, system-ui, sans-serif', | |
| }); | |
| mermaidInitialized = true; | |
| } | |
| let currentMermaidThemeIsDark: boolean | null = null; | |
| function ensureMermaidInit(isDark: boolean): void { | |
| // Only re-initialize if the theme has changed. | |
| if (currentMermaidThemeIsDark === isDark) { | |
| return; | |
| } | |
| const theme = isDark ? 'dark' : 'default'; | |
| mermaid.initialize({ | |
| startOnLoad: false, | |
| theme, | |
| securityLevel: 'strict', | |
| fontFamily: 'ui-sans-serif, system-ui, sans-serif', | |
| }); | |
| currentMermaidThemeIsDark = isDark; | |
| } |
| if (!cancelled) { | ||
| setError(err instanceof Error ? err.message : 'Failed to render mermaid diagram'); | ||
| setSvg(''); | ||
| } |
There was a problem hiding this comment.
It's great that you're catching errors from the Mermaid renderer. To aid in debugging, it would be beneficial to log the full error object to the console, as it might contain more details than just the message string, especially for non-Error objects.
if (!cancelled) {
console.error('Failed to render mermaid diagram:', err);
setError(err instanceof Error ? err.message : 'Failed to render mermaid diagram');
setSvg('');
}
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a235c6f06
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (lang === 'mermaid') { | ||
| return <MermaidViewer code={text} />; |
There was a problem hiding this comment.
Preserve search highlighting for
mermaid code blocks
When a search query matches text inside a ```mermaid fence, findMarkdownSearchMatches still counts those hits because `src/shared/utils/markdownTextSearch.ts` treats block `code` nodes as searchable text that must stay aligned with `createMarkdownComponents`. This early return bypasses `hl(children)`, so chat search can jump to a message and report a match count, but the mermaid block never renders any `` for the hit (the same regression is mirrored in `MarkdownViewer.tsx`).
Useful? React with 👍 / 👎.
| try { | ||
| ensureMermaidInit(isDark); | ||
| const id = `mermaid-${uniqueId}`; | ||
| const { svg: rendered } = await mermaid.render(id, code); |
There was a problem hiding this comment.
Keep Mermaid interaction hooks when rendering the SVG
For diagrams that use Mermaid interactions like click handlers or tooltips, mermaid.render() returns a bindFunctions callback that has to be run after the SVG is inserted into the DOM. This code only keeps svg, then injects it with dangerouslySetInnerHTML, so those diagrams render as static images and lose the interactive behavior this feature is meant to add.
Useful? React with 👍 / 👎.
- Track last theme to skip redundant mermaid.initialize() calls - Replace MutationObserver with useTheme() hook for consistency - Remove unused containerRef - Add console.error for full error logging on render failure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds Mermaid diagram support: installs the mermaid dependency, introduces a new exported Changes
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/renderer/components/chat/viewers/MarkdownViewer.tsx (1)
36-36: Please route the new Mermaid imports throughviewers/index.ts.This file and
src/renderer/components/chat/markdownComponents.tsxboth importMermaidViewerfrom the concrete file even thoughsrc/renderer/components/chat/viewers/index.tsnow exports it. Using the barrel keeps the domain boundary consistent.As per coding guidelines, "Use barrel exports from domain folders for imports (e.g.,
import { ChunkBuilder, ProjectScanner } from './services')."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/chat/viewers/MarkdownViewer.tsx` at line 36, The import of MermaidViewer in MarkdownViewer.tsx (and in markdownComponents.tsx) should use the barrel export from viewers/index.ts instead of importing directly from the concrete file; update the import statements to import { MermaidViewer } from '.../viewers' (the viewers barrel) so the domain boundary is respected and future re-exports remain centralized, keeping the symbol MermaidViewer referenced but routed through viewers/index.ts.src/renderer/components/chat/markdownComponents.tsx (1)
121-126: Consolidate duplicated Mermaid handling and harden theprerenderer.Both
markdownComponents.tsxandMarkdownViewer.tsxcontain identical code andprehandlers for Mermaid detection. Additionally,React.Children.only(children)throws ifreact-markdownever emits more than one child element. UsingChildren.toArray()withisValidElement()and a length check provides defensive safety without adding complexity.Safer local unwrap
pre: ({ children }) => { - const child = React.Children.only(children) as React.ReactElement; - if (child?.type === MermaidViewer) { - return children as React.ReactElement; + const items = React.Children.toArray(children); + const child = items[0]; + if (items.length === 1 && React.isValidElement(child) && child.type === MermaidViewer) { + return child; } return (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/chat/markdownComponents.tsx` around lines 121 - 126, The pre-renderer duplicates Mermaid handling and unsafely unwraps children using React.Children.only; update the pre handler in markdownComponents.tsx to delegate Mermaid detection to the single source of truth (the existing detection in MarkdownViewer.tsx or a new shared util) and replace React.Children.only(children) with a safe unwrap using React.Children.toArray(children), filter via React.isValidElement, ensure the resulting array has exactly one element, then read its props.children for the code text; keep the existing lang check (className?.replace('language-', '')) and MermaidViewer usage (MermaidViewer) but remove the duplicated branch so only the consolidated location handles Mermaid blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/renderer/components/chat/viewers/MermaidViewer.tsx`:
- Around line 21-29: The code intentionally renders static, non-interactive SVGs
because ensureMermaidInit calls mermaid.initialize with securityLevel: 'strict'
and the mermaid.render path never uses the returned bindFunctions; if you want
interactive diagrams instead, change mermaid.initialize securityLevel from
'strict' to a less restrictive value like 'loose' or 'antiscript' in
ensureMermaidInit and, where mermaid.render is called (the code that receives
bindFunctions), invoke bindFunctions on the injected container element so
Mermaid click/link directives are bound; otherwise leave as-is to maintain the
current security-first behavior.
---
Nitpick comments:
In `@src/renderer/components/chat/markdownComponents.tsx`:
- Around line 121-126: The pre-renderer duplicates Mermaid handling and unsafely
unwraps children using React.Children.only; update the pre handler in
markdownComponents.tsx to delegate Mermaid detection to the single source of
truth (the existing detection in MarkdownViewer.tsx or a new shared util) and
replace React.Children.only(children) with a safe unwrap using
React.Children.toArray(children), filter via React.isValidElement, ensure the
resulting array has exactly one element, then read its props.children for the
code text; keep the existing lang check (className?.replace('language-', ''))
and MermaidViewer usage (MermaidViewer) but remove the duplicated branch so only
the consolidated location handles Mermaid blocks.
In `@src/renderer/components/chat/viewers/MarkdownViewer.tsx`:
- Line 36: The import of MermaidViewer in MarkdownViewer.tsx (and in
markdownComponents.tsx) should use the barrel export from viewers/index.ts
instead of importing directly from the concrete file; update the import
statements to import { MermaidViewer } from '.../viewers' (the viewers barrel)
so the domain boundary is respected and future re-exports remain centralized,
keeping the symbol MermaidViewer referenced but routed through viewers/index.ts.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9aae5294-c939-4824-b15c-e50f89291821
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
package.jsonsrc/renderer/components/chat/markdownComponents.tsxsrc/renderer/components/chat/viewers/MarkdownViewer.tsxsrc/renderer/components/chat/viewers/MermaidViewer.tsxsrc/renderer/components/chat/viewers/index.ts
| function ensureMermaidInit(isDark: boolean): void { | ||
| const theme = isDark ? 'dark' : 'default'; | ||
| mermaid.initialize({ | ||
| startOnLoad: false, | ||
| theme, | ||
| securityLevel: 'strict', | ||
| fontFamily: 'ui-sans-serif, system-ui, sans-serif', | ||
| }); | ||
| mermaidInitialized = true; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/renderer/components/chat/viewers/MermaidViewer.tsxRepository: matt1398/claude-devtools
Length of output: 5744
🌐 Web query:
Mermaid securityLevel strict disable interactivity click directives
💡 Result:
In Mermaid.js, the securityLevel configuration controls security and interactivity for rendered diagrams, particularly click directives and callbacks. To disable interactivity and click directives while maintaining strict security: - Set securityLevel: "strict" (default). This encodes HTML tags in text and disables click functionality, preventing click directives like click A callback or click A "url". Available securityLevel values: - "strict" (default): HTML encoded, clicks disabled. - "loose": HTML allowed, clicks enabled. - "antiscript": HTML allowed (scripts removed), clicks enabled (note: recent bug reports indicate clicks may be disabled in some versions). - "sandbox": Renders in sandboxed iframe, no JS execution, hinders interactivity. Configuration methods: 1. Global via mermaid.initialize: mermaid.initialize({ securityLevel: 'strict' }); 2. Per-diagram via directives (%%{init}%%): %%{init: { "securityLevel": "strict" }}%% graph LR A --> B For maximum security with untrusted input, use "strict" or "sandbox". Clicks require "loose" or "antiscript". Directive changes to securityLevel may be restricted if secure array includes it (default: ["securityLevel", ...]). Sources confirm "strict" explicitly disables clicks for security (official schema/docs). Use directives for diagram-specific overrides on top of global config.
Citations:
- 1: https://mermaid.js.org/config/directives.html
- 2: https://mermaid.js.org/config/schema-docs/config-properties-securitylevel.html
- 3: https://mermaid.ai/open-source/config/schema-docs/config.html
- 4: https://github.com/mermaid-js/mermaid/blob/develop/docs/config/usage.md
- 5: Flowchart
securityLevel: "antiscript"disables click callback functionality mermaid-js/mermaid#5944 - 6: https://labs.snyk.io/resources/exploiting-diagram-renderers/
- 7: https://stackoverflow.com/questions/78141342/click-callback-in-mermaid-not-working-in-tutorial
🌐 Web query:
Mermaid render bindFunctions what does it do
💡 Result:
The bindFunctions is an optional function returned by Mermaid's render API as part of the RenderResult object. It binds event listeners (such as click handlers defined in the diagram via click statements) to the SVG elements after the SVG code has been inserted into the DOM. This is necessary because manually inserting the SVG via innerHTML bypasses Mermaid's automatic event binding. Usage example: const { svg, bindFunctions } = await mermaid.render('id1', 'graph TD;A-->B click A callback'); element.innerHTML = svg; bindFunctions?.(element); Without calling bindFunctions, interactive features like clicks won't work when using the render API for dynamic diagrams.
Citations:
- 1: http://mermaid.js.org/config/setup/mermaid/interfaces/RenderResult.html
- 2: https://mermaid.ai/open-source/config/setup/mermaid/interfaces/RenderResult.html
- 3: https://mermaid.ai/open-source/config/usage.html
- 4: https://stackoverflow.com/questions/72922602/event-on-node-not-calling-the-function-when-using-the-render-function
Static SVG rendering is intentional due to security settings, but interactive click directives are disabled by design.
securityLevel: 'strict' (line 30) explicitly disables Mermaid click directives, and the bindFunctions returned by mermaid.render() (line 58) is never called, preventing event binding even if security allowed it. This is a deliberate security-first approach for untrusted AI-generated content, but users should be aware diagrams will not support Mermaid's click or link directives. If interactivity is desired, reduce security level to 'loose' or 'antiscript' and call bindFunctions on the container element after injection.
Also applies to: 72-74
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/components/chat/viewers/MermaidViewer.tsx` around lines 21 - 29,
The code intentionally renders static, non-interactive SVGs because
ensureMermaidInit calls mermaid.initialize with securityLevel: 'strict' and the
mermaid.render path never uses the returned bindFunctions; if you want
interactive diagrams instead, change mermaid.initialize securityLevel from
'strict' to a less restrictive value like 'loose' or 'antiscript' in
ensureMermaidInit and, where mermaid.render is called (the code that receives
bindFunctions), invoke bindFunctions on the injected container element so
Mermaid click/link directives are bound; otherwise leave as-is to maintain the
current security-first behavior.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/renderer/components/chat/viewers/MermaidViewer.tsx (3)
95-103: Add button accessibility state metadata.Expose pressed state for assistive tech and set explicit button type.
♿ Suggested update
<button + type="button" onClick={() => setShowCode(!showCode)} + aria-pressed={showCode} + aria-label={showCode ? 'Show diagram' : 'Show code'} className="flex items-center gap-1 rounded px-1.5 py-0.5 text-xs transition-colors hover:bg-white/10" style={{ color: COLOR_TEXT_MUTED }} title={showCode ? 'Show diagram' : 'Show code'} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/chat/viewers/MermaidViewer.tsx` around lines 95 - 103, The toggle button in MermaidViewer (the onClick using setShowCode and the showCode state) needs explicit accessibility metadata: add type="button" and set aria-pressed to the current boolean state (aria-pressed={showCode}) so assistive tech knows the pressed/toggled state; keep the existing title and text but ensure aria-pressed references the showCode symbol and the button element includes the type attribute.
1-14: Reorder imports to match repository import order.External packages should be grouped before
@renderer/*path aliases.♻️ Suggested import order
import React, { useEffect, useId, useState } from 'react'; +import { Code, GitBranch } from 'lucide-react'; +import mermaid from 'mermaid'; import { CopyButton } from '@renderer/components/common/CopyButton'; import { CODE_BG, CODE_BORDER, @@ } from '@renderer/constants/cssVariables'; import { useTheme } from '@renderer/hooks/useTheme'; -import { Code, GitBranch } from 'lucide-react'; -import mermaid from 'mermaid';As per coding guidelines: "Organize imports in order: external packages, path aliases (
@main,@renderer,@shared,@preload), then relative imports".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/chat/viewers/MermaidViewer.tsx` around lines 1 - 14, The import order in MermaidViewer.tsx is incorrect; reorder imports so external packages (React, mermaid, lucide-react) come first, then path aliases like `@renderer/`* (e.g., `@renderer/hooks/useTheme`, `@renderer/components/common/CopyButton`, `@renderer/constants/cssVariables`), and finally any relative imports; update the import block at the top of the file (where useEffect, useId, useState, mermaid, Code, GitBranch, useTheme, CopyButton, and the CSS constants are imported) to follow that sequence.
52-71: Reset render state when a new render starts to avoid stale diagram flashes.When
codechanges, previoussvgremains visible until the new async render resolves.💡 Suggested tweak
useEffect(() => { let cancelled = false; const render = async (): Promise<void> => { + if (!cancelled) { + setError(null); + setSvg(''); + } try { ensureMermaidInit(isDark); const id = `mermaid-${uniqueId}`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/chat/viewers/MermaidViewer.tsx` around lines 52 - 71, When starting the async render inside useEffect/render, clear the previous render state immediately to avoid flashes: inside the render() function (before calling ensureMermaidInit/mermaid.render and referencing uniqueId/code), call setSvg('') and setError(null) so the old SVG is cleared and error state reset while the new render is pending; keep the existing cancelled guard and error handling around mermaid.render (functions/variables: useEffect, render, ensureMermaidInit, mermaid.render, uniqueId, code, cancelled, setSvg, setError).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/renderer/components/chat/viewers/MermaidViewer.tsx`:
- Line 58: The SVG returned from mermaid.render in MermaidViewer (the const {
svg: rendered } = await mermaid.render(id, code) usage) is user/AI-controlled
and must be sanitized before being passed to dangerouslySetInnerHTML; update the
component to run the rendered SVG through a sanitizer such as DOMPurify (e.g.,
DOMPurify.sanitize(rendered, { ADD_TAGS/ATTRS if needed, FORBID_TAGS:
['script','foreignObject'], FORBID_ATTR: ['on*','xlink:href'] })) and use the
sanitized result for injection, and apply the same sanitization where the
component injects SVG elsewhere in this file (the other mermaid
render/dangerouslySetInnerHTML usage).
---
Nitpick comments:
In `@src/renderer/components/chat/viewers/MermaidViewer.tsx`:
- Around line 95-103: The toggle button in MermaidViewer (the onClick using
setShowCode and the showCode state) needs explicit accessibility metadata: add
type="button" and set aria-pressed to the current boolean state
(aria-pressed={showCode}) so assistive tech knows the pressed/toggled state;
keep the existing title and text but ensure aria-pressed references the showCode
symbol and the button element includes the type attribute.
- Around line 1-14: The import order in MermaidViewer.tsx is incorrect; reorder
imports so external packages (React, mermaid, lucide-react) come first, then
path aliases like `@renderer/`* (e.g., `@renderer/hooks/useTheme`,
`@renderer/components/common/CopyButton`, `@renderer/constants/cssVariables`), and
finally any relative imports; update the import block at the top of the file
(where useEffect, useId, useState, mermaid, Code, GitBranch, useTheme,
CopyButton, and the CSS constants are imported) to follow that sequence.
- Around line 52-71: When starting the async render inside useEffect/render,
clear the previous render state immediately to avoid flashes: inside the
render() function (before calling ensureMermaidInit/mermaid.render and
referencing uniqueId/code), call setSvg('') and setError(null) so the old SVG is
cleared and error state reset while the new render is pending; keep the existing
cancelled guard and error handling around mermaid.render (functions/variables:
useEffect, render, ensureMermaidInit, mermaid.render, uniqueId, code, cancelled,
setSvg, setError).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bee27418-7d28-433c-ab47-00ad4682e36a
📒 Files selected for processing (1)
src/renderer/components/chat/viewers/MermaidViewer.tsx
| try { | ||
| ensureMermaidInit(isDark); | ||
| const id = `mermaid-${uniqueId}`; | ||
| const { svg: rendered } = await mermaid.render(id, code); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For Mermaid 11.13.0, does securityLevel: "strict"guarantee XSS-safe SVG output for untrusted diagram text, or is additional sanitization (e.g., DOMPurify) recommended before ReactdangerouslySetInnerHTML?
💡 Result:
No, securityLevel: "strict" does not fully guarantee XSS-safe SVG output for untrusted diagram text in Mermaid 11.13.0. Additional sanitization with DOMPurify (or equivalent) is recommended before using the SVG in React's dangerouslySetInnerHTML. Mermaid's official documentation states that "strict" (the default) encodes HTML tags in text and disables click functionality, primarily protecting against script execution via click handlers and text content. However, Mermaid internally uses DOMPurify for sanitization during rendering, indicating awareness of broader XSS risks in generated SVG. Past critical XSS vulnerabilities (e.g., CVE-2025-54881 in sequence diagrams <11.10.0, fixed by sanitizing before innerHTML) show risks in SVG generation even in default configs. While 11.13.0 (post-11.10.0) has no open advisories per Snyk and GitHub security, apps using untrusted input via dangerouslySetInnerHTML remain vulnerable to SVG-based XSS vectors like <script>, javascript: URLs in links/images, or foreignObject (if not blocked). Third-party advisories (e.g., OneUptime, docmost) explicitly recommend DOMPurify.sanitize(svg) alongside "strict" for safe innerHTML insertion. Mermaid's security page notes configurable DOMPurify but emphasizes keeping updated, implying it's not foolproof alone for untrusted use. For React dangerouslySetInnerHTML={{html: svg}}, always sanitize externally as best practice.
Citations:
- 1: https://mermaid.js.org/community/security.html
- 2: https://mermaid.js.org/config/schema-docs/config-properties-securitylevel.html
- 3: GHSA-7rqq-prvp-x9jh
- 4: https://security.snyk.io/package/npm/mermaid
- 5: GHSA-wvh5-6vjm-23qh
- 6: GHSA-r4hj-mc62-jmwj
- 7: https://github.com/mermaid-js/mermaid/security
🏁 Script executed:
# Find and examine the MermaidViewer.tsx file
fd "MermaidViewer.tsx" --type fRepository: matt1398/claude-devtools
Length of output: 122
🏁 Script executed:
# Check if contentSanitizer.ts exists
fd "contentSanitizer.ts" --type fRepository: matt1398/claude-devtools
Length of output: 104
🏁 Script executed:
# Look for any existing DOMPurify usage in the project
rg "DOMPurify" --type ts --type tsxRepository: matt1398/claude-devtools
Length of output: 95
🏁 Script executed:
# Read MermaidViewer.tsx with line numbers, focusing on the mentioned lines
cat -n src/renderer/components/chat/viewers/MermaidViewer.tsx | head -70Repository: matt1398/claude-devtools
Length of output: 2723
🏁 Script executed:
# Read contentSanitizer.ts to understand available sanitization methods
cat -n src/shared/utils/contentSanitizer.tsRepository: matt1398/claude-devtools
Length of output: 8026
🏁 Script executed:
# Search for DOMPurify usage with correct syntax
rg "DOMPurify" -t tsRepository: matt1398/claude-devtools
Length of output: 50
🏁 Script executed:
# Read the rest of MermaidViewer.tsx to see how svg is rendered, particularly around lines 139-141
cat -n src/renderer/components/chat/viewers/MermaidViewer.tsx | tail -100Repository: matt1398/claude-devtools
Length of output: 3816
Sanitize rendered SVG before injecting HTML.
code is user/AI-provided input; even with securityLevel: "strict", Mermaid's SVG output requires additional sanitization before dangerouslySetInnerHTML. This path is vulnerable to SVG-based XSS vectors (e.g., <script>, javascript: URLs, foreignObject). DOMPurify sanitization is the recommended best practice for safe HTML injection with untrusted SVG content.
🛡️ Suggested hardening
+import DOMPurify from 'dompurify';
@@
ensureMermaidInit(isDark);
const id = `mermaid-${uniqueId}`;
const { svg: rendered } = await mermaid.render(id, code);
+ const sanitized = DOMPurify.sanitize(rendered, {
+ USE_PROFILES: { svg: true, svgFilters: true },
+ });
if (!cancelled) {
- setSvg(rendered);
+ setSvg(sanitized);
setError(null);
}Also applies to: 139-141
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/components/chat/viewers/MermaidViewer.tsx` at line 58, The SVG
returned from mermaid.render in MermaidViewer (the const { svg: rendered } =
await mermaid.render(id, code) usage) is user/AI-controlled and must be
sanitized before being passed to dangerouslySetInnerHTML; update the component
to run the rendered SVG through a sanitizer such as DOMPurify (e.g.,
DOMPurify.sanitize(rendered, { ADD_TAGS/ATTRS if needed, FORBID_TAGS:
['script','foreignObject'], FORBID_ATTR: ['on*','xlink:href'] })) and use the
sanitized result for injection, and apply the same sanitization where the
component injects SVG elsewhere in this file (the other mermaid
render/dangerouslySetInnerHTML usage).
Mermaid fenced code blocks in AI responses are now rendered as SVG diagrams instead of raw text. Includes code/diagram toggle, dark/light theme support, and error fallback for invalid syntax.
Summary by CodeRabbit