feat(coding-agent): rename UI mode to TUI mode

This commit is contained in:
Armin Ronacher 2026-08-05 22:56:58 +02:00
parent 2c819a41ff
commit 5446cd7545
16 changed files with 107 additions and 93 deletions

View file

@ -75,8 +75,8 @@
- Added built-in Baseten provider support with `BASETEN_API_KEY` authentication and `zai-org/GLM-5.2` as the default model.
- Added `CredentialSynchronizationError` for credential changes that commit successfully but fail to synchronize local model state.
- Added chainable `pi.registerMarkdownTransformer()` hooks for display-only transformation of user and assistant Markdown.
- Added an experimental fullscreen UI mode, selectable through `--ui-mode fullscreen` or `/settings` ([#7304](https://github.com/earendil-works/pi/issues/7304)).
- Added runtime switching between regular and fullscreen UI modes through `/settings`.
- Added an experimental fullscreen TUI mode, selectable through `--tui-mode fullscreen` or `/settings` ([#7304](https://github.com/earendil-works/pi/issues/7304)).
- Added runtime switching between regular and fullscreen TUI modes through `/settings`.
- Added a sticky editor, status, widget, and footer dock to fullscreen mode while keeping the transcript independently scrollable.
- Added a draggable transcript scrollbar to fullscreen mode with configurable `auto`, `always`, and `hidden` modes through `/settings`; `always` reserves the rightmost column.
- Added page scrolling and marked-message navigation shortcuts to fullscreen mode.
@ -97,7 +97,7 @@
- Fixed `Ctrl+X` copy confirmations in fullscreen mode adding a transcript status line instead of showing the transient `Copied!` marker.
- Fixed Kitty image previews in fullscreen mode overlapping the sticky editor and footer dock while scrolling.
- Fixed image-heavy fullscreen sessions lagging when layout changes retransmitted visible Kitty image payloads and rendered the transcript twice per frame.
- Fixed spaces in `/settings` searches toggling the highlighted setting while typing multi-word queries such as **UI mode** or **Quiet startup**.
- Fixed spaces in `/settings` searches toggling the highlighted setting while typing multi-word queries such as **TUI mode** or **Quiet startup**.
- Fixed custom editors not inheriting the default editor's autocomplete dropdown item limit ([#7333](https://github.com/earendil-works/pi/issues/7333)).
- Fixed malformed resource arrays in package manifests crashing session startup ([#7187](https://github.com/earendil-works/pi/issues/7187)).
- Fixed the DOOM overlay example downloading its shareware WAD from a dead URL.

View file

@ -608,7 +608,7 @@ Combine `--no-*` with explicit flags to load exactly what you need, ignoring set
|--------|-------------|
| `--system-prompt <text>` | Replace default prompt (context files and skills still appended) |
| `--append-system-prompt <text>` | Append to system prompt |
| `--ui-mode <mode>` | UI mode: `regular` (default) or experimental `fullscreen` |
| `--tui-mode <mode>` | TUI mode: `regular` (default) or experimental `fullscreen` |
| `--verbose` | Force verbose startup |
| `-a`, `--approve` | Trust project-local files for this run |
| `-na`, `--no-approve` | Ignore project-local files for this run |

View file

@ -84,7 +84,7 @@ The dedicated history actions always change history entries, regardless of the c
### TUI Fullscreen Viewport
These actions apply when interactive mode uses `--ui-mode fullscreen` and target the primary transcript scroll region. Two-finger trackpad and mouse-wheel input scroll the region under the pointer, falling back to the transcript over the fixed editor/status/footer dock. Clicking an OSC 8 hyperlink opens it in the default handler. Dragging with the primary mouse button selects text and copies it to the clipboard; holding at the transcript's top or bottom edge auto-scrolls into off-screen content.
These actions apply when interactive mode uses `--tui-mode fullscreen` and target the primary transcript scroll region. Two-finger trackpad and mouse-wheel input scroll the region under the pointer, falling back to the transcript over the fixed editor/status/footer dock. Clicking an OSC 8 hyperlink opens it in the default handler. Dragging with the primary mouse button selects text and copies it to the clipboard; holding at the transcript's top or bottom edge auto-scrolls into off-screen content.
Fullscreen transcript bindings take precedence over editor bindings. The default unmodified navigation keys therefore control the transcript in fullscreen mode, while their `ctrl` variants continue to control the editor. Outside fullscreen mode, both variants control the editor.

View file

@ -65,8 +65,8 @@ Use `/trust` in interactive mode to save a project trust decision for future ses
| `outputPad` | number | `1` | Horizontal padding for user messages, assistant messages, and thinking (0 or 1) |
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
| `showHardwareCursor` | boolean | `false` | Show the terminal cursor while TUI positions it for IME support |
| `uiMode` | string | `"regular"` | Interactive UI mode: `"regular"` or experimental `"fullscreen"`. Changes from `/settings` apply immediately; `--ui-mode` overrides this setting at startup |
| `fullscreenScrollbar` | string | `"auto"` | Fullscreen transcript scrollbar: `"auto"` shows it temporarily while scrolling, `"always"` reserves the rightmost column and keeps it visible, and `"hidden"` hides it. Has no effect in regular UI mode |
| `tuiMode` | string | `"regular"` | Interactive TUI mode: `"regular"` or experimental `"fullscreen"`. Changes from `/settings` apply immediately; `--tui-mode` overrides this setting at startup |
| `fullscreenScrollbar` | string | `"auto"` | Fullscreen transcript scrollbar: `"auto"` shows it temporarily while scrolling, `"always"` reserves the rightmost column and keeps it visible, and `"hidden"` hides it. Has no effect in regular TUI mode |
For VS Code, include `--wait` so pi resumes after the editor exits:

View file

@ -241,7 +241,7 @@ pi --no-extensions -e ./my-extension.ts
|--------|-------------|
| `--system-prompt <text>` | Replace default prompt; context files and skills are still appended |
| `--append-system-prompt <text>` | Append to system prompt |
| `--ui-mode <mode>` | UI mode: `regular` (default) or experimental `fullscreen` |
| `--tui-mode <mode>` | TUI mode: `regular` (default) or experimental `fullscreen` |
| `--verbose` | Force verbose startup |
| `-a`, `--approve` | Trust project-local files for this run |
| `-na`, `--no-approve` | Ignore project-local files for this run |
@ -250,7 +250,7 @@ pi --no-extensions -e ./my-extension.ts
In `fullscreen` mode, the transcript scrolls inside the terminal viewport while queued messages, working status, extension widgets, editor, and footer remain fixed at the bottom. Mouse/trackpad input scrolls the region under the pointer; keyboard viewport actions always remain available. Inline images work in terminals that support the Kitty graphics protocol, including Kitty and Ghostty. In iTerm2 they render as text placeholders because its inline-image protocol cannot delete or crop placements during application-owned scrolling. In `regular` mode, pi uses the main screen and terminal-owned scrollback, and iTerm2 inline images continue to render normally.
Set **UI mode** in `/settings` to switch between `regular` and `fullscreen` immediately and choose the default for future sessions.
Set **TUI mode** in `/settings` to switch between `regular` and `fullscreen` immediately and choose the default for future sessions.
### File Arguments

View file

@ -6,7 +6,7 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
import chalk from "chalk";
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from "../config.ts";
import type { ExtensionFlag } from "../core/extensions/types.ts";
import type { UiMode } from "../core/settings-manager.ts";
import type { TuiMode } from "../core/settings-manager.ts";
export type Mode = "text" | "json" | "rpc";
@ -46,7 +46,7 @@ export interface Args {
noContextFiles?: boolean;
listModels?: string | true;
offline?: boolean;
uiMode?: UiMode;
tuiMode?: TuiMode;
verbose?: boolean;
projectTrustOverride?: boolean;
messages: string[];
@ -177,18 +177,18 @@ export function parseArgs(args: string[]): Args {
} else {
result.listModels = true;
}
} else if (arg === "--ui-mode") {
} else if (arg === "--tui-mode") {
const mode = args[i + 1];
if (mode === "regular" || mode === "fullscreen") {
result.uiMode = mode;
result.tuiMode = mode;
i++;
} else if (mode === undefined || mode.startsWith("-")) {
result.diagnostics.push({ type: "error", message: "--ui-mode requires regular or fullscreen" });
result.diagnostics.push({ type: "error", message: "--tui-mode requires regular or fullscreen" });
} else {
i++;
result.diagnostics.push({
type: "error",
message: `Invalid UI mode "${mode}". Valid values: regular, fullscreen`,
message: `Invalid TUI mode "${mode}". Valid values: regular, fullscreen`,
});
}
} else if (arg === "--verbose") {
@ -288,7 +288,7 @@ ${chalk.bold("Options:")}
--export <file> Export session file to HTML and exit
--list-models [search] List available models (with optional fuzzy search)
--verbose Force verbose startup (overrides quietStartup setting)
--ui-mode <mode> UI mode: regular (default) or fullscreen
--tui-mode <mode> TUI mode: regular (default) or fullscreen
--approve, -a Trust project-local files for this run
--no-approve, -na Ignore project-local files for this run
--offline Disable startup network operations (same as PI_OFFLINE=1)

View file

@ -1,6 +1,6 @@
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
import type { Transport } from "@earendil-works/pi-ai";
import type { ScrollViewScrollbar, TuiMode } from "@earendil-works/pi-tui";
import type { TuiMode as RendererTuiMode, ScrollViewScrollbar } from "@earendil-works/pi-tui";
import { randomUUID } from "crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path";
@ -33,7 +33,7 @@ export interface RetrySettings {
provider?: ProviderRetrySettings;
}
export type UiMode = TuiMode;
export type TuiMode = RendererTuiMode;
export interface TerminalSettings {
showImages?: boolean; // default: true (only relevant if terminal supports images)
@ -132,8 +132,8 @@ export interface Settings {
httpProxy?: string; // Proxy URL applied as HTTP_PROXY and HTTPS_PROXY for Pi-managed HTTP clients
httpIdleTimeoutMs?: number; // HTTP header/body idle timeout in milliseconds; 0 disables it
websocketConnectTimeoutMs?: number; // WebSocket connect/open handshake timeout in milliseconds; 0 disables it
uiMode?: UiMode; // default: "regular"
fullscreenScrollbar?: ScrollViewScrollbar; // default: "auto"; no effect in regular UI mode
tuiMode?: TuiMode; // default: "regular"
fullscreenScrollbar?: ScrollViewScrollbar; // default: "auto"; no effect in regular TUI mode
}
function isMergeableObject(value: unknown): value is Record<string, unknown> {
@ -1125,13 +1125,13 @@ export class SettingsManager {
this.save();
}
getUiMode(): UiMode {
return this.settings.uiMode === "fullscreen" ? "fullscreen" : "regular";
getTuiMode(): TuiMode {
return this.settings.tuiMode === "fullscreen" ? "fullscreen" : "regular";
}
setUiMode(mode: UiMode): void {
this.globalSettings.uiMode = mode;
this.markModified("uiMode");
setTuiMode(mode: TuiMode): void {
this.globalSettings.tuiMode = mode;
this.markModified("tuiMode");
this.save();
}

View file

@ -257,7 +257,7 @@ export {
type RetrySettings,
SettingsManager,
type SettingsManagerCreateOptions,
type UiMode,
type TuiMode,
} from "./core/settings-manager.ts";
// Skills
export {

View file

@ -891,7 +891,7 @@ export async function main(args: string[], options?: MainOptions) {
initialImages,
initialMessages: parsed.messages,
verbose: parsed.verbose,
uiMode: parsed.uiMode,
tuiMode: parsed.tuiMode,
});
if (startupBenchmark) {
await interactiveMode.init();

View file

@ -17,7 +17,7 @@ import { formatHttpIdleTimeoutMs, HTTP_IDLE_TIMEOUT_CHOICES } from "../../../cor
import type {
DefaultProjectTrust,
MermaidRenderingMode,
UiMode,
TuiMode,
WarningSettings,
} from "../../../core/settings-manager.ts";
import {
@ -86,7 +86,7 @@ export interface SettingsConfig {
defaultProjectTrust: DefaultProjectTrust;
clearOnShrink: boolean;
showTerminalProgress: boolean;
uiMode: UiMode;
tuiMode: TuiMode;
fullscreenScrollbar: ScrollViewScrollbar;
warnings: WarningSettings;
}
@ -120,7 +120,7 @@ export interface SettingsCallbacks {
onDefaultProjectTrustChange: (defaultProjectTrust: DefaultProjectTrust) => void;
onClearOnShrinkChange: (enabled: boolean) => void;
onShowTerminalProgressChange: (enabled: boolean) => void;
onUiModeChange: (mode: UiMode) => void;
onTuiModeChange: (mode: TuiMode) => void;
onFullscreenScrollbarChange: (mode: ScrollViewScrollbar) => void;
onWarningsChange: (warnings: WarningSettings) => void;
onCancel: () => void;
@ -630,10 +630,10 @@ export class SettingsSelectorComponent extends Container {
),
},
{
id: "ui-mode",
label: "UI mode",
id: "tui-mode",
label: "TUI mode",
description: "Interface layout; fullscreen mode is experimental",
currentValue: config.uiMode,
currentValue: config.tuiMode,
values: ["regular", "fullscreen"],
},
{
@ -855,8 +855,8 @@ export class SettingsSelectorComponent extends Container {
case "terminal-progress":
callbacks.onShowTerminalProgressChange(newValue === "true");
break;
case "ui-mode":
callbacks.onUiModeChange(newValue as UiMode);
case "tui-mode":
callbacks.onTuiModeChange(newValue as TuiMode);
break;
case "fullscreen-scrollbar":
callbacks.onFullscreenScrollbarChange(newValue as ScrollViewScrollbar);

View file

@ -92,7 +92,7 @@ import { DefaultPackageManager } from "../../core/package-manager.ts";
import type { ResourceDiagnostic } from "../../core/resource-loader.ts";
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.ts";
import { type SessionEntry, SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.ts";
import type { UiMode } from "../../core/settings-manager.ts";
import type { TuiMode } from "../../core/settings-manager.ts";
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.ts";
import type { SourceInfo } from "../../core/source-info.ts";
import { isInstallTelemetryEnabled } from "../../core/telemetry.ts";
@ -327,12 +327,12 @@ export interface InteractiveModeOptions {
initialMessages?: string[];
/** Force verbose startup (overrides quietStartup setting) */
verbose?: boolean;
/** UI layout mode. */
uiMode?: UiMode;
/** TUI layout mode. */
tuiMode?: TuiMode;
}
interface InteractiveTuiOptions {
uiMode: UiMode;
tuiMode: TuiMode;
showHardwareCursor: boolean;
logDirectory: string;
terminal?: Terminal;
@ -341,7 +341,7 @@ interface InteractiveTuiOptions {
/** Composition root for selecting the interactive terminal renderer. */
export function createInteractiveTui(options: InteractiveTuiOptions): TuiMainScreen | TuiAltScreen {
const terminal = options.terminal ?? new ProcessTerminal();
if (options.uiMode === "fullscreen") {
if (options.tuiMode === "fullscreen") {
return new TuiAltScreen(terminal, options.showHardwareCursor, options.logDirectory, { openUrl: openBrowser });
}
return new TuiMainScreen(terminal, options.showHardwareCursor, options.logDirectory);
@ -512,8 +512,8 @@ export class InteractiveMode {
constructor(runtimeHost: AgentSessionRuntime, options: InteractiveModeOptions = {}) {
this.runtimeHost = runtimeHost;
const uiMode = options.uiMode ?? this.settingsManager.getUiMode();
this.options = { ...options, uiMode };
const tuiMode = options.tuiMode ?? this.settingsManager.getTuiMode();
this.options = { ...options, tuiMode };
this.autoTrustOnReloadCwd = options.autoTrustOnReloadCwd;
this.runtimeHost.setBeforeSessionInvalidate(() => {
this.resetExtensionUI();
@ -523,7 +523,7 @@ export class InteractiveMode {
});
this.version = VERSION;
this.renderer = createInteractiveTui({
uiMode,
tuiMode,
showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
logDirectory: getAgentDir(),
});
@ -763,13 +763,13 @@ export class InteractiveMode {
private stopInteractiveTui(): void {
if (this.renderer.mode === "fullscreen") {
while (this.renderer.hasOverlayEntries) this.renderer.hideOverlay();
this.switchUiMode("regular", false, false);
this.switchTuiMode("regular", false, false);
this.renderer.renderNow();
}
this.ui.stop();
}
private switchUiMode(mode: UiMode, restoreProgress = true, startRenderer = true): boolean {
private switchTuiMode(mode: TuiMode, restoreProgress = true, startRenderer = true): boolean {
const previousUi = this.renderer;
if (mode === previousUi.mode) return true;
if (previousUi.hasOverlayEntries) return false;
@ -790,7 +790,7 @@ export class InteractiveMode {
if (TuiLayouts.isViewportTUI(previousUi)) previousUi.setLayoutRoot(undefined);
const nextUi = createInteractiveTui({
uiMode: mode,
tuiMode: mode,
showHardwareCursor,
logDirectory: getAgentDir(),
terminal,
@ -801,7 +801,7 @@ export class InteractiveMode {
nextUi.restoreRenderState(this.mainScreenRenderState);
}
this.renderer = nextUi;
this.options.uiMode = mode;
this.options.tuiMode = mode;
this.mountInteractiveTui(nextUi, components);
nextUi.invalidate();
nextUi.setFocus(focus);
@ -2041,7 +2041,7 @@ export class InteractiveMode {
this.activeStatusIndicator?.dispose();
this.activeStatusIndicator = undefined;
this.statusContainer.clear();
if (hadActiveStatusIndicator && this.options.uiMode === "regular" && this.ui.getClearOnShrink()) {
if (hadActiveStatusIndicator && this.options.tuiMode === "regular" && this.ui.getClearOnShrink()) {
this.statusContainer.addChild(this.idleStatus);
}
}
@ -4377,7 +4377,7 @@ export class InteractiveMode {
quietStartup: this.settingsManager.getQuietStartup(),
clearOnShrink: this.settingsManager.getClearOnShrink(),
showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
uiMode: this.ui.mode,
tuiMode: this.ui.mode,
fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
warnings: this.settingsManager.getWarnings(),
},
@ -4524,15 +4524,15 @@ export class InteractiveMode {
onShowTerminalProgressChange: (enabled) => {
this.settingsManager.setShowTerminalProgress(enabled);
},
onUiModeChange: (mode) => {
if (!this.switchUiMode(mode)) {
selector?.getSettingsList().updateValue("ui-mode", this.ui.mode);
this.showStatus("Close active overlays before changing UI mode");
onTuiModeChange: (mode) => {
if (!this.switchTuiMode(mode)) {
selector?.getSettingsList().updateValue("tui-mode", this.ui.mode);
this.showStatus("Close active overlays before changing TUI mode");
return;
}
this.settingsManager.setUiMode(mode);
this.settingsManager.setTuiMode(mode);
if (!this.activeStatusIndicator) this.statusContainer.clear();
this.showStatus(`UI mode: ${mode}`);
this.showStatus(`TUI mode: ${mode}`);
},
onFullscreenScrollbarChange: (mode) => {
this.settingsManager.setFullscreenScrollbar(mode);

View file

@ -329,22 +329,28 @@ describe("parseArgs", () => {
});
});
describe("--ui-mode flag", () => {
describe("--tui-mode flag", () => {
test.each(["regular", "fullscreen"] as const)("parses %s mode", (mode) => {
const result = parseArgs(["--ui-mode", mode]);
expect(result.uiMode).toBe(mode);
const result = parseArgs(["--tui-mode", mode]);
expect(result.tuiMode).toBe(mode);
});
test("rejects invalid modes", () => {
const result = parseArgs(["--ui-mode", "other"]);
const result = parseArgs(["--tui-mode", "other"]);
expect(result.diagnostics).toEqual([
{ type: "error", message: 'Invalid UI mode "other". Valid values: regular, fullscreen' },
{ type: "error", message: 'Invalid TUI mode "other". Valid values: regular, fullscreen' },
]);
});
test("requires a mode", () => {
const result = parseArgs(["--ui-mode"]);
expect(result.diagnostics).toEqual([{ type: "error", message: "--ui-mode requires regular or fullscreen" }]);
const result = parseArgs(["--tui-mode"]);
expect(result.diagnostics).toEqual([{ type: "error", message: "--tui-mode requires regular or fullscreen" }]);
});
test("does not recognize the old --ui-mode flag", () => {
const result = parseArgs(["--ui-mode", "fullscreen"]);
expect(result.tuiMode).toBeUndefined();
expect(result.unknownFlags.get("ui-mode")).toBe("fullscreen");
});
});

View file

@ -60,16 +60,16 @@ describe("experimental CLI command composition", () => {
});
test("rejects existing options that the client command does not support yet", () => {
expect(experimentalCli.parse(["client", "--ui-mode", "fullscreen", "@prompt.md"])).toEqual({
expect(experimentalCli.parse(["client", "--tui-mode", "fullscreen", "@prompt.md"])).toEqual({
ok: false,
errors: [UNSUPPORTED_CLIENT_OPTIONS],
});
});
test("reports existing parser errors before capability errors", () => {
expect(experimentalCli.parse(["client", "--ui-mode", "wrong", "--model", "claude-sonnet"])).toEqual({
expect(experimentalCli.parse(["client", "--tui-mode", "wrong", "--model", "claude-sonnet"])).toEqual({
ok: false,
errors: ['Invalid UI mode "wrong". Valid values: regular, fullscreen', UNSUPPORTED_CLIENT_OPTIONS],
errors: ['Invalid TUI mode "wrong". Valid values: regular, fullscreen', UNSUPPORTED_CLIENT_OPTIONS],
});
});

View file

@ -2,7 +2,7 @@ import type { Component, Terminal, TUI } from "@earendil-works/pi-tui";
import { Container, isViewportTUI, Text } from "@earendil-works/pi-tui";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { VirtualTerminal } from "../../tui/test/virtual-terminal.ts";
import type { UiMode } from "../src/core/settings-manager.ts";
import type { TuiMode } from "../src/core/settings-manager.ts";
import {
createInteractiveTui,
createInteractiveTuiReference,
@ -41,7 +41,7 @@ describe("createInteractiveTui", () => {
it("selects the alternate-screen renderer only when requested", async () => {
const mainTerminal = new RecordingTerminal();
const mainTui = createInteractiveTui({
uiMode: "regular",
tuiMode: "regular",
showHardwareCursor: false,
logDirectory: "/tmp",
terminal: mainTerminal,
@ -55,7 +55,7 @@ describe("createInteractiveTui", () => {
const altTerminal = new RecordingTerminal();
const altTui = createInteractiveTui({
uiMode: "fullscreen",
tuiMode: "fullscreen",
showHardwareCursor: false,
logDirectory: "/tmp",
terminal: altTerminal,
@ -71,13 +71,13 @@ describe("createInteractiveTui", () => {
it("replaces the renderer while preserving components and focus", async () => {
const terminal = new RecordingTerminal(40, 8);
const renderer = createInteractiveTui({
uiMode: "regular",
tuiMode: "regular",
showHardwareCursor: false,
logDirectory: "/tmp",
terminal,
});
let stableUi: TUI;
const invalidatedModes: UiMode[] = [];
const invalidatedModes: TuiMode[] = [];
const component: Component & { focused: boolean } = {
focused: false,
render: () => ["content"],
@ -90,7 +90,7 @@ describe("createInteractiveTui", () => {
renderer: ReturnType<typeof createInteractiveTui>;
ui: TUI;
fullscreenLayoutRoot: Component;
options: { uiMode?: UiMode };
options: { tuiMode?: TuiMode };
themeController: { rebindTui: () => void };
extensionTerminalInputSubscriptions: Set<never>;
};
@ -98,20 +98,20 @@ describe("createInteractiveTui", () => {
renderer,
ui: undefined as unknown as TUI,
fullscreenLayoutRoot: component,
options: { uiMode: "regular" as UiMode },
options: { tuiMode: "regular" as TuiMode },
themeController: { rebindTui: () => {} },
extensionTerminalInputSubscriptions: new Set<never>(),
}) as SwitchContext;
stableUi = createInteractiveTuiReference(() => context.renderer);
context.ui = stableUi;
const { stopInteractiveTui, switchUiMode } = InteractiveMode.prototype as unknown as {
const { stopInteractiveTui, switchTuiMode } = InteractiveMode.prototype as unknown as {
stopInteractiveTui(this: SwitchContext): void;
switchUiMode(this: SwitchContext, mode: UiMode, restoreProgress?: boolean): boolean;
switchTuiMode(this: SwitchContext, mode: TuiMode, restoreProgress?: boolean): boolean;
};
renderer.start();
await terminal.waitForRender();
expect(switchUiMode.call(context, "fullscreen", false)).toBe(true);
expect(switchTuiMode.call(context, "fullscreen", false)).toBe(true);
await terminal.waitForRender();
expect(stableUi.mode).toBe("fullscreen");
@ -152,7 +152,7 @@ describe("InteractiveMode copy confirmation", () => {
it("flashes Copied! for the copy shortcut in fullscreen mode", async () => {
const terminal = new RecordingTerminal(40, 4);
const ui = createInteractiveTui({
uiMode: "fullscreen",
tuiMode: "fullscreen",
showHardwareCursor: false,
logDirectory: "/tmp",
terminal,
@ -183,7 +183,7 @@ describe("InteractiveMode copy confirmation", () => {
it("keeps the status-line confirmation for the copy shortcut in regular mode", async () => {
const ui = createInteractiveTui({
uiMode: "regular",
tuiMode: "regular",
showHardwareCursor: false,
logDirectory: "/tmp",
terminal: new RecordingTerminal(),
@ -207,7 +207,7 @@ describe("InteractiveMode copy confirmation", () => {
type ClearStatusContext = {
activeStatusIndicator: { kind: "working"; dispose: () => void } | undefined;
statusContainer: Container;
options: { uiMode?: UiMode };
options: { tuiMode?: TuiMode };
ui: { getClearOnShrink: () => boolean };
idleStatus: Component;
};
@ -220,7 +220,7 @@ const interactiveModePrototype = InteractiveMode.prototype as unknown as Interac
describe("clear-on-shrink status spacing", () => {
it("reserves status height only on the main-screen renderer", () => {
for (const [uiMode, expectedChildren] of [
for (const [tuiMode, expectedChildren] of [
["regular", 1],
["fullscreen", 0],
] as const) {
@ -228,7 +228,7 @@ describe("clear-on-shrink status spacing", () => {
const context: ClearStatusContext = {
activeStatusIndicator: { kind: "working", dispose },
statusContainer: new Container(),
options: { uiMode },
options: { tuiMode },
ui: { getClearOnShrink: () => true },
idleStatus: new Text("", 0, 0),
};

View file

@ -397,26 +397,34 @@ describe("SettingsManager", () => {
});
});
describe("UI mode", () => {
describe("TUI mode", () => {
it("defaults to regular and persists fullscreen mode", async () => {
const manager = SettingsManager.create(projectDir, agentDir);
expect(manager.getUiMode()).toBe("regular");
expect(manager.getTuiMode()).toBe("regular");
manager.setUiMode("fullscreen");
manager.setTuiMode("fullscreen");
await manager.flush();
expect(manager.getUiMode()).toBe("fullscreen");
expect(manager.getTuiMode()).toBe("fullscreen");
const savedSettings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
expect(savedSettings.uiMode).toBe("fullscreen");
expect(savedSettings.tuiMode).toBe("fullscreen");
});
it("falls back to regular for unsupported values", () => {
writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ uiMode: "other" }));
writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ tuiMode: "other" }));
const manager = SettingsManager.create(projectDir, agentDir);
expect(manager.getUiMode()).toBe("regular");
expect(manager.getTuiMode()).toBe("regular");
});
it("does not recognize the old uiMode setting", () => {
writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ uiMode: "fullscreen" }));
const manager = SettingsManager.create(projectDir, agentDir);
expect(manager.getTuiMode()).toBe("regular");
});
});

View file

@ -12,8 +12,8 @@ const testTheme: SettingsListTheme = {
const items = [
{
id: "ui-mode",
label: "UI mode",
id: "tui-mode",
label: "TUI mode",
currentValue: "regular",
values: ["regular", "fullscreen"],
},
@ -31,13 +31,13 @@ describe("SettingsList", () => {
{ enableSearch: true },
);
for (const character of "UI mode") list.handleInput(character);
for (const character of "TUI mode") list.handleInput(character);
assert.deepStrictEqual(changes, []);
assert.match(list.render(80)[0] ?? "", /UI mode/);
assert.match(list.render(80)[0] ?? "", /TUI mode/);
list.handleInput("\r");
assert.deepStrictEqual(changes, [{ id: "ui-mode", value: "fullscreen" }]);
assert.deepStrictEqual(changes, [{ id: "tui-mode", value: "fullscreen" }]);
});
it("keeps Space as a change shortcut before a search query is entered", () => {
@ -53,6 +53,6 @@ describe("SettingsList", () => {
list.handleInput(" ");
assert.deepStrictEqual(changes, [{ id: "ui-mode", value: "fullscreen" }]);
assert.deepStrictEqual(changes, [{ id: "tui-mode", value: "fullscreen" }]);
});
});