diff --git a/Cargo.lock b/Cargo.lock index 3e1e34f..8fe9f11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -674,6 +674,7 @@ version = "2.0.0" dependencies = [ "anyhow", "async-trait", + "atty", "chrono", "dirs", "gyandoot-browser", diff --git a/crates/gyandoot-agent/Cargo.toml b/crates/gyandoot-agent/Cargo.toml index dc08d14..809248f 100644 --- a/crates/gyandoot-agent/Cargo.toml +++ b/crates/gyandoot-agent/Cargo.toml @@ -22,4 +22,5 @@ uuid.workspace = true chrono.workspace = true async-trait.workspace = true rand.workspace = true -dirs.workspace = true \ No newline at end of file +dirs.workspace = true +atty.workspace = true \ No newline at end of file diff --git a/crates/gyandoot-agent/src/agent_loop.rs b/crates/gyandoot-agent/src/agent_loop.rs index 2cf9252..171c048 100644 --- a/crates/gyandoot-agent/src/agent_loop.rs +++ b/crates/gyandoot-agent/src/agent_loop.rs @@ -24,6 +24,18 @@ use tracing::{debug, error, info, span, warn, Level}; use crate::context::ContextCompressor; use crate::tools::ToolExecutor; +// ─── CLI color constants ────────────────────────────────────────────── +mod cli_color { + pub const RESET: &str = "\x1b[0m"; + pub const BOLD: &str = "\x1b[1m"; + pub const DIM: &str = "\x1b[2m"; + pub const CYAN: &str = "\x1b[36m"; + pub const BRIGHT_CYAN: &str = "\x1b[96m"; + pub const BRIGHT_MAGENTA: &str = "\x1b[95m"; + pub const GREEN: &str = "\x1b[32m"; + pub const YELLOW: &str = "\x1b[33m"; +} + // ─── Error classification ─────────────────────────────────────────────── /// Categories of errors the agent loop can encounter during LLM calls. @@ -369,7 +381,12 @@ impl AgentLoop { .build_context(&messages, &self.config.system_prompt, &self.tool_executor.lock().await.definitions(), self.config.max_tokens); *self.status.lock().await = AgentStatus::Thinking; - let response = self.call_with_retry(context).await?; + + // Show thinking animation while waiting for LLM + let spinner_handle = self.start_thinking_indicator(); + let response = self.call_with_retry(context).await; + self.stop_thinking_indicator(spinner_handle).await; + let response = response?; messages.push(Message::new(MessageRole::Assistant, &response.content) .with_tool_calls(response.tool_calls.clone().unwrap_or_default())); @@ -525,9 +542,73 @@ impl AgentLoop { ))) } + /// Display output to stdout with GyanDoot prefix. + fn display_output(&self, output: &str) { + println!( + "\n{bold}{green}G{reset}> {output}", + bold = cli_color::BOLD, + green = cli_color::GREEN, + reset = cli_color::RESET, + output = output, + ); + } + + /// Start the thinking animation (spinner on stderr). + /// Returns a handle to stop the animation. + fn start_thinking_indicator(&self) -> tokio::sync::watch::Sender { + use std::io::Write; + let (tx, mut rx) = tokio::sync::watch::channel(true); + let frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + + tokio::spawn(async move { + let mut frame_idx = 0; + let start = std::time::Instant::now(); + while *rx.borrow_and_update() { + let spinner = frames[frame_idx % frames.len()]; + frame_idx += 1; + let elapsed = start.elapsed().as_secs_f64(); + eprint!( + "\r{magenta}{spinner}{reset} {bold}answering{reset} ({elapsed:.1}s) ", + magenta = cli_color::BRIGHT_MAGENTA, + spinner = spinner, + reset = cli_color::RESET, + bold = cli_color::BOLD, + elapsed = elapsed, + ); + std::io::stderr().flush().ok(); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + // Clear the spinner line + eprint!("\r\x1b[K"); + std::io::stderr().flush().ok(); + }); + + tx + } + + /// Stop the thinking animation. + async fn stop_thinking_indicator(&self, handle: tokio::sync::watch::Sender) { + let _ = handle.send(false); + // Small delay for the spinner task to finish and clear the line + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + /// Read a line of input from stdin. async fn read_input(&self) -> anyhow::Result { + use std::io::Write; use tokio::io::AsyncBufReadExt; + + // Show input prompt + if atty::is(atty::Stream::Stdin) { + print!( + "{bold}{cyan}»{reset} ", + bold = cli_color::BOLD, + cyan = cli_color::BRIGHT_CYAN, + reset = cli_color::RESET, + ); + std::io::stdout().flush().ok(); + } + let mut line = String::new(); let mut reader = tokio::io::BufReader::new(tokio::io::stdin()); reader.read_line(&mut line).await?; @@ -538,11 +619,6 @@ impl AgentLoop { fn should_exit(&self, input: &str) -> bool { matches!(input.to_lowercase().as_str(), "exit" | "quit" | "bye") } - - /// Display output to stdout. - fn display_output(&self, output: &str) { - println!("\n{}", output); - } } /// Calculate exponential backoff with jitter.