CLI UX: Input prompt (»), G> output prefix, answering spinner
Three UX improvements for interactive mode: 1. Input prompt: Shows '»' (bold cyan) on stdin before reading user input 2. Output prefix: Agent responses prefixed with 'G>' (bold green) 3. Thinking indicator: Animated spinner on stderr showing 'answering (N.Ns)' with braille frame characters, auto-clears when response arrives - Start/stop via tokio::sync::watch channel + tokio::spawn - Only shows when interactive (atty detection) - Does not affect gateway/sub-agent modes
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -674,6 +674,7 @@ version = "2.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"atty",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs",
|
"dirs",
|
||||||
"gyandoot-browser",
|
"gyandoot-browser",
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ chrono.workspace = true
|
|||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
dirs.workspace = true
|
dirs.workspace = true
|
||||||
|
atty.workspace = true
|
||||||
@@ -24,6 +24,18 @@ use tracing::{debug, error, info, span, warn, Level};
|
|||||||
use crate::context::ContextCompressor;
|
use crate::context::ContextCompressor;
|
||||||
use crate::tools::ToolExecutor;
|
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 ───────────────────────────────────────────────
|
// ─── Error classification ───────────────────────────────────────────────
|
||||||
|
|
||||||
/// Categories of errors the agent loop can encounter during LLM calls.
|
/// 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);
|
.build_context(&messages, &self.config.system_prompt, &self.tool_executor.lock().await.definitions(), self.config.max_tokens);
|
||||||
|
|
||||||
*self.status.lock().await = AgentStatus::Thinking;
|
*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)
|
messages.push(Message::new(MessageRole::Assistant, &response.content)
|
||||||
.with_tool_calls(response.tool_calls.clone().unwrap_or_default()));
|
.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<bool> {
|
||||||
|
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<bool>) {
|
||||||
|
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.
|
/// Read a line of input from stdin.
|
||||||
async fn read_input(&self) -> anyhow::Result<String> {
|
async fn read_input(&self) -> anyhow::Result<String> {
|
||||||
|
use std::io::Write;
|
||||||
use tokio::io::AsyncBufReadExt;
|
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 line = String::new();
|
||||||
let mut reader = tokio::io::BufReader::new(tokio::io::stdin());
|
let mut reader = tokio::io::BufReader::new(tokio::io::stdin());
|
||||||
reader.read_line(&mut line).await?;
|
reader.read_line(&mut line).await?;
|
||||||
@@ -538,11 +619,6 @@ impl AgentLoop {
|
|||||||
fn should_exit(&self, input: &str) -> bool {
|
fn should_exit(&self, input: &str) -> bool {
|
||||||
matches!(input.to_lowercase().as_str(), "exit" | "quit" | "bye")
|
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.
|
/// Calculate exponential backoff with jitter.
|
||||||
|
|||||||
Reference in New Issue
Block a user