diff --git a/packages/coding-agent/src/cli/config-selector.ts b/packages/coding-agent/src/cli/config-selector.ts index b21e1231..cba05d86 100644 --- a/packages/coding-agent/src/cli/config-selector.ts +++ b/packages/coding-agent/src/cli/config-selector.ts @@ -22,7 +22,7 @@ export async function selectConfig(options: ConfigSelectorOptions): Promise { - const ui = new TUI(new ProcessTerminal()); + const ui = new TUI(new ProcessTerminal(), undefined, options.agentDir); let resolved = false; const selector = new ConfigSelectorComponent( diff --git a/packages/coding-agent/src/cli/startup-ui.ts b/packages/coding-agent/src/cli/startup-ui.ts index 73c0271c..12669e79 100644 --- a/packages/coding-agent/src/cli/startup-ui.ts +++ b/packages/coding-agent/src/cli/startup-ui.ts @@ -79,7 +79,7 @@ export async function createStartupTui(settingsManager: SettingsManager): Promis const terminalTheme = detectTerminalBackgroundFromEnv().theme; initTheme(resolveThemeSetting(settingsManager.getThemeSetting(), terminalTheme) ?? terminalTheme); setKeybindings(KeybindingsManager.create()); - const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor()); + const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor(), getAgentDir()); ui.setClearOnShrink(settingsManager.getClearOnShrink()); return ui; } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 7a31aa84..6a20b677 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -450,7 +450,7 @@ export class InteractiveMode { await this.rebindCurrentSession({ renderBeforeBind: true }); }); this.version = VERSION; - this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor()); + this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor(), getAgentDir()); this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink()); this.headerContainer = new Container(); this.loadedResourcesContainer = new Container(); diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index b7ebaea9..954256a1 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -319,15 +319,17 @@ export class TUI extends Container { private pendingOsc11BackgroundQueries: PendingOsc11BackgroundQuery[] = []; private terminalColorSchemeListeners = new Set<(scheme: TerminalColorScheme) => void>(); private terminalColorSchemeNotificationsEnabled = false; + private readonly logDirectory: string; // Overlay stack for modal components rendered on top of base content private focusOrderCounter = 0; private overlayStack: OverlayStackEntry[] = []; private overlayFocusRestore: OverlayFocusRestoreState = { status: "inactive" }; - constructor(terminal: Terminal, showHardwareCursor?: boolean) { + 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"); if (showHardwareCursor !== undefined) { this.showHardwareCursor = showHardwareCursor; } @@ -1329,8 +1331,9 @@ export class TUI extends Container { const debugRedraw = process.env.PI_DEBUG_REDRAW === "1"; const logRedraw = (reason: string): void => { if (!debugRedraw) return; - const logPath = path.join(os.homedir(), ".pi", "agent", "pi-debug.log"); + const logPath = path.join(this.logDirectory, "pi-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); }; @@ -1521,7 +1524,7 @@ export class TUI extends Container { buffer += "\x1b[2K"; // Clear current line if (!isImage && visibleWidth(line) > width) { // Log all lines to crash file for debugging - const crashLogPath = path.join(os.homedir(), ".pi", "agent", "pi-crash.log"); + const crashLogPath = path.join(this.logDirectory, "pi-crash.log"); const crashData = [ `Crash at ${new Date().toISOString()}`, `Terminal width: ${width}`, diff --git a/packages/tui/test/tui-render.test.ts b/packages/tui/test/tui-render.test.ts index ab038ac7..484639ba 100644 --- a/packages/tui/test/tui-render.test.ts +++ b/packages/tui/test/tui-render.test.ts @@ -1,4 +1,7 @@ import assert from "node:assert"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, it } from "node:test"; import type { Terminal as XtermTerminalType } from "@xterm/headless"; import { Image } from "../src/components/image.ts"; @@ -71,6 +74,28 @@ function getCellItalic(terminal: VirtualTerminal, row: number, col: number): num return cell.isItalic(); } +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 () => { + const terminal = new VirtualTerminal(40, 10); + const tui = new TUI(terminal, undefined, logDir); + const component = new TestComponent(); + tui.addChild(component); + component.lines = ["test"]; + tui.start(); + await terminal.waitForRender(); + + assert.match(readFileSync(join(logDir, "pi-debug.log"), "utf-8"), /fullRender: first render/); + tui.stop(); + }); + } finally { + rmSync(logDir, { 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 });