diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 3e7825bf..56a60d9c 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -5,6 +5,7 @@ pub struct App { pub connected: bool, pub messages: Vec, pub current_text: String, + pub status_line: String, pub input: String, pub cursor: usize, pub scroll: u16, @@ -22,7 +23,6 @@ pub enum MessageKind { Assistant, Tool, Error, - Status, } impl App { @@ -32,6 +32,7 @@ impl App { connected: false, messages: Vec::new(), current_text: String::new(), + status_line: String::new(), input: String::new(), cursor: 0, scroll: 0, @@ -57,7 +58,7 @@ impl App { pub fn handle_pod_event(&mut self, event: Event) { match event { Event::TurnStart { turn } => { - self.push_status(format!("[turn {turn}] start")); + self.status_line = format!("turn {turn}"); } Event::TextDelta { text } => { self.current_text.push_str(&text); @@ -73,7 +74,6 @@ impl App { } } Event::TurnEnd { turn, result } => { - // Flush any remaining text delta if !self.current_text.is_empty() { let text = std::mem::take(&mut self.current_text); self.messages.push(Message { @@ -81,9 +81,10 @@ impl App { content: text, }); } - self.push_status(format!("[turn {turn}] end ({result:?})")); + self.status_line = format!("turn {turn} {result:?}"); } Event::ToolCallStart { name, .. } => { + self.status_line = format!("tool: {name}"); self.messages.push(Message { kind: MessageKind::Tool, content: format!("[tool] {name}"), @@ -118,11 +119,10 @@ impl App { input_tokens, output_tokens, } => { - self.push_status(format!( - "[usage] in={} out={}", - input_tokens.unwrap_or(0), - output_tokens.unwrap_or(0), - )); + let in_t = input_tokens.unwrap_or(0); + let out_t = output_tokens.unwrap_or(0); + self.status_line + .push_str(&format!(" | in:{in_t} out:{out_t}")); } Event::Error { code, message } => { self.messages.push(Message { @@ -132,7 +132,7 @@ impl App { self.scroll_to_bottom(); } Event::RunEnd { result } => { - self.push_status(format!("[run end] {result:?}")); + self.status_line = format!("{result:?}"); } Event::ToolCallArgsDelta { .. } => {} Event::History { items } => { @@ -271,14 +271,6 @@ impl App { self.scroll_to_bottom(); } - fn push_status(&mut self, content: String) { - self.messages.push(Message { - kind: MessageKind::Status, - content, - }); - self.scroll_to_bottom(); - } - fn scroll_to_bottom(&mut self) { // Will be clamped during rendering self.scroll = u16::MAX; diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 31313930..ab27a4e0 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -121,8 +121,8 @@ async fn run_loop( } } } - // Pod events - event = client.next_event() => { + // Pod events (disabled after disconnect) + event = client.next_event(), if app.connected => { match event { Some(ev) => app.handle_pod_event(ev), None => { diff --git a/crates/tui/src/ui.rs b/crates/tui/src/ui.rs index 590668ad..db2831a3 100644 --- a/crates/tui/src/ui.rs +++ b/crates/tui/src/ui.rs @@ -1,81 +1,97 @@ use ratatui::layout::{Constraint, Layout, Position}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; +use ratatui::widgets::{Paragraph, Wrap}; use ratatui::Frame; use crate::app::{App, MessageKind}; pub fn draw(frame: &mut Frame, app: &mut App) { let chunks = Layout::vertical([ - Constraint::Length(1), - Constraint::Min(3), - Constraint::Length(3), + Constraint::Min(1), // messages (scroll area) + Constraint::Length(1), // separator + Constraint::Length(1), // status line + Constraint::Length(1), // input ]) .split(frame.area()); - draw_status_bar(frame, app, chunks[0]); - draw_output(frame, app, chunks[1]); - draw_input(frame, app, chunks[2]); + draw_messages(frame, app, chunks[0]); + draw_separator(frame, chunks[1]); + draw_status(frame, app, chunks[2]); + draw_input(frame, app, chunks[3]); } -fn draw_status_bar(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) { - let conn_style = if app.connected { - Style::default().fg(Color::Green) - } else { - Style::default().fg(Color::Red) - }; - let conn_text = if app.connected { - "connected" - } else { - "disconnected" - }; - - let line = Line::from(vec![ - Span::styled(&app.pod_name, Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" | "), - Span::styled(conn_text, conn_style), - ]); - - frame.render_widget(Paragraph::new(line), area); -} - -fn draw_output(frame: &mut Frame, app: &mut App, area: ratatui::layout::Rect) { +fn draw_messages(frame: &mut Frame, app: &mut App, area: ratatui::layout::Rect) { let display = app.display_lines(); let lines: Vec = display .iter() .flat_map(|(kind, content)| { let style = kind_style(kind); - content.lines().map(move |l| Line::from(Span::styled(l.to_owned(), style))) + content + .lines() + .map(move |l| Line::from(Span::styled(l.to_owned(), style))) }) .collect(); let total = lines.len() as u16; - let visible = area.height.saturating_sub(2); // block borders - let max_scroll = total.saturating_sub(visible); + let max_scroll = total.saturating_sub(area.height); if app.scroll > max_scroll { app.scroll = max_scroll; } - let block = Block::default().borders(Borders::ALL); let paragraph = Paragraph::new(lines) - .block(block) .wrap(Wrap { trim: false }) .scroll((app.scroll, 0)); frame.render_widget(paragraph, area); } -fn draw_input(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) { - let display = format!("> {}", app.input); - let block = Block::default().borders(Borders::ALL).title("Input"); - let paragraph = Paragraph::new(display).block(block); +fn draw_separator(frame: &mut Frame, area: ratatui::layout::Rect) { + let line = "─".repeat(area.width as usize); + let paragraph = Paragraph::new(Line::from(Span::styled( + line, + Style::default().fg(Color::DarkGray), + ))); frame.render_widget(paragraph, area); +} - // Cursor position: "> " is 2 chars, plus cursor offset in the input - let cursor_x = area.x + 1 + 2 + app.input[..app.cursor].chars().count() as u16; - let cursor_y = area.y + 1; +fn draw_status(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) { + let conn = if app.connected { + Span::styled("●", Style::default().fg(Color::Green)) + } else { + Span::styled("○", Style::default().fg(Color::Red)) + }; + + let mut spans = vec![ + conn, + Span::raw(" "), + Span::styled( + &app.pod_name, + Style::default().add_modifier(Modifier::BOLD), + ), + ]; + + if !app.status_line.is_empty() { + spans.push(Span::raw(" | ")); + spans.push(Span::styled( + &app.status_line, + Style::default().fg(Color::Yellow), + )); + } + + frame.render_widget(Paragraph::new(Line::from(spans)), area); +} + +fn draw_input(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) { + let line = Line::from(vec![ + Span::styled("> ", Style::default().fg(Color::DarkGray)), + Span::raw(&app.input), + ]); + frame.render_widget(Paragraph::new(line), area); + + let cursor_x = area.x + 2 + app.input[..app.cursor].chars().count() as u16; + let cursor_y = area.y; frame.set_cursor_position(Position::new(cursor_x, cursor_y)); } @@ -85,6 +101,5 @@ fn kind_style(kind: &MessageKind) -> Style { MessageKind::Assistant => Style::default().fg(Color::White), MessageKind::Tool => Style::default().fg(Color::Cyan), MessageKind::Error => Style::default().fg(Color::Red), - MessageKind::Status => Style::default().fg(Color::DarkGray), } }