fix(ui): preserve user ordered-list markers (closes #5013)

This commit is contained in:
Armin Ronacher
2026-05-27 01:02:04 +02:00
Unverified
parent 59ec8003db
commit 2531fc130d
6 changed files with 59 additions and 5 deletions
+1
View File
@@ -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)).
@@ -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);
}
+4
View File
@@ -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.
+18 -1
View File
@@ -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);
+1 -1
View File
@@ -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,
+25
View File
@@ -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