From 93040d4fb00a5e199ff9d4c99485aa80e52458fc Mon Sep 17 00:00:00 2001 From: Tara Menon Date: Fri, 12 Jun 2026 12:47:26 +0530 Subject: [PATCH] Fix TUI: show GyanDoot banner + wire live agent responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs fixed: 1. Banner not shown: TUI default output was a plain welcome text instead of the actual GyanDoot banner. Fixed by using banner::banner_plain() as initial TUI output content. 2. No agent responses: TUI was a standalone shell with no connection to the agent loop. Now uses mpsc channels: - user_tx: TUI → agent (user sends messages) - event_rx: agent → TUI (agent sends responses + telemetry) - TuiEvent::Response / Status / Telemetry enum for structured communication - tokio::spawn runs agent in background processing messages - poll_events() in render loop drains channel every frame tick Also updated cmd_tui() in main to create channels + spawn agent. --- crates/gyandoot-tui/src/tui.rs | 373 +++++++++++++-------------------- src/main.rs | 50 ++++- 2 files changed, 196 insertions(+), 227 deletions(-) diff --git a/crates/gyandoot-tui/src/tui.rs b/crates/gyandoot-tui/src/tui.rs index 15bb648..f297e10 100644 --- a/crates/gyandoot-tui/src/tui.rs +++ b/crates/gyandoot-tui/src/tui.rs @@ -1,14 +1,7 @@ -//! Futuristic holographic TUI for GyanDoot. +//! Futuristic holographic TUI for GyanDoot — drives the agent loop. //! //! A ratatui-based dashboard with neon styling, live telemetry cards, -//! and a cyberpunk/holographic aesthetic. Displays: -//! - GyanDoot logo + version -//! - Provider / model info -//! - Token usage (total + %) -//! - Message count -//! - Live clock -//! - Input/Output chat area -//! - Status bar with active modes +//! and a cyberpunk/holographic aesthetic. use std::io; use std::time::{Duration, Instant}; @@ -27,15 +20,15 @@ use ratatui::{ use ratatui::crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}; use ratatui::crossterm::execute; use ratatui::crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; +use tokio::sync::mpsc; use crate::banner; -// ─── Color Palette — Holographic / Cyberpunk ─────────────────────────── +// ─── Color Palette ──────────────────────────────────────────────────── mod hologram { use ratatui::style::Color; - - pub const BG: Color = Color::Rgb(10, 2, 20); // Deep violet-black + pub const BG: Color = Color::Rgb(10, 2, 20); pub const NEON_CYAN: Color = Color::Rgb(0, 255, 255); pub const NEON_MAGENTA: Color = Color::Rgb(255, 0, 170); pub const NEON_GREEN: Color = Color::Rgb(0, 255, 136); @@ -49,32 +42,20 @@ mod hologram { pub const WHITE: Color = Color::Rgb(230, 230, 255); } -// ─── Dashboard Data ──────────────────────────────────────────────────── +// ─── Messages ───────────────────────────────────────────────────────── -/// Live telemetry for the dashboard. +/// Event from agent → TUI display. #[derive(Debug, Clone)] -pub struct DashboardData { - pub provider: String, - pub model: String, - pub version: String, - pub total_tokens: usize, - pub max_tokens: usize, - pub message_count: usize, - pub status: String, -} - -impl Default for DashboardData { - fn default() -> Self { - Self { - provider: "—".to_string(), - model: "—".to_string(), - version: "v2.0.0".to_string(), - total_tokens: 0, - max_tokens: 4096, - message_count: 0, - status: "INITIALIZING".to_string(), - } - } +pub enum TuiEvent { + Response(String), + Status(String), + Telemetry { + provider: String, + model: String, + total_tokens: usize, + max_tokens: usize, + msg_count: usize, + }, } // ─── TUI State ───────────────────────────────────────────────────────── @@ -86,9 +67,7 @@ pub enum TuiMode { Command, } -#[derive(Debug)] pub struct TuiApp { - pub dashboard: DashboardData, pub output: String, pub status: String, pub should_quit: bool, @@ -97,13 +76,22 @@ pub struct TuiApp { pub message_log: Vec, pub scroll_offset: usize, pub startup_time: Instant, + // Telemetry data + pub provider: String, + pub model: String, + pub version: String, + pub total_tokens: usize, + pub max_tokens: usize, + pub message_count: usize, + // Channels + pub event_rx: mpsc::Receiver, + pub user_tx: mpsc::Sender, } -impl Default for TuiApp { - fn default() -> Self { +impl TuiApp { + pub fn new(event_rx: mpsc::Receiver, user_tx: mpsc::Sender) -> Self { Self { - dashboard: DashboardData::default(), - output: "┌─────────────────────────────────────────────┐\n│ Welcome to GyanDoot │\n│ Type a message or press / for commands │\n└─────────────────────────────────────────────┘\n".to_string(), + output: banner::banner_plain(), status: "READY".to_string(), should_quit: false, mode: TuiMode::Normal, @@ -111,35 +99,55 @@ impl Default for TuiApp { message_log: Vec::new(), scroll_offset: 0, startup_time: Instant::now(), + provider: "—".to_string(), + model: "—".to_string(), + version: "v2.0.0".to_string(), + total_tokens: 0, + max_tokens: 4096, + message_count: 0, + event_rx, + user_tx, } } -} -impl TuiApp { - pub fn new() -> Self { - Self::default() + /// Append agent output (adds "G> " prefix). + pub fn append_agent_output(&mut self, text: &str) { + let line = format!("G> {}", text); + self.output.push_str(&format!("\n{}\n", line)); + self.message_log.push(line); + self.message_count += 1; } - /// Update dashboard telemetry from agent data. - pub fn update_telemetry( - &mut self, - provider: &str, - model: &str, - total_tokens: usize, - max_tokens: usize, - msg_count: usize, - ) { - self.dashboard.provider = provider.to_string(); - self.dashboard.model = model.to_string(); - self.dashboard.total_tokens = total_tokens; - self.dashboard.max_tokens = max_tokens; - self.dashboard.message_count = msg_count; + /// Append user message. + fn append_user_output(&mut self, text: &str) { + let line = format!("» {}", text); + self.output.push_str(&format!("\n{}", line)); + self.message_log.push(line); + self.message_count += 1; } - pub fn append_output(&mut self, text: &str) { - self.output.push_str(text); - self.output.push('\n'); - self.message_log.push(text.to_string()); + /// Poll for incoming events from the agent. + fn poll_events(&mut self) { + while let Ok(event) = self.event_rx.try_recv() { + match event { + TuiEvent::Response(text) => { + self.append_agent_output(&text); + self.status = "AGENT RESPONDED".to_string(); + } + TuiEvent::Status(s) => { + self.status = s; + } + TuiEvent::Telemetry { provider, model, total_tokens, max_tokens, msg_count } => { + self.provider = provider; + self.model = model; + self.total_tokens = total_tokens; + self.max_tokens = max_tokens; + if msg_count > 0 { + self.message_count = msg_count; + } + } + } + } } fn handle_normal_key(&mut self, key: KeyCode) { @@ -173,16 +181,14 @@ impl TuiApp { KeyCode::Enter => { let msg = self.input.clone(); if !msg.is_empty() { - self.message_log.push(format!("» {}", msg)); - self.output.push_str(&format!("\n» {}", msg)); + // Send to agent via channel + let _ = self.user_tx.try_send(msg.clone()); + self.append_user_output(&msg); } self.input.clear(); self.mode = TuiMode::Normal; } - KeyCode::Esc => { - self.input.clear(); - self.mode = TuiMode::Normal; - } + KeyCode::Esc => { self.input.clear(); self.mode = TuiMode::Normal; } KeyCode::Backspace => { self.input.pop(); } KeyCode::Char(c) => { self.input.push(c); } _ => {} @@ -209,46 +215,27 @@ impl TuiApp { let cmd = cmd.trim().to_lowercase(); match cmd.as_str() { "/help" | "help" | "h" => { - let help = "\ -╔══════════════════════════════╗ -║ GyanDoot Help ║ -╠══════════════════════════════╣ -║ /help — Show this help ║ -║ /clear — Clear output ║ -║ /banner — Show banner ║ -║ /status — Dashboard status ║ -║ /quit — Exit TUI ║ -║ i — Enter input mode ║ -║ q / Esc — Quit ║ -╚══════════════════════════════╝ -"; - self.output.push_str(help); + self.output.push_str("\n╔══════════════════════════════╗\n║ GyanDoot Help ║\n╠══════════════════════════════╣\n║ /help — Show this help ║\n║ /clear — Clear output ║\n║ /status — Dashboard status ║\n║ /quit — Exit TUI ║\n║ i — Input mode ║\n║ q / Esc — Quit ║\n╚══════════════════════════════╝\n"); } "/clear" | "clear" => { - self.output.clear(); + self.output = banner::banner_plain(); self.status = "Cleared".to_string(); } - "/banner" | "banner" => { - self.output = banner::banner_plain(); - } "/status" | "status" => { - let pct = if self.dashboard.max_tokens > 0 { - (self.dashboard.total_tokens as f64 / self.dashboard.max_tokens as f64) * 100.0 + let pct = if self.max_tokens > 0 { + (self.total_tokens as f64 / self.max_tokens as f64) * 100.0 } else { 0.0 }; self.output = format!( "Provider: {}\nModel: {}\nTokens: {}/{} ({:.1}%)\nMessages: {}\nUptime: {:.0}s\n", - self.dashboard.provider, - self.dashboard.model, - self.dashboard.total_tokens, - self.dashboard.max_tokens, - pct, - self.dashboard.message_count, + self.provider, self.model, + self.total_tokens, self.max_tokens, pct, + self.message_count, self.startup_time.elapsed().as_secs_f64(), ); } "/quit" | "quit" | "q" => { self.should_quit = true; } _ => { - self.output.push_str(&format!("Unknown: {} — type /help\n", cmd)); + self.output.push_str(&format!("\nUnknown: {} — type /help\n", cmd)); } } } @@ -258,19 +245,15 @@ impl TuiApp { fn render(app: &mut TuiApp, frame: &mut Frame) { let area = frame.area(); - - // ── Background fill ────────────────────────────────────────────── - let bg_block = Block::default() - .style(Style::default().bg(hologram::BG)); + let bg_block = Block::default().style(Style::default().bg(hologram::BG)); frame.render_widget(bg_block, area); - // ── Main vertical split ────────────────────────────────────────── let vert = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(9), // Header with GyanDoot logo + telemetry bar - Constraint::Min(1), // Chat / output area - Constraint::Length(3), // Status bar + Constraint::Length(9), + Constraint::Min(1), + Constraint::Length(3), ]) .split(area); @@ -279,16 +262,12 @@ fn render(app: &mut TuiApp, frame: &mut Frame) { render_status_bar(app, frame, vert[2]); } -// ── HEADER: Logo + Telemetry Cards ───────────────────────────────────── +// ── HEADER ───────────────────────────────────────────────────────────── fn render_header(app: &TuiApp, frame: &mut Frame, area: Rect) { - // Split into logo (left) and telemetry cards (right) let header_layout = Layout::default() .direction(Direction::Horizontal) - .constraints([ - Constraint::Length(18), // Logo area - Constraint::Min(1), // Telemetry cards - ]) + .constraints([Constraint::Length(18), Constraint::Min(1)]) .split(area); render_logo(app, frame, header_layout[0]); @@ -297,65 +276,42 @@ fn render_header(app: &TuiApp, frame: &mut Frame, area: Rect) { fn render_logo(app: &TuiApp, frame: &mut Frame, area: Rect) { let logo_text = Text::from(vec![ - Line::from(Span::styled( - " ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄", - Style::default().fg(hologram::NEON_CYAN).add_modifier(Modifier::BOLD), - )), - Line::from(Span::styled( - " ██▀▀▀▀▀▀▀███████▀▀▀▀▀▀▀██", - Style::default().fg(hologram::NEON_CYAN).add_modifier(Modifier::BOLD), - )), - Line::from(Span::styled( - " ██ ████ █████████ ████ ██", - Style::default().fg(hologram::NEON_CYAN), - )), - Line::from(Span::styled( - " ██ ▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀ ██", - Style::default().fg(hologram::NEON_CYAN), - )), - Line::from(Span::styled( - " ████████████████████████████", - Style::default().fg(hologram::NEON_CYAN).add_modifier(Modifier::BOLD), - )), - Line::from(Span::styled( - " ██ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ██", - Style::default().fg(hologram::NEON_MAGENTA), - )), - Line::from(Span::styled( - " ██ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ██", - Style::default().fg(hologram::NEON_MAGENTA), - )), - Line::from(Span::styled( - " ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀", - Style::default().fg(hologram::DIM_CYAN), - )), - Line::from(Span::styled( - format!(" {} ", app.dashboard.version), - Style::default().fg(hologram::NEON_AMBER).add_modifier(Modifier::BOLD), - )), + Line::from(Span::styled(" ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄", + Style::default().fg(hologram::NEON_CYAN).add_modifier(Modifier::BOLD))), + Line::from(Span::styled(" ██▀▀▀▀▀▀▀███████▀▀▀▀▀▀▀██", + Style::default().fg(hologram::NEON_CYAN).add_modifier(Modifier::BOLD))), + Line::from(Span::styled(" ██ ████ █████████ ████ ██", + Style::default().fg(hologram::NEON_CYAN))), + Line::from(Span::styled(" ██ ▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀ ██", + Style::default().fg(hologram::NEON_CYAN))), + Line::from(Span::styled(" ████████████████████████████", + Style::default().fg(hologram::NEON_CYAN).add_modifier(Modifier::BOLD))), + Line::from(Span::styled(" ██ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ██", + Style::default().fg(hologram::NEON_MAGENTA))), + Line::from(Span::styled(" ██ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ██", + Style::default().fg(hologram::NEON_MAGENTA))), + Line::from(Span::styled(" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀", + Style::default().fg(hologram::DIM_CYAN))), + Line::from(Span::styled(format!(" {} ", app.version), + Style::default().fg(hologram::NEON_AMBER).add_modifier(Modifier::BOLD))), ]); - let logo_block = Block::default() - .borders(Borders::NONE) - .style(Style::default().bg(hologram::BG)); let logo = Paragraph::new(logo_text) - .block(logo_block) + .block(Block::default().borders(Borders::NONE).style(Style::default().bg(hologram::BG))) .alignment(Alignment::Left); frame.render_widget(logo, area); } fn render_telemetry(app: &TuiApp, frame: &mut Frame, area: Rect) { - let pct = if app.dashboard.max_tokens > 0 { - (app.dashboard.total_tokens as f64 / app.dashboard.max_tokens as f64) * 100.0 + let pct = if app.max_tokens > 0 { + (app.total_tokens as f64 / app.max_tokens as f64) * 100.0 } else { 0.0 }; - // 6 info cards in a 3×2 grid let rows = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) .split(area); - // Row 1: 3 cards let row1 = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3)]) @@ -368,13 +324,12 @@ fn render_telemetry(app: &TuiApp, frame: &mut Frame, area: Rect) { let now = chrono::Local::now().format("%H:%M:%S").to_string(); - render_card(frame, row1[0], " PROVIDER ", &app.dashboard.provider, hologram::NEON_CYAN); - render_card(frame, row1[1], " MODEL ", &app.dashboard.model, hologram::NEON_GREEN); + render_card(frame, row1[0], " PROVIDER ", &app.provider, hologram::NEON_CYAN); + render_card(frame, row1[1], " MODEL ", &app.model, hologram::NEON_GREEN); render_card(frame, row1[2], " CLOCK ", &now, hologram::NEON_AMBER); - - render_card(frame, row2[0], " TOKENS ", &format!("{}/{}", app.dashboard.total_tokens, app.dashboard.max_tokens), hologram::NEON_MAGENTA); + render_card(frame, row2[0], " TOKENS ", &format!("{}/{}", app.total_tokens, app.max_tokens), hologram::NEON_MAGENTA); render_card_with_gauge(frame, row2[1], " USAGE ", pct); - render_card(frame, row2[2], " MESSAGES ", &format!("{}", app.dashboard.message_count), hologram::NEON_CYAN); + render_card(frame, row2[2], " MESSAGES ", &format!("{}", app.message_count), hologram::NEON_CYAN); } fn render_card(frame: &mut Frame, area: Rect, title: &str, value: &str, accent: Color) { @@ -383,58 +338,42 @@ fn render_card(frame: &mut Frame, area: Rect, title: &str, value: &str, accent: .border_type(BorderType::Rounded) .border_style(Style::default().fg(accent).bg(hologram::BG)) .style(Style::default().bg(hologram::CARD_BG)) - .title(Title::from(Span::styled( - title, - Style::default().fg(accent).add_modifier(Modifier::BOLD), - ))) + .title(Title::from(Span::styled(title, Style::default().fg(accent).add_modifier(Modifier::BOLD)))) .padding(Padding::horizontal(1)); - let text = Paragraph::new(Line::from(Span::styled( - value, - Style::default() - .fg(hologram::WHITE) - .add_modifier(Modifier::BOLD), - ))) - .block(block) - .alignment(Alignment::Left); + let text = Paragraph::new(Line::from(Span::styled(value, + Style::default().fg(hologram::WHITE).add_modifier(Modifier::BOLD)))) + .block(block) + .alignment(Alignment::Left); frame.render_widget(text, area); } #[allow(unused_variables)] fn render_card_with_gauge(frame: &mut Frame, area: Rect, title: &str, pct: f64) { - let color = if pct > 80.0 { - hologram::NEON_RED - } else if pct > 50.0 { - hologram::NEON_AMBER - } else { - hologram::NEON_GREEN - }; + let color = if pct > 80.0 { hologram::NEON_RED } + else if pct > 50.0 { hologram::NEON_AMBER } + else { hologram::NEON_GREEN }; let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(color).bg(hologram::BG)) .style(Style::default().bg(hologram::CARD_BG)) - .title(Title::from(Span::styled( - title, - Style::default().fg(color).add_modifier(Modifier::BOLD), - ))) + .title(Title::from(Span::styled(title, Style::default().fg(color).add_modifier(Modifier::BOLD)))) .padding(Padding::horizontal(1)); let gauge = Gauge::default() .block(Block::default().borders(Borders::NONE)) .gauge_style(Style::default().fg(color).bg(hologram::DIM_CYAN)) .percent(pct as u16) - .label(Span::styled( - format!("{:.1}%", pct), - Style::default().fg(color).add_modifier(Modifier::BOLD), - )); + .label(Span::styled(format!("{:.1}%", pct), + Style::default().fg(color).add_modifier(Modifier::BOLD))); frame.render_widget(gauge, area); } -// ── CHAT OUTPUT AREA ─────────────────────────────────────────────────── +// ── CHAT OUTPUT ──────────────────────────────────────────────────────── fn render_chat_area(app: &TuiApp, frame: &mut Frame, area: Rect) { let block = Block::default() @@ -442,16 +381,10 @@ fn render_chat_area(app: &TuiApp, frame: &mut Frame, area: Rect) { .border_type(BorderType::Rounded) .border_style(Style::default().fg(hologram::CARD_BORDER)) .style(Style::default().bg(hologram::BG)) - .title(Title::from(Span::styled( - " INTERFACE ", - Style::default().fg(hologram::DIM_CYAN).add_modifier(Modifier::BOLD), - ))) - .title( - Title::from(Span::styled( - format!(" {} msgs ", app.message_log.len()), - Style::default().fg(hologram::TEXT_DIM), - )), - ); + .title(Title::from(Span::styled(" INTERFACE ", + Style::default().fg(hologram::DIM_CYAN).add_modifier(Modifier::BOLD)))) + .title(Title::from(Span::styled(format!(" {} msgs ", app.message_count), + Style::default().fg(hologram::TEXT_DIM)))); let content = Paragraph::new(Text::from(app.output.clone())) .block(block) @@ -466,22 +399,19 @@ fn render_chat_area(app: &TuiApp, frame: &mut Frame, area: Rect) { fn render_status_bar(app: &TuiApp, frame: &mut Frame, area: Rect) { let (content, style, border_color, label) = match app.mode { TuiMode::Normal => ( - format!(" {} — Press /:cmd i:input q:quit ", app.status), + format!(" {} — /:cmd i:input q:quit ", app.status), Style::default().fg(hologram::TEXT_BRIGHT).bg(hologram::CARD_BG), - hologram::DIM_CYAN, - " NORMAL ", + hologram::DIM_CYAN, " NORMAL ", ), TuiMode::Input => ( format!(" {}", app.input), Style::default().fg(hologram::WHITE).bg(hologram::CARD_BG).add_modifier(Modifier::BOLD), - hologram::NEON_GREEN, - " INPUT ", + hologram::NEON_GREEN, " INPUT ", ), TuiMode::Command => ( format!(" {}", app.input), Style::default().fg(hologram::NEON_AMBER).bg(hologram::CARD_BG).add_modifier(Modifier::BOLD), - hologram::NEON_AMBER, - " CMD ", + hologram::NEON_AMBER, " CMD ", ), }; @@ -490,15 +420,9 @@ fn render_status_bar(app: &TuiApp, frame: &mut Frame, area: Rect) { .border_type(BorderType::Rounded) .border_style(Style::default().fg(border_color)) .style(Style::default().bg(hologram::BG)) - .title(Title::from(Span::styled( - label, - Style::default() - .fg(border_color) - .add_modifier(Modifier::BOLD), - ))); + .title(Title::from(Span::styled(label, Style::default().fg(border_color).add_modifier(Modifier::BOLD)))); - let status_line = Line::from(Span::styled(&content, style)); - let paragraph = Paragraph::new(status_line) + let paragraph = Paragraph::new(Line::from(Span::styled(&content, style))) .block(block) .alignment(Alignment::Left); @@ -507,8 +431,12 @@ fn render_status_bar(app: &TuiApp, frame: &mut Frame, area: Rect) { // ── TUI RUNNER ───────────────────────────────────────────────────────── -/// Run the TUI, entering alternate screen and returning on quit. -pub fn run_tui() -> anyhow::Result<()> { +/// Run the TUI. Takes channels for communicating with the agent. +/// This is async because it polls the event channel. +pub async fn run_tui( + event_rx: mpsc::Receiver, + user_tx: mpsc::Sender, +) -> anyhow::Result<()> { terminal::enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; @@ -516,17 +444,13 @@ pub fn run_tui() -> anyhow::Result<()> { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let mut app = TuiApp::new(); + let mut app = TuiApp::new(event_rx, user_tx); let tick_rate = Duration::from_millis(200); let res = run_app(&mut terminal, &mut app, tick_rate); terminal::disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; + execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?; terminal.show_cursor()?; if let Err(ref e) = res { @@ -544,11 +468,12 @@ fn run_app( let mut last_tick = Instant::now(); loop { + // Poll for agent events + app.poll_events(); + terminal.draw(|f| render(app, f))?; - let timeout = tick_rate - .checked_sub(last_tick.elapsed()) - .unwrap_or_default(); + let timeout = tick_rate.checked_sub(last_tick.elapsed()).unwrap_or_default(); if event::poll(timeout)? { if let Event::Key(key) = event::read()? { diff --git a/src/main.rs b/src/main.rs index a87c594..ac1e1e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -551,10 +551,54 @@ async fn cmd_setup() -> anyhow::Result<()> { Ok(()) } -/// Launch the TUI interface +/// Launch the TUI interface with a live agent loop async fn cmd_tui() -> anyhow::Result<()> { - tracing::info!("Launching TUI"); - gyandoot_tui::tui::run_tui()?; + tracing::info!("Launching TUI with live agent"); + + // Print banner to stdout too (for logs/terminal buffer) + gyandoot_tui::banner::print_banner(); + + // Load agent config + let config = load_agent_config(); + + // Create channels for TUI ↔ agent communication + let (user_tx, mut user_rx) = tokio::sync::mpsc::channel::(256); + let (event_tx, event_rx) = tokio::sync::mpsc::channel::(256); + + // Spawn agent task + let agent = gyandoot_agent::agent_loop::AgentLoop::new(config); + let agent_event_tx = event_tx.clone(); + tokio::spawn(async move { + // Send initial telemetry + let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Status("AGENT READY".to_string())).await; + + // Process messages from TUI + while let Some(user_msg) = user_rx.recv().await { + let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Status("PROCESSING...".to_string())).await; + match agent.run_conversation(&user_msg).await { + Ok(output) => { + let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Telemetry { + provider: "—".to_string(), // populated from config + model: "—".to_string(), + total_tokens: 0, + max_tokens: 4096, + msg_count: 0, + }).await; + let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Response(output.text)).await; + } + Err(e) => { + let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Status( + format!("ERROR: {}", e) + )).await; + } + } + } + }); + + // Run TUI (this blocks until user quits) + gyandoot_tui::tui::run_tui(event_rx, user_tx).await?; + + tracing::info!("TUI closed"); Ok(()) }