Skip to content

🤖 feat: render Mermaid diagrams with Pele in the selected theme - #7820

Merged
ThomasK33 merged 17 commits into
mainfrom
mermaid-y436
Oct 31, 2026
Merged

ThomasK33 merged 17 commits into
mainfrom
mermaid-y436

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Summary

Xum now draws Mermaid diagrams with Pele instead of the mermaid package. Diagrams use the colors of the selected Xum theme, and they change their layout to fit narrow screens. Pele reads the same Mermaid syntax, has no dependencies, and is much smaller and faster.

Background

Xum always drew diagrams in Mermaid's fixed dark theme, also in the light themes. On a phone, a wide diagram kept its desktop layout and scrolled sideways. Pele is an MIT-licensed renderer from Obsidian. It supports every diagram type built into Mermaid 12.1, takes its colors from CSS variables, and can reflow a diagram to a given width.

Measured in Chromium against Mermaid 11.17.2: a flowchart took 3.2 ms instead of 34 ms, and a sequence diagram took 0.18 ms instead of 32 ms. The lazy diagram chunk is 260 kB gzipped instead of 976 kB. The first diagram also loads the shared 77 kB KaTeX chunk if markdown math has not loaded it yet.

Implementation

  • Colors. One rule maps the --pele-* variables to Xum theme tokens. The neutral roles use tokens that already exist. Xum's dark and light themes already use Coder's product surface tokens. This PR adds Coder's link color and its six highlight colors (--color-content-link, --color-highlight-*) for accents and chart series. The two Flexoki themes set these tokens to Flexoki's 600 shades (light) and 400 shades (dark). A theme switch recolors a diagram without a new render.
  • Width. useResizeObserver measures the diagram box, and Xum passes the width to Pele as maxWidth. Below 640 px, Pele can switch a wide LR flowchart to top-to-bottom, and Xum removes the 300 px height cap so the labels stay readable. I removed the old 70% width cap. Pele never makes a diagram larger, so the cap only made wide diagrams smaller. The expand dialog renders its own copy at the dialog's width, so a diagram from a narrow pane opens in its wide layout. Xum loads the label fonts before Pele measures, so the boxes fit the real font.
  • Security. Diagram text is untrusted model output. Pele drops script, HTML labels and javascript: links. Xum also turns off links and images (an image fetches from the network as soon as it renders). The existing sanitizer stays as a second layer. Pele's output is well-formed XML, so the sanitizer now uses a strict parse only, and it removes foreignObject.
  • Math. $$...$$ labels are typeset with KaTeX as MathML, as Mermaid did. The sanitizer keeps a foreignObject only in Pele's exact math wrapper. Every element in it must be a MathML element that KaTeX emits. Names are checked as well as namespaces, because the HTML parser turns a MathML-namespace <img> inside <mtext> into a real image.
  • Removed. The mermaid dependency, an unused stub, a vite alias that pointed to a missing file, and VITE_DISABLE_MERMAID in the e2e setup.

Validation

  • Tests use real Pele. They cover the render, keeping the last valid diagram on an error, sanitizing, and a re-render when the width changes. I broke the code on purpose 7 ways (links on, images on, no font wait, foreignObject kept, no maxWidth, width not in the effect keys, no height-cap lift), and each break failed its matching test.
  • I checked these in Storybook in 4 themes, at 390 px and 1280 px. At 390 px, a 10-node flowchart, a sequence diagram and a pie fit with no sideways scroll, and their labels stay at 14 px. A resize from 1280 px to 390 px and back reflows the diagram both ways. Zoom and the expand dialog work.
  • In every theme, each chart color has a contrast of at least 3.9:1 against the diagram background.
  • New MermaidPhone and MermaidLaptop stories pin the Pixel viewports. Their plays check the top-to-bottom layout below 640 px and the LR layout above it.
  • I regenerated docs/img/plan-mermaid.webp from its story.

Risks

Low severity. The change affects only how diagrams look in chat, plans and Mermaid artifacts.

  • Pele uses its own layout. Node positions and spacing differ from Mermaid's, and Mermaid theme settings such as %%{init: {theme}}%% and themeVariables no longer apply.
  • Diagram links no longer open.
  • On a host with no math font (for example headless Linux), a single Greek letter in a math label draws as blank space. Mermaid emitted the same MathML.
  • On desktop, a long LR chain that is wider than the chat column still scrolls sideways. The 640 px breakpoint keeps it LR, because a top-to-bottom chain under the 300 px height cap would be unreadable.
  • Pele is new: 1.0 shipped in October 2026. It is pinned to 1.9.0, the newest release older than the 7-day minimum release age.

A problem that predates this PR, left as a follow-up: the zoom buttons are nearly invisible in the light theme (#7821). Two hardening items for the sanitizer's second layer are tracked in #7828. This PR fixes the other old zoom problem: the buttons now work in the artifact viewer.

Screenshots

Dark, light and Flexoki dark at 1280 px:

Dark theme
Light theme
Flexoki dark theme

A 10-node LR flowchart at 390 px (it changes to top-to-bottom) and at 1280 px (it stays LR):

Flowchart at 390 px
Flowchart at 1280 px


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $8.15

Adds pele@1.9.0 (newest release past minimumReleaseAge) and the Coder product
content-link and highlight-* tokens that the Pele diagram theme uses. Dark
values sit in theme-dark.css; light and flexoki-light share one light rule.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
Replaces the mermaid package with Pele's synchronous render(). Diagrams wait
for web fonts, then measure labels with the same --font-primary and
--font-monospace that .mermaid-container draws with. links and images are off
because chart text is untrusted model output.

.mermaid-container maps Pele's CSS variables to Coder tokens, so diagrams
follow every theme without a re-render. sanitizeMermaidSvg now accepts only
strict SVG and removes foreignObject, since Pele never emits it.

Tests render with real Pele instead of a mermaid module mock.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
Keeps Mermaid's cyclomatic complexity from growing (crap-check floor-cc):
renderMermaidSvg owns the font wait, the Pele call and sanitization.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
The vite alias pointed at src/mocks/mermaidStub.ts, which does not exist, so
e2e runs already used the real renderer. With mermaid gone the stub, the alias
and the env var have nothing to do.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
Pele SVGs keep their measured width, so a diagram wider than its frame
overflows. justify-content: center pushed the left part out of the scroll
area at phone widths; safe center keeps it reachable.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
The link and highlight tokens that feed Pele's accent and series colors
held Coder values in every theme. Light and dark are Coder-derived and
keep them. flexoki-light now uses Flexoki 600 and flexoki-dark Flexoki
400, matching each block's other accents.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `medium`_
The 70% cap only shrank wide diagrams, and Pele never upscales small
ones, so it wasted about 100 px of a phone screen. Without the cap the
auto margins center nothing, so they go too.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
On phones and narrow panes a diagram kept its desktop layout and
scrolled sideways. The diagram now measures its container and passes
the width to Pele as maxWidth, so Pele wraps labels, tightens spacing
and, below 640 px, turns layouts like flowchart LR top-to-bottom. A
width change re-renders at once, without the 350 ms chart debounce.
An unmeasured (zero) width keeps the natural layout.

Below 640 px the 300 px height clamp is lifted too: a top-to-bottom
diagram would otherwise shrink until its labels are unreadable, and
the transcript scrolls vertically instead.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
The narrow-width rules raised the Mermaid component's complexity above
the CRAP gate's floor. diagramLayout now turns the measured container
size into Pele's maxWidth and the height-clamp style, so the zero-width
rule lives in one place.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
Dropping the 70% centered box left small diagrams at the left edge of the
artifact viewer, which renders Mermaid outside .markdown-content. The flex
centering and overflow rules now apply to every .mermaid-container.

---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
@mintlify

mintlify Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
Mux 🟢 Ready View Preview Oct 31, 2026, 7:34 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T08:51:40.693988Z 4e866d8 Manual request
🔒 Security Review ✅ Completed 2026-10-31T08:52:15.371553Z 4e866d8 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: efe7ed7f26

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

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

Copy link
Copy Markdown

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

ℹ️ 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".

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

🟢 e2e stories (Haiku): 14 passed

22 agent steps · 7 replayed from cache · 15 model calls · 46.4k tokens (24% cached)
Spend: $0.09 of the $1.00 cap (proxy ledger)

All 14 tests in 9 files
Test Agent Time
🟢 chatRestart.e2e.ts · 1 passed 1 step · 1 call 5.3s
🟢 a chat survives a restart of the server 1 step · 1 call 5.3s
🟢 compaction.e2e.ts · 1 passed 1 step · 1 call 4.1s
🟢 /compact replaces the old chat with a summary 1 step · 1 call 4.1s
🟢 pluginTrustRestart.e2e.ts · 1 passed 4 steps · 2 calls 9.1s
🟢 a trusted plugin hook stays trusted after the server restarts 4 steps · 2 calls 9.1s
🟢 settingsRoundTrip.e2e.ts · 1 passed 2 steps · 1 call 8.5s
🟢 a chat survives trips through Settings 2 steps · 1 call 8.5s
🟢 sidebarPerWorkspace.e2e.ts · 1 passed 4 steps · 1 call 4.9s
🟢 a new workspace starts with its right sidebar collapsed while another keeps it open 4 steps · 1 call 4.9s
🟢 smoke.e2e.ts · 1 passed 2 steps · 1 call 2.5s
🟢 the agent opens the playground workspace 2 steps · 1 call 2.5s
🟢 streamErrors.e2e.ts · 1 passed 1 step · 1 call 6.6s
🟢 stream errors show in the chat, and the chat recovers 1 step · 1 call 6.6s
🟢 todoNudge.e2e.ts · 1 passed 1 step · 1 call 3.9s
🟢 the agent finishes open TODO items after the turn-end nudge 1 step · 1 call 3.9s
🟢 touchAlignment.e2e.ts · 6 passed 6 steps · 6 calls 25.4s
🟢 the Settings close button is level with the title on a touch screen 390px wide 1 step · 1 call 3.7s
🟢 the Settings close button is level with the title on a touch screen 768px wide 1 step · 1 call 4.1s
🟢 Backup settings checkboxes are small and level with their labels on a touch phone 1 step · 1 call 4.4s
🟢 the heartbeat checkbox is small and level with its label on a touch phone 1 step · 1 call 3.7s
🟢 a reply's model and time stay on one line on a touch phone 1 step · 1 call 6.5s
🟢 the workspace actions menu stays on the title row on a touch phone 1 step · 1 call 3.0s

e2e 0.20.0 · 1m 13s · web · run artifacts

🟢 e2e Electron stories (Haiku): 3 passed

3 agent steps · 3 model calls · 8.0k tokens (22% cached)
Spend: $0.02 of the $1.00 cap (proxy ledger)

All 3 tests in 3 files
Test Agent Time
🟢 electron/deepLink.e2e.ts · 1 passed 1 step · 1 call 5.1s
🟢 a deep link of a second launch starts a chat in the running app 1 step · 1 call 5.1s
🟢 electron/menuBar.e2e.ts · 1 passed 1 step · 1 call 5.5s
🟢 the workspace menu bar is the desktop title bar 1 step · 1 call 5.5s
🟢 electron/smoke.e2e.ts · 1 passed 1 step · 1 call 5.0s
🟢 Xum Electron opens the playground workspace 1 step · 1 call 5.0s

e2e 0.20.0 · 17.6s · electron · run artifacts

🟢 e2e agent repros (Haiku): no tests selected

Spend: $0.00 of the $1.00 cap (proxy ledger)

e2e 0.20.0 · 5.3s · 2 targets (web, phone) · run artifacts

The artifact viewer renders Mermaid without a .markdown-content
ancestor, so the svg rules scoped under it never matched there: the
zoom buttons changed --diagram-zoom but the diagram kept its size, and
the expanded view lost its 80vh cap. The container rule was already
global; the svg and modal svg rules now are too.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=xhigh -->
Mermaid rendered $$...$$ labels as KaTeX MathML. Pele shows the raw TeX
unless render() gets a math resolver, so the switch to Pele regressed
these labels.

renderMathLabel typesets each label with KaTeX (MathML output, default
trust: false) and measures it in an offscreen probe. It returns null
for invalid TeX, and Pele then shows the source. KaTeX is now a direct
dependency. Mermaid.tsx imports it statically, and the Diagram lazy
boundary keeps it off the first load.

sanitizeMermaidSvg still removes every foreignObject except the exact
shape Pele wraps around a math result. The predicate also checks each
MathML element name, because the HTML parser turns a MathML-namespace
<img> inside <mtext> into a real image.

The layout-width tests now restore their getBoundingClientRect spy.
happy-dom shares element classes across windows, so the spy leaked into
later tests.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=xhigh -->
Architecture diagrams nest icon svgs. The descendant selector zoomed them a
second time and applied the height cap to each icon.

---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Both round-1 findings are fixed (see the thread replies). Please take another look.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 1203c3bef7

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

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

Copy link
Copy Markdown

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: 1203c3bef7

ℹ️ 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".

document.fonts.ready does not start loading an @font-face that nothing on
the page uses yet, such as Geist Mono on Linux. Pele then sized label boxes
for the fallback font, and the boxes were wrong once the real font swapped
in. renderMermaidSvg now requests the primary and monospace families with
document.fonts.load before it awaits ready. Promise.allSettled keeps a
failed font load from blocking the render.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high -->
The expand dialog copied the inline SVG, so in a narrow chat column or the
artifact sidebar it showed the narrow, often vertical layout at full
screen size.

Measurement and rendering move into a useDiagramSvg hook that both frames
call. The inline frame keeps its debounce, last-valid render, error text,
zoom and copy source. The dialog renders renderedDiagram.chart (a chart
that already rendered) at its own width, with its own id prefix. The
modal innerHTML effect and its ref are gone. A render error now records
the chart that failed, so a newer chart hides it without an effect that
clears it.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high -->
AGENTS.md asks breakpoint-dependent UI to pin a Pixel viewport variant,
mirror it with the story viewport global, and back it with a play
contract. The 640 px Mermaid breakpoint had none, so Pixel snapshots only
showed the laptop layout.

MermaidPhone renders a six-node flowchart LR in a 358 px column (390 px
Pixel phone viewport). Its play checks the pinned variant, then asserts
the diagram is taller than wide, does not overflow sideways, and has no
max-height cap. MermaidLaptop renders the same chart in a 768 px column
and asserts it stays wider than tall. Both columns have fixed widths
because the test runner plays at desktop width. They add 2 Pixel
snapshots (685 of 690) and no new story file.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high -->
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

All three round-2 findings are fixed (see the thread replies). Please take another look.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 4e866d8296

ℹ️ 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".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 4e866d8296

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33

Copy link
Copy Markdown
Member Author

Readiness record for head 4e866d829607:


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $8.15

@ThomasK33

Copy link
Copy Markdown
Member Author

Verifier verdict: PASS
Head: 4e866d8 (parent 85abd10). Storybook on :6201, driven with agent-browser. Scratch stories (artifact wrapper, math label, invalid chart, architecture, pie) were deleted. git status is clean.

a. Theme colors: --pele-series-1 / --pele-accent on .mermaid-container equal the theme tokens in all 4 themes. dark hsl(269 100% 74%)/hsl(213 94% 68%), light hsl(271 61% 35%)/hsl(221 83% 53%), flexoki-light #5e409d/#205ea6, flexoki-dark #8b7ec8/#4385be. Pie slice fills equal series-1 and series-2 in each theme (for example flexoki-dark rgb(139,126,200) and rgb(58,169,159)). Flowchart node fill, text and line colors equal surface-secondary, content-primary and content-secondary.
b. Narrow layout: MermaidPhone at 390 px has viewBox 0 0 106 526 (top-to-bottom), scrollWidth-clientWidth = 0, max-height none. The artifact wrapper at a real 390 px viewport gives the same result. At 1280 px both have viewBox 0 0 679 61 (LR), overflow 0, max-height 300px.
c. Expand dialog: opened from the 358 px MermaidPhone column, the inline viewBox stays 0 0 106 526 and the dialog renders 0 0 679 61 at 1024 px wide.
d. Zoom: in chat (MermaidLaptop) and in the artifact wrapper, +3 gives 679x61 → 883x79 (zoom 1.3), -6 gives 475x43 (0.7), and +3 returns to 679x61. Architecture at 1.5x: root 210x255 → 315x383. The 4 nested icon svgs keep zoom: 1 and grow 1.5x (14→21, 16→24, 18→27 px), not 2.25x.
e. Math: the $$\frac{a}{b}$$ label renders one foreignObject.pele-math with <math> and <mfrac>, and no $$ in the text. The screenshot shows a typeset fraction. Invalid chart: "Mermaid Error: Parse error on line 2 ... got 'DOUBLECIRCLESTART'" in a <pre>, with no diagram container.
f. Console: no errors or warnings on MermaidPhone, MermaidLaptop, PlanMermaidWithCosts or any scratch story. A console.error probe showed that capture worked.
Baseline: on the parent, PlanMermaidWithCosts renders with mermaid (flowchart-elk, 4 nodes, viewBox 276x406, fixed fill:#ccc). The head renders the same 4-node chart with Pele (viewBox 215x340) in theme colors.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit f84be72 Oct 31, 2026
40 of 41 checks passed
@ThomasK33
ThomasK33 deleted the mermaid-y436 branch October 31, 2026 10:18
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.

1 participant