diff --git a/.changeset/fix-pi-tui-debug-redraw-log-dir.md b/.changeset/fix-pi-tui-debug-redraw-log-dir.md new file mode 100644 index 0000000000..8dca016725 --- /dev/null +++ b/.changeset/fix-pi-tui-debug-redraw-log-dir.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/pi-tui": patch +--- + +Make the TUI's debug logging robust on Windows: the `PI_DEBUG_REDRAW` and `PI_TUI_DEBUG` diagnostics no longer crash the render loop on a filesystem error, and `PI_TUI_DEBUG` writes render dumps to the OS temp dir instead of a hardcoded `/tmp`. diff --git a/packages/pi-tui/src/tui.ts b/packages/pi-tui/src/tui.ts index c578c7d93e..5c74f07e54 100644 --- a/packages/pi-tui/src/tui.ts +++ b/packages/pi-tui/src/tui.ts @@ -1353,7 +1353,13 @@ export class TUI extends Container { if (!debugRedraw) return; const logPath = path.join(os.homedir(), ".pi", "agent", "pi-debug.log"); const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.previousLines.length}, new=${newLines.length}, height=${height})\n`; - fs.appendFileSync(logPath, msg); + + try { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + fs.appendFileSync(logPath, msg); + } catch { + // never let debug logging break rendering + } }; // First render - just output everything without clearing (assumes clean screen) @@ -1566,8 +1572,7 @@ export class TUI extends Container { buffer += "\x1b[?2026l"; // End synchronized output if (process.env['PI_TUI_DEBUG'] === "1") { - const debugDir = "/tmp/tui"; - fs.mkdirSync(debugDir, { recursive: true }); + const debugDir = path.join(os.tmpdir(), "tui"); const debugPath = path.join(debugDir, `render-${Date.now()}-${Math.random().toString(36).slice(2)}.log`); const debugData = [ `firstChanged: ${firstChanged}`, @@ -1591,7 +1596,13 @@ export class TUI extends Container { "=== buffer ===", JSON.stringify(buffer), ].join("\n"); - fs.writeFileSync(debugPath, debugData); + + try { + fs.mkdirSync(debugDir, { recursive: true }); + fs.writeFileSync(debugPath, debugData); + } catch { + //never let debug logging break rendering. + } } // Write entire buffer at once diff --git a/packages/pi-tui/test/tui-render.test.ts b/packages/pi-tui/test/tui-render.test.ts index f928f7015d..065060d2a7 100644 --- a/packages/pi-tui/test/tui-render.test.ts +++ b/packages/pi-tui/test/tui-render.test.ts @@ -1,4 +1,7 @@ import assert from "node:assert"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; import { describe, it } from "node:test"; import type { Terminal as XtermTerminalType } from "@xterm/headless"; import { Image } from "../src/components/image.ts"; @@ -402,6 +405,75 @@ describe("TUI resize handling", () => { }); }); +describe("TUI debug logging", () => { + it("does not crash the render loop when the debug-redraw log directory is missing", async () => { + const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), "pi-tui-debug-redraw-")); + try { + // `fakeHome` exists but `fakeHome/.pi/agent` does not — this mirrors a + // fresh machine that has never written to ~/.pi/agent before. + await withEnv({ PI_DEBUG_REDRAW: "1", HOME: fakeHome, USERPROFILE: fakeHome }, async () => { + const terminal = new VirtualTerminal(40, 10); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + component.lines = ["Line 0", "Line 1"]; + tui.start(); + await terminal.waitForRender(); + + // A width change forces `logRedraw` to run, which previously threw + // ENOENT (missing parent directory) and crashed the whole TUI as an + // uncaughtException. + terminal.resize(60, 10); + await terminal.waitForRender(); + + const viewport = terminal.getViewport(); + assert.ok(viewport[0]?.includes("Line 0"), "TUI keeps rendering after the debug log write"); + + tui.stop(); + }); + + const logPath = path.join(fakeHome, ".pi", "agent", "pi-debug.log"); + const contents = await fs.readFile(logPath, "utf8"); + assert.ok(contents.includes("terminal width changed"), "the debug log should still be written"); + } finally { + await fs.rm(fakeHome, { recursive: true, force: true }); + } + }); + + it("writes PI_TUI_DEBUG render dumps into the OS temp dir, not a hardcoded /tmp", async () => { + const fakeTmp = await fs.mkdtemp(path.join(os.tmpdir(), "pi-tui-debug-render-")); + try { + await withEnv( + { PI_TUI_DEBUG: "1", TMPDIR: fakeTmp, TEMP: fakeTmp, TMP: fakeTmp }, + async () => { + const terminal = new VirtualTerminal(40, 10); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + component.lines = ["Line 0", "Line 1"]; + tui.start(); + await terminal.waitForRender(); + component.lines = ["Line 0", "CHANGED"]; + tui.requestRender(); + await terminal.waitForRender(); + const viewport = terminal.getViewport(); + assert.ok(viewport[1]?.includes("CHANGED"), "TUI keeps rendering with PI_TUI_DEBUG on"); + tui.stop(); + }, + ); + const dumpDir = path.join(fakeTmp, "tui"); + const files = await fs.readdir(dumpDir); + assert.ok( + files.some((name) => name.startsWith("render-")), + "a render dump should be written under the OS temp dir", + ); + } finally { + await fs.rm(fakeTmp, { recursive: true, force: true }); + } + }); +}); + describe("TUI content shrinkage", () => { it("clears empty rows when content shrinks significantly", async () => { const terminal = new VirtualTerminal(40, 10);