Skip to content

feat(web): render mermaid fences as diagrams in chat

MacroscopeApp / Macroscope - UI Consistency failed Aug 28, 2026 in 5m 29s

UI Consistency: 4 issues found

Reviewed the new mermaid diagram surface in chat markdown. Four inline comments were posted.

  • apps/web/src/index.css (L2341-2345): .chat-markdown .chat-markdown-mermaid svg { max-width: none } is defeated by mermaid's inline style="max-width: <width>px" (emitted by configureSvgSize when useMaxWidth is on, the default). The declaration never applies, so the diagram keeps scaling to container width and the new overflow-x-auto wrapper can never scroll. Fix: disable useMaxWidth in mermaid.initialize, or mark the declaration !important.
  • apps/web/src/components/chat/MermaidDiagram.tsx (L50-72): the loading and error branches use <p> inside .chat-markdown, picking up the unlayered .chat-markdown p { margin: 0.65rem 0 } prose margins inside the code-block chrome (layered Tailwind my-0 would not win). Existing chat-markdown placeholders use a non-p element with a status role.
  • apps/web/src/components/chat/MermaidDiagram.tsx (L76): the new horizontal scroll container does not use the chat-markdown thin scrollbar treatment used by .chat-markdown pre and the file-link tooltip, so toggling between diagram and source changes scrollbar geometry and color inside the same code block.
  • apps/web/src/components/ChatMarkdown.tsx (L874-878): the diagram error message has the same <p> prose-margin leak inside the code-block chrome.

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.

Methodology:

  • Read the full changed files at head: ChatMarkdown.tsx (MarkdownCodeBlock, pre renderer), chat/MermaidDiagram.tsx, lib/mermaidRenderer.ts, lib/mermaidLanguage.ts, and the added index.css rule.
  • Traced cascade ownership for the new markup: confirmed the markdown root carries chat-markdown (ChatMarkdown.tsx:2307) and that .chat-markdown p, .chat-markdown pre, and the chat-markdown-chrome-action color rules are unlayered global CSS, which the file itself documents as outranking Tailwind utilities (CHAT_MARKDOWN_WORKSPACE_IMAGE_CLASS_NAME comment).
  • Verified mermaid's SVG sizing behavior against upstream source (packages/mermaid/src/setupGraphViewbox.js, calculateSvgSizeAttrs) and confirmed cleanUpSvgCode/DOMPurify do not strip the inline style attribute, so the injected inline max-width survives into the rendered markup.
  • Compared the new overflow container with the existing chat-markdown overflow surfaces (.chat-markdown pre scrollbar rules in index.css, the tooltip path container in ChatMarkdown.tsx:1659) and the global ::-webkit-scrollbar fallback, which sets width only.
  • Checked items that are not findings: the source/diagram toggle correctly reuses the shared Button + Tooltip primitives with the existing chat-markdown-chrome-action contract and aria-pressed; the clipboard contract reuses the pre-existing generic data-markdown-copy sink in markdown-clipboard.ts; diagram rendering is gated off while streaming; securityLevel: "strict" and htmlLabels: false are set before rendering; theme changes re-initialize mermaid and re-render mounted diagrams.

Scope: only files matching apps/web/src/**/*.{ts,tsx,css} were targeted; package.json and pnpm-lock.yaml were used as context only.


🔍 Agent activity — 21 tool calls

💭 I want to check the changed files in the repo, including the chat markdown component, mermaid diagram handling, and related styling and plan files, so let me look at the actual code.

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

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

💭 I'm checking UI consistency concerns—the mermaid SVG rendering with strict security level, theme prop handling between light/dark and dark/neutral mappings, and the global CSS ensuring mermaid's generated SVG displays as a block element without max-width constraints. Since mermaid injects raw SVG via innerHTML, Tailwind can't style it directly, so global CSS is justified per the rules for imperative DOM content. I'm now checking whether the CSS selector correctly nests under the chat-markdown root class, and looking at how data-markdown-copy is used in the clipboard serializer—since the diff only touched the test file, that attribute must already be supported elsewhere. Moving on, I'm checking that the onError callback in MermaidDiagram stays stable via useCallback, that the loading placeholder is reasonable, and that the error-state flow correctly falls back to showing source with an alert role when rendering fails. I'm also verifying the diagram/streaming condition, icon color consistency with existing toolbar buttons, and how hiding the wrap button when the diagram renders affects the toolbar layout—then checking index.css for any related codeblock styling around the mermaid data attribute. Now I want to dig into whether the mermaid theme ("dark"/"neutral") should be hardcoded or derived from the app's semantic color tokens, since contrast/accessibility rules require chrome elements to use those tokens, and I need to confirm whether custom themes beyond light/dark need to be preserved here.

3. git_tools — chat-markdown-codeblock|chat-markdown-shiki|chat-markdown-mermaid

4. git_tools — data-markdown-copy

💭 The clipboard handling looks generic and solid. I'm considering whether the SVG's inline max-width override should instead be handled through a Tailwind arbitrary variant targeting the nested svg, since that DOM is generated rather than authored directly. Then I'm weighing whether the mermaid theme choice properly respects custom app themes, since diagrams could end up mismatched against a themed palette—though this feels like a stretch rather than a real bug. Everything else checks out: strict security sanitization is in place, the temporary render container is fine, and the source-view toggle attribute works as expected, though I want to double check accessibility when the source view is dismissed after an error. Now I'm noticing the wrap button is intentionally hidden while a diagram displays, and the show source/diagram toggle button correctly updates its aria-pressed state and label. More interestingly, the diagram's scrollable container relies on default browser scrollbars while the code block's pre element uses custom-styled scrollbars nearby, which could look visually inconsistent within the same block—I want to check the scrollbar CSS rules to confirm.

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

6. git_tools — isMermaidFenceLanguage|renderDiagram|MermaidDiagram

💭 I'm looking at how the code component in ChatMarkdown.tsx derives its theme and language settings.

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

💭 I want to check where resolvedTheme comes from, likely useTheme, and see if there's an existing lazy-loading pattern or skeleton primitive used elsewhere in chat markdown components for loading states, so I can follow the same convention here. I'm also walking through a few more edge cases: the effect resetting showSource/diagramError on code changes during streaming seems harmless, the wrap-lines button reappearing when toggling source view seems fine, and the aria-pressed styling on the source toggle looks reasonable, though I should check for potential duplicate error rendering. This scrolling surface doesn't pick up the thin-scrollbar treatment used elsewhere in the chat markdown, which is a real consistency gap worth checking against a shared scrollbar utility class if one exists. I'm also considering whether the mermaid diagram's background respects custom theme tokens, since it only seems to distinguish light/dark rather than adapting to the themed code-background variable.

8. git_tools — scrollbar-width|scrollbar-thin|::-webkit-scrollbar

9. git_tools — resolvedTheme

💭 I notice line 1659 in ChatMarkdown.tsx already has a horizontally scrolling surface with thin scrollbar styling matching the chat-markdown pre blocks, but the new mermaid container's overflow-x-auto doesn't adopt that same treatment—so there's a real inconsistency in scrollbar styling within the same code-block chrome. I'm also checking whether the global webkit scrollbar width variable affects horizontal scrollbars, since it may only set width and leave height at default, so I want to look at how it's defined in index.css.

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

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

💭 I'm noticing the scrollbar styling mismatch: the chat markdown code blocks use a custom thin scrollbar, but the new mermaid container falls back to the global scrollbar rule that only sets width, not height, leaving it with a thicker default horizontal scrollbar. In Firefox, the same container doesn't inherit thin scrollbar-width either, so toggling between source view and diagram would produce visibly inconsistent scrollbar chrome — a moderate but real finding, fixable by extending the existing scrollbar rule to also target the mermaid container. Now I'm looking for a theming concern: renderMermaidSvg caches an initializedTheme at module scope, which could create a race if multiple diagrams render concurrently under different themes since mermaid.initialize is a global, shared call. I check that theme switches correctly re-run the effect since theme is a dependency, and confirm the temporary render target and injected SVG don't create real accessibility or selection issues since mermaid already adds appropriate ARIA attributes. I also verify the copy button behavior stays consistent and start checking whether the new loading/error <p> elements inside the code block might clash with the existing .chat-markdown p margin rules.

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

💭 I see that the .chat-markdown p margin rule unintentionally applies to the loading and error <p> elements inside the code block, since they're nested within .chat-markdown. This creates a layout inconsistency where the placeholder text gets extra prose margins on top of its existing padding, which isn't the intended styling. I'm also noticing the two error surfaces use inconsistent padding between components — ChatMarkdown uses pb-1 while MermaidDiagram uses pb-2, a small but concrete visual discrepancy worth flagging. Beyond that, I'm spotting a more substantial issue: mermaid sets an inline max-width style on its SVG when useMaxWidth is enabled, and inline styles override stylesheet rules unless marked !important. So the CSS rule intended to let diagrams render at natural width and scroll horizontally is likely a no-op against mermaid's own inline styling, silently defeating that layout intent. Since it's an inline style attribute, the CSS max-width: none rule gets overridden and effectively becomes dead code, while height: auto still works since inline height is a presentational attribute with lower priority. This is a well-known mermaid quirk (the "useMaxWidth" issue), so I'm fairly confident flagging it, but I should phrase it carefully since it's a partial fix rather than a full bug. Since I shouldn't guess without proof, I should actually verify this against mermaid's real source rather than assume — and since mermaid-js/mermaid is public on GitHub, I can search its repo for the configureSvgSize function to confirm my assumption.

13. git_tools — max-width

💭 Mermaid injects an inline max-width style on the SVG since useMaxWidth defaults to true, and that inline style overrides my stylesheet's max-width:none rule since inline styles beat author stylesheets without !important. That means my override is dead code, and wide diagrams just get scaled down instead of triggering the overflow-x-auto scroll wrapper. I should double-check whether mermaid's render pipeline (DOMPurify sanitization, cleanUpSvgCode) strips or modifies that style attribute before I chase this further, so let me look at how mermaidAPI actually processes the output.

14. git_tools — max-width|useMaxWidth

💭 Let me search more broadly across the mermaid rendering code for cleanUpSvgCode.

15. git_tools — cleanUpSvgCode

💭 The inline style still overrides the stylesheet's max-width:none, so the intended natural-size horizontal scroll never kicks in — that's a dead declaration. I'm also checking mermaid's injected SVG style block, but its rules are properly scoped by id so that's not an issue. Theme routing looks fine too since resolvedTheme normalizes to light/dark even for custom themes, and securityLevel/htmlLabels settings are correct. The remaining minor point is that loading/error placeholders inherit .chat-markdown p margin and overflow-wrap behavior, which I might fold into one combined note — and I should check how the workspace image component handles its loading state to see if there's an existing spinner/skeleton pattern worth reusing for consistency.

16. git_tools — function ChatMarkdownWorkspaceImage|function ChatMarkdownImageFallback

💭 I'm noticing the mermaid loading placeholder diverges from the existing chat-markdown convention: it uses a <p> with visible text and no role="status", unlike the skeleton-block pattern used elsewhere, and it doesn't reserve space to prevent layout shift when the diagram replaces it. I think I have enough findings now to wrap up. The strongest issue is that max-width: none in index.css gets overridden by mermaid's inline max-width style, since inline styles win over unqualified CSS rules — meaning the intended horizontal-scroll wrapper never actually scrolls and wide diagrams just get scaled down instead. That's a genuine layout regression worth flagging, likely fixable via a mermaid config option or an !important override, and it falls within the scroll/overflow scope I'm reviewing. I'm also looking at whether .chat-markdown p margins cause spacing issues. Second, I'm noticing that placeholder and error <p> elements inside the code block chrome inherit .chat-markdown p margins, adding unwanted spacing since they don't follow the existing role="status" span convention — that's a concrete spacing bug in MermaidDiagram.tsx and ChatMarkdown.tsx, since the code block's overflow-hidden creates a new block formatting context so the margin actually renders as visible gaps rather than collapsing. Third, I'm considering that scrollbar styling might look inconsistent between the mermaid overflow container and the existing .chat-markdown pre scroll treatment, though this issue is somewhat contingent on the first fix being applied. I'm settling on three focused inline comments: one on the index.css rule about max-width being defeated by mermaid's inline style, one on the placeholder/error paragraph margin issue with the role=status convention note, and one on the scrollbar treatment matching .chat-markdown pre for horizontal overflow. I'm verifying the exact diff line numbers for each comment so they land within the correct hunks in both files. I want to check where ChatMarkdown imports its icons from and confirm Code2Icon and EyeIcon actually exist in that library.

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

18. git_tools — Code2Icon|EyeIcon|EyeOffIcon

💭 The icons look fine, and I notice the existing chrome-action icons don't use aria-hidden while other icon usages elsewhere in the file do—so this is consistent with the local pattern already. Now I want to verify the paragraph margin claim by checking whether ChatMarkdown's root class is actually "chat-markdown".

19. git_tools — "chat-markdown"

20. git_tools — chat-markdown

💭 I'm confirming the root uses .chat-markdown, so .chat-markdown p sets margin on the new paragraphs. There's a comment noting these unlayered global rules intentionally outrank Tailwind utilities, since Tailwind lives in a @layer utilities cascade layer that loses to unlayered CSS — meaning even my-0 wouldn't override .chat-markdown p. That means a fix would need my-0! (important) or, better, swapping the <p> for a <div> with role="status", matching the pattern used for the workspace-image placeholder. I'm also checking the error <p> in ChatMarkdown — same issue applies there. Now I'm drafting the review comment about index.css: mermaid injects an inline max-width style on its root SVG when useMaxWidth is enabled, and since inline styles beat stylesheet rules, this stylesheet's max-width: none declaration gets overridden. The cleanest fix is disabling useMaxWidth in mermaid's initialize config for the diagram types in use, so mermaid emits real width/height attributes instead — though marking the CSS declaration !important would also work as a smaller, self-contained patch. I'm weighing whether to give a diff suggestion for the !important approach versus just describing the config-based fix, since the config change spans files while the CSS tweak is a single line I could suggest directly. Now looking at Comment B, the placeholder and error-state markup in MermaidDiagram.tsx uses a <p> tag inside .chat-markdown, which picks up unlayered CSS margin rules that override Tailwind's utilities — the same issue that required block! elsewhere for markdown images. Since other chat-markdown placeholders already avoid this by using a div with role="status" instead of <p>, I'm drafting an inline suggestion that swaps the placeholder element to a div with aria-busy and role="status" attributes. I want to extend this fix to also cover the error branch, converting both the loading and error return blocks to use div elements with matching data attributes, while making sure the test assertions checking for the rendered text and aria-busy attribute still pass. I'm working out the correct line range so the suggestion block replaces both branches cleanly without breaking indentation. I also notice ChatMarkdown.tsx has the same paragraph-wrapping issue around line 875, so I'll add a separate, smaller comment there suggesting the diagram error be wrapped in a div instead of a p, since that diff hunk covers roughly lines 874-878. Then I want to flag a scrollbar consistency issue on the mermaid overflow container in MermaidDiagram.tsx around line 76, since other horizontal-overflow containers handle scrolling differently. Still writing scrollbar styles... With three call sites now sharing this scrollbar treatment, I'm weighing whether to extract it into a shared rule in index.css by extending the existing .chat-markdown pre selector to also cover the mermaid container, since that keeps ownership centralized even though those rules also carry border/padding/background concerns beyond just scrollbar styling. I'm deciding to keep the aesthetic-leaning consistency comment since it concerns concrete scrollbar geometry within the same surface, which the review criteria explicitly care about—so I'll phrase it as a suggestion rather than a hard requirement, and land on an overall failure verdict given the findings, then move to drafting the review after double-checking exact line contents and indentation from the file.

21. modify_pr — Reviewed the mermaid chat-diagram changes against the shared chat-markdown chrome. Three concrete issues on the new rendering surface: a dead max-width override that defeats the diagram's own scroll container, prose margins leaking into the code-block chrome from the new <p> placeholders, and an unstyled horizontal scrollbar next to the styled pre scrollbar. Everything else (primitive reuse for the toggle, clipboard contract, streaming gate) looks consistent.


Agent Credits: 183 credits

Token usage: 32 uncached input · 1,180,231 cache-read input · 103,987 cache-write input · 19,793 output
Agent Credits may also include non-token charges from external tools such as web research.