import { VERSION, type Theme } from "@earendil-works/pi-coding-agent"; import { renderAnimation, type LogoAnimation } from "./logos/animations.js"; import { buildResponsiveLogoHeaderLines } from "./logos/text-table.js"; const LOGO_GAP = 2; const HEADER_LEFT_PADDING = 2; const MIN_CONTENT_WIDTH = 24; const LIFTED_HELP_RESERVED_LINES = 3; const MINI_LOGO_LINES = ["█▀█", "█▀ █"]; const COMPACT_SHORTCUTS = "escape interrupt · ctrl+c/ctrl+d clear/exit · / commands · ! bash · ctrl+o more"; const MINI_SHORTCUTS = "escape to interrupt"; const EXPANDED_HELP_SHORTCUTS = [ "escape to interrupt", "ctrl+c to clear", "ctrl+c twice to exit", "ctrl+d to exit (empty)", "ctrl+z to suspend", "ctrl+k to delete to end", "shift+tab to cycle thinking level", "ctrl+p/shift+ctrl+p to cycle models", "ctrl+l to select model", "ctrl+o to expand tools", "ctrl+t to expand thinking", "ctrl+g for external editor", "/ for commands", "! to run bash", "!! to run bash (no context)", "alt+enter to queue follow-up", "alt+up to edit all queued messages", "ctrl+v to paste image", "drop files to attach", ]; type HeaderTheme = Pick; type HeaderTui = { requestRender(): void; }; interface HeaderComponentOptions { tui: HeaderTui; theme: HeaderTheme; logoLines: string[]; animation: LogoAnimation; } export interface HeaderComponent { render(width: number): string[]; setExpanded(value: boolean): void; setLogoAnimation(logoLines: string[], animation: LogoAnimation): void; invalidate(): void; dispose(): void; stopAnimation(): void; } function buildCompactLines(titleLine: string, onboardingLine: string, theme: HeaderTheme): string[] { return [ titleLine, theme.fg("muted", COMPACT_SHORTCUTS), theme.fg("dim", "Press ctrl+o to show full startup help and loaded resources."), "", onboardingLine, ]; } function buildExpandedHelpLines(theme: HeaderTheme): string[] { return EXPANDED_HELP_SHORTCUTS.map((shortcut) => theme.fg("muted", shortcut)); } export function createHeaderComponent({ tui, theme, logoLines, animation }: HeaderComponentOptions): HeaderComponent { let expanded = false; let selectedLogoLines = logoLines; let selectedAnimation = animation; let frameCount = 0; let animationStopped = selectedAnimation.id === "static"; let interval: ReturnType | undefined; const titleLine = `${theme.bold(theme.fg("accent", "pi"))}${theme.fg("dim", ` v${VERSION}`)}`; const miniHelpLine = theme.fg("muted", MINI_SHORTCUTS); const onboardingLine = theme.fg("dim", "Pi can explain its own features and look up its docs. Ask it how to use or extend Pi."); const compactLines = buildCompactLines(titleLine, onboardingLine, theme); const expandedHelpLines = buildExpandedHelpLines(theme); function stopInterval(): void { if (!interval) return; clearInterval(interval); interval = undefined; } function startInterval(): void { if (interval) return; interval = setInterval(() => { frameCount += 1; tui.requestRender(); }, 120); } if (!animationStopped) { startInterval(); } return { render(width: number): string[] { const logoLines = renderAnimation(selectedAnimation, { logoLines: selectedLogoLines, frameCount, colorize: animationStopped ? undefined : (color, value) => theme.fg(color, value), }); return buildResponsiveLogoHeaderLines({ width, expanded, logoLines, miniLogoLines: MINI_LOGO_LINES, titleLine, miniHelpLine, onboardingLine, compactLines, expandedHelpLines, gap: LOGO_GAP, leftPadding: HEADER_LEFT_PADDING, minContentWidth: MIN_CONTENT_WIDTH, liftedHelpReservedLines: LIFTED_HELP_RESERVED_LINES, }); }, setExpanded(value: boolean): void { if (expanded === value) return; expanded = value; tui.requestRender(); }, setLogoAnimation(nextLogoLines: string[], nextAnimation: LogoAnimation): void { selectedLogoLines = nextLogoLines; selectedAnimation = nextAnimation; frameCount = 0; animationStopped = nextAnimation.id === "static"; if (animationStopped) { stopInterval(); } else { startInterval(); } tui.requestRender(); }, invalidate(): void { tui.requestRender(); }, stopAnimation(): void { if (animationStopped) return; animationStopped = true; stopInterval(); tui.requestRender(); }, dispose(): void { stopInterval(); }, }; }