Skip to content

feat(apollo-wind): render math fences as display math and keep open math blocks as code - #1400

Draft
BenGSchulz wants to merge 1 commit into
mainfrom
feat/apollo-wind-math-fences
Draft

BenGSchulz wants to merge 1 commit into
mainfrom
feat/apollo-wind-math-fences

Conversation

@BenGSchulz

Copy link
Copy Markdown
Contributor

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 math through KaTeX. This brings the apollo-wind Markdown to parity, and makes partial math safe to render while a reply streams.

Premise correction

The ticket assumed a ```math fence fell through to CodeBlock. It did not: rehype-katex 7 already renders any pre > code.language-math as display math, so a lowercase ```math fence already worked. The real gaps were:

  • ```latex, ```tex and other casings (```MATH) rendered as plain code.
  • An unclosed ```math fence or $$ block rendered as a red KaTeX parse error until it closed, and a line opening with $$ (for example $$b = B before its closing $$) parsed as an empty display formula, so its text vanished.
  • The Wind chat's stream buffer (incomplete-tokens.ts) only holds back [[resource-token:, so these partial blocks reach Markdown as they arrive.

What changed

packages/apollo-wind/src/components/ui/markdown.tsx gains remarkMathBlocks, a small remark plugin that runs after remark-math and before host plugins:

  • A closed fence tagged math, latex or tex (any case, meta ignored) becomes the same display math node remark-math makes for $$ blocks, so the existing rehype-katex step renders it with the current options (output: 'mathml', trust: false, strict: 'ignore'). No direct katex import and no second render path.
  • A math fence or $$ block without its closing line 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 (the list item content column), so fence-like lines inside the block stay content.
  • The Wind chat's citationPlugin and resourceTokenPlugin only visit text nodes, so they never touch math or code nodes.

Streaming behaviour

The plugin looks at the source, not the streaming prop: 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

  • 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. A host overriding pre or code should read both (documented in the Markdown header). renderCodeActions receives it resolved: MarkdownCodeBlock.language is the first word of the info string, or math for an open $$ block.
  • With no lang, a host plugin that replaces hProperties loses the label but can never bring back the language-math class that would make KaTeX render half a formula.

Stories

  • New MathFences story with a math and a latex fence. The existing Math story is unchanged.
  • The Streaming story's source gains a short $$ block so the open-block behaviour is visible while it plays.

Tests

markdown.test.tsx:

  • Math fence renders <math display="block"> and no data-slot="code-block"; latex, tex, MATH with meta, ~~~, blockquote and list fences render as math; a js fence is unaffected; $$ math still works; the axe test includes a math fence.
  • Open fence and open $$ block stay code (labelled math, no .katex or .katex-error) and render as math once closed; $$b = B keeps its text while open.
  • Closer rules: a ``` 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.
  • An unclosed $$ block with streaming={false} renders as code.

Checks: Biome on the touched files, tsc --noEmit, the full apollo-wind suite and pnpm --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

…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>
Copilot AI balanced review requested due to automatic review settings October 17, 2026 01:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

Dependency License Review

  • ✅ 1947 package(s) scanned
  • ✅ No license issues found
  • ⚠️ 2 package(s) excluded (see details below)
License distribution
License Packages
MIT 1713
ISC 91
Apache-2.0 56
BSD-3-Clause 28
BSD-2-Clause 23
BlueOak-1.0.0 8
MPL-2.0 4
MIT-0 3
CC0-1.0 3
MIT OR Apache-2.0 2
(MIT OR Apache-2.0) 2
Unlicense 2
LGPL-3.0-or-later 1
Python-2.0 1
CC-BY-4.0 1
(MPL-2.0 OR Apache-2.0) 1
Unknown 1
Artistic-2.0 1
(WTFPL OR MIT) 1
(BSD-2-Clause OR MIT OR Apache-2.0) 1
CC-BY-3.0 1
0BSD 1
(MIT OR CC0-1.0) 1
MIT AND ISC 1
Excluded packages
Package Version License Reason
@img/sharp-libvips-linux-x64 1.3.4 LGPL-3.0-or-later LGPL pre-built binary, not linked
khroma 2.1.0 Unknown MIT per GitHub repo, missing license field in package.json

@github-actions

Copy link
Copy Markdown
Contributor

Storybook visual diff

⚠️ Visual changes detected: 2 changed, 2 added (of 32 compared, 28 unchanged). View report

Baseline is the deployed main Storybook, so changes merged to main after this branch was last updated can also appear here. Logs

Updated (PT): Oct 16, 2026, 06:12:43 PM

@github-actions

Copy link
Copy Markdown
Contributor

📊 Coverage + size by package

Per-package coverage and bundle size on this PR. New-line coverage = of the source lines this PR adds or changes, the % hit by tests.

Package Coverage New-line coverage Packed (gzip) Unpacked vs main
@uipath/apollo-core — — 42.02 MB 50.17 MB ±0
@uipath/apollo-react 51.3% — 7.99 MB 31.45 MB ±0
@uipath/apollo-ui-icons — — 2.86 MB 6.97 MB ±0
@uipath/apollo-vertex — — — — —
@uipath/apollo-wind 79.1% 100.0% (36/36) 787.0 KB 4.56 MB +1.9 KB
@uipath/ap-chat 85.8% — 40.37 MB 45.58 MB ±0

"Coverage" is each package's own coverage.include scope (e.g. apollo-core instruments only scripts/). "Packed"/"Unpacked" come from npm pack --dry-run and only cover built packages — "—" means not measured this run (package not affected / not built). "vs main" is the packed (gzipped) delta against the last successful main build (the package-sizes artifact from the Release workflow); "—" there means no main baseline was available this run. The baseline is main's latest build, not this PR's exact merge-base, so it includes any drift since the branch diverged. Packages with no vitest config are omitted.

This branch was successfully deployed

4 active deployments
preview/apollo-landing — 36cf1677 Deployed Oct 17, 2026 by github-actions[bot]
preview/apollo-vertex — 36cf1677 Deployed Oct 17, 2026 by github-actions[bot]
preview/apollo-docs — 36cf1677 Deployed Oct 17, 2026 by github-actions[bot]
preview/apollo-design — 36cf1677 Deployed Oct 17, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg:apollo-wind size:L 100-499 changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants