Skip to content

feat: render mermaid code blocks as interactive diagrams (#124)#128

Open
matt1398 wants to merge 2 commits intomainfrom
feat/mermaid-diagram-visualization
Open

feat: render mermaid code blocks as interactive diagrams (#124)#128
matt1398 wants to merge 2 commits intomainfrom
feat/mermaid-diagram-visualization

Conversation

@matt1398
Copy link
Owner

@matt1398 matt1398 commented Mar 23, 2026

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

  • New Features
    • Render Mermaid diagrams inline within Markdown chat messages.
    • Diagrams adapt to light and dark themes.
    • Toggle between rendered diagram and source code view.
    • Copy button to copy Mermaid source.
    • Graceful error handling with a fallback that shows the original source.

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>
Copilot AI review requested due to automatic review settings March 23, 2026 05:21
@gemini-code-assist
Copy link

Summary of Changes

Hello, 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

  • Interactive Mermaid Diagram Rendering: Mermaid fenced code blocks within AI responses are now rendered as interactive SVG diagrams, enhancing visual understanding of complex structures.
  • Code/Diagram Toggle: Users can easily switch between viewing the rendered Mermaid diagram and the raw Mermaid code for inspection or copying.
  • Theme Support: The rendered Mermaid diagrams automatically adapt to the application's dark or light theme, ensuring visual consistency.
  • Error Handling: A robust error fallback mechanism is in place to display syntax errors gracefully, preventing broken diagrams and showing the raw code instead.
  • Dependency Updates: The mermaid library and its extensive set of dependencies, including various D3.js modules, Cytoscape, and other parsing/rendering tools, were added to support this feature.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai coderabbitai bot added feature request New feature or request dependencies Pull requests that update a dependency file labels Mar 23, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 MermaidViewer React component that renders Mermaid code to SVG and provides a header UI (toggle + copy).
  • Updates both markdown renderers (MarkdownViewer and createMarkdownComponents) to detect language-mermaid code fences and render them as diagrams, skipping the <pre> wrapper.
  • Adds the mermaid dependency (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.

Comment on lines +19 to +29
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;
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +65
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();
}, []);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
</div>
) : svg ? (
<div
ref={containerRef}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
ref={containerRef}

Copilot uses AI. Check for mistakes.
Comment on lines 121 to +127
if (isBlock) {
const lang = className?.replace('language-', '') ?? '';
const text = content.replace(/\n$/, '');

if (lang === 'mermaid') {
return <MermaidViewer code={text} />;
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +19 to +30
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;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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;
}

Comment on lines +80 to +83
if (!cancelled) {
setError(err instanceof Error ? err.message : 'Failed to render mermaid diagram');
setSvg('');
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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('');
        }

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +125 to +126
if (lang === 'mermaid') {
return <MermaidViewer code={text} />;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@coderabbitai
Copy link

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Adds Mermaid diagram support: installs the mermaid dependency, introduces a new exported MermaidViewer component, and updates markdown/MarkdownViewer renderers to detect mermaid code blocks and render them via MermaidViewer with theme-aware rendering, code/diagram toggle, and error handling.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Added mermaid ^11.13.0 to runtime dependencies.
MermaidViewer component & export
src/renderer/components/chat/viewers/MermaidViewer.tsx, src/renderer/components/chat/viewers/index.ts
Added MermaidViewer React component (prop: code: string); initializes/uses mermaid to render SVG asynchronously, includes theme handling, code/diagram toggle, copy button, error handling, and exported from viewers index.
Markdown rendering integration
src/renderer/components/chat/markdownComponents.tsx, src/renderer/components/chat/viewers/MarkdownViewer.tsx
Detect mermaid fenced code blocks and render them with MermaidViewer; pre/code renderers updated to unwrap MermaidViewer children to avoid double-wrapping and trim trailing newline from mermaid content.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/renderer/components/chat/viewers/MarkdownViewer.tsx (1)

36-36: Please route the new Mermaid imports through viewers/index.ts.

This file and src/renderer/components/chat/markdownComponents.tsx both import MermaidViewer from the concrete file even though src/renderer/components/chat/viewers/index.ts now 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 the pre renderer.

Both markdownComponents.tsx and MarkdownViewer.tsx contain identical code and pre handlers for Mermaid detection. Additionally, React.Children.only(children) throws if react-markdown ever emits more than one child element. Using Children.toArray() with isValidElement() 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

📥 Commits

Reviewing files that changed from the base of the PR and between f23e881 and 3a235c6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • package.json
  • src/renderer/components/chat/markdownComponents.tsx
  • src/renderer/components/chat/viewers/MarkdownViewer.tsx
  • src/renderer/components/chat/viewers/MermaidViewer.tsx
  • src/renderer/components/chat/viewers/index.ts

Comment on lines +21 to +29
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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/renderer/components/chat/viewers/MermaidViewer.tsx

Repository: 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:


🌐 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:


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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 code changes, previous svg remains 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3a235c6 and 9c5562d.

📒 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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 Script executed:

# Find and examine the MermaidViewer.tsx file
fd "MermaidViewer.tsx" --type f

Repository: matt1398/claude-devtools

Length of output: 122


🏁 Script executed:

# Check if contentSanitizer.ts exists
fd "contentSanitizer.ts" --type f

Repository: matt1398/claude-devtools

Length of output: 104


🏁 Script executed:

# Look for any existing DOMPurify usage in the project
rg "DOMPurify" --type ts --type tsx

Repository: 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 -70

Repository: matt1398/claude-devtools

Length of output: 2723


🏁 Script executed:

# Read contentSanitizer.ts to understand available sanitization methods
cat -n src/shared/utils/contentSanitizer.ts

Repository: matt1398/claude-devtools

Length of output: 8026


🏁 Script executed:

# Search for DOMPurify usage with correct syntax
rg "DOMPurify" -t ts

Repository: 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 -100

Repository: 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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file feature request New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants