From 09e9de5749193beab234f30ed220a77f3d91cfad Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 31 Mar 2026 22:40:20 +0200 Subject: [PATCH] fix(tui): stop heading underline leaking into padding --- packages/tui/CHANGELOG.md | 1 + packages/tui/src/components/markdown.ts | 4 ++++ packages/tui/test/markdown.test.ts | 30 +++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index dfc539f9..faecd143 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed markdown H1 headings ending with inline code from leaking underline styling into trailing line padding - Fixed slash-command argument autocomplete to await async `getArgumentCompletions()` results and ignore invalid return values, preventing crashes when extension commands provide asynchronous completions ([#2719](https://github.com/badlogic/pi-mono/issues/2719)) ## [0.64.0] - 2026-03-29 diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index a19387d6..3cdf6373 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -505,6 +505,10 @@ export class Markdown implements Component { } } + while (stylePrefix && result.endsWith(stylePrefix)) { + result = result.slice(0, -stylePrefix.length); + } + return result; } diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index 6b9bd3dc..55cd2bca 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -20,6 +20,16 @@ function getCellItalic(terminal: VirtualTerminal, row: number, col: number): num return cell.isItalic(); } +function getCellUnderline(terminal: VirtualTerminal, row: number, col: number): number { + const xterm = (terminal as unknown as { xterm: XtermTerminalType }).xterm; + const buffer = xterm.buffer.active; + const line = buffer.getLine(buffer.viewportY + row); + assert.ok(line, `Missing buffer line at row ${row}`); + const cell = line.getCell(col); + assert.ok(cell, `Missing cell at row ${row} col ${col}`); + return cell.isUnderline(); +} + describe("Markdown component", () => { describe("Nested lists", () => { it("should render simple nested list", () => { @@ -1016,6 +1026,26 @@ bar`, assert.ok(precedingChunk.includes("\x1b[4m"), `Should re-apply underline for h1: ${precedingChunk}`); }); + it("should not leak h1 underline into padding when inline code is the last token", async () => { + const markdown = new Markdown("# Important distinction from `open()`", 0, 0, defaultMarkdownTheme); + const terminal = new VirtualTerminal(80, 4); + const tui = new TUI(terminal); + tui.addChild(markdown); + tui.start(); + await terminal.flush(); + + const renderedLine = markdown.render(80)[0]; + assert.ok(renderedLine, "Should render heading line"); + const contentWidth = renderedLine.replace(/\x1b\[[0-9;]*m/g, "").trimEnd().length; + assert.ok(contentWidth > 0, "Should have visible heading content"); + + for (let col = contentWidth; col < 80; col++) { + assert.strictEqual(getCellUnderline(terminal, 0, col), 0, `Expected no underline in padding at col ${col}`); + } + + tui.stop(); + }); + it("should preserve heading styling after bold text", () => { const markdown = new Markdown("## Heading with **bold** and more", 0, 0, defaultMarkdownTheme);