From 38e0dfc4df5c366b77c54a88647a2f43f8ceabe1 Mon Sep 17 00:00:00 2001 From: Caldalis Date: Sun, 19 Jul 2026 12:10:00 +0800 Subject: [PATCH 1/3] fix(pi-tui): create debug-redraw log directory before writing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PI_DEBUG_REDRAW=1 wrote to ~/.pi/agent/pi-debug.log via fs.appendFileSync without ensuring the parent directory existed. On a machine that had never written there, the first full redraw threw an uncaught ENOENT and silently killed the whole TUI (no console output at all — the crash only surfaced in ~/.kimi-code/logs/kimi-code.log). Create the directory before appending, and wrap the write so a logging failure can never crash rendering. --- .changeset/fix-pi-tui-debug-redraw-log-dir.md | 5 +++ packages/pi-tui/src/tui.ts | 8 +++- packages/pi-tui/test/tui-render.test.ts | 40 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-pi-tui-debug-redraw-log-dir.md 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..673c7bb52e --- /dev/null +++ b/.changeset/fix-pi-tui-debug-redraw-log-dir.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/pi-tui": patch +--- + +Fix a crash when `PI_DEBUG_REDRAW=1` is set on a machine that has never written to `~/.pi/agent` before: the debug-redraw logger wrote to that directory without creating it first, so the first full redraw threw an uncaught `ENOENT` and silently killed the whole TUI. The logger now creates the directory before appending, and never lets a logging failure crash rendering. diff --git a/packages/pi-tui/src/tui.ts b/packages/pi-tui/src/tui.ts index c578c7d93e..18b3118ef2 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) diff --git a/packages/pi-tui/test/tui-render.test.ts b/packages/pi-tui/test/tui-render.test.ts index f928f7015d..b5690e619b 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,43 @@ describe("TUI resize handling", () => { }); }); +describe("TUI debug redraw 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 }); + } + }); +}); + describe("TUI content shrinkage", () => { it("clears empty rows when content shrinks significantly", async () => { const terminal = new VirtualTerminal(40, 10); From ff5cbc1eef33d4abdbe44a8d87cb3937f2027184 Mon Sep 17 00:00:00 2001 From: Caldalis Date: Sun, 19 Jul 2026 12:18:33 +0800 Subject: [PATCH 2/3] chore: tighten changeset wording per gen-changesets conventions --- .changeset/fix-pi-tui-debug-redraw-log-dir.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-pi-tui-debug-redraw-log-dir.md b/.changeset/fix-pi-tui-debug-redraw-log-dir.md index 673c7bb52e..dea2cf7e11 100644 --- a/.changeset/fix-pi-tui-debug-redraw-log-dir.md +++ b/.changeset/fix-pi-tui-debug-redraw-log-dir.md @@ -2,4 +2,4 @@ "@moonshot-ai/pi-tui": patch --- -Fix a crash when `PI_DEBUG_REDRAW=1` is set on a machine that has never written to `~/.pi/agent` before: the debug-redraw logger wrote to that directory without creating it first, so the first full redraw threw an uncaught `ENOENT` and silently killed the whole TUI. The logger now creates the directory before appending, and never lets a logging failure crash rendering. +Fix a crash when `PI_DEBUG_REDRAW=1` is set on a machine that has never used `~/.pi/agent` before. From 350f44f4deb92174919e51968dd5cd7e9c26a1c9 Mon Sep 17 00:00:00 2001 From: Caldalis Date: Sun, 19 Jul 2026 15:50:11 +0800 Subject: [PATCH 3/3] fix(pi-tui): write PI_TUI_DEBUG render dumps to the OS temp dir The PI_TUI_DEBUG render-dump path hardcoded "/tmp/tui", which on Windows resolves to the current drive root (e.g. C:\tmp\tui) instead of the real temp location, and it had no error guard an unwritable target would throw and crash the TUI (the same failure mode as the PI_DEBUG_REDRAW logger). Use os.tmpdir() and swallow filesystem errors, matching the diagnostic-logging convention. --- .changeset/fix-pi-tui-debug-redraw-log-dir.md | 2 +- packages/pi-tui/src/tui.ts | 11 ++++-- packages/pi-tui/test/tui-render.test.ts | 34 ++++++++++++++++++- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/.changeset/fix-pi-tui-debug-redraw-log-dir.md b/.changeset/fix-pi-tui-debug-redraw-log-dir.md index dea2cf7e11..8dca016725 100644 --- a/.changeset/fix-pi-tui-debug-redraw-log-dir.md +++ b/.changeset/fix-pi-tui-debug-redraw-log-dir.md @@ -2,4 +2,4 @@ "@moonshot-ai/pi-tui": patch --- -Fix a crash when `PI_DEBUG_REDRAW=1` is set on a machine that has never used `~/.pi/agent` before. +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 18b3118ef2..5c74f07e54 100644 --- a/packages/pi-tui/src/tui.ts +++ b/packages/pi-tui/src/tui.ts @@ -1572,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}`, @@ -1597,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 b5690e619b..065060d2a7 100644 --- a/packages/pi-tui/test/tui-render.test.ts +++ b/packages/pi-tui/test/tui-render.test.ts @@ -405,7 +405,7 @@ describe("TUI resize handling", () => { }); }); -describe("TUI debug redraw logging", () => { +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 { @@ -440,6 +440,38 @@ describe("TUI debug redraw logging", () => { 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", () => {