Render Mermaid diagrams in chat replies - #760
Merged
wingleeio merged 4 commits intoOct 3, 2026
Merged
Conversation
Assistant replies now draw ```mermaid fences as diagrams with the file preview's engine, fence frame, source toggle, Copy action and lightbox. Inline images in chat keep their text rendering. Streaming never renders a fence that may still be growing: only blocks a later row of the same reply follows, or blocks of a completed reply, request a diagram, so per-token commits start no render work. Until a diagram is ready the fence shows its source, and a failed render keeps the source with the engine's diagnostic in a header marker. Rows request their fences while laying out, so only painted diagrams are rendered, one at a time off the UI thread; requests whose rows scrolled away are dropped. Results are retained under a byte budget, least recently painted first out. A swap remeasures only the rows painting it and uses the existing layout signals, so the bottom pin follows and the own-turn runway absorbs the change without moving the sent prompt.
decode_image reserved the largest preview any view could request (900x480 at 4x density) for every SVG and Mermaid diagram, so a 177x348 class diagram counted 7.6 MiB against the 64 MiB media budgets. The file preview stopped drawing diagrams after about eight per document, the chat cache retained about eight, and the lightbox had little room left to enlarge. Media now counts its source plus the CPU and GPU copies of the raster it holds. Since admission no longer reserves headroom, the file preview and the chat diagram cache re-check their budget before a larger re-raster (MediaImage::preview_within); a variant that does not fit keeps the current raster.
jsgrrchg
marked this pull request as draft
October 3, 2026 02:20
Render diagrams on a transparent canvas so they sit on the fence body, with rounded hairline cards for nodes, muted labels and connectors, and a soft accent tint on default decisions, sequence notes and activations. Gantt bars take their hues from the accent and gridlines use the theme border. Explicit style and classDef colors are kept. Tighter spacing keeps labels larger once a diagram is scaled to the reading column, and the lightbox draws diagrams on a plate in the fence body color.
jsgrrchg
marked this pull request as ready for review
October 3, 2026 03:28
ready_count and ready_media serve the transcript tests, which only compile on Linux, so they were dead code (with a warning) on macOS and Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wingleeio
approved these changes
Oct 3, 2026
wingleeio
left a comment
Contributor
There was a problem hiding this comment.
Security-audited and reviewed. Pushed one small fix: gated the Linux-only MermaidCache test helpers so they don't warn as dead code on macOS/Windows. Thanks!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The file preview already draws
```mermaidfences with the nativemermaid-rs-rendererengine, but chat did not: when an agent replied with a diagram, the transcript showed only the source. The renderer had a hook for this (MediaUi.diagram), and the transcript passedmedia: None.While measuring the chat integration, I also found that every prepared SVG and Mermaid diagram was charged against the media budget at the largest raster any view could ask for. This applied to the file preview as well. As a result, the file preview stopped drawing diagrams after about eight per document.
Change
Mermaid in chat
MediaUi.imageis now optional, so this change does not alter them.Streaming safety
A fence that may still be growing is never rendered:
Scroll and runway
When a diagram replaces its source, only the rows painting that diagram are remeasured. The change then raises the same layout signals as other row-height changes: the viewport layout revision,
wake_springwhen the transcript is pinned, andown_turn_kickwhen an own turn is active. As a result:Media accounting fix (file preview and chat)
decode_imagereserved a 900×480 raster at 4× density for every SVG, so a 177×348 class diagram counted as 7.6 MiB. Media now counts its source plus the CPU and GPU copies of the raster it actually holds.Admission no longer reserves headroom. Instead, the file preview's
resize_mediaand the chat cache'sset_viewre-check the budget before re-rasterizing at a higher width or density (MediaImage::preview_within). A larger variant that does not fit keeps the current raster: slightly softer, never over budget.Screenshot
Mermaid flowchart rendered directly in an assistant reply in the running Linux app.
Performance
All numbers below come from a temporary release-mode harness. It is not included in the PR. It used the six fixtures in
scripts/fixtures/markdown-preview/, on Linux, with 10 iterations per diagram.UI thread: effectively zero added cost
begin_frame+ unchangedset_view)Even with five diagrams on screen at 60 fps, this stays below 1 µs of a 16.6 ms frame. Rows without Mermaid only clone one shared
Rchandler, which is built once per transcript rather than once per frame.Streaming adds no work: the tail fence is never rendered.
Off the UI thread: one-time work per diagram
Rows request fences while they lay out, so only painted diagrams cost anything. One serialized loop per transcript renders them on the background executor. Between renders it picks the next source and drops requests whose rows have scrolled away, rather than queueing them. GPUI loads the resulting image asset on its background executor too, so neither step blocks a frame.
The source stays visible for these few milliseconds, and the diagram then replaces it.
Memory: retention budget and accounting fix
The chat keeps diagrams under a 64 MiB / 64-entry budget, evicting the least recently painted first. Diagrams painted in the latest two passes are never evicted, and an evicted diagram simply renders again when its row returns. A theme change discards every diagram and rejects results that were computed under the old theme.
The accounting fix (second commit), measured before and after with the same harness:
In the "after" row, the other 4 lightbox diagrams are small and already shown at full resolution in the column, so there is no larger raster to make. Per-frame cost is unchanged: the budget sum is only recomputed when the width or display density changes.
These are synthetic measurements. Frame timing in the running app (
ZERON_FRAME_STATS=1) has not been captured yet.Implementation notes
markdown/mermaid_cache.rs(new) holds the cache: requests scoped to paint passes, stale-request dropping, LRU eviction, theme invalidation, raster view tracking, and source toggles.markdown/render.rs: the diagram handler returnsDiagramView::{Source, Failed(reason), Diagram(DiagramUi)}.CodeBlockTooltiptakes aSharedStringso the failure tooltip can show a dynamic diagnostic.image_media::preview_elementis the clickable centered media element, now shared by the file preview and chat.MediaImage::preview_withinis the budget-aware re-raster.transcript.rs:ensure_diagram_worker/finish_diagram);diagram_layout_changed;attachment_previewthroughlightbox_with_sizewith the diagram's natural size, and releases its enlarged raster on close.docs/markdown-preview.mdgains a "Mermaid in chat" section, along with the updated limits and validation notes.Testing
cargo test -p zeron-ui --lib -- --test-threads=1: 1529/1529 passed.markdown_drag_tracks_each_table_column_and_wrapped_cellandrendered_truncation_resizes_and_selects_the_original_urlfailed. Both pass alone and in the sequential run; it is the same parallel-only flakiness as in earlier PRs.cargo check -p zeronpasses. Rustfmt is clean for the changed files, andgit diff --checkpasses. Clippy reports no new lint kinds; the warnings it shows on changed lines are the sametype_complexityandcollapsible_ifpatterns as the original code.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.