Files
pi-extensions/markdown-body-color.ts
T
shahondin1624 f1ceeb4363 Refactor pass: shared utils, non-blocking discovery, safer monkey-patches
Audit produced five concrete improvements:

1) New shared/ module (zero-dep pure utilities)
   - shared/ansi.ts: hexToRgb (throws on malformed input instead of
     silently producing NaN), fgFromHex, stripAnsi, visibleWidth,
     ANSI_RESET_FG / ANSI_RESET_ALL constants.
   - shared/format.ts: formatTokens, formatElapsed.
   - shared/ctx.ts: safely() and safelyAsync() helpers for dealing with
     pi's "stale after session replacement or reload" ExtensionRunner
     semantics.

   Removes duplicate helpers from mechanicus-footer, markdown-body-color,
   dark-mechanicus-indicator.

2) ai-server: non-blocking startup + short-race timeout
   - Factory registers STATIC_MODELS immediately so pi startup isn't
     blocked on the HTTPS round-trip.
   - Races discoverModels() against a 300ms timeout. On LAN (~40ms) the
     live list wins and pi --list-models sees the real models. Past the
     timeout, fallback remains and background discovery updates the
     provider later.
   - listModelsCached() with 5s TTL for tab completions (was firing a
     round-trip on every keystroke).
   - loadModel/unloadModel invalidate the cache.

3) dark-mechanicus-indicator: stale-ctx guard
   - Wrap the setInterval ticker body in safely() so a race between
     session_shutdown and the ticker can't crash node. Same pattern as
     the earlier footer fix.

4) Safer monkey-patches in markdown-body-color and mechanicus-thinking-label
   - Feature-detect Markdown/Editor/AssistantMessageComponent's target
     method before patching. Warn-and-skip rather than silently create
     a broken prototype if a pi-tui upgrade renames the internal method.

5) Minor
   - Replaced five `as any` casts with typed Record<string, unknown>
     access in the monkey-patch sites.
   - ai-server debug log only fires when actual discovery succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 00:05:18 +02:00

85 lines
3.3 KiB
TypeScript

/**
* 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).
*
* Two patches:
*
* 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 { Editor, Markdown } from "@mariozechner/pi-tui";
import { ANSI_RESET_FG, fgFromHex } from "./shared/ansi.js";
const FALLBACK_HEX = process.env.PI_MARKDOWN_BODY_COLOR ?? "#d4c5e8";
const OPEN = fgFromHex(FALLBACK_HEX);
const CLOSE = ANSI_RESET_FG;
// 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;
export default function (_pi: ExtensionAPI) {
if (patched) return;
patched = true;
// ─ 1. Markdown component ────────────────────────────────────────────
const mdProto = Markdown.prototype as Record<string, unknown>;
const origMdRender = mdProto.render as ((width: number) => string[]) | undefined;
if (typeof origMdRender !== "function") {
console.warn(
"[markdown-body-color] pi-tui Markdown.prototype.render is missing; skipping patch. (pi-tui upgrade?)",
);
return;
}
mdProto.render = function (this: any, width: number) {
if (!this.defaultTextStyle) {
this.defaultTextStyle = { color: wrap };
if (typeof this.invalidate === "function") this.invalidate();
}
return origMdRender.call(this, width);
};
// ─ 2. Editor component ──────────────────────────────────────────────
const edProto = Editor.prototype as Record<string, unknown>;
const origEdRender = edProto.render as ((width: number) => string[]) | undefined;
if (typeof origEdRender !== "function") {
console.warn(
"[markdown-body-color] pi-tui Editor.prototype.render is missing; skipping Editor patch.",
);
return;
}
edProto.render = function (this: any, width: number) {
const lines: string[] = origEdRender.call(this, width);
return lines.map(wrap);
};
if (process.env.PI_DEBUG) {
console.log(
`[markdown-body-color] Body color = ${FALLBACK_HEX} (Markdown + Editor)`,
);
}
}