Skip to content

Fix V2 diagram sizing, scrolling and render failures - #1393

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

Fix V2 diagram sizing, scrolling and render failures#1393
Paul Lizer (paullizer) merged 2 commits into
paullizer-react-v2-uifrom
paullizer-mermaid-diagram-ux

Conversation

@paullizer

Copy link
Copy Markdown
Collaborator

Diagrams in the V2 chat rendered at roughly a quarter of their natural size, made long threads unusable, and sometimes failed to render with nothing to explain why.

Every cause was reproduced against the vendored mermaid 11.17.2 bundle in Chromium, configured exactly as MermaidDiagram.tsx configures it, before anything was changed.

Diagrams rendered too small, and grew when Colors was opened

Same bug, seen from two sides. The assistant bubble is a shrink-to-fit flex item, and mermaid emits width="100%" with a viewBox and no height, which contributes nothing to CSS intrinsic sizing. The bubble therefore collapsed to the width of the diagram's own toolbar. Opening Colors rendered a wrapping palette row that does have a natural width, which is why the same diagram suddenly became legible.

Measured in Chromium, using the natural size of the Azure governance diagram from the report:

Panel Diagram drawn at
Before 358px 300px — 27% of natural size
Before, Colors open 575px 517px — 47% of natural size
After 1024px 966px — 88% of natural size
After, Colors open 1024px unchanged

The panel now takes its width from the diagram's measured natural width. Because that width is definite, the colour menu can no longer resize anything.

flowchart.wrappingWidth is also raised from mermaid's default of 200, which was wrapping long labels into narrow columns and making diagrams taller: the same diagram measures 273 × 955 at 200 and 497 × 867 at 500.

Long diagrams broke scrolling

Three compounding faults:

  • No height cap. A flowchart at mermaid's own limit of 500 edges measures 50,466 pixels tall, and went straight into the scroll container where the browser re-rasterizes it on every scroll frame. The stage is now bounded and scrolls internally.
  • MessageBubble was not memoised, and readMaskState() ran unmemoised. The list re-renders on every streaming token and every time the scroll crossed the pinned-to-bottom threshold, so the whole remark/rehype pipeline re-ran for every message in the thread each time.
  • The bottom was unreachable. The auto-scroll effect never re-fired after a diagram rendered asynchronously and replaced its 96px placeholder with a much taller panel. A ResizeObserver now follows content that grows late.

Some diagrams never rendered, with no diagnostic

The error object was discarded outright — .catch(() => setState({ status: 'error' })). No message, no console.warn, nothing beyond the words "Diagram could not be rendered". There was also no render timeout and no source-size guard, both of which the classic client has.

Fourteen distinct parse failures were reproduced, all of them things models actually write:

Source Mermaid's response
end, graph, class, style as node ids Parse error — all reserved
End closing a subgraph Parse error — only lowercase end is accepted
A subgraph with no end Parse error, reported at the last line of the diagram
a[""] Parse error
a[App (main)] Parse error
a -->|metadata: {}| b Parse error
a["He said "hello" loudly"] Parse error — the string token ends at the second quote
a["A"] b["B"] on one line Parse error
A trailing b --> with no target Parse error
A leading byte-order mark "No diagram type detected"
500 edges "Edge limit exceeded"

The reason is now kept, logged, and shown behind Show details, with a Copy source button. Mermaid's limit errors are reworded. Before the reader sees any of that, the source is repaired and rendered once more.

Two properties of the repair matter:

  • It only runs after a failure. A diagram mermaid accepts is handed over untouched, so nothing that renders today can be changed by it. All six working diagrams in the test set are byte-identical after repair.
  • It is scoped to flowcharts. subgraph, end, square labels and piped edge labels all mean something else in the other diagram types — ||--o{ in an erDiagram, for one.

No way to make a diagram bigger

  • / + / fit, scaling between 0.4× and 4× of the fit-to-width scale.
  • A drag handle on the stage's bottom edge, exposed as a slider so it works from the keyboard, with Home returning to the automatic height. The chosen height is stored on the message beside the colours, and the two are independent: resetting colours does not resize a diagram, and resizing one does not stop it following the reader's palette.
  • Expand opens a full-screen viewer with its own zoom and PNG download, following the conventions ImageLightbox established.

The viewer is defined inside MermaidDiagram.tsx rather than its own file. It writes diagram markup to the DOM, and test_v2_rich_rendering.py::test_sanitizer_boundary_at_every_html_sink fixes the set of components allowed to do that; keeping it in the reviewed file preserves that invariant rather than widening it.

Prompt guidance

The guidance now names the reserved words, requires a lowercase end for every subgraph, tells the model not to carry placeholders such as <random GUID> out of pasted text into a label, and asks for short labels across several nodes instead of one node holding a dozen <br/> lines.

Defects caught in review

Four, all silent rather than visible, each fixed with a regression test:

  1. A stored height survived a source change. The previous entry's height was carried forward without comparing its source_hash, then re-stamped with the new fingerprint — defeating the guard the client already honours.
  2. Renaming a reserved id ate the subgraph terminators. end is both the most common reserved node id and the keyword closing a subgraph. Renaming every occurrence rewrote the terminators, so one was appended at the bottom and everything after the original was swallowed into the group. The result still parsed, so it showed the wrong structure rather than an error.
  3. A pipe inside a node label was paired with the edge-label pipe, escaping the arrow into the middle of a label.
  4. {"height": Infinity} returned a 500. json.loads accepts the bare Infinity token and round(float('inf')) raises OverflowError, which is not a VisualStyleError.

Validation

functional_tests/test_v2_diagram_viewer_controls.py — 18 checks, including 58 bundled TypeScript behaviour checks.

Verified in Chromium against the real mermaid bundle: all 14 reproduced failures render after repair; all 6 working diagrams are untouched; the layout measurements above; and DiagramStage mounted directly to measure fit scale, zoom, resize, clamping and reset.

Full suite re-run and passing after merging the base branch: diagram viewer 18, visual style 16, rich rendering 13, inline diagram 5, prompt guidance 7, mermaid server render 13, browser rasterizer 3, export mermaid/tex 18, agent-model exclusivity 9, reasoning persistence 7, export progress 8, docs coverage 7, docs quality 6. Typecheck and vite build clean.

Notes

No new npm package and no remote asset. Mermaid and DOMPurify were already vendored locally, so the default-src 'self' Content-Security-Policy is unchanged.

Version 0.261.0360.261.037.

Diagrams in the V2 chat rendered at roughly a quarter of their natural size,
made long threads unusable, and sometimes failed to render with nothing to
explain why. Each cause was reproduced against the vendored mermaid 11.17.2
bundle in Chromium before anything was changed.

Sizing. The assistant bubble is shrink-to-fit and mermaid emits width="100%",
which contributes nothing to intrinsic sizing, so the bubble collapsed to the
width of the diagram's own toolbar. Opening the Colors menu introduced a
palette row that does have a natural width, which is why the same diagram
suddenly became legible. The panel now takes its width from the diagram's
measured natural width: 300px to 966px on the reported diagram, and the colour
menu no longer resizes anything.

Long diagrams. A flowchart at mermaid's 500-edge limit measures 50,466px tall
and went straight into the scroll container. The stage is now capped and
scrolls internally. MessageBubble was also unmemoised, so every streaming token
re-ran the markdown pipeline for every message, and the auto-scroll never
re-fired after a diagram rendered asynchronously and grew the thread, which is
why the bottom became unreachable.

Render failures. The error object was discarded entirely. It is now kept,
logged and shown behind a details disclosure, with a render timeout and a
source-size guard matching the classic client. Fourteen reproduced parse
failures are repaired and retried; the repair runs only after mermaid has
already refused the source and is scoped to flowcharts, so nothing that renders
today can be changed by it.

Viewing. Zoom, a keyboard-operable resize handle whose height is stored on the
message beside the colours, and a full-screen viewer. The viewer lives inside
MermaidDiagram.tsx so diagram markup still reaches the DOM in exactly one
reviewed sanitizer boundary.

No new npm package and no remote asset, so the Content-Security-Policy is
unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The base branch added an export-registration hook to MermaidDiagram.tsx and
moved the version to 0.261.036. Both merged cleanly against the diagram viewer
rewrite: `registerExportDiagram` still runs before the early returns, and it
still reads the inline SVG through the figure ref, because the new full-screen
viewer renders outside the figure rather than inside it.

Resolved by hand:
- config.py, renumbered to 0.261.037.
- release_notes.md, with the diagram viewer entries moved to their new version
  and the base's 0.261.034 through 0.261.036 sections left in place.

Version references in the new test and fix documentation renumbered to match.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) merged commit 15bb3e8 into paullizer-react-v2-ui Sep 3, 2026
2 checks passed
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