fix(tui): remove coding-agent config reads from pi-tui

Fixes #8698
This commit is contained in:
Anton Geraschenko 2026-09-03 02:59:14 -07:00 • committed by GitHub
parent 3316c4e35b
commit c505f4c198
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 72 additions and 17 deletions

View file

@ -22,7 +22,12 @@ export async function selectConfig(options: ConfigSelectorOptions): Promise<void
initTheme(options.settingsManager.getTheme(), true); initTheme(options.settingsManager.getTheme(), true);
return new Promise((resolve) => { return new Promise((resolve) => {
const ui: TUI = new TuiMainScreen(new ProcessTerminal(), undefined, options.agentDir); const ui: TUI = new TuiMainScreen(
new ProcessTerminal(),
options.settingsManager.getShowHardwareCursor(),
options.agentDir,
);
ui.setClearOnShrink(options.settingsManager.getClearOnShrink());
let resolved = false; let resolved = false;
const selector = new ConfigSelectorComponent( const selector = new ConfigSelectorComponent(

View file

@ -288,7 +288,7 @@ When a `Focusable` component has focus, TUI:
3. Positions the hardware terminal cursor at that location 3. Positions the hardware terminal cursor at that location
4. Shows the hardware cursor only when `showHardwareCursor` is enabled 4. Shows the hardware cursor only when `showHardwareCursor` is enabled
The cursor remains hidden by default. This keeps the fake cursor rendering, while still positioning the hardware cursor for terminals that track IME candidate windows with hidden cursors. Some terminals require a visible hardware cursor for IME positioning; enable it with the renderer constructor's `showHardwareCursor` argument, `setShowHardwareCursor(true)`, or `PI_HARDWARE_CURSOR=1`. The `Editor` and `Input` built-in components already implement this interface. The cursor remains hidden by default. This keeps the fake cursor rendering, while still positioning the hardware cursor for terminals that track IME candidate windows with hidden cursors. Some terminals require a visible hardware cursor for IME positioning; enable it with the renderer constructor's `showHardwareCursor` argument or `setShowHardwareCursor(true)`. The `Editor` and `Input` built-in components already implement this interface.
**Container components with embedded inputs:** When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child: **Container components with embedded inputs:** When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child:

View file

@ -1,4 +1,5 @@
import * as fs from "node:fs"; import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path"; import * as path from "node:path";
import { deleteKittyImage, isImageLine } from "./terminal-image.ts"; import { deleteKittyImage, isImageLine } from "./terminal-image.ts";
import { type TUI, TuiBase, type TuiStopOptions } from "./tui.ts"; import { type TUI, TuiBase, type TuiStopOptions } from "./tui.ts";
@ -317,10 +318,10 @@ export class TuiMainScreen extends TuiBase implements TUI {
this.previousHeight = height; this.previousHeight = height;
}; };
const debugRedraw = process.env.PI_DEBUG_REDRAW === "1"; const redrawLogDirectory = process.env.PI_TUI_DEBUG_REDRAW === "1" ? this.logDirectory : undefined;
const logRedraw = (reason: string): void => { const logRedraw = (reason: string): void => {
if (!debugRedraw) return; if (redrawLogDirectory === undefined) return;
const logPath = path.join(this.logDirectory, "pi-debug.log"); const logPath = path.join(redrawLogDirectory, "pi-tui-debug.log");
const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.previousLines.length}, new=${newLines.length}, height=${height})\n`; const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.previousLines.length}, new=${newLines.length}, height=${height})\n`;
fs.mkdirSync(path.dirname(logPath), { recursive: true }); fs.mkdirSync(path.dirname(logPath), { recursive: true });
fs.appendFileSync(logPath, msg); fs.appendFileSync(logPath, msg);
@ -351,7 +352,7 @@ export class TuiMainScreen extends TuiBase implements TUI {
// Content shrunk below the working area and no overlays - re-render to clear empty rows // Content shrunk below the working area and no overlays - re-render to clear empty rows
// (overlays need the padding, so only do this when no overlays are active) // (overlays need the padding, so only do this when no overlays are active)
// Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var // Configurable via setClearOnShrink()
if (this.getClearOnShrink() && newLines.length < this.maxLinesRendered && !this.hasOverlayEntries) { if (this.getClearOnShrink() && newLines.length < this.maxLinesRendered && !this.hasOverlayEntries) {
logRedraw(`clearOnShrink (maxLinesRendered=${this.maxLinesRendered})`); logRedraw(`clearOnShrink (maxLinesRendered=${this.maxLinesRendered})`);
fullRender(true); fullRender(true);
@ -515,7 +516,7 @@ export class TuiMainScreen extends TuiBase implements TUI {
output.append("\x1b[2K"); // Clear current line output.append("\x1b[2K"); // Clear current line
if (!isImage && visibleWidth(line) > width) { if (!isImage && visibleWidth(line) > width) {
// Log all lines to crash file for debugging // Log all lines to crash file for debugging
const crashLogPath = path.join(this.logDirectory, "pi-crash.log"); const crashLogPath = path.join(this.logDirectory ?? os.tmpdir(), "pi-tui-crash.log");
const crashData = [ const crashData = [
`Crash at ${new Date().toISOString()}`, `Crash at ${new Date().toISOString()}`,
`Terminal width: ${width}`, `Terminal width: ${width}`,

View file

@ -2,8 +2,6 @@
* Minimal TUI implementation with differential rendering * Minimal TUI implementation with differential rendering
*/ */
import * as os from "node:os";
import * as path from "node:path";
import { performance } from "node:perf_hooks"; import { performance } from "node:perf_hooks";
import { isKeyRelease, matchesKey } from "./keys.ts"; import { isKeyRelease, matchesKey } from "./keys.ts";
import type { Terminal } from "./terminal.ts"; import type { Terminal } from "./terminal.ts";
@ -466,15 +464,16 @@ export abstract class TuiBase extends Container implements TUI {
private renderTimer: NodeJS.Timeout | undefined; private renderTimer: NodeJS.Timeout | undefined;
private lastRenderAt = 0; private lastRenderAt = 0;
private static readonly MIN_RENDER_INTERVAL_MS = 16; private static readonly MIN_RENDER_INTERVAL_MS = 16;
private showHardwareCursor = process.env.PI_HARDWARE_CURSOR === "1"; private showHardwareCursor = false;
private clearOnShrink = process.env.PI_CLEAR_ON_SHRINK === "1"; private clearOnShrink = false;
protected fullRedrawCount = 0; protected fullRedrawCount = 0;
protected stopped = false; protected stopped = false;
private pendingOsc11BackgroundReplies = 0; private pendingOsc11BackgroundReplies = 0;
private pendingOsc11BackgroundQueries: PendingOsc11BackgroundQuery[] = []; private pendingOsc11BackgroundQueries: PendingOsc11BackgroundQuery[] = [];
private terminalColorSchemeListeners = new Set<(scheme: TerminalColorScheme) => void>(); private terminalColorSchemeListeners = new Set<(scheme: TerminalColorScheme) => void>();
private terminalColorSchemeNotificationsEnabled = false; private terminalColorSchemeNotificationsEnabled = false;
protected readonly logDirectory: string; /** Directory for debug/crash logs. When undefined, debug logging is disabled and crash dumps fall back to the OS temp directory. */
protected readonly logDirectory: string | undefined;
// Overlay stack for modal components rendered on top of base content // Overlay stack for modal components rendered on top of base content
private focusOrderCounter = 0; private focusOrderCounter = 0;
@ -489,7 +488,7 @@ export abstract class TuiBase extends Container implements TUI {
constructor(terminal: Terminal, showHardwareCursor?: boolean, logDirectory?: string) { constructor(terminal: Terminal, showHardwareCursor?: boolean, logDirectory?: string) {
super(); super();
this.terminal = terminal; this.terminal = terminal;
this.logDirectory = logDirectory ?? process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent"); this.logDirectory = logDirectory;
if (showHardwareCursor !== undefined) { if (showHardwareCursor !== undefined) {
this.showHardwareCursor = showHardwareCursor; this.showHardwareCursor = showHardwareCursor;
} }
@ -530,8 +529,8 @@ export abstract class TuiBase extends Container implements TUI {
/** /**
* Set whether to trigger full re-render when content shrinks. * Set whether to trigger full re-render when content shrinks.
* When true (default), empty rows are cleared when content shrinks. * When true, empty rows are cleared when content shrinks.
* When false, empty rows remain (reduces redraws on slower terminals). * When false (default), empty rows remain (reduces redraws on slower terminals).
*/ */
setClearOnShrink(enabled: boolean): void { setClearOnShrink(enabled: boolean): void {
this.clearOnShrink = enabled; this.clearOnShrink = enabled;

View file

@ -143,7 +143,7 @@ describe("TUI debug logging", () => {
it("writes redraw logs to the provided directory", async () => { it("writes redraw logs to the provided directory", async () => {
const logDir = mkdtempSync(join(tmpdir(), "pi-tui-log-")); const logDir = mkdtempSync(join(tmpdir(), "pi-tui-log-"));
try { try {
await withEnv({ PI_DEBUG_REDRAW: "1" }, async () => { await withEnv({ PI_TUI_DEBUG_REDRAW: "1" }, async () => {
const terminal = new VirtualTerminal(40, 10); const terminal = new VirtualTerminal(40, 10);
const tui: TUI = new TuiMainScreen(terminal, undefined, logDir); const tui: TUI = new TuiMainScreen(terminal, undefined, logDir);
const component = new TestComponent(); const component = new TestComponent();
@ -152,7 +152,7 @@ describe("TUI debug logging", () => {
tui.start(); tui.start();
await terminal.waitForRender(); await terminal.waitForRender();
assert.match(readFileSync(join(logDir, "pi-debug.log"), "utf-8"), /fullRender: first render/); assert.match(readFileSync(join(logDir, "pi-tui-debug.log"), "utf-8"), /fullRender: first render/);
tui.stop(); tui.stop();
}); });
} finally { } finally {
@ -206,6 +206,56 @@ describe("TUI bounded render output", () => {
}); });
}); });
/** Set each environment variable to `value`, returning a function that restores the previous state. */
function overrideEnv(names: readonly string[], value: string): () => void {
const previousValues = names.map((name) => [name, process.env[name]] as const);
for (const name of names) {
process.env[name] = value;
}
return () => {
for (const [name, previousValue] of previousValues) {
if (previousValue === undefined) delete process.env[name];
else process.env[name] = previousValue;
}
};
}
describe("TUI crash dump without configured log directory", () => {
it("writes the crash dump to the OS temp directory instead of a home-directory default", async () => {
// The TUI falls back to os.tmpdir() when no log directory is configured, so
// isolate the test by pointing the temp directory at a fresh directory rather
// than sharing the real one with concurrent test runs. os.tmpdir() reads
// TMPDIR on POSIX and TEMP/TMP on Windows, so override all three.
const crashDir = mkdtempSync(join(tmpdir(), "pi-tui-crash-"));
const crashLogPath = join(crashDir, "pi-tui-crash.log");
const restoreTmpdirEnv = overrideEnv(["TMPDIR", "TEMP", "TMP"], crashDir);
try {
const terminal = new VirtualTerminal(40, 10);
const tui: TUI = new TuiMainScreen(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = ["ok"];
tui.start();
await terminal.waitForRender();
// Width overflow is detected in the differential render path
component.lines = ["ok", "x".repeat(60)];
assert.throws(
() => tui.renderNow(),
(error: unknown) => {
assert.ok(error instanceof Error);
assert.ok(error.message.includes(crashLogPath), `error message should reference ${crashLogPath}`);
return true;
},
);
assert.match(readFileSync(crashLogPath, "utf-8"), /Terminal width: 40/);
} finally {
restoreTmpdirEnv();
rmSync(crashDir, { recursive: true, force: true });
}
});
});
describe("TUI Kitty image cleanup", () => { describe("TUI Kitty image cleanup", () => {
it("clears reserved Kitty image rows before drawing appended image placements", async () => { it("clears reserved Kitty image rows before drawing appended image placements", async () => {
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });