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

Fixes #8698
This commit is contained in:
Anton Geraschenko
2026-09-03 11:59:14 +02:00
committed by GitHub
parent 3316c4e35b
commit c505f4c198
5 changed files with 72 additions and 17 deletions
@@ -22,7 +22,12 @@ export async function selectConfig(options: ConfigSelectorOptions): Promise<void
initTheme(options.settingsManager.getTheme(), true);
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;
const selector = new ConfigSelectorComponent(
+1 -1
View File
@@ -288,7 +288,7 @@ When a `Focusable` component has focus, TUI:
3. Positions the hardware terminal cursor at that location
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:
+6 -5
View File
@@ -1,4 +1,5 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { deleteKittyImage, isImageLine } from "./terminal-image.ts";
import { type TUI, TuiBase, type TuiStopOptions } from "./tui.ts";
@@ -317,10 +318,10 @@ export class TuiMainScreen extends TuiBase implements TUI {
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 => {
if (!debugRedraw) return;
const logPath = path.join(this.logDirectory, "pi-debug.log");
if (redrawLogDirectory === undefined) return;
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`;
fs.mkdirSync(path.dirname(logPath), { recursive: true });
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
// (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) {
logRedraw(`clearOnShrink (maxLinesRendered=${this.maxLinesRendered})`);
fullRender(true);
@@ -515,7 +516,7 @@ export class TuiMainScreen extends TuiBase implements TUI {
output.append("\x1b[2K"); // Clear current line
if (!isImage && visibleWidth(line) > width) {
// 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 = [
`Crash at ${new Date().toISOString()}`,
`Terminal width: ${width}`,
+7 -8
View File
@@ -2,8 +2,6 @@
* 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 { isKeyRelease, matchesKey } from "./keys.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 lastRenderAt = 0;
private static readonly MIN_RENDER_INTERVAL_MS = 16;
private showHardwareCursor = process.env.PI_HARDWARE_CURSOR === "1";
private clearOnShrink = process.env.PI_CLEAR_ON_SHRINK === "1";
private showHardwareCursor = false;
private clearOnShrink = false;
protected fullRedrawCount = 0;
protected stopped = false;
private pendingOsc11BackgroundReplies = 0;
private pendingOsc11BackgroundQueries: PendingOsc11BackgroundQuery[] = [];
private terminalColorSchemeListeners = new Set<(scheme: TerminalColorScheme) => void>();
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
private focusOrderCounter = 0;
@@ -489,7 +488,7 @@ export abstract class TuiBase extends Container implements TUI {
constructor(terminal: Terminal, showHardwareCursor?: boolean, logDirectory?: string) {
super();
this.terminal = terminal;
this.logDirectory = logDirectory ?? process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
this.logDirectory = logDirectory;
if (showHardwareCursor !== undefined) {
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.
* When true (default), empty rows are cleared when content shrinks.
* When false, empty rows remain (reduces redraws on slower terminals).
* When true, empty rows are cleared when content shrinks.
* When false (default), empty rows remain (reduces redraws on slower terminals).
*/
setClearOnShrink(enabled: boolean): void {
this.clearOnShrink = enabled;
+52 -2
View File
@@ -143,7 +143,7 @@ describe("TUI debug logging", () => {
it("writes redraw logs to the provided directory", async () => {
const logDir = mkdtempSync(join(tmpdir(), "pi-tui-log-"));
try {
await withEnv({ PI_DEBUG_REDRAW: "1" }, async () => {
await withEnv({ PI_TUI_DEBUG_REDRAW: "1" }, async () => {
const terminal = new VirtualTerminal(40, 10);
const tui: TUI = new TuiMainScreen(terminal, undefined, logDir);
const component = new TestComponent();
@@ -152,7 +152,7 @@ describe("TUI debug logging", () => {
tui.start();
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();
});
} 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", () => {
it("clears reserved Kitty image rows before drawing appended image placements", async () => {
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });