Skip to content

Commit 7ba2323

Browse files
authored
feat(tui): add TUI foundation with event architecture and ratatui components (#10)
## Summary Add a TUI foundation with ratatui + crossterm, including an event architecture, component system, and agent loop refactoring. This is the structural plumbing that future work (markdown rendering, multi-line input, tool display, viewport virtualization) builds on. The core design decision is the `AgentSink` trait, which decouples the agent loop from display. The same `agent_turn` code drives all three modes — the sink implementation determines where events go. `ChannelSink` sends events through an mpsc channel for the TUI's `tokio::select!` event loop; `StdioSink` writes directly to stdout / stderr for the bare REPL and headless modes. This keeps the agent loop DRY across all display modes. - Add `tui/event.rs`: `AgentEvent` enum (stream tokens, thinking, tool calls, turn complete, error), `UserAction` enum, `AgentSink` trait with `ChannelSink` and `StdioSink` implementations. `tool_call_title` helper extracts display titles (bash commands shown inline) for both sinks. - Add `tui/app.rs`: root `App` struct with `tokio::select!` event loop multiplexing crossterm events, agent channel events, and a 60 FPS tick interval for render coalescing. Layout: status bar (top) + chat (fill) + input (bottom). `finish_turn` helper consolidates the turn-complete reset sequence. App creates a single `Theme` and passes it to all component constructors. - Add `tui/component.rs`: `Component` trait (`handle_event` + `render`) and `Action` enum for upward communication from components to the root app. - Add `tui/components/chat.rs`: scrollable chat message list with streaming buffer. Auto-scrolls on new content, pauses on manual scroll up, resumes at bottom. `Cell` for content height avoids double `build_text` per frame. - Add `tui/components/input.rs`: single-line input with cursor navigation, character insertion, backspace, forward-delete. Enter submits, Ctrl+C / Ctrl+D quits. Cached `char_count` avoids repeated O(n) scans; `byte_offset` helper DRYs up char-to-byte index conversion. - Add `tui/components/status.rs`: status bar displaying `ox │ model │ status` with pipe separators. Status: Idle (green), Streaming (yellow), ToolRunning (yellow). - Add `tui/terminal.rs`: terminal init / restore, synchronized output (`BeginSynchronizedUpdate` / `EndSynchronizedUpdate` for flicker prevention), panic-safe restore hook. - Add `tui/theme.rs`: Catppuccin Mocha palette with 11 named color slots (grouped: text hierarchy → surfaces → semantic accents → status indicators by ascending severity) and style helpers. `Copy` derive for zero-cost pass-by-value. `separator_span` helper DRYs up the styled pipe separator. Transparent background, designed for future user-configurable overrides. - Refactor `main.rs` into three entry points: `run_tui` (default), `bare_repl` (`--no-tui`), `headless` (`-p`). Tool registry created once and passed to all modes. `agent_turn` / `stream_response` take `&dyn AgentSink`. `apply_delta` returns `Result` to propagate sink errors. Abort agent task on quit to prevent hanging on active API streams. - Add `docs/research/tui.md`: reference project analysis (claude-code Ink architecture, opencode, Codex, Rust TUI apps), flickering root cause investigation, streaming markdown strategy, design decisions. ## Changes | File | Description | | ---- | ----------- | | `Cargo.toml` | Add `ratatui`, `crossterm`, `futures` to workspace dependencies | | `crates/oxide-code/Cargo.toml` | Wire workspace deps to binary crate | | `client/anthropic.rs` | `#[derive(Clone)]` on `Client`, `model()` accessor, preserve initial text from `ContentBlockInfo::Text` | | `main.rs` | Three entry points (`run_tui`, `bare_repl`, `headless`), tool registry created once and passed, `agent_turn` / `stream_response` take `&dyn AgentSink`, `apply_delta` returns `Result`, abort agent task on quit | | `tui.rs` | Module root with `pub(crate)` submodules | | `tui/event.rs` | `AgentEvent`, `UserAction`, `AgentSink` trait, `ChannelSink`, `StdioSink`, `tool_call_title` helper | | `tui/theme.rs` | Catppuccin Mocha palette (11 color slots, `Copy`), style helpers, `separator_span` | | `tui/terminal.rs` | Terminal init / restore, synchronized output, panic hook | | `tui/component.rs` | `Component` trait and `Action` enum | | `tui/components/chat.rs` | Scrollable chat with streaming buffer, auto-scroll, `Cell` content height | | `tui/components/input.rs` | Single-line input with cursor, Delete key, cached `char_count`, `byte_offset` helper | | `tui/components/status.rs` | Status bar with model and status indicator | | `tui/app.rs` | Root `App` with `tokio::select!` loop, shared `Theme`, `finish_turn`, `tool_call_title` | | `CLAUDE.md` | Updated crate structure diagram with full `tui/` module tree | | `docs/roadmap.md` | Move TUI foundation to "Working Today", update "Current Focus" | | `docs/research/tui.md` | TUI research: reference projects, flickering, ecosystem, design decisions | | `docs/research/README.md` | Research docs index | ## Test plan - [x] `cargo fmt --all --check` — clean - [x] `cargo build` compiles cleanly - [x] `cargo clippy --all-targets -- -D warnings` — zero warnings - [x] `cargo test` — 226 tests pass - [x] `ox` launches TUI with status bar, chat area, input area - [x] `ox --no-tui` launches bare REPL with same agent behavior - [x] `ox -p "hello"` runs headless and exits - [x] Typing visible in input, Enter submits, response streams in chat - [x] Mouse scroll and keyboard scroll (when input disabled) work - [x] Ctrl+C / Ctrl+D quit from any state - [x] Quit during active streaming aborts cleanly (no hang) - [x] `pub(crate)` visibility on all TUI types
1 parent 820f463 commit 7ba2323

22 files changed

Lines changed: 2751 additions & 180 deletions

File tree

.cspell/words.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,40 @@
11
anthropic
22
anthropics
33
anyhow
4+
atuin
5+
catppuccin
46
claudemd
57
clippy
8+
cmark
69
codex
710
creds
11+
crossterm
812
deserialize
913
dtolnay
1014
feff
15+
gitui
1116
hakula
1217
impls
1318
indoc
19+
isatty
20+
mpsc
1421
println
22+
pulldown
1523
RAII
1624
ratatui
1725
replacen
1826
reqwest
1927
rfind
2028
rustls
2129
serde
30+
serie
2231
strum
2332
swatinem
33+
syntect
2434
thiserror
35+
throbber
2536
tokio
2637
tracing
2738
venv
2839
xxhash
40+
yazi

CLAUDE.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,32 @@ ox # Start an interactive session
3131
├── config/
3232
│ ├── file.rs # TOML config file discovery, parsing, and merge (user + project)
3333
│ └── oauth.rs # Claude Code OAuth credentials (macOS Keychain + file), token refresh, file locking
34-
├── main.rs # CLI entry point, agent loop, async REPL
34+
├── main.rs # CLI entry point, agent loop, TUI / REPL / headless dispatch
3535
├── message.rs # Conversation message types
3636
├── prompt.rs # System prompt builder (section assembly, static content)
3737
├── prompt/
3838
│ ├── environment.rs # Runtime environment detection (platform, git, date)
3939
│ └── instructions.rs # Instruction file discovery and loading (CLAUDE.md, AGENTS.md)
4040
├── tool.rs # Tool trait, registry, definitions
41-
└── tool/
42-
├── bash.rs # Shell command execution with timeout
43-
├── edit.rs # Exact string replacement in files
44-
├── glob.rs # File pattern matching (glob)
45-
├── grep.rs # Content search via regex
46-
├── read.rs # File reading with line numbers and pagination
47-
└── write.rs # File writing with directory creation
41+
├── tool/
42+
│ ├── bash.rs # Shell command execution with timeout
43+
│ ├── edit.rs # Exact string replacement in files
44+
│ ├── glob.rs # File pattern matching (glob)
45+
│ ├── grep.rs # Content search via regex
46+
│ ├── read.rs # File reading with line numbers and pagination
47+
│ └── write.rs # File writing with directory creation
48+
├── tui.rs # TUI module root
49+
└── tui/
50+
├── app.rs # Root App struct, tokio::select! event loop, render dispatch
51+
├── component.rs # Component trait and Action enum
52+
├── components.rs # Components module root
53+
├── components/
54+
│ ├── chat.rs # Scrollable chat message list with streaming buffer
55+
│ ├── input.rs # Single-line input area with cursor navigation
56+
│ └── status.rs # Status bar (model, status indicator)
57+
├── event.rs # AgentEvent, UserAction, AgentSink trait, ChannelSink, StdioSink
58+
├── terminal.rs # Terminal init / restore, synchronized output, panic hook
59+
└── theme.rs # Catppuccin Mocha palette, style helpers
4860
```
4961

5062
## Coding Conventions

0 commit comments

Comments
 (0)