From 2531fc130d969e89395ef96c4f93b759d73e7113 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 27 May 2026 01:02:04 +0200 Subject: [PATCH] fix(ui): preserve user ordered-list markers (closes #5013) --- packages/coding-agent/CHANGELOG.md | 1 + .../interactive/components/user-message.ts | 13 +++++++--- packages/tui/CHANGELOG.md | 4 +++ packages/tui/src/components/markdown.ts | 19 +++++++++++++- packages/tui/src/index.ts | 2 +- packages/tui/test/markdown.test.ts | 25 +++++++++++++++++++ 6 files changed, 59 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 35412c129..6c91ca23c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed user message transcript rendering to preserve user-authored ordered-list markers ([#5013](https://github.com/earendil-works/pi/issues/5013)). - Fixed self-update commands to bypass npm, pnpm, and Bun minimum release age gates for explicit `pi update` runs ([#4929](https://github.com/earendil-works/pi/issues/4929)). - Fixed context token estimates to count user image attachments consistently with tool result images ([#4983](https://github.com/earendil-works/pi/issues/4983)). - Fixed `RpcClient` to reject pending requests and consume stdin pipe errors when the child process exits unexpectedly ([#4764](https://github.com/earendil-works/pi/issues/4764)). diff --git a/packages/coding-agent/src/modes/interactive/components/user-message.ts b/packages/coding-agent/src/modes/interactive/components/user-message.ts index 8fda83e9a..45d655776 100644 --- a/packages/coding-agent/src/modes/interactive/components/user-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/user-message.ts @@ -15,9 +15,16 @@ export class UserMessageComponent extends Container { super(); this.contentBox = new Box(1, 1, (content: string) => theme.bg("userMessageBg", content)); this.contentBox.addChild( - new Markdown(text, 0, 0, markdownTheme, { - color: (content: string) => theme.fg("userMessageText", content), - }), + new Markdown( + text, + 0, + 0, + markdownTheme, + { + color: (content: string) => theme.fg("userMessageText", content), + }, + { preserveOrderedListMarkers: true }, + ), ); this.addChild(this.contentBox); } diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 045039e95..c4e913d8d 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added an opt-in Markdown renderer option to preserve source ordered-list markers for transcript rendering ([#5013](https://github.com/earendil-works/pi/issues/5013)). + ### Fixed - Fixed `Shift+Enter` in Apple Terminal by detecting local macOS modifier state when Terminal.app sends plain Return. diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 2f0ab68df..d5e3f477c 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -70,6 +70,11 @@ export interface MarkdownTheme { codeBlockIndent?: string; } +export interface MarkdownOptions { + /** Preserve source ordered-list markers instead of normalizing them from the list start. */ + preserveOrderedListMarkers?: boolean; +} + interface InlineStyleContext { applyText: (text: string) => string; stylePrefix: string; @@ -81,6 +86,7 @@ export class Markdown implements Component { private paddingY: number; // Top/bottom padding private defaultTextStyle?: DefaultTextStyle; private theme: MarkdownTheme; + private options: MarkdownOptions; private defaultStylePrefix?: string; // Cache for rendered output @@ -94,12 +100,14 @@ export class Markdown implements Component { paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, + options?: MarkdownOptions, ) { this.text = text; this.paddingX = paddingX; this.paddingY = paddingY; this.theme = theme; this.defaultTextStyle = defaultTextStyle; + this.options = options ? { ...options } : {}; } setText(text: string): void { @@ -548,6 +556,11 @@ export class Markdown implements Component { return result; } + private getOrderedListMarker(item: Tokens.ListItem): string | undefined { + const match = /^(?: {0,3})(\d{1,9}[.)])[ \t]+/.exec(item.raw); + return match ? `${match[1]} ` : undefined; + } + /** * Render a list with proper nesting support */ @@ -559,7 +572,11 @@ export class Markdown implements Component { for (let i = 0; i < token.items.length; i++) { const item = token.items[i]; - const bullet = token.ordered ? `${startNumber + i}. ` : "- "; + const bullet = token.ordered + ? this.options.preserveOrderedListMarkers + ? (this.getOrderedListMarker(item) ?? `${startNumber + i}. `) + : `${startNumber + i}. ` + : "- "; const taskMarker = item.task ? `[${item.checked ? "x" : " "}] ` : ""; const marker = bullet + taskMarker; const firstPrefix = indent + this.theme.listBullet(marker); diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 9404aa655..a645c8a65 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -15,7 +15,7 @@ export { Editor, type EditorOptions, type EditorTheme } from "./components/edito export { Image, type ImageOptions, type ImageTheme } from "./components/image.ts"; export { Input } from "./components/input.ts"; export { Loader, type LoaderIndicatorOptions } from "./components/loader.ts"; -export { type DefaultTextStyle, Markdown, type MarkdownTheme } from "./components/markdown.ts"; +export { type DefaultTextStyle, Markdown, type MarkdownOptions, type MarkdownTheme } from "./components/markdown.ts"; export { type SelectItem, SelectList, diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index 584c583c7..fd5345df6 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -104,6 +104,31 @@ describe("Markdown component", () => { assert.ok(plainLines.some((line) => line.includes("2. Second"))); }); + it("should normalize ordered list markers by default", () => { + const markdown = new Markdown("1. alpha\n1. beta\n1. gamma", 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd()); + + assert.deepStrictEqual(lines, ["1. alpha", "2. beta", "3. gamma"]); + }); + + it("should preserve source ordered list markers when configured", () => { + const markdown = new Markdown( + " 4. forth\n 3. third\n\n10) ten\n7) seven", + 0, + 0, + defaultMarkdownTheme, + undefined, + { + preserveOrderedListMarkers: true, + }, + ); + + const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd()); + + assert.deepStrictEqual(lines, ["4. forth", "3. third", "", "10) ten", "7) seven"]); + }); + it("should render mixed ordered and unordered nested lists", () => { const markdown = new Markdown( `1. Ordered item