Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
AgentSinktrait, which decouples the agent loop from display. The sameagent_turncode drives all three modes — the sink implementation determines where events go.ChannelSinksends events through an mpsc channel for the TUI'stokio::select!event loop;StdioSinkwrites directly to stdout / stderr for the bare REPL and headless modes. This keeps the agent loop DRY across all display modes.tui/event.rs:AgentEventenum (stream tokens, thinking, tool calls, turn complete, error),UserActionenum,AgentSinktrait withChannelSinkandStdioSinkimplementations.tool_call_titlehelper extracts display titles (bash commands shown inline) for both sinks.tui/app.rs: rootAppstruct withtokio::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_turnhelper consolidates the turn-complete reset sequence. App creates a singleThemeand passes it to all component constructors.tui/component.rs:Componenttrait (handle_event+render) andActionenum for upward communication from components to the root app.tui/components/chat.rs: scrollable chat message list with streaming buffer. Auto-scrolls on new content, pauses on manual scroll up, resumes at bottom.Cellfor content height avoids doublebuild_textper frame.tui/components/input.rs: single-line input with cursor navigation, character insertion, backspace, forward-delete. Enter submits, Ctrl+C / Ctrl+D quits. Cachedchar_countavoids repeated O(n) scans;byte_offsethelper DRYs up char-to-byte index conversion.tui/components/status.rs: status bar displayingox │ model │ statuswith pipe separators. Status: Idle (green), Streaming (yellow), ToolRunning (yellow).tui/terminal.rs: terminal init / restore, synchronized output (BeginSynchronizedUpdate/EndSynchronizedUpdatefor flicker prevention), panic-safe restore hook.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.Copyderive for zero-cost pass-by-value.separator_spanhelper DRYs up the styled pipe separator. Transparent background, designed for future user-configurable overrides.main.rsinto three entry points:run_tui(default),bare_repl(--no-tui),headless(-p). Tool registry created once and passed to all modes.agent_turn/stream_responsetake&dyn AgentSink.apply_deltareturnsResultto propagate sink errors. Abort agent task on quit to prevent hanging on active API streams.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
Cargo.tomlratatui,crossterm,futuresto workspace dependenciescrates/oxide-code/Cargo.tomlclient/anthropic.rs#[derive(Clone)]onClient,model()accessor, preserve initial text fromContentBlockInfo::Textmain.rsrun_tui,bare_repl,headless), tool registry created once and passed,agent_turn/stream_responsetake&dyn AgentSink,apply_deltareturnsResult, abort agent task on quittui.rspub(crate)submodulestui/event.rsAgentEvent,UserAction,AgentSinktrait,ChannelSink,StdioSink,tool_call_titlehelpertui/theme.rsCopy), style helpers,separator_spantui/terminal.rstui/component.rsComponenttrait andActionenumtui/components/chat.rsCellcontent heighttui/components/input.rschar_count,byte_offsethelpertui/components/status.rstui/app.rsAppwithtokio::select!loop, sharedTheme,finish_turn,tool_call_titleCLAUDE.mdtui/module treedocs/roadmap.mddocs/research/tui.mddocs/research/README.mdTest plan
cargo fmt --all --check— cleancargo buildcompiles cleanlycargo clippy --all-targets -- -D warnings— zero warningscargo test— 226 tests passoxlaunches TUI with status bar, chat area, input areaox --no-tuilaunches bare REPL with same agent behaviorox -p "hello"runs headless and exitspub(crate)visibility on all TUI types