From 87d53f7035b311ade294f8b9bd38baedda21fc5e Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Sun, 15 Feb 2026 14:02:40 -0500 Subject: [PATCH] feat: add CODETYPER ASCII art to exit summary Add prominent CODETYPER logo using ASCII box-drawing characters at the top of the exit summary for enhanced branding. This provides a polished, professional appearance when exiting the CLI. Key improvements: - Add ASCII logo to session summary output - Simplify exit flow to use global message storage in terminal.ts - Remove duplicate exit message handling from ExitProvider - Fix signal handlers to prevent duplicate exit messages - Clean up debug logging from app.tsx - Ensure exit message persists on terminal after process exit The exit summary now displays comprehensive session statistics with: - CODETYPER ASCII logo - Total usage and Premium requests - API time and total session time - Code changes breakdown (+additions/-deletions) - Per-model token usage - Resume command with session ID Works correctly on all exit paths (normal exit, SIGINT, SIGTERM, errors). --- src/tui-solid/app.tsx | 1 - src/tui-solid/context/app.tsx | 1 - src/tui-solid/context/exit.tsx | 26 +++++++--------- src/utils/core/session-stats.ts | 9 +++++- src/utils/core/terminal.ts | 53 ++++++++++++++++++++------------- 5 files changed, 51 insertions(+), 39 deletions(-) diff --git a/src/tui-solid/app.tsx b/src/tui-solid/app.tsx index 186c6c0..18bb5df 100644 --- a/src/tui-solid/app.tsx +++ b/src/tui-solid/app.tsx @@ -118,7 +118,6 @@ function AppContent(props: AppProps) { setAppStoreRef(app); - // Set exit message reactively (like OpenCode does) // This ensures the message is pre-computed and ready when exit is called createEffect(() => { const state = appStore.getState(); diff --git a/src/tui-solid/context/app.tsx b/src/tui-solid/context/app.tsx index 37b430c..756dfb6 100644 --- a/src/tui-solid/context/app.tsx +++ b/src/tui-solid/context/app.tsx @@ -1035,7 +1035,6 @@ export const { provider: AppStoreProvider, use: useAppStore } = updateMcpServerStatus, // Modified file tracking - modifiedFiles: () => store.modifiedFiles, addModifiedFile, clearModifiedFiles, diff --git a/src/tui-solid/context/exit.tsx b/src/tui-solid/context/exit.tsx index 84c6470..e1e2bff 100644 --- a/src/tui-solid/context/exit.tsx +++ b/src/tui-solid/context/exit.tsx @@ -1,18 +1,18 @@ import { createSignal, onCleanup } from "solid-js"; import { useRenderer } from "@opentui/solid"; import { createSimpleContext } from "./helper"; +import { setGlobalExitMessage } from "@utils/core/terminal"; interface ExitContextInput extends Record { onExit?: () => void | Promise; } interface ExitContextValue { - exit: (code?: number) => void; - exitCode: () => number; - isExiting: () => boolean; + exit: (code?: number) => Promise; requestExit: () => void; - cancelExit: () => void; - confirmExit: () => void; + getExitCode: () => number; + isExiting: () => boolean; + exitRequested: () => boolean; setExitMessage: (message: string | undefined) => void; getExitMessage: () => string | undefined; } @@ -27,7 +27,6 @@ export const { provider: ExitProvider, use: useExit } = createSimpleContext< const [exitCode, setExitCode] = createSignal(0); const [isExiting, setIsExiting] = createSignal(false); const [exitRequested, setExitRequested] = createSignal(false); - const [exitMessage, setExitMessageState] = createSignal(undefined); const exit = async (code = 0): Promise => { setExitCode(code); @@ -46,22 +45,19 @@ export const { provider: ExitProvider, use: useExit } = createSimpleContext< // Call the onExit callback (may be async) await props.onExit?.(); - // Write the stored exit message after renderer is destroyed - const message = exitMessage(); - if (message) { - process.stdout.write(message + "\n"); - } + // Exit message will be written by emergencyTerminalCleanup + // which is registered on process "exit" event in terminal.ts // Exit the process process.exit(code); }; const setExitMessage = (message: string | undefined): void => { - setExitMessageState(message); + setGlobalExitMessage(message); }; const getExitMessage = (): string | undefined => { - return exitMessage(); + return undefined; // Message is stored in terminal.ts }; const requestExit = (): void => { @@ -81,13 +77,13 @@ export const { provider: ExitProvider, use: useExit } = createSimpleContext< onCleanup(() => { setIsExiting(false); setExitRequested(false); - setExitMessageState(undefined); }); return { exit, - exitCode, + getExitCode: exitCode, isExiting, + exitRequested, requestExit, cancelExit, confirmExit, diff --git a/src/utils/core/session-stats.ts b/src/utils/core/session-stats.ts index 8b1dadf..bb1ea9f 100644 --- a/src/utils/core/session-stats.ts +++ b/src/utils/core/session-stats.ts @@ -80,13 +80,20 @@ export interface SessionSummaryInput { } export function generateSessionSummary(input: SessionSummaryInput): string { - const { sessionId, sessionStats, modifiedFiles, modelName, providerName } = input; + const { sessionId, sessionStats, modifiedFiles, modelName } = input; const { additions, deletions } = calculateCodeChanges(modifiedFiles); const totalSessionTime = Date.now() - sessionStats.startTime; // Build the summary lines const lines: string[] = [ + "", + " ██████╗ ██████╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ███████╗██████╗ ", + " ██╔════╝██╔═══██╗██╔══██╗██╔════╝╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔════╝██╔══██╗", + " ██║ ██║ ██║██║ ██║█████╗ ██║ ╚████╔╝ ██████╔╝█████╗ ██████╔╝", + " ██║ ██║ ██║██║ ██║██╔══╝ ██║ ╚██╔╝ ██╔═══╝ ██╔══╝ ██╔══██╗", + " ╚██████╗╚██████╔╝██████╔╝███████╗ ██║ ██║ ██║ ███████╗██║ ██║", + " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝", "", "═══════════════════════════════════════════════════════════════", "", diff --git a/src/utils/core/terminal.ts b/src/utils/core/terminal.ts index c68bc94..0e85a23 100644 --- a/src/utils/core/terminal.ts +++ b/src/utils/core/terminal.ts @@ -19,6 +19,25 @@ let spinner: Ora | null = null; */ let exitHandlersRegistered = false; +/** + * Global exit message to display on process exit (set by ExitProvider in TUI mode) + */ +let globalExitMessage: string | undefined; + +/** + * Set the exit message to display when the process exits + */ +export const setGlobalExitMessage = (message: string | undefined): void => { + globalExitMessage = message; +}; + +/** + * Get the current global exit message + */ +export const getGlobalExitMessage = (): string | undefined => { + return globalExitMessage; +}; + /** * Drain any pending stdin data (e.g. DECRQM responses from @opentui/core's * theme-mode detection that queries mode 997). The terminal responds with @@ -80,6 +99,11 @@ export const drainStdin = (): Promise => const emergencyTerminalCleanup = (): void => { try { writeSync(1, TERMINAL_RESET); + + // Write exit message if set (from TUI ExitProvider) + if (globalExitMessage) { + writeSync(1, globalExitMessage + "\n"); + } } catch { // Ignore errors during cleanup - stdout may already be closed } @@ -93,28 +117,15 @@ export const registerExitHandlers = (): void => { if (exitHandlersRegistered) return; exitHandlersRegistered = true; + // Emergency cleanup will be called once on ANY exit process.on("exit", emergencyTerminalCleanup); - process.on("beforeExit", emergencyTerminalCleanup); - process.on("SIGINT", () => { - emergencyTerminalCleanup(); - process.exit(130); - }); - process.on("SIGTERM", () => { - emergencyTerminalCleanup(); - process.exit(143); - }); - process.on("SIGHUP", () => { - emergencyTerminalCleanup(); - process.exit(128); - }); - process.on("uncaughtException", () => { - emergencyTerminalCleanup(); - process.exit(1); - }); - process.on("unhandledRejection", () => { - emergencyTerminalCleanup(); - process.exit(1); - }); + + // Signal handlers just call process.exit() which triggers "exit" event + process.on("SIGINT", () => process.exit(130)); + process.on("SIGTERM", () => process.exit(143)); + process.on("SIGHUP", () => process.exit(128)); + process.on("uncaughtException", () => process.exit(1)); + process.on("unhandledRejection", () => process.exit(1)); }; /**