Skip to content

feat(web): add mermaid rendering to chat UI + zoom and pan - #7497

Closed
reckerp wants to merge 10 commits into
pingdotgg:mainfrom
reckerp:reckerp/chat-mermaid-fb7c
Closed

reckerp wants to merge 10 commits into
pingdotgg:mainfrom
reckerp:reckerp/chat-mermaid-fb7c

Conversation

@reckerp

@reckerp reckerp commented Aug 19, 2026 •

Copy link
Copy Markdown

What Changed

Adding Mermaid chart rendering in the chat UI with direct preview and a larger view for zoom and pan.

Why

Claude's output became unreadable, thus asking for visualizations help. Always copying the mermaid code between T3Code and mermaid.live is not ideal

UI Changes

Screen.Recording.2026-08-19.at.09.38.24.mov

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

Medium Risk
Injects mermaid-generated SVG via innerHTML; sanitization and strict mode reduce XSS risk but this is still untrusted model output in the DOM. New mermaid dependency and streaming remount work also affect chat rendering.

Overview
Chat markdown now renders mermaid/mmd fences as diagrams on web and desktop (mobile still shows source). Users can toggle source vs diagram, copy the fence, and expand into a pan/zoom overlay.

Rendering is lazy, LRU-cached, and initialized in mermaid strict mode with extra SVG sanitization. Streaming parse failures keep the last good diagram. ChatMarkdown reads streaming text from refs so diagram state is not remounted every token.

Copying a rendered (or failed) diagram pastes the original mermaid fence, not SVG.

Reviewed by Cursor Bugbot for commit 7a8deff. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add mermaid diagram rendering, zoom/pan overlay, and source copy to chat markdown

  • Chat markdown detects mermaid/mmd fenced code blocks and renders them inline as SVG diagrams via a new mermaidRendering.ts pipeline with LRU caching, strict-mode init, and SVG sanitization to strip XSS vectors.
  • MermaidDiagram.tsx debounces rendering during streaming and retains last-good state on parse errors; MermaidOverlay.tsx provides a modal with wheel/keyboard zoom and pointer panning bounded by mermaidViewport.ts.
  • MarkdownCodeBlock gains a toolbar toggle between diagram and source code, plus an expand button; wrap-lines control is hidden in diagram view.
  • ChatMarkdown now uses refs (textRef, isStreamingRef, link-metadata maps) to prevent ReactMarkdown remounts during streaming, preserving diagram state across token updates.
  • Clipboard serialization reads data-markdown-copy from mermaid surfaces so copying a rendered diagram yields the original fence source, not raw SVG.
  • Risk: MarkdownCodeBlock adds three new optional props (diagram, canExpandDiagram, onExpandDiagram); existing callers that render code blocks without these props are unaffected, but any code relying on the absence of a data-mermaid-view attribute or the wrap-lines control always being present in the pre renderer path should be checked.

Macroscope summarized 7a8deff.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5eb6b70f-2c9e-44ad-9d6c-b68933f806c8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 19, 2026
Comment thread apps/web/src/lib/mermaidRendering.ts Outdated
Comment thread apps/web/src/lib/mermaidRendering.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

UI consistency review of the mermaid diagram/overlay work. Two findings, both about ownership of the new overlay styling rather than the feature itself.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/MermaidOverlay.tsx Outdated
Comment thread apps/web/src/index.css
Comment thread apps/web/src/components/chat/MermaidDiagram.tsx
@macroscopeapp

macroscopeapp Bot commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — ...

You can add or adjust custom eligibility rules. Learn more.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One finding: the overlay scene's surface loses the custom-theme bridge that the inline mermaid code block has, so the two diverge under html[data-theme-id] palettes.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/index.css Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One finding: the streaming/idle mermaid source block does not carry the code font-size preference that every other chat code surface uses.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/index.css Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One finding: the mermaid error fallback in ChatMarkdown.tsx reuses the chat-markdown-mermaid-error surface without the data-markdown-copy contract the other three mermaid states carry, which changes copy behavior for that surface. Earlier findings (raw .dark selector, portaled overlay chrome/theming, pending-source font size) look resolved on this head.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/ChatMarkdown.tsx Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One finding on the new mermaid CSS: the pending-source rule's color loses to the runtime theme bridge, so the dimmed treatment only applies on the default palette. Everything else (chrome variants, @variant dark usage, copy fences, overlay theme override) looks consistent with the surrounding chat-markdown surfaces.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/index.css Outdated
Comment thread apps/web/src/lib/mermaidRendering.ts
Comment thread apps/web/src/components/chat/MermaidOverlay.tsx
cursoragent and others added 10 commits August 21, 2026 12:12
Agent mermaid fences showed as highlighted source only. Chat markdown now
lazy-loads mermaid, draws the diagram by default, and lets the user toggle
back to the code. Copy still yields the original fence.

Co-authored-by: paul <paul@recker.io>
Wide diagrams still shrink to the chat column inline. An expand control opens a dialog where wheel zooms and drag pans, so chat scroll no longer fights the diagram.

Co-authored-by: paul <paul@recker.io>
The first layout pass ran before Base UI put the overlay scene in the DOM, so wide diagrams opened at 100% instead of shrinking to the viewport. Fit and wheel listeners now bind when the scene element actually exists.

Co-authored-by: paul <paul@recker.io>
The detail view cloned mermaid SVG without rewriting its stylesheet selectors, so nodes fell back to black fills and inherited black label text. Zoom also scaled a shrink-to-fit SVG around the wrong origin and could throw the diagram off-screen.

The overlay now remaps theme CSS, pins the SVG to its viewBox size, treats fit as 100%, zooms around the diagram center, and keeps a sliver of the diagram in view.

Co-authored-by: paul <paul@recker.io>
…orrect

Review on pingdotgg#7497 found cache-key collisions, overlay CSS rewriting hex colors, portal-dead chrome classes, and mermaid remounting on every streamed token. Cache by full source, remap stylesheet selectors only, use ghost-muted overlay actions, and keep markdown renderers stable via refs.

Cursor Grok 4.6

Co-authored-by: paul <paul@recker.io>
The expand overlay is portaled out of .chat-markdown, so it missed the custom-theme bridge that switches inline mermaid/code blocks to --code-background. Use a portal-safe html[data-theme-id] selector so the expanded diagram sits on the same surface as the preview.

Cursor Grok 4.6

Co-authored-by: paul <paul@recker.io>
The streaming/idle mermaid fence is a bare pre, so it missed the --font-size-code token that other chat code surfaces use. Route it through the same preference so the source does not jump size when the diagram settles.

Cursor Grok 4.6

Co-authored-by: paul <paul@recker.io>
The RenderErrorBoundary fallback used the mermaid error surface without data-markdown-copy, so selecting it dropped the block instead of copying the fence. Pass the same mermaid source payload the other mermaid states already use.

Cursor Grok 4.6

Co-authored-by: paul <paul@recker.io>
Mermaid defaults htmlLabels to true, so diagrams could emit foreignObject HTML even with securityLevel strict. Turn htmlLabels off and lock it, strip foreignObject/object and data:text/html URLs, remap quoted url(#id) and single-quoted ids for the overlay, keep a zoomed diagram on-canvas across resizes, and seed the inline preview from the SVG cache so a remount does not flash source.

Cursor Grok 4.6

Co-authored-by: paul <paul@recker.io>
Strip unclosed dangerous SVG tags, keep pending source dim under custom themes, and refit the overlay when a streaming diagram's canvas changes after pan/zoom.

Co-authored-by: paul <paul@recker.io>
@cursor
cursor Bot force-pushed the reckerp/chat-mermaid-fb7c branch from 83de423 to 7a8deff Compare August 21, 2026 12:13

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7a8deff. Configure here.

}
});

return svg;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cached SVG ID collisions

Medium Severity

renderMermaidSvg caches the sanitized SVG with bake-in DOM ids from nextMermaidDomId, and inline mounts inject that string unchanged. A second fence with the same source+theme reuses those ids, so markers, gradients, and #t3-mermaid-… theme selectors collide in the document. The overlay already remaps ids via prepareMermaidOverlaySvg; inline previews do not.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7a8deff. Configure here.

event.preventDefault();
if (sceneRef.current) fitToScene(sceneRef.current);
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overlay zoom ignores IME

Low Severity

The new overlay onKeyDown handles printable keys (+, =, -, _, 0) and always preventDefaults without bailing on event.isComposing. That violates the project rule for new keyboard handlers that intercept printable keys: they need an isComposing guard so IME composition is not disrupted.

Fix in Cursor Fix in Web

Triggered by learned rule: Keyboard shortcut handlers: check isComposing and use correct -popup data-slot selectors

Reviewed by Cursor Bugbot for commit 7a8deff. Configure here.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One ownership finding on the new overlay CSS. Everything else (shared Button/Dialog/Tooltip composition, @variant dark usage, the themed --code-background bridge, and the chat-chrome toolbar treatment on the inline code block) looks consistent with the existing chat markdown surfaces.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/index.css
Comment on lines +2262 to +2280
.chat-markdown-mermaid-overlay-scene {
position: relative;
flex: 1;
min-width: 0;
min-height: 0;
overflow: hidden;
overscroll-behavior: none;
touch-action: none;
cursor: grab;
user-select: none;
/* Default palette matches the inline mermaid code block. Custom themes tint
--secondary/--input with the accent, so the overlay uses --code-background
via html[data-theme-id] .chat-markdown-mermaid-overlay-scene. */
background: var(--secondary);

@variant dark {
background: color-mix(in srgb, var(--input) 32%, transparent);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These declarations are single-owner presentation for MermaidOverlay's scene, and they restate in global CSS what the equivalent inline surface already expresses at its owner: MarkdownCodeBlock's root carries bg-secondary ... dark:bg-input/32 in ChatMarkdown.tsx, with only the themed override living here. Splitting the same palette/geometry across two owners is what lets the inline block and the overlay drift apart on the next tweak.

Suggest keeping only the html[data-theme-id] .chat-markdown-mermaid-overlay-scene bridge (line 1788) in index.css and moving the rest onto the elements in MermaidOverlay.tsx: scene → relative flex-1 min-w-0 min-h-0 overflow-hidden overscroll-none touch-none cursor-grab select-none bg-secondary dark:bg-input/32 data-[panning=true]:cursor-grabbing, canvas → absolute top-0 left-0 origin-top-left. The unlayered themed rule still outranks those utilities, exactly as it does for .chat-markdown-codeblock today. .chat-markdown-mermaid-overlay-canvas svg should stay global since it styles the injected SVG.

Posted via Macroscope — UI Consistency

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

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants