Skip to content

Render Mermaid diagrams instead of describing them in ASCII - #1385

Merged
Paul Lizer (paullizer) merged 3 commits into
paullizer-react-v2-uifrom
paullizer-mermaid-diagram-rendering
Sep 2, 2026
Merged

Render Mermaid diagrams instead of describing them in ASCII#1385
Paul Lizer (paullizer) merged 3 commits into
paullizer-react-v2-uifrom
paullizer-mermaid-diagram-rendering

Conversation

@paullizer

Copy link
Copy Markdown
Collaborator

Asking for a diagram came back as ASCII box art in a ```text fence: unreadable, not exportable as a picture, meaningless to a screen reader.

This is not a Mermaid rendering bug. The renderer never saw a mermaid fence.

Two causes

1. Nothing told the model that ```mermaid renders. SimpleChat already attaches capability guidance for inline charts and image proposals when a request calls for one. Diagrams had none, so the model fell back on drawing with keyboard characters. The chart guidance made it worse — it ended with "Do not output Mermaid, matplotlib/Python, Vega, or other chart code blocks", steering the model away from diagrams on any analytical request.

2. The classic UI never rendered mermaid in chat. Mermaid was wired only into the export rasterizer, so a diagram became a picture in an exported Word file or PDF while the same diagram stayed a code block in the conversation it came from. V2 (MermaidDiagram.tsx) was already fine.

Changes

Backend

New functions_diagram_operations.py, mirroring functions_chart_operations.py:

  • user_requested_diagram() — matches diagram, flowchart, sequence, architecture, data flow, ERD, state machine, mind map, and phrasings like "turn this into a diagram" or "map out the architecture".
  • Guidance that names the fence that actually renders, rules out ASCII art and ```text, maps intent onto supported diagram types, and gives the syntax constraints that decide whether a generated diagram parses first time — quoted node labels above all.

Detection is deliberately narrow so it does not compete with chart detection:

Request Routed to
draw the request flow diagram
give me an ERD for the orders table diagram
visualize revenue by month chart
plot the sales trend chart

maybe_append_diagram_system_message() is wired into all three generation paths (agent, model fallback, streaming), inserted after existing system messages and never duplicated.

The chart guidance is rescoped: Mermaid is barred only as a substitute for a data chart, and structural diagrams are explicitly still expected to use a ```mermaid fence.

Classic UI

chat-inline-diagrams.js follows the same extract → tokenize → sanitize → inject → hydrate pipeline as chat-inline-charts.js. Fences are lifted out before marked parses, which also protects diagram source from the table-conversion passes.

  • Streaming fence becomes a "Preparing diagram" placeholder instead of being handed half its own source and failing to parse on every token.
  • Unparseable diagram falls back to showing its source rather than vanishing — the source is still the answer the model gave.
  • SVG cached by theme and source; diagrams re-render on light/dark switch.
  • Copy and export still produce the original fence.
  • DOMPurify pass over the SVG on top of Mermaid's securityLevel: 'strict'; bindFunctions is never called.

The non-obvious trap

mermaid.initialize() sets global config and mermaid.render() reads it back. Inline chat wants a theme-aware, useMaxWidth diagram; export wants neutral and fixed-size, because a dark, width-constrained diagram does not survive being painted onto a canvas. Whichever configured last would have decided for both — meaning inline rendering could silently corrupt export PNGs.

chat-mermaid-runtime.js now owns the library: it loads the 3.4 MB bundle once (lazily), applies the caller's preset immediately before each render, and serialises renders so config always stays paired with the diagram that asked for it. The rasterizer requests export, chat requests inline. V2 already solves this the same way internally.

Also fixed

Reply previews and text-to-speech were reading internal chart/diagram/image placeholder tokens aloud. Now stripped.

Coverage

Surface Before After
V2 chat Rendered Rendered
Classic chat Code block Rendered
Exports Image Image
Model asked for a diagram ASCII art in text fence mermaid fence

V2 needed no rendering change — it picks up the guidance and starts receiving diagrams it could already draw.

Testing

10 suites pass:

  • test_mermaid_diagram_prompt_guidance.py (new) — intent detection including chart requests that must not trigger it, guidance content, idempotent insertion, wiring on all three paths, and that chart guidance no longer contradicts diagram guidance.
  • test_chat_inline_diagram_rendering.py (new) — headless Chromium renders a real diagram and asserts its labels survive; pending placeholder, source fallback, render cache, teardown, fence-detection parity with the rasterizer, and no request leaves the origin.
  • test_export_mermaid_browser_rasterizer.py, test_export_mermaid_server_render.py, test_conversation_export_mermaid_tex_images.py — exports unchanged.
  • test_chart_tool_prompt_handoff.py, test_v2_rich_rendering.py, test_browser_alert_replacement.py, test_docs_app_surface_coverage.py, test_docs_site_quality.py.

Two export tests pinned Mermaid config strings to the rasterizer file. Those assertions moved to the runtime, preserving their intent (browser and server renderers must still agree on htmlLabels: false, securityLevel: 'strict', suppressErrorRendering: true, neutral theme).

Notes

  • Version 0.261.0270.261.028.
  • No new enable_* setting and no new chat control, so the generated docs inventory is untouched.
  • No new browser dependency; the Mermaid bundle was already vendored.
  • Docs: new MERMAID_DIAGRAM_RENDERING.md, corrected the now-stale "the classic chat interface does not render Mermaid" claim in the export doc, plus release notes.

Asking for a diagram came back as box art in a ```text fence. Nothing told the
model that SimpleChat renders ```mermaid, so it drew with keyboard characters,
and the chart guidance flatly banned Mermaid on top of that.

Add intent-detected diagram guidance alongside the existing chart and image
guidance, wired into all three generation paths, and scope the chart guidance so
it only rules out Mermaid as a substitute for a data chart.

The classic client also never rendered a mermaid fence in chat, only for export.
Add inline rendering through the same pipeline as inline charts, with a pending
placeholder while streaming and a source fallback when parsing fails.

mermaid.initialize is global and render reads it back, so inline chat rendering
would have decided the theme and sizing for export PNGs. Both now go through one
runtime that applies its preset immediately before each render and serialises
them.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The V2 chat notices work landed on the base branch and claimed 0.261.028, the
same version this branch used. Git merged config.py cleanly because both sides
wrote the identical line, so the collision was only visible in release_notes.md.

Their version stands, since it landed first. This work moves to 0.261.029, in
config.py and in the test headers, version assertions and documentation that
name it. Their release notes section is kept intact under 0.261.028 with a new
0.261.029 section above it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The inline image proposal work landed on the base branch and took 0.261.029,
the version this branch had moved to after the previous merge. As before, git
merged config.py cleanly because both sides wrote the identical line, so the
collision only surfaced in release_notes.md.

This work moves to 0.261.031, coordinated with the inline image proposal and
conversation id sessions so 0.261.029 and 0.261.030 stay with them. The base
sections for 0.261.029 and 0.261.028 are preserved untouched, with a new
0.261.031 section above them.

Their changes to richBlocks.ts and AssistantMarkdown.tsx are additive: mermaid
is still in RICH_FENCE_LANGUAGES and PENDING_LANGUAGES, so markPendingFences
still guards a half-streamed fence, and the code renderer still routes mermaid
to MermaidDiagram before the chart and image proposal branches.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) merged commit 854e761 into paullizer-react-v2-ui Sep 2, 2026
2 checks passed
Paul Lizer (paullizer) added a commit that referenced this pull request Sep 2, 2026
PR #1385 (Mermaid diagram rendering) landed first and took 0.261.031, so this
branch's 0.261.030 was no longer ahead of the base and both sides claimed a
changelog section at the top.

The version moves to 0.261.032 in all four places that carry it -- config.py,
the release note section heading, the test's docstring header and its
assert_app_version_at_least floor -- rather than adopting the base's 031 and
leaving this feature documented under a number that never shipped containing
it. The base's VERSION must strictly increase on every merge into it; keeping
031 would have left two different base commits indistinguishable by version,
which is the one thing a running deployment can report about itself. The gap
where 030 was costs nothing.

Note that 032 is on neither side of the conflict: this is not a pick-one
resolution, which is the shape most easily resolved by reflex.

Changelog reads 032 -> 031 -> 029 -> 028, with the diagram sections intact and
this branch's delta being additions only. Verified: no leftover markers, no
lingering 030 reference anywhere, the V2 suite (24 Python files plus the
inline image proposal runtime checks) passes, the two diagram tests arriving
with the base pass, both docs tests pass, and the bundle builds.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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