feat(web): render LaTeX math in chat via KaTeX - #1035
Conversation
🦋 Changeset detectedLatest commit: df0bba4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7fce349091
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // runs once on first import of this module and is safe at module scope. Without | ||
| // the CSS the math renders unstyled, so both must travel together. | ||
| import 'katex/dist/katex.min.css'; | ||
| enableKatex(); |
There was a problem hiding this comment.
Guard literal dollar text before enabling inline math
When KaTeX is enabled globally here, ordinary assistant text that contains two literal dollar-prefixed tokens, such as Check $PATH before $HOME or costs $5 and $10, is now parsed as a single $...$ inline formula between the first two dollars and rendered as math instead of readable prose; before this patch those dollars stayed literal. Please add a guard/escape for common literal-dollar patterns or otherwise avoid enabling single-dollar math globally without that protection.
Useful? React with 👍 / 👎.
Resolve the modify/rename conflict: the KaTeX math-rendering change moved from src/components/Markdown.vue to its new home at src/components/chat/Markdown.vue after main's component-subdirectory refactor, keeping the feature intact on the new path.
Enabling KaTeX turned plain prose with two dollar-prefixed tokens
(`Check $PATH before $HOME`, `costs $5 and $10`) into a single
inline formula, since markstream's $…$ tokenizer has no
"no whitespace inside the delimiters" rule.
Add a postTransformTokens guard that turns a single-$ inline span back
into literal text when its content starts or ends with whitespace. Real
inline math is written tight (`$E=mc^2$`, `$\frac{1}{2}$`), while
the prose false-positives always have whitespace inside the delimiters,
so this keeps inline/block math working while leaving prices, env vars,
and ranges as readable text. Code spans are already excluded by the
tokenizer, and running on the flat token stream also covers dollars
nested inside lists and blockquotes.
Addresses the Codex review comment on PR #1035.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bdb5c351c9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if ( | ||
| child.type === 'math_inline' && | ||
| child.markup === '$' && | ||
| /^\s|\s$/.test(child.content ?? '') |
There was a problem hiding this comment.
Handle compact currency ranges before rendering math
When an assistant message contains compact prose amounts such as costs $5-$10 or $5/$10, the two dollar signs are still parsed as one math_inline span whose content has no leading/trailing whitespace, so this predicate lets it through. With KaTeX now enabled, the first amount is rendered as math and the second dollar delimiter disappears, corrupting common price/range text; the dollar guard needs to reject these non-LaTeX currency/variable patterns as well as the spaced examples.
Useful? React with 👍 / 👎.
Resolve an import conflict in chat/Markdown.vue: keep main's new copyTextToClipboard import (clipboard-over-HTTP fix) alongside the KaTeX literal-dollar guard import.
The literal-dollar guard only caught prose whose content had whitespace inside the delimiters, so a compact range like `costs $5/$10` still rendered `5/` as a formula and dropped the second dollar. (markstream's own currency check rejects `-`/`~` ranges but not `/`.) Extend the guard to also reject a single-$ span whose content is a numeric amount with a trailing range connector (`/`, `-`, `~`, en/em dash) -- a complete formula never ends in a dangling operator. Scoped to digit-led content so symbolic math is left alone, and numeric math that is not a range (`$5/2$`, `$5-2$`, `$0.5$`) still renders. Added tests for the range cases and the non-range math. Addresses the follow-up Codex review comment on PR #1035.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 08deac2884
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (/^\s|\s$/.test(content)) return true; | ||
| if (TRAILING_RANGE_CONNECTOR.test(content.trim())) return true; |
There was a problem hiding this comment.
Treat shell-variable pairs as literal dollars
When an assistant message mentions adjacent shell variables or a PATH-like value outside backticks, e.g. Use $HOME/bin:$PATH or $PATH:$HOME, markstream pairs the two dollar signs and produces a math_inline child with content like HOME/bin:/PATH:. Because this guard only rejects leading/trailing whitespace or numeric range connectors, those spans still go through KaTeX, so the web chat drops the literal dollars and typesets a shell path as math. Please extend the prose-dollar guard for shell-variable/path separators before enabling KaTeX globally.
Useful? React with 👍 / 👎.
Adjacent shell variables and PATH-like values (`Use \$HOME/bin:\$PATH`,
`\$PATH:\$HOME`) were still rendered as math, because the prose-dollar
guard only looked at the span's own content (whitespace inside the
delimiters, or a trailing numeric range connector) and never at what
touches the delimiters from the outside.
Replace the two bespoke heuristics with the two industry-standard rules,
now driven by the surrounding text tokens:
- Pandoc (tex_math_dollars): no whitespace immediately inside the
delimiters.
- GitHub: each \$ must be bounded on its outer side by whitespace, a
line boundary, or structural punctuation. A letter or digit there
means a second prose token, so the span is literal text.
The GitHub outer-boundary rule subsumes the old numeric-range check (a
closing \$ in \$5/\$10 is followed by a digit) and also catches
shell/path cases Pandoc's inner rule misses. Normal math -- including
bare \$x\$, \$x^2\$., and (\$x^2\$) -- still renders. Added
tests for shell/path values and punctuation-wrapped math.
Addresses the third Codex review comment on PR #1035.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 50c92564a3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const LEFT_BOUNDARY = /[\s([{]/; | ||
| const RIGHT_BOUNDARY = /[\s.,;:!?)\]}]/; |
There was a problem hiding this comment.
Allow localized punctuation around math spans
When a valid formula is followed by full-width/CJK punctuation or wrapped in quotes, this boundary guard classifies it as prose and rewrites the math_inline token back to text because only ASCII punctuation is accepted here. For example, Chinese chat output like 公式为 $E=mc^2$,其中... or a quoted formula like “$x$” will show raw dollars instead of KaTeX, which makes the new math rendering fail in common localized prose contexts.
Useful? React with 👍 / 👎.
The outer-boundary guard only accepted ASCII punctuation, so a formula followed by full-width punctuation or wrapped in typographic quotes was misclassified as prose: `公式为 \$E=mc^2\$,其中` and `“\$x\$”` showed raw dollars instead of rendering. Invert the boundary check from an allow-list of ASCII punctuation to a deny-list of ASCII letters/digits. A \$ glued to an ASCII letter/digit still means a second prose token (\$PATH:\$HOME, \$5/\$10), but whitespace, line boundaries, and every other character -- full-width punctuation, CJK ideographs, curly quotes -- is now a valid math boundary, which is the correct behavior for localized prose. Addresses the fourth Codex review comment on PR #1035.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 06201d3da5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (isProseDollarSpan(child.content ?? '', prevChar, nextChar)) { | ||
| child.type = 'text'; | ||
| child.markup = ''; | ||
| child.content = `$${child.content ?? ''}$`; |
There was a problem hiding this comment.
Preserve later math after literal-dollar spans
When a message contains a prose dollar before a real formula in the same inline run—for example costs $5 and formula $x$ or Use $HOME before $E=mc^2$—the tokenizer has already paired the first literal $ with the formula's opening $. This branch only converts that paired token back to text ($5 and formula $) and never re-tokenizes the remaining x$, so the actual formula is left as raw text even though KaTeX is now enabled. The guard needs to split/reparse or avoid consuming a later math opener as the closing delimiter for prose dollars.
Useful? React with 👍 / 👎.
A prose dollar in front of a real formula in the same inline run (`costs $5 and formula $x$`, `Use \$HOME before $E=mc^2$`) exposed the core limit of the token-level guard: markstream's tokenizer greedily pairs the first literal \$ with the formula's opening \$ before any hook runs, so converting that span back to text could only blank it -- the later formula's opening \$ was already consumed and the formula rendered as raw text. Move the guard from postTransformTokens to a source-level preprocessor that runs before tokenization. escapeProseDollars protects code spans, fenced code blocks, and \$\$…\$\$ display math, then pairs single \$ delimiters using the Pandoc (tight delimiters) and GitHub-style outer-boundary rules: any \$ without a valid partner is escaped as \\\$, so the tokenizer leaves it literal while real formulas -- including ones that come after a prose dollar -- still parse as math. The component now preprocesses each markdown segment's text and the postTransformTokens hook is gone. Rewrote the tests around the string-in/string-out helper, including the prose-before-formula case, code spans, fenced code, and block math. Addresses the fifth Codex review comment on PR #1035.
1 similar comment
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 35346a8760
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| let text = src.replace(FENCED_CODE_RE, save); | ||
| text = text.replace(INLINE_CODE_RE, save); | ||
| text = text.replace(BLOCK_MATH_RE, save); |
There was a problem hiding this comment.
Protect indented code before escaping dollars
When a message uses a valid 4-space indented Markdown code block instead of backticks, this protection pass does not stash it before pairAndEscape runs. For snippets like echo $HOME, the dollar is rewritten to \$HOME, and because Markdown code blocks render backslashes literally, the web chat now corrupts code that previously displayed accurately. Please include indented code blocks in the protected regions before escaping prose dollars.
Useful? React with 👍 / 👎.
The dollar-escaping preprocessor stashed fenced code blocks, inline code, and display math, but not 4-space / tab indented code blocks. So a snippet like ` echo \$HOME` had its dollar rewritten to `\\$HOME`, and because Markdown renders backslashes literally inside code, the web chat corrupted the code to show a stray backslash. Add an indented-code regex and protect those lines too. Also make the placeholder restore iterative, so nested protected regions (e.g. inline code that looks like display math) restore correctly instead of leaving a placeholder behind. Addresses the sixth Codex review comment on PR #1035.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1b4a575561
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // A 4-space / tab indented line is a Markdown indented code block; protect | ||
| // each such line so its dollars are not rewritten (code renders backslashes | ||
| // literally, so `\$HOME` would show instead of `$HOME`). | ||
| const INDENTED_CODE_RE = /^(?: {4}|\t)[^\n]*/gm; |
There was a problem hiding this comment.
Don’t treat every indented line as code
When a line is indented by four spaces inside a list item, Markdown treats it as normal list content/continuation rather than an indented code block (code under - needs deeper indentation). Because this regex protects every four-space line before pairAndEscape, messages such as - total\n costs $5 and $10 restore that nested list text with raw dollars, so the newly enabled KaTeX parser can still consume the price range as math instead of rendering literal dollars.
Useful? React with 👍 / 👎.
The indented-code regex protected every 4-space line, but inside a list item a 4-space indent is a normal continuation paragraph, not a code block (code under a list marker needs deeper indentation). So a message like `- total\n costs \$5 and \$10` had that nested line stashed as "code", leaving its dollars un-escaped -- and the KaTeX parser then rendered the price range as math. Narrow the indented-code rule to a run of 4-space / tab lines that is preceded by a blank line (or the start of the text). That still protects real top-level indented code blocks and deeper-indented code inside lists, while letting 4-space list-continuation lines get their dollars escaped. Addresses the seventh Codex review comment on PR #1035.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8343b59920
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| * tokenizer leaves as a literal dollar. Code spans, fenced code blocks, and | ||
| * `$$…$$` display math are left untouched. | ||
| */ | ||
| const FENCED_CODE_RE = /(^[ \t]{0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]{0,3}\2[ \t]*(?=\n|$))/gm; |
There was a problem hiding this comment.
Preserve dollars inside unclosed code fences
When a fenced code block is still streaming, or when the assistant finishes with an unterminated fence, this regex does not protect the block because it requires a closing fence. The markdown renderer can still treat an EOF-closed fence as code, but escapeProseDollars now rewrites shell/env examples inside it, e.g. ```sh\necho $HOMEbecomesecho $HOME`, so users see corrupted code until a closer arrives or permanently if it never does.
Useful? React with 👍 / 👎.
Enable KaTeX for display math only: disable markstream's inline math rule
(`md.inline.ruler.disable('math')`) via customMarkdownIt, leaving the
`math_block` rule for $$…$$. Single $ now stays literal everywhere, so
prices, env vars, shell paths, and code are never mis-rendered as math --
with no escaping, no code detection, and no preprocessor.
This removes the escapeProseDollars normalization layer and all of its
code-protection machinery (the 8 review comments it attracted were
symptoms of trying to make a lax single-$ tokenizer behave). Display
$$…$$ math continues to render via KaTeX.
Changeset updated to describe display-math-only support.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 961c172706
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| "@fontsource-variable/jetbrains-mono": "^5.2.8", | ||
| "@xterm/addon-fit": "^0.11.0", | ||
| "@xterm/xterm": "^6.0.0", | ||
| "katex": "^0.16.22", |
There was a problem hiding this comment.
Refresh the Nix pnpm dependency hash
In the Nix packaging path, flake.nix includes pnpm-lock.yaml in the fixed-output fetchPnpmDeps source and pins it with the hash at flake.nix:153; adding katex changes the lockfile/dependency set but this commit leaves that hash unchanged, so nix build will fail with a fixed-output hash mismatch before the app builds. Please update the pnpmDeps hash together with this new dependency.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Checked this one: no flake.nix hash update is needed for the current diff. katex@0.16.47 was already present in pnpm-lock.yaml via markstream-vue on main; this PR only adds it as a direct importer dependency for apps/kimi-web, so the fetched dependency store content is unchanged. The Nix CI also confirms it: nix build .#kimi-code passed on commit 961c172706 (job https://github.com/MoonshotAI/kimi-code/actions/runs/28083761728/job/83144618679).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6f8220dcab
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // without any escaping or code-detection gymnastics. `math_block` (the $$ rule) | ||
| // is left enabled. | ||
| function disableInlineMath(md: MarkdownIt): MarkdownIt { | ||
| md.inline.ruler.disable('math'); |
There was a problem hiding this comment.
Preserve inline-scanned $$ display math
When a model writes display math in the common paragraph form, e.g. Here is $$E=mc^2$$, this disables markstream's whole inline math rule, which is also the rule that scans $$...$$ inside inline text; math_block only catches line-start/block forms. As a result those $$ formulas remain raw even though the change promises $$…$$ display math, so please avoid disabling the whole inline rule if the intent is only to suppress single-dollar parsing.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch. I am going to keep this out of this PR intentionally: the current change only guarantees the block/display forms that math_block catches ($$...$$ on its own line / block form), while avoiding the complexity of reintroducing a custom inline math scanner just to keep paragraph-form Here is $$...$$. Re-enabling the built-in inline rule would also bring back single-$ false positives, which this PR is deliberately avoiding. I have linked #972 for the remaining delimiter policy work; paragraph-form $$...$$ / inline delimiters can be handled there if we decide the product should support them.
Resolve a conflict in apps/kimi-web/test/lib-logic.test.ts: keep main's new createCoalescedAsyncRunner tests (from #1087) in place of the removed KaTeX prose-dollar tests.
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
Related Issue
Closes #1080.
Refs #972. This PR fixes the display-math / KaTeX CSS path covered by that report, while single
$...$and inline delimiter rendering stay intentionally out of scope.Problem
The web chat math path was only partially wired.
markstream-vuecan parse math nodes and render them through KaTeX, butkimi webdid not explicitly own that integration:katexwas only present as an optional peer, the renderer loader was not enabled by app code, andkatex/dist/katex.min.csswas not imported.That missing stylesheet is the root cause of #1080: KaTeX renders stretchable radicals with very wide SVG pieces and relies on
katex.min.cssto clip and position them. Without the stylesheet, formulas such as\sqrt{\mu_0 \varepsilon_0}show an abnormally long radical bar and become unreadable.What changed
Enable KaTeX for display math only in the web chat:
katexas a direct dependency and importkatex/dist/katex.min.css, including the bundled KaTeX fonts/styles required for radicals and other stretchy symbols.$$...$$display math is rendered through KaTeX as an explicit app behavior rather than an accidental optional-peer side effect.md.inline.ruler.disable('math')) viacustomMarkdownIt, leaving only themath_blockrule active.Why display math only
Single
$inline math is intentionally not rendered.$is overloaded in chat text (prices$5, env vars$PATH, shell paths$HOME/bin), and markstream's tokenizer pairs any two$characters with no notion of prose vs. formula - so enabling single$would mis-render$5 and $10,$PATH:$HOME,costs $5 and formula $x$, etc. This matches MathJax's own default ($...$is not an inline delimiter unless explicitly enabled). As a result, single$stays literal everywhere with no escaping and no code-detection logic.No application logic changes beyond the renderer wiring.
Checklist
data-markstream-pending, no raw$$), that\sqrt{...}radicals render at normal width, that single$/ prices / env vars / shell paths stay literal, that KaTeX fonts load with no 404s, that long display formulas scroll instead of overflowing, and that colors are correct across terminal/modern/kimi themes in light and dark; the existing web tests still pass and a code block still highlights (no regression).gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update. - N/A: no user-facing docs affected.