Fix TUI: show GyanDoot banner + wire live agent responses

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.
This commit is contained in:
2026-06-12 12:47:26 +05:30
parent 73bd24dcfb
commit 93040d4fb0
2 changed files with 196 additions and 227 deletions

View File

@@ -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<String>,
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<TuiEvent>,
pub user_tx: mpsc::Sender<String>,
}
impl Default for TuiApp {
fn default() -> Self {
impl TuiApp {
pub fn new(event_rx: mpsc::Receiver<TuiEvent>, user_tx: mpsc::Sender<String>) -> 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()
}
/// 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 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;
}
pub fn append_output(&mut self, text: &str) {
self.output.push_str(text);
self.output.push('\n');
self.message_log.push(text.to_string());
/// 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;
}
/// 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,18 +338,11 @@ 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),
)))
let text = Paragraph::new(Line::from(Span::styled(value,
Style::default().fg(hologram::WHITE).add_modifier(Modifier::BOLD))))
.block(block)
.alignment(Alignment::Left);
@@ -403,38 +351,29 @@ fn render_card(frame: &mut Frame, area: Rect, title: &str, value: &str, accent:
#[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<TuiEvent>,
user_tx: mpsc::Sender<String>,
) -> 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<B: Backend>(
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()? {

View File

@@ -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::<String>(256);
let (event_tx, event_rx) = tokio::sync::mpsc::channel::<gyandoot_tui::tui::TuiEvent>(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(())
}