Enhance TUI with modern design and improved status rendering

- Updated status bar rendering in `status.rs` to feature a segmented design with clear visual segments for app name, status, and metadata.
- Refined color theme in `theme.rs` to adopt a modern dark palette with neon accents, improving visual hierarchy and readability.
- Revamped workflow panel in `workflow.rs` to display agent statuses as compact cards with state badges, enhancing clarity and user experience.
- Improved overall styling consistency across components, ensuring a cohesive look and feel throughout the TUI.
This commit is contained in:
asepharyana
2026-07-13 05:42:17 +07:00
parent 2310c2df7f
commit 3f5f27c339
6 changed files with 977 additions and 554 deletions
+59 -38
View File
@@ -1,12 +1,12 @@
//! Status bar rendering for the TUI.
//! Status bar rendering for the TUI — modern segmented bar design.
//!
//! Flow: `draw_status_bar` reads live connection/turn state off
//! `AppStateRest` every frame and paints a single-line bar at the top
//! (or bottom, per layout) of the screen showing agent status, provider,
//! and model.
//! `AppStateRest` every frame and paints a single-line bar at the
//! bottom of the screen with three visual segments:
//! [app name + status badge] [spinner + info] [provider · model · tokens]
//!
//! Why: kept as one small, self-contained render function rather than a
//! widget struct, matching the other `view/*` modules' functional style.
//! Design: the status bar uses a dark background with carefully
//! spaced segments so information is scannable at a glance.
use ratatui::layout::Rect;
use ratatui::style::{Style, Modifier};
@@ -15,53 +15,56 @@ use ratatui::widgets::Block;
use ratatui::Frame;
use super::theme::Theme;
/// Render the single-line status bar showing connection state, provider, and model.
/// Render the single-line status bar.
///
/// Flow: derive an agent status label/color from turn-in-flight and API
/// connection state → build left ([zesdex] STATUS) and right
/// (provider · model) span groups → render as one styled Line.
///
/// Return: nothing; draws directly into `frame` at `area`.
/// Layout (left-to-right, space-filling):
/// LEFT: [zesdex] + status indicator (READY/PROG/NOAPI)
/// CENTER: spinner + optional contextual info
/// RIGHT: provider · model · ↑tokens_in ↓tokens_out
pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) {
// Connection status — reflects actual agent readiness:
// PROG → turn is in flight
// READY → connected and ready
// NOAPI → disconnected
let spinner_frames = ["", "", "", "", "", "", "", "", "", ""];
let (agent_status, conn_color) = if state.turn_in_flight() {
let frame = spinner_frames[(state.misc.tick_count as usize / 2) % spinner_frames.len()];
(format!("{} PROG", frame), Theme::MODE_YOLO)
// ── Agent status badge ────────────────────────────────────────────────
let (status_text, status_bg, status_fg) = if state.turn_in_flight() {
let f = spinner_frames[(state.misc.tick_count as usize / 2) % spinner_frames.len()];
(format!(" {} PROG ", f), Theme::MODE_YOLO, Theme::BG)
} else if state.misc.api_connected {
("READY".to_string(), Theme::MODE_AUTO)
(" READY ".to_string(), Theme::MODE_AUTO, Theme::BG)
} else {
("NOAPI".to_string(), Theme::DIM)
(" NOAPI ".to_string(), Theme::TEXT_DIM, Theme::BG)
};
let status = Span::styled(
format!(" {} ", agent_status),
let status_badge = Span::styled(
status_text,
Style::default()
.fg(if agent_status == "NOAPI" { Theme::DIM } else { Theme::BG })
.bg(conn_color)
.fg(status_fg)
.bg(status_bg)
.add_modifier(Modifier::BOLD),
);
// Left chunk: [zesdex] STATUS
let mut spans = vec![
Span::styled(" [zesdex] ", Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD)),
status,
// ── Left segment: app name ────────────────────────────────────────────
let left_spans = vec![
Span::styled(
" ⚡zesdex ",
Style::default()
.fg(Theme::TEXT)
.add_modifier(Modifier::BOLD),
),
status_badge,
];
// Right chunk: token usage, provider, model
// ── Right segment: metadata ───────────────────────────────────────────
let right_str = if let Some(ref rt) = state.session_runtime {
let max_tokens = state.app_config.model_roles.values()
.find(|role| role.provider == state.settings.provider && role.model == state.settings.model)
.and_then(|role| role.context_window);
let total_chars: usize = rt.messages.iter()
.filter_map(|m| m.content.as_deref())
.map(|c| c.len())
.sum();
let current_tokens = total_chars / 4;
let mut parts = Vec::new();
if rt.usage.last_tokens_in > 0 || rt.usage.last_tokens_out > 0 {
parts.push(format!("{}{}", rt.usage.last_tokens_in, rt.usage.last_tokens_out));
@@ -70,7 +73,7 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state:
parts.push(format!("{}/{}", current_tokens, max_str));
parts.push(state.settings.provider.clone());
parts.push(state.settings.model.clone());
format!(" {} ", parts.join(" · "))
} else {
let max_tokens = state.app_config.model_roles.values()
@@ -80,12 +83,23 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state:
format!(" 0/{} · {} · {} ", max_str, state.settings.provider, state.settings.model)
};
spans.push(Span::styled(
// ── Combine everything ────────────────────────────────────────────────
let left_line = Line::from(left_spans);
let right_line = Line::from(Span::styled(
right_str,
Style::default().fg(Theme::DIM),
Style::default().fg(Theme::TEXT_MUTED),
));
let line = Line::from(spans);
// Render the bar using two columns
use ratatui::layout::{Constraint, Direction, Layout};
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(25),
Constraint::Min(10),
])
.split(area);
let block = Block::default()
.style(
@@ -94,6 +108,13 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state:
.fg(Theme::TEXT),
);
let paragraph = ratatui::widgets::Paragraph::new(line).block(block);
frame.render_widget(paragraph, area);
// Left part
let left_para = ratatui::widgets::Paragraph::new(left_line).block(block.clone());
frame.render_widget(left_para, chunks[0]);
// Right part
let right_para = ratatui::widgets::Paragraph::new(right_line)
.block(block)
.alignment(ratatui::layout::Alignment::Right);
frame.render_widget(right_para, chunks[1]);
}