From f2e9d75388fe17325ebe31372e5287b4acdb67a3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 27 Jun 2026 19:23:52 +0200 Subject: [PATCH] fix(coding-agent): preserve backslash escapes in user messages closes #6105 --- packages/coding-agent/CHANGELOG.md | 1 + .../interactive/components/user-message.ts | 2 +- packages/tui/CHANGELOG.md | 4 ++++ packages/tui/src/components/markdown.ts | 6 ++++++ packages/tui/test/markdown.test.ts | 20 +++++++++++++++++++ 5 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 266d72b3..8e85e09c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed `--session` and `SessionManager.open()` to reject non-empty invalid session files without overwriting them ([#6002](https://github.com/earendil-works/pi/issues/6002)). +- Fixed user-message transcript rendering to keep visible backslashes in Markdown escape sequences such as `\"` ([#6105](https://github.com/earendil-works/pi/issues/6105)). - Fixed assistant messages stopped by output length to show a visible incomplete-response error ([#4290](https://github.com/earendil-works/pi/issues/4290)). - Fixed `--no-session --session-id` so ephemeral CLI runs can use deterministic session IDs for provider cache affinity ([#6070](https://github.com/earendil-works/pi/issues/6070)). - Fixed disk BMP image files to be detected, converted to PNG, and attached through `read` and CLI `@file` inputs ([#6047](https://github.com/earendil-works/pi/issues/6047)). 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 45d65577..d1b39343 100644 --- a/packages/coding-agent/src/modes/interactive/components/user-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/user-message.ts @@ -23,7 +23,7 @@ export class UserMessageComponent extends Container { { color: (content: string) => theme.fg("userMessageText", content), }, - { preserveOrderedListMarkers: true }, + { preserveOrderedListMarkers: true, preserveBackslashEscapes: true }, ), ); this.addChild(this.contentBox); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 2823c818..5c207d8c 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 backslash escapes for transcript rendering ([#6105](https://github.com/earendil-works/pi/issues/6105)). + ## [0.80.2] - 2026-06-23 ## [0.80.1] - 2026-06-23 diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 1034cb47..88c2358b 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -98,6 +98,8 @@ export interface MarkdownTheme { export interface MarkdownOptions { /** Preserve source list markers instead of normalizing them. */ preserveOrderedListMarkers?: boolean; + /** Preserve source backslash escapes instead of normalizing escaped punctuation. */ + preserveBackslashEscapes?: boolean; } interface InlineStyleContext { @@ -498,6 +500,10 @@ export class Markdown implements Component { for (const token of tokens) { switch (token.type) { + case "escape": + result += applyTextWithNewlines(this.options.preserveBackslashEscapes ? token.raw : token.text); + break; + case "text": // Text tokens in list items can have nested tokens for inline formatting if (token.tokens && token.tokens.length > 0) { diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index 47bc0a81..c166a582 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -654,6 +654,26 @@ describe("Markdown component", () => { }); }); + describe("Backslash escapes", () => { + it("should normalize escaped punctuation by default", () => { + const markdown = new Markdown(String.raw`"\"`, 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd()); + + assert.deepStrictEqual(lines, [`""`]); + }); + + it("should preserve source backslash escapes when configured", () => { + const markdown = new Markdown(String.raw`"\"`, 0, 0, defaultMarkdownTheme, undefined, { + preserveBackslashEscapes: true, + }); + + const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd()); + + assert.deepStrictEqual(lines, [String.raw`"\"`]); + }); + }); + describe("Pre-styled text (thinking traces)", () => { it("should preserve gray italic styling after inline code", () => { // This replicates how thinking content is rendered in assistant-message.ts