# Flynn TUI Redesign **Date:** 2026-02-05 **Status:** Approved **Author:** Will + Claude ## Overview Redesign Flynn's TUI to provide a polished chat experience with streaming responses, rich markdown rendering, and model switching. Both minimal and fullscreen modes get the same core features. ## Goals - Streaming responses (chunk-based, as API delivers) - Markdown rendering (bold, italic, code blocks with syntax highlighting, tables, links, lists) - Model switching via `/model ` command - Scrollable message history with keyboard navigation - Enhanced status bar with token usage - Inline error handling with auto-retry - Feature parity between minimal and fullscreen modes ## User Documentation ### Modes **Minimal mode** (`pnpm tui`) - Readline-based interface for quick queries - Streaming text output in terminal - Markdown rendered inline - Type `/fs` to switch to fullscreen **Fullscreen mode** (`pnpm tui -f` or `/fs`) - Panel-based UI with status bar, message list, input bar - Scrollable message history - Press `Esc` to exit ### Commands | Command | Description | |---------|-------------| | `/help` | Show available commands | | `/model ` | Switch model (e.g., `/model local`, `/model opus`) | | `/model` | Show current model and available options | | `/reset` | Clear conversation history | | `/status` | Show session info and token usage | | `/quit` or `/exit` | Exit TUI | | `/fs` or `/fullscreen` | Switch to fullscreen mode (minimal only) | ### Keybindings (Fullscreen) | Key | Action | |-----|--------| | `Esc` | Exit (or cancel streaming) | | `↑/↓` | Scroll messages | | `PgUp/PgDn` | Scroll full page | | `Home/End` | Jump to start/end of history | ### Streaming - Responses appear chunk-by-chunk as they arrive from the API - Press `Esc` during streaming to cancel (partial response is kept) - Input is disabled while streaming ### Error Handling Errors appear inline in the chat: ``` You: Tell me about quantum computing ⚠ Connection error: Request timed out. Retrying (1/3)... Flynn: Quantum computing uses quantum mechanical phenomena... ``` - Network/timeout errors auto-retry up to 3 times - Rate limits show wait time and auto-retry - Auth errors suggest checking config ## Technical Design ### Architecture ``` src/ models/ anthropic.ts # Add streaming method: chatStream() types.ts # Add ChatStreamResponse type router.ts # Add stream routing frontends/tui/ markdown.ts # NEW: Markdown rendering utilities streaming.ts # NEW: Stream handling & display commands.ts # NEW: Unified command parsing & handling minimal.ts # Update: streaming + markdown fullscreen.ts # Update: streaming + markdown components/ App.tsx # Update: streaming state, scroll handling MessageList.tsx # Update: markdown rendering, scroll indicator StatusBar.tsx # Update: token usage display InputBar.tsx # Update: streaming state StreamingText.tsx # NEW: animated streaming display ``` ### Streaming Implementation **New types in `types.ts`:** ```typescript export interface ChatStreamEvent { type: 'content' | 'done' | 'error'; content?: string; // For 'content' events usage?: TokenUsage; // For 'done' events error?: Error; // For 'error' events } export interface ModelClient { chat(request: ChatRequest): Promise; chatStream?(request: ChatRequest): AsyncIterable; } ``` **AnthropicClient.chatStream():** ```typescript async *chatStream(request: ChatRequest): AsyncIterable { const stream = await this.client.messages.stream({ model: this.model, max_tokens: request.maxTokens ?? this.defaultMaxTokens, system: request.system, messages: request.messages.map((m) => ({ role: m.role, content: m.content, })), }); for await (const event of stream) { if (event.type === 'content_block_delta') { yield { type: 'content', content: event.delta.text }; } } const finalMessage = await stream.finalMessage(); yield { type: 'done', usage: { inputTokens: finalMessage.usage.input_tokens, outputTokens: finalMessage.usage.output_tokens, }, }; } ``` **Data flow:** 1. User submits message 2. `modelClient.chatStream()` returns async iterator 3. Chunks arrive → update display incrementally 4. On completion → finalize message, apply markdown, update token count 5. Session saves complete message ### Markdown Rendering **Dependencies:** - `marked` - Markdown parser - `marked-terminal` - Terminal renderer for marked - `cli-highlight` - Syntax highlighting for code blocks **markdown.ts:** ```typescript import { marked } from 'marked'; import TerminalRenderer from 'marked-terminal'; import { highlight } from 'cli-highlight'; marked.setOptions({ renderer: new TerminalRenderer({ code: (code, lang) => highlight(code, { language: lang || 'plaintext' }), // ... other options }), }); export function renderMarkdown(text: string): string { return marked.parse(text); } ``` **Rendering strategy:** - During streaming: show raw text (no markdown processing) - On completion: render full message with markdown - This avoids incomplete markdown artifacts during streaming ### Fullscreen Layout ``` ┌─────────────────────────────────────────────────────────────────┐ │ Flynn | Model: opus-4.5 | Messages: 12 | Tokens: 2.4k/8k in/out │ ← StatusBar ├─────────────────────────────────────────────────────────────────┤ │ │ │ You: How do I parse JSON in Rust? │ │ │ │ Flynn: Use the **serde** crate with `serde_json`: │ ← MessageList │ │ (scrollable) │ ```rust │ │ use serde_json::Value; │ │ let v: Value = serde_json::from_str(data)?; │ │ ``` │ │ [85%] ↓ │ ← scroll indicator ├─────────────────────────────────────────────────────────────────┤ │ > Type a message... (Esc=exit, /help) │ ← InputBar └─────────────────────────────────────────────────────────────────┘ ``` ### StatusBar Updates ```typescript export interface StatusBarProps { sessionId: string; messageCount: number; model: string; tokenUsage?: { input: number; output: number; }; isStreaming?: boolean; } ``` Display format: `Flynn | Model: opus-4.5 | Messages: 12 | Tokens: 2.4k/8k in/out` ### Model Switching **Command parsing:** - `/model` - List available models and current selection - `/model ` - Switch to named model **Model names** map to config tiers: - `default`, `opus` → `models.default` - `fast`, `sonnet` → `models.fast` - `complex` → `models.complex` - `local`, `ollama` → `models.local` **Implementation:** - ModelRouter exposes `setTier(tier: ModelTier)` method - Commands update router tier - StatusBar reflects current model ### Unified Command Handling **New `commands.ts`:** ```typescript export type Command = | { type: 'quit' } | { type: 'reset' } | { type: 'help' } | { type: 'status' } | { type: 'fullscreen' } | { type: 'model'; name?: string } | { type: 'message'; content: string }; export function parseCommand(input: string): Command | null; export function getHelpText(): string; export function getAvailableModels(config: Config): string[]; ``` Both minimal and fullscreen modes use the same command parser. ### Error Handling **Error types and behavior:** | Error Type | Behavior | |------------|----------| | Network/timeout | Auto-retry 3x with exponential backoff, show progress | | Auth (401/403) | Show message, suggest checking config | | Rate limit (429) | Show wait time, auto-retry after delay | | Model error | Show error, suggest `/model` to switch | | Stream interrupted | Keep partial response, mark as `[interrupted]` | **Inline error display:** ```typescript interface SystemMessage { type: 'error' | 'info'; content: string; timestamp: Date; } ``` Rendered with warning icon: `⚠ Connection error: Request timed out. Retrying (1/3)...` ### Scroll Implementation (Fullscreen) **State:** ```typescript const [scrollOffset, setScrollOffset] = useState(0); const [autoScroll, setAutoScroll] = useState(true); ``` **Behavior:** - Auto-scroll to bottom on new messages (if already at bottom) - Manual scroll disables auto-scroll - Pressing `End` re-enables auto-scroll - Scroll indicator shows position: `[85%] ↓` **Keybindings:** ```typescript useInput((input, key) => { if (key.upArrow) scrollUp(); if (key.downArrow) scrollDown(); if (key.pageUp) scrollPageUp(); if (key.pageDown) scrollPageDown(); if (input === 'Home') scrollToTop(); if (input === 'End') scrollToBottom(); }); ``` ## Dependencies **Add to package.json:** ```json { "dependencies": { "marked": "^12.0.0", "marked-terminal": "^7.0.0", "cli-highlight": "^2.1.11" } } ``` ## Implementation Order 1. **Streaming foundation** - Add `ChatStreamEvent` type - Implement `AnthropicClient.chatStream()` - Add `ModelRouter.chatStream()` with fallback handling 2. **Markdown rendering** - Add dependencies - Create `markdown.ts` utilities - Unit test rendering 3. **Unified commands** - Create `commands.ts` - Add `/model` command - Update minimal.ts to use shared parser 4. **Minimal mode updates** - Integrate streaming display - Add markdown rendering - Update error handling 5. **Fullscreen mode updates** - Add `StreamingText` component - Update `MessageList` with markdown + scroll - Update `StatusBar` with token usage - Add keyboard navigation - Update `App` with scroll state 6. **Polish** - Error retry logic - Cancellation handling - Testing & edge cases ## Future Improvements Deferred for later iterations: - Vim-like keybindings (j/k scroll, / search, y yank) - Command palette (Ctrl+P) - Multiple panes (sessions/models sidebar) - Message-based navigation (n/p jump between messages) - Detailed status bar (response time, cost estimate, connection status) - Named sessions (save/load/list) - Session browser UI ## Testing - Unit tests for markdown rendering - Unit tests for command parsing - Integration tests for streaming (mock API) - Manual testing for keyboard navigation - Test both modes for feature parity