diff --git a/src/constants/exit-message.ts b/src/constants/exit-message.ts new file mode 100644 index 0000000..acfe648 --- /dev/null +++ b/src/constants/exit-message.ts @@ -0,0 +1,34 @@ +/** + * Exit message constants for the post-exit banner + * + * Displayed after the TUI exits to show session info + * and the resume command. + */ + +/** Small block art logo (3 rows) used as visual marker */ +export const EXIT_LOGO = [ + "█▀▀█", + "█ █", + "▀▀▀▀", +] as const; + +/** ANSI style codes for exit message */ +export const EXIT_STYLES = { + RESET: "\x1b[0m", + DIM: "\x1b[90m", + HIGHLIGHT: "\x1b[96m", + BOLD: "\x1b[1m", + LOGO_COLOR: "\x1b[36m", +} as const; + +/** Maximum width for the session description before truncation */ +export const EXIT_DESCRIPTION_MAX_WIDTH = 50; + +/** Padding before each exit message line */ +export const EXIT_LINE_PADDING = " "; + +/** Gap between logo and text */ +export const EXIT_LOGO_GAP = " "; + +/** Truncation indicator */ +export const EXIT_TRUNCATION_MARKER = "\u2026"; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 56a0e3c..82d2119 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -15,6 +15,7 @@ export interface TuiInput { export interface TuiOutput { exitCode: number; sessionId?: string; + sessionTitle?: string; } export interface Config { diff --git a/src/services/exit-message.ts b/src/services/exit-message.ts new file mode 100644 index 0000000..b7307d6 --- /dev/null +++ b/src/services/exit-message.ts @@ -0,0 +1,50 @@ +/** + * Exit message formatter + * + * Formats the post-exit banner shown after the TUI closes, + * displaying session info and the resume command. + */ + +import { EOL } from "os"; +import { + EXIT_LOGO, + EXIT_STYLES, + EXIT_DESCRIPTION_MAX_WIDTH, + EXIT_LINE_PADDING, + EXIT_LOGO_GAP, + EXIT_TRUNCATION_MARKER, +} from "@constants/exit-message"; + +/** Truncate text to a max width, appending ellipsis if needed */ +const truncateText = (text: string, maxWidth: number): string => { + if (text.length <= maxWidth) return text; + return text.slice(0, maxWidth - 1) + EXIT_TRUNCATION_MARKER; +}; + +/** Format the exit banner with session info */ +export const formatExitMessage = ( + sessionId?: string, + sessionTitle?: string, +): string => { + if (!sessionId) return ""; + + const { RESET, DIM, HIGHLIGHT, LOGO_COLOR } = EXIT_STYLES; + const pad = EXIT_LINE_PADDING; + const gap = EXIT_LOGO_GAP; + + const description = sessionTitle + ? truncateText(sessionTitle, EXIT_DESCRIPTION_MAX_WIDTH) + : ""; + + const resumeCommand = `codetyper --resume ${sessionId}`; + + const lines: string[] = [ + "", + `${pad}${LOGO_COLOR}${EXIT_LOGO[0]}${RESET}${gap}${HIGHLIGHT}${description}${RESET}`, + `${pad}${LOGO_COLOR}${EXIT_LOGO[1]}${RESET}${gap}${DIM}${resumeCommand}${RESET}`, + `${pad}${LOGO_COLOR}${EXIT_LOGO[2]}${RESET}`, + "", + ]; + + return lines.join(EOL); +}; diff --git a/src/tui-solid/app.tsx b/src/tui-solid/app.tsx index a5dd6a4..9bdeba1 100644 --- a/src/tui-solid/app.tsx +++ b/src/tui-solid/app.tsx @@ -17,6 +17,7 @@ import { getExecutionState, } from "@services/chat-tui-service"; import { TERMINAL_RESET } from "@constants/terminal"; +import { formatExitMessage } from "@services/exit-message"; import { copyToClipboard } from "@services/clipboard/text-clipboard"; import versionData from "@/version.json"; import { ExitProvider, useExit } from "@tui-solid/context/exit"; @@ -25,6 +26,7 @@ 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"; @@ -111,6 +113,7 @@ function AppContent(props: AppProps) { const toast = useToast(); const theme = useTheme(); const renderer = useRenderer(); + renderer.disableStdoutInterception(); const [fileList, setFileList] = createSignal([]); setAppStoreRef(app); @@ -561,6 +564,16 @@ export function tui(options: TuiRenderOptions): Promise { const handleExit = (output: TuiOutput): void => { try { writeSync(1, TERMINAL_RESET); + + const state = appStore.getState(); + const firstUserLog = state?.logs?.find( + (log: { type: string }) => log.type === "user", + ); + const sessionTitle = firstUserLog?.content; + const exitMsg = formatExitMessage(output.sessionId, sessionTitle); + if (exitMsg) { + writeSync(1, exitMsg); + } } catch { // Ignore - stdout may already be closed } @@ -571,7 +584,6 @@ export function tui(options: TuiRenderOptions): Promise { targetFps: 60, exitOnCtrlC: false, useKittyKeyboard: {}, - useMouse: true, }); }); }