diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 8f56c122..b852bd24 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -8,6 +8,7 @@ - Fixed Kitty CSI-u handling to ignore unsupported modifiers so modifier-only events do not insert stray printable characters ([#1807](https://github.com/badlogic/pi-mono/issues/1807)) - Fixed single-line paste performance by inserting pasted text atomically instead of character-by-character, preventing repeated `@` autocomplete scans during paste ([#1812](https://github.com/badlogic/pi-mono/issues/1812)) - Fixed `visibleWidth()` to ignore generic OSC escape sequences (including OSC 133 semantic prompt markers), preventing width drift when terminals emit semantic zone markers ([#1805](https://github.com/badlogic/pi-mono/issues/1805)) +- Fixed markdown blockquotes dropping nested list content by rendering blockquote children as block-level tokens ([#1787](https://github.com/badlogic/pi-mono/issues/1787)) ## [0.55.4] - 2026-03-02 diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 9bb15b1c..cb0cc1c9 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -331,19 +331,36 @@ export class Markdown implements Component { case "blockquote": { const quoteStyle = (text: string) => this.theme.quote(this.theme.italic(text)); - const quoteStyleContext: InlineStyleContext = { - applyText: quoteStyle, - stylePrefix: this.getStylePrefix(quoteStyle), + const quoteStylePrefix = this.getStylePrefix(quoteStyle); + const applyQuoteStyle = (line: string): string => { + if (!quoteStylePrefix) { + return quoteStyle(line); + } + const lineWithReappliedStyle = line.replace(/\x1b\[0m/g, `\x1b[0m${quoteStylePrefix}`); + return quoteStyle(lineWithReappliedStyle); }; - const quoteText = this.renderInlineTokens(token.tokens || [], quoteStyleContext); - const quoteLines = quoteText.split("\n"); // Calculate available width for quote content (subtract border "│ " = 2 chars) const quoteContentWidth = Math.max(1, width - 2); - for (const quoteLine of quoteLines) { - // Wrap the styled line, then add border to each wrapped line - const wrappedLines = wrapTextWithAnsi(quoteLine, quoteContentWidth); + // Blockquotes contain block-level tokens (paragraph, list, code, etc.), so render + // children with renderToken() instead of renderInlineTokens(). + const quoteTokens = token.tokens || []; + const renderedQuoteLines: string[] = []; + for (let i = 0; i < quoteTokens.length; i++) { + const quoteToken = quoteTokens[i]; + const nextQuoteToken = quoteTokens[i + 1]; + renderedQuoteLines.push(...this.renderToken(quoteToken, quoteContentWidth, nextQuoteToken?.type)); + } + + // Avoid rendering an extra empty quote line before the outer blockquote spacing. + while (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1] === "") { + renderedQuoteLines.pop(); + } + + for (const quoteLine of renderedQuoteLines) { + const styledLine = applyQuoteStyle(quoteLine); + const wrappedLines = wrapTextWithAnsi(styledLine, quoteContentWidth); for (const wrappedLine of wrappedLines) { lines.push(this.theme.quoteBorder("│ ") + wrappedLine); } diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index 824ff3ca..2bf859bb 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -757,6 +757,29 @@ bar`, assert.ok(!barLine?.includes("\x1b[36m"), `bar line should NOT have cyan color: ${barLine}`); }); + it("should render list content inside blockquotes", () => { + const markdown = new Markdown( + `> 1. bla bla +> - nested bullet`, + 0, + 0, + defaultMarkdownTheme, + ); + + const lines = markdown.render(80); + const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); + const quotedLines = plainLines.filter((line) => line.startsWith("│ ")); + + assert.ok( + quotedLines.some((line) => line.includes("1. bla bla")), + `Missing ordered list item: ${JSON.stringify(quotedLines)}`, + ); + assert.ok( + quotedLines.some((line) => line.includes("- nested bullet")), + `Missing unordered list item: ${JSON.stringify(quotedLines)}`, + ); + }); + it("should wrap long blockquote lines and add border to each wrapped line", () => { const longText = "This is a very long blockquote line that should wrap to multiple lines when rendered"; const markdown = new Markdown(`> ${longText}`, 0, 0, defaultMarkdownTheme);