Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-pi-tui-debug-redraw-log-dir.md
Original file line number Diff line number Diff line change
@@ -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`.
19 changes: 15 additions & 4 deletions packages/pi-tui/src/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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}`,
Expand All @@ -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
Expand Down
72 changes: 72 additions & 0 deletions packages/pi-tui/test/tui-render.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand Down