From b6b89996f3cdcdae2b2cc92c5ba423dec5472738 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Sun, 15 Feb 2026 13:27:15 -0500 Subject: [PATCH] fix: prevent session summary from being cleared on exit The session stats summary was being displayed momentarily then cleared. This was caused by drainStdin() re-enabling raw mode AFTER we had already exited the alternate screen and written the summary. Changes: - Remove drainStdin() from normal exit flow - it was interfering with terminal - Drain pending DECRQM responses BEFORE exiting alternate screen, while still in raw mode - Add newline after summary to ensure shell prompt appears correctly - Keep drainStdin() available for signal handlers but don't use it in normal exit --- src/commands/components/execute/index.ts | 6 +-- src/tui-solid/app.tsx | 50 ++++++++++++++++++------ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/commands/components/execute/index.ts b/src/commands/components/execute/index.ts index 11a5d03..2ec8fc1 100644 --- a/src/commands/components/execute/index.ts +++ b/src/commands/components/execute/index.ts @@ -18,7 +18,6 @@ import { getThinkingMessage } from "@constants/status-messages"; import { enterFullscreen, registerExitHandlers, - drainStdin, } from "@utils/core/terminal"; import { createCallbacks } from "@commands/chat-tui"; import { agentLoader } from "@services/agent-loader"; @@ -32,9 +31,8 @@ interface ExecuteContext { const createHandleExit = (): (() => void) => (): void => { cleanupPermissionHandler(); // Note: Session stats are displayed by the TUI exit handler in app.tsx - // Drain stdin to consume pending terminal responses (e.g. DECRQM 997;1n) - // before exiting, so they don't echo as garbage text in the shell - drainStdin().then(() => process.exit(0)); + // The TUI handles terminal cleanup and draining stdin before exit + process.exit(0); }; const createHandleModelSelect = diff --git a/src/tui-solid/app.tsx b/src/tui-solid/app.tsx index 2dccc4b..5a6b063 100644 --- a/src/tui-solid/app.tsx +++ b/src/tui-solid/app.tsx @@ -579,21 +579,47 @@ export function tui(options: TuiRenderOptions): Promise { const handleExit = (output: TuiOutput): void => { try { - writeSync(1, TERMINAL_RESET); - - const state = appStore.getState(); - const summary = generateSessionSummary({ - sessionId: output.sessionId ?? "unknown", - sessionStats: state.sessionStats, - modifiedFiles: state.modifiedFiles, - modelName: state.model, - providerName: state.provider, - }); - writeSync(1, summary); + // First, drain any pending terminal responses (e.g. DECRQM 997;1n) + // while still in raw mode, before exiting alternate screen + const drainTimeout = 50; // 50ms is enough for terminal responses + + const finishExit = (): void => { + writeSync(1, TERMINAL_RESET); + + const state = appStore.getState(); + const summary = generateSessionSummary({ + sessionId: output.sessionId ?? "unknown", + sessionStats: state.sessionStats, + modifiedFiles: state.modifiedFiles, + modelName: state.model, + providerName: state.provider, + }); + writeSync(1, summary + "\n"); + resolve(output); + }; + + // If stdin is TTY and in raw mode, try to drain pending data + if (process.stdin.isTTY && process.stdin.isRaw) { + const sink = (): void => {}; + process.stdin.on("data", sink); + + const cleanup = (): void => { + process.stdin.removeListener("data", sink); + // Read and discard any buffered data + while (process.stdin.read() !== null) { + // drain + } + finishExit(); + }; + + setTimeout(cleanup, drainTimeout); + } else { + finishExit(); + } } catch { // Ignore - stdout may already be closed + resolve(output); } - resolve(output); }; render(() => , {