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:
@@ -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()? {
|
||||
|
||||
50
src/main.rs
50
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::<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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user