Skip to content

Feat/mermaid viewer - #24189

Closed
blackshot wants to merge 8 commits into
stablyai:mainfrom
blackshot:feat/mermaid-viewer
Closed

blackshot wants to merge 8 commits into
stablyai:mainfrom
blackshot:feat/mermaid-viewer

Conversation

@blackshot

Copy link
Copy Markdown

ELI5

Big Mermaid diagrams in markdown were shrunk to fit the column, so their text was too small to read. Now every rendered diagram has a small "expand" button in its corner. Clicking it opens the diagram over the whole window, where you can zoom with the mouse wheel or keyboard and drag to move around. You can also copy the diagram as a PNG image to paste into chats or documents. Escape or the close button takes you back.

What Changed

Before: a Mermaid diagram inside a markdown file (preview, rich editor, comments, or a .mmd file) was always scaled down to the width of its container. Large sequence or flow diagrams became unreadable. There was no way to enlarge them, and no way to get them out as an image.

After:

  • Hovering a rendered diagram reveals an expand button in its top-right corner. It is always visible on touch devices and reachable by keyboard.
  • Clicking it opens a full-window viewer that fits the diagram to the screen. Very large diagrams can scale below 10% so they still fit whole.
  • Mouse wheel / trackpad pinch zooms, anchored at the cursor.
  • Click and drag pans, with a grab / grabbing cursor.
  • Keyboard: arrow keys pan (Shift for bigger steps), + / = zoom in, - zooms out, and 0 fits to screen. These keys keep working after a toolbar button has focus.
  • Copy as PNG puts the diagram on the clipboard as an image. It is rendered at 2x over the current theme's background, so a dark-theme diagram stays readable when pasted into a light document.
  • A floating toolbar in the bottom-right corner has zoom out, zoom percentage, zoom in, fit to screen, copy as PNG, and close. Tooltips show each keyboard shortcut. The toolbar stays clear of the Windows/Linux window controls and the macOS traffic lights.
  • Resizing the window refits the diagram, but only while you haven't zoomed or panned since the last fit. A resize never throws away the view you chose.
  • Escape closes the viewer on the first press. Focus goes to the diagram area, not the first toolbar button, so no tooltip swallows the first Escape.

Mechanism:

  • MermaidBlock already renders and DOMPurify-sanitizes the SVG inline. When the viewer opens, it reads that sanitized markup from the inline diagram. Nothing is stored beforehand, and the copy is dropped after the close animation. In the copy, the render ID is renamed so the diagram's scoped <style> and url(#marker) references don't collide with the inline diagram. MermaidBlock also tracks which content and theme it last rendered, and hides the expand button while a newer render waits in its queue, so it never opens a stale diagram.
  • MermaidDiagramLightbox is built on the existing Dialog primitive, which already handles Escape, the focus trap, and returning focus to the button. The SVG is pinned to its natural viewBox size and moved with a CSS translate() scale() transform. The wheel listener is non-passive so it can stop Electron's page zoom and scrolling. A ResizeObserver drives the refit. All user inputs go through one helper that ignores inputs that change nothing (a horizontal wheel, zooming past a limit, a click without movement), so those don't count as the user adjusting the view. Dragging applies per-move deltas, so zooming mid-drag doesn't snap the diagram back. Toolbar tooltips hand Escape to the viewer, so a focused button's tooltip doesn't swallow the first Escape.
  • The zoom and pan math is a set of pure functions in mermaid-diagram-viewport.ts, with unit tests. It covers fitting, zooming around a point, the per-diagram minimum scale, wheel deltas in pixel, line, and page modes, and mapping keys to actions.
  • Copy as PNG (mermaid-diagram-png.ts) serializes the SVG and draws it on a canvas over the theme's background color. It writes the result through the existing window.api.ui.writeClipboardImage bridge. The output size is capped to the clipboard pixel budget and Chromium's maximum canvas side. An image still too large for the clipboard shows a toast instead of failing silently in the main process.
  • DialogContent gets a fullscreen variant. The design-system lint doesn't allow restyling the primitive from outside, so the edge-to-edge layout lives in the primitive.
  • New UI strings are translated into en, es, fr, ja, ko, and zh.

Why

Diagrams are the one piece of markdown content that can't reflow, so fitting them to the column is the only safe inline default. What was missing was a way to read them large, and to get them out as an image. Other approaches I considered:

  • Horizontal scroll at 1:1 inside the preview. Tall diagrams still overflow, and it breaks reading flow in the rich editor, where the diagram sits under editable source.
  • Opening the diagram in a new editor tab. Heavier, it leaves a tab to clean up, and it doesn't cover the comments sidebar.
  • Adding a pan/zoom library. The needed behavior is a small amount of transform math, which is easy to test and adds no dependency.
  • Using html-to-image for the PNG (already a dependency). It clones every node and inlines computed styles, which is slow for large diagrams. The Mermaid SVG already carries its own styles, so drawing it straight onto a canvas is enough.

Putting the button in MermaidBlock covers every place a diagram renders (markdown preview, rich markdown editor, sidebar comments, full .mmd / .mermaid files) with a single change.

Linked Issue

Fixes #22372

Visual Proof

image image

Testing

  • Windows 11: opened a markdown file with a large Mermaid sequence diagram and checked:

    • the expand button on hover and fit on open
    • wheel zoom at the cursor and drag pan
    • keyboard pan and zoom, including after clicking a toolbar button
    • the toolbar buttons, and that the toolbar doesn't overlap the window controls
    • refit on window resize
    • Copy as PNG, pasted into another app
    • a single Escape to close, and the close button
  • Unit tests:

    • mermaid-diagram-viewport.test.ts: fit (wide diagram, small-diagram upscale cap, diagrams needing less than 10%, unknown size), zoom anchored at a point, the per-diagram minimum scale, no-op input detection, wheel deltas in pixel, line, and page modes, and the key-to-action mapping.
    • mermaid-diagram-png.test.ts: 2x output, downscaling to the clipboard pixel budget, the canvas side limit, and unmeasured diagrams.
  • Renderer tsc, oxlint, check:code-quality:changed, and the related editor, sidebar-comment, and i18n test suites pass.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Implemented with Claude Code (Claude Opus 5.5), reviewed and tested manually by the author.

Review

AI code review summary:

  • Cross-platform: no platform-specific code. The toolbar sits at the bottom, away from both the Windows/Linux custom window controls (fixed top-right, z-index: 9999) and the macOS traffic lights (top-left). Only standard pointer, wheel, and keyboard events are used. Keys pressed with Ctrl, ⌘, or Alt are left to the app (e.g. Ctrl+= app zoom). The clipboard write goes through the existing Electron bridge.
  • SSH / remote / local: purely renderer-side. It reuses the already-rendered SVG, adds no new IPC or file access, and behaves the same for local, SSH, and folder workspaces. In the paired web client, writeClipboardImage resolves without writing images, so the Copy as PNG button is hidden there (via isWebClientLocation()) instead of reporting a copy that never happened.
  • Agents / integrations / git providers: not affected.
  • Performance: no extra Mermaid render. The expanded SVG is read only when the viewer opens and dropped on close, so idle diagrams hold no second copy. Pan/zoom only updates a CSS transform. will-change is deliberately not used, so the SVG re-rasterizes sharply when zoomed. The PNG is rendered only on click.
  • Security: the markup comes from MermaidBlock's existing DOMPurify SVG-profile pass. It is serialized from the inline diagram and re-parsed in the same context (innerHTML of a div). The only transformation is renaming the render ID, which is unique per instance. The PNG canvas draws that same sanitized SVG and no external resources.
  • UI quality: built on the Dialog, Button, Tooltip, and Separator primitives. The floating toolbar uses the same bg-popover / shadow-floating tokens as other floating surfaces. Shortcut chips use ShortcutKeyCombo. The design-system lint gate passes.
  • Backwards compatibility: DialogContent's variant defaults to 'default', which uses the same classes as before, so existing dialogs are unchanged.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Mobile: not affected (renderer desktop UI only).
  • The viewer covers the Orca window; it does not enter OS-level fullscreen.

Author

X (Twitter): @HEchiburu

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

blackshot and others added 8 commits September 22, 2026 23:06
Large mermaid diagrams render too small to read inline. A corner button on
every rendered diagram now opens it full-window with wheel zoom (anchored at
the cursor), drag pan, a floating zoom/fit/close toolbar, and Escape to close.

Adds a `fullscreen` variant to DialogContent so the viewer does not restyle
the primitive from outside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Fit no longer clamps to the 10% floor, and zoom-out uses a per-diagram
  floor at or below the fit scale, so very wide diagrams show whole.
- The expanded SVG is keyed to the content/theme it was rendered from, so
  the expand button hides while a newer render waits in the queue instead
  of opening the previous diagram.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each MermaidBlock kept a second serialized copy of its SVG in state even if
it was never expanded. The viewer now reads the sanitized inline diagram's
markup when it opens and drops it after the close animation, so idle
diagrams hold no extra copy. MermaidBlock only tracks which content/theme
it last rendered, which still hides the expand button during a pending
re-render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A ResizeObserver refits the diagram when the viewer's size changes, but
  only while the user has not zoomed or panned since the last fit, so a
  window resize never discards a chosen view.
- Wheel events in page mode (deltaMode 2) are converted to pixels (800px
  per page) instead of being read as a tiny pixel delta.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A horizontal wheel event, a zoom click already at the scale limit, or a
click without pointer movement marked the view as user-adjusted, so a later
resize stopped refitting a diagram that was still fitted. All user inputs
now go through one helper that compares the next transform with the current
one and only flags an adjustment when the view actually changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewer could only be panned by dragging, so keyboard users could not
reach clipped parts of a zoomed diagram. The diagram viewport is now a
focusable application region that receives focus on open:
- Arrow keys pan (Shift for bigger steps), moving the view like scrolling.
- + / = zoom in, - zooms out, 0 fits to screen.
Modified keys are left to the app, and Escape/Tab keep their dialog
behavior. Toolbar tooltips show the matching key chips, and the hint and
viewport label are translated in all six locales.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… toolbar clicks

- Adds a "Copy as PNG" toolbar button. The rendered SVG is rasterized on a
  canvas at 2x over the current theme background (so light-on-dark diagrams
  stay readable when pasted), scaled down to fit the clipboard pixel budget
  and Chromium's canvas side limit, and written through the existing
  writeClipboardImage bridge. Oversized results and failures show a toast
  instead of silently doing nothing.
- The viewer key handler moves from the viewport to the wrapper around the
  viewport and toolbar, so arrows, +/- and 0 keep working after a toolbar
  button takes focus.
- The toolbar icon button moves to its own module for reuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Hide "Copy as PNG" in the web client, where the clipboard bridge resolves
  without writing images and the button would report a copy that never
  happened.
- A focused toolbar button's tooltip is the topmost dismissable layer and
  swallowed the first Escape. Toolbar tooltips now close the whole viewer
  on Escape through a small close context.
- Dragging applies per-move deltas instead of offsets from the drag start,
  so zooming mid-drag no longer snaps the diagram back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Adds diagram viewer UI with zoom, pan, and PNG export.

The PR should not merge until an already-open viewer survives diagram rerenders without losing the user's view.

Findings

  1. P1 Rerenders close the viewer ▶
  2. P2 PNG export remains untested ▶

Summary

The PR adds a fullscreen viewer and PNG-copy controls to rendered Mermaid diagrams, with viewport helpers, a fullscreen dialog variant, translations, and sizing tests.

  • The render-current gate prevents opening an outdated inline SVG, but also closes an already-open viewer during a rerender.
  • Export-path tests would complement the new sizing tests.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[MermaidBlock renders sanitized SVG] --> B[Expand opens lightbox]
  B --> C[Fit, zoom and pan]
  B --> D[Serialize SVG to PNG]
  D --> E[Clipboard bridge]
  A --> F[Content or theme changes]
  F --> G[Current-render gate unmounts lightbox]
Loading

Reviews (1) · Last reviewed commit: "fix(editor): mermaid viewer web copy, to..."

@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 PNG export remains untested. These tests check canvas dimensions but never exercise SVG serialization, image loading, rasterization, or the clipboard write. A blank or incorrectly rendered copy could therefore pass the suite. Add a test that exports a representative styled SVG.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@coderabbitai

coderabbitai Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 6d2dfef7-64ce-414f-bb6a-0a4db4621d26

📥 Commits

Reviewing files that changed from the base of the PR and between 46d6b76 and 84d7759.

📒 Files selected for processing (15)
  • src/renderer/src/components/editor/MermaidBlock.tsx
  • src/renderer/src/components/editor/MermaidDiagramCopyPngButton.tsx
  • src/renderer/src/components/editor/MermaidDiagramLightbox.tsx
  • src/renderer/src/components/editor/MermaidDiagramToolbarButton.tsx
  • src/renderer/src/components/editor/mermaid-diagram-png.test.ts
  • src/renderer/src/components/editor/mermaid-diagram-png.ts
  • src/renderer/src/components/editor/mermaid-diagram-viewport.test.ts
  • src/renderer/src/components/editor/mermaid-diagram-viewport.ts
  • src/renderer/src/components/ui/dialog.tsx
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Mermaid diagrams can now open in a fullscreen viewer with fit, zoom, pan, and keyboard controls. The viewer includes a PNG copy control outside the web client. The change also adds viewport and PNG layout helpers, tests, fullscreen dialog styling, and localized viewer text.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 84d77

This adds a fullscreen Mermaid viewer with zoom, pan, and PNG copy. The supplied context shows no concrete failure path, and it is ready to merge after the normal CI checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 84d77

The viewer reuses sanitized diagrams, and PNG export retains existing clipboard access and size checks. No material security bypass was established. An export can finish after the viewer closes, and incomplete coverage limits confidence in the overall assessment.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The new privileged outcome is replacement of the local desktop clipboard with a user-selected diagram image. The traced operation sends neither raw SVG nor a file path to the privileged handler.

Trust Boundaries and Controls

  • observed — The clipboard handler applies the existing trusted-renderer check and accepts only bounded PNG data that decodes to a nonempty image within the dimension limit. The new caller also checks the encoded payload length before invoking IPC.
  • observed — The new PNG layout scales output against the shared pixel budget and a 16,384-pixel side limit before allocating the canvas.

Resilience and Maintainability Implications

  • inferred — An initiated export can write after close or unmount because the mounted check occurs after clipboard invocation. Reopening can permit overlapping exports and stale final clipboard contents. Same-instance disabling limits repetition, but no material security consequence or cross-session sequencing guarantee was established.

Hardening Proposals

  • proposed — Consider explicitly defining whether closing cancels an export and sequencing exports across viewer sessions so an obsolete operation cannot replace a newer clipboard selection. This would strengthen clipboard-intent guarantees; it is not a verified vulnerability.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.89% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 9 files. (6 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title identifies the main change: adding a Mermaid diagram viewer. It is concise and related to the pull request, although the "Feat/" prefix is less descriptive than a sentence-style title.
Description check ✅ Passed The description is complete and follows the repository template. It explains the user-facing change, implementation, rationale, linked issue, visual proof, testing, AI use, review considerations, comp…
Linked Issues check ✅ Passed The changes implement the coding requirements in issue [#22372]. MermaidBlock adds an expand control for each rendered diagram. MermaidDiagramLightbox opens a fullscreen DialogContent, fits the …
Out of Scope Changes check ✅ Passed The changes stay connected to the Mermaid viewer objective in [#22372]. The fullscreen dialog variant, localized labels, viewport and PNG helper tests, SVG ID isolation, and render-state handling supp…
Full details: Docstring Coverage

Explanation

Docstring coverage is 57.89% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 9 files. (6 skipped: 6 unsupported.)

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

@nwparker

Copy link
Copy Markdown
Contributor

Thank you for this. The writeup and the viewport math were really thorough. Since this PR was opened, Orca got a shared pan/zoom viewer (PanZoomImageDialog, used for comment diagrams and chat images), so we're taking this over in #36575, which connects Mermaid diagrams in markdown preview, the rich editor and .mmd files to that viewer instead of adding a second lightbox. Copy-as-PNG didn't make it into that PR. We may bring your approach over as a follow-up. Closing in favor of #36575.

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.

[Feature]: Open Mermaid diagrams in a full-window zoom/pan viewer

2 participants