Skip to content

fix(markdown): render LaTeX math in chat with KaTeX - #3409

Merged
benjaminshafii merged 4 commits into
devfrom
fix/render-latex-math-katex
Aug 1, 2026
Merged

fix(markdown): render LaTeX math in chat with KaTeX#3409
benjaminshafii merged 4 commits into
devfrom
fix/render-latex-math-katex

Conversation

@benjaminshafii

Copy link
Copy Markdown
Member

What

OpenWork renders Markdown but had no math engine, so $...$, $$...$$, \(...\) and \[...\] all displayed as raw LaTeX source — making technical/physics conversations hard to read.

Reported by Daniel Kovner (kovnerd@gmail.com) against v0.18.12 via in-app feedback.

Addresses all three asks from the report:

  1. Render standard $...$ / $$...$$ with KaTeX, via marked-katex-extension (the upstream opencode web approach).
  2. Also accept \(...\) / \[...\], which models commonly emit.
  3. Fail gracefully on malformed LaTeX without breaking the rest of the message.

How

  • apps/app/src/components/markdown/markdown-math.ts (new) — one marked extension. marked-katex-extension handles the dollar delimiters; local block + inline extensions handle \(...\) / \[...\], which markdown would otherwise swallow as backslash escapes. Kept the standard (not nonStandard) dollar rule so $5 and $10 stays prose.
  • Both parsers get the extension — otherwise formulas would flicker away when a message containing a fenced code block upgrades to the Shiki render.
  • DOMPurify: allow <semantics> / <annotation>. They are not in DOMPurify's default MathML allowlist, so the accessible MathML branch (and copy-as-TeX) was being silently stripped.
  • Graceful failure: every math renderer is wrapped. throwOnError: false only catches KaTeX ParseError; anything else would throw straight out of the synchronous React render and blank the whole message. Fallback shows the original LaTeX.
  • text-highlights.ts: skip .katex so find-in-conversation cannot inject <mark> into formula layout spans.
  • CSS: import katex/dist/katex.min.css; keep formulas at surrounding text size and let long display math scroll.

Tests

Command Result
pnpm --filter @openwork/app typecheck clean
bun test --isolate tests/ (apps/app) 584 pass, 2 fail
pnpm --filter @openwork/app build clean, 59 KaTeX font files emitted
pnpm fraimz --flow markdown-math PASSED (5 frames)
pnpm fraimz --flow markdown-primitive steps 1-3 pass (regression check)

The 2 unit failures (message-list-loading.test.tsx) are pre-existing — baseline origin/dev gives an identical 574 pass / 2 fail, and the file passes in isolation on both.

markdown-primitive step 4 times out mounting the Browser panel; verified identical on baseline origin/dev in the same sandbox, so it is a sandbox limitation, not a regression. Steps 1-3 (chat markdown, code blocks, search highlighting) pass on both.

New unit tests in apps/app/tests/markdown-math.test.ts cover all four delimiters, MathML survival through sanitization, graceful failure, currency amounts, code spans, both parsers and both presentations.

Proof

fraimz run driving the real Electron app on Daytona — full artifact posted below.

OpenWork rendered Markdown but had no math engine, so $...$, $$...$$,
\(...\) and \[...\] all showed up as raw LaTeX source in technical
conversations (reported against v0.18.12).

- Add a markdown-math marked extension: marked-katex-extension covers the
  standard dollar delimiters (same approach as upstream opencode web), plus
  local block/inline extensions for the \(...\) and \[...\] pairs models
  commonly emit, which markdown would otherwise swallow as escapes.
- Register it on both the sync and Shiki-highlighted parsers so formulas do
  not flicker when a message with a code block upgrades.
- Allow <semantics>/<annotation> through DOMPurify; they are not in its
  default MathML allowlist, so the accessible MathML branch (and copy-as-TeX)
  was being stripped.
- Wrap every math renderer so malformed LaTeX degrades to visible source
  instead of throwing out of the React render and blanking the message.
- Skip .katex when applying find-in-conversation highlights.
- Import katex CSS and keep formulas at the surrounding text size.

Tests: apps/app/tests/markdown-math.test.ts covers all four delimiters,
MathML survival through sanitization, graceful failure, currency amounts,
code spans, both parsers and both presentations.
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openwork-app Ready Ready Preview Aug 1, 2026 7:51am
openwork-den Ready Ready Preview Aug 1, 2026 7:51am
openwork-den-worker-proxy Ready Ready Preview Aug 1, 2026 7:51am
openwork-diagnostics Ready Ready Preview Aug 1, 2026 7:51am
openwork-landing Ready Ready Preview, v0 Aug 1, 2026 7:51am

@benjaminshafii

Copy link
Copy Markdown
Member Author

fraimz — ✅ PASSED

1 passed · 0 failed · 0 skipped — run 2026-08-01T07-09-58-047Z

Full frame proof with validated screenshots: evals/results/2026-08-01T07-09-58-047Z/fraimz.html (re-run: pnpm fraimz --flow markdown-math)

✅ markdown-math — LaTeX math renders as typeset math in chat responses

User-facing flow demo

  1. OpenWork displays a normal session before the math proof starts

    🎙 OpenWork opens an existing conversation and displays the session normally, with no errors before the math proof begins.

    • ✅ URL hash includes "/session/"
    • ✅ Visible text does not include "Something went wrong"
    • 📸 markdown-math-01-conversation-open.png — 5 validations passed
    markdown-math-01-conversation-open.png
  2. Inline $...$ and (...) math render as typeset formulas, not raw LaTeX source

    🎙 A technical answer arrives and the inline formulas read as real typeset math, so dollar-delimited LaTeX no longer shows up as raw source in the sentence.

    • 📸 markdown-math-02-inline-math-rendered.png — 6 validations passed
    markdown-math-02-inline-math-rendered.png
  3. $$...$$ and [...] both render as centred display equations

    🎙 The same message shows centred display equations for the Hamiltonian and the quadratic formula, including the bracket delimiters that models commonly emit.

    • 📸 markdown-math-03-display-math-rendered.png — 6 validations passed
    markdown-math-03-display-math-rendered.png
  4. Each formula keeps a MathML branch so screen readers announce the equation

    🎙 Every formula carries an accessible MathML copy underneath, so screen readers announce the equation instead of reading out backslashes.

  5. Malformed LaTeX and plain currency never break the surrounding message

    🎙 Malformed LaTeX degrades to plain visible source and ordinary prices stay untouched, so one bad formula never takes down the rest of the message.

    • ✅ Visible text does not include "Something went wrong"
    • 📸 markdown-math-04-malformed-latex-graceful.png — 7 validations passed
    markdown-math-04-malformed-latex-graceful.png
    • ✅ Script frame 1 narrated: "OpenWork opens an existing conversation and displays the session normally, with no error"
    • ✅ Script frame 2 narrated: "A technical answer arrives and the inline formulas read as real typeset math, so dollar-"
    • ✅ Script frame 3 narrated: "The same message shows centred display equations for the Hamiltonian and the quadratic f"
    • ✅ Script frame 4 narrated: "Every formula carries an accessible MathML copy underneath, so screen readers announce t"
    • ✅ Script frame 5 narrated: "Malformed LaTeX degrades to plain visible source and ordinary prices stay untouched, so "

Comment thread evals/flows/markdown-math.flow.mjs Fixed
…zation'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@benjaminshafii
benjaminshafii merged commit 1fecabf into dev Aug 1, 2026
22 checks passed
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.

3 participants