import { CustomEditor, type ExtensionAPI, type ExtensionContext, type KeybindingsManager, } from "@earendil-works/pi-coding-agent"; import type { EditorTheme, TUI } from "@earendil-works/pi-tui"; import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; import { findBottomBorderIndex, isEditorBorderLine, stripAnsi } from "./utils.ts"; function fillLine(content: string, width: number): string { const truncated = truncateToWidth(content, Math.max(0, width), ""); const pad = " ".repeat(Math.max(0, width - visibleWidth(truncated))); return `${truncated}${pad}`; } function roundedBorder( width: number, kind: "top" | "bottom", paint: (s: string) => string, sourceLine?: string, ): string { if (width < 2) return paint(truncateToWidth(kind === "top" ? "╭╮" : "╰╯", width, "")); const corners = kind === "top" ? (["╭", "╮"] as const) : (["╰", "╯"] as const); if (sourceLine) { const plain = stripAnsi(sourceLine); const scrollMatch = plain.match(/([↑↓]\s+\d+\s+more)/); if (scrollMatch) { const label = `─── ${scrollMatch[1]} `; const fill = Math.max(0, width - 2 - visibleWidth(label)); return paint(`${corners[0]}${label}${"─".repeat(fill)}${corners[1]}`); } } return paint(`${corners[0]}${"─".repeat(Math.max(0, width - 2))}${corners[1]}`); } export class OpenTuiEditor extends CustomEditor { private readonly getRail: () => string; private readonly getBorder: (s: string) => string; constructor( tui: TUI, editorTheme: EditorTheme, keybindings: KeybindingsManager, ) { super(tui, editorTheme, keybindings, { paddingX: 0 }); // ponytail: route the frame through this.borderColor so Pi can recolor it // via updateEditorBorderColor() — bash mode ("! " prefix → green) and // thinking-level borders both flow through this one property. this.getRail = () => this.borderColor("│"); this.getBorder = (s: string) => this.borderColor(s); } override setPaddingX(_padding: number): void { // The custom rail owns the horizontal inset and keeps one stable text gap. super.setPaddingX(0); } render(width: number): string[] { if (width < 4) return super.render(width); const rail = this.getRail(); const borderPaint = this.getBorder; // ponytail: 1-char rail + 1-char gap on each side = 4 chars of chrome. const innerWidth = Math.max(0, width - 4); const baseLines = super.render(innerWidth); const bottomIdx = findBottomBorderIndex(baseLines); const result: string[] = []; result.push(roundedBorder(width, "top", borderPaint, baseLines[0])); for (let i = 1; i < bottomIdx; i++) { const line = baseLines[i] ?? ""; if (isEditorBorderLine(line)) { result.push(`${rail} ${fillLine("", innerWidth)} ${rail}`); } else { result.push(`${rail} ${fillLine(line, innerWidth)} ${rail}`); } } result.push(roundedBorder(width, "bottom", borderPaint, baseLines[bottomIdx])); for (let i = bottomIdx + 1; i < baseLines.length; i++) { result.push(baseLines[i]!); } return result.map((line) => truncateToWidth(line, width, "")); } } export function installEditor(_pi: ExtensionAPI, ctx: ExtensionContext): () => void { ctx.ui.setEditorComponent((tui, editorTheme, keybindings) => new OpenTuiEditor(tui, editorTheme, keybindings), ); return () => { ctx.ui.setEditorComponent(undefined); }; }