tui lib for nicer cli

This commit is contained in:
Dhanji Prasanna
2025-10-01 11:19:34 +10:00
parent 5f642061de
commit 1621d081ec
4 changed files with 384 additions and 72 deletions

281
Cargo.lock generated
View File

@@ -436,7 +436,7 @@ dependencies = [
"encode_unicode", "encode_unicode",
"libc", "libc",
"once_cell", "once_cell",
"unicode-width", "unicode-width 0.2.0",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -475,6 +475,24 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -510,6 +528,115 @@ dependencies = [
"libc", "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]] [[package]]
name = "crunchy" name = "crunchy"
version = "0.2.4" version = "0.2.4"
@@ -539,6 +666,27 @@ dependencies = [
"syn", "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]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@@ -611,6 +759,15 @@ dependencies = [
"const-random", "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]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.15.0"
@@ -818,6 +975,7 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"clap", "clap",
"crossterm",
"dirs 5.0.1", "dirs 5.0.1",
"g3-config", "g3-config",
"g3-core", "g3-core",
@@ -825,6 +983,7 @@ dependencies = [
"rustyline", "rustyline",
"serde", "serde",
"serde_json", "serde_json",
"termimad",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tracing", "tracing",
@@ -1310,7 +1469,7 @@ dependencies = [
"console", "console",
"number_prefix", "number_prefix",
"portable-atomic", "portable-atomic",
"unicode-width", "unicode-width 0.2.0",
"web-time", "web-time",
] ]
@@ -1405,6 +1564,29 @@ dependencies = [
"serde", "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]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -1470,13 +1652,19 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "litrs"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
[[package]] [[package]]
name = "llama_cpp" name = "llama_cpp"
version = "0.3.2" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f126770a2ed5e0e4596119479dc56f56b99037246bf0e36c544f7581a9458fd" checksum = "7f126770a2ed5e0e4596119479dc56f56b99037246bf0e36c544f7581a9458fd"
dependencies = [ dependencies = [
"derive_more", "derive_more 0.99.20",
"futures", "futures",
"llama_cpp_sys", "llama_cpp_sys",
"num_cpus", "num_cpus",
@@ -1540,6 +1728,15 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 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]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"
@@ -1562,6 +1759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [ dependencies = [
"libc", "libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1", "wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -2167,7 +2365,7 @@ dependencies = [
"nix", "nix",
"radix_trie", "radix_trie",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width 0.2.0",
"utf8parse", "utf8parse",
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
@@ -2340,6 +2538,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 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]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.6" version = "1.4.6"
@@ -2387,6 +2606,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strict"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@@ -2461,6 +2686,22 @@ dependencies = [
"windows-sys 0.60.2", "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]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@@ -2757,9 +2998,15 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.1" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "url" name = "url"
@@ -2981,6 +3228,22 @@ dependencies = [
"rustix 0.38.44", "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]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.11" version = "0.1.11"
@@ -2990,6 +3253,12 @@ dependencies = [
"windows-sys 0.61.0", "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]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.62.1" version = "0.62.1"

View File

@@ -19,3 +19,5 @@ dirs = "5.0"
tokio-util = "0.7" tokio-util = "0.7"
indicatif = "0.17" indicatif = "0.17"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
crossterm = "0.29.0"
termimad = "0.34.0"

View File

@@ -8,6 +8,9 @@ use std::path::PathBuf;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{error, info}; use tracing::{error, info};
mod tui;
use tui::SimpleOutput;
#[derive(Parser)] #[derive(Parser)]
#[command(name = "g3")] #[command(name = "g3")]
#[command(about = "A modular, composable AI coding agent")] #[command(about = "A modular, composable AI coding agent")]
@@ -122,14 +125,16 @@ pub async fn run() -> Result<()> {
} else if let Some(task) = cli.task { } else if let Some(task) = cli.task {
// Single-shot mode // Single-shot mode
info!("Executing task: {}", task); info!("Executing task: {}", task);
let output = SimpleOutput::new();
let result = agent let result = agent
.execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true) .execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true)
.await?; .await?;
println!("{}", result); output.print_markdown(&result);
} else { } else {
let output = SimpleOutput::new();
// Interactive mode (default) // Interactive mode (default)
info!("Starting interactive mode"); 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?; 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<()> { async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -> Result<()> {
let output = SimpleOutput::new();
println!(); output.print("");
println!("🤖 G3 AI Coding Agent - Interactive Mode"); output.print("🤖 G3 AI Coding Agent - Interactive Mode");
println!( output.print(
"I solve problems by writing and executing code. Tell me what you need to accomplish!" "I solve problems by writing and executing code. Tell me what you need to accomplish!"
); );
println!(); output.print("");
// Display provider and model information // Display provider and model information
match agent.get_provider_info() { match agent.get_provider_info() {
Ok((provider, model)) => { Ok((provider, model)) => {
println!("🔧 Provider: {} | Model: {}", provider, model); output.print(&format!("🔧 Provider: {} | Model: {}", provider, model));
} }
Err(e) => { Err(e) => {
error!("Failed to get provider info: {}", e); error!("Failed to get provider info: {}", e);
} }
} }
println!(); output.print("");
println!("Type 'exit' or 'quit' to exit, use Up/Down arrows for command history"); output.print("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"); output.print("For multiline input: use \\ at the end of a line to continue");
println!("Submit multiline with Enter (without backslash)"); output.print("Submit multiline with Enter (without backslash)");
println!(); output.print("");
// Initialize rustyline editor with history // Initialize rustyline editor with history
let mut rl = DefaultEditor::new()?; let mut rl = DefaultEditor::new()?;
@@ -180,7 +186,7 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -
loop { loop {
// Display context window progress bar before each prompt // 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 // Adjust prompt based on whether we're in multi-line mode
let prompt = if in_multiline { "... > " } else { "g3> " }; 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 // 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 { } else {
// Single line input // Single line input
let input = line.trim().to_string(); 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)?; rl.add_history_entry(&input)?;
// Process the single line 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) => { Err(ReadlineError::Interrupted) => {
// Ctrl-C pressed // Ctrl-C pressed
if in_multiline { if in_multiline {
// Cancel multiline input // Cancel multiline input
println!("Multi-line input cancelled"); output.print("Multi-line input cancelled");
multiline_buffer.clear(); multiline_buffer.clear();
in_multiline = false; in_multiline = false;
} else { } else {
println!("CTRL-C"); output.print("CTRL-C");
} }
continue; continue;
} }
Err(ReadlineError::Eof) => { Err(ReadlineError::Eof) => {
println!("CTRL-D"); output.print("CTRL-D");
break; break;
} }
Err(err) => { 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); let _ = rl.save_history(history_path);
} }
println!("👋 Goodbye!"); output.print("👋 Goodbye!");
Ok(()) 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 // Show thinking indicator immediately
print!("🤔 Thinking..."); output.print("🤔 Thinking...");
std::io::stdout().flush().unwrap(); // Note: flush is handled internally by println
// Create cancellation token for this request // Create cancellation token for this request
let cancellation_token = CancellationToken::new(); 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() => { _ = tokio::signal::ctrl_c() => {
cancel_token_clone.cancel(); cancel_token_clone.cancel();
println!("\n⚠️ Operation cancelled by user (Ctrl+C)"); output.print("\n⚠️ Operation cancelled by user (Ctrl+C)");
return; return;
} }
}; };
match execution_result { match execution_result {
Ok(response) => println!("{}", response), Ok(response) => output.print_markdown(&response),
Err(e) => { Err(e) => {
if e.to_string().contains("cancelled") { if e.to_string().contains("cancelled") {
println!("⚠️ Operation cancelled by user"); output.print("⚠️ Operation cancelled by user");
} else { } else {
error!("Error: {}", e); 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 context = agent.get_context_window();
let percentage = context.percentage_used(); output.print_context(context.used_tokens, context.total_tokens, 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
);
} }
/// Set up the workspace directory for autonomous mode /// Set up the workspace directory for autonomous mode
@@ -342,10 +333,11 @@ fn setup_workspace_directory() -> Result<PathBuf> {
// Create the directory if it doesn't exist // Create the directory if it doesn't exist
if !workspace_dir.exists() { if !workspace_dir.exists() {
std::fs::create_dir_all(&workspace_dir)?; std::fs::create_dir_all(&workspace_dir)?;
println!( let output = SimpleOutput::new();
output.print(&format!(
"📁 Created workspace directory: {}", "📁 Created workspace directory: {}",
workspace_dir.display() workspace_dir.display()
); ));
} }
Ok(workspace_dir) Ok(workspace_dir)
@@ -359,14 +351,16 @@ async fn run_autonomous(
show_code: bool, show_code: bool,
max_turns: usize, max_turns: usize,
) -> Result<()> { ) -> Result<()> {
println!("🤖 G3 AI Coding Agent - Autonomous Mode"); let output = SimpleOutput::new();
println!("📁 Using workspace: {}", project.workspace().display());
output.print("🤖 G3 AI Coding Agent - Autonomous Mode");
output.print(&format!("📁 Using workspace: {}", project.workspace().display()));
// Check if requirements exist // Check if requirements exist
if !project.has_requirements() { if !project.has_requirements() {
println!("❌ Error: requirements.md not found in workspace directory"); output.print("❌ Error: requirements.md not found in workspace directory");
println!(" Please create a requirements.md file with your project requirements at:"); output.print(" Please create a requirements.md file with your project requirements at:");
println!(" {}/requirements.md", project.workspace().display()); output.print(&format!(" {}/requirements.md", project.workspace().display()));
return Ok(()); return Ok(());
} }
@@ -374,20 +368,20 @@ async fn run_autonomous(
let requirements = match project.read_requirements()? { let requirements = match project.read_requirements()? {
Some(content) => content, Some(content) => content,
None => { None => {
println!("❌ Error: Could not read requirements.md"); output.print("❌ Error: Could not read requirements.md");
return Ok(()); return Ok(());
} }
}; };
println!("📋 Requirements loaded from requirements.md"); output.print("📋 Requirements loaded from requirements.md");
println!("🔄 Starting coach-player feedback loop..."); output.print("🔄 Starting coach-player feedback loop...");
let mut turn = 1; let mut turn = 1;
let mut coach_feedback = String::new(); let mut coach_feedback = String::new();
let mut implementation_approved = false; let mut implementation_approved = false;
loop { 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) // Player mode: implement requirements (with coach feedback if available)
let player_prompt = if coach_feedback.is_empty() { 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 let player_result = agent
.execute_task_with_timing(&player_prompt, None, false, show_prompt, show_code, true) .execute_task_with_timing(&player_prompt, None, false, show_prompt, show_code, true)
.await; .await;
if let Err(e) = player_result { 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 // 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 // Ensure coach agent is also in the workspace directory
project.enter_workspace()?; 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 // Coach mode: critique the implementation
let coach_prompt = format!( let coach_prompt = format!(
@@ -442,26 +436,26 @@ Keep your response concise and focused on actionable items.",
requirements requirements
); );
println!("🎓 Starting coach review..."); output.print("🎓 Starting coach review...");
let coach_result = coach_agent let coach_result = coach_agent
.execute_task_with_timing(&coach_prompt, None, false, show_prompt, show_code, true) .execute_task_with_timing(&coach_prompt, None, false, show_prompt, show_code, true)
.await?; .await?;
println!("🎓 Coach review completed"); output.print("🎓 Coach review completed");
println!("Coach feedback: {}", coach_result); output.print(&format!("Coach feedback: {}", coach_result));
// Check if coach approved the implementation // Check if coach approved the implementation
if coach_result.contains("IMPLEMENTATION_APPROVED") { if coach_result.contains("IMPLEMENTATION_APPROVED") {
println!("\n=== SESSION COMPLETED - IMPLEMENTATION APPROVED ==="); output.print("\n=== SESSION COMPLETED - IMPLEMENTATION APPROVED ===");
println!("✅ Coach approved the implementation!"); output.print("✅ Coach approved the implementation!");
implementation_approved = true; implementation_approved = true;
break; break;
} }
// Check if we've reached max turns // Check if we've reached max turns
if turn >= max_turns { if turn >= max_turns {
println!("\n=== SESSION COMPLETED - MAX TURNS REACHED ==="); output.print("\n=== SESSION COMPLETED - MAX TURNS REACHED ===");
println!("⏰ Maximum turns ({}) reached", max_turns); output.print(&format!("⏰ Maximum turns ({}) reached", max_turns));
break; break;
} }
@@ -469,13 +463,13 @@ Keep your response concise and focused on actionable items.",
coach_feedback = coach_result; coach_feedback = coach_result;
turn += 1; turn += 1;
println!("🔄 Coach provided feedback for next iteration"); output.print("🔄 Coach provided feedback for next iteration");
} }
if implementation_approved { if implementation_approved {
println!("\n🎉 Autonomous mode completed successfully"); output.print("\n🎉 Autonomous mode completed successfully");
} else { } else {
println!("\n🔄 Autonomous mode completed (max iterations)"); output.print("\n🔄 Autonomous mode completed (max iterations)");
} }
Ok(()) Ok(())

47
crates/g3-cli/src/tui.rs Normal file
View File

@@ -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
);
}
}