42d1175b02
- 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
385 lines
11 KiB
Markdown
385 lines
11 KiB
Markdown
# 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
|