diff --git a/Cargo.lock b/Cargo.lock index a9b6e2b..6c5b728 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width", + "unicode-width 0.2.0", "windows-sys 0.59.0", ] @@ -475,6 +475,24 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "coolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3" +dependencies = [ + "crossterm", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -510,6 +528,115 @@ dependencies = [ "libc", ] +[[package]] +name = "crokey" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51360853ebbeb3df20c76c82aecf43d387a62860f1a59ba65ab51f00eea85aad" +dependencies = [ + "crokey-proc_macros", + "crossterm", + "once_cell", + "serde", + "strict", +] + +[[package]] +name = "crokey-proc_macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf1a727caeb5ee5e0a0826a97f205a9cf84ee964b0b48239fef5214a00ae439" +dependencies = [ + "crossterm", + "proc-macro2", + "quote", + "strict", + "syn", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.9.4", + "crossterm_winapi", + "derive_more 2.0.1", + "document-features", + "mio", + "parking_lot", + "rustix 1.0.8", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -539,6 +666,27 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case 0.7.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -611,6 +759,15 @@ dependencies = [ "const-random", ] +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "either" version = "1.15.0" @@ -818,6 +975,7 @@ dependencies = [ "anyhow", "chrono", "clap", + "crossterm", "dirs 5.0.1", "g3-config", "g3-core", @@ -825,6 +983,7 @@ dependencies = [ "rustyline", "serde", "serde_json", + "termimad", "tokio", "tokio-util", "tracing", @@ -1310,7 +1469,7 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.2.0", "web-time", ] @@ -1405,6 +1564,29 @@ dependencies = [ "serde", ] +[[package]] +name = "lazy-regex" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1470,13 +1652,19 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + [[package]] name = "llama_cpp" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f126770a2ed5e0e4596119479dc56f56b99037246bf0e36c544f7581a9458fd" dependencies = [ - "derive_more", + "derive_more 0.99.20", "futures", "llama_cpp_sys", "num_cpus", @@ -1540,6 +1728,15 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimad" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2" +dependencies = [ + "once_cell", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1562,6 +1759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -2167,7 +2365,7 @@ dependencies = [ "nix", "radix_trie", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.0", "utf8parse", "windows-sys 0.60.2", ] @@ -2340,6 +2538,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -2387,6 +2606,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strict" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" + [[package]] name = "strsim" version = "0.11.1" @@ -2461,6 +2686,22 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "termimad" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ff5ca043d65d4ea43b65cdb4e3aba119657d0d12caf44f93212ec3168a8e20" +dependencies = [ + "coolor", + "crokey", + "crossbeam", + "lazy-regex", + "minimad", + "serde", + "thiserror 2.0.16", + "unicode-width 0.1.14", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2757,9 +2998,15 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "url" @@ -2981,6 +3228,22 @@ dependencies = [ "rustix 0.38.44", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2990,6 +3253,12 @@ dependencies = [ "windows-sys 0.61.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.1" diff --git a/crates/g3-cli/Cargo.toml b/crates/g3-cli/Cargo.toml index 87c479f..9c15e3e 100644 --- a/crates/g3-cli/Cargo.toml +++ b/crates/g3-cli/Cargo.toml @@ -19,3 +19,5 @@ dirs = "5.0" tokio-util = "0.7" indicatif = "0.17" chrono = { version = "0.4", features = ["serde"] } +crossterm = "0.29.0" +termimad = "0.34.0" diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index 5aefade..6f1223b 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -8,6 +8,9 @@ use std::path::PathBuf; use tokio_util::sync::CancellationToken; use tracing::{error, info}; +mod tui; +use tui::SimpleOutput; + #[derive(Parser)] #[command(name = "g3")] #[command(about = "A modular, composable AI coding agent")] @@ -122,14 +125,16 @@ pub async fn run() -> Result<()> { } else if let Some(task) = cli.task { // Single-shot mode 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?; - println!("{}", result); + output.print_markdown(&result); } else { + let output = SimpleOutput::new(); // Interactive mode (default) info!("Starting interactive mode"); - println!("šŸ“ Workspace: {}", project.workspace().display()); + output.print(&format!("šŸ“ Workspace: {}", project.workspace().display())); run_interactive(agent, cli.show_prompt, cli.show_code).await?; } @@ -137,29 +142,30 @@ pub async fn run() -> Result<()> { } async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -> Result<()> { + let output = SimpleOutput::new(); - println!(); - println!("šŸ¤– G3 AI Coding Agent - Interactive Mode"); - println!( + output.print(""); + output.print("šŸ¤– G3 AI Coding Agent - Interactive Mode"); + output.print( "I solve problems by writing and executing code. Tell me what you need to accomplish!" ); - println!(); + output.print(""); // Display provider and model information match agent.get_provider_info() { Ok((provider, model)) => { - println!("šŸ”§ Provider: {} | Model: {}", provider, model); + output.print(&format!("šŸ”§ Provider: {} | Model: {}", provider, model)); } Err(e) => { error!("Failed to get provider info: {}", e); } } - println!(); - println!("Type 'exit' or 'quit' to exit, use Up/Down arrows for command history"); - println!("For multiline input: use \\ at the end of a line to continue"); - println!("Submit multiline with Enter (without backslash)"); - println!(); + output.print(""); + output.print("Type 'exit' or 'quit' to exit, use Up/Down arrows for command history"); + output.print("For multiline input: use \\ at the end of a line to continue"); + output.print("Submit multiline with Enter (without backslash)"); + output.print(""); // Initialize rustyline editor with history let mut rl = DefaultEditor::new()?; @@ -180,7 +186,7 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) - loop { // Display context window progress bar before each prompt - display_context_progress(&agent); + display_context_progress(&agent, &output); // Adjust prompt based on whether we're in multi-line mode let prompt = if in_multiline { "... > " } else { "g3> " }; @@ -220,7 +226,7 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) - } // Process the multiline input - execute_task(&mut agent, &input, show_prompt, show_code).await; + execute_task(&mut agent, &input, show_prompt, show_code, &output).await; } else { // Single line input let input = line.trim().to_string(); @@ -237,23 +243,23 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) - rl.add_history_entry(&input)?; // Process the single line input - execute_task(&mut agent, &input, show_prompt, show_code).await; + execute_task(&mut agent, &input, show_prompt, show_code, &output).await; } } Err(ReadlineError::Interrupted) => { // Ctrl-C pressed if in_multiline { // Cancel multiline input - println!("Multi-line input cancelled"); + output.print("Multi-line input cancelled"); multiline_buffer.clear(); in_multiline = false; } else { - println!("CTRL-C"); + output.print("CTRL-C"); } continue; } Err(ReadlineError::Eof) => { - println!("CTRL-D"); + output.print("CTRL-D"); break; } Err(err) => { @@ -268,14 +274,14 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) - let _ = rl.save_history(history_path); } - println!("šŸ‘‹ Goodbye!"); + output.print("šŸ‘‹ Goodbye!"); Ok(()) } -async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_code: bool) { +async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_code: bool, output: &SimpleOutput) { // Show thinking indicator immediately - print!("šŸ¤” Thinking..."); - std::io::stdout().flush().unwrap(); + output.print("šŸ¤” Thinking..."); + // Note: flush is handled internally by println // Create cancellation token for this request let cancellation_token = CancellationToken::new(); @@ -290,16 +296,16 @@ async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_co } _ = tokio::signal::ctrl_c() => { cancel_token_clone.cancel(); - println!("\nāš ļø Operation cancelled by user (Ctrl+C)"); + output.print("\nāš ļø Operation cancelled by user (Ctrl+C)"); return; } }; match execution_result { - Ok(response) => println!("{}", response), + Ok(response) => output.print_markdown(&response), Err(e) => { if e.to_string().contains("cancelled") { - println!("āš ļø Operation cancelled by user"); + output.print("āš ļø Operation cancelled by user"); } else { error!("Error: {}", e); } @@ -307,24 +313,9 @@ async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_co } } -fn display_context_progress(agent: &Agent) { +fn display_context_progress(agent: &Agent, output: &SimpleOutput) { let context = agent.get_context_window(); - let percentage = context.percentage_used(); - - // Create a simple visual progress bar using the requested characters (10 dots max) - let bar_width = 10; - let filled_width = ((percentage / 100.0) * bar_width as f32) as usize; - let empty_width = bar_width - filled_width; - - let filled_chars = "ā—".repeat(filled_width); - let empty_chars = "ā—‹".repeat(empty_width); - let progress_bar = format!("{}{}", filled_chars, empty_chars); - - // Print context info with visual progress bar - println!( - "Context: {} {:.1}% | {}/{} tokens", - progress_bar, percentage, context.used_tokens, context.total_tokens - ); + output.print_context(context.used_tokens, context.total_tokens, context.percentage_used()); } /// Set up the workspace directory for autonomous mode @@ -342,10 +333,11 @@ fn setup_workspace_directory() -> Result { // Create the directory if it doesn't exist if !workspace_dir.exists() { std::fs::create_dir_all(&workspace_dir)?; - println!( + let output = SimpleOutput::new(); + output.print(&format!( "šŸ“ Created workspace directory: {}", workspace_dir.display() - ); + )); } Ok(workspace_dir) @@ -359,14 +351,16 @@ async fn run_autonomous( show_code: bool, max_turns: usize, ) -> Result<()> { - println!("šŸ¤– G3 AI Coding Agent - Autonomous Mode"); - println!("šŸ“ Using workspace: {}", project.workspace().display()); + let output = SimpleOutput::new(); + + output.print("šŸ¤– G3 AI Coding Agent - Autonomous Mode"); + output.print(&format!("šŸ“ Using workspace: {}", project.workspace().display())); // Check if requirements exist if !project.has_requirements() { - println!("āŒ Error: requirements.md not found in workspace directory"); - println!(" Please create a requirements.md file with your project requirements at:"); - println!(" {}/requirements.md", project.workspace().display()); + output.print("āŒ Error: requirements.md not found in workspace directory"); + output.print(" Please create a requirements.md file with your project requirements at:"); + output.print(&format!(" {}/requirements.md", project.workspace().display())); return Ok(()); } @@ -374,20 +368,20 @@ async fn run_autonomous( let requirements = match project.read_requirements()? { Some(content) => content, None => { - println!("āŒ Error: Could not read requirements.md"); + output.print("āŒ Error: Could not read requirements.md"); return Ok(()); } }; - println!("šŸ“‹ Requirements loaded from requirements.md"); - println!("šŸ”„ Starting coach-player feedback loop..."); + output.print("šŸ“‹ Requirements loaded from requirements.md"); + output.print("šŸ”„ Starting coach-player feedback loop..."); let mut turn = 1; let mut coach_feedback = String::new(); let mut implementation_approved = false; loop { - println!("\n=== TURN {}/{} - PLAYER MODE ===", turn, max_turns); + output.print(&format!("\n=== TURN {}/{} - PLAYER MODE ===", turn, max_turns)); // Player mode: implement requirements (with coach feedback if available) let player_prompt = if coach_feedback.is_empty() { @@ -402,13 +396,13 @@ async fn run_autonomous( ) }; - println!("šŸŽÆ Starting player implementation..."); + output.print("šŸŽÆ Starting player implementation..."); let player_result = agent .execute_task_with_timing(&player_prompt, None, false, show_prompt, show_code, true) .await; if let Err(e) = player_result { - println!("āŒ Player implementation failed: {}", e); + output.print(&format!("āŒ Player implementation failed: {}", e)); } // Create a new agent instance for coach mode to ensure fresh context @@ -418,7 +412,7 @@ async fn run_autonomous( // Ensure coach agent is also in the workspace directory project.enter_workspace()?; - println!("\n=== TURN {}/{} - COACH MODE ===", turn, max_turns); + output.print(&format!("\n=== TURN {}/{} - COACH MODE ===", turn, max_turns)); // Coach mode: critique the implementation let coach_prompt = format!( @@ -442,26 +436,26 @@ Keep your response concise and focused on actionable items.", requirements ); - println!("šŸŽ“ Starting coach review..."); + output.print("šŸŽ“ Starting coach review..."); let coach_result = coach_agent .execute_task_with_timing(&coach_prompt, None, false, show_prompt, show_code, true) .await?; - println!("šŸŽ“ Coach review completed"); - println!("Coach feedback: {}", coach_result); + output.print("šŸŽ“ Coach review completed"); + output.print(&format!("Coach feedback: {}", coach_result)); // Check if coach approved the implementation if coach_result.contains("IMPLEMENTATION_APPROVED") { - println!("\n=== SESSION COMPLETED - IMPLEMENTATION APPROVED ==="); - println!("āœ… Coach approved the implementation!"); + output.print("\n=== SESSION COMPLETED - IMPLEMENTATION APPROVED ==="); + output.print("āœ… Coach approved the implementation!"); implementation_approved = true; break; } // Check if we've reached max turns if turn >= max_turns { - println!("\n=== SESSION COMPLETED - MAX TURNS REACHED ==="); - println!("ā° Maximum turns ({}) reached", max_turns); + output.print("\n=== SESSION COMPLETED - MAX TURNS REACHED ==="); + output.print(&format!("ā° Maximum turns ({}) reached", max_turns)); break; } @@ -469,13 +463,13 @@ Keep your response concise and focused on actionable items.", coach_feedback = coach_result; turn += 1; - println!("šŸ”„ Coach provided feedback for next iteration"); + output.print("šŸ”„ Coach provided feedback for next iteration"); } if implementation_approved { - println!("\nšŸŽ‰ Autonomous mode completed successfully"); + output.print("\nšŸŽ‰ Autonomous mode completed successfully"); } else { - println!("\nšŸ”„ Autonomous mode completed (max iterations)"); + output.print("\nšŸ”„ Autonomous mode completed (max iterations)"); } Ok(()) diff --git a/crates/g3-cli/src/tui.rs b/crates/g3-cli/src/tui.rs new file mode 100644 index 0000000..08e1ce2 --- /dev/null +++ b/crates/g3-cli/src/tui.rs @@ -0,0 +1,47 @@ +use crossterm::style::Color; +use termimad::MadSkin; + +/// Simple output handler with markdown support +pub struct SimpleOutput { + mad_skin: MadSkin, +} + +impl SimpleOutput { + pub fn new() -> Self { + let mut mad_skin = MadSkin::default(); + // Configure termimad skin for better markdown rendering + mad_skin.set_headers_fg(Color::Cyan); + mad_skin.bold.set_fg(Color::Yellow); + mad_skin.italic.set_fg(Color::Magenta); + mad_skin.code_block.set_bg(Color::Rgb { r: 40, g: 40, b: 40 }); + + Self { mad_skin } + } + + pub fn print(&self, text: &str) { + println!("{}", text); + } + + pub fn print_markdown(&self, markdown: &str) { + self.mad_skin.print_text(markdown); + } + + pub fn print_status(&self, status: &str) { + println!("šŸ“Š {}", status); + } + + pub fn print_context(&self, used: u32, total: u32, percentage: f32) { + let bar_width = 10; + let filled_width = ((percentage / 100.0) * bar_width as f32) as usize; + let empty_width = bar_width - filled_width; + + let filled_chars = "ā—".repeat(filled_width); + let empty_chars = "ā—‹".repeat(empty_width); + let progress_bar = format!("{}{}", filled_chars, empty_chars); + + println!( + "Context: {} {:.1}% | {}/{} tokens", + progress_bar, percentage, used, total + ); + } +}