…ath blocks as code
The Material chat renders a fenced block tagged math through KaTeX, and models
often write formulas that way. rehype-katex already rendered a lowercase
```math fence, so the gaps were elsewhere: ```latex, ```tex and other casings
(```MATH) fell through to CodeBlock as plain text, and an unclosed ```math
fence or $$ block rendered as a red KaTeX parse error (or a line opening with
$$ vanished into an empty formula) until it closed. The Wind chat's stream
buffer only holds back resource tokens, so these partial blocks reach Markdown
as they arrive.
A small remark plugin now turns a closed math, latex or tex fence (any case,
meta ignored) into the same display math node remark-math makes for $$
blocks, so the existing rehype-katex step renders it with the current
options. A math fence or $$ block without its closing line, still streaming
or cut off, renders as a code block labelled math until the closing line
arrives. A closer must use the opener's character, be at least as long, sit
in the same blockquotes and be indented at most 3 columns past the opener's
container, so fence-like lines inside the block stay content.
Host-visible: an open math block is a code node with no lang, so its <code>
has no language-* class and carries the label in data-language instead
(MarkdownCodeBlock.language still reports it, as math for an open $$ block).
Hosts overriding pre or code should read both, as the Markdown docs now say.
A finished message cut off mid-formula now shows code where it showed a KaTeX
error or partial math before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
MST-16784, part of the Wind chat epic MST-16172. First PR of a small apollo-wind stack (math fences, then chat primitive gaps, then compact density).
The Material chat renders a fenced block tagged
maththrough KaTeX. This brings the apollo-windMarkdownto parity, and makes partial math safe to render while a reply streams.Premise correction
The ticket assumed a
```mathfence fell through toCodeBlock. It did not:rehype-katex7 already renders anypre > code.language-mathas display math, so a lowercase```mathfence already worked. The real gaps were:```latex,```texand other casings (```MATH) rendered as plain code.```mathfence or$$block rendered as a red KaTeX parse error until it closed, and a line opening with$$(for example$$b = Bbefore its closing$$) parsed as an empty display formula, so its text vanished.incomplete-tokens.ts) only holds back[[resource-token:, so these partial blocks reachMarkdownas they arrive.What changed
packages/apollo-wind/src/components/ui/markdown.tsxgainsremarkMathBlocks, a small remark plugin that runs after remark-math and before host plugins:math,latexortex(any case, meta ignored) becomes the same displaymathnode remark-math makes for$$blocks, so the existingrehype-katexstep renders it with the current options (output: 'mathml',trust: false,strict: 'ignore'). No directkateximport and no second render path.$$block without its closing line renders as a code block labelledmathuntil the closing line arrives.citationPluginandresourceTokenPluginonly visittextnodes, so they never touch math or code nodes.Streaming behaviour
The plugin looks at the source, not the
streamingprop: a math block without its closing line, still streaming or cut off, renders as code. A finished message cut off mid-formula therefore shows a code block where main showed a KaTeX error or partial math. The streaming caret sits after a trailing open block, as for any code block, and after the formula once it closes.Host-visible notes
lang, so its<code>has nolanguage-*class and carries the label indata-language. A host overridingpreorcodeshould read both (documented in theMarkdownheader).renderCodeActionsreceives it resolved:MarkdownCodeBlock.languageis the first word of the info string, ormathfor an open$$block.lang, a host plugin that replaceshPropertiesloses the label but can never bring back thelanguage-mathclass that would make KaTeX render half a formula.Stories
MathFencesstory with amathand alatexfence. The existingMathstory is unchanged.Streamingstory's source gains a short$$block so the open-block behaviour is visible while it plays.Tests
markdown.test.tsx:<math display="block">and nodata-slot="code-block";latex,tex,MATHwith meta,~~~, blockquote and list fences render as math; ajsfence is unaffected;$$math still works; the axe test includes a math fence.$$block stay code (labelledmath, no.katexor.katex-error) and render as math once closed;$$b = Bkeeps its text while open.```line inside a```` fence, a backtick run in a~~~fence, a `$$` line in a `$$$` block, `$$` followed by text, a 4-space indented fence and a quoted fence inside a top-level fence all stay code. CRLF, a list nested 4 columns deep and a closer indented within 3 columns close.$$block withstreaming={false}renders as code.Checks: Biome on the touched files,
tsc --noEmit, the full apollo-wind suite andpnpm --filter @uipath/apollo-wind build.Follow-up
A
$$block whose closer sits at the end of a content line (for example\end{aligned}$$) is never closed by remark-math. Main showed a KaTeX error followed by the rest of the message; now the rest of the message shows inside one code block. Material uses the same remark-math, so this is parity, but it may be worth a follow-up.🤖 Generated with Claude Code