Darken warpGreen to nurgle-green + patch Editor for cursor/typing

- Theme: warpGreen #7a8a42 -> #5a6b2e (darker, more "nurgle", still
  readable for syntaxString / success / toolDiffAdded).
- markdown-body-color: extend to also monkey-patch Editor.prototype.render
  so typed-text and the cursor (reverse-video) inherit the same inkPurple
  body color instead of falling through to the terminal-default fg.
  Re-opens the color after \x1b[0m (cursor's full reset) and \x1b[39m
  (nested theme.fg close) so color survives those breaks within a line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 22:09:01 +02:00
parent 3f9ec6ef3c
commit 42a4d48b0b
2 changed files with 47 additions and 30 deletions
+46 -29
View File
@@ -1,30 +1,37 @@
/**
* markdown-body-color — force a theme-aware color on markdown paragraph text.
* markdown-body-color — force a theme-aware color on text paths that pi-tui
* emits uncoloured, so they stop inheriting the terminal profile's default
* foreground color (green on many themes).
*
* 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.
* Two patches:
*
* 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).
* 1. Markdown.prototype.render — pi-tui's Markdown component only wraps
* paragraph text when the caller passes a `defaultTextStyle.color` fn.
* pi-coding-agent does this for thinking blocks but not for regular
* assistant content. We install a fallback defaultTextStyle before the
* original render runs.
*
* 2. Editor.prototype.render — the Editor emits typed text and the cursor
* (via \x1b[7m reverse-video) with no color wrapping at all. We wrap the
* whole line in our body color and re-open the color after any `\x1b[0m`
* (cursor's full-reset) or `\x1b[39m` (default-fg reset, used by nested
* theme.fg wrappers) so the color persists across those breaks.
*
* Body color defaults to the dark-mechanicus "inkPurple" (#d4c5e8). Override
* via PI_MARKDOWN_BODY_COLOR env var.
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Markdown } from "@mariozechner/pi-tui";
import { Editor, 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);
const n = parseInt(
m.length === 3 ? m.split("").map((c) => c + c).join("") : m,
16,
);
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
}
@@ -32,11 +39,14 @@ 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;
// Re-open the body color after any ANSI reset inside a line. Cursor inverse
// video uses \x1b[0m (full reset) and theme.fg helpers close with \x1b[39m.
function reopenAfterResets(s: string): string {
return s.replace(/\x1b\[0m/g, `\x1b[0m${OPEN}`).replace(/\x1b\[39m/g, `\x1b[39m${OPEN}`);
}
function wrap(s: string): string {
return OPEN + reopenAfterResets(s) + CLOSE;
}
let patched = false;
@@ -45,19 +55,26 @@ 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) {
// ─ 1. Markdown component ────────────────────────────────────────────
const mdProto = Markdown.prototype as any;
const origMdRender = mdProto.render;
mdProto.render = function (width: number) {
if (!this.defaultTextStyle) {
this.defaultTextStyle = { color: wrapWithBodyColor };
// Force cache invalidation so the first post-patch render colors content.
this.defaultTextStyle = { color: wrap };
if (typeof this.invalidate === "function") this.invalidate();
}
return originalRender.call(this, width);
return origMdRender.call(this, width);
};
// ─ 2. Editor component ──────────────────────────────────────────────
const edProto = Editor.prototype as any;
const origEdRender = edProto.render;
edProto.render = function (width: number) {
const lines: string[] = origEdRender.call(this, width);
return lines.map(wrap);
};
console.log(
`[markdown-body-color] Default markdown body color = ${FALLBACK_HEX}`,
`[markdown-body-color] Body color = ${FALLBACK_HEX} (Markdown + Editor)`,
);
}
+1 -1
View File
@@ -25,7 +25,7 @@
"steel": "#5a5f6a",
"gunmetal": "#3d4148",
"oldBronze": "#8b6914",
"warpGreen": "#7a8a42",
"warpGreen": "#5a6b2e",
"voidPurple": "#4a2d4d",
"cognitorPink": "#c75a8a",