Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ Capability discovery defaults to `explicit`, preserving the zero-resident OpenPI

OpenPI Web theme defaults to `system`; `light` and `dark` are explicit canonical setup choices, and the browser consumes them from each authoritative snapshot without writing a competing local preference.

On Windows, OpenPI enables Pi's `clearOnShrink` compatibility behavior for
the regular TUI so shrinking slash-command autocomplete lists do not leave
stale rows on screen. Fullscreen TUI keeps its configured behavior. The
native `PI_CLEAR_ON_SHRINK=0` setting remains an explicit opt-out.

Legacy `footerItems` is accepted and migrated at the input boundary, but new setup writes persist only canonical `footerLines`. Configurations written by this version are not guaranteed to retain their Footer layout when read by an older OpenPI version.

<!-- config-contract: capabilities.discovery suggestions.enabled suggestions.model workflows.concurrency workflows.maxAgentCalls ui.webTheme ui.showHeader ui.customFooter ui.footerStyle ui.footerLines ui.subagentResultDisplay ui.bashToolDisplay ui.fileMutationDisplay postEdit.command subagents.roleModels -->
Expand Down
86 changes: 86 additions & 0 deletions extensions/windows-terminal-compat/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type {
ExtensionAPI,
ExtensionContext,
} from "@earendil-works/pi-coding-agent";
import {
registerEditorLayer,
removeEditorLayer,
} from "../shared/editor-layers.ts";

export const WINDOWS_TERMINAL_COMPAT_LAYER = "windows-terminal-compat";

type CompatibleTui = {
mode: "regular" | "fullscreen";
getClearOnShrink(): boolean;
setClearOnShrink(enabled: boolean): void;
};

export function shouldClearShrunkRows(
platform: NodeJS.Platform,
mode: "regular" | "fullscreen",
enabled = true,
) {
return enabled && platform === "win32" && mode === "regular";
}

export function applyWindowsTerminalCompatibility(
tui: CompatibleTui,
platform: NodeJS.Platform,
enabled = true,
) {
if (
shouldClearShrunkRows(platform, tui.mode, enabled) &&
!tui.getClearOnShrink()
) {
tui.setClearOnShrink(true);
}
}

function install(ctx: ExtensionContext, pi: ExtensionAPI) {
if (ctx.mode !== "tui") {
return;
}

const windowsCompatEnabled =
process.platform === "win32" && process.env.PI_CLEAR_ON_SHRINK !== "0";
let currentTui: CompatibleTui | undefined;
let lastMode: CompatibleTui["mode"] | undefined;
const removeInputListener = ctx.ui.onTerminalInput(() => {
if (currentTui && currentTui.mode !== lastMode) {
lastMode = currentTui.mode;
applyWindowsTerminalCompatibility(
currentTui,
process.platform,
windowsCompatEnabled,
);
}
});
registerEditorLayer(pi, ctx, {
id: WINDOWS_TERMINAL_COMPAT_LAYER,
order: 100,
wrap: (base, tui) => {
currentTui = tui;
lastMode = tui.mode;
// The regular renderer otherwise leaves rows behind when autocomplete
// shrinks. This is a terminal redraw compatibility setting, not an
// editor replacement, so all existing input behavior remains intact.
applyWindowsTerminalCompatibility(
tui,
process.platform,
windowsCompatEnabled,
);
return base;
},
});
pi.on("session_shutdown", () => {
removeInputListener();
currentTui = undefined;
});
}

export default function windowsTerminalCompat(pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => install(ctx, pi));
pi.on("session_shutdown", () => {
removeEditorLayer(pi, WINDOWS_TERMINAL_COMPAT_LAYER);
});
}
127 changes: 127 additions & 0 deletions tests/extensions/windows-terminal-compat/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
applyWindowsTerminalCompatibility,
shouldClearShrunkRows,
} from "../../../extensions/windows-terminal-compat/index.ts";

test("enables shrink cleanup for the Windows regular renderer", () => {
assert.equal(shouldClearShrunkRows("win32", "regular"), true);

let enabled: boolean | undefined;
applyWindowsTerminalCompatibility(
{
mode: "regular",
getClearOnShrink() {
return false;
},
setClearOnShrink(value) {
enabled = value;
},
},
"win32",
);

assert.equal(enabled, true);
});

test("does not change non-Windows or fullscreen rendering", () => {
assert.equal(shouldClearShrunkRows("linux", "regular"), false);
assert.equal(shouldClearShrunkRows("win32", "fullscreen"), false);

let calls = 0;
const tui = {
mode: "fullscreen" as const,
getClearOnShrink() {
return false;
},
setClearOnShrink() {
calls += 1;
},
};

applyWindowsTerminalCompatibility(tui, "win32");
applyWindowsTerminalCompatibility({ ...tui, mode: "regular" }, "linux");

assert.equal(calls, 0);
});

test("supports an explicit environment opt-out", () => {
assert.equal(shouldClearShrunkRows("win32", "regular", false), false);

let calls = 0;
applyWindowsTerminalCompatibility(
{
mode: "regular",
getClearOnShrink() {
return false;
},
setClearOnShrink() {
calls += 1;
},
},
"win32",
false,
);

assert.equal(calls, 0);
});

test("preserves an explicit native false renderer setting", () => {
let calls = 0;
applyWindowsTerminalCompatibility(
{
mode: "regular",
getClearOnShrink() {
return false;
},
setClearOnShrink() {
calls += 1;
},
},
"win32",
false,
);
assert.equal(calls, 0);
});

test("reapplies compatibility after renderer replacement", () => {
let listener: ((data: string) => unknown) | undefined;
const remove = (value: (data: string) => unknown) => {
listener = value;
return () => {
listener = undefined;
};
};
let oldClearOnShrink = false;
const oldRenderer = {
mode: "fullscreen" as const,
getClearOnShrink: () => oldClearOnShrink,
setClearOnShrink: (value: boolean) => {
oldClearOnShrink = value;
},
};
let newClearOnShrink = false;
const newRenderer = {
mode: "regular" as const,
getClearOnShrink: () => newClearOnShrink,
setClearOnShrink: (value: boolean) => {
newClearOnShrink = value;
},
};
let current: {
mode: "regular" | "fullscreen";
getClearOnShrink: () => boolean;
setClearOnShrink: (value: boolean) => void;
} = oldRenderer;
const unsubscribe = remove(() => {
applyWindowsTerminalCompatibility(current, "win32");
});
listener?.("input");
assert.equal(oldClearOnShrink, false);
current = newRenderer;
listener?.("input");
assert.equal(newClearOnShrink, true);
unsubscribe();
assert.equal(listener, undefined);
});
Loading