From 1854c38dfb364355f307229dd047f01925c0fb04 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Sun, 15 Feb 2026 13:31:46 -0500 Subject: [PATCH] fix: move exit summary to ExitProvider and destroy renderer before display The session summary wasn't appearing and 997;1n garbage was showing because: 1. The renderer was still running when we wrote the summary, overwriting it 2. The renderer continued sending DECRQM queries after exit Changes: - Move summary generation and display into ExitProvider - Call renderer.destroy() BEFORE writing the summary - ExitProvider now handles process.exit() after displaying summary - Simplify tui() handleExit to just resolve the promise - ExitProvider receives sessionId prop for summary generation --- src/tui-solid/app.tsx | 51 +++------------------------------- src/tui-solid/context/exit.tsx | 26 +++++++++++++++++ 2 files changed, 30 insertions(+), 47 deletions(-) diff --git a/src/tui-solid/app.tsx b/src/tui-solid/app.tsx index 5a6b063..d1031be 100644 --- a/src/tui-solid/app.tsx +++ b/src/tui-solid/app.tsx @@ -17,8 +17,6 @@ import { getExecutionState, } from "@services/chat-tui-service"; import { matchesAction } from "@services/keybind-resolver"; -import { TERMINAL_RESET } from "@constants/terminal"; -import { generateSessionSummary } from "@utils/core/session-stats"; import { copyToClipboard } from "@services/clipboard/text-clipboard"; import versionData from "@/version.json"; import { ExitProvider, useExit } from "@tui-solid/context/exit"; @@ -27,7 +25,6 @@ import { AppStoreProvider, useAppStore, setAppStoreRef, - appStore, } from "@tui-solid/context/app"; import { ThemeProvider, useTheme } from "@tui-solid/context/theme"; import { KeybindProvider } from "@tui-solid/context/keybind"; @@ -521,6 +518,7 @@ function App(props: AppProps) { return ( }> props.onExit({ exitCode: 0, sessionId: props.sessionId })} > @@ -575,51 +573,10 @@ export interface TuiRenderOptions extends TuiInput { export function tui(options: TuiRenderOptions): Promise { return new Promise((resolve) => { - const { writeSync } = require("fs"); - const handleExit = (output: TuiOutput): void => { - try { - // 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); - } + // ExitProvider handles the summary display and process exit + // We just resolve the promise here for cleanup + resolve(output); }; render(() => , { diff --git a/src/tui-solid/context/exit.tsx b/src/tui-solid/context/exit.tsx index 0e2ff85..97d10e9 100644 --- a/src/tui-solid/context/exit.tsx +++ b/src/tui-solid/context/exit.tsx @@ -1,7 +1,11 @@ import { createSignal, onCleanup } from "solid-js"; +import { useRenderer } from "@opentui/solid"; import { createSimpleContext } from "./helper"; +import { generateSessionSummary } from "@utils/core/session-stats"; +import { appStore } from "./app"; interface ExitContextInput extends Record { + sessionId?: string; onExit?: () => void; } @@ -20,6 +24,7 @@ export const { provider: ExitProvider, use: useExit } = createSimpleContext< >({ name: "Exit", init: (props) => { + const renderer = useRenderer(); const [exitCode, setExitCode] = createSignal(0); const [isExiting, setIsExiting] = createSignal(false); const [exitRequested, setExitRequested] = createSignal(false); @@ -27,7 +32,28 @@ export const { provider: ExitProvider, use: useExit } = createSimpleContext< const exit = (code = 0): void => { setExitCode(code); setIsExiting(true); + + // Generate and store exit message before destroying renderer + const state = appStore.getState(); + const summary = generateSessionSummary({ + sessionId: props.sessionId ?? "unknown", + sessionStats: state.sessionStats, + modifiedFiles: state.modifiedFiles, + modelName: state.model, + providerName: state.provider, + }); + + // Destroy renderer first to stop rendering + renderer.destroy(); + + // Call the onExit callback props.onExit?.(); + + // Write the summary after renderer is destroyed + process.stdout.write(summary + "\n"); + + // Exit the process + process.exit(code); }; const requestExit = (): void => {