diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index 3facf6a..5b64b65 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -167,14 +167,12 @@ use tokio_util::sync::CancellationToken; use tracing::{error, info}; use g3_core::error_handling::{classify_error, ErrorType, RecoverableError}; -mod retro_tui; -mod theme; -pub mod tui; mod ui_writer_impl; -use retro_tui::RetroTui; -use theme::ColorTheme; -use tui::SimpleOutput; -use ui_writer_impl::{ConsoleUiWriter, RetroTuiWriter}; +mod simple_output; +use simple_output::SimpleOutput; +mod machine_ui_writer; +use machine_ui_writer::MachineUiWriter; +use ui_writer_impl::ConsoleUiWriter; #[derive(Parser)] #[command(name = "g3")] @@ -220,13 +218,9 @@ pub struct Cli { #[arg(long)] pub interactive_requirements: bool, - /// Use retro terminal UI (inspired by 80s sci-fi) + /// Enable machine-friendly output mode with JSON markers and stats #[arg(long)] - pub retro: bool, - - /// Color theme for retro mode (default, dracula, or path to theme file) - #[arg(long, value_name = "THEME")] - pub theme: Option, + pub machine: bool, /// Override the configured provider (anthropic, databricks, embedded, openai) #[arg(long, value_name = "PROVIDER")] @@ -253,7 +247,7 @@ pub async fn run() -> Result<()> { let cli = Cli::parse(); // Only initialize logging if not in retro mode - if !cli.retro { + if !cli.machine { // Initialize logging with filtering use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; @@ -291,16 +285,16 @@ pub async fn run() -> Result<()> { tracing_subscriber::registry().with(filter).init(); } - if !cli.retro { + if !cli.machine { info!("Starting G3 AI Coding Agent"); } // Set up workspace directory - let workspace_dir = if let Some(ws) = cli.workspace { - ws + let workspace_dir = if let Some(ws) = &cli.workspace { + ws.clone() } else if cli.autonomous { // For autonomous mode, use G3_WORKSPACE env var or default - setup_workspace_directory()? + setup_workspace_directory(cli.machine)? } else { // Default to current directory for interactive/single-shot mode std::env::current_dir()? @@ -421,9 +415,9 @@ Output ONLY the markdown content, no explanations or meta-commentary."#, } } - if let Some(requirements_text) = cli.requirements { + if let Some(requirements_text) = &cli.requirements { // Use requirements text override - Project::new_autonomous_with_requirements(workspace_dir.clone(), requirements_text)? + Project::new_autonomous_with_requirements(workspace_dir.clone(), requirements_text.clone())? } else { // Use traditional requirements.md file Project::new_autonomous(workspace_dir.clone())? @@ -436,7 +430,7 @@ Output ONLY the markdown content, no explanations or meta-commentary."#, project.ensure_workspace_exists()?; project.enter_workspace()?; - if !cli.retro { + if !cli.machine { info!("Using workspace: {}", project.workspace().display()); } @@ -450,7 +444,7 @@ Output ONLY the markdown content, no explanations or meta-commentary."#, // Apply macax flag override if cli.macax { config.macax.enabled = true; - if !cli.retro { + if !cli.machine { info!("macOS Accessibility API tools enabled"); } } @@ -473,7 +467,7 @@ Output ONLY the markdown content, no explanations or meta-commentary."#, } // Initialize agent - let ui_writer = ConsoleUiWriter::new(); + // ui_writer will be created conditionally based on machine mode // Combine AGENTS.md and README content if both exist let combined_content = match (agents_content.clone(), readme_content.clone()) { @@ -485,28 +479,117 @@ Output ONLY the markdown content, no explanations or meta-commentary."#, (None, None) => None, }; - let mut agent = if cli.autonomous { - Agent::new_autonomous_with_readme_and_quiet( - config.clone(), - ui_writer, - combined_content.clone(), - cli.quiet, - ) - .await? + // Execute task, autonomous mode, or start interactive mode based on machine mode + if cli.machine { + // Machine mode - use MachineUiWriter + let ui_writer = MachineUiWriter::new(); + + let agent = if cli.autonomous { + Agent::new_autonomous_with_readme_and_quiet( + config.clone(), + ui_writer, + combined_content.clone(), + cli.quiet, + ) + .await? + } else { + Agent::new_with_readme_and_quiet( + config.clone(), + ui_writer, + combined_content.clone(), + cli.quiet, + ) + .await? + }; + + run_with_machine_mode(agent, cli, project).await?; } else { - Agent::new_with_readme_and_quiet( - config.clone(), - ui_writer, - combined_content.clone(), - cli.quiet, - ) - .await? + // Normal mode - use ConsoleUiWriter + let ui_writer = ConsoleUiWriter::new(); + + let agent = if cli.autonomous { + Agent::new_autonomous_with_readme_and_quiet( + config.clone(), + ui_writer, + combined_content.clone(), + cli.quiet, + ) + .await? + } else { + Agent::new_with_readme_and_quiet( + config.clone(), + ui_writer, + combined_content.clone(), + cli.quiet, + ) + .await? + }; + + run_with_console_mode(agent, cli, project, combined_content).await?; + } + + Ok(()) +} + +// Simplified machine mode version of autonomous mode +async fn run_autonomous_machine( + mut agent: Agent, + project: Project, + show_prompt: bool, + show_code: bool, + max_turns: usize, + _quiet: bool, +) -> Result<()> { + println!("AUTONOMOUS_MODE_STARTED"); + println!("WORKSPACE: {}", project.workspace().display()); + println!("MAX_TURNS: {}", max_turns); + + // Check if requirements exist + if !project.has_requirements() { + println!("ERROR: requirements.md not found in workspace directory"); + return Ok(()); + } + + // Read requirements + let requirements = match project.read_requirements()? { + Some(content) => content, + None => { + println!("ERROR: Could not read requirements"); + return Ok(()); + } }; + println!("REQUIREMENTS_LOADED"); + + // For now, just execute a simple autonomous loop + // This is a simplified version - full implementation would need coach-player loop + let task = format!( + "You are G3 in implementation mode. Read and implement the following requirements:\n\n{}\n\nImplement this step by step, creating all necessary files and code.", + requirements + ); + + println!("TASK_START"); + let result = agent.execute_task_with_timing(&task, None, false, show_prompt, show_code, true).await?; + println!("AGENT_RESPONSE:"); + println!("{}", result.response); + println!("END_AGENT_RESPONSE"); + println!("TASK_END"); + + println!("AUTONOMOUS_MODE_ENDED"); + Ok(()) +} + +async fn run_with_console_mode( + mut agent: Agent, + cli: Cli, + project: Project, + combined_content: Option, +) -> Result<()> { + // Execute task, autonomous mode, or start interactive mode if cli.autonomous { // Autonomous mode with coach-player feedback loop - if !cli.retro { + if !cli.machine { info!("Starting autonomous mode"); } run_autonomous( @@ -520,7 +603,7 @@ Output ONLY the markdown content, no explanations or meta-commentary."#, .await?; } else if let Some(task) = cli.task { // Single-shot mode - if !cli.retro { + if !cli.machine { info!("Executing task: {}", task); } let output = SimpleOutput::new(); @@ -530,26 +613,43 @@ Output ONLY the markdown content, no explanations or meta-commentary."#, output.print_smart(&result.response); } else { // Interactive mode (default) - if !cli.retro { + if !cli.machine { info!("Starting interactive mode"); } + println!("šŸ“ Workspace: {}", project.workspace().display()); + run_interactive(agent, cli.show_prompt, cli.show_code, combined_content).await?; + } - if cli.retro { - // Use retro terminal UI - run_interactive_retro( - config, // Already has overrides applied - cli.show_prompt, - cli.show_code, - cli.theme, - combined_content, - ) + Ok(()) +} + +async fn run_with_machine_mode( + mut agent: Agent, + cli: Cli, + project: Project, +) -> Result<()> { + if cli.autonomous { + // Autonomous mode with coach-player feedback loop + run_autonomous_machine( + agent, + project, + cli.show_prompt, + cli.show_code, + cli.max_turns, + cli.quiet, + ) + .await?; + } else if let Some(task) = cli.task { + // Single-shot mode + let result = agent + .execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true) .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, combined_content).await?; - } + println!("AGENT_RESPONSE:"); + println!("{}", result.response); + println!("END_AGENT_RESPONSE"); + } else { + // Interactive mode + run_interactive_machine(agent, cli.show_prompt, cli.show_code).await?; } Ok(()) @@ -691,274 +791,6 @@ fn extract_readme_heading(readme_content: &str) -> Option { None } -async fn run_interactive_retro( - config: Config, - show_prompt: bool, - show_code: bool, - theme_name: Option, - combined_content: Option, -) -> 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"); - - // Load the color theme - let theme = match ColorTheme::load(theme_name.as_deref()) { - Ok(t) => t, - Err(e) => { - eprintln!("Failed to load theme: {}. Using default.", e); - ColorTheme::default() - } - }; - - // Initialize the retro terminal UI - let tui = RetroTui::start(theme).await?; - - // Create agent with RetroTuiWriter - let ui_writer = RetroTuiWriter::new(tui.clone()); - let mut agent = Agent::new_with_readme_and_quiet(config, ui_writer, combined_content.clone(), false).await?; - - // Display initial system messages - tui.output("SYSTEM: AGENT ONLINE\n\n"); - - // Display message if AGENTS.md or README was loaded - if let Some(ref content) = combined_content { - // Check what was loaded - let has_agents = content.contains("Agent Configuration"); - let has_readme = content.contains("Project README"); - - if has_agents { - tui.output("SYSTEM: AGENT CONFIGURATION LOADED\n\n"); - } - - if has_readme { - // Extract the first heading or title from the README - let readme_snippet = extract_readme_heading(content) - .unwrap_or_else(|| "PROJECT DOCUMENTATION LOADED".to_string()); - - tui.output(&format!( - "SYSTEM: PROJECT README LOADED - {}\n\n", - readme_snippet - )); - } - } - tui.output("SYSTEM: READY FOR INPUT\n\n"); - tui.output("\n\n"); - - // Display provider and model information - match agent.get_provider_info() { - Ok((provider, model)) => { - tui.update_provider_info(&provider, &model); - } - Err(e) => { - tui.update_provider_info("ERROR", &e.to_string()); - } - } - - // Track multiline input - let mut multiline_buffer = String::new(); - let mut in_multiline = false; - - // 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(), - ); - - // 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; - } - // Emacs/bash-like shortcuts - KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => { - tui.cursor_home(); - } - KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { - tui.cursor_end(); - } - KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => { - tui.delete_word(); - } - KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => { - tui.delete_to_end(); - } - KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Delete from beginning to cursor (similar to Ctrl-K but opposite direction) - let (input_buffer, cursor_pos) = tui.get_input_state(); - if cursor_pos > 0 { - let after = input_buffer.chars().skip(cursor_pos).collect::(); - tui.update_input(&after); - tui.cursor_home(); - } - } - KeyCode::Left => { - tui.cursor_left(); - } - KeyCode::Right => { - tui.cursor_right(); - } - KeyCode::Home if !key.modifiers.contains(KeyModifiers::CONTROL) => { - tui.cursor_home(); - } - KeyCode::End if !key.modifiers.contains(KeyModifiers::CONTROL) => { - tui.cursor_end(); - } - KeyCode::Delete => { - tui.delete_char(); - } - KeyCode::Enter => { - let (input_buffer, _) = tui.get_input_state(); - if !input_buffer.is_empty() { - // Clear the input for next command - tui.update_input(""); - let trimmed = input_buffer.trim_end(); - - // Check if line ends with backslash for continuation - if let Some(without_backslash) = trimmed.strip_suffix('\\') { - // Remove the backslash and add to buffer - multiline_buffer.push_str(without_backslash); - multiline_buffer.push('\n'); - in_multiline = true; - 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() - }; - - 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"); - - const MAX_TIMEOUT_RETRIES: u32 = 3; - let mut attempt = 0; - - loop { - attempt += 1; - - match agent - .execute_task_with_timing( - &input, - None, - false, - show_prompt, - show_code, - true, - ) - .await - { - Ok(result) => { - if attempt > 1 { - tui.output(&format!( - "SYSTEM: REQUEST SUCCEEDED AFTER {} ATTEMPTS", - attempt - )); - } - tui.output(&result.response); - tui.status("READY"); - break; - } - Err(e) => { - // Check if this is a timeout error that we should retry - let error_type = classify_error(&e); - - if matches!( - error_type, - ErrorType::Recoverable(RecoverableError::Timeout) - ) && attempt < MAX_TIMEOUT_RETRIES - { - // Calculate retry delay with exponential backoff - let delay_ms = 1000 * (2_u64.pow(attempt - 1)); - let delay = std::time::Duration::from_millis(delay_ms); - - tui.output(&format!("SYSTEM: TIMEOUT ERROR (ATTEMPT {}/{}). RETRYING IN {:?}...", - attempt, MAX_TIMEOUT_RETRIES, delay)); - tui.status("RETRYING"); - - // Wait before retrying - tokio::time::sleep(delay).await; - continue; - } - - // For non-timeout errors or after max retries - tui.error(&format!("Task execution failed: {}", e)); - tui.status("ERROR"); - break; - } - } - } - } - } - KeyCode::Char(c) => { - tui.insert_char(c); - } - KeyCode::Backspace => { - tui.backspace(); - } - KeyCode::Up => { - tui.scroll_up(); - } - KeyCode::Down => { - tui.scroll_down(); - } - KeyCode::PageUp => { - tui.scroll_page_up(); - } - KeyCode::PageDown => { - tui.scroll_page_down(); - } - KeyCode::Home if key.modifiers.contains(KeyModifiers::CONTROL) => { - tui.scroll_home(); // Ctrl+Home for scrolling to top - } - KeyCode::End if key.modifiers.contains(KeyModifiers::CONTROL) => { - tui.scroll_end(); // Ctrl+End for scrolling to bottom - } - _ => {} - } - } - } - - // Small delay to prevent CPU spinning - tokio::time::sleep(Duration::from_millis(10)).await; - } - - tui.output("SYSTEM: SHUTDOWN INITIATED"); - Ok(()) -} - async fn run_interactive( mut agent: Agent, show_prompt: bool, @@ -1109,7 +941,7 @@ async fn run_interactive( } "/thinnify" => { let summary = agent.force_thin(); - output.print_context_thinning(&summary); + println!("{}", summary); continue; } "/readme" => { @@ -1247,6 +1079,178 @@ async fn execute_task( } } +async fn run_interactive_machine( + mut agent: Agent, + show_prompt: bool, + show_code: bool, +) -> Result<()> { + println!("INTERACTIVE_MODE_STARTED"); + + // Display provider and model information + match agent.get_provider_info() { + Ok((provider, model)) => { + println!("PROVIDER: {}", provider); + println!("MODEL: {}", model); + } + Err(e) => { + println!("ERROR: Failed to get provider info: {}", e); + } + } + + // Initialize rustyline editor with history + let mut rl = DefaultEditor::new()?; + + // Try to load history from a file in the user's home directory + let history_file = dirs::home_dir().map(|mut path| { + path.push(".g3_history"); + path + }); + + if let Some(ref history_path) = history_file { + let _ = rl.load_history(history_path); + } + + loop { + let readline = rl.readline(""); + match readline { + Ok(line) => { + let input = line.trim().to_string(); + + if input.is_empty() { + continue; + } + + if input == "exit" || input == "quit" { + break; + } + + // Add to history + rl.add_history_entry(&input)?; + + // Check for control commands + if input.starts_with('/') { + match input.as_str() { + "/compact" => { + println!("COMMAND: compact"); + match agent.force_summarize().await { + Ok(true) => println!("RESULT: Summarization completed"), + Ok(false) => println!("RESULT: Summarization failed"), + Err(e) => println!("ERROR: {}", e), + } + continue; + } + "/thinnify" => { + println!("COMMAND: thinnify"); + let summary = agent.force_thin(); + println!("{}", summary); + continue; + } + _ => { + println!("ERROR: Unknown command: {}", input); + continue; + } + } + } + + // Execute task + println!("TASK_START"); + execute_task_machine(&mut agent, &input, show_prompt, show_code).await; + println!("TASK_END"); + } + Err(ReadlineError::Interrupted) => continue, + Err(ReadlineError::Eof) => break, + Err(err) => { + println!("ERROR: {:?}", err); + break; + } + } + } + + // Save history before exiting + if let Some(ref history_path) = history_file { + let _ = rl.save_history(history_path); + } + + println!("INTERACTIVE_MODE_ENDED"); + Ok(()) +} + +async fn execute_task_machine( + agent: &mut Agent, + input: &str, + show_prompt: bool, + show_code: bool, +) { + const MAX_TIMEOUT_RETRIES: u32 = 3; + let mut attempt = 0; + + // Create cancellation token for this request + let cancellation_token = CancellationToken::new(); + let cancel_token_clone = cancellation_token.clone(); + + loop { + attempt += 1; + + // Execute task with cancellation support + let execution_result = tokio::select! { + result = agent.execute_task_with_timing_cancellable( + input, None, false, show_prompt, show_code, true, cancellation_token.clone() + ) => { + result + } + _ = tokio::signal::ctrl_c() => { + cancel_token_clone.cancel(); + println!("CANCELLED"); + return; + } + }; + + match execution_result { + Ok(result) => { + if attempt > 1 { + println!("RETRY_SUCCESS: attempt {}", attempt); + } + println!("AGENT_RESPONSE:"); + println!("{}", result.response); + println!("END_AGENT_RESPONSE"); + return; + } + Err(e) => { + if e.to_string().contains("cancelled") { + println!("CANCELLED"); + return; + } + + // Check if this is a timeout error that we should retry + let error_type = classify_error(&e); + + if matches!( + error_type, + ErrorType::Recoverable(RecoverableError::Timeout) + ) && attempt < MAX_TIMEOUT_RETRIES + { + // Calculate retry delay with exponential backoff + let delay_ms = 1000 * (2_u64.pow(attempt - 1)); + let delay = std::time::Duration::from_millis(delay_ms); + + println!("TIMEOUT: attempt {} of {}, retrying in {:?}", attempt, MAX_TIMEOUT_RETRIES, delay); + + // Wait before retrying + tokio::time::sleep(delay).await; + continue; + } + + // For non-timeout errors or after max retries + println!("ERROR: {}", e); + if attempt > 1 { + println!("FAILED_AFTER_RETRIES: {}", attempt); + } + return; + } + } + } +} + fn handle_execution_error(e: &anyhow::Error, input: &str, output: &SimpleOutput, attempt: u32) { // Enhanced error logging with detailed information error!("=== TASK EXECUTION ERROR ==="); @@ -1280,16 +1284,13 @@ fn handle_execution_error(e: &anyhow::Error, input: &str, 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(), - ); + output.print(&format!("Context: {}/{} tokens ({:.1}%)", + context.used_tokens, context.total_tokens, context.percentage_used())); } /// Set up the workspace directory for autonomous mode /// Uses G3_WORKSPACE environment variable or defaults to ~/tmp/workspace -fn setup_workspace_directory() -> Result { +fn setup_workspace_directory(machine_mode: bool) -> Result { let workspace_dir = if let Ok(env_workspace) = std::env::var("G3_WORKSPACE") { PathBuf::from(env_workspace) } else { @@ -1302,7 +1303,7 @@ fn setup_workspace_directory() -> Result { // Create the directory if it doesn't exist if !workspace_dir.exists() { std::fs::create_dir_all(&workspace_dir)?; - let output = SimpleOutput::new(); + let output = SimpleOutput::new_with_mode(machine_mode); output.print(&format!( "šŸ“ Created workspace directory: {}", workspace_dir.display() diff --git a/crates/g3-cli/src/machine_ui_writer.rs b/crates/g3-cli/src/machine_ui_writer.rs new file mode 100644 index 0000000..bc4e61b --- /dev/null +++ b/crates/g3-cli/src/machine_ui_writer.rs @@ -0,0 +1,93 @@ +use g3_core::ui_writer::UiWriter; +use std::io::{self, Write}; + +/// Machine-mode implementation of UiWriter that prints plain, unformatted output +/// This is designed for programmatic consumption and outputs everything verbatim +pub struct MachineUiWriter; + +impl MachineUiWriter { + pub fn new() -> Self { + Self + } +} + +impl UiWriter for MachineUiWriter { + fn print(&self, message: &str) { + print!("{}", 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!("{}", prompt); + println!("END_SYSTEM_PROMPT"); + println!(); + } + + fn print_context_status(&self, message: &str) { + println!("CONTEXT_STATUS: {}", message); + } + + fn print_context_thinning(&self, message: &str) { + println!("CONTEXT_THINNING: {}", message); + } + + fn print_tool_header(&self, tool_name: &str) { + println!("TOOL_CALL: {}", tool_name); + } + + fn print_tool_arg(&self, key: &str, value: &str) { + println!("TOOL_ARG: {} = {}", key, value); + } + + fn print_tool_output_header(&self) { + println!("TOOL_OUTPUT:"); + } + + fn update_tool_output_line(&self, line: &str) { + println!("{}", line); + } + + fn print_tool_output_line(&self, line: &str) { + println!("{}", line); + } + + fn print_tool_output_summary(&self, count: usize) { + println!("TOOL_OUTPUT_LINES: {}", count); + } + + fn print_tool_timing(&self, duration_str: &str) { + println!("TOOL_DURATION: {}", duration_str); + println!("END_TOOL_OUTPUT"); + println!(); + } + + fn print_agent_prompt(&self) { + let _ = io::stdout().flush(); + } + + fn print_agent_response(&self, content: &str) { + print!("{}", content); + let _ = io::stdout().flush(); + } + + fn notify_sse_received(&self) { + // No-op for machine mode + } + + fn flush(&self) { + let _ = io::stdout().flush(); + } + + fn wants_full_output(&self) -> bool { + true // Machine mode wants complete, untruncated output + } +} diff --git a/crates/g3-cli/src/simple_output.rs b/crates/g3-cli/src/simple_output.rs new file mode 100644 index 0000000..456da9e --- /dev/null +++ b/crates/g3-cli/src/simple_output.rs @@ -0,0 +1,32 @@ +/// Simple output helper for printing messages +pub struct SimpleOutput { + machine_mode: bool, +} + +impl SimpleOutput { + pub fn new() -> Self { + SimpleOutput { machine_mode: false } + } + + pub fn new_with_mode(machine_mode: bool) -> Self { + SimpleOutput { machine_mode } + } + + pub fn print(&self, message: &str) { + if !self.machine_mode { + println!("{}", message); + } + } + + pub fn print_smart(&self, message: &str) { + if !self.machine_mode { + println!("{}", message); + } + } +} + +impl Default for SimpleOutput { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/g3-cli/src/ui_writer_impl.rs b/crates/g3-cli/src/ui_writer_impl.rs index ec1a203..2f336fd 100644 --- a/crates/g3-cli/src/ui_writer_impl.rs +++ b/crates/g3-cli/src/ui_writer_impl.rs @@ -1,8 +1,6 @@ -use crate::retro_tui::RetroTui; use g3_core::ui_writer::UiWriter; use std::io::{self, Write}; use std::sync::Mutex; -use std::time::Instant; /// Console implementation of UiWriter that prints to stdout pub struct ConsoleUiWriter { @@ -347,241 +345,3 @@ impl UiWriter for ConsoleUiWriter { } } -/// RetroTui implementation of UiWriter that sends output to the TUI -pub struct RetroTuiWriter { - tui: RetroTui, - current_tool_name: Mutex>, - current_tool_output: Mutex>, - current_tool_start: Mutex>, - current_tool_caption: Mutex, -} - -impl RetroTuiWriter { - pub fn new(tui: RetroTui) -> Self { - Self { - tui, - current_tool_name: Mutex::new(None), - current_tool_output: Mutex::new(Vec::new()), - current_tool_start: Mutex::new(None), - current_tool_caption: Mutex::new(String::new()), - } - } -} - -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_context_thinning(&self, message: &str) { - // For TUI, we'll use a highlighted output with special formatting - // The TUI will handle the visual presentation - - // Add visual separators and emphasis - self.tui.output(""); - self.tui.output("═══════════════════════════════════════════════════════════"); - self.tui.output(&format!("✨ {} ✨", message)); - self.tui.output(" └─ Context optimized successfully"); - self.tui.output("═══════════════════════════════════════════════════════════"); - self.tui.output(""); - } - - fn print_tool_header(&self, tool_name: &str) { - // Start collecting tool output - *self.current_tool_start.lock().unwrap() = Some(Instant::now()); - *self.current_tool_name.lock().unwrap() = Some(tool_name.to_string()); - self.current_tool_output.lock().unwrap().clear(); - self.current_tool_output - .lock() - .unwrap() - .push(format!("Tool: {}", tool_name)); - - // Initialize caption - *self.current_tool_caption.lock().unwrap() = String::new(); - } - - fn print_tool_arg(&self, key: &str, value: &str) { - // Filter out any keys that look like they might be agent message content - // (e.g., keys that are suspiciously long or contain message-like content) - let is_valid_arg_key = key.len() < 50 - && !key.contains('\n') - && !key.contains("I'll") - && !key.contains("Let me") - && !key.contains("Here's") - && !key.contains("I can"); - - if is_valid_arg_key { - self.current_tool_output - .lock() - .unwrap() - .push(format!("{}: {}", key, value)); - } - - // Build caption from first argument (usually the most important one) - let mut caption = self.current_tool_caption.lock().unwrap(); - if caption.is_empty() && (key == "file_path" || key == "command" || key == "path") { - // Truncate long values for the caption - let truncated = if value.len() > 50 { - // Use char_indices to safely truncate at character boundary - let truncate_at = value.char_indices() - .nth(47) - .map(|(i, _)| i) - .unwrap_or(value.len()); - format!("{}...", &value[..truncate_at]) - } else { - value.to_string() - }; - - // Add range information for read_file tool calls - let tool_name = self.current_tool_name.lock().unwrap(); - let range_suffix = if tool_name.as_ref().is_some_and(|name| name == "read_file") { - // We need to check if start/end args will be provided - for now just check if this is a partial read - // This is a simplified approach since we're building the caption incrementally - String::new() // We'll handle this in print_tool_output_header instead - } else { - String::new() - }; - - *caption = format!("{}{}", truncated, range_suffix); - } - } - - fn print_tool_output_header(&self) { - // This is called right before tool execution starts - // Send the initial tool header to the TUI now - if let Some(tool_name) = self.current_tool_name.lock().unwrap().as_ref() { - let mut caption = self.current_tool_caption.lock().unwrap().clone(); - - // Add range information for read_file tool calls - if tool_name == "read_file" { - // Check the tool output for start/end parameters - let output = self.current_tool_output.lock().unwrap(); - let has_start = output.iter().any(|line| line.starts_with("start:")); - let has_end = output.iter().any(|line| line.starts_with("end:")); - - if has_start || has_end { - let start_val = output.iter().find(|line| line.starts_with("start:")).map(|line| line.split(':').nth(1).unwrap_or("0").trim()).unwrap_or("0"); - let end_val = output.iter().find(|line| line.starts_with("end:")).map(|line| line.split(':').nth(1).unwrap_or("end").trim()).unwrap_or("end"); - caption = format!("{} [{}..{}]", caption, start_val, end_val); - } - } - - // Send the tool output with initial header - self.tui.tool_output(tool_name, &caption, ""); - } - - self.current_tool_output.lock().unwrap().push(String::new()); - self.current_tool_output - .lock() - .unwrap() - .push("Output:".to_string()); - } - - fn update_tool_output_line(&self, line: &str) { - // For retro mode, we'll just add to the output buffer - self.current_tool_output - .lock() - .unwrap() - .push(line.to_string()); - } - - fn print_tool_output_line(&self, line: &str) { - self.current_tool_output - .lock() - .unwrap() - .push(line.to_string()); - } - - fn print_tool_output_summary(&self, hidden_count: usize) { - self.current_tool_output.lock().unwrap().push(format!( - "... ({} more line{})", - hidden_count, - if hidden_count == 1 { "" } else { "s" } - )); - } - - fn print_tool_timing(&self, duration_str: &str) { - self.current_tool_output - .lock() - .unwrap() - .push(format!("āš”ļø {}", duration_str)); - - // Calculate the actual duration - let duration_ms = if let Some(start) = *self.current_tool_start.lock().unwrap() { - start.elapsed().as_millis() - } else { - 0 - }; - - // Get the tool name and caption - if let Some(tool_name) = self.current_tool_name.lock().unwrap().as_ref() { - let content = self.current_tool_output.lock().unwrap().join("\n"); - let caption = self.current_tool_caption.lock().unwrap().clone(); - let caption = if caption.is_empty() { - "Completed".to_string() - } else { - caption - }; - - // Update the tool detail panel with the complete output without adding a new header - // This keeps the original header in place to be updated by tool_complete - self.tui.update_tool_detail(tool_name, &content); - - // Determine success based on whether there's an error in the output - // This is a simple heuristic - you might want to make this more sophisticated - let success = !content.contains("error") - && !content.contains("Error") - && !content.contains("ERROR"); - - // Send the completion status to update the header - self.tui - .tool_complete(tool_name, success, duration_ms, &caption); - } - - // Clear the buffers - *self.current_tool_name.lock().unwrap() = None; - self.current_tool_output.lock().unwrap().clear(); - *self.current_tool_start.lock().unwrap() = None; - *self.current_tool_caption.lock().unwrap() = String::new(); - } - - fn print_agent_prompt(&self) { - self.tui.output("\nšŸ’¬ "); - } - - fn print_agent_response(&self, content: &str) { - self.tui.output(content); - } - - fn notify_sse_received(&self) { - // Notify the TUI that an SSE was received - self.tui.sse_received(); - } - - fn flush(&self) { - // No-op for TUI since it handles its own rendering - } -} diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index b32dce9..6b3d991 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -2677,12 +2677,19 @@ Template: if tool_call.tool != "final_output" { let output_lines: Vec<&str> = tool_result.lines().collect(); + // Check if UI wants full output (machine mode) or truncated (human mode) + let wants_full = self.ui_writer.wants_full_output(); + // Helper function to safely truncate strings at character boundaries - let truncate_line = |line: &str, max_width: usize| -> String { - let char_count = line.chars().count(); - if char_count <= max_width { + let truncate_line = |line: &str, max_width: usize, truncate: bool| -> String { + if !truncate { + // Machine mode - return full line + line.to_string() + } else if line.chars().count() <= max_width { + // Human mode - line fits within limit line.to_string() } else { + // Human mode - truncate long line let truncated: String = line .chars() .take(max_width.saturating_sub(3)) @@ -2697,18 +2704,18 @@ Template: // For todo tools, show all lines without truncation let is_todo_tool = tool_call.tool == "todo_read" || tool_call.tool == "todo_write"; - let max_lines_to_show = if is_todo_tool { output_len } else { MAX_LINES }; + let max_lines_to_show = if is_todo_tool || wants_full { output_len } else { MAX_LINES }; for (idx, line) in output_lines.iter().enumerate() { - if !is_todo_tool && idx >= max_lines_to_show { + if !is_todo_tool && !wants_full && idx >= max_lines_to_show { break; } // Clip line to max width - let clipped_line = truncate_line(line, MAX_LINE_WIDTH); + let clipped_line = truncate_line(line, MAX_LINE_WIDTH, !wants_full); self.ui_writer.update_tool_output_line(&clipped_line); } - if !is_todo_tool && output_len > MAX_LINES { + if !is_todo_tool && !wants_full && output_len > MAX_LINES { self.ui_writer.print_tool_output_summary(output_len); } } diff --git a/crates/g3-core/src/ui_writer.rs b/crates/g3-core/src/ui_writer.rs index b907ea6..49e29b9 100644 --- a/crates/g3-core/src/ui_writer.rs +++ b/crates/g3-core/src/ui_writer.rs @@ -52,6 +52,10 @@ pub trait UiWriter: Send + Sync { /// Flush any buffered output fn flush(&self); + + /// Returns true if this UI writer wants full, untruncated output + /// Default is false (truncate for human readability) + fn wants_full_output(&self) -> bool { false } } /// A no-op implementation for when UI output is not needed @@ -75,4 +79,5 @@ impl UiWriter for NullUiWriter { fn print_agent_response(&self, _content: &str) {} fn notify_sse_received(&self) {} fn flush(&self) {} + fn wants_full_output(&self) -> bool { false } } \ No newline at end of file