Skip to content

fix(desktop): repair inline math rendering for LLM output#3666

Closed
lightfront wants to merge 15 commits into
esengine:main-v2from
lightfront:fix/inline-math-rendering
Closed

fix(desktop): repair inline math rendering for LLM output#3666
lightfront wants to merge 15 commits into
esengine:main-v2from
lightfront:fix/inline-math-rendering

Conversation

@lightfront

@lightfront lightfront commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Fix: Inline math rendering for LLM output

Problem

LLM output has a long tail of Markdown idiosyncrasies around math
delimiters — glued $$, stray , before closing fences, currency that
shouldn't be math, code blocks containing $ regex literals, malformed
inline code, etc. Reasonix's normalizeMath pre-pass and isLikelyInlineMath
classifier were tuned to a narrower set of cases, so each new quirk
surfaced as a red katex-error block (or worse, a swallowed-document
parser failure) in the chat.

This PR hardens the math-rendering pre-pass against the most common
LLM-output shapes, with a regression test pinning each one.

What this PR fixes

Nine classes of LLM-Markdown quirks, all repaired or classified at the
pre-render stage:

  1. Block math $$ glued to prose. When a model writes
    decomposes as$$\n\mathbf{6}… or …D(q^2),$$\nwith …, the
    closing $$ is not on its own line. micromark-extension-math
    only recognises a closing $$ fence at the start of a new line,
    so without a blank line it consumes the rest of the document as
    math and katex fails on stray $ in the next paragraph. The
    pre-pass now inserts a blank line before any $$ preceded by a
    letter, bracket, full-stop punctuation, or comma.

  2. Inline math rejected as currency / prose. The classifier now
    accepts single digits, single uppercase letters, comma-separated
    tokens, numbers with implicit-multiplication variables ($2.5x$),
    percentages, and one-sided comparisons (< B, A <) as math.
    These are common in physics, chemistry, and non-English math
    prose (Chinese/Japanese textbooks, for example, almost always
    write $S$ for a set, not the English word).

  3. Unary plus/minus. Expressions like $+2$, $-x$,
    $+\alpha$ are now classified as math instead of literal text.

  4. $ inside code blocks. Single- and multi-line code blocks
    (including malformed single-line docs) have their $ content
    protected from katex's math parser. Regex patterns, template
    literals, and pasted documentation about the math pipeline itself
    no longer trigger red katex-error blocks.

  5. Top-level % in math. KaTeX treats unescaped % as a LaTeX
    comment char and silently truncates the formula at end-of-line —
    $x = 50%$ previously rendered as x = 50 with no error.
    latexNormalizeForKatex now escapes top-level % to \%;
    already-escaped \% is handled as a 2-char command so there's
    no double-escape.

  6. Prose currency preserved. Strings like "These two apples
    cost $5 and $6" leave their $ signs visible instead of
    converting them to HTML entities.

  7. Array column specs & ket pipes (the d450aec1 follow-up).
    Inside \begin{array}{c|c} the | means "draw a vertical rule"
    and must not be rewritten to \vert (which raised KaTeX "Unknown
    column alignment"
    ). Separately, in GFM Markdown tables an LLM
    writes kets as \|uud\rangle, but \| is the "parallel-to"
    double bar in KaTeX, not the single bar | kets use. The
    {…} column preamble is now copied verbatim, and \| is
    converted to \vert for ket openers / bra closers while matched
    \|x\| norms are preserved.

  8. Multi-letter group notation (e.g. $SO(3,1)$). The
    classifier's function-call rule previously accepted only a single
    letter before the parentheses (f(x)), so group notation like
    $SO(3,1)$, $SU(2)$, $SL(2)$, $GL(n)$, $Sp(2n)$,
    $Spin(n)$, $Diff(M)$ fell through to the prose fallback and
    rendered as literal $. The identifier is now allowed to be
    1–6 letters.

  9. LaTeX command followed by a digit (e.g. $\tfrac12$). The
    classifier's backslash-command rule ended in \b (a word
    boundary). $\tfrac12$, $\frac12$, $\sqrt2$, $\log3$ have
    no word boundary between the command name and the trailing digit
    (both are word characters), so they were rejected and rendered as
    literal $. The \b is dropped — a backslash command is a
    backslash command regardless of what follows.

Files changed

File Role
desktop/frontend/src/components/mathNormalize.ts The pre-pass: blank-line repair for glued $$, code-block protection, single-line code-block handling.
desktop/frontend/src/components/mathClassify.ts The inline-math classifier: pure numbers, single uppercase letters, comma-separated tokens, numbers with variables, one-sided comparisons, unary +/-, multi-letter group notation.
desktop/frontend/src/components/latexNormalize.ts The LaTeX→KaTeX escape helper: top-level %\%, verbatim array/tabular column specs, ket/bra |\vert.
desktop/frontend/src/__tests__/math-golden.test.ts Regression tests for every case above.

Design notes

  • All repairs happen in the pre-pass, not in remark-rehype-katex.
    The pre-pass is the right layer: it sees the source text and can
    make string-level decisions before any parser has had a chance to
    misbehave. Catching these at the katex-render layer would mean
    building fallback display paths for each failure mode.

  • Character class is the right place for the blank-line repair. The
    regex on line 58 of mathNormalize.ts reads
    [A-Za-z\)\]\>\.。!?,]. Each character was added in response to a
    real user report (closing bracket, full-stop, CJK punctuation,
    comma). The pattern is intentionally narrow: it excludes digits so
    that c^2$$ inside a formula is left alone (and the existing test
    pins that case).

  • The classifier's permissiveness is a deliberate trade-off. Single
    digits, single uppercase letters, etc. could be English prose, but
    in the context of a chat assistant for physics and math, accepting
    them as math is the right call. The currency pattern "costs $5 and
    $6" still works because the multi-currency step doesn't classify
    those as math.

  • Top-level % escaping is in latexNormalizeForKatex, not in
    normalizeMath.
    This is the same layer that handles |→\vert
    and \,-preservation — KaTeX-specific concerns that need to run
    inside the math body, after the pre-pass has identified the math
    boundaries.

  • Array column specs are detected by environment, not by counting
    braces.
    COLUMN_SPEC_ENVS lists array/tabular/aligned etc.
    whose first {…} arg is a column preamble; that brace group is
    copied verbatim so its | rules survive. Pipes elsewhere still
    convert to \vert normally.

Trade-offs and known limitations

Orphan $$ is not repaired

If a model writes $$\nformula and forgets the closing $$,
micromark-extension-math swallows everything until the next $$
which is the same root cause as case 1 above, but inverted. Every
attempt to rescue an orphan from the renderer side has produced
worse output (whole prose paragraphs wrapped in math spans). This
case is left to upstream prompt engineering or a post-generation
lint.

Fenced code detection is CommonMark-strict

fencedCodeEnd requires the opening fence to be at the start of a
line. Prose like "wrap code in ```blocks``` here" is correctly
not treated as a code block. Single-line docs with embedded
``` are still handled via a fallback path.

Testing

All 182 math-golden tests pass (was 108; +74 regression tests across
the review-feedback, display-math-repair, array/ket, group-notation, and
LaTeX-command-followed-by-digit rounds). The full frontend test suite
(13 files) is green.

Note: a pre-existing TypeScript error in src/lib/bridge.ts is
present at the branch tip without this PR too — unrelated to the
math pipeline and not touched by any commit here.

cd desktop/frontend
pnpm test

Regression-test coverage

Each new case has a dedicated test:

  • Pure numbers: $1$, $42$, $2.5$ → math
  • Numbers with variables: $2.5x$ → math
  • Numbers with LaTeX: $10\%$ → math
  • Comma-separated lists: A, B, (A, B) → math
  • One-sided comparisons: < B, A < → math
  • Single uppercase letters: $S$, $A$ → math
  • Unary plus/minus: $+2$, $-x$ → math
  • Single-line code blocks with $ → protected
  • Prose currency: "cost $5 and $6" → dollars preserved
  • Top-level % in math: $x = 50%$ → escaped to \%
  • Closing $$ after comma: …D(q^2),$$\nwith … → repaired to …D(q^2),\n\n$$
  • Array column specs: {c|c}, {cc|c}, {|c|c|}, tabular| preserved
  • Ket / bra / inner product: \|uud\rangle, \langle\psi\|, \langle x\|y\rangle\vert
  • Norm preserved: \|x\| → double bar kept
  • Multi-letter group notation: $SO(3,1)$, $SU(2)$, $SL(2)$, $GL(n)$, $Sp(2n)$, $Spin(3)$, $Diff(M)$ → math
  • LaTeX command + digit: $\tfrac12$, $\frac12$, $\sqrt2$, $\log3$, $\overline3$ → math

Real-world examples (from user reports)

### Chinese math text
$1$ 和 $2$ 之间有无穷多有理数
→ Both 1 and 2 render as math

$S$ 非空
→ S renders as math (set name)

把 $(A, B)$ 整体当作一个新对象
→ (A, B) renders as math (ordered pair)

A 的每个元素 $< B$ 的每个元素
→ < B renders as math (comparison)

### Math with numbers
$2.5x + 3$
→ Entire expression renders as math

$10\%$ increase
→ 10\% renders as math

$42$ elements
→ 42 renders as math

### Currency in prose
costs $5 and $6
→ Dollar signs preserved unchanged

costs $5$ today
→ $5$ renders as math (pure number)

price is $10.50$
→ $10.50$ renders as math (decimal)

### Code blocks
```javascript
r = r.replace(/\$\$/, ...);

→ No KaTeX errors, $ symbols protected

Display math glued after a comma

$$
\langle \pi(p')|T^{\mu\nu}(0)|\pi(p)\rangle
= 2 P^\mu P^\nu,A(q^2) + 2!\left(q^\mu q^\nu - q^2 g^{\mu\nu}\right)!D(q^2),$$
with $P=(p+p')/2$, $q=p'-p$.

→ Closing `$$` is normalised to its own line, math renders,
downstream `$P=...$` and `$q=...$` inline math renders correctly.

### Group notation (physics)
The Lorentz group $SO(3,1)$ and gauge group $SU(3)_C \times SU(2)_L \times U(1)_Y$.
→ Multi-letter group names render as math, not literal `$`.

Changelog (cumulative, since PR opened)

  • 0b03c948 — initial: block math repair, classifier improvements, code-block $ protection, currency preservation.
  • ae0a04dd — test rename: "non-English math prose" → "minimal LaTeX patterns" (the rules are language-agnostic, but the test cases include real Chinese examples).
  • c712bd64&#36; escape in code blocks (since dropped in the review-feedback pass).
  • ccef134a — single-line code-block handling.
  • ed672359 — classifier hardening + single-line code-block fix.
  • b1fb63e4 — preserve prose currency.
  • 8c477b5c — review feedback pass: dropped the &#36; round-trip, dropped the redundant +? change, restricted fence detection to line-start, added top-level % escape, trimmed oversized comments.
  • c93af0ac — unary plus/minus classifier rule.
  • 45482b20 — closing $$ after comma (caught during a chat session, regression test pins the case).
  • 70bcb8a6 — step 6 wraps non-math $…$ in &#36; entities (maintainer review patch).
  • d450aec1 — repair display math regression: extract $$…$$ pairs as a unit before the repair regex so the closing $$ of well-formed pairs is never split off.
  • 072d38c3 — add youngDiagrams.ts dependency for mathNormalize (required by d450aec1's mathNormalize rewrite).
  • eb029d5e — multi-letter group notation (SO(3,1), SU(2), …) classified as inline math.
  • 2b090416 — correct pipe handling for array column specs ({c|c}) and kets (\|uud\rangle); brings in the latexNormalize.ts implementation that d450aec1's tests were already asserting.
  • 841da58a — classify a LaTeX command followed by a digit (\tfrac12, \frac12, \sqrt2, \log3) as math; drop the \b from the backslash-command rule.

@github-actions github-actions Bot added desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development labels Jun 9, 2026
@lightfront lightfront force-pushed the fix/inline-math-rendering branch 2 times, most recently from a8c5f5a to 5532392 Compare June 9, 2026 11:07
@github-actions github-actions Bot added agent Core agent loop (internal/agent, internal/control) provider Model providers & selection (internal/provider) labels Jun 9, 2026
@lightfront lightfront force-pushed the fix/inline-math-rendering branch from 7843e3b to 83dda9b Compare June 9, 2026 14:52
@esengine

Copy link
Copy Markdown
Owner

Still interested in this fix — inline math rendering breakage is a recurring complaint. Two things before it can land: the branch conflicts with current main-v2 (a lot merged today, including streaming-markdown work in the same area), and the lint job is failing. Please rebase and fix lint; once green I'll review promptly.

@esengine

Copy link
Copy Markdown
Owner

Posting the technical specifics now so the rebase round can address everything at once:

  1. The &#36; escape/unescape pair is a no-op as written — protected segments are stored out-of-band and swapped back wholesale, so the escape never matters… except code that legitimately contains the literal text &#36; gets rewritten to $ on restore (corruption). Delete the dance; the PR description also claims restore does not unescape, which is stale vs the code.
  2. Step 5's greedy→non-greedy change does nothing — the char class already excludes $, so the match extent is identical — and its comment describes behavior it doesn't have. Drop both.
  3. Mid-line fence detection diverges from CommonMark: prose like "wrap code in blocks" now swallows everything to the next fence/EOF and kills math for the rest of the message, while remark itself won't treat it as code. Restrict to line-start fences or justify the divergence.
  4. Verify % survives inside math ($100%$) — KaTeX treats % as a comment char, so unescaped it silently truncates the formula.
  5. Comment policy: several 8–12 line essays (one in mathNormalize.ts describes code that no longer exists) and // was: … history notes in tests need to go; also the "\mathbf" test string's backslash isn't escaped, so the test actually exercises mathbf.

@lightfront lightfront force-pushed the fix/inline-math-rendering branch from 83dda9b to 885ade7 Compare June 11, 2026 08:08
@lightfront

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review — much clearer than the parenthesised bullet list. Rebased onto current main-v2 (7 commits, single squashed fix commit on top) and addressed all five points:

  1. $ escape/unescape dance — dropped. pushSegment no longer touches $, and the restore loop just swaps the segment back wholesale. The header comment is also updated (it claimed restore did not unescape, which didn't match the code). Confirmed that no test relied on the literal &#36; round-trip.

  2. Step 5's +? non-greedy change — dropped. Reverted the regex to + and removed the comment that claimed the change prevents cross-pair matching. Kept the return _m behaviour for non-math pairs (so currency dollars stay visible), with a comment that actually describes what it does.

  3. Fence detection — restricted to line-start. Restored if (start !== 0 && s[start - 1] !== "\n") return -1; in fencedCodeEnd. The single-line "next matching fence is the closer" path is still there for malformed one-line docs, but only reachable when the doc has no newlines, so the prose-swallowing regression is gone.

  4. % inside math — verified and fixed. Confirmed that $x = 50%$ was indeed silently truncated to x = 50 by KaTeX (with a commentAtEnd warning). Added a top-level case in latexNormalizeForKatex that emits \%; already-escaped \% is handled by the existing 2-char command branch above, so no double-escape. New tests cover $x = 50%$, $100%$, $a%b$, and $10\%$.

  5. Comment policy — trimmed. Removed both // was: … notes in the tests, plus the 8-12 line essays in mathNormalize and mathClassify that described code that no longer exists or that the reader can see from the regex. The header still lists the pipeline as a map.

The \mathbf test string is actually \\mathbf{6} (the JS escape is a backslash — verified the runtime string is \mathbf{6} and the assertion checks for \mathbf{6} in the output), so it does test \mathbf rendering through KaTeX. Left it as is.

All 128 math-golden tests + 73 tests across the rest of the frontend suite pass, typecheck clean, and CI is green across ubuntu/macos/windows (including the lint job that was failing before). The branch is now mergeable.

@lightfront lightfront force-pushed the fix/inline-math-rendering branch from 885ade7 to 8c477b5 Compare June 11, 2026 08:14
@lightfront

Copy link
Copy Markdown
Contributor Author

Description updated to reflect the current state of the branch (9 commits, post-review feedback pass + unary +/- + the new comma case).

Key changes from the previous description:

  • Removed the stale claim that "restoration does NOT unescape $ back to $" — the $ round-trip was dropped entirely in the review pass.
  • Removed the misleading "non-greedy regex prevents cross-pair matching" claim — the +? change was dropped too.
  • Added the top-level % escape in latexNormalizeForKatex ($x = 50%$$x = 50%$), which was added in the review-feedback commit.
  • Added the new comma case (closing $$ after a comma on the same line as content), pinned by a regression test next to the existing closing-bracket test.
  • Cumulative changelog at the bottom lists all 9 commits in order, with one-line summaries of what each one actually does (instead of just the original 8).

Also folded the +2, -x unary-plus/minus case into the body of "What this PR fixes" rather than burying it in a separate commit message.

The maintainer's review comments (5 specific issues, all addressed in commit 8c477b5) are now accurately reflected in the body — the old description still claimed the $ round-trip existed, and the +? non-greedy change, which were both dropped.

@esengine esengine left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the rebase and the test pass — the %-escape, the $$ blank-line repairs (including the new comma case), the fence line-start restriction, and the classifier rules all look right, and CI is green.

One blocker, though, in the step-6 change. The golden tests can't see it because they assert on normalizeMath's output string, not on what remark-math does with that string downstream.

Step 6 returning _m for non-math pairs is a render regression

main-v2 wraps non-math pairs in &#36; entities precisely so remark-math never sees a $. remark-math@6 (micromark-extension-math@3.1.0) parses any $…$ as math — our classifier's reject verdict only matters if we keep the $ away from the parser. Running the literal strings this PR now emits through the actual extension:

"it costs $5 and $10 total"  → "it costs <math>5 and</math>10 total"   (parsed as math)
"env $PATH$ here"            → "env <math>PATH</math> here"            (parsed as math)

versus the entity form main-v2 emits:

"…cost &#36;5 and &#36;6"    → "<p>…cost $5 and $6</p>"                (literal — correct)
"env &#36;PATH&#36; here"    → "<p>env $PATH$ here</p>"                (literal — correct)

So with this PR every classifier-rejected pair — currency ($5 and $6), env vars ($PATH$), version tokens ($v1$), plain words ($foo$/$TODO$) — renders as italic KaTeX instead of literal text. That contradicts case 6 in the description ("prose currency preserved") and regresses the $PATH$/$TODO$ cases the classifier still rejects. It doesn't surface as a katex-error (KaTeX happily renders 5 and), which is why the suite stays green — it's a silent semantic mis-render.

This came from over-applying my earlier point 1: the &#36; I asked you to drop was the no-op pair inside pushSegment (code protection). The &#36; wrapping in step 6 is load-bearing. Step 5 already keeps it (${DOLLAR}${m}${DOLLAR}); step 6 should match.

Fix: revert step 6's non-math branch to return + "" + ${DOLLAR}${m}${DOLLAR} + "". Then the golden assertions that currently pin the literal form — normalizeMath("env $PATH$ here") === "env $PATH$ here", the it costs $5 and $10 total case, and the passthrough entries — need to flip to the &#36; form; they're currently pinning the bug.

And please add one test that renders a currency / env-var line through remark-math end-to-end (not just normalizeMath). The current golden tests only run KaTeX on an already-sliced $…$, so they never exercise the prose→parser boundary where this regressed — a render-level assertion is the only thing that would have caught it.

Minor

  • The step-3 comment block in mathNormalize.ts is ~9 lines; the repo caps block comments at 3 (the micromark closing-fence quirk is worth a line or two, but trim the rest).

Everything else is good to land once the step-6 protection is restored.

@esengine

Copy link
Copy Markdown
Owner

Here's the exact fix for the step-6 blocker, verified locally — applying this and I'll merge.

The substantive change is one line: step 6's non-math branch goes back to wrapping in &#36; entities (what step 5 already does) instead of returning the literal $…$. The rest is the test updates — flipping the two golden assertions that were pinning the literal form, and a new render-level section that goes through the real react-markdown + remark-math + rehype-katex path so this can't silently regress again.

I ran the full math-golden suite with this applied: 132 passed, 0 failed, including the three new boundary tests (which fail on the current return _m):

normalizeMath → remark-math render boundary
  PASS  currency '$5 and $6' renders as literal dollars, not math
  PASS  env var $PATH$ renders as literal, not math
  PASS  real inline math $x^2$ still renders as KaTeX
diff --git a/desktop/frontend/src/components/mathNormalize.ts b/desktop/frontend/src/components/mathNormalize.ts
--- a/desktop/frontend/src/components/mathNormalize.ts
+++ b/desktop/frontend/src/components/mathNormalize.ts
@@ -8,7 +8,7 @@
 //   4. Inline `$$` glued to prose gets a blank line inserted before it
 //      (CommonMark requires that block math be paragraph-separated).
 //   5. $$…$$ → display placeholders, $…$ → inline placeholders, gated by
-//      isLikelyInlineMath so currency / env-var tokens pass through.
+//      isLikelyInlineMath; currency / env-var tokens become &#36; entities.
 //   6. Each recognised math source is run through latexNormalizeForKatex
 //      (text-mode escapes, |→\vert, %→\%).
@@ -81,11 +77,12 @@ function normalizeMathText(s: string): string {
     return `${IM}${latexNormalizeForKatex(m)}${IM}`;
   });
 
-  // Step 6: remaining $…$ → classifier-gated inline math. Non-math
-  // pairs (e.g. currency like "$5 and $6") are left unchanged so the
-  // dollars remain visible; remark-math will not try to parse them.
+  // Step 6: remaining $…$ → classifier-gated inline math. remark-math
+  // parses any literal $…$ it sees, so non-math pairs (currency $5,
+  // env vars $PATH$) are wrapped in &#36; entities — remark-math never
+  // sees a $, and the decoded entity still renders as a literal dollar.
   r = r.replace(/\$([^$\n]+)\$/g, (_m, m) => {
-    if (!isLikelyInlineMath(m.trim())) return _m;
+    if (!isLikelyInlineMath(m.trim())) return `${DOLLAR}${m}${DOLLAR}`;
     return `${IM}${latexNormalizeForKatex(m)}${IM}`;
   });
diff --git a/desktop/frontend/src/__tests__/math-golden.test.ts b/desktop/frontend/src/__tests__/math-golden.test.ts
--- a/desktop/frontend/src/__tests__/math-golden.test.ts
+++ b/desktop/frontend/src/__tests__/math-golden.test.ts
@@ -6,6 +6,12 @@
 // mathClassify) rather than reimplementing them inline, so this file
 // catches regressions in the actual code path that runs inside <Markdown>.
 
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import remarkMath from "remark-math";
+import rehypeKatex from "rehype-katex";
 import katex from "katex";
 import { latexNormalizeForKatex, stripMathDelimiters } from "../components/latexNormalize";
 import { isLikelyInlineMath } from "../components/mathClassify";
@@ -238,12 +238,12 @@ console.log("\nnormalizeMath — non-math dollar filtering");
 eq(normalizeMath("costs $1$ today"), "costs $1$ today", "$1$ is math (single-digit index)");
-eq(normalizeMath("env $PATH$ here"), "env $PATH$ here", "$PATH$ not math (env var, dollars preserved)");
+eq(normalizeMath("env $PATH$ here"), "env &#36;PATH&#36; here", "$PATH$ not math (env var → &#36; entities so remark-math leaves it literal)");
 eq(normalizeMath("solve $x^2 + y^2 = z^2$ please"), "solve $x^2 + y^2 = z^2$ please", "$x^2+y^2$ is math");
 eq(normalizeMath("$\\alpha + \\beta$"), "$\\alpha + \\beta$", "$\\alpha+\\beta$ is math");
 eq(normalizeMath("price is $10.50$ each"), "price is $10.50$ each", "$10.50$ is math (decimal number)");
 eq(normalizeMath("$I$ think"), "$I$ think", "$I$ is math (uppercase single letter)");
-eq(normalizeMath("it costs $5 and $10 total"), "it costs $5 and $10 total", "multiple prose $ stays literal (dollars preserved)");
+eq(normalizeMath("it costs $5 and $10 total"), "it costs &#36;5 and &#36;10 total", "multiple prose $ → &#36; entities (dollars preserved, not parsed as math)");
@@ -358,9 +358,6 @@ console.log("\nnormalizeMath — non-math inputs pass through");
 type Passthrough = { src: string; expected: string; label: string };
 const passthrough: Passthrough[] = [
-  // $5$ is filtered to dollar entities so remark-math leaves it literal
-  // and the rendered prose still shows normal dollar signs.
-  // (the previous "costs $5$ today" passthrough case is now a no-op — single-digit $N$ is math)
   { src: "costs $100$ today", expected: "costs $100$ today", label: "multi-digit number is math" },
   { src: "line break \\\\[4pt] here", expected: "line break \\\\[4pt] here", label: "LaTeX line-break spacing" },
   { src: "hello world", expected: "hello world", label: "plain text" },
@@ -366,6 +366,38 @@ for (const { src, expected, label } of passthrough) {
   check(`${label}: ${src}`, () => normalizeMath(src) === expected);
 }
 
+// ── remark-math render boundary ────────────────────────────────────────────────
+// A literal $…$ in normalizeMath output is NOT enough to keep a non-math token
+// out of KaTeX: remark-math parses any $…$ it sees, so the classifier's reject
+// verdict only holds when the $ is hidden as a &#36; entity. These render through
+// the real react-markdown + remark-math + rehype-katex path; the normalizeMath-only
+// golden cases above never cross the prose→parser boundary.
+
+console.log("\nnormalizeMath → remark-math render boundary");
+
+function renderHtml(src: string): string {
+  return renderToStaticMarkup(
+    createElement(ReactMarkdown, {
+      remarkPlugins: [remarkGfm, remarkMath],
+      rehypePlugins: [rehypeKatex],
+      children: normalizeMath(src),
+    }),
+  );
+}
+
+check("currency '$5 and $6' renders as literal dollars, not math", () => {
+  const html = renderHtml("These two apples cost $5 and $6");
+  return !html.includes("katex") && html.includes("$5") && html.includes("$6");
+});
+check("env var $PATH$ renders as literal, not math", () => {
+  const html = renderHtml("env $PATH$ here");
+  return !html.includes("katex") && html.includes("$PATH$");
+});
+check("real inline math $x^2$ still renders as KaTeX", () => {
+  const html = renderHtml("the value $x^2$ here");
+  return html.includes("katex");
+});
+
 // ── Summary ───────────────────────────────────────────────────────────────────

One thing to double-check on your side: the new test imports react-dom/server, react-markdown, remark-gfm, remark-math, rehype-katex — all already in desktop/frontend, so pnpm test picks them up with no dependency change. Ping me when it's pushed and I'll merge.

lightfront pushed a commit to lightfront/DeepSeek-Reasonix that referenced this pull request Jun 12, 2026
Per maintainer review on PR esengine#3666: the literal $…$ pair is not
enough to keep a non-math token out of KaTeX, because remark-math
parses any $…$ it sees in the source and the classifier's reject
verdict only holds when the $ is hidden as a &esengine#36; entity.

Step 6: non-math pairs (currency $5, env vars $PATH) now wrap in
&esengine#36; entities, matching what step 5 already does for the
$<cmd>{…}$ pair. The decoded entity still renders as a literal
dollar.

Test updates:
- Two eq assertions flipped to expect the entity form.
- Drop the stale '$5$ is filtered to entities' comment.
- New render-boundary section runs the real react-markdown +
  remark-math + rehype-katex path; the previous golden cases never
  crossed the prose→parser boundary, so the regression was
  undetectable at the normalizeMath layer.

132 passed, 0 failed (was 129).
lightfront pushed a commit to lightfront/DeepSeek-Reasonix that referenced this pull request Jun 12, 2026
Companion fix to PR esengine#3666 (fix/inline-math-rendering). The character
class in mathNormalize.ts step 3 missed the comma case: a model that
emits a display block whose closing $$ is on the same line as the
trailing comma of the equation content (…D(q^2),$$) leaves the
closing fence glued to the content line, which micromark-extension-math
does not recognise as a closing fence (it only checks for $$ at
the start of a new line). The rest of the document is then consumed
as math and katex fails on the stray $ in the next paragraph.

Add ',' to the character class so the closing $$ is forced onto its
own line, matching the existing 'inline $$ after closing bracket'
behaviour. A regression test pins the comma case.

This commit was previously 45482b2 on fix/inline-math-rendering
(the PR's tip), but cherry-picking the older PR commits onto
dev-new-features (which had this fix from an earlier cherry-pick
overwritten by the older base state) reverted the regex change.
Re-applying it here.
lightfront pushed a commit to lightfront/DeepSeek-Reasonix that referenced this pull request Jun 12, 2026
Cherry-picks the maintainer's review patch from PR esengine#3666 onto
dev-new-features so the local Reasonix app renders currency and
env-var tokens as literal dollars instead of triggering katex
errors.

Per maintainer feedback: a literal $…$ pair in normalizeMath
output is not enough to keep a non-math token out of KaTeX, because
remark-math parses any $…$ it sees in the source. The classifier's
reject verdict only holds when the $ is hidden as a &esengine#36; entity.

Step 6: non-math pairs (currency $5, env vars $PATH) now wrap in
&esengine#36; entities. Decoded entity still renders as a literal dollar.

Test updates:
- Two eq assertions flipped to expect the entity form.
- Drop the stale '$5$ is filtered to entities' comment.
- New render-boundary section runs the real react-markdown +
  remark-math + rehype-katex path; the previous golden cases never
  crossed the prose→parser boundary, so the regression was
  undetectable at the normalizeMath layer.

132 passed, 0 failed (was 129).
@lightfront

lightfront commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Done — your step-6 patch applied and pushed. 70bcb8a6 is the new tip.

mathNormalize.ts step 6 now returns `${DOLLAR}${m}${DOLLAR}` for non-math pairs, the file-header comment is updated to match, the two eq assertions for $PATH$ and $5 and $10 are flipped to expect the entity form, the stale $5$ passthrough comment is dropped, and the new render-boundary section runs the real react-markdown + remark-math + rehype-katex path on the three boundary cases (currency, env var, real inline math).

Local results: 132 passed, 0 failed in math-golden.test.ts (was 129; +3 new boundary tests), and all 12 frontend test files green. The three new boundary tests fail on the previous return _m and pass on the new entity form, which is what makes this a real regression net rather than a tautology.

@lightfront lightfront requested a review from esengine June 13, 2026 17:46
lightfront pushed a commit to lightfront/DeepSeek-Reasonix that referenced this pull request Jun 13, 2026
PR esengine#3666 (commit f63e2e5) added comma to the Step 3 $$ repair regex,
and 0fbb908 later added {}. These additions were meant to fix closing
$$ glued to content on the same line (…D(q^2),$$), but the regex also
matched the closing $$ of well-formed $$…$$ display pairs — every
equation ending in }, ), or , had its closing delimiter split off,
emptying the entire equation into a pair of empty display blocks.

The bug manifested as display equations rendering as blank space with
no visible content: remark-math parsed $$\n\n$$ (empty) and leaked the
equation body as prose. Inline math was unaffected, which is why only
display equations like the proton SU(6) wave function vanished.

User-reported: proton wave function section showed gaps where display
equations should be, and copy-paste produced doubled text artifacts
(KaTeX MathML layer + leaked prose).

Fix: extract $$…$$ pairs as a unit before the repair regex runs, so
the closing $$ is never touched. When the opening $$ is glued to
preceding prose (the original PR esengine#3666 case), insert a blank line
before the extracted pair. Restore display delimiters with newlines
($$\n…\n$$) for remark-math block-math recognition (it requires $$ on
its own line, not glued to content like $$x$$).

All 168 golden tests pass. Updated test expectations that previously
encoded the buggy behavior.

Fixes: f63e2e5, 0fbb908 (PR esengine#3666 follow-ups)
@lightfront

Copy link
Copy Markdown
Contributor Author

Display math regression fix added

Investigation of a user-reported bug ("math formulas don't render") traced the root cause to the $$ repair regex in mathNormalize.ts Step 3 (introduced in this PR, commit 45482b20, extended in 0fbb9086).

The bug: The regex ([A-Za-z\)\]\>\.。!?,{}])\$\$ was designed to repair opening $$ glued to prose, but it also matched the closing $$ of well-formed $$…$$ display pairs. Every display equation ending in }, ), or , had its closing delimiter split off (}$$}\n\n$$), which caused remark-math to parse an empty display block ($$\n\n$$) and leak the equation body as prose.

Symptoms:

  • Display equations rendered as blank space (empty KaTeX blocks)
  • Inline math showed doubled text on copy-paste (MathML layer + leaked prose)
  • User-visible: the proton SU(6) wave function and all $$…$$ equations vanished

The fix (commit d450aec1):

  1. Extract $$…$$ pairs as a unit before the repair regex runs, so the closing $$ is never touched
  2. When the opening $$ is glued to preceding prose (the original intent), insert \n\n before the extracted pair only
  3. Restore display delimiters with newlines ($$\n…\n$$) for remark-math block-math recognition

All 168 golden tests pass. The 15 ket/array tests require PR #4320 (latexNormalize.ts fixes) which is independent.

@lightfront

Copy link
Copy Markdown
Contributor Author

Two follow-up commits pushed — test suite now fully green

While testing the branch I found that d450aec1 ("repair display math regression") shipped its tests for the array-column-spec and ket-pipe cases but not the latexNormalize.ts implementation they depend on — that implementation lives in a separate commit (2756e7f0 on dev-new-features) that wasn't carried over to this branch. Net result: 15 array/ket tests were red on the PR branch (not from any review-blocking issue, just an incomplete cherry-pick). Pushed two commits to fix that, plus an adjacent gap I hit while testing:

2b090416 — correct pipe handling for array column specs and kets

Brings in the latexNormalize.ts logic that d450aec1's tests were already asserting:

  • \begin{array}{c|c} (and tabular) column specs are now copied verbatim — the | means "draw a vertical rule" and must not become \vert. Previously this raised KaTeX "Unknown column alignment: \vert".
  • \|uud\rangle ket openers and \langle\psi\| bra closers convert \|\vert (single bar), while matched \|x\| norms are preserved as the double bar. Kets were rendering as ‖ψ⟩ (parallel-to) instead of |ψ⟩.

Touches only latexNormalize.ts (+229/−1).

eb029d5e — render multi-letter group notation like SO(3,1) as inline math

The classifier's function-call rule only accepted a single letter before the parentheses (f(x), g(x)), so multi-letter group notation — $SO(3,1)$, $SU(2)$, $SL(2)$, $GL(n)$, $Sp(2n)$, $Spin(n)$, $Diff(M)$ — fell through to the prose fallback and rendered as literal $ instead of math. Broadened the identifier to 1–6 letters (in-scope for this PR's classifier work). +9 regression tests.

Test status

Before After
math-golden.test.ts 162 / 15 fail 177 / 0 fail
Full frontend suite (13 files) all green
Typecheck bridge.ts error bridge.ts error (pre-existing, unrelated — not touched by any commit here)

The bridge.ts typecheck error is the one noted in the PR description as pre-existing on the branch tip without this PR.

The rebase onto current main-v2 is the only remaining item from your last review note — happy to do that next if helpful.

Reasonix and others added 2 commits June 14, 2026 17:18
Three targeted fixes to the math-pipeline pre-pass that resolve cases
where the rendered chat output showed LaTeX source as raw text:

1. mathNormalize.ts (Step 2.5): when the model writes block math with
   the opening $$ glued to prose on the same line ('…decomposes
   as$$\n\mathbf{6}…'), CommonMark requires a blank line before
   the $$. remark-math otherwise creates an empty math node and the
   formula leaks out as literal text. Insert \n\n before any $$
   preceded by a letter or end-of-sentence punctuation. The
   freshly-rewritten \] → $$ from step 2 is not affected.

2. mathClassify.ts: classify single digits ($1$, $2$) as math —
   commonly used as set / sequence indices. Multi-digit numbers,
   decimals, and percentages stay literal (still currency / percentage).
   This is a deliberate behavior change documented in the comment.

3. mathClassify.ts: allow comma-separated tokens ('A, B', '1, 2, 3',
   '\\alpha, \\beta', '(A, B)') as math. These are typical of
   ordered-pair / tuple / enumeration notation. Currency and env-var
   usage never looks like this.

4. mathClassify.ts: allow single uppercase letters as math. In
   non-English math prose (Chinese / Japanese / Korean textbooks)
   single capital letters are extremely common as set / algebra /
   group / vector-space names, and the closing-dollar form $X$ is
   essentially never written for English words like I/A/V by hand.

Test changes: 4 existing currency/acronym assertions updated to
reflect the new behavior, 13 new regression tests covering all four
fixes including the user's specific cases ('$1$ 和 $2$' and
'$S$ 非空 / $S$ 有上界'). 98 math-golden tests pass, 112/112 across
all suites, typecheck clean.

Orphan $$ (model wrote display math but forgot the closing $$) is
documented as not-fixed-from-the-renderer: every attempt to rescue
the orphan from the renderer side made the output worse, so the fix
for that case is on the LLM side (post-generation lint or stricter
system prompt).
The classifier rules are language-agnostic, not specific to CJK text.
Updated test section name and descriptions to reflect that patterns
like single digits, comma-separated tokens, and one-sided operators
apply universally across languages. Chinese text in test cases remains
as real user examples, but the rules themselves are not CJK-specific.
light-front-theory and others added 12 commits June 14, 2026 17:18
Add defensive escaping for code blocks containing $ characters.
When protecting code (inline `...` or fenced ```...```), replace
$ with &esengine#36; (HTML entity). On restoration, unescape back to $.

This prevents KaTeX from attempting to parse math delimiters that
appear in code examples, regex patterns, or template literals.

Fixes: Pasted documentation about the math pipeline itself no longer
shows red KaTeX error text.

Tests: 3 new cases added, 106/106 passing
Remove the requirement that ``` must appear after a newline. This
handles cases where documentation is pasted on a single line with
embedded code blocks containing $ symbols.

Previously: ``` markers were only recognized after \n
Now: ``` markers are recognized anywhere

This prevents KaTeX errors (red text) when processing malformed code
blocks that contain $ in regex patterns, template literals, or other
code examples.

All 120 tests pass.
Enhancements to inline math detection:
- Reject pure numbers (1, 2.5, 10) as currency/percentages
- Accept numbers with variables (2.5x, 3y^2) as math
- Accept numbers with LaTeX escapes (10\%) as math
- Fix single-line code block detection to protect $ in malformed markdown

This better matches real-world usage where 'costs $5' is currency
but '$2.5x + 3$' is clearly a mathematical expression.

All 122 tests pass (108 math-golden + 8 text-size + 6 provider-model-refresh).
Previously, the Step 5 regex would greedily match '$5 and $' as a single
math expression with content '5 and ', then convert it to '&esengine#36;5 and &esengine#36;'
because the classifier correctly identified it as non-math. This was visually
correct but had two problems:

1. The greedy match would consume the closing dollar that belonged to the
   next currency token, causing cascade replacements.
2. Prose currency like 'These two apples cost $5 and $6' would have its
   dollar signs converted to HTML entities, which works but is unnecessary
   noise in the rendered output.

Changes:
- Step 5 regex now uses non-greedy matching (+\?) so '$5 and $' doesn't
  match '$5 and $' as a single pair
- When the classifier rejects a match, the original text is preserved
  unchanged (return _m) instead of being wrapped in HTML entities
- This keeps dollar signs visible in prose while still preventing them from
  being parsed as math

All 122 tests pass.
Rebase onto current main-v2 plus five targeted cleanups called out in
the review:

1. Drop the &esengine#36; escape/unescape dance. Protected segments are stored
   out-of-band and swapped back wholesale, so the round-trip is a no-op
   — except for code that legitimately contains the literal text &esengine#36;
   (which got silently rewritten to $ on restore, corrupting the source).
   The header comment is also stale: the description claims restore does
   not unescape, but the code did.

2. Revert Step 5's greedy→non-greedy change. The char class
   [^$\n]+ already excludes $, so changing + to +? has no effect
   on match extent; the comment claiming it prevents cross-pair
   matching is wrong. Drop the change and the misleading comment.
   The "leave non-math pairs unchanged" behaviour is kept.

3. Restrict fenced-code detection to line-start. Allowing ``` anywhere
   in the line would swallow prose like "wrap code in ```blocks``` here"
   into a code region and break the math for the rest of the message —
   the CommonMark spec requires fences at line start. Single-line docs
   are still handled (the next matching fence is the closer).

4. Escape top-level % in math. KaTeX treats unescaped % as a LaTeX
   comment char and silently truncates the formula at end-of-line —
   "$x = 50%$" rendered as "x = 50" with no error. Add a top-level
   case in latexNormalizeForKatex that emits \% (already-escaped \%
   is handled above as a 2-char command, so no double-escape).

5. Trim oversized comments. Drop the // was: ... history notes in tests
   and the 8-12 line essays in mathNormalize / mathClassify that
   describe code that no longer exists or that the reader can see
   from the regex. The header still lists the pipeline as a map.

128 tests pass; typecheck clean.
Expressions starting with a unary + or - (e.g. +2, -x, +\alpha)
were rejected by isLikelyInlineMath because none of the existing
patterns matched them — the operator-pattern on line 10 requires
a character before the operator, and the pure-number pattern
requires the first character to be a digit.

This caused \( +2 \) to be treated as non-math text, rendering
as literal '$+2$' instead of rendering the KaTeX unary plus.

Add a dedicated pattern: /^[+\-]\s*(?:\d+(?:\.\d+)?|[A-Za-z\])/
that matches unary operator + digit/variable/backslash-command.
Companion fix to the inline-math-rendering PR. The character class in
mathNormalize.ts step 3 missed the comma case: a model that emits a
display block whose closing $$ is on the same line as the trailing
comma of the equation content (…D(q^2),$$) leaves the closing
fence glued to the content line, which micromark-extension-math does
not recognise as a closing fence (it only checks for $$ at the
start of a new line). The rest of the document is then consumed as
math and katex fails on the stray $ in the next paragraph.

Add ',' to the character class so the closing $$ is forced onto its
own line, matching the existing 'inline $$ after closing bracket'
behaviour. A regression test pins the comma case.
Per maintainer review on PR esengine#3666: the literal $…$ pair is not
enough to keep a non-math token out of KaTeX, because remark-math
parses any $…$ it sees in the source and the classifier's reject
verdict only holds when the $ is hidden as a &esengine#36; entity.

Step 6: non-math pairs (currency $5, env vars $PATH) now wrap in
&esengine#36; entities, matching what step 5 already does for the
$<cmd>{…}$ pair. The decoded entity still renders as a literal
dollar.

Test updates:
- Two eq assertions flipped to expect the entity form.
- Drop the stale '$5$ is filtered to entities' comment.
- New render-boundary section runs the real react-markdown +
  remark-math + rehype-katex path; the previous golden cases never
  crossed the prose→parser boundary, so the regression was
  undetectable at the normalizeMath layer.

132 passed, 0 failed (was 129).
PR esengine#3666 (commit f63e2e5) added comma to the Step 3 $$ repair regex,
and 0fbb908 later added {}. These additions were meant to fix closing
$$ glued to content on the same line (…D(q^2),$$), but the regex also
matched the closing $$ of well-formed $$…$$ display pairs — every
equation ending in }, ), or , had its closing delimiter split off,
emptying the entire equation into a pair of empty display blocks.

The bug manifested as display equations rendering as blank space with
no visible content: remark-math parsed $$\n\n$$ (empty) and leaked the
equation body as prose. Inline math was unaffected, which is why only
display equations like the proton SU(6) wave function vanished.

User-reported: proton wave function section showed gaps where display
equations should be, and copy-paste produced doubled text artifacts
(KaTeX MathML layer + leaked prose).

Fix: extract $$…$$ pairs as a unit before the repair regex runs, so
the closing $$ is never touched. When the opening $$ is glued to
preceding prose (the original PR esengine#3666 case), insert a blank line
before the extracted pair. Restore display delimiters with newlines
($$\n…\n$$) for remark-math block-math recognition (it requires $$ on
its own line, not glued to content like $$x$$).

All 168 golden tests pass. Updated test expectations that previously
encoded the buggy behavior.

Fixes: f63e2e5, 0fbb908 (PR esengine#3666 follow-ups)
Cherry-picking the display-math fix onto fix/inline-math-rendering
introduced a dependency on youngDiagrams.ts (expandYoungDiagrams)
which exists on dev-new-features but not on this branch. Bringing in
just that file so mathNormalize.ts compiles.
…ne math

The inline-math classifier's function-call rule only accepted a single
letter before the parentheses (f(x), g(x)), so multi-letter group
notation — SO(3,1), SU(2), SL(2), GL(n), Sp(2n), Spin(n), Diff(M) —
fell through to the prose fallback and rendered as literal dollar signs
instead of going through remark-math/rehype-katex.

Broaden the identifier from one letter to 1-6 letters. The cap plus the
requirement that the whole token sits inside one $...$ span keep prose
parentheticals out.

Adds 9 regression cases to the math-golden suite. 177/177 pass.
Two distinct bugs in latexNormalize.ts | handling that surfaced as
broken math rendering in the chat:

1. Array column specs corrupted

   \begin{array}{c|c} was rewritten to \begin{array}{c\vert c},
   causing KaTeX "Unknown column alignment: \vert" parse errors. In
   LaTeX, the | inside an array/tabular preamble means "draw a
   vertical rule" between columns and must not become \vert.

   Fix: COLUMN_SPEC_ENVS lists environments whose first {...} arg is
   a column spec. When \begin{<env>} is found, the spec brace group
   is copied verbatim (no | or % rewriting). Pipes outside the spec
   still convert to \vert normally.

2. Ket delimiters in GFM tables render as double bars

   In Markdown tables, | is the column delimiter, so an LLM writes
   kets as \|uud\rangle to avoid breaking the table. But \| in KaTeX
   is the "parallel-to" symbol (U+2225), the heavy double bar used for
   norms, not the single bar (U+2223) kets use. The result: kets
   rendered with double bars instead of single bars.

   Fix: fixKetPipes() converts \| to \vert when it is a ket opener
   (\|...\rangle) or bra closer (\langle...\|), while preserving
   matched \|...\| norm pairs. Disambiguation is by forward scan to
   the next \| or \rangle, with a backward scan for unmatched
   \langle to catch bra closers.

15 golden tests were broken at HEAD without this code (the tests were
committed referencing features the implementation did not yet have).
This commit restores them to green.

Cherry-picks 746a724 from fix/pipe-column-spec-and-kets.
@lightfront lightfront force-pushed the fix/inline-math-rendering branch from 2b09041 to fbcfbbf Compare June 14, 2026 09:20
lightfront pushed a commit to lightfront/DeepSeek-Reasonix that referenced this pull request Jun 14, 2026
PR esengine#3666 (commit f63e2e5) added comma to the Step 3 $$ repair regex,
and 0fbb908 later added {}. These additions were meant to fix closing
$$ glued to content on the same line (…D(q^2),$$), but the regex also
matched the closing $$ of well-formed $$…$$ display pairs — every
equation ending in }, ), or , had its closing delimiter split off,
emptying the entire equation into a pair of empty display blocks.

The bug manifested as display equations rendering as blank space with
no visible content: remark-math parsed $$\n\n$$ (empty) and leaked the
equation body as prose. Inline math was unaffected, which is why only
display equations like the proton SU(6) wave function vanished.

User-reported: proton wave function section showed gaps where display
equations should be, and copy-paste produced doubled text artifacts
(KaTeX MathML layer + leaked prose).

Fix: extract $$…$$ pairs as a unit before the repair regex runs, so
the closing $$ is never touched. When the opening $$ is glued to
preceding prose (the original PR esengine#3666 case), insert a blank line
before the extracted pair. Restore display delimiters with newlines
($$\n…\n$$) for remark-math block-math recognition (it requires $$ on
its own line, not glued to content like $$x$$).

All 168 golden tests pass. Updated test expectations that previously
encoded the buggy behavior.

Fixes: f63e2e5, 0fbb908 (PR esengine#3666 follow-ups)
The classifier's backslash-command rule used \b after the command name:
  if (/\\[A-Za-z]+\b/.test(math)) return true;

\b is a word boundary, but \tfrac12 / \frac12 / \sqrt2 / \log3 /
\overline3 have no boundary between the name and a trailing digit
('c' and '1' are both word chars), so the regex rejected them and
these common LaTeX forms rendered as literal dollar signs instead of
going through remark-math/rehype-katex.

Drop the \b — a backslash command is a backslash command regardless of
what follows. \alpha, \frac{x}{y}, \cdot 3 (all already passing) are
unaffected; the currency / env-var guards below catch any new
false-positives.

+5 regression cases. 182/182 pass.
@lightfront

Copy link
Copy Markdown
Contributor Author

One more inline-math case: \tfrac12, \sqrt2, \log3 — follow-up fix

While re-reading my own rendered answer I spotted that $\tfrac12$ (and the whole family of a LaTeX command immediately followed by a digit) was rendering as literal $ instead of math.

Root cause: the classifier's backslash-command rule had a \b after the command name:

if (/\\[A-Za-z]+\b/.test(math)) return true;

\b is a word boundary, but \tfrac12 / \frac12 / \sqrt2 / \log3 have no boundary between the name and the trailing digit (c and 1 are both word characters), so the regex rejected them. The fix is to drop the \b — a backslash command is a backslash command regardless of what follows.

-  if (/\\[A-Za-z]+\b/.test(math)) return true;
+  if (/\\[A-Za-z]+/.test(math)) return true;

\alpha, \frac{x}{y}, \cdot 3 (all already passing) are unaffected; the currency / env-var guards further down still catch false-positives. +5 regression tests.

Test status

  • math-golden: 182 / 0 (was 177/0)
  • typecheck: 0 errors
  • branch still rebased on current main-v2

@SivanCola

Copy link
Copy Markdown
Collaborator

Closing as superseded by #4216.

For this math-rendering area, #4216 is now the retained integration path because it carries the Young diagram/tableau work on top of the same math normalization/rendering surface.

Thank you @lightfront for the substantial inline-math work in this PR. The retained PR includes a contribution note acknowledging #3666 as part of the shared repository contribution behind the final math-rendering track.

@SivanCola SivanCola closed this Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Core agent loop (internal/agent, internal/control) desktop Wails desktop app (desktop/**) provider Model providers & selection (internal/provider) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants