feat(gui): render LaTeX math in chat via KaTeX (#208) - #212
Open
justrach wants to merge 3 commits into
Open
Conversation
Agent responses emit LaTeX, but the dependency-free chat Markdown parser had no math node, so display formulas like `\[ G \approx 1+\alpha \]` and inline `$...$` rendered as raw source with visible backslashes and delimiters. Parser (types.ts, parser.ts): adds an inline and a display `math` AST node and recognizes all four agent-emitted forms — display `\[...\]` / `$$...$$` (block, multi-line, checked before other blocks) and inline `\(...\)` / `$...$`. Inline code keeps top precedence so a `$` inside backticks stays code, and fences are captured before math so `$$` inside a fence stays code. The `$...$` form is deliberately narrow (opener/closer hug the content, never against a digit, never after a backslash) so prose currency like "$5 and $10" and an escaped `\$` are left as text. An unclosed delimiter (still streaming) degrades to open math / literal text instead of throwing or swallowing the rest of the message. Renderer (KatexMath.tsx, MarkdownRenderer.tsx): a new component typesets the captured LaTeX with KaTeX. renderToString is pure (no DOM), so it stays server-renderable and unit-testable; throwOnError:false makes malformed/streaming input render as a visible error rather than throw; trust:false keeps \href and raw-HTML commands from injecting anything unsafe. katex.min.css is bundled via the existing @import block in styles/index.css (mirroring shadcn/tailwind.css) so the component needs no CSS import the test runner can't resolve. Tests: parser.test.ts (+7: code/fence protection, escaped `$`, currency, streaming degradation) and MarkdownRenderer.test.tsx (+4 full-pipeline renderToStaticMarkup: display/inline typeset, code stays code, unsafe \href emits no anchor or javascript: href). gui `bun test` 146/146; eslint clean; the new files typecheck clean under strict tsc. Note: the full Tauri/Vite build (`bun run build`) is not exercised here (its gen:desktop-contracts step needs cargo), so the bundled-CSS visual result was not run in this environment; the @import follows the repo's existing pattern. Refs #208 Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com>
Self-review follow-up on the inline `$...$` matcher: it let the closing `$` sit
against a space, so a price followed by a later `$word` ("I have $5 and $funds")
matched "5 and" as math. Require the closer to hug its content — (?<!\s) before
the closing `$` — so it can't close on a space. Real inline math never ends in a
space before `$`, so `$x = 5$` still parses. Adds the regression case.
Refs #208
Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com>
Locks in the parser's degrade-safely behavior against inputs the streaming chat will hit: empty/malformed delimiters ($$, \(\), lone $) stay literal instead of emitting an empty formula or throwing; a literal $ inside \( ... \) reaches KaTeX verbatim rather than being treated as a nested delimiter; and multi-byte glyphs inside math content are preserved. Refs #208 Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Agent responses emit LaTeX, but the dependency-free chat Markdown parser had no math node. Display formulas and inline math rendered as raw source:
The user saw backslashes, commands, and delimiters instead of typeset notation.
Fix
Parser (
types.ts,parser.ts) — adds an inline and a displaymathAST node and recognizes all four agent-emitted forms:\[...\]$$...$$\(...\)$...$Guardrails, all covered by tests:
$inside backticks stays code; fences are captured before math, so$$inside a fenced block stays code.$...$form is deliberately narrow (opener/closer hug the content, never against a digit, never after a backslash), so prose currency like$5 and $10and an escaped\$are left as text.Renderer (
KatexMath.tsx,MarkdownRenderer.tsx) — a new component typesets the captured LaTeX with KaTeX:renderToStringis pure (no DOM), so it stays server-renderable and unit-testable.throwOnError: false→ malformed/streaming input renders as a visible KaTeX error rather than throwing.trust: false→\href/raw-HTML commands can't inject anything unsafe (verified: no anchor, nojavascript:href, raw<img onerror>escaped).katex.min.cssis bundled via the existing@importblock instyles/index.css(mirroringshadcn/tailwind.css), so the component carries no CSS import.Tests
parser.test.ts+7: display/inline parsing, code + fence protection, escaped$, prose currency, streaming degradation.MarkdownRenderer.test.tsx+4 (full pipeline viarenderToStaticMarkup): display + inline typeset to KaTeX,$in code stays code, unsafe\hrefemits no anchor /javascript:href.bun test: 146/146; eslint clean; new files typecheck clean under stricttsc.Honest note on verification
The full Tauri/Vite build (
bun run build) is not exercised in my environment (itsgen:desktop-contractsstep needs cargo), so the bundled-CSS visual result wasn't run here. The HTML pipeline is fully unit-tested and the CSS@importfollows the repo's existing pattern; a quick visual sanity check on a real build is worthwhile before merge.Dependency
Adds
katex(~0.17) +@types/katex. Follows the existing library-backed-render precedent (MermaidDiagram).Closes #208