Skip to content

Merge branch 'main' into feat/web-mermaid-chat-diagrams

MacroscopeApp / Macroscope - UI Consistency failed Aug 29, 2026 in 7m 22s

UI Consistency: 1 issue found

  • apps/web/src/lib/mermaidRenderer.ts — mermaid is initialized without suppressErrorRendering, so a failed mermaid.render() leaves its temporary div#dt3mermaid<n> (containing the "Syntax error in text" diagram) attached to document.body; mermaid throws before removeTempElements() runs, and the per-call unique id prevents later cleanup. Suggested fix: add suppressErrorRendering: true to the initialize config.

Prior findings (mermaid max-width handling, prose-margin leak in the placeholders, empty text/html clipboard flavor for copied diagrams) are resolved at this head: useMaxWidth is now disabled per diagram type instead of forcing max-width: none, placeholders use non-p elements, and sanitizedHtmlFrom rewrites .chat-markdown-mermaid[data-markdown-copy] hosts into a fenced <pre><code> block so mixed prose + diagram selections keep the fence.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Files reviewed (in scope): apps/web/src/components/ChatMarkdown.tsx, apps/web/src/components/chat/MermaidDiagram.tsx, apps/web/src/lib/mermaidLanguage.ts, apps/web/src/lib/mermaidRenderer.ts, apps/web/src/markdown-clipboard.ts, apps/web/src/proposedPlan.ts, apps/web/src/index.css.

Checks performed and found clean:

  • Toolbar controls reuse the shared Button primitive with size="icon-xs", variant="ghost", and the existing .chat-markdown-chrome-action class, keeping hover/aria-pressed treatment and tooltip semantics identical to the copy/wrap actions.
  • New global CSS is limited to mermaid's imperatively generated SVG (display: block; height: auto) and to extending the existing .chat-markdown pre scrollbar contract to the new overflow surface — appropriate global ownership; component-local geometry stays as Tailwind classes on the owner. No theme-only declarations were added, so no @variant dark/light concerns.
  • htmlLabels: false is honored at the root level in mermaid 11 (getEffectiveHtmlLabels gives the root setting precedence), so node labels render as SVG text and do not inherit .chat-markdown p prose margins inside foreignObject.
  • Loading and error placeholders use div with role="status"/role="alert", avoiding the unlayered .chat-markdown p margin rule.
  • buildCollapsedProposedPlanPreviewMarkdown now consumes whole fences; ProposedPlanCard clips the collapsed preview with max-h-104 overflow-hidden, so the taller preview cannot expand the card.
  • Clipboard: text and html flavors both restore the mermaid fence for selections inside the diagram and for selections spanning surrounding prose.

🔍 Agent activity — 23 tool calls

💭 I want to check the current PR head to see what's changed since previous review comments — specifically whether the index.css max-width issue was resolved and whether the markdown-clipboard HTML flavor concern was addressed via mermaidFenceHtml in sanitizedHtmlFrom. Let me pull up the current head files to verify.

1. browse_code — apps/web/src/components/chat/MermaidDiagram.tsx

2. browse_code — apps/web/src/markdown-clipboard.ts

💭 Checking the CSS and ChatMarkdown.tsx changes, I see they addressed the width issue by disabling useMaxWidth in the mermaid renderer config rather than using a CSS override like I expected. But this fix isn't applied consistently across all diagram types—journey, timeline, and quadrantChart diagrams may still need the same treatment.

3. browse_code — apps/web/src/index.css

4. browse_code — apps/web/src/components/ChatMarkdown.tsx

💭 Checking the remaining details: the copy button works for both cases, the wrap toggle disappearing when the diagram shows is acceptable since it changes toolbar geometry, and the error state correctly forces source view with the error banner shown. Now I'm thinking through how clipboard selection behaves when the ancestor is above the current element. Serializing a selection encounters the mermaid host div, finds its markdown-copy attribute, and returns that correctly. The HTML sanitization replaces the diagram's innerHTML with a pre/code fallback before querying and removing anything matching the sanitized selector, so the svg is properly gone by that point. Escaping order in the mermaid fence HTML generation looks right — ampersands handled before angle brackets. I also confirm the loading and error placeholders are now divs rather than paragraphs, which fixes the earlier issue, and even though they lack the mermaid-specific class, the generic markdown-copy attribute handling in serializeNode still covers them correctly. Now I'm considering a new concern: using a strict security level alongside dangerouslySetInnerHTML for Mermaid's rendered output. I'm weighing whether this is really a security issue rather than a UI consistency one, and decide it's likely out of scope for this review, so I'll skip it. Turning to theming, I note the mermaid diagram's colors come from a simple light/dark prop, so it won't adapt to any custom themes or semantic contrast tokens the app might support beyond those two options — worth checking how theme is actually computed in ChatMarkdown, since using mermaid's built-in dark/neutral themes might be an oversimplification. I also look at the scrollbar CSS added for the mermaid wrapper class, confirming it's reasonably placed alongside other generated markdown styling in the global stylesheet, and that it works fine alongside the Tailwind overflow classes on the wrapper div. I also check whether the mermaid wrapper is missing a max-width constraint like other pre blocks have, but conclude the overflow-hidden container already bounds it, so it's fine. I then consider the SVG's explicit width/height with height set to auto, which should work correctly, and think about how mermaid's built-in themes might inject their own background styling into the SVG that could visually clash with the code block's own background classes in dark mode. I also check the ID generation for each mermaid render, which increments per render to stay unique, so concurrent renders shouldn't collide even though the IDs get baked into CSS selectors within the generated SVG. Then I'm considering the theme initialization, which is a module-global variable — if two diagrams with different themes render at nearly the same time, one could end up rendering with the wrong theme since mermaid's initialize call is global to the whole app, though this is a minor edge case. I also notice a small UI inconsistency: toggling to source view shifts the toolbar buttons around since the Wrap button appears conditionally, causing Copy to shift position slightly — a cosmetic issue rather than a functional bug. For streaming, once the code block finishes streaming it correctly re-renders as a diagram, and the copy-code button copies the raw code as expected. The bigger question I'm circling back to is the useMaxWidth:false behavior — it's only applied to a specific list of diagram types, so diagram types not in that list still shrink to fit via max-width, which could be an inconsistency in how different diagram types are sized. That's an inconsistency in the feature itself rather than a regression, so I wouldn't fail on it. I'm also double-checking whether the config keys used, like per-diagram-type useMaxWidth settings and a top-level htmlLabels flag, actually match mermaid's real config schema, since some of these might not be standard properties. I'm realizing that even with top-level htmlLabels false, flowchart.htmlLabels defaults true, so foreignObject-rendered labels with divs could still inherit the global chat-markdown prose CSS rules — that's exactly the kind of style bleed this check is meant to catch. I need to verify whether the top-level setting actually propagates down to flowchart's own htmlLabels flag. I'm not entirely sure it's top-level though, and I want to verify by checking the actual mermaid-js/mermaid repo whether flowchart rendering reads htmlLabels from the top-level config or from the flowchart-specific config option. This clicks — mermaid v11 wraps node label text in <p> tags within the foreignObject span, and a known issue exists where global <p> margin rules break flowchart label layout. So a rule like .chat-markdown p { margin: 0.65rem 0 } being unscoped could bleed into .chat-markdown-mermaid labels and misalign text inside nodes, since mermaid measures label size using the same document styles. But if htmlLabels: false is set, mermaid falls back to native SVG <text> elements instead, which sidesteps the whole issue — I should check whether that's actually the default or configured here, and verify how mermaid measures label text since it sometimes renders into a temporary detached DOM node outside the app's stylesheet scope. That measurement gap versus final placement inside .chat-markdown would explain a mismatch — labels get measured without the paragraph margin but then displayed with it applied, so I want to confirm how mermaid v11's flowchart resolves the htmlLabels setting.

5. git_tools — htmlLabels

6. git_tools — htmlLabels

💭 Root-level htmlLabels seems to take precedence in recent mermaid versions, so setting it to false should correctly disable HTML labels and render them as SVG text instead — that resolves my earlier concern. I still want to verify this is actually true for 11.17.2 by checking how the config resolution works for flowchart rendering.

7. git_tools — htmlLabels

💭 Root-level config should work fine for this version. I'm also considering other UI consistency issues—mermaid's default font family doesn't match the app's design system, so I should probably override it with the app's font variable instead. I'm not too worried about that being a hard requirement, since off-palette colors already exist by default and it seems like a minor aesthetic issue rather than a functional bug. Checking through other consistency points: the copy button behavior, selection copy as plain text, accessibility attributes on the injected SVG, and whether the wrapper needs additional styling classes—all of these seem fine as-is. I'm now looking at how the diagram replaces the original pre element and whether existing markdown styles still apply correctly. renderToStaticMarkup in tests works fine since the effect doesn't fire during SSR. The new mermaid SVG styling rules belong in global CSS since they target imperative DOM content, and the scrollbar rules are similarly appropriate there. Now I'm checking the proposedPlan.ts change, which is in scope, looking at the fence-tracking logic that detects when a markdown code fence opens. Closing fences require the marker to match the opening character and length, with empty info for tilde fences, which seems fine, though there's a minor edge case where a fence opened inside a blockquote and closed outside wouldn't be distinguished since blockquote markers get stripped. I'm also considering what happens when an unclosed fence starts at index 0 — the preview would truncate to nothing before the fence, so I want to trace through the rest of the function to see how it handles building the final preview with the "..." suffix.

8. browse_code — apps/web/src/proposedPlan.ts

💭 I'm worried the collapsed preview could now include an entire fence beyond maxLines, potentially making the card much taller unless the consumer actually CSS-clips it with something like line-clamp or max-height. I need to check where buildCollapsedProposedPlanPreviewMarkdown gets consumed and whether it renders through ChatMarkdown, since a large mermaid diagram could blow up the collapsed height if there's no clipping.

9. git_tools — buildCollapsedProposedPlanPreviewMarkdown

10. browse_code — apps/web/src/components/chat/ProposedPlanCard.tsx

💭 I'm checking the edge case where an unclosed mermaid fence spans the entire plan body, confirming the preview correctly falls back to the title rather than showing truncated garbage. Now I'm tracing through the skipped-element logic for data-markdown-copy attributes—checking whether elements like codeblock headers or file-link icons get properly excluded or included when selections start partway through an anchor's descendants, since the closest() lookup in a detached clone might miss the ancestor anchor entirely. I want to verify this new fallback path only matters for live-tree serialization (not detached clones), and I'm searching for other call sites of serializeRenderedMarkdownFragment to see if any pass live nodes where the duplication risk could actually occur.

11. git_tools — serializeRenderedMarkdownFragment|chatMarkdownClipboardPayload|mermaidMarkdownCopyFromLiveNode

💭 I'm noting the only production caller uses a detached container, so those mermaid-restore branches only ever get exercised by tests, confirming my earlier flag. The author added a separate production path but left the dead branches in place, so I'm weighing whether these unreachable TS branches count as dead code worth re-flagging, since the "trace before calling dead" guidance was really aimed at selectors, not this case. Since I already flagged this at an earlier commit and they only partially addressed it, flagging it again would be a near-duplicate, so I'm leaning toward leaving it alone and instead digging into a genuine behavioral edge case: selections that span from prose into the mermaid diagram, checking whether the clone path correctly preserves the full fence via cloned data attributes and the sanitized HTML replacement. I'm confirming the same holds true for selections that start inside the diagram and end in prose afterward, since ancestor cloning preserves the chain either way, and there's no mermaid content nested inside code blocks to worry about there. Now I want to trace through the copy handler itself to verify the mermaid early-return doesn't break the logic for multi-range selections. I'm now checking UI consistency for the new component: the placeholder divs, error text styling, and whether errors could ever double up when a diagram fails — they don't, since the diagram unmounts cleanly. I'm also confirming the error callback is stable across renders so it won't cause unnecessary effect reruns. The "Show diagram" toggle clears the error state and re-attempts rendering, which seems fine even if it fails again. There's a brief empty flash during the loading placeholder, but that's acceptable. Now I want to trace where the theme value in MarkdownCodeBlock actually comes from, so I'm going to look at how ChatMarkdown determines and passes down the theme name.

12. browse_code — apps/web/src/components/ChatMarkdown.tsx

💭 When showDiagram is true, children aren't rendered so the shiki syntax highlighting suspense is skipped, avoiding redundant work. I'm also noting that the code block header would show "mermaid" as plain text since there's no dedicated icon for that language, and now I'm thinking through how strict security mode with DOMPurify sanitization interacts with dangerouslySetInnerHTML for the rendered diagram output. Checking dark theme switching, I see resolvedTheme changes trigger the effect to re-run and re-render the diagram correctly, and with multiple diagrams flipping theme simultaneously, only the first initializes the theme while others skip safely. I also verify unique render IDs are generated synchronously before any async calls, so simultaneous diagram renders won't collide, and confirm the toolbar's new source-toggle button reuses the same action class and aria-pressed pattern as existing buttons for consistency. Now I'm tracing through the mermaid render path when the wrap button gets removed from the DOM while the diagram displays, checking that the toolbar's role stays intact, and confirming mermaid.render works properly even when appending to a temporary detached element in document.body. I'm also verifying that mermaid's injected style tags remain scoped to unique IDs and render correctly inside SVG via innerHTML, avoiding any style leakage between diagrams. Rather than speculating further, I should diff the previous reviewed commit against the current head to see exactly what changed, so I can focus my review on the new code instead of re-litigating things already checked.

13. git_tools — ad2ac32..6f46986

💭 Rather than tracking down the force-push, I decide to just carefully re-check the current state for anything new. I'm double-checking the CSS rules around code blocks and scrollbars — making sure the padding, borders, and overflow behavior stay consistent between regular code blocks and the mermaid wrapper. The horizontal padding mismatch between diagram and code source view is just a minor aesthetic nit, not a real issue, since the header still aligns with the diagram. I'm also confirming the mermaid SVG sizing works correctly — with useMaxWidth disabled, the explicit width attribute combined with height:auto should preserve the aspect ratio via the viewBox properly. I'm checking the streaming-to-rendered transition, the test-only data-mermaid attribute, and the copy button behavior — all seem consistent, though the clipboard copy preserves whatever fence language (like "mmd") the user originally wrote, which is a minor but acceptable quirk. I'm also noting that loading/error placeholders carry the same copy attribute as the rendered diagram. This copy mismatch during loading is trivial and not worth flagging. I'm scanning other areas — shared controls, CSS ownership, scrollbar styling — and confirming the scrollbar-width rule was split off from the pre block correctly. I keep checking for real issues: when a diagram renders, the code block's children aren't in the DOM, but the block copy button still uses the code state so that's fine, and select-all copy would grab the mermaid host's data attribute instead — also fine. I'm also confirming overflow-hidden on the container doesn't clip the diagram since the mermaid wrapper scrolls horizontally and the container grows vertically, so no real regression there. Now I'm reconsidering the useMaxWidth: false choice as a potential UI regression, since mermaid's own output width behaves differently without that flag.

14. git_tools — text-destructive|role="alert"|aria-busy

💭 I'm noticing both MermaidDiagram's built-in error branch and MarkdownCodeBlock's error banner exist, but since onError sets showSource=true and unmounts MermaidDiagram, its own error UI is effectively dead code in this usage — a minor duplicate branch worth flagging. Checking the sanitization path further, mutating innerHTML on the detached clone container is safe, and the multi-range clipboard HTML joining multiple meta charset tags follows an existing pattern elsewhere, so that's fine too. Selecting a partial label inside a diagram now copies the entire fence rather than just the selected text, which is a deliberate tradeoff since partial diagram copies wouldn't make sense. The bigger observation is that diagrams render with mermaid's built-in dark/neutral palettes rather than the app's custom theme system, which seems like a reasonable v1 choice rather than a real regression given it's a new feature, not something being removed. No dark/light theming needed for these new rules, that's fine. But I'm noticing the .chat-markdown pre rule explicitly caps max-width: 100%, which suggests they hit overflow issues in flex containers — yet the mermaid wrapper doesn't have the same constraint, even though it sits inside the same codeblock container as the pre element. That's still block-level, so its width stays constrained—no new violation found there. Given the output format requires structured JSON, I'll resolve the conflict by setting state to success with a title and summary along the lines of "All clear" rather than a bare string. Let me also check the mermaid renderer for a possible cleanup bug: whether renderMermaidSvg properly removes the temporary DOM node mermaid creates during rendering, since mermaid v11 normally appends and removes a temp element unless no container is passed—worth verifying against mermaid's actual behavior before flagging it.

15. git_tools — removeTempElements|enclosingDiv|#d${id}

💭 I'm noticing that when suppressErrorRendering isn't set, mermaid renders the error diagram into the temp element but then throws before reaching removeTempElements—meaning that temporary div stays attached to document.body permanently on any parse error. This looks like a genuine leftover-DOM bug: an invalid mermaid fence leaves a visible "Syntax error" bomb-graphic SVG appended to the body with no cleanup, and since it's rendered with full width and no containing style, it could actually show up on the page rather than staying hidden. I want to verify this ordering holds in the specific version (11.17.2) rather than just the main branch, since this resembles a known long-standing mermaid issue about parse errors leaving stray DOM elements behind. The typical fix apps use is either calling mermaid.parse() before rendering, or setting suppressErrorRendering: true, which properly cleans up temp elements before throwing instead of leaving the error diagram behind. I should check whether that same file at the v11.17.2 tag confirms this same ordering issue.

16. git_tools — parseEncounteredException|removeTempElements()

💭 Let me try the tree URL form with the git_grep repo URL instead.

17. git_tools — parseEncounteredException

18. github_api_read_only — repos/{owner}/{repo}/tags?per_page=30&page=1

💭 I'm checking whether this ordering issue—throwing the parse exception before cleaning up temp elements—has been present in mermaid's codebase since v11.x, confirming it's a longstanding, known bug rather than something recent. Tracing through both error paths: for parse errors, the exception thrown at line 653 leaves the temp div behind; for draw errors, the code renders an error SVG then rethrows without ever calling removeTempElements(), so either way an invalid diagram leaves a stray <div id="dt3mermaidN"> appended to document.body. Now I want to know if this leaked div is actually visible in this app's UI—it depends on whether body or the root container clips overflow. If overflow is hidden and the root is a fixed viewport height, the leaked div would be invisible but still polluting the DOM; if not, it could create an unwanted scrollbar or visible artifact at the page bottom, so I should check the app's CSS rules for body/html and the root element. This looks like a genuine, small, self-contained fix: setting suppressErrorRendering: true in the mermaid initialize config would stop the DOM leak and let the component's existing error-handling (showing source + error) work cleanly, scoped to mermaidRenderer.ts.

19. git_tools — ^body|^html|#root

💭 Since the body has hidden overflow, that leaked temp div gets clipped below the viewport rather than showing visually — but it still piles up DOM nodes with SVGs and styles over time, so it's a silent memory leak rather than a visible bug. I'm also noting that the cleanup function removes an element by matching id before the next render, but IDs should be unique per instance. The real question is whether this counts as a UI consistency issue or more of a correctness/behavior bug outside that check's scope — missing suppressErrorRendering causes wasted render work and leaked nodes, but functionally the component still shows the source and error message correctly. I'm weighing whether this defect is significant enough to flag given the check's focus areas. I want to confirm this ordering actually holds for 11.17.2 specifically rather than just the current default branch, so I should check the mermaid repo's history for when suppressErrorRendering was introduced and whether removeTempElements was ever reordered relative to the throw. Let me search the git log with pickaxe on both terms to be sure, and also sanity-check whether this is a known reported issue in mermaid's tracker.

20. git_tools — packages/mermaid/src/mermaidAPI.ts

💭 I'm tracing the commit history to confirm this code structure has been stable since the 2023 release, and double-checking that suppressErrorRendering exists in the mermaid 11.17.2 config type, since it was introduced back in mermaid 10.7. That confirms finding #1: mermaid render failures leak a temp error div into document.body, fixable by adding suppressErrorRendering: true. I'm weighing whether this counts as a genuine regression worth failing the check, given the leaked node is invisible due to overflow hidden, or whether I should keep looking for a more clear-cut UI-consistency issue — like the diagram types not covered by useMaxWidth: false, which might reveal a more concrete rendering inconsistency. When useMaxWidth is true, the width becomes 100% with a max-width style cap but no height attribute is set, leaving sizing to the viewBox aspect ratio. Without the new CSS rule, an SVG lacking a height attribute traditionally defaults to height:100% per spec, which is the classic bug that causes distorted or oversized rendering — so the new height: auto rule is what actually fixes that. For the other case where both width and height are set as pixel attributes, height: auto in CSS overrides the height attribute, recomputing height from the aspect ratio and the used width, which ends up matching the natural size anyway, so that's consistent too. The CSS approach seems sound overall, and I don't see problems with display: block or the markdown line-height styling, nor with the dark/neutral theme fallback logic. Let me check another part of the code now. Checking the diagram toggle button behavior confirms it reappears correctly on render errors, and the copy-selection path for diagrams within streaming messages also looks fine. I want to verify how isMermaidFenceLanguage and extractFenceLanguage interact with MarkdownCodeBlock passing the language down to MermaidDiagram for clipboard purposes — that seems fine too. My conclusion is to leave one inline comment about the temporary element leak in the mermaid renderer. I'm also weighing whether this leak actually falls within scope, since it touches accessibility as well as UI consistency — the leaked error SVG left in the document body isn't just visually hidden, its text content ("Syntax error in text", version string) remains in the accessibility tree, meaning screen readers and browser find-in-page could still surface it even though it's clipped from view. I need to add suppressErrorRendering: true, right after the securityLevel: "strict" line, so I'm locating the exact line number in the new file to anchor the inline suggestion correctly, tracing through the module's structure from the type definitions down to where the mermaid config object is initialized.

21. browse_code — apps/web/src/lib/mermaidRenderer.ts

💭 I'm figuring out the right diff line to attach the inline comment to, suggesting that line 21 be replaced with both the securityLevel and suppressErrorRendering settings, properly indented. Then I pause to consider whether there's anything else worth flagging in the Mermaid component. I want to double-check that suppressErrorRendering is actually a valid property on MermaidConfig — I recall it was added in v10.7, but I should verify this in the type definitions to be sure.

22. git_tools — suppressErrorRendering

💭 Since I found a moderate severity issue, and the default guidance is to fail unless there are no findings, I'll set the check state to failure. Now let me post the inline comment.

23. modify_pr — One finding on the new mermaid renderer: failed renders leave mermaid's temporary error-diagram element attached to document.body.


Agent Credits: 267 credits

Token usage: 42 uncached input · 2,024,549 cache-read input · 140,320 cache-write input · 25,774 output
Agent Credits may also include non-token charges from external tools such as web research.