Commit 39d86d850029

Vincent Demeester <vincent@sbr.pm>
2026-02-18 13:55:57
feat(dots): move mode label from editor border to footer
Removed mode label rendering from the editor top border (was inconsistently visible). Mode name now shows in the footer via setStatus, displayed as [mode-name] when not in default mode. Hidden when mode is 'default'.
1 parent 866d3a9
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/cwd-history.ts
@@ -353,10 +353,6 @@ function getModeBorderColor(ctx: ExtensionContext, pi: ExtensionAPI, mode: strin
 	return theme.getThinkingBorderColor(pi.getThinkingLevel());
 }
 
-function formatModeLabel(mode: string): string {
-	return mode;
-}
-
 async function resolveModesPath(cwd: string): Promise<string> {
 	const projectPath = getProjectModesPath(cwd);
 	if (await fileExists(projectPath)) return projectPath;
@@ -451,6 +447,17 @@ const runtime: ModeRuntime = {
 // Updated by setEditor() when the custom editor is instantiated.
 let requestEditorRender: (() => void) | undefined;
 
+// Update the mode status in the footer via ctx.ui.setStatus
+function updateModeStatus(ctx: ExtensionContext): void {
+	if (!ctx.hasUI) return;
+	const mode = runtime.currentMode;
+	if (mode && mode !== "default") {
+		ctx.ui.setStatus("mode", ctx.ui.theme.fg("dim", `[${mode}]`));
+	} else {
+		ctx.ui.setStatus("mode", undefined);
+	}
+}
+
 async function ensureRuntime(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
 	const filePath = await resolveModesPath(ctx.cwd);
 
@@ -549,7 +556,10 @@ async function applyMode(pi: ExtensionAPI, ctx: ExtensionContext, mode: string):
 	if (mode === CUSTOM_MODE_NAME) {
 		runtime.currentMode = CUSTOM_MODE_NAME;
 		customOverlay = getCurrentSelectionSpec(pi, ctx);
-		if (ctx.hasUI) requestEditorRender?.();
+		if (ctx.hasUI) {
+			updateModeStatus(ctx);
+			requestEditorRender?.();
+		}
 		return;
 	}
 
@@ -602,6 +612,7 @@ async function applyMode(pi: ExtensionAPI, ctx: ExtensionContext, mode: string):
 	}
 
 	if (ctx.hasUI) {
+		updateModeStatus(ctx);
 		requestEditorRender?.();
 	}
 }
@@ -807,6 +818,7 @@ async function editModeUI(pi: ExtensionAPI, ctx: ExtensionContext, mode: string)
 			if (runtime.lastRealMode === modeName) {
 				runtime.lastRealMode = "default";
 			}
+			updateModeStatus(ctx);
 			requestEditorRender?.();
 			ctx.ui.notify(`Deleted mode \"${modeName}\"`, "info");
 			return;
@@ -851,6 +863,7 @@ async function renameModeUI(pi: ExtensionAPI, ctx: ExtensionContext, oldName: st
 
 		if (runtime.currentMode === oldName) runtime.currentMode = newName;
 		if (runtime.lastRealMode === oldName) runtime.lastRealMode = newName;
+		updateModeStatus(ctx);
 		requestEditorRender?.();
 
 		ctx.ui.notify(`Renamed \"${oldName}\" → \"${newName}\"`, "info");
@@ -927,12 +940,6 @@ interface PromptEntry {
 }
 
 class PromptEditor extends CustomEditor {
-	public modeLabelProvider?: () => string;
-	/**
-	 * Color function for the mode label. If unset, the label inherits the border color.
-	 * We use this to keep the label consistent (e.g. same as the footer/status bar).
-	 */
-	public modeLabelColor?: (text: string) => string;
 	private lockedBorder = false;
 	private _borderColor?: (text: string) => string;
 
@@ -958,41 +965,6 @@ class PromptEditor extends CustomEditor {
 		this.lockedBorder = true;
 	}
 
-	render(width: number): string[] {
-		const lines = super.render(width);
-		const mode = this.modeLabelProvider?.();
-		if (!mode) return lines;
-
-		const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
-		const topPlain = stripAnsi(lines[0] ?? "");
-
-		// If the editor is scrolled, the built-in editor renders a scroll indicator on the top border.
-		// Preserve it, but still show the mode label.
-		const scrollPrefixMatch = topPlain.match(/^(─── ↑ \d+ more )/);
-		const prefix = scrollPrefixMatch?.[1] ?? "──";
-
-		let label = formatModeLabel(mode);
-
-		// Compute how much room we have for the label core (without truncating the prefix).
-		const labelLeftSpace = prefix.endsWith(" ") ? "" : " ";
-		const labelRightSpace = " ";
-		const minRightBorder = 1; // keep at least one border cell on the right
-		const maxLabelLen = Math.max(0, width - prefix.length - labelLeftSpace.length - labelRightSpace.length - minRightBorder);
-		if (maxLabelLen <= 0) return lines;
-		if (label.length > maxLabelLen) label = label.slice(0, maxLabelLen);
-
-		const labelChunk = `${labelLeftSpace}${label}${labelRightSpace}`;
-
-		const remaining = width - prefix.length - labelChunk.length;
-		if (remaining < 0) return lines;
-
-		const right = "─".repeat(Math.max(0, remaining));
-
-		const labelColor = this.modeLabelColor ?? ((text: string) => this.borderColor(text));
-		lines[0] = this.borderColor(prefix) + labelColor(labelChunk) + this.borderColor(right);
-		return lines;
-	}
-
 	public requestRenderNow(): void {
 		this.tui.requestRender();
 	}
@@ -1146,9 +1118,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();
-		editor.modeLabelProvider = () => runtime.currentMode;
-		// Keep the mode label color stable (match footer/status bar).
-		editor.modeLabelColor = (text: string) => ctx.ui.theme.fg("dim", text);
 		const borderColor = (text: string) => {
 			const isBashMode = editor.getText().trimStart().startsWith("!");
 			if (isBashMode) {
@@ -1264,6 +1233,7 @@ export default function (pi: ExtensionAPI) {
 			customOverlay = getCurrentSelectionSpec(pi, ctx);
 		}
 
+		updateModeStatus(ctx);
 		applyEditor(pi, ctx);
 	});
 
@@ -1281,6 +1251,7 @@ export default function (pi: ExtensionAPI) {
 			customOverlay = getCurrentSelectionSpec(pi, ctx);
 		}
 
+		updateModeStatus(ctx);
 		applyEditor(pi, ctx);
 	});
 
@@ -1308,6 +1279,7 @@ export default function (pi: ExtensionAPI) {
 
 		// Do not persist/select custom.
 		if (ctx.hasUI) {
+			updateModeStatus(ctx);
 			requestEditorRender?.();
 		}
 	});