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
This commit is contained in:
2026-02-15 13:27:15 -05:00
parent 18a5eca3ae
commit b6b89996f3
2 changed files with 40 additions and 16 deletions

View File

@@ -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 =

View File

@@ -579,21 +579,47 @@ export function tui(options: TuiRenderOptions): Promise<TuiOutput> {
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(() => <App {...options} onExit={handleExit} />, {