Files
owlen/crates/app/ui/src/components/status_bar.rs
vikingowl 09c8c9d83e 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>
2025-11-01 22:57:25 +01:00

110 lines
3.4 KiB
Rust

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);
}
}