From 3379f88ea4b357526861b5f06c4707244621030d Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Tue, 28 Apr 2026 21:43:32 -0400 Subject: [PATCH] docs: clarify wrapForFrac and streaming math-fence rationale Address two Copilot review comments on PR #17175. - `wrapForFrac` doc said "additive operators or whitespace" but the implementation also matches `*` and `/`. The wider behaviour is the one we want (nested products and fractions need parens to disambiguate inline `/`), so the doc is updated to match instead of tightening the regex. - `fenceOpenAt` was flagged as "overly conservative" vs. `markdown.tsx`, which falls back to paragraph rendering for unclosed `$$` openers. Mirroring that fallback in the streaming chunker would prematurely commit a paragraph rendering of the unclosed opener to the monotonic stable prefix, where it would be frozen and become wrong the moment the closer streams in. The asymmetry is deliberate; document why so it isn't "fixed" again later. Made-with: Cursor --- ui-tui/src/components/streamingMarkdown.tsx | 12 ++++++++++++ ui-tui/src/lib/mathUnicode.ts | 11 +++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/ui-tui/src/components/streamingMarkdown.tsx b/ui-tui/src/components/streamingMarkdown.tsx index 86dde930e5..1be70b283a 100644 --- a/ui-tui/src/components/streamingMarkdown.tsx +++ b/ui-tui/src/components/streamingMarkdown.tsx @@ -42,6 +42,18 @@ import { Md } from './markdown.js' // snippets like ` ```\n$$x$$\n``` ` (math example inside a code block) // don't double-count. A `$$x$$` line that opens AND closes on its own // produces zero net toggles; that's `len >= 4` plus `endsDollar`. +// +// NB: this is INTENTIONALLY more conservative than `markdown.tsx`'s +// parser, which falls back to paragraph rendering when an `$$` opener +// has no matching closer. The renderer can do that safely because it +// always sees the full text on every call. The streaming chunker +// cannot — once a chunk is committed to the monotonic stable prefix it +// is frozen, so prematurely deciding "this `$$` is just prose" would +// permanently commit a paragraph rendering that becomes wrong the +// instant the closer streams in. Treating any unmatched `$$` opener +// as still-open keeps the boundary parked behind it until the closer +// arrives (or the stream ends and the non-streaming `` takes over, +// at which point the renderer's fallback kicks in correctly). const fenceOpenAt = (s: string, end: number) => { let codeOpen = false let mathOpen = false diff --git a/ui-tui/src/lib/mathUnicode.ts b/ui-tui/src/lib/mathUnicode.ts index 7c6f8939ce..17af85ee03 100644 --- a/ui-tui/src/lib/mathUnicode.ts +++ b/ui-tui/src/lib/mathUnicode.ts @@ -644,10 +644,13 @@ const replaceFracs = (input: string): string => { } // Wrap multi-token expressions in parens so `\frac{a+b}{c}` becomes -// `(a+b)/c` rather than `a+b/c`. We only wrap when the expression has -// loose precedence — additive operators or whitespace that would change -// meaning under inline `/`. Atomic factors like `n!`, `x^2`, `\sin x` -// don't need parens; wrapping them just clutters the output. +// `(a+b)/c` rather than `a+b/c`. We wrap whenever inline `/` would +// change the meaning — that's any binary operator (`+`, `-`, `*`, `/`) +// or whitespace separating tokens. `*` and `/` matter because nested +// fractions and products like `\frac{a*b}{c}` and `\frac{1/x}{y}` would +// otherwise read as `a*b/c` (right-associative ambiguity) and `1/x/y`. +// Atomic factors like `n!`, `x^2`, `\sin x` don't trigger any of these +// and stay un-parenthesised — wrapping them just clutters the output. const wrapForFrac = (expr: string) => { const trimmed = expr.trim()