Skip to content

Recover math after unmatched delimiters

MacroscopeApp / Macroscope - UI Consistency failed Aug 27, 2026 in 3m 45s

UI Consistency: 1 issue found

apps/web/src/index.css — The new KaTeX scroll-lane rules (scrollbar-color at line 2132, ::-webkit-scrollbar-thumb background at line 2152) read raw var(--border), while all other .chat-markdown chrome — including the pre scrollbar treatment they mirror — reads the runtime contrast-adjusted var(--contrast-border) (which is also what Tailwind's border-* resolves to via --color-border). With the appearance contrast boost enabled, math scroll lanes stay unadjusted next to adjusted code-block lanes. Inline comment posted with the suggested fix.

No other findings: display-math left alignment now overrides KaTeX's centering, the error color uses var(--destructive) with the native title stripped (consistent with the sanitize schema), inline math has an overflow owner with the shared thin-scrollbar geometry, and the clipboard serializer round-trips math to explicit LaTeX delimiters before the HTML branch is prepared on a cloned fragment.

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 at head 118927f7:

  • apps/web/src/index.css — new .katex-display / inline .katex rules, margin-rhythm group, global katex/dist/katex.min.css import. Global CSS ownership is appropriate here (generated markdown DOM). Specificity checked against KaTeX's own .katex-display > .katex { text-align: center } (0,3,0 vs 0,2,0, so the left-alignment override wins); both stylesheets are unlayered, so cascade order is not a factor for these selectors.
  • apps/web/src/components/ChatMarkdown.tsx — rehypeKatex now runs on both the raw-HTML and non-raw-HTML pipelines, so the two rendering paths no longer disagree; sanitize runs before KaTeX and the code className allowlist admits math-inline/math-display; the "*" filter that drops title is still respected because rehypeStripKatexErrorTitle removes KaTeX's post-sanitize tooltip.
  • apps/web/src/markdown-math.ts / .test.ts — delimiter normalization is length-preserving (task-list offsets intact) and skips code, HTML attributes, link destinations, and autolinks.
  • apps/web/src/markdown-clipboard.ts / .test.ts — text serialization runs before prepareKatexHtmlForClipboard mutates the cloned fragment, so removing the MathML branch does not affect the live message DOM or the markdown round-trip.

Role indirection was verified against the :root, [data-app-sidebar] contrast block (line ~1905) and --color-border: var(--contrast-border) (line 170).


🔍 Agent activity — 11 tool calls

💭 I'm checking the current PR diff against prior review findings, and it looks like the fixes are in place — text alignment, error color variable, KaTeX error title stripping, the thin scrollbar for inline math overflow, and clipboard serialization all appear addressed. Now I'm scanning the CSS changes and ChatMarkdown component for any new issues to flag. I want to check what the .chat-markdown-table-container rule contains now that .katex-display was added to it, and I'm also thinking through the KaTeX CSS import in index.css — since Tailwind v4 wraps everything in cascade layers, an unlayered global import could end up overriding Tailwind's own utility styles depending on how our other rules are layered. I should pull up the actual files to verify this isn't a real conflict.

1. git_tools — apps/web/src/index.css f6f2be3..118927f

2. git_tools — katex

💭 I'm checking the CSS selector logic for distinguishing inline versus display KaTeX math, confirming that rehype-katex wraps display math in a katex-display span, and thinking through how the margin rules for display equations should be applied. Since both katex.min.css and our custom .chat-markdown rules sit unlayered outside Tailwind's cascade layers, specificity alone determines which margin rule wins for .katex-display, and our more specific selector takes priority. I'm also verifying that katex's internal classes like .base, .strut, and .mord are scoped tightly enough to avoid clashing with any existing app class names. I'm now revisiting the earlier fix where rehypeKatex was only applied on the parseRawHtml branch — that's been corrected so both branches include it, though I need to check whether the plugin list constant for the non-raw-HTML branch was updated consistently.

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

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

💭 I notice the raw HTML branch is where sanitization applies since that's where the risk is introduced, and I'm checking how the sanitize schema extends the default code className pattern to include the math code class names. I trace through the pipeline order — raw parsing, normalization, sanitize, then katex — confirming katex output isn't stripped since it runs after sanitization, while checking that the math-display and math-inline classNames set by the bracket-promotion logic survive the sanitize allowlist. I then consider a UI consistency issue: setting inline katex spans to overflow-auto with inline-block display for scrollability could clip tall elements like square roots or accents, since there's a known KaTeX quirk where overflow hidden on the container cuts off content that extends beyond the computed strut height. I also weigh whether a horizontal scrollbar on inline math could visually overlap surrounding text in constrained line-heights, but that seems minor. Checking the katex-display margin and text-align overrides, I confirm the .chat-markdown .katex-display > .katex selector has higher specificity than KaTeX's default centering rule, so that override correctly wins and resolves the earlier concern. Now I'm comparing specificity between our .chat-markdown :not(.katex-display) > .katex { display: inline-block } rule and KaTeX's own display-math rule — since ours only targets non-display math, it doesn't conflict. I should also verify whether every markdown renderer in the app actually gets wrapped in the .chat-markdown class, since these styles only apply within that scope. I notice recreating the remark plugin array on every text change means react-markdown reruns its full pipeline during streaming, since normalizeLatexMathDelimiters does a full remark parse per token — a real perf concern but outside the stated scope of the UI consistency check, so I'll set it aside and stay focused on that scope. Now I'm checking whether there's already a shared scrollbar contract in the CSS, since the new rules duplicate the same thin-scrollbar declaration across pre, katex-display, and inline katex selectors — which looks like exactly the kind of duplication the check flags for overflow/scroll treatments that should use a shared class instead of repeating themselves per selector.

5. git_tools — scrollbar

💭 I notice the existing scrollbar styling uses a contrast-adjusted border variable, while the new katex rules reference the plain border variable instead — that inconsistency is exactly what the audit is flagging, since it risks splitting markdown chrome into adjusted and unadjusted colors. Let me check where --contrast-border is actually defined to confirm this divergence.

6. git_tools — --contrast-border

💭 This confirms a real inconsistency: all the .chat-markdown chrome uses the runtime contrast-adjusted --contrast-border variable, but the new katex scrollbar rules reference the raw var(--border) instead, so they'll fail to respond to the "increase contrast" setting the way headings, menus, and previews do. I'm also checking the error color variable to see if it has the same unadjusted-literal problem.

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

💭 I've confirmed this finding: the new markdown chrome reads raw var(--border) instead of --contrast-border, breaking the contrast-adjusted role, so I need to swap in var(--contrast-border) across the scrollbar-color and thumb background rules. Checking further, KaTeX's use of currentColor looks fine, and I'm now scanning for another potential issue around an import statement. Since KaTeX's CSS imports are all scoped to .katex-prefixed selectors, that unlayered import doesn't clash with Tailwind's base layer in any meaningful way. I'm now looking closer at a selector combining :not(.katex-display) with .katex to see if it accidentally matches nested cases it shouldn't. So for display math, the <pre><code> block gets replaced entirely by KaTeX's rendered span structure, while inline math just swaps the code element for a katex span directly. The bracket-promotion case is trickier though: when remarkPromoteBracketDisplayMath forces an inline math node into display mode via hProperties, it produces a <code> element with math-display class but sitting inside a <p>, not a <pre>. That means the katex plugin's parent-detection logic treats the paragraph itself as the scope's parent, so the resulting .katex-display span ends up nested inside a <p> tag rather than replacing a full block element. I'm confirming that being a valid HTML pattern -- a block-level span inside a paragraph still parses fine, splitting the paragraph's text visually around the block math, which is the intended rendering. I also check that this nested case doesn't accidentally trigger the inline-math selector rule, since .katex-display > .katex still excludes it correctly. Then I start reasoning about margin collapsing between the paragraph's own margin and the nested block span's margin, since inline-context block elements can behave unpredictably with margin collapse.

8. git_tools — "chat-markdown

💭 Since .chat-markdown is always present and KaTeX inherits currentColor fine, the new CSS rules placed above .chat-markdown h1 make sense. This is generated markdown content, so it fits the criteria for genuinely reusable, behaviorally complex global CSS rather than one-owner presentation logic. Looking at the inline math styling more closely, I realize overflow-x: auto combined with overflow-y: hidden on an inline-block could cause a real issue: when a horizontal scrollbar appears, it takes up space from the content box, shrinking the vertical content area by roughly 7px. Since overflow-y is hidden, that clipped bottom portion (like descenders in subscripts) can't be scrolled into view, meaning long inline expressions with subscripts could have their bottoms visually cut off. But I decide this is too speculative without actually rendering it, and since I've already recommended the scrollbar approach in an earlier review pass, flagging it now would be somewhat self-contradictory, so I'll let it go. Turning to --border usage, I check whether .chat-markdown ever uses raw var(--border) versus Tailwind's border-border/70 class, which resolves through --color-border to --contrast-border. That confirms Tailwind's border token is the contrast-adjusted variable, so any raw var(--border) in the new CSS is indeed the unadjusted one — a solid finding worth keeping. I also verify the .katex-display margin rule doesn't need a chat-markdown rhythm variable instead, and that seems fine as is. Checking markdown-math.ts, it's pure logic — the rehypeStripKatexErrorTitle function removing the title attribute aligns with the sanitize schema's own title removal, so that's consistent. Then I start looking at whether errorColor: "var(--destructive)" might also affect KaTeX's color fallback behavior for \color commands. Checking whether --destructive has a contrast-adjusted variant, I find it doesn't — --color-destructive just maps directly to --destructive, so that's fine. I then trace through the WITHOUT_RAW_HTML branch, noting that rehypeKatex now runs on user messages without sanitize, but since there's no raw HTML in that path, the sanitize schema's className allowances aren't relevant there anyway. I also confirm that with parseRawHtml true, sanitize runs before katex and the promoted display math className passes through correctly in both branches. I decide one finding is probably sufficient, but I want to rule out a second real issue — checking whether the :not(.katex-display) > .katex selector might unintentionally apply inside .katex-error, which it doesn't. Then I turn to the clipboard handling, wondering whether prepareKatexHtmlForClipboard mutates the live DOM directly from the selection range's container. I need to trace where sanitizedHtmlFrom gets its container from — if it operates on a clone, there's no issue, but if it's working on the live DOM, stripping .katex-mathml for clipboard purposes could permanently damage the accessibility of the actual rendered message.

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

💭 Confirming it's a clone, so text serialization happens before the sanitization step mutates it, and the ordering is correct. I'm now checking a subtlety around how serializeKatex handles the annotation element inside the MathML branch, and whether isSkippedElement's aria-hidden check conflicts with serializeNode's katex class detection. My real finding is the var(--border) vs var(--contrast-border) inconsistency, which does violate the stated rule about not splitting markdown chrome into adjusted and unadjusted colors — minor but genuine, so I'll flag it as a failure with an inline comment. I need to map this to the correct diff line numbers, targeting the scrollbar-color and thumb background declarations around lines 2126-2151, and I'm working out how to structure a multi-line comment covering both occurrences.

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

💭 I'll go with a single inline comment on line 2132 with a suggested fix, and just mention that line 2152 needs the same change rather than duplicating the comment.

11. modify_pr — One finding: the new KaTeX scroll lanes read the unadjusted --border role instead of the contrast-adjusted --contrast-border used by the rest of .chat-markdown. Everything else from the earlier rounds (display-math left alignment, themed error color without a native title, inline overflow ownership with the shared thin-scrollbar treatment, KaTeX round-trip in the clipboard serializer) looks consistent now.


Agent Credits: 102 credits