Skip to content

feat(markdown): render mermaid diagrams in plan viewer#44

Merged
johannesjo merged 1 commit intojohannesjo:mainfrom
Maskar:pr/mermaid-rendering
Mar 29, 2026
Merged

feat(markdown): render mermaid diagrams in plan viewer#44
johannesjo merged 1 commit intojohannesjo:mainfrom
Maskar:pr/mermaid-rendering

Conversation

@Maskar
Copy link
Copy Markdown
Contributor

@Maskar Maskar commented Mar 28, 2026

Summary

Mermaid code blocks in markdown are rendered as SVG diagrams in the plan viewer dialog. The mermaid library is lazy-loaded only when diagrams are present.

Motivation

Implementation plans created by AI agents often include mermaid diagrams for architecture, flow charts, and sequence diagrams. Rendering them inline makes plans easier to review.

Test plan

  • Open a plan that contains a ```mermaid code block
  • The diagram renders as an SVG in the plan viewer
  • Plans without mermaid blocks are unaffected (no extra loading)
  • Raw mermaid source is shown briefly before the diagram renders

Copy link
Copy Markdown
Owner

@johannesjo johannesjo left a comment

Choose a reason for hiding this comment

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

Nice feature — lazy-loaded mermaid rendering with clean integration into the existing marked/Shiki pipeline. A few things to address:

Important

1. No error handling on mermaid.render() (PlanViewerDialog.tsx:110-114)

If a diagram has a syntax error, this produces an unhandled promise rejection and the user sees a blank block. Add a .catch():

mermaid.render(id, source).then(({ svg }) => {
  el.innerHTML = svg;
  el.classList.add('mermaid-rendered');
}).catch(() => {
  // Keep the raw source visible (already in the element)
});

2. mermaid.initialize() called on every effect run (PlanViewerDialog.tsx:108)

Each time planHtml() changes, initialize() is called again, resetting Mermaid's internal state. Move initialization to a one-time guard:

let mermaidReady: Promise<typeof import('mermaid')['default']> | null = null;
function getMermaid() {
  if (!mermaidReady) {
    mermaidReady = import('mermaid').then(({ default: m }) => {
      m.initialize({ startOnLoad: false, theme: 'dark' });
      return m;
    });
  }
  return mermaidReady;
}

3. Race condition if planHtml updates while rendering is in-flight (PlanViewerDialog.tsx:106-115)

The async import() + render() chain has no cancellation. If plan content changes before rendering completes, stale SVGs could be written to already-replaced DOM nodes. At minimum, check el.isConnected before writing:

if (el.isConnected) {
  el.innerHTML = svg;
  el.classList.add('mermaid-rendered');
}

Minor

  • Date.now() in render ID — could collide if the effect re-fires within the same millisecond. A monotonic counter (let mermaidIdCounter = 0) would be more robust.
  • Consider explicitly setting securityLevel: 'strict' in the Mermaid config — it's the default in v11, but being explicit is good practice for a tool rendering user/AI-generated content.

Everything else looks good — the blockIndex stays in sync correctly, the CSS fallback with raw source is a nice touch, and the lazy loading means zero cost for plans without diagrams.

Mermaid code blocks in markdown are rendered as SVG diagrams.
The mermaid library is lazy-loaded only when diagrams are present.
Before rendering completes, the raw mermaid source is shown as text.
@Maskar Maskar force-pushed the pr/mermaid-rendering branch from 0f06b8f to 662c5cd Compare March 28, 2026 20:18
@johannesjo
Copy link
Copy Markdown
Owner

Thank you! I am bit concerned about the extra load, so maybe it will be neccessary to remove this in the future, but for now lets add and test it in production use.

@johannesjo johannesjo merged commit e56a9fc into johannesjo:main Mar 29, 2026
2 checks passed
@Maskar
Copy link
Copy Markdown
Contributor Author

Maskar commented Mar 29, 2026

Thanks for the thorough review! All feedback addressed:

Your points:

  • Added .catch() on mermaid.render() — syntax errors log a warning, raw source stays visible
  • mermaid.initialize() now called once via a lazy singleton (src/lib/mermaid-render.ts)
  • Added el.isConnected check before writing SVG to prevent stale renders
  • Replaced Date.now() with monotonic counter for render IDs
  • Added securityLevel: 'strict' to mermaid config

Additional fixes found during testing:

  • DOMPurify was stripping mermaid source from data-mermaid attribute — switched to reading from textContent instead
  • Effect wasn't re-firing on dialog reopen — now tracks props.open as dependency
  • DOM not ready when effect runs — added requestAnimationFrame delay
  • Mermaid rendering also added to the markdown viewer dialog (not just plan viewer)
  • Links in markdown viewer: HTTP opens in browser, .md links navigate within the viewer
  • Viewer max dimensions expanded to calc(100vw - 32px) / calc(100vh - 32px)

Extracted shared logic into src/lib/mermaid-render.ts — used by both PlanViewerDialog and MarkdownViewerDialog.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants