# 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 <name>` 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 <name>` | 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<ChatResponse>;
  chatStream?(request: ChatRequest): AsyncIterable<ChatStreamEvent>;
}
```

**AnthropicClient.chatStream():**

```typescript
async *chatStream(request: ChatRequest): AsyncIterable<ChatStreamEvent> {
  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 <name>` - 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
