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
This commit is contained in:
@@ -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 (
|
||||
<ErrorBoundary fallback={(err: Error) => <ErrorFallback error={err} />}>
|
||||
<ExitProvider
|
||||
sessionId={props.sessionId}
|
||||
onExit={() => props.onExit({ exitCode: 0, sessionId: props.sessionId })}
|
||||
>
|
||||
<RouteProvider>
|
||||
@@ -575,51 +573,10 @@ export interface TuiRenderOptions extends TuiInput {
|
||||
|
||||
export function tui(options: TuiRenderOptions): Promise<TuiOutput> {
|
||||
return new Promise<TuiOutput>((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(() => <App {...options} onExit={handleExit} />, {
|
||||
|
||||
@@ -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<string, unknown> {
|
||||
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 => {
|
||||
|
||||
Reference in New Issue
Block a user