Add markdown-body-color extension — color unwrapped markdown paragraphs

pi-tui's Markdown component only colors paragraph text when the caller
passes a defaultTextStyle.color wrapper. Pi-coding-agent does this for
thinking blocks but not for regular assistant content, so paragraphs
emit uncoloured and inherit the terminal profile's foreground color
(green on many themes).

This extension monkey-patches Markdown.prototype.render to install a
fallback defaultTextStyle (inkPurple #d4c5e8 by default, overridable
via PI_MARKDOWN_BODY_COLOR) when one isn't already set. Thinking blocks
and other custom-colored markdowns are untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 22:02:56 +02:00
parent 471e429ed0
commit 3f9ec6ef3c
2 changed files with 64 additions and 0 deletions
+1
View File
@@ -10,6 +10,7 @@
!/README.md
!/ai-server/
!/local-llama.ts
!/markdown-body-color.ts
!/mechanicus-banner.ts
!/scripts/
!/themes/
+63
View File
@@ -0,0 +1,63 @@
/**
* markdown-body-color — force a theme-aware color on markdown paragraph text.
*
* Why: pi-tui's Markdown component only wraps paragraph text in a color when
* the caller passes a `defaultTextStyle.color` fn (assistant-message.js does
* this for thinking blocks but not for regular assistant content). Without it,
* paragraph output is emitted uncoloured and inherits the terminal profile's
* foreground color — which on many terminal themes is green.
*
* Fix: monkey-patch Markdown.prototype.render so that if no defaultTextStyle
* is set at render time, we install one that wraps output in the active pi
* theme's `text` token color. This leaves thinking blocks and custom-colored
* markdowns alone (they already have their own defaultTextStyle).
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Markdown } from "@mariozechner/pi-tui";
// Hardcoded fallback — matches themes/dark-mechanicus.json "inkPurple" var.
// Override via PI_MARKDOWN_BODY_COLOR env var (hex, e.g. "#d4c5e8").
const FALLBACK_HEX = process.env.PI_MARKDOWN_BODY_COLOR ?? "#d4c5e8";
function hexToRgb(hex: string): [number, number, number] {
const m = hex.replace(/^#/, "");
const n = parseInt(m.length === 3
? m.split("").map((c) => c + c).join("")
: m, 16);
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
}
const [R, G, B] = hexToRgb(FALLBACK_HEX);
const OPEN = `\x1b[38;2;${R};${G};${B}m`;
const CLOSE = `\x1b[39m`;
function wrapWithBodyColor(text: string): string {
// Re-open the color after any existing `\x1b[39m` (default-fg reset) so
// nested styled runs inside a paragraph don't bleed back to terminal fg.
const safe = text.split(CLOSE).join(CLOSE + OPEN);
return OPEN + safe + CLOSE;
}
let patched = false;
export default function (_pi: ExtensionAPI) {
if (patched) return;
patched = true;
const proto = Markdown.prototype as any;
const originalRender = proto.render;
proto.render = function (width: number) {
if (!this.defaultTextStyle) {
this.defaultTextStyle = { color: wrapWithBodyColor };
// Force cache invalidation so the first post-patch render colors content.
if (typeof this.invalidate === "function") this.invalidate();
}
return originalRender.call(this, width);
};
console.log(
`[markdown-body-color] Default markdown body color = ${FALLBACK_HEX}`,
);
}