From f892bbe6cabab5824cf8a5101d7364217fb249c1 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Tue, 10 Feb 2026 13:11:32 -0800 Subject: [PATCH] feat(tui): add ASCII art banner on startup --- src/frontends/tui/banner.ts | 32 ++++++++++++++++++++ src/frontends/tui/components/MessageList.tsx | 8 ++++- src/frontends/tui/index.ts | 1 + src/frontends/tui/minimal.ts | 4 ++- 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/frontends/tui/banner.ts diff --git a/src/frontends/tui/banner.ts b/src/frontends/tui/banner.ts new file mode 100644 index 0000000..d6cf413 --- /dev/null +++ b/src/frontends/tui/banner.ts @@ -0,0 +1,32 @@ +// ASCII art banner for Flynn TUI startup + +/** + * Flynn ASCII art banner using block characters. + * Raw art lines without any color codes — callers apply their own styling. + */ +export const FLYNN_BANNER = ` + ███████╗██╗ ██╗ ██╗███╗ ██╗███╗ ██╗ + ██╔════╝██║ ╚██╗ ██╔╝████╗ ██║████╗ ██║ + █████╗ ██║ ╚████╔╝ ██╔██╗ ██║██╔██╗ ██║ + ██╔══╝ ██║ ╚██╔╝ ██║╚██╗██║██║╚██╗██║ + ██║ ███████╗ ██║ ██║ ╚████║██║ ╚████║ + ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝`; + +/** + * Returns the banner with ANSI color codes for terminal output. + * Uses orange (208) for the art and dim for the box frame. + */ +export function getColoredBanner(): string { + const orange = '\x1b[38;5;208m'; + const reset = '\x1b[0m'; + + return `${orange}${FLYNN_BANNER}${reset}`; +} + +/** + * Returns plain banner lines for use in Ink/React components. + * Each line is returned individually for flexible rendering. + */ +export function getBannerLines(): string[] { + return FLYNN_BANNER.split('\n').filter((line) => line.length > 0); +} diff --git a/src/frontends/tui/components/MessageList.tsx b/src/frontends/tui/components/MessageList.tsx index 517f1c1..49dc892 100644 --- a/src/frontends/tui/components/MessageList.tsx +++ b/src/frontends/tui/components/MessageList.tsx @@ -3,6 +3,7 @@ import { Box, Text, Static } from 'ink'; import type { Message } from '../../../models/types.js'; import { getMessageText } from '../../../models/media.js'; import { renderMarkdown } from '../markdown.js'; +import { getBannerLines } from '../banner.js'; export interface MessageListProps { messages: Message[]; @@ -80,7 +81,12 @@ export const MessageList = memo(function MessageList({ return ( {visibleMessages.length === 0 && !streamingContent ? ( - No messages yet. Start typing to chat with Flynn. + + {getBannerLines().map((line, i) => ( + {line} + ))} + {'\n'}Start typing to chat with Flynn. + ) : ( <> diff --git a/src/frontends/tui/index.ts b/src/frontends/tui/index.ts index 2e01b51..cf4187e 100644 --- a/src/frontends/tui/index.ts +++ b/src/frontends/tui/index.ts @@ -13,5 +13,6 @@ export { export { renderMarkdown } from './markdown.js'; export { parseCommand as parseCommandUtil, getHelpText, resolveModelAlias } from './commands.js'; +export { FLYNN_BANNER, getColoredBanner, getBannerLines } from './banner.js'; export { App, StatusBar, MessageList, InputBar } from './components/index.js'; diff --git a/src/frontends/tui/minimal.ts b/src/frontends/tui/minimal.ts index 01147b6..acea89c 100644 --- a/src/frontends/tui/minimal.ts +++ b/src/frontends/tui/minimal.ts @@ -10,6 +10,7 @@ import { OllamaClient, LlamaCppClient } from '../../models/index.js'; import { createClientFromConfig } from '../../daemon/index.js'; import { loginGitHub } from '../../auth/index.js'; import type { PairingManager } from '../../channels/pairing.js'; +import { getColoredBanner } from './banner.js'; export { parseCommand, type Command }; @@ -125,7 +126,8 @@ export class MinimalTui { readline.emitKeypressEvents(process.stdin); } - console.log(`${colors.orange}${colors.bold}Flynn TUI${colors.reset} ${colors.dim}(minimal mode)${colors.reset}`); + console.log(getColoredBanner()); + console.log(`\n${colors.orange}${colors.bold}Flynn TUI${colors.reset} ${colors.dim}(minimal mode)${colors.reset}`); console.log(`${colors.gray}Type /help for commands, /fullscreen for panel mode${colors.reset}\n`); await this.promptLoop();