From 42d1175b02127c2a546b5c502edb1949667d4942 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 10:41:20 -0800 Subject: [PATCH] docs: add TUI redesign plan - Streaming responses (chunk-based) - Rich markdown rendering with syntax highlighting - Model switching via /model command - Scrollable message history - Enhanced status bar with token usage - Inline error handling with auto-retry - Feature parity between minimal and fullscreen modes --- docs/plans/2026-02-05-tui-redesign.md | 384 ++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 docs/plans/2026-02-05-tui-redesign.md diff --git a/docs/plans/2026-02-05-tui-redesign.md b/docs/plans/2026-02-05-tui-redesign.md new file mode 100644 index 0000000..356541e --- /dev/null +++ b/docs/plans/2026-02-05-tui-redesign.md @@ -0,0 +1,384 @@ +# 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