Skip to content

feat(tui): add TUI foundation with event architecture and ratatui components#10

Merged
hakula139 merged 20 commits intomainfrom
feat/tui
Apr 6, 2026
Merged

feat(tui): add TUI foundation with event architecture and ratatui components#10
hakula139 merged 20 commits intomainfrom
feat/tui

Conversation

@hakula139
Copy link
Copy Markdown
Owner

@hakula139 hakula139 commented Apr 6, 2026

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

  • cargo fmt --all --check — clean
  • cargo build compiles cleanly
  • cargo clippy --all-targets -- -D warnings — zero warnings
  • cargo test — 226 tests pass
  • ox launches TUI with status bar, chat area, input area
  • ox --no-tui launches bare REPL with same agent behavior
  • ox -p "hello" runs headless and exits
  • Typing visible in input, Enter submits, response streams in chat
  • Mouse scroll and keyboard scroll (when input disabled) work
  • Ctrl+C / Ctrl+D quit from any state
  • Quit during active streaming aborts cleanly (no hang)
  • pub(crate) visibility on all TUI types

Synthesize findings from claude-code (Ink), opencode (Solid.js),
ratatui ecosystem, and the flickering issue (anthropics/claude-code#1913)
into a research doc covering reference projects, anti-flicker techniques,
crate stack, streaming markdown strategy, and design decisions.
Add ratatui, crossterm (with event-stream), and color-eyre to workspace
dependencies. Derive Clone on Client so it can be moved into a spawned
tokio task for the TUI agent loop. Add Client::model() accessor.
Introduce the TUI module structure:
- event.rs: AgentEvent/UserAction enums, AgentSink trait with
  ChannelSink (TUI) and StdioSink (bare REPL) implementations
- theme.rs: Catppuccin Mocha palette with transparent background
- terminal.rs: alternate screen setup/restore, synchronized output,
  panic hook for terminal cleanup
- component.rs: Component trait for self-contained UI widgets
- components/: ChatView (scrollable message list), InputArea (single-line
  input with cursor), StatusBar (model + status display)
- app.rs: root App struct with tokio::select! event loop over crossterm
  events, agent events, and 60fps tick interval
Replace direct stdout/stderr writes in the agent loop with AgentSink
trait calls. The same agent_turn/stream_response code now drives three
modes:
- TUI (default): ChannelSink sends events to the ratatui render loop
- Bare REPL (--no-tui): StdioSink writes directly to stdout
- Headless (-p): StdioSink for single-prompt CI usage

Auto-detects non-interactive terminals via std::io::IsTerminal.
…ojects

Replace Oatmeal (unmaintained) and bottom with a curated list of
modern, high-star ratatui apps: Codex (67k+), yazi (33k+), atuin (22k+),
gitui (19k+), serie, television, slumber.
@hakula139 hakula139 added the enhancement New feature or request label Apr 6, 2026
@hakula139 hakula139 self-assigned this Apr 6, 2026
hakula139 added 15 commits April 6, 2026 17:50
Fix dirty flag bug: keystrokes in the input area were not triggering
re-renders because handle_event returned None for character input,
leaving the dirty flag unset. Now all key events mark dirty.

Improve spacing: add double blank lines between messages, blank line
after role labels, increase content indent to 4 chars, add role
indicators (❯ You / ⟡ Assistant), add bottom border to status bar,
increase status bar to 2 rows.
- Remove unused color-eyre dependency.
- Eliminate duplicate ToolRegistry: extract create_tool_registry()
  helper, remove unused tools param from run_tui.
- Remove show_thinking from agent_turn/stream_response (now handled
  by the sink).
- Take user_rx by value in agent_loop_task (sole consumer).
- Fix #[expect] reason strings to describe current state, not future
  plans (project convention).
- Update stale tool.rs exit_code expect reason.
- Update CLAUDE.md crate structure with tui/ modules.
- Update roadmap.md with shipped TUI foundation.
Remove blank line between role label and content, reduce inter-message
gap from two blank lines to one. Matches Claude Code's compact layout.
API stream tokens may include leading newlines, causing a blank line
between the role label and content. Trim before iterating lines.
- Route mouse scroll events directly to ChatView (previously never
  triggered re-render because handle_event returns None, not an Action).
- Route keyboard scroll keys to ChatView when input is disabled
  (previously all Key events went exclusively to InputArea).
- Use Cell<u16> for content_height so render_inner (&self) can cache
  the line count during the render pass, eliminating the second
  build_text call in update_layout.
- Remove dead if-auto_scroll block and sync_scroll method.
- Store initial text from ContentBlockInfo::Text in the accumulator
  instead of discarding it. Send to display if non-empty.
- Make apply_delta return Result and propagate sink send errors,
  so broken pipes in StdioSink are detected instead of silenced.
- Normalize let _ = to _ = throughout for consistency.
- Add Ctrl+D as a quit shortcut alongside Ctrl+C. Escape is reserved
  for future rewind functionality.
- Use saturating_add and clamp cursor position to prevent u16 overflow
  on very wide input.
- Fix doc comments that incorrectly mentioned Escape as quit binding.
Binary crate has no external consumers. Replace pub with pub(crate)
on all TUI types, fields, methods, and module declarations per
project visibility conventions.
Replace "PR 3.x" references with generic phrasing. Internal planning
details belong in the plan file, not in source comments.
- Abort agent task on quit to prevent hanging on active API streams.
  Log panics from the agent task instead of silently dropping them.
- Use saturating_add in scroll_down to prevent u16 wrapping.
- Show bash commands inline in TUI tool call display.
- Extract push_message_lines helper to deduplicate build_text logic.
- Make ChatMessage and ChatRole private (only used within chat.rs).
- Fix theme doc referencing unimplemented config section.
Move public API before trait impl, private helpers after their callers.
Group: constructor + API → Component impl → scroll helpers → rendering.
Swap first_user_text and parse_sse_frame test sections to match the
order these functions appear in production code.
- Mouse scroll now moves 1 line per tick instead of 3 (merge
  Up/ScrollUp and Down/ScrollDown arms since they share the body).
- Add forward-delete (Delete key) in input area.
- Pass shared Theme from App to all component constructors instead
  of each component creating its own Theme::default(). Add Copy
  derive to Theme (11 Color values, trivially cheap to copy).
- Extract separator_span() helper on Theme to DRY up the styled
  pipe separator constructed in both StatusBar and InputArea.
- Extract byte_offset() helper in InputArea to replace 5 repeated
  char-to-byte-index conversion blocks.
- Cache char_count in InputArea to avoid 3 redundant O(n) scans
  of buffer.chars().count().
- Extract tool_call_title() in event.rs to DRY up bash command
  display logic between TUI App and StdioSink.
- Extract finish_turn() in App to DRY up the turn-complete reset
  sequence (commit_streaming + set Idle + enable input).
- Fix function ordering: move handle_action after its caller
  handle_crossterm_event, move submit to private helpers section.
- Create tool registry once in main() and pass to all modes.
- Replace unicode ellipsis with ASCII in status bar and comments.
Group fields and style methods into: text hierarchy → surfaces →
semantic accents → status indicators (ascending severity: info →
success → warning → error). Composite helpers (separator, border)
follow at the end.
@hakula139 hakula139 merged commit 7ba2323 into main Apr 6, 2026
1 check passed
@hakula139 hakula139 deleted the feat/tui branch April 6, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant