From 441a4677c9ee2b38f75f9d37e72b3debe7b948b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=94=E8=B7=91=E7=9A=84=E9=91=AB?= <3089729486@qq.com> Date: Sat, 5 Sep 2026 20:43:26 +0800 Subject: [PATCH 1/2] fix(windows): avoid stale TUI autocomplete rows --- README.md | 3 + SETUP.md | 22 ++ extensions/windows-tui-compatibility/index.ts | 133 ++++++++++++ .../windows-tui-compatibility/index.test.ts | 189 ++++++++++++++++++ 4 files changed, 347 insertions(+) create mode 100644 extensions/windows-tui-compatibility/index.ts create mode 100644 tests/extensions/windows-tui-compatibility/index.test.ts diff --git a/README.md b/README.md index c4435940..20ff848a 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,9 @@ Skill 正文通过原生用户消息或工具结果进入正常 Session 历史 > [!IMPORTANT] > 默认安装是安静的:不改主题、不绑定 Provider 或模型、不开启下一步预测,也不执行 post-edit 命令。Capability discovery 默认 `explicit`;只有用户通过 `/openpi-setup` 选择 `adaptive` 后,模型才会常驻看到一个小型发现网关并可自主加载额外能力。 +> [!TIP] +> Windows 用户如果在输入 `/lo` 等斜杠命令时看到旧的补全行残留,OpenPI 会在没有明确 TUI 配置时为下一次启动选择 `fullscreen`,避免 Pi `regular` TUI 的旧补全行残留;如果你明确选择 `regular`,OpenPI 会保留该选择并启用尽量安全的收缩清理。 + ```text /openpi-setup ``` diff --git a/SETUP.md b/SETUP.md index 0d03718d..ac85a6d1 100644 --- a/SETUP.md +++ b/SETUP.md @@ -14,6 +14,28 @@ pi install git:github.com/openpi-dev/openpi Pi installs the package dependencies automatically. Restart Pi or run `/reload` after installation. +### Windows terminal compatibility + +On Windows, OpenPI detects the stale autocomplete-row redraw problem in Pi's +`regular` (main-screen) renderer. When neither global nor project settings +explicitly selects a TUI mode, it saves `fullscreen` as the safe default for +the next start and shows a one-time restart notice. This prevents old +slash-command autocomplete rows from making commands look duplicated. + +If you explicitly select `regular`, OpenPI preserves that choice and enables +Pi's supported `clear-on-shrink` behavior as a best-effort mitigation. + +If you prefer the alternate-screen renderer, Pi's `fullscreen` mode remains +available from `/settings` or `settings.json`: + +```json +{ + "tuiMode": "fullscreen" +} +``` + +The compatibility behavior is tracked in [openpi#407](https://github.com/openpi-dev/openpi/issues/407). + ## fd, rg, and read-only git tools The `file-search` extension registers `fd` and `rg` as model tools, and `git-read` registers `git_show`, `git_diff`, and `git_log` (read-only git inspection). They stay outside an ordinary parent turn until the user explicitly asks to use `fd`/`rg`/git history, or structured file search, or the model loads the `search` group through `openpi_load_tools`. Entering or restoring Plan Mode is a runtime-safety exception: it loads `search` for that Session so diff investigation can use the structured Git boundary. The gateway is shown after an explicit OpenPI-capability request, or remains visible when the user opts into adaptive discovery; child sessions may still receive these tools through the reviewed child-safe allowlist (the read-only git tools let reviewer/advisor subagents inspect diffs, which a bash-free tool boundary otherwise excludes). No setup is normally needed: at startup `fd`/`rg` silently use a system-installed binary (`fd`/`fdfind` and `rg`) when available, or an existing binary in the agent's private managed bin directory (`~/.pi/agent/bin`). Only when neither exists does it download an official release binary (macOS/Linux, arm64/x64, over HTTPS) into that directory — a persistent cache that survives package updates — and show a one-time notification. If your platform is unsupported, install `fd` and `rg` with your package manager and restart Pi. The git tools require a system `git`. diff --git a/extensions/windows-tui-compatibility/index.ts b/extensions/windows-tui-compatibility/index.ts new file mode 100644 index 00000000..b46bc44a --- /dev/null +++ b/extensions/windows-tui-compatibility/index.ts @@ -0,0 +1,133 @@ +import type { + ExtensionAPI, + ExtensionContext, +} from "@earendil-works/pi-coding-agent"; + +type TuiMode = "regular" | "fullscreen"; + +interface TuiSettingsManager { + getGlobalSettings(): { tuiMode?: TuiMode }; + getProjectSettings(): { tuiMode?: TuiMode }; + setTuiMode?(mode: TuiMode): void; + flush?(): Promise; +} + +type TuiSettingsManagerFactory = ( + cwd: string, +) => TuiSettingsManager | Promise; + +const WIDGET_KEY = "openpi-windows-tui-compatibility"; + +/** + * The main-screen renderer can leave stale autocomplete rows on Windows. + * Keep the workaround limited to interactive Windows sessions so RPC/print + * users and non-Windows terminals are unaffected. + */ +export function shouldInstallWindowsTuiCompatibility( + platform: NodeJS.Platform, + mode: ExtensionContext["mode"], +) { + return platform === "win32" && mode === "tui"; +} + +export function shouldPreferWindowsFullscreen(options: { + platform: NodeJS.Platform; + mode: ExtensionContext["mode"]; + globalTuiMode?: TuiMode; + projectTuiMode?: TuiMode; + explicitCliTuiMode?: boolean; +}) { + return ( + shouldInstallWindowsTuiCompatibility(options.platform, options.mode) && + options.globalTuiMode === undefined && + options.projectTuiMode === undefined && + options.explicitCliTuiMode !== true + ); +} + +/** + * Register the Windows renderer workaround. + * + * Pi exposes the renderer to widget factories, but not as a direct property + * on ExtensionContext. The zero-height widget lets us apply the supported + * renderer setting without replacing OpenPI's header, footer, or editor. + */ +export function registerWindowsTuiCompatibility( + pi: ExtensionAPI, + platform: NodeJS.Platform, + settingsManagerFactory?: TuiSettingsManagerFactory, +) { + let activeUi: ExtensionContext["ui"] | undefined; + + const cleanup = () => { + const ui = activeUi; + activeUi = undefined; + try { + ui?.setWidget(WIDGET_KEY, undefined); + } catch { + // The renderer may already be gone during shutdown. + } + }; + + pi.on("session_start", async (_event, ctx) => { + cleanup(); + if (!shouldInstallWindowsTuiCompatibility(platform, ctx.mode)) return; + + activeUi = ctx.ui; + ctx.ui.setWidget( + WIDGET_KEY, + (tui) => { + // The renderer can be replaced at runtime when the user switches TUI + // modes, so apply this when the factory receives the active renderer. + if (tui.mode === "regular") tui.setClearOnShrink(true); + return { + render: () => [], + invalidate() {}, + }; + }, + { placement: "belowEditor" }, + ); + + if (!settingsManagerFactory) return; + + try { + const settingsManager = await settingsManagerFactory(ctx.cwd); + const globalSettings = settingsManager.getGlobalSettings(); + const projectSettings = settingsManager.getProjectSettings(); + const explicitCliTuiMode = process.argv.some( + (arg) => arg === "--tui-mode" || arg.startsWith("--tui-mode="), + ); + + if ( + !shouldPreferWindowsFullscreen({ + platform, + mode: ctx.mode, + globalTuiMode: globalSettings.tuiMode, + projectTuiMode: projectSettings.tuiMode, + explicitCliTuiMode, + }) || + settingsManager.setTuiMode === undefined + ) { + return; + } + + settingsManager.setTuiMode("fullscreen"); + await settingsManager.flush?.(); + ctx.ui.notify( + "OpenPI detected the Windows regular-TUI redraw issue and selected fullscreen mode for the next start. Restart Pi to apply it.", + "warning", + ); + } catch { + // A settings write must never prevent the OpenPI session from starting. + } + }); + + pi.on("session_shutdown", cleanup); +} + +export default function windowsTuiCompatibility(pi: ExtensionAPI) { + registerWindowsTuiCompatibility(pi, process.platform, async (cwd) => { + const { SettingsManager } = await import("@earendil-works/pi-coding-agent"); + return SettingsManager.create(cwd); + }); +} diff --git a/tests/extensions/windows-tui-compatibility/index.test.ts b/tests/extensions/windows-tui-compatibility/index.test.ts new file mode 100644 index 00000000..06779188 --- /dev/null +++ b/tests/extensions/windows-tui-compatibility/index.test.ts @@ -0,0 +1,189 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import type { + ExtensionAPI, + ExtensionContext, +} from "@earendil-works/pi-coding-agent"; +import type { Component, TUI } from "@earendil-works/pi-tui"; +import { + registerWindowsTuiCompatibility, + shouldInstallWindowsTuiCompatibility, + shouldPreferWindowsFullscreen, +} from "../../../extensions/windows-tui-compatibility/index.ts"; + +type WidgetFactory = ( + tui: TUI, + theme: unknown, +) => Component & { dispose?(): void }; + +function createHarness( + platform: NodeJS.Platform, + mode: ExtensionContext["mode"] = "tui", + settingsManagerFactory?: Parameters< + typeof registerWindowsTuiCompatibility + >[2], +) { + const hooks = new Map< + string, + (event: unknown, ctx: ExtensionContext) => unknown + >(); + let widgetFactory: WidgetFactory | undefined; + let widgetCleared = false; + const notifications: string[] = []; + + const pi = { + on(event: string, handler: unknown) { + hooks.set( + event, + handler as (event: unknown, ctx: ExtensionContext) => unknown, + ); + }, + } as unknown as ExtensionAPI; + + const ctx = { + cwd: "C:\\project", + mode, + hasUI: mode === "tui", + ui: { + setWidget(_key: string, content: WidgetFactory | undefined) { + if (content) widgetFactory = content; + else { + widgetFactory = undefined; + widgetCleared = true; + } + }, + notify(message: string) { + notifications.push(message); + }, + }, + } as unknown as ExtensionContext; + + registerWindowsTuiCompatibility(pi, platform, settingsManagerFactory); + + return { + ctx, + emit(event: string) { + return hooks.get(event)?.({}, ctx); + }, + mount(tui: TUI) { + return widgetFactory?.(tui, {}); + }, + get widgetFactory() { + return widgetFactory; + }, + get widgetCleared() { + return widgetCleared; + }, + get notifications() { + return notifications; + }, + }; +} + +test("installs only for interactive Windows sessions", () => { + assert.equal(shouldInstallWindowsTuiCompatibility("win32", "tui"), true); + assert.equal(shouldInstallWindowsTuiCompatibility("linux", "tui"), false); + assert.equal(shouldInstallWindowsTuiCompatibility("win32", "rpc"), false); + + assert.equal( + shouldPreferWindowsFullscreen({ platform: "win32", mode: "tui" }), + true, + ); + assert.equal( + shouldPreferWindowsFullscreen({ + platform: "win32", + mode: "tui", + globalTuiMode: "regular", + }), + false, + ); + assert.equal( + shouldPreferWindowsFullscreen({ + platform: "win32", + mode: "tui", + projectTuiMode: "fullscreen", + }), + false, + ); + assert.equal( + shouldPreferWindowsFullscreen({ + platform: "win32", + mode: "tui", + explicitCliTuiMode: true, + }), + false, + ); + + const linux = createHarness("linux"); + linux.emit("session_start"); + assert.equal(linux.widgetFactory, undefined); +}); + +test("enables clear-on-shrink for regular TUI but not fullscreen", () => { + const harness = createHarness("win32"); + harness.emit("session_start"); + + const clearOnShrink: boolean[] = []; + harness.mount({ + mode: "regular", + setClearOnShrink(enabled: boolean) { + clearOnShrink.push(enabled); + }, + requestRender(force?: boolean) { + assert.equal(force, undefined); + }, + } as TUI); + assert.deepEqual(clearOnShrink, [true]); + + clearOnShrink.length = 0; + harness.mount({ + mode: "fullscreen", + setClearOnShrink(enabled: boolean) { + clearOnShrink.push(enabled); + }, + requestRender(force?: boolean) { + assert.equal(force, undefined); + }, + } as TUI); + assert.deepEqual(clearOnShrink, []); +}); + +test("persists fullscreen only when no TUI mode is configured", async () => { + let selectedMode: string | undefined; + let flushCount = 0; + const unset = createHarness("win32", "tui", async () => ({ + getGlobalSettings: () => ({}), + getProjectSettings: () => ({}), + setTuiMode: (mode: "regular" | "fullscreen") => { + selectedMode = mode; + }, + flush: async () => { + flushCount += 1; + }, + })); + await unset.emit("session_start"); + assert.equal(selectedMode, "fullscreen"); + assert.equal(flushCount, 1); + assert.match(unset.notifications[0] ?? "", /Restart Pi/); + + const explicit = createHarness("win32", "tui", async () => ({ + getGlobalSettings: () => ({ tuiMode: "regular" as const }), + getProjectSettings: () => ({}), + setTuiMode: () => { + throw new Error("must not override an explicit mode"); + }, + })); + await explicit.emit("session_start"); + assert.deepEqual(explicit.notifications, []); +}); + +test("cleans up the compatibility widget on shutdown", () => { + const harness = createHarness("win32"); + harness.emit("session_start"); + assert.ok(harness.widgetFactory); + + harness.emit("session_shutdown"); + + assert.equal(harness.widgetFactory, undefined); + assert.equal(harness.widgetCleared, true); +}); From ddc7066685e812d5eeb6eab44deaf4fe08982ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=94=E8=B7=91=E7=9A=84=E9=91=AB?= <3089729486@qq.com> Date: Sun, 6 Sep 2026 10:19:27 +0800 Subject: [PATCH 2/2] fix(windows): keep tui workaround session-local --- README.md | 2 +- SETUP.md | 21 +++-- extensions/windows-tui-compatibility/index.ts | 94 ++++++++++--------- .../windows-tui-compatibility/index.test.ts | 82 +++++++++------- 4 files changed, 110 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 20ff848a..09749d35 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Skill 正文通过原生用户消息或工具结果进入正常 Session 历史 > 默认安装是安静的:不改主题、不绑定 Provider 或模型、不开启下一步预测,也不执行 post-edit 命令。Capability discovery 默认 `explicit`;只有用户通过 `/openpi-setup` 选择 `adaptive` 后,模型才会常驻看到一个小型发现网关并可自主加载额外能力。 > [!TIP] -> Windows 用户如果在输入 `/lo` 等斜杠命令时看到旧的补全行残留,OpenPI 会在没有明确 TUI 配置时为下一次启动选择 `fullscreen`,避免 Pi `regular` TUI 的旧补全行残留;如果你明确选择 `regular`,OpenPI 会保留该选择并启用尽量安全的收缩清理。 +> Windows 用户如果在输入 `/lo` 等斜杠命令时看到旧的补全行残留,OpenPI 会在当前交互式会话中为未显式配置 `terminal.clearOnShrink` 的 `regular` TUI 启用收缩清理。该兜底不会修改 Pi 的全局或项目偏好;显式的 `terminal.clearOnShrink` 值会被保留。需要永久选择 TUI 模式时,请使用 Pi 原生 `/settings`、`settings.json` 或 `--tui-mode`。 ```text /openpi-setup diff --git a/SETUP.md b/SETUP.md index ac85a6d1..eeb32e8c 100644 --- a/SETUP.md +++ b/SETUP.md @@ -18,15 +18,18 @@ Pi installs the package dependencies automatically. Restart Pi or run `/reload` On Windows, OpenPI detects the stale autocomplete-row redraw problem in Pi's `regular` (main-screen) renderer. When neither global nor project settings -explicitly selects a TUI mode, it saves `fullscreen` as the safe default for -the next start and shows a one-time restart notice. This prevents old -slash-command autocomplete rows from making commands look duplicated. - -If you explicitly select `regular`, OpenPI preserves that choice and enables -Pi's supported `clear-on-shrink` behavior as a best-effort mitigation. - -If you prefer the alternate-screen renderer, Pi's `fullscreen` mode remains -available from `/settings` or `settings.json`: +explicitly sets `terminal.clearOnShrink`, it enables Pi's supported +`clear-on-shrink` behavior for the current renderer only. This prevents old +slash-command autocomplete rows from making commands look duplicated without +changing a global or project preference. + +An explicit `terminal.clearOnShrink` value is always preserved, including +`false`. OpenPI also leaves the selected `tuiMode` unchanged; use Pi's native +`/settings`, `settings.json`, or the `--tui-mode` flag when you want to choose +that mode explicitly. + +For example, Pi's alternate-screen renderer remains available from `/settings` +or `settings.json`: ```json { diff --git a/extensions/windows-tui-compatibility/index.ts b/extensions/windows-tui-compatibility/index.ts index b46bc44a..ad040d6d 100644 --- a/extensions/windows-tui-compatibility/index.ts +++ b/extensions/windows-tui-compatibility/index.ts @@ -3,13 +3,16 @@ import type { ExtensionContext, } from "@earendil-works/pi-coding-agent"; -type TuiMode = "regular" | "fullscreen"; +interface TuiSettings { + terminal?: { + clearOnShrink?: boolean; + }; +} interface TuiSettingsManager { - getGlobalSettings(): { tuiMode?: TuiMode }; - getProjectSettings(): { tuiMode?: TuiMode }; - setTuiMode?(mode: TuiMode): void; - flush?(): Promise; + getGlobalSettings(): TuiSettings; + getProjectSettings(): TuiSettings; + drainErrors?(): readonly unknown[]; } type TuiSettingsManagerFactory = ( @@ -30,27 +33,39 @@ export function shouldInstallWindowsTuiCompatibility( return platform === "win32" && mode === "tui"; } -export function shouldPreferWindowsFullscreen(options: { +export function shouldEnableWindowsClearOnShrink(options: { platform: NodeJS.Platform; mode: ExtensionContext["mode"]; - globalTuiMode?: TuiMode; - projectTuiMode?: TuiMode; - explicitCliTuiMode?: boolean; + globalClearOnShrink?: boolean; + projectClearOnShrink?: boolean; }) { return ( shouldInstallWindowsTuiCompatibility(options.platform, options.mode) && - options.globalTuiMode === undefined && - options.projectTuiMode === undefined && - options.explicitCliTuiMode !== true + options.globalClearOnShrink === undefined && + options.projectClearOnShrink === undefined ); } +function readClearOnShrinkSettings(settingsManager: TuiSettingsManager) { + const globalSettings = settingsManager.getGlobalSettings(); + const projectSettings = settingsManager.getProjectSettings(); + return { + globalClearOnShrink: globalSettings.terminal?.clearOnShrink, + projectClearOnShrink: projectSettings.terminal?.clearOnShrink, + }; +} + /** * Register the Windows renderer workaround. * * Pi exposes the renderer to widget factories, but not as a direct property * on ExtensionContext. The zero-height widget lets us apply the supported * renderer setting without replacing OpenPI's header, footer, or editor. + * + * The renderer setting is deliberately session-local. Pi's SettingsManager + * setters persist global preferences, so this compatibility extension only + * reads the existing clear-on-shrink settings and never changes tuiMode or + * terminal preferences on the user's behalf. */ export function registerWindowsTuiCompatibility( pi: ExtensionAPI, @@ -73,13 +88,33 @@ export function registerWindowsTuiCompatibility( cleanup(); if (!shouldInstallWindowsTuiCompatibility(platform, ctx.mode)) return; + let enableClearOnShrink = false; + if (settingsManagerFactory) { + try { + const settingsManager = await settingsManagerFactory(ctx.cwd); + const settings = readClearOnShrinkSettings(settingsManager); + const settingsErrors = settingsManager.drainErrors?.() ?? []; + enableClearOnShrink = + settingsErrors.length === 0 && + shouldEnableWindowsClearOnShrink({ + platform, + mode: ctx.mode, + ...settings, + }); + } catch { + // Settings reads must never prevent the OpenPI session from starting. + } + } + activeUi = ctx.ui; ctx.ui.setWidget( WIDGET_KEY, (tui) => { // The renderer can be replaced at runtime when the user switches TUI // modes, so apply this when the factory receives the active renderer. - if (tui.mode === "regular") tui.setClearOnShrink(true); + if (tui.mode === "regular" && enableClearOnShrink) { + tui.setClearOnShrink(true); + } return { render: () => [], invalidate() {}, @@ -87,39 +122,6 @@ export function registerWindowsTuiCompatibility( }, { placement: "belowEditor" }, ); - - if (!settingsManagerFactory) return; - - try { - const settingsManager = await settingsManagerFactory(ctx.cwd); - const globalSettings = settingsManager.getGlobalSettings(); - const projectSettings = settingsManager.getProjectSettings(); - const explicitCliTuiMode = process.argv.some( - (arg) => arg === "--tui-mode" || arg.startsWith("--tui-mode="), - ); - - if ( - !shouldPreferWindowsFullscreen({ - platform, - mode: ctx.mode, - globalTuiMode: globalSettings.tuiMode, - projectTuiMode: projectSettings.tuiMode, - explicitCliTuiMode, - }) || - settingsManager.setTuiMode === undefined - ) { - return; - } - - settingsManager.setTuiMode("fullscreen"); - await settingsManager.flush?.(); - ctx.ui.notify( - "OpenPI detected the Windows regular-TUI redraw issue and selected fullscreen mode for the next start. Restart Pi to apply it.", - "warning", - ); - } catch { - // A settings write must never prevent the OpenPI session from starting. - } }); pi.on("session_shutdown", cleanup); diff --git a/tests/extensions/windows-tui-compatibility/index.test.ts b/tests/extensions/windows-tui-compatibility/index.test.ts index 06779188..4c95d34e 100644 --- a/tests/extensions/windows-tui-compatibility/index.test.ts +++ b/tests/extensions/windows-tui-compatibility/index.test.ts @@ -7,8 +7,8 @@ import type { import type { Component, TUI } from "@earendil-works/pi-tui"; import { registerWindowsTuiCompatibility, + shouldEnableWindowsClearOnShrink, shouldInstallWindowsTuiCompatibility, - shouldPreferWindowsFullscreen, } from "../../../extensions/windows-tui-compatibility/index.ts"; type WidgetFactory = ( @@ -86,30 +86,30 @@ test("installs only for interactive Windows sessions", () => { assert.equal(shouldInstallWindowsTuiCompatibility("win32", "rpc"), false); assert.equal( - shouldPreferWindowsFullscreen({ platform: "win32", mode: "tui" }), + shouldEnableWindowsClearOnShrink({ platform: "win32", mode: "tui" }), true, ); assert.equal( - shouldPreferWindowsFullscreen({ + shouldEnableWindowsClearOnShrink({ platform: "win32", mode: "tui", - globalTuiMode: "regular", + globalClearOnShrink: false, }), false, ); assert.equal( - shouldPreferWindowsFullscreen({ + shouldEnableWindowsClearOnShrink({ platform: "win32", mode: "tui", - projectTuiMode: "fullscreen", + globalClearOnShrink: true, }), false, ); assert.equal( - shouldPreferWindowsFullscreen({ + shouldEnableWindowsClearOnShrink({ platform: "win32", mode: "tui", - explicitCliTuiMode: true, + projectClearOnShrink: false, }), false, ); @@ -119,9 +119,12 @@ test("installs only for interactive Windows sessions", () => { assert.equal(linux.widgetFactory, undefined); }); -test("enables clear-on-shrink for regular TUI but not fullscreen", () => { - const harness = createHarness("win32"); - harness.emit("session_start"); +test("enables clear-on-shrink for regular TUI but not fullscreen", async () => { + const harness = createHarness("win32", "tui", async () => ({ + getGlobalSettings: () => ({}), + getProjectSettings: () => ({}), + })); + await harness.emit("session_start"); const clearOnShrink: boolean[] = []; harness.mount({ @@ -148,33 +151,46 @@ test("enables clear-on-shrink for regular TUI but not fullscreen", () => { assert.deepEqual(clearOnShrink, []); }); -test("persists fullscreen only when no TUI mode is configured", async () => { - let selectedMode: string | undefined; - let flushCount = 0; - const unset = createHarness("win32", "tui", async () => ({ - getGlobalSettings: () => ({}), - getProjectSettings: () => ({}), - setTuiMode: (mode: "regular" | "fullscreen") => { - selectedMode = mode; - }, - flush: async () => { - flushCount += 1; - }, - })); - await unset.emit("session_start"); - assert.equal(selectedMode, "fullscreen"); - assert.equal(flushCount, 1); - assert.match(unset.notifications[0] ?? "", /Restart Pi/); - +test("keeps the workaround session-local and respects explicit clear-on-shrink", async () => { + const writes: string[] = []; const explicit = createHarness("win32", "tui", async () => ({ - getGlobalSettings: () => ({ tuiMode: "regular" as const }), + getGlobalSettings: () => ({ terminal: { clearOnShrink: false } }), getProjectSettings: () => ({}), - setTuiMode: () => { - throw new Error("must not override an explicit mode"); - }, + drainErrors: () => [], })); await explicit.emit("session_start"); + + const clearOnShrink: boolean[] = []; + explicit.mount({ + mode: "regular", + setClearOnShrink(enabled: boolean) { + writes.push("clear-on-shrink"); + clearOnShrink.push(enabled); + }, + requestRender() {}, + } as TUI); + + assert.deepEqual(clearOnShrink, []); assert.deepEqual(explicit.notifications, []); + assert.deepEqual(writes, []); +}); + +test("fails closed when settings cannot be read", async () => { + const harness = createHarness("win32", "tui", async () => { + throw new Error("malformed settings"); + }); + await harness.emit("session_start"); + + const clearOnShrink: boolean[] = []; + harness.mount({ + mode: "regular", + setClearOnShrink(enabled: boolean) { + clearOnShrink.push(enabled); + }, + requestRender() {}, + } as TUI); + + assert.deepEqual(clearOnShrink, []); }); test("cleans up the compatibility widget on shutdown", () => {