Commit bd8ee0395c16

Vincent Demeester <vincent@sbr.pm>
2026-02-18 14:49:11
fix(dots): patch Editor.prototype.render for mode border colors
Replaced per-instance borderColor locking with a prototype-level render patch. Multiple extensions (file-picker, shell-completions) call setEditorComponent, so the last one wins and our editor was never rendered. Patching Editor.prototype.render ensures all editors pick up the current mode's border color at render time.
1 parent 9d7b368
Changed files (1)
dots
pi
agent
dots/pi/agent/extensions/prompt-editor.ts
@@ -343,28 +343,39 @@ function hexToAnsi(hex: string): string {
 	return `\x1b[38;2;${r};${g};${b}m`;
 }
 
-function getModeBorderColor(ctx: ExtensionContext, pi: ExtensionAPI, mode: string): (text: string) => string {
-	const theme = ctx.ui.theme;
-	const spec = runtime.data.modes[mode];
+/**
+ * Patch Editor.prototype.render so that every editor instance (file-picker,
+ * shell-completions, default, etc.) picks up the current mode's border color
+ * at render time. This avoids fighting with setEditorComponent ordering and
+ * pi's updateEditorBorderColor which we can't trigger from the extension API.
+ *
+ * On each render call, if the current mode has a custom color, we temporarily
+ * swap the editor's borderColor, call the original render, then restore it.
+ */
+let editorPatchInstalled = false;
+function installEditorBorderPatch(): void {
+	if (editorPatchInstalled) return;
 
-	// Hex color from modes.json
-	if (spec?.color && /^#[0-9a-fA-F]{6}$/.test(spec.color)) {
-		const ansi = hexToAnsi(spec.color);
-		return (text: string) => `${ansi}${text}\x1b[39m`;
-	}
+	// Get Editor.prototype via the CustomEditor import
+	const EditorProto = Object.getPrototypeOf(CustomEditor.prototype);
+	if (!EditorProto || typeof EditorProto.render !== "function") return;
 
-	// Theme token fallback
-	if (spec?.color) {
-		try {
-			theme.getFgAnsi(spec.color as any);
-			return (text: string) => theme.fg(spec.color as any, text);
-		} catch {
-			// fall through to thinking-based colors
+	const originalRender = EditorProto.render as (this: { borderColor: (text: string) => string }, width: number) => string[];
+
+	EditorProto.render = function patchedRender(this: { borderColor: (text: string) => string }, width: number): string[] {
+		const spec = runtime.data.modes[runtime.currentMode];
+		if (spec?.color && /^#[0-9a-fA-F]{6}$/.test(spec.color)) {
+			const ansi = hexToAnsi(spec.color);
+			const saved = this.borderColor;
+			this.borderColor = (text: string) => `${ansi}${text}\x1b[39m`;
+			const result = originalRender.call(this, width);
+			this.borderColor = saved;
+			return result;
 		}
-	}
+		return originalRender.call(this, width);
+	};
 
-	// Default: derive from the current thinking level.
-	return theme.getThinkingBorderColor(pi.getThinkingLevel());
+	editorPatchInstalled = true;
 }
 
 async function resolveModesPath(cwd: string): Promise<string> {
@@ -958,31 +969,6 @@ interface PromptEntry {
 }
 
 class PromptEditor extends CustomEditor {
-	private lockedBorder = false;
-	private _borderColor?: (text: string) => string;
-
-	constructor(
-		tui: ConstructorParameters<typeof CustomEditor>[0],
-		theme: ConstructorParameters<typeof CustomEditor>[1],
-		keybindings: ConstructorParameters<typeof CustomEditor>[2],
-	) {
-		super(tui, theme, keybindings);
-		delete (this as { borderColor?: (text: string) => string }).borderColor;
-		Object.defineProperty(this, "borderColor", {
-			get: () => this._borderColor ?? ((text: string) => text),
-			set: (value: (text: string) => string) => {
-				if (this.lockedBorder) return;
-				this._borderColor = value;
-			},
-			configurable: true,
-			enumerable: true,
-		});
-	}
-
-	lockBorderColor() {
-		this.lockedBorder = true;
-	}
-
 	public requestRenderNow(): void {
 		this.tui.requestRender();
 	}
@@ -1136,16 +1122,6 @@ function setEditor(pi: ExtensionAPI, ctx: ExtensionContext, history: PromptEntry
 	ctx.ui.setEditorComponent((tui, theme, keybindings) => {
 		const editor = new PromptEditor(tui, theme, keybindings);
 		requestEditorRender = () => editor.requestRenderNow();
-		const borderColor = (text: string) => {
-			const isBashMode = editor.getText().trimStart().startsWith("!");
-			if (isBashMode) {
-				return ctx.ui.theme.getBashModeBorderColor()(text);
-			}
-			return getModeBorderColor(ctx, pi, runtime.currentMode)(text);
-		};
-
-		editor.borderColor = borderColor;
-		editor.lockBorderColor();
 		for (const prompt of history) {
 			editor.addToHistory?.(prompt.text);
 		}
@@ -1240,6 +1216,7 @@ export default function (pi: ExtensionAPI) {
 		lastObservedModel = { provider: ctx.model?.provider, modelId: ctx.model?.id };
 		await ensureRuntime(pi, ctx);
 		customOverlay = null;
+		installEditorBorderPatch();
 
 		const inferred = inferModeFromSelection(ctx, pi, runtime.data);
 		if (inferred) {