retro mode ui!

This commit is contained in:
Dhanji Prasanna
2025-10-02 14:47:19 +10:00
parent a87f81042a
commit 57b1b51e65
6 changed files with 971 additions and 42 deletions

211
Cargo.lock generated
View File

@@ -229,7 +229,7 @@ dependencies = [
"bitflags 2.9.4",
"cexpr",
"clang-sys",
"itertools",
"itertools 0.12.1",
"lazy_static",
"lazycell",
"log",
@@ -279,6 +279,21 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]]
name = "cc"
version = "1.2.35"
@@ -408,6 +423,20 @@ dependencies = [
"memchr",
]
[[package]]
name = "compact_str"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]]
name = "config"
version = "0.14.1"
@@ -490,7 +519,7 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3"
dependencies = [
"crossterm",
"crossterm 0.29.0",
]
[[package]]
@@ -535,7 +564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51360853ebbeb3df20c76c82aecf43d387a62860f1a59ba65ab51f00eea85aad"
dependencies = [
"crokey-proc_macros",
"crossterm",
"crossterm 0.29.0",
"once_cell",
"serde",
"strict",
@@ -547,7 +576,7 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf1a727caeb5ee5e0a0826a97f205a9cf84ee964b0b48239fef5214a00ae439"
dependencies = [
"crossterm",
"crossterm 0.29.0",
"proc-macro2",
"quote",
"strict",
@@ -610,6 +639,22 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.9.4",
"crossterm_winapi",
"mio",
"parking_lot",
"rustix 0.38.44",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm"
version = "0.29.0"
@@ -653,6 +698,41 @@ dependencies = [
"typenum",
]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "derive_more"
version = "0.99.20"
@@ -846,6 +926,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
@@ -975,11 +1061,12 @@ dependencies = [
"anyhow",
"chrono",
"clap",
"crossterm",
"crossterm 0.29.0",
"dirs 5.0.1",
"g3-config",
"g3-core",
"indicatif",
"ratatui",
"rustyline",
"serde",
"serde_json",
@@ -1147,6 +1234,11 @@ name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "hashlink"
@@ -1430,6 +1522,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.1.0"
@@ -1474,6 +1572,25 @@ dependencies = [
"web-time",
]
[[package]]
name = "indoc"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "instability"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a"
dependencies = [
"darling",
"indoc",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "io-uring"
version = "0.7.10"
@@ -1506,6 +1623,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
@@ -1702,6 +1828,15 @@ version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "lru"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "matchers"
version = "0.2.0"
@@ -1991,6 +2126,12 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.3"
@@ -2163,6 +2304,27 @@ dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "ratatui"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [
"bitflags 2.9.4",
"cassowary",
"compact_str",
"crossterm 0.28.1",
"indoc",
"instability",
"itertools 0.13.0",
"lru",
"paste",
"strum",
"unicode-segmentation",
"unicode-truncate",
"unicode-width 0.2.0",
]
[[package]]
name = "redox_syscall"
version = "0.5.17"
@@ -2607,6 +2769,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strict"
version = "0.2.0"
@@ -2619,6 +2787,28 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "2.0.106"
@@ -2997,6 +3187,17 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-truncate"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
dependencies = [
"itertools 0.13.0",
"unicode-segmentation",
"unicode-width 0.1.14",
]
[[package]]
name = "unicode-width"
version = "0.1.14"

View File

@@ -20,4 +20,5 @@ tokio-util = "0.7"
indicatif = "0.17"
chrono = { version = "0.4", features = ["serde"] }
crossterm = "0.29.0"
ratatui = "0.29"
termimad = "0.34.0"

View File

@@ -10,8 +10,10 @@ use tracing::{error, info};
mod tui;
mod ui_writer_impl;
mod retro_tui;
use tui::SimpleOutput;
use ui_writer_impl::ConsoleUiWriter;
use ui_writer_impl::{ConsoleUiWriter, RetroTuiWriter};
use retro_tui::RetroTui;
#[derive(Parser)]
#[command(name = "g3")]
@@ -48,39 +50,60 @@ pub struct Cli {
/// Maximum number of turns in autonomous mode (default: 5)
#[arg(long, default_value = "5")]
pub max_turns: usize,
/// Use retro terminal UI (inspired by 80s sci-fi)
#[arg(long)]
pub retro: bool,
}
pub async fn run() -> Result<()> {
let cli = Cli::parse();
// Initialize logging with filtering
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
// Only initialize logging if not in retro mode
if !cli.retro {
// Initialize logging with filtering
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
// Create a filter that suppresses llama_cpp logs unless in verbose mode
let filter = if cli.verbose {
EnvFilter::from_default_env()
.add_directive(format!("{}=debug", env!("CARGO_PKG_NAME")).parse().unwrap())
.add_directive("g3_core=debug".parse().unwrap())
.add_directive("g3_cli=debug".parse().unwrap())
.add_directive("g3_execution=debug".parse().unwrap())
.add_directive("g3_providers=debug".parse().unwrap())
// Create a filter that suppresses llama_cpp logs unless in verbose mode
let filter = if cli.verbose {
EnvFilter::from_default_env()
.add_directive(format!("{}=debug", env!("CARGO_PKG_NAME")).parse().unwrap())
.add_directive("g3_core=debug".parse().unwrap())
.add_directive("g3_cli=debug".parse().unwrap())
.add_directive("g3_execution=debug".parse().unwrap())
.add_directive("g3_providers=debug".parse().unwrap())
} else {
EnvFilter::from_default_env()
.add_directive(format!("{}=info", env!("CARGO_PKG_NAME")).parse().unwrap())
.add_directive("g3_core=info".parse().unwrap())
.add_directive("g3_cli=info".parse().unwrap())
.add_directive("g3_execution=info".parse().unwrap())
.add_directive("g3_providers=info".parse().unwrap())
.add_directive("llama_cpp=off".parse().unwrap()) // Suppress all llama_cpp logs
.add_directive("llama=off".parse().unwrap()) // Suppress all llama.cpp logs
};
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(filter)
.init();
} else {
EnvFilter::from_default_env()
.add_directive(format!("{}=info", env!("CARGO_PKG_NAME")).parse().unwrap())
.add_directive("g3_core=info".parse().unwrap())
.add_directive("g3_cli=info".parse().unwrap())
.add_directive("g3_execution=info".parse().unwrap())
.add_directive("g3_providers=info".parse().unwrap())
.add_directive("llama_cpp=off".parse().unwrap()) // Suppress all llama_cpp logs
.add_directive("llama=off".parse().unwrap()) // Suppress all llama.cpp logs
};
// In retro mode, we don't want any logging output to interfere with the TUI
// We'll use a no-op subscriber
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
// Create a filter that suppresses ALL logs in retro mode
let filter = EnvFilter::from_default_env()
.add_directive("off".parse().unwrap()); // Turn off all logging
tracing_subscriber::registry()
.with(filter)
.init();
}
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(filter)
.init();
info!("Starting G3 AI Coding Agent");
if !cli.retro {
info!("Starting G3 AI Coding Agent");
}
// Set up workspace directory
let workspace_dir = if let Some(ws) = cli.workspace {
@@ -104,19 +127,23 @@ pub async fn run() -> Result<()> {
project.ensure_workspace_exists()?;
project.enter_workspace()?;
info!("Using workspace: {}", project.workspace().display());
if !cli.retro {
info!("Using workspace: {}", project.workspace().display());
}
// Load configuration
let config = Config::load(cli.config.as_deref())?;
// Initialize agent
let ui_writer = ConsoleUiWriter::new();
let mut agent = Agent::new(config, ui_writer).await?;
let mut agent = Agent::new(config.clone(), ui_writer).await?;
// Execute task, autonomous mode, or start interactive mode
if cli.autonomous {
// Autonomous mode with coach-player feedback loop
info!("Starting autonomous mode");
if !cli.retro {
info!("Starting autonomous mode");
}
run_autonomous(
agent,
project,
@@ -127,23 +154,187 @@ pub async fn run() -> Result<()> {
.await?;
} else if let Some(task) = cli.task {
// Single-shot mode
info!("Executing task: {}", task);
if !cli.retro {
info!("Executing task: {}", task);
}
let output = SimpleOutput::new();
let result = agent
.execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true)
.await?;
output.print_markdown(&result);
} else {
let output = SimpleOutput::new();
// Interactive mode (default)
info!("Starting interactive mode");
output.print(&format!("📁 Workspace: {}", project.workspace().display()));
run_interactive(agent, cli.show_prompt, cli.show_code).await?;
if !cli.retro {
info!("Starting interactive mode");
}
if cli.retro {
// Use retro terminal UI
run_interactive_retro(config, cli.show_prompt, cli.show_code).await?;
} else {
// Use standard terminal UI
let output = SimpleOutput::new();
output.print(&format!("📁 Workspace: {}", project.workspace().display()));
run_interactive(agent, cli.show_prompt, cli.show_code).await?;
}
}
Ok(())
}
async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: bool) -> Result<()> {
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
use std::time::Duration;
// Set environment variable to suppress println in other crates
std::env::set_var("G3_RETRO_MODE", "1");
// Initialize the retro terminal UI
let tui = RetroTui::start().await?;
// Create agent with RetroTuiWriter
let ui_writer = RetroTuiWriter::new(tui.clone());
let mut agent = Agent::new(config, ui_writer).await?;
// Display initial system messages
tui.output("SYSTEM: G3 AI CODING AGENT ONLINE");
tui.output("SYSTEM: READY FOR INPUT");
tui.output("");
// Display provider and model information
match agent.get_provider_info() {
Ok((provider, model)) => {
tui.output(&format!("SYSTEM: PROVIDER: {} | MODEL: {}", provider, model));
}
Err(e) => {
tui.error(&format!("Failed to get provider info: {}", e));
}
}
tui.output("");
tui.output("Type 'exit' or 'quit' to exit, use Up/Down arrows to scroll output");
tui.output("For multiline input: use \\ at the end of a line to continue");
tui.output("");
// Track multiline input
let mut multiline_buffer = String::new();
let mut in_multiline = false;
let mut input_buffer = String::new();
// Main event loop
loop {
// Update context window display
let context = agent.get_context_window();
tui.update_context(context.used_tokens, context.total_tokens, context.percentage_used());
// Update the displayed input buffer
tui.update_input(&input_buffer);
// Poll for keyboard events
if event::poll(Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
tui.exit();
break;
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
tui.exit();
break;
}
KeyCode::Enter => {
if !input_buffer.is_empty() {
let trimmed = input_buffer.trim_end();
// Check if line ends with backslash for continuation
if trimmed.ends_with('\\') {
// Remove the backslash and add to buffer
let without_backslash = &trimmed[..trimmed.len() - 1];
multiline_buffer.push_str(without_backslash);
multiline_buffer.push('\n');
in_multiline = true;
input_buffer.clear();
tui.status("MULTILINE INPUT");
continue;
}
// If we're in multiline mode and no backslash, this is the final line
let final_input = if in_multiline {
multiline_buffer.push_str(&input_buffer);
in_multiline = false;
let result = multiline_buffer.clone();
multiline_buffer.clear();
tui.status("READY");
result
} else {
input_buffer.clone()
};
input_buffer.clear();
let input = final_input.trim().to_string();
if input.is_empty() {
continue;
}
if input == "exit" || input == "quit" {
tui.exit();
break;
}
// Execute the task
tui.output(&format!("> {}", input));
tui.status("PROCESSING");
match agent.execute_task_with_timing(&input, None, false, show_prompt, show_code, true).await {
Ok(response) => {
tui.output(&response);
tui.status("READY");
}
Err(e) => {
tui.error(&format!("Task execution failed: {}", e));
tui.status("ERROR");
}
}
}
}
KeyCode::Char(c) => {
input_buffer.push(c);
}
KeyCode::Backspace => {
input_buffer.pop();
}
KeyCode::Up => {
tui.scroll_up();
}
KeyCode::Down => {
tui.scroll_down();
}
KeyCode::PageUp => {
tui.scroll_page_up();
}
KeyCode::PageDown => {
tui.scroll_page_down();
}
KeyCode::Home => {
tui.scroll_home();
}
KeyCode::End => {
tui.scroll_end();
}
_ => {}
}
}
}
// Small delay to prevent CPU spinning
tokio::time::sleep(Duration::from_millis(10)).await;
}
tui.output("SYSTEM: SHUTDOWN INITIATED");
Ok(())
}
async fn run_interactive<W: UiWriter>(mut agent: Agent<W>, show_prompt: bool, show_code: bool) -> Result<()> {
let output = SimpleOutput::new();

View File

@@ -0,0 +1,448 @@
use anyhow::Result;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap},
Frame, Terminal,
};
use std::io;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
// Retro sci-fi color scheme inspired by Alien terminals
const TERMINAL_GREEN: Color = Color::Rgb(0, 255, 65); // Bright phosphor green
const TERMINAL_AMBER: Color = Color::Rgb(255, 176, 0); // Amber for warnings
const TERMINAL_DIM_GREEN: Color = Color::Rgb(0, 128, 32); // Dimmed green for borders
const TERMINAL_BG: Color = Color::Rgb(0, 10, 0); // Very dark green background
const TERMINAL_CYAN: Color = Color::Rgb(0, 255, 255); // Cyan for highlights
const TERMINAL_RED: Color = Color::Rgb(255, 0, 0); // Red for errors
/// Message types for communication between threads
#[derive(Debug, Clone)]
pub enum TuiMessage {
AgentOutput(String),
SystemStatus(String),
ContextUpdate {
used: u32,
total: u32,
percentage: f32,
},
Error(String),
Exit,
}
/// Shared state for the retro terminal
struct TerminalState {
/// Current input buffer
input_buffer: String,
/// Output history
output_history: Vec<String>,
/// Scroll position in output
scroll_offset: usize,
/// Cursor blink state
cursor_blink: bool,
/// Last cursor blink time
last_blink: Instant,
/// System status line
status_line: String,
/// Context window info
context_info: (u32, u32, f32),
/// Should exit
should_exit: bool,
}
impl TerminalState {
fn new() -> Self {
Self {
input_buffer: String::new(),
output_history: vec![
"WEYLAND-YUTANI SYSTEMS".to_string(),
"MU/TH/UR 6000 - INTERFACE 2.4.1".to_string(),
"".to_string(),
"SYSTEM INITIALIZED".to_string(),
"AWAITING COMMAND...".to_string(),
"".to_string(),
],
scroll_offset: 0,
cursor_blink: true,
last_blink: Instant::now(),
status_line: "READY".to_string(),
context_info: (0, 0, 0.0),
should_exit: false,
}
}
/// Add text to output history
fn add_output(&mut self, text: &str) {
// Split text by newlines and add each line
for line in text.lines() {
self.output_history.push(line.to_string());
}
// Auto-scroll to bottom
self.scroll_offset = self.output_history.len().saturating_sub(1);
}
}
/// Public interface for the retro terminal
#[derive(Clone)]
pub struct RetroTui {
tx: mpsc::UnboundedSender<TuiMessage>,
state: Arc<Mutex<TerminalState>>,
terminal: Arc<Mutex<Terminal<CrosstermBackend<io::Stdout>>>>,
}
impl RetroTui {
/// Create and start the retro terminal UI
pub async fn start() -> Result<Self> {
// Setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
// Create message channel
let (tx, mut rx) = mpsc::unbounded_channel::<TuiMessage>();
let state = Arc::new(Mutex::new(TerminalState::new()));
let terminal = Arc::new(Mutex::new(terminal));
// Clone for the background task
let state_clone = state.clone();
let terminal_clone = terminal.clone();
// Spawn background task to handle messages and redraw
tokio::spawn(async move {
let mut last_draw = Instant::now();
loop {
// Check for messages
while let Ok(msg) = rx.try_recv() {
let mut state = state_clone.lock().unwrap();
match msg {
TuiMessage::AgentOutput(text) => {
state.add_output(&text);
}
TuiMessage::SystemStatus(status) => {
state.status_line = status;
}
TuiMessage::ContextUpdate {
used,
total,
percentage,
} => {
state.context_info = (used, total, percentage);
}
TuiMessage::Error(err) => {
state.add_output(&format!("ERROR: {}", err));
}
TuiMessage::Exit => {
state.should_exit = true;
break;
}
}
}
// Check if we should exit
if state_clone.lock().unwrap().should_exit {
break;
}
// Update cursor blink
{
let mut state = state_clone.lock().unwrap();
if state.last_blink.elapsed() > Duration::from_millis(500) {
state.cursor_blink = !state.cursor_blink;
state.last_blink = Instant::now();
}
}
// Redraw at ~60fps
if last_draw.elapsed() > Duration::from_millis(16) {
let state = state_clone.lock().unwrap();
let mut term = terminal_clone.lock().unwrap();
let _ = Self::draw(&mut term, &state);
last_draw = Instant::now();
}
// Small sleep to prevent busy waiting
tokio::time::sleep(Duration::from_millis(10)).await;
}
});
// Initial draw
{
let state = state.lock().unwrap();
let mut term = terminal.lock().unwrap();
Self::draw(&mut term, &state)?;
}
Ok(Self {
tx,
state,
terminal,
})
}
/// Draw the terminal UI
fn draw(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
state: &TerminalState,
) -> Result<()> {
terminal.draw(|f| {
let size = f.area();
// Create main layout - header, input, output
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(5), // Header/input area
Constraint::Min(10), // Main output area
Constraint::Length(1), // Status bar
])
.split(size);
// Draw header/input area
Self::draw_input_area(f, chunks[0], &state.input_buffer, state.cursor_blink);
// Draw main output area
Self::draw_output_area(f, chunks[1], &state.output_history, state.scroll_offset);
// Draw status bar
Self::draw_status_bar(f, chunks[2], &state.status_line, state.context_info);
})?;
Ok(())
}
/// Draw the input area with prompt
fn draw_input_area(f: &mut Frame, area: Rect, input_buffer: &str, cursor_blink: bool) {
// Show the actual input buffer content with prompt
let input_text = if cursor_blink {
format!("g3> {}", input_buffer)
} else {
format!("g3> {} ", input_buffer)
};
let input = Paragraph::new(input_text)
.style(Style::default().fg(TERMINAL_GREEN))
.block(
Block::default()
.title(" COMMAND INPUT ")
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_style(Style::default().fg(TERMINAL_DIM_GREEN))
.style(Style::default().bg(TERMINAL_BG)),
);
f.render_widget(input, area);
}
/// Draw the main output area
fn draw_output_area(
f: &mut Frame,
area: Rect,
output_history: &[String],
scroll_offset: usize,
) {
// Calculate visible lines
let visible_height = area.height.saturating_sub(2) as usize; // Account for borders
let total_lines = output_history.len();
// Adjust scroll offset to ensure it's valid
let max_scroll = total_lines.saturating_sub(visible_height);
let scroll = scroll_offset.min(max_scroll);
// Get visible lines
let visible_lines: Vec<Line> = output_history
.iter()
.skip(scroll)
.take(visible_height)
.map(|line| {
// Apply different colors based on content
let style = if line.starts_with("ERROR:") {
Style::default()
.fg(TERMINAL_RED)
.add_modifier(Modifier::BOLD)
} else if line.starts_with('>') {
Style::default().fg(TERMINAL_CYAN)
} else if line.starts_with("SYSTEM:")
|| line.starts_with("WEYLAND")
|| line.starts_with("MU/TH/UR")
{
Style::default()
.fg(TERMINAL_AMBER)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(TERMINAL_GREEN)
};
Line::from(Span::styled(line.clone(), style))
})
.collect();
let output = Paragraph::new(visible_lines)
.block(
Block::default()
.title(" SYSTEM OUTPUT ")
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_style(Style::default().fg(TERMINAL_DIM_GREEN))
.style(Style::default().bg(TERMINAL_BG)),
)
.wrap(Wrap { trim: false });
f.render_widget(output, area);
// Draw scrollbar if needed
if total_lines > visible_height {
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some(""))
.track_symbol(Some(""))
.thumb_symbol("")
.style(Style::default().fg(TERMINAL_DIM_GREEN));
let mut scrollbar_state = ScrollbarState::new(total_lines)
.position(scroll)
.viewport_content_length(visible_height);
f.render_stateful_widget(
scrollbar,
area.inner(ratatui::layout::Margin {
vertical: 1,
horizontal: 0,
}),
&mut scrollbar_state,
);
}
}
/// Draw the status bar
fn draw_status_bar(
f: &mut Frame,
area: Rect,
status_line: &str,
context_info: (u32, u32, f32),
) {
let (used, total, percentage) = context_info;
// Create context meter
let bar_width = 10;
let filled = ((percentage / 100.0) * bar_width as f32) as usize;
let meter = format!("[{}{}]", "".repeat(filled), "".repeat(bar_width - filled));
let status_text = format!(
" STATUS: {} | CONTEXT: {} {:.1}% ({}/{} tokens) | ↑↓ SCROLL | CTRL-C EXIT ",
status_line, meter, percentage, used, total
);
let status = Paragraph::new(status_text)
.style(
Style::default()
.fg(TERMINAL_AMBER)
.bg(TERMINAL_BG)
.add_modifier(Modifier::BOLD),
)
.alignment(Alignment::Left);
f.render_widget(status, area);
}
/// Send output to the terminal
pub fn output(&self, text: &str) {
let _ = self.tx.send(TuiMessage::AgentOutput(text.to_string()));
}
/// Update system status
pub fn status(&self, status: &str) {
let _ = self.tx.send(TuiMessage::SystemStatus(status.to_string()));
}
/// Update context window information
pub fn update_context(&self, used: u32, total: u32, percentage: f32) {
let _ = self.tx.send(TuiMessage::ContextUpdate {
used,
total,
percentage,
});
}
/// Send error message
pub fn error(&self, error: &str) {
let _ = self.tx.send(TuiMessage::Error(error.to_string()));
}
/// Signal exit
pub fn exit(&self) {
let _ = self.tx.send(TuiMessage::Exit);
}
/// Update input buffer (for display)
pub fn update_input(&self, input: &str) {
if let Ok(mut state) = self.state.lock() {
state.input_buffer = input.to_string();
}
}
/// Handle scrolling
pub fn scroll_up(&self) {
if let Ok(mut state) = self.state.lock() {
if state.scroll_offset > 0 {
state.scroll_offset -= 1;
}
}
}
pub fn scroll_down(&self) {
if let Ok(mut state) = self.state.lock() {
state.scroll_offset += 1;
}
}
pub fn scroll_page_up(&self) {
if let Ok(mut state) = self.state.lock() {
state.scroll_offset = state.scroll_offset.saturating_sub(10);
}
}
pub fn scroll_page_down(&self) {
if let Ok(mut state) = self.state.lock() {
state.scroll_offset += 10;
}
}
pub fn scroll_home(&self) {
if let Ok(mut state) = self.state.lock() {
state.scroll_offset = 0;
}
}
pub fn scroll_end(&self) {
if let Ok(mut state) = self.state.lock() {
state.scroll_offset = state.output_history.len().saturating_sub(1);
}
}
}
impl Drop for RetroTui {
fn drop(&mut self) {
// Restore terminal
let _ = disable_raw_mode();
if let Ok(mut term) = self.terminal.lock() {
let _ = execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
);
}
}
}

View File

@@ -1,5 +1,6 @@
use g3_core::ui_writer::UiWriter;
use std::io::{self, Write};
use crate::retro_tui::RetroTui;
/// Console implementation of UiWriter that prints to stdout
pub struct ConsoleUiWriter;
@@ -78,4 +79,85 @@ impl UiWriter for ConsoleUiWriter {
fn flush(&self) {
let _ = io::stdout().flush();
}
}
/// RetroTui implementation of UiWriter that sends output to the TUI
pub struct RetroTuiWriter {
tui: RetroTui,
}
impl RetroTuiWriter {
pub fn new(tui: RetroTui) -> Self {
Self { tui }
}
}
impl UiWriter for RetroTuiWriter {
fn print(&self, message: &str) {
self.tui.output(message);
}
fn println(&self, message: &str) {
self.tui.output(message);
}
fn print_inline(&self, message: &str) {
// For inline printing, we'll just append to the output
self.tui.output(message);
}
fn print_system_prompt(&self, prompt: &str) {
self.tui.output("🔍 System Prompt:");
self.tui.output("================");
for line in prompt.lines() {
self.tui.output(line);
}
self.tui.output("================");
self.tui.output("");
}
fn print_context_status(&self, message: &str) {
self.tui.output(message);
}
fn print_tool_header(&self, tool_name: &str) {
self.tui.output(&format!("┌─ {}", tool_name));
}
fn print_tool_arg(&self, key: &str, value: &str) {
self.tui.output(&format!("{}: {}", key, value));
}
fn print_tool_output_header(&self) {
self.tui.output("├─ output:");
}
fn print_tool_output_line(&self, line: &str) {
self.tui.output(&format!("{}", line));
}
fn print_tool_output_summary(&self, hidden_count: usize) {
self.tui.output(&format!(
"│ ... ({} more line{} hidden)",
hidden_count,
if hidden_count == 1 { "" } else { "s" }
));
}
fn print_tool_timing(&self, duration_str: &str) {
self.tui.output(&format!("└─ ⚡️ {}", duration_str));
self.tui.output("");
}
fn print_agent_prompt(&self) {
self.tui.output("🤖 ");
}
fn print_agent_response(&self, content: &str) {
self.tui.output(content);
}
fn flush(&self) {
// No-op for TUI since it handles its own rendering
}
}

View File

@@ -320,7 +320,9 @@ impl OAuthFlow {
// Open the browser which will redirect with the code to the server
let authorization_url = self.get_authorization_url();
println!("🔐 Opening browser for Databricks authentication...");
if std::env::var("G3_RETRO_MODE").is_err() {
println!("🔐 Opening browser for Databricks authentication...");
}
if webbrowser::open(&authorization_url).is_err() {
println!(
"Please open this URL in your browser:\n{}",
@@ -339,7 +341,9 @@ impl OAuthFlow {
// Stop the server
server_handle.abort();
println!("✅ Authentication successful! Exchanging code for token...");
if std::env::var("G3_RETRO_MODE").is_err() {
println!("✅ Authentication successful! Exchanging code for token...");
}
// Exchange the code for a token
self.exchange_code_for_token(&code).await
@@ -422,7 +426,9 @@ pub async fn get_oauth_token_async(
// Cache and return
token_cache.save_token(&token)?;
println!("🎉 Databricks authentication complete!");
if std::env::var("G3_RETRO_MODE").is_err() {
println!("🎉 Databricks authentication complete!");
}
Ok(token.access_token)
}