feat(ui): add TUI with streaming agent integration and theming
Add a new terminal UI crate (crates/app/ui) built with ratatui providing an interactive chat interface with real-time LLM streaming and tool visualization. Features: - Chat panel with horizontal padding for improved readability - Input box with cursor navigation and command history - Status bar with session statistics and uniform background styling - 7 theme presets: Tokyo Night (default), Dracula, Catppuccin, Nord, Synthwave, Rose Pine, and Midnight Ocean - Theme switching via /theme <name> and /themes commands - Streaming LLM responses that accumulate into single messages - Real-time tool call visualization with success/error states - Session tracking (messages, tokens, tool calls, duration) - REPL commands: /help, /status, /cost, /checkpoint, /rewind, /clear, /exit Integration: - CLI automatically launches TUI mode when running interactively (no prompt) - Falls back to legacy text REPL with --no-tui flag - Uses existing agent loop with streaming support - Supports all existing tools (read, write, edit, glob, grep, bash) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
109
crates/app/ui/src/components/status_bar.rs
Normal file
109
crates/app/ui/src/components/status_bar.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use crate::theme::Theme;
|
||||
use agent_core::SessionStats;
|
||||
use permissions::Mode;
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
text::{Line, Span},
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub struct StatusBar {
|
||||
model: String,
|
||||
mode: Mode,
|
||||
stats: SessionStats,
|
||||
last_tool: Option<String>,
|
||||
theme: Theme,
|
||||
}
|
||||
|
||||
impl StatusBar {
|
||||
pub fn new(model: String, mode: Mode, theme: Theme) -> Self {
|
||||
Self {
|
||||
model,
|
||||
mode,
|
||||
stats: SessionStats::new(),
|
||||
last_tool: None,
|
||||
theme,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_stats(&mut self, stats: SessionStats) {
|
||||
self.stats = stats;
|
||||
}
|
||||
|
||||
pub fn set_last_tool(&mut self, tool: String) {
|
||||
self.last_tool = Some(tool);
|
||||
}
|
||||
|
||||
pub fn render(&self, frame: &mut Frame, area: Rect) {
|
||||
let elapsed = self.stats.start_time.elapsed().unwrap_or_default();
|
||||
let elapsed_str = SessionStats::format_duration(elapsed);
|
||||
|
||||
let (mode_str, mode_icon) = match self.mode {
|
||||
Mode::Plan => ("Plan", "🔍"),
|
||||
Mode::AcceptEdits => ("AcceptEdits", "✏️"),
|
||||
Mode::Code => ("Code", "⚡"),
|
||||
};
|
||||
|
||||
let last_tool_str = self
|
||||
.last_tool
|
||||
.as_ref()
|
||||
.map(|t| format!("● {}", t))
|
||||
.unwrap_or_else(|| "○ idle".to_string());
|
||||
|
||||
// Build status line with colorful sections
|
||||
let separator_style = self.theme.status_bar;
|
||||
let mut spans = vec![
|
||||
Span::styled(" ", separator_style),
|
||||
Span::styled(mode_icon, self.theme.status_bar),
|
||||
Span::styled(" ", separator_style),
|
||||
Span::styled(mode_str, self.theme.status_bar),
|
||||
Span::styled(" │ ", separator_style),
|
||||
Span::styled("⚙", self.theme.status_bar),
|
||||
Span::styled(" ", separator_style),
|
||||
Span::styled(&self.model, self.theme.status_bar),
|
||||
Span::styled(" │ ", separator_style),
|
||||
Span::styled(
|
||||
format!("{} msgs", self.stats.total_messages),
|
||||
self.theme.status_bar,
|
||||
),
|
||||
Span::styled(" │ ", separator_style),
|
||||
Span::styled(
|
||||
format!("{} tools", self.stats.total_tool_calls),
|
||||
self.theme.status_bar,
|
||||
),
|
||||
Span::styled(" │ ", separator_style),
|
||||
Span::styled(
|
||||
format!("~{} tok", self.stats.estimated_tokens),
|
||||
self.theme.status_bar,
|
||||
),
|
||||
Span::styled(" │ ", separator_style),
|
||||
Span::styled("⏱", self.theme.status_bar),
|
||||
Span::styled(" ", separator_style),
|
||||
Span::styled(elapsed_str, self.theme.status_bar),
|
||||
Span::styled(" │ ", separator_style),
|
||||
Span::styled(last_tool_str, self.theme.status_bar),
|
||||
];
|
||||
|
||||
// Add help text on the right
|
||||
let help_text = " ? /help ";
|
||||
|
||||
// Calculate current length
|
||||
let current_len: usize = spans.iter()
|
||||
.map(|s| unicode_width::UnicodeWidthStr::width(s.content.as_ref()))
|
||||
.sum();
|
||||
|
||||
// Add padding
|
||||
let padding = area
|
||||
.width
|
||||
.saturating_sub((current_len + help_text.len()) as u16);
|
||||
|
||||
spans.push(Span::styled(" ".repeat(padding as usize), separator_style));
|
||||
spans.push(Span::styled(help_text, self.theme.status_bar));
|
||||
|
||||
let line = Line::from(spans);
|
||||
let paragraph = Paragraph::new(line);
|
||||
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user