Skip to content

feat(gui): render LaTeX math in chat via KaTeX (#208) - #212

Open
justrach wants to merge 3 commits into
mainfrom
fix/208-latex-math
Open

feat(gui): render LaTeX math in chat via KaTeX (#208)#212
justrach wants to merge 3 commits into
mainfrom
fix/208-latex-math

Conversation

@justrach

Copy link
Copy Markdown
Owner

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:

\[
G \approx 1+\alpha+\alpha^2+\cdots+\alpha^m.
\]

The user saw backslashes, commands, and delimiters instead of typeset notation.

Fix

Parser (types.ts, parser.ts) — adds an inline and a display math AST node and recognizes all four agent-emitted forms:

form example level
\[...\] multi-line display block
$$...$$ display block
\(...\) inline inline
$...$ inline inline

Guardrails, all covered by tests:

  • Inline code keeps top precedence, so a $ inside backticks stays code; fences are captured before math, so $$ inside a fenced block 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 → 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, no javascript: href, raw <img onerror> escaped).
  • katex.min.css is bundled via the existing @import block in styles/index.css (mirroring shadcn/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 via renderToStaticMarkup): display + inline typeset to KaTeX, $ in code stays code, unsafe \href emits no anchor / javascript: href.
  • gui bun test: 146/146; eslint clean; new files typecheck clean under strict tsc.

Honest note on verification

The full Tauri/Vite build (bun run build) is not exercised in my environment (its gen:desktop-contracts step needs cargo), so the bundled-CSS visual result wasn't run here. The HTML pipeline is fully unit-tested and the CSS @import follows 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

justrach and others added 3 commits July 14, 2026 13:55
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GUI chat renders LaTeX equations as raw source

1 participant