feat(web): add LaTeX math formula rendering with KaTeX#436
feat(web): add LaTeX math formula rendering with KaTeX#436hqhq1025 merged 4 commits intotiann:mainfrom
Conversation
Add remark-math + rehype-katex to the markdown rendering pipeline so inline ($...$) and display ($$...$$) math formulas are rendered as proper KaTeX output in chat messages and tool results. Closes tiann#237
There was a problem hiding this comment.
Findings
- [Major] Single-dollar math is now enabled on every shared markdown surface, including arbitrary tool/result text rendered through
MarkdownRenderer, so common coding strings such asexport PATH=$HOME/bin:$PATHcan be reinterpreted as math instead of literal text. Evidenceweb/src/components/assistant-ui/markdown-text.tsx:17,web/src/components/MarkdownRenderer.tsx:21,web/src/components/ToolCard/views/_results.tsx:129,web/src/components/ToolCard/views/_results.tsx:137.
Suggested fix:export const MARKDOWN_PLUGINS = [ remarkGfm, [remarkMath, { singleDollarTextMath: false }], remarkDisableIndentedCode, ]
- [Minor] The shared plugin list now adds
remarkMath, but the reasoning renderer still only passesremarkPlugins, so formulas in reasoning blocks will not render the same way as the main chat/tool markdown. Evidenceweb/src/components/assistant-ui/markdown-text.tsx:17,web/src/components/assistant-ui/reasoning.tsx:41.
Suggested fix:<MarkdownTextPrimitive remarkPlugins={MARKDOWN_PLUGINS} rehypePlugins={MARKDOWN_REHYPE_PLUGINS} components={defaultComponents} className={cn('aui-reasoning-content min-w-0 max-w-full break-words text-sm text-[var(--app-hint)]')} />
Summary
Review mode: initial. Two regressions introduced by the shared KaTeX rollout: single-dollar parsing now applies to arbitrary tool/result text, and the reasoning renderer was not updated to use the matching rehype pipeline.
Testing
- Not run (automation; PR content treated as untrusted). Suggested coverage: markdown snapshots for
$HOME:$PATH,$5,$$x^2$$, and reasoning messages containing math.
HAPI Bot
| import { CopyIcon, CheckIcon } from '@/components/icons' | ||
|
|
||
| export const MARKDOWN_PLUGINS = [remarkGfm, remarkDisableIndentedCode] | ||
| export const MARKDOWN_PLUGINS = [remarkGfm, remarkMath, remarkDisableIndentedCode] |
There was a problem hiding this comment.
[MAJOR] This shared plugin list now affects MarkdownRenderer too, and that renderer is fed arbitrary tool/result text (web/src/components/ToolCard/views/_results.tsx:129-137). With default remark-math settings, common coding strings such as export PATH=$HOME/bin:$PATH or paired dollar amounts can be parsed as math instead of literal text. If you want math here, please at least disable single-dollar parsing.
Suggested fix:
export const MARKDOWN_PLUGINS = [
remarkGfm,
[remarkMath, { singleDollarTextMath: false }],
remarkDisableIndentedCode,
]|
|
||
| export const MARKDOWN_PLUGINS = [remarkGfm, remarkDisableIndentedCode] | ||
| export const MARKDOWN_PLUGINS = [remarkGfm, remarkMath, remarkDisableIndentedCode] | ||
| export const MARKDOWN_REHYPE_PLUGINS = [rehypeKatex] |
There was a problem hiding this comment.
[MINOR] The shared math pipeline is only wired into MarkdownText and MarkdownRenderer. web/src/components/assistant-ui/reasoning.tsx:41-44 still passes remarkPlugins={MARKDOWN_PLUGINS} without the matching rehypePlugins, so reasoning blocks will render math differently from the rest of the chat UI.
Suggested fix:
<MarkdownTextPrimitive
remarkPlugins={MARKDOWN_PLUGINS}
rehypePlugins={MARKDOWN_REHYPE_PLUGINS}
components={defaultComponents}
className={cn('aui-reasoning-content min-w-0 max-w-full break-words text-sm text-[var(--app-hint)]')}
/>- Set singleDollarTextMath: false to prevent $HOME, $PATH etc from being misinterpreted as math formulas. Only $$...$$ (display) is parsed; inline math requires explicit \(...\) or $$...$$. - Add rehypePlugins to the reasoning renderer so math formulas render consistently across chat, tool results, and reasoning blocks.
There was a problem hiding this comment.
Findings
- [Minor] Shared markdown plugin exports now use
any[]plus@typescript-eslint/no-explicit-anysuppressions, which drops type safety on a hot path reused by chat, reasoning, andMarkdownRenderer. That makes future plugin tuple mistakes invisible to the compiler. Evidenceweb/src/components/assistant-ui/markdown-text.tsx:17,web/src/components/assistant-ui/markdown-text.tsx:20.
Suggested fix:import { MarkdownTextPrimitive, unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, useIsMarkdownCodeBlock, type CodeHeaderProps, type MarkdownTextPrimitiveProps, } from '@assistant-ui/react-markdown' export const MARKDOWN_PLUGINS = [ remarkGfm, [remarkMath, { singleDollarTextMath: false }], remarkDisableIndentedCode, ] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']> export const MARKDOWN_REHYPE_PLUGINS = [ rehypeKatex, ] satisfies NonNullable<MarkdownTextPrimitiveProps['rehypePlugins']>
Summary
Review mode: follow-up after new commits. The follow-up resolves the earlier math-rendering regressions, but it now weakens the shared markdown pipeline by exporting the plugin lists as any[] with lint suppressions on web/src/components/assistant-ui/markdown-text.tsx:17 and web/src/components/assistant-ui/markdown-text.tsx:20.
Testing
- Not run (automation; PR content treated as untrusted). Suggested coverage: renderer snapshots for inline math, display math, and literal shell/currency strings across chat, reasoning, and
MarkdownRenderer.
HAPI Bot
|
|
||
| export const MARKDOWN_PLUGINS = [remarkGfm, remarkDisableIndentedCode] | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| export const MARKDOWN_PLUGINS: any[] = [remarkGfm, [remarkMath, { singleDollarTextMath: false }], remarkDisableIndentedCode] |
There was a problem hiding this comment.
[MINOR] These shared markdown plugin exports are now typed as any[] with @typescript-eslint/no-explicit-any suppressions. That sidesteps the repo's strict TypeScript checks on a pipeline reused by chat, reasoning, and MarkdownRenderer, so future plugin tuple mistakes will be invisible to the compiler.
Suggested fix:
import {
MarkdownTextPrimitive,
unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
useIsMarkdownCodeBlock,
type CodeHeaderProps,
type MarkdownTextPrimitiveProps,
} from '@assistant-ui/react-markdown'
export const MARKDOWN_PLUGINS = [
remarkGfm,
[remarkMath, { singleDollarTextMath: false }],
remarkDisableIndentedCode,
] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']>
export const MARKDOWN_REHYPE_PLUGINS = [
rehypeKatex,
] satisfies NonNullable<MarkdownTextPrimitiveProps['rehypePlugins']>Replace any[] with satisfies NonNullable<MarkdownTextPrimitiveProps[...]> to preserve type safety on the shared plugin lists without needing eslint suppressions.
There was a problem hiding this comment.
Findings
- [Major] Single-dollar inline math is still disabled, so the new markdown pipeline only renders formulas when users wrap inline expressions in two or more dollar signs. Most LLM markdown and existing KaTeX/remark-math examples use
$...$, so expressions like$E=mc^2$will keep showing up as raw text. Evidenceweb/src/components/assistant-ui/markdown-text.tsx:19.
Suggested fix:export const MARKDOWN_PLUGINS = [ remarkGfm, remarkMath, remarkDisableIndentedCode, ] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']>
Summary
Review mode: follow-up after new commits. The previous typing issue is fixed, but the current parser config in web/src/components/assistant-ui/markdown-text.tsx:19 still blocks the dominant inline math syntax ($...$), so the feature remains only partially implemented.
Testing
- Not run (automation). Suggested coverage: inline
$...$, display$$...$$, and literal currency/shell strings across chat, reasoning, andMarkdownRenderer.
HAPI Bot
| export const MARKDOWN_PLUGINS = [remarkGfm, remarkDisableIndentedCode] | ||
| import type { MarkdownTextPrimitiveProps } from '@assistant-ui/react-markdown' | ||
|
|
||
| export const MARKDOWN_PLUGINS = [remarkGfm, [remarkMath, { singleDollarTextMath: false }], remarkDisableIndentedCode] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']> |
There was a problem hiding this comment.
[MAJOR] remark-math is configured with singleDollarTextMath: false here, which means the shared markdown pipeline will not parse the dominant inline syntax $...$. With this setting, examples like $E=mc^2$ still render as plain text in chat, reasoning, and tool-result markdown.
Suggested fix:
export const MARKDOWN_PLUGINS = [
remarkGfm,
remarkMath,
remarkDisableIndentedCode,
] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']>Re-enable $...$ parsing (remark-math default) so inline formulas like $E=mc^2$ render correctly. Shell variables like $HOME typically appear inside code spans/blocks which remark-math does not parse, so false positives are minimal in practice.
There was a problem hiding this comment.
Findings
- [Major] KaTeX math will inherit the repo's existing
code/prechrome, so formulas render as inline-code pills or code blocks instead of plain math. Evidenceweb/src/components/assistant-ui/markdown-text.tsx:20,web/src/components/assistant-ui/markdown-text.tsx:43,web/src/components/assistant-ui/markdown-text.tsx:59.
Summary
Review mode: follow-up after new commits. The previous single-dollar parser issue is fixed, but the new rehype-katex hookup still collides with the custom code and pre renderers, so math output is styled like code in chat, tool, and reasoning markdown.
Testing
- Not run (automation;
bunnot installed in this runner). Suggested coverage: inline$...$and display$$...$$render paths inMarkdownText,MarkdownRenderer, andReasoning, with assertions that math output is not wrapped in the inline-code or code-block UI.
HAPI Bot
| import type { MarkdownTextPrimitiveProps } from '@assistant-ui/react-markdown' | ||
|
|
||
| export const MARKDOWN_PLUGINS = [remarkGfm, remarkMath, remarkDisableIndentedCode] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']> | ||
| export const MARKDOWN_REHYPE_PLUGINS = [rehypeKatex] satisfies NonNullable<MarkdownTextPrimitiveProps['rehypePlugins']> |
There was a problem hiding this comment.
[MAJOR] rehype-katex renders math into code.math-inline and pre > code.math-display, but this file still routes every code/pre node through the repo's inline-code and code-block components. That means inline formulas pick up the gray code pill and display formulas pick up the code-block wrapper instead of plain KaTeX. Evidence: web/src/components/assistant-ui/markdown-text.tsx:20, web/src/components/assistant-ui/markdown-text.tsx:43, web/src/components/assistant-ui/markdown-text.tsx:59.
Suggested fix:
function Code(props: ComponentPropsWithoutRef<'code'>) {
if (props.className?.includes('math-inline') || props.className?.includes('math-display')) {
return <code {...props} />
}
const isCodeBlock = useIsMarkdownCodeBlock()
// existing code styling...
}
function Pre(props: ComponentPropsWithoutRef<'pre'>) {
const child = React.Children.only(props.children)
if (React.isValidElement(child) && typeof child.props.className === 'string' && child.props.className.includes('math-display')) {
return <div className="max-w-full overflow-x-auto">{props.children}</div>
}
// existing code-block wrapper...
}* feat(web): add LaTeX math formula rendering with KaTeX Add remark-math + rehype-katex to the markdown rendering pipeline so inline ($...$) and display ($$...$$) math formulas are rendered as proper KaTeX output in chat messages and tool results. Closes tiann#237 * fix(web): disable single-dollar math parsing and add KaTeX to reasoning - Set singleDollarTextMath: false to prevent $HOME, $PATH etc from being misinterpreted as math formulas. Only $$...$$ (display) is parsed; inline math requires explicit \(...\) or $$...$$. - Add rehypePlugins to the reasoning renderer so math formulas render consistently across chat, tool results, and reasoning blocks. * refactor(web): use satisfies for type-safe plugin exports Replace any[] with satisfies NonNullable<MarkdownTextPrimitiveProps[...]> to preserve type safety on the shared plugin lists without needing eslint suppressions. * fix(web): enable single-dollar inline math syntax Re-enable $...$ parsing (remark-math default) so inline formulas like $E=mc^2$ render correctly. Shell variables like $HOME typically appear inside code spans/blocks which remark-math does not parse, so false positives are minimal in practice.
Summary
Add KaTeX-based LaTeX formula rendering to the web chat UI. Both inline (
$E=mc^2$) and display ($$\int_0^1 x^2 dx$$) math syntax are now rendered as formatted formulas.Changes
remark-math,rehype-katex, andkatexdependenciesremarkMathto the remark plugin pipeline andrehypeKatexto the rehype plugin pipelineindex.cssMarkdownText(chat messages) andMarkdownRenderer(tool results) share the same plugin configHow it works
remark-mathparses$...$(inline) and$$...$$(display) syntax into math AST nodesrehype-katexrenders those nodes into KaTeX HTMLThis is the same standard approach used by Claude.ai and ChatGPT.
$E=mc^2$shown as raw text$$\int_0^1 x^2 dx$$shown as raw textCloses #237