diff --git a/TODO b/TODO index 1a94a18..7232b2f 100644 --- a/TODO +++ b/TODO @@ -1,11 +1,18 @@ next tasks -- get something working with autonomous mode +x get something working with autonomous mode - g3d +x ui abstraction from core - context token counting bug +- embedded model + - prompt rewriting + - generates status messages "ruffling feathers..." + - project description? +- treesitter + friends - error where it just gives up turn - "project" behaviors (read readme first) -- ui - - git +- advance project mgmt + - git for reverting - swarm + - ui tests / computer controller diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index e80a935..9de06e1 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -1,7 +1,7 @@ use anyhow::Result; use clap::Parser; use g3_config::Config; -use g3_core::{project::Project, Agent}; +use g3_core::{project::Project, Agent, ui_writer::UiWriter}; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; use std::path::PathBuf; @@ -9,7 +9,9 @@ use tokio_util::sync::CancellationToken; use tracing::{error, info}; mod tui; +mod ui_writer_impl; use tui::SimpleOutput; +use ui_writer_impl::ConsoleUiWriter; #[derive(Parser)] #[command(name = "g3")] @@ -108,7 +110,8 @@ pub async fn run() -> Result<()> { let config = Config::load(cli.config.as_deref())?; // Initialize agent - let mut agent = Agent::new(config).await?; + let ui_writer = ConsoleUiWriter::new(); + let mut agent = Agent::new(config, ui_writer).await?; // Execute task, autonomous mode, or start interactive mode if cli.autonomous { @@ -141,7 +144,7 @@ pub async fn run() -> Result<()> { Ok(()) } -async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -> Result<()> { +async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -> Result<()> { let output = SimpleOutput::new(); output.print(""); @@ -278,7 +281,7 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) - Ok(()) } -async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_code: bool, output: &SimpleOutput) { +async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_code: bool, output: &SimpleOutput) { // Show thinking indicator immediately output.print("šŸ¤” Thinking..."); // Note: flush is handled internally by println @@ -337,7 +340,7 @@ async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_co } } -fn display_context_progress(agent: &Agent, output: &SimpleOutput) { +fn display_context_progress(agent: &Agent, output: &SimpleOutput) { let context = agent.get_context_window(); output.print_context(context.used_tokens, context.total_tokens, context.percentage_used()); } @@ -369,7 +372,7 @@ fn setup_workspace_directory() -> Result { // Simplified autonomous mode implementation async fn run_autonomous( - mut agent: Agent, + mut agent: Agent, project: Project, show_prompt: bool, show_code: bool, @@ -443,7 +446,8 @@ async fn run_autonomous( // Create a new agent instance for coach mode to ensure fresh context let config = g3_config::Config::load(None)?; - let mut coach_agent = Agent::new(config).await?; + let ui_writer = ConsoleUiWriter::new(); + let mut coach_agent = Agent::new(config, ui_writer).await?; // Ensure coach agent is also in the workspace directory project.enter_workspace()?; diff --git a/crates/g3-cli/src/ui_writer_impl.rs b/crates/g3-cli/src/ui_writer_impl.rs new file mode 100644 index 0000000..c79e57d --- /dev/null +++ b/crates/g3-cli/src/ui_writer_impl.rs @@ -0,0 +1,81 @@ +use g3_core::ui_writer::UiWriter; +use std::io::{self, Write}; + +/// Console implementation of UiWriter that prints to stdout +pub struct ConsoleUiWriter; + +impl ConsoleUiWriter { + pub fn new() -> Self { + Self + } +} + +impl UiWriter for ConsoleUiWriter { + fn print(&self, message: &str) { + println!("{}", message); + } + + fn println(&self, message: &str) { + println!("{}", message); + } + + fn print_inline(&self, message: &str) { + print!("{}", message); + let _ = io::stdout().flush(); + } + + fn print_system_prompt(&self, prompt: &str) { + println!("šŸ” System Prompt:"); + println!("================"); + println!("{}", prompt); + println!("================"); + println!(); + } + + fn print_context_status(&self, message: &str) { + println!("{}", message); + } + + fn print_tool_header(&self, tool_name: &str) { + println!("ā”Œā”€ {}", tool_name); + } + + fn print_tool_arg(&self, key: &str, value: &str) { + println!("│ {}: {}", key, value); + } + + fn print_tool_output_header(&self) { + println!("ā”œā”€ output:"); + } + + fn print_tool_output_line(&self, line: &str) { + println!("│ {}", line); + } + + fn print_tool_output_summary(&self, hidden_count: usize) { + println!( + "│ ... ({} more line{} hidden)", + hidden_count, + if hidden_count == 1 { "" } else { "s" } + ); + } + + fn print_tool_timing(&self, duration_str: &str) { + println!("└─ āš”ļø {}", duration_str); + println!(); + } + + fn print_agent_prompt(&self) { + print!("šŸ¤– "); + let _ = io::stdout().flush(); + } + + fn print_agent_response(&self, content: &str) { + print!("{}", content); + let _ = io::stdout().flush(); + } + + fn flush(&self) { + let _ = io::stdout().flush(); + } +} \ No newline at end of file diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index e7037b1..afd8aa9 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -1,5 +1,7 @@ pub mod error_handling; pub mod project; +pub mod ui_writer; +use crate::ui_writer::UiWriter; #[cfg(test)] mod error_handling_test; @@ -331,15 +333,16 @@ Format this as a detailed but concise summary that can be used to resume the con } } -pub struct Agent { +pub struct Agent { providers: ProviderRegistry, context_window: ContextWindow, session_id: Option, tool_call_metrics: Vec<(String, Duration, bool)>, // (tool_name, duration, success) + ui_writer: W, } -impl Agent { - pub async fn new(config: Config) -> Result { +impl Agent { + pub async fn new(config: Config, ui_writer: W) -> Result { let mut providers = ProviderRegistry::new(); // Only register providers that are configured AND selected as the default provider @@ -428,6 +431,7 @@ impl Agent { context_window, session_id: None, tool_call_metrics: Vec::new(), + ui_writer, }) } @@ -630,7 +634,7 @@ The tool will execute immediately and you'll receive the result (success or erro - **str_replace**: Replace text in a file using a diff - Format: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"path/to/file\", \"diff\": \"--- old\\n-old text\\n+++ new\\n+new text\"}} - - Example: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"src/main.rs\", \"diff\": \"--- old\\n-println!(\\\"old\\\");\\n+++ new\\n+println!(\\\"new\\\");\"}} + - Example: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"src/main.rs\", \"diff\": \"--- old\\n-old_code();\\n+++ new\\n+new_code();\"}} - **final_output**: Signal task completion with a detailed summary of work done in markdown format - Format: {\"tool\": \"final_output\", \"args\": {\"summary\": \"what_was_accomplished\"}} @@ -651,11 +655,7 @@ The tool will execute immediately and you'll receive the result (success or erro }; if show_prompt { - println!("šŸ” System Prompt:"); - println!("================"); - println!("{}", system_prompt); - println!("================"); - println!(); + self.ui_writer.print_system_prompt(&system_prompt); } // Add system message to context window @@ -1029,7 +1029,6 @@ The tool will execute immediately and you'll receive the result (success or erro mut request: CompletionRequest, ) -> Result<(String, Duration)> { use crate::error_handling::ErrorContext; - use std::io::{self, Write}; use tokio_stream::StreamExt; debug!("Starting stream_completion_with_tools"); @@ -1052,10 +1051,10 @@ The tool will execute immediately and you'll receive the result (success or erro ); // Notify user about summarization - println!( + self.ui_writer.print_context_status(&format!( "\nšŸ“Š Context window reaching capacity ({}%). Creating summary...", self.context_window.percentage_used() as u32 - ); + )); // Create summary request with FULL history let summary_prompt = self.context_window.create_summary_prompt(); @@ -1135,7 +1134,7 @@ The tool will execute immediately and you'll receive the result (success or erro // Get the summary match provider.complete(summary_request).await { Ok(summary_response) => { - println!("āœ… Summary created successfully. Resetting context window...\n"); + self.ui_writer.print_context_status("āœ… Summary created successfully. Resetting context window...\n"); // Extract the latest user message from the request let latest_user_msg = request @@ -1152,11 +1151,11 @@ The tool will execute immediately and you'll receive the result (success or erro // Update the request with new context request.messages = self.context_window.conversation_history.clone(); - println!("šŸ”„ Context reset complete. Continuing with your request...\n"); + self.ui_writer.print_context_status("šŸ”„ Context reset complete. Continuing with your request...\n"); } Err(e) => { error!("Failed to create summary: {}", e); - println!("āš ļø Unable to create summary. Consider starting a new session if you continue to see errors.\n"); + self.ui_writer.print_context_status("āš ļø Unable to create summary. Consider starting a new session if you continue to see errors.\n"); // Don't continue with the original request if summarization failed // as we're likely at token limit return Err(anyhow::anyhow!("Context window at capacity and summarization failed. Please start a new session.")); @@ -1318,20 +1317,20 @@ The tool will execute immediately and you'll receive the result (success or erro if !new_content.trim().is_empty() { if !response_started { - print!("\ršŸ¤– "); + self.ui_writer.print_agent_prompt(); response_started = true; } - print!("{}", new_content); - io::stdout().flush()?; + self.ui_writer.print_agent_response(&new_content); + self.ui_writer.flush(); } // Execute the tool with formatted output - println!(); // New line before tool execution + self.ui_writer.println(""); // New line before tool execution // Skip printing tool call details for final_output if tool_call.tool != "final_output" { // Tool call header - println!("ā”Œā”€ {}", tool_call.tool); + self.ui_writer.print_tool_header(&tool_call.tool); if let Some(args_obj) = tool_call.args.as_object() { for (key, value) in args_obj { let value_str = match value { @@ -1356,10 +1355,10 @@ The tool will execute immediately and you'll receive the result (success or erro } _ => value.to_string(), }; - println!("│ {}: {}", key, value_str); + self.ui_writer.print_tool_arg(key, &value_str); } } - println!("ā”œā”€ output:"); + self.ui_writer.print_tool_output_header(); } let exec_start = Instant::now(); @@ -1382,18 +1381,14 @@ The tool will execute immediately and you'll receive the result (success or erro if output_lines.len() <= MAX_LINES { for line in output_lines { - println!("│ {}", line); + self.ui_writer.print_tool_output_line(line); } } else { for line in output_lines.iter().take(MAX_LINES) { - println!("│ {}", line); + self.ui_writer.print_tool_output_line(line); } let hidden_count = output_lines.len() - MAX_LINES; - println!( - "│ ... ({} more line{} hidden)", - hidden_count, - if hidden_count == 1 { "" } else { "s" } - ); + self.ui_writer.print_tool_output_summary(hidden_count); } } @@ -1405,7 +1400,7 @@ The tool will execute immediately and you'll receive the result (success or erro full_response.push_str(&format!("\n\n=> {}", summary_str)); } } - println!(); + self.ui_writer.println(""); let ttft = first_token_time.unwrap_or_else(|| stream_start.elapsed()); return Ok((full_response, ttft)); @@ -1413,10 +1408,8 @@ The tool will execute immediately and you'll receive the result (success or erro // Closure marker with timing if tool_call.tool != "final_output" { - println!("└─ āš”ļø {}", Self::format_duration(exec_duration)); - println!(); - print!("šŸ¤– "); - io::stdout().flush()?; + self.ui_writer.print_tool_timing(&Self::format_duration(exec_duration)); + self.ui_writer.print_agent_prompt(); } // Add the tool call and result to the context window @@ -1482,12 +1475,12 @@ The tool will execute immediately and you'll receive the result (success or erro if !filtered_content.is_empty() { if !response_started { - print!("\ršŸ¤– "); + self.ui_writer.print_agent_prompt(); response_started = true; } - print!("{}", filtered_content); - let _ = io::stdout().flush(); + self.ui_writer.print_agent_response(&filtered_content); + self.ui_writer.flush(); current_response.push_str(&filtered_content); } } @@ -1599,7 +1592,7 @@ The tool will execute immediately and you'll receive the result (success or erro full_response.push_str(¤t_response); } - println!(); + self.ui_writer.println(""); let ttft = first_token_time.unwrap_or_else(|| stream_start.elapsed()); return Ok((full_response, ttft)); @@ -1643,7 +1636,7 @@ The tool will execute immediately and you'll receive the result (success or erro ); } else { full_response.push_str(¤t_response); - println!(); + self.ui_writer.println(""); } let ttft = first_token_time.unwrap_or_else(|| stream_start.elapsed()); diff --git a/crates/g3-core/src/ui_writer.rs b/crates/g3-core/src/ui_writer.rs new file mode 100644 index 0000000..4be0fbc --- /dev/null +++ b/crates/g3-core/src/ui_writer.rs @@ -0,0 +1,66 @@ +/// Interface for UI output operations +/// This trait abstracts all UI operations to allow different implementations +/// (console, TUI, web, etc.) without coupling the core logic to specific output methods. +pub trait UiWriter: Send + Sync { + /// Print a simple message + fn print(&self, message: &str); + + /// Print a message with a newline + fn println(&self, message: &str); + + /// Print without newline (for progress indicators) + fn print_inline(&self, message: &str); + + /// Print a system prompt section + fn print_system_prompt(&self, prompt: &str); + + /// Print a context window status message + fn print_context_status(&self, message: &str); + + /// Print a tool execution header + fn print_tool_header(&self, tool_name: &str); + + /// Print a tool argument + fn print_tool_arg(&self, key: &str, value: &str); + + /// Print tool output header + fn print_tool_output_header(&self); + + /// Print a tool output line + fn print_tool_output_line(&self, line: &str); + + /// Print tool output summary (when output is truncated) + fn print_tool_output_summary(&self, hidden_count: usize); + + /// Print tool execution timing + fn print_tool_timing(&self, duration_str: &str); + + /// Print the agent prompt indicator + fn print_agent_prompt(&self); + + /// Print agent response inline (for streaming) + fn print_agent_response(&self, content: &str); + + /// Flush any buffered output + fn flush(&self); +} + +/// A no-op implementation for when UI output is not needed +pub struct NullUiWriter; + +impl UiWriter for NullUiWriter { + fn print(&self, _message: &str) {} + fn println(&self, _message: &str) {} + fn print_inline(&self, _message: &str) {} + fn print_system_prompt(&self, _prompt: &str) {} + fn print_context_status(&self, _message: &str) {} + fn print_tool_header(&self, _tool_name: &str) {} + fn print_tool_arg(&self, _key: &str, _value: &str) {} + fn print_tool_output_header(&self) {} + fn print_tool_output_line(&self, _line: &str) {} + fn print_tool_output_summary(&self, _hidden_count: usize) {} + fn print_tool_timing(&self, _duration_str: &str) {} + fn print_agent_prompt(&self) {} + fn print_agent_response(&self, _content: &str) {} + fn flush(&self) {} +} \ No newline at end of file