From cd1ea342aea61c6c77127c7a249763dc5227c98d Mon Sep 17 00:00:00 2001 From: "robopipi[bot]" <336283572+robopipi[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:09:08 +0100 Subject: [PATCH] fix(tui): fall back to raw LaTeX when display math is wider than the content width (#11487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display math (`$$…$$`, `\[…\]`) renders as several 2D rows: numerator, fraction bar and denominator are separate lines. When the layout was wider than the Markdown content width, `render()` wrapped each row with `wrapTextWithAnsi` on its own. Only the overflowing row broke, so the rows below moved down and the equation no longer lined up. For example, `dx` landed between the fraction bar and the denominators. The `latexBlock` case in `Markdown.renderToken()` now checks the rendered layout against the available width. If any row is too wide, it uses the raw LaTeX source, the same fallback already used when parsing fails. The raw source then wraps like normal text. Layouts that fit render exactly as before. **Testing** - Added a regression test in `packages/tui/test/markdown.test.ts` using the equation from the issue. At width 40 it renders the 2D layout unchanged. At width 30 it falls back to the raw `\[ … \]` source, with no fraction bars and no line wider than 30. The test fails without the fix and passes with it. - `node --test test/markdown.test.ts`: 93 pass. `node --test test/latex.test.ts`: 111 pass. - `npm run check` reports no errors in the touched files. The only errors are 6 type errors in `packages/ai/test`, caused by stale generated model data, and they are the same on `main` without this change. - Reproduction at width 30, before the fix: ``` ∂² u ∂² u n 1 1 ──── + ──── = ∑ ── +∫ f(x) dx ∂ x² ∂ y² i=1 i² 0 ``` After the fix, the raw source, wrapped: ``` \[ \frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 ... \] ``` Fixes #11483 Requested by @badlogic Co-authored-by: triagent --- packages/tui/CHANGELOG.md | 1 + packages/tui/src/components/markdown.ts | 10 +++++++--- packages/tui/test/markdown.test.ts | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index bb7634ec6..23cb8dc2f 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -11,6 +11,7 @@ - Fixed fullscreen double-click selection splitting paths and tokens at `~`, `.`, `\`, and `+`. Word characters now match iTerm2's defaults ([#10980](https://github.com/earendil-works/pi/issues/10980)) - Fixed `fuzzyMatch()` ranking a query scattered over an earlier part of the text above a contiguous match later in it. It now also scores each contiguous occurrence of the query and keeps the best alignment ([#11220](https://github.com/earendil-works/pi/issues/11220), [#11296](https://github.com/earendil-works/pi/pull/11296)) - Fixed `ProcessTerminal.setTitle()` writing control characters from the title, which could end the title sequence and send the rest to the terminal as escape sequences +- Fixed display math wider than the content width being wrapped row by row, which split fractions and sums apart. It now falls back to the raw LaTeX source ([#11483](https://github.com/earendil-works/pi/issues/11483)) ## [1.4.0] - 2026-10-21 diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 9194c2251..a3b526d73 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -536,10 +536,14 @@ export class Markdown implements Component { case "latexBlock": { const latexToken = token as LatexToken; - const rendered = + let rendered = !latexToken.pending && this.options.renderLatex !== false - ? (renderLatex(latexToken.text, { display: true }) ?? latexToken.raw.trim()) - : latexToken.raw.trim(); + ? renderLatex(latexToken.text, { display: true }) + : undefined; + // A 2D layout cannot be wrapped row by row; fall back to the raw source when it does not fit. + if (rendered === undefined || rendered.split("\n").some((line) => visibleWidth(line) > width)) { + rendered = latexToken.raw.trim(); + } for (const line of rendered.split("\n")) { lines.push(this.applyDefaultStyle(line)); } diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index 38e6ceb5a..b20d84dfe 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -966,6 +966,29 @@ Inline \(A_{sf}\)`, ); }); + // Regression test for https://github.com/earendil-works/pi/issues/11483 + it("falls back to raw source when display math is wider than the content width", () => { + const source = [ + "\\[", + String.raw`\frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2} = \sum_{i=1}^{n} \frac{1}{i^2} + \int_0^1 f(x)\,dx`, + "\\]", + ].join("\n"); + const render = (width: number) => + new Markdown(source, 0, 0, defaultMarkdownTheme).render(width).map((line) => stripAnsi(line).trimEnd()); + + assert.deepStrictEqual(render(40), [ + "∂² u ∂² u n 1 1", + "──── + ──── = ∑ ── +∫ f(x) dx", + "∂ x² ∂ y² i=1 i² 0", + ]); + + const narrow = render(30); + assert.strictEqual(narrow[0], "\\["); + assert.strictEqual(narrow[narrow.length - 1], "\\]"); + assert.ok(!narrow.some((line) => line.includes("─"))); + assert.ok(narrow.every((line) => line.length <= 30)); + }); + // Regression tests for https://github.com/earendil-works/pi/issues/11159 describe("unclosed display math before tables (#11159)", () => { const table = ["| A | B |", "|---|---|", "| 1 | 2 |"];