Skip to content

fix(web): tighten mermaid preview, copy, and chrome

MacroscopeApp / Macroscope - UI Consistency failed Aug 28, 2026 in 4m 39s

UI Consistency: 1 issue found

apps/web/src/markdown-clipboard.ts — the new mermaid clipboard-restore branches are unreachable from the only production caller. chatMarkdownClipboardPayload always serializes a freshly created, detached <div> holding range.cloneContents(), so the container is never inside an svg and never carries data-markdown-copy; likewise a cloned <svg> has no .chat-markdown-mermaid ancestor for the new closest("[data-markdown-copy]") fallback to reach (and when the wrapper is in the clone, the existing getAttribute check already short-circuits). Selecting only inside a rendered diagram still copies SVG label text rather than the mermaid fence, despite the new test that asserts otherwise by passing a live-attached fake element. Suggested fix posted inline: resolve the copy scope from the live range ancestor next to the existing closest("pre") bail-out.

Previously flagged issues verified as fixed: the <p> prose-margin leak in MermaidDiagram placeholder/error rows (now <div>), the missing chat-markdown scrollbar treatment on the diagram scroll container (.chat-markdown .chat-markdown-mermaid now shares the pre scrollbar rules), and the mermaid inline max-width override (now paired with useMaxWidth: false in the renderer config).

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 examined in scope: apps/web/src/components/ChatMarkdown.tsx, apps/web/src/components/chat/MermaidDiagram.tsx, apps/web/src/lib/mermaidRenderer.ts, apps/web/src/lib/mermaidLanguage.ts, apps/web/src/markdown-clipboard.ts, apps/web/src/proposedPlan.ts, apps/web/src/index.css.

Checks performed:

  • Shared primitives: the new source/diagram toggle reuses Button variant="ghost" size="icon-xs" with the shared chat-markdown-chrome-action class, Tooltip/TooltipTrigger render props, and aria-pressed/aria-label — consistent with the existing wrap and copy actions.
  • CSS ownership: the diagram scrollbar and svg rules target imperatively injected markup (dangerouslySetInnerHTML) and are correctly grouped with the existing .chat-markdown pre global rules; owner-local geometry (padding, overflow) stays as Tailwind on the component.
  • Theme handling: resolvedTheme is threaded from ChatMarkdown into MermaidDiagram and the renderer re-initializes on theme change; text colors use the same semantic tokens (--color-foreground bridges to --contrast-foreground) already used elsewhere in chat markdown.
  • Clipboard behavior: traced serializeRenderedMarkdownFragment and serializeNode to their single production caller chatMarkdownClipboardPayload (ChatMarkdown.tsx:1391) to confirm reachability of the new branches — this produced the reported finding.

🔍 Agent activity — 17 tool calls

💭 I'm comparing the current PR head against the previous reviewed commit to see what's changed since the last review round. It looks like the earlier feedback about using a div instead of a p tag in MermaidDiagram.tsx has already been addressed, and I'm now checking the accompanying CSS updates.

1. git_tools — 40e9e54..2820c4d

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

💭 With the prior comments addressed, I'm now checking the current state for new issues—specifically the CSS rule combining chat-markdown pre and mermaid diagram styling in index.css, and the MarkdownCodeBlock changes around hiding the wrap button when showing a diagram and how the copy button behaves.

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

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

💭 I'm checking for potential issues: the data-mermaid attribute looks fine, the toolbar button layout shift seems intentional, and now I'm looking at whether the theme prop mapping (light/dark) correctly handles the app's theming system. The bigger question is around styling consistency—whether the error message's use of text-destructive matches how other error surfaces in chat-markdown handle color, since that context seems to rely on contrast-adjusted custom properties rather than standard semantic tokens. I'm also noting the mermaid loading placeholder uses a different muted-foreground style than the codeblock header, which could be another inconsistency worth flagging. I'm also tracing through whether the SVG rendering path is safe: since mermaid is configured with htmlLabels: false and useMaxWidth: false, the diagram shouldn't produce foreignObject HTML that global chat-markdown CSS rules could accidentally style, and the inline max-width behavior should be suppressed by the config rather than relying solely on the !important override. The scrollbar CSS added to .chat-markdown-mermaid matches the earlier scrollbar comment, and I'm now weighing whether these global scrollbar/svg selectors are correctly placed, since they target generated DOM from dangerouslySetInnerHTML and shared scrollbar styling rather than plain component-owned presentation. I'm checking whether the mermaid wrapper needs an explicit max-width given it already has overflow-x-auto, and now I'm tracing where the mermaid theme prop actually comes from in ChatMarkdown, since mermaid's own "dark"/"neutral" themes won't automatically follow the app's custom theme tokens. I'm also verifying that the module-level render counter keeps mermaid diagram IDs unique across concurrent renders, and considering whether mermaid.render is safe to call concurrently for multiple diagrams at once. Checking accessibility of the inserted SVG—mermaid already adds aria-roledescription and title, so that seems fine. I'm also confirming the copy button copies the raw diagram source correctly. Now I'm looking at the markdown clipboard serializer change to isSkippedElement, checking whether it properly skips buttons and other UI chrome nested inside elements marked with data-markdown-copy, and whether other markdown content could be affected by this new closest-lookup logic. I'm tracing a potential double-emission bug: when a skipped SVG sits inside a data-markdown-copy wrapper, serializeNode returns the closest ancestor's full text, but I need to verify the wrapper div itself isn't also separately serialized, causing duplicate output. Let me open markdown-clipboard.ts to check.

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

6. git_tools — data-markdown-copy

💭 I'm checking whether a span with a data-markdown-copy attribute properly short-circuits descent into skipped child elements, confirming that the closest markdown-copy value is returned before descendants are visited — but I still need to check the case where a skipped element sits nested inside another container. I'm wondering if duplication could occur when a skipped element sits nested inside an ancestor marked with the copy attribute — but since serialization already short-circuits at the ancestor level, this seems safe unless the range starts partway through the tree. I should check how range.cloneContents() interacts with this logic further down.

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

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

💭 I realize cloneContents actually preserves the ancestor chain for partially selected nodes, so checking closest() on the cloned fragment should still find ancestors with the data-markdown-copy attribute, since the range copy is built in a detached container but keeps that structural nesting intact. I realize the real-world scenario is trickier: if the user selects text nodes or <g>/<text> elements deep inside the svg without the svg root being included in the clone, serialization falls back to plain text extraction and the mermaid source restoration never kicks in. The test that supposedly covers this passes the live svg element directly rather than simulating a realistic partial selection, so it masks this gap — which looks like a genuine correctness issue worth flagging. So that's a genuine gap in restoring mermaid markdown for that selection case. But then I realize the test itself may be misleading, since it constructs a detached svg whose parent is the diagram div directly, while in production serializeRenderedMarkdownFragment is always called on the plain container div wrapping the cloned range — meaning the new closest("svg") branch may never actually fire in real usage. I need to check whether cloneContents on a range spanning inside the svg actually includes the svg element itself in the resulting fragment, since that determines whether the added branch is reachable at all. So the real bug: selecting just inside the rendered diagram — the exact case the new tests claim to cover — still copies SVG label text instead of the mermaid source, because the fragment is a detached clone and closest() can never reach the wrapper, while the new closest("svg") branch also never fires since it's always called against a fresh div. I should check who actually calls serializeRenderedMarkdownFragment.

9. git_tools — serializeRenderedMarkdownFragment

10. git_tools — chatMarkdownClipboardPayload

💭 I'm confirming the only production caller passes a freshly created div containing the cloned range contents. I'm also noticing the line numbers differ from my earlier view, so I'm double-checking whether that's due to viewing a different revision than the grep result.

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

12. git_tools — closest

💭 I'm confirming that the clipboard path always wraps content in a detached plain div, which means the new svg-closest check in serializeRenderedMarkdownFragment can never match. The data-markdown-copy fallback in serializeNode only works when that mermaid wrapper div actually ends up inside the cloned fragment. Working through the selection cases: if the whole diagram or a partial span including the wrapper div gets cloned, the wrapper is present and serialization returns correctly. But if the selection lies entirely inside the svg itself, the cloned fragment only contains detached <g>/<text> nodes with no wrapper to find, so the copy ends up leaking raw label text instead of the mermaid fence. I'm noting this is the exact scenario their second test claims to cover, but their test actually passes a live-attached svg rather than a truly detached clone, so it gives false confidence. This feels like a legitimate, concrete behavioral gap worth flagging as an inline comment on markdown-clipboard.ts — it's in scope and backed by a real broken code path, even if it's a borderline "UI consistency" case. I also want to check the edge case where the range's common ancestor is a text node inside an svg <text> element, since closest("pre") would still return null there too. I'm working through other selection scenarios — dragging across just the diagram, or double-clicking a label word inside the svg — that would hit this same gap and produce raw label text instead of proper copy. I'll suggest fixing it by resolving the copy target from the live range using something like a closest("[data-markdown-copy]") check, mirroring the existing pre bail-out logic, rather than relying on the detached clone. Before writing this up, I want to check a couple other potential findings, starting with how the theme prop flows into MarkdownCodeBlock and whether custom themes beyond light/dark are properly mapped to mermaid's built-in options. I'm wondering whether --muted-foreground and --destructive are contrast-adjusted like --contrast-foreground is, since the rules require runtime-adjusted roles to stay consistent. If a user raises markdown contrast, the new placeholder/error text might not follow suit, so I want to check how the contrast token system actually works in index.css.

13. git_tools — contrast-foreground

14. git_tools — text-muted-foreground|text-destructive

💭 I'm confirming the foreground/muted-foreground token mapping is consistent with existing usage. Then I trace the error handling flow: when a diagram errors, showSource gets set to true which unmounts MermaidDiagram, so I need to check whether that conflicts with the error region rendering in MarkdownCodeBlock. There might be a brief double-render where both error states flash for a frame, but that's minor. I'm also checking icon consistency for the show-source toggle against other toggles in the codebase, the data-mermaid attribute value, whether wrap preferences persist correctly when switching views, and accessibility of the horizontally scrollable diagram container. The prior max-width concern is now resolved by disabling mermaid's built-in max-width in favor of an important override. I'm now verifying whether the mermaid SVG's max-width and sizing rules actually apply everywhere—specifically whether the .chat-markdown-mermaid styles only take effect when nested inside a .chat-markdown root, and whether MarkdownCodeBlock (and therefore MermaidDiagram) is ever rendered outside that root, like in a proposed-plan preview card, which would mean the scrollbar and sizing rules wouldn't apply there. I want to trace where the chat-markdown class gets applied to confirm coverage.

15. git_tools — "chat-markdown |chat-markdown\b[^-]

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

💭 I'm noting that renderDiagram only becomes true once streaming stops, and since the components memo depends on isStreaming, that identity change likely causes the markdown code block subtree to remount. I should also check how theme switching between light and dark affects this rendering behavior. Looking closer, the mermaid renderer re-initializes globally on a theme change, and I want to confirm there's no race condition when multiple diagrams render concurrently after a switch — mermaid's unique IDs should prevent collisions even though it briefly appends elements to document.body. I'm also checking the CSS diff that splits out scrollbar styling into a shared selector for both pre blocks and mermaid diagrams, which looks like a reasonable refactor rather than a bug. There's a minor padding mismatch between the codeblock's pre element (0.8rem/12.8px) and the diagram wrapper's Tailwind padding (12px/px-3), causing a roughly 2px horizontal shift when toggling between source and diagram views — this feels too marginal to flag as a real issue. The vertical padding also differs slightly (0.8rem top vs pt-1's 4px), but diagrams likely have their own visual spacing that compensates. I'm also checking whether the strict security level with DOMPurify sanitization for the injected HTML is properly configured, which looks fine, and now I'm considering whether the top-level htmlLabels config key is even valid in mermaid's schema, since I recall it typically lives nested under flowchart, class, and state configs rather than at the root level. Checking htmlLabels and class config keys against mermaid's schema, they look valid so I won't flag those. The real finding is the clipboard behavior — I want to verify Range.cloneContents() actually works the way I think before asserting it: when selection spans a single text node, the cloned fragment contains just that text node, so the container div would only wrap plain text. For multi-element selections spanning SVG groups, I'm now checking how tagName casing and the SKIPPED_TAGS check interact for lowercase SVG tag names like "g" and "text" versus the uppercase HTML tag matching, since this affects whether those elements get skipped during serialization. I'm concluding that since the closest() call happens on a detached clone rather than the live DOM, the new code paths added in this PR are essentially dead code — the production caller never actually exercises them, which explains why tests pass with attached fake elements but the real behavior wouldn't hit these branches. I'm double-checking though whether there's an edge case with nested markdown-copy ancestors like SkillInlineText spans that could still reach that line. I want to draft one comment that covers both spots in markdown-clipboard.ts, suggesting a fix that checks the live range ancestor for a data-markdown-copy scope before serializing, similar to the existing closest("pre") bail-out. I'm deciding whether to anchor it near line 205 or split it across both locations, since the guidance favors precise inline comments. I'll go with a range comment covering lines 273-280 for the serializeRenderedMarkdownFragment block, referencing line 205 separately, but I need to double check these lines fall within the diff hunk since GitHub's review API will error otherwise. I'm also verifying the copy-button code path in ChatMarkdown that calls chatMarkdownClipboardPayload on selection, and confirming there's no other issue with the mermaid diagram's scrollbar styling since it's properly scoped inside the chat-markdown container. The error handling looks fine, showing both the error message and the source code when diagrams fail. The useEffect resetting showSource and diagramError on code changes is a minor concern since it overrides a user's manual toggle during streaming updates, but that's an acceptable tradeoff. I'm now checking whether the toolbar buttons shift position when switching between diagram and source views, since the conditional rendering could cause layout jumps. Switching to source view adds a wrap button, which shifts the copy button rightward by one button width — a small but real interaction quirk since the copy button ends up under the cursor after clicking toggle. It's a minor UX wart rather than a functional bug, so I'll note it but not treat it as blocking. My core finding still rests on the assumption that Range.cloneContents doesn't include ancestors above the common ancestor, so I want to verify that claim carefully before deciding whether to flag it as a failure. Confirming that cloneContents only clones range contents with partially-contained nodes included, excluding the common ancestor itself — so if the selection spans beyond the mermaid div (making it only partially contained), the wrapper is still cloned and picked up correctly. It's only when the selection is entirely inside the diagram that the wrapper gets missed, which matches what the test names are describing. Now I want to draft a fix that checks for a closest element with a data-markdown-copy attribute, and if found, uses that stored markdown source and sanitized HTML for the clipboard payload instead of falling through to the default serialization. I'm putting together the review comment explaining that these new mermaid-restore branches are unreachable in production, since the real caller always builds a detached, freshly-created container from cloned range contents, so it never sits inside an svg or carries the data-markdown-copy attribute, and cloneContents drops the ancestor chain needed for closest() to find the markdown wrapper. I'm recommending resolving the scope from the live range ancestor instead, right alongside the existing pre bail-out, so selections inside the rendered diagram still work correctly. Now I'm identifying the exact line range in the diff to anchor this comment to, tracing through the added conditional block that checks the element's SVG ancestor and returns tidied markdown when a copy attribute is found.

17. modify_pr — One finding: the new mermaid clipboard-restore branches in markdown-clipboard.ts are unreachable from the only production caller, so selecting inside a rendered diagram still copies SVG label text. The earlier findings (prose-margin leak on the placeholder/error rows, missing .chat-markdown scrollbar treatment on the diagram scroll container, and the max-width override) all look addressed.


Agent Credits: 140 credits

Token usage: 20 uncached input · 627,129 cache-read input · 90,730 cache-write input · 17,960 output
Agent Credits may also include non-token charges from external tools such as web research.