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).
This commit is contained in:
2026-02-15 14:02:40 -05:00
parent 6c72ff0212
commit 87d53f7035
5 changed files with 51 additions and 39 deletions

View File

@@ -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();

View File

@@ -1035,7 +1035,6 @@ export const { provider: AppStoreProvider, use: useAppStore } =
updateMcpServerStatus,
// Modified file tracking
modifiedFiles: () => store.modifiedFiles,
addModifiedFile,
clearModifiedFiles,

View File

@@ -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<string, unknown> {
onExit?: () => void | Promise<void>;
}
interface ExitContextValue {
exit: (code?: number) => void;
exitCode: () => number;
isExiting: () => boolean;
exit: (code?: number) => Promise<void>;
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<string | undefined>(undefined);
const exit = async (code = 0): Promise<void> => {
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,

View File

@@ -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[] = [
"",
" ██████╗ ██████╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ███████╗██████╗ ",
" ██╔════╝██╔═══██╗██╔══██╗██╔════╝╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔════╝██╔══██╗",
" ██║ ██║ ██║██║ ██║█████╗ ██║ ╚████╔╝ ██████╔╝█████╗ ██████╔╝",
" ██║ ██║ ██║██║ ██║██╔══╝ ██║ ╚██╔╝ ██╔═══╝ ██╔══╝ ██╔══██╗",
" ╚██████╗╚██████╔╝██████╔╝███████╗ ██║ ██║ ██║ ███████╗██║ ██║",
" ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝",
"",
"═══════════════════════════════════════════════════════════════",
"",

View File

@@ -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<void> =>
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));
};
/**