From 426a9b88a9a9557a6b3a1774a6742e82d214d17b Mon Sep 17 00:00:00 2001 From: Dhanji Prasanna Date: Fri, 10 Oct 2025 14:08:37 +1100 Subject: [PATCH] readme tweaks --- crates/g3-cli/src/lib.rs | 71 +++++++++++- crates/g3-cli/src/ui_writer_impl.rs | 4 +- crates/g3-core/src/lib.rs | 167 +++++++++++++++++++--------- 3 files changed, 185 insertions(+), 57 deletions(-) diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index 270fd92..b5b843b 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -244,6 +244,55 @@ fn read_project_readme(workspace_dir: &Path) -> Option { None } +/// Extract the main heading or title from README content +fn extract_readme_heading(readme_content: &str) -> Option { + // Process the content line by line, skipping the prefix line if present + let lines_iter = readme_content.lines(); + let mut content_lines = Vec::new(); + + for line in lines_iter { + // Skip the "📚 Project README (from ...):" line + if line.starts_with("📚 Project README") { + continue; + } + content_lines.push(line); + } + let content = content_lines.join("\n"); + + // Look for the first markdown heading + for line in content.lines() { + let trimmed = line.trim(); + + // Check for H1 heading (# Title) + if trimmed.starts_with("# ") { + let title = trimmed[2..].trim(); + if !title.is_empty() { + // Return the full title (including any description after dash) + return Some(title.to_string()); + } + } + + // Skip other markdown headings for now (##, ###, etc.) + // We're only looking for the main H1 heading + } + + // If no H1 heading found, look for the first non-empty, non-metadata line as a fallback + for line in content.lines().take(5) { + let trimmed = line.trim(); + // Skip empty lines, other heading markers, and metadata + if !trimmed.is_empty() && !trimmed.starts_with("📚") && !trimmed.starts_with('#') + && !trimmed.starts_with("==") && !trimmed.starts_with("--") { + // Limit length for display + return Some(if trimmed.len() > 100 { + format!("{}...", &trimmed[..97]) + } else { + trimmed.to_string() + }); + } + } + None +} + async fn run_interactive_retro( config: Config, show_prompt: bool, @@ -278,7 +327,14 @@ async fn run_interactive_retro( // Display message if README was loaded if readme_content.is_some() { - tui.output("SYSTEM: PROJECT README LOADED INTO CONTEXT\n\n"); + // Extract the first heading or title from the README + let readme_snippet = if let Some(ref content) = readme_content { + extract_readme_heading(content) + .unwrap_or_else(|| "PROJECT DOCUMENTATION LOADED".to_string()) + } 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"); @@ -511,7 +567,18 @@ async fn run_interactive( // Display message if README was loaded if readme_content.is_some() { - output.print("📚 Project README loaded into context"); + // Extract the first heading or title from the README + let readme_snippet = if let Some(ref content) = readme_content { + extract_readme_heading(content) + .unwrap_or_else(|| "Project documentation loaded".to_string()) + } else { + "Project documentation loaded".to_string() + }; + + output.print(&format!("📚 Project README loaded: {}", readme_snippet)); + if readme_snippet.len() > 80 { + output.print(" (Full README available in context)"); + } output.print(""); } diff --git a/crates/g3-cli/src/ui_writer_impl.rs b/crates/g3-cli/src/ui_writer_impl.rs index 3438da6..78aca49 100644 --- a/crates/g3-cli/src/ui_writer_impl.rs +++ b/crates/g3-cli/src/ui_writer_impl.rs @@ -79,10 +79,10 @@ impl UiWriter for ConsoleUiWriter { value.clone() }; // Print with bold green formatting using ANSI escape codes - println!("\x1b[1;32m┌─ {} | {}\x1b[0m", tool_name, display_value); + println!("┌─\x1b[1;32m {} | {}\x1b[0m", tool_name, display_value); } else { // Print with bold green formatting using ANSI escape codes - println!("\x1b[1;32m┌─ {}\x1b[0m", tool_name); + println!("┌─\x1b[1;32m {}\x1b[0m", tool_name); } // Print any additional arguments (optional - can be removed if not wanted) diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index c1c745f..1c4b8ad 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -1,7 +1,7 @@ pub mod error_handling; pub mod project; -pub mod ui_writer; pub mod task_result; +pub mod ui_writer; pub use task_result::TaskResult; #[cfg(test)] @@ -231,7 +231,7 @@ impl StreamingToolParser { pub struct ContextWindow { pub used_tokens: u32, pub total_tokens: u32, - pub cumulative_tokens: u32, // Track cumulative tokens across all interactions + pub cumulative_tokens: u32, // Track cumulative tokens across all interactions pub conversation_history: Vec, } @@ -248,7 +248,7 @@ impl ContextWindow { pub fn add_message(&mut self, message: Message) { self.add_message_with_tokens(message, None); } - + /// Add a message with optional token count from the provider pub fn add_message_with_tokens(&mut self, message: Message, tokens: Option) { // Skip messages with empty content to avoid API errors @@ -262,7 +262,7 @@ impl ContextWindow { self.used_tokens += token_count; self.cumulative_tokens += token_count; self.conversation_history.push(message); - + debug!( "Added message with {} tokens (used: {}/{}, cumulative: {})", token_count, self.used_tokens, self.total_tokens, self.cumulative_tokens @@ -276,13 +276,13 @@ impl ContextWindow { let old_used = self.used_tokens; self.used_tokens = usage.total_tokens; self.cumulative_tokens = self.cumulative_tokens - old_used + usage.total_tokens; - + debug!( "Updated token usage from provider: {} -> {} (cumulative: {})", old_used, self.used_tokens, self.cumulative_tokens ); } - + /// More accurate token estimation fn estimate_tokens(text: &str) -> u32 { // Better heuristic: @@ -301,7 +301,7 @@ impl ContextWindow { // Deprecated: Use update_usage_from_response instead self.update_usage_from_response(usage); } - + /// Update cumulative token usage (for streaming) pub fn add_streaming_tokens(&mut self, new_tokens: u32) { self.used_tokens += new_tokens; @@ -388,11 +388,19 @@ impl Agent { Self::new_with_mode(config, ui_writer, false).await } - pub async fn new_with_readme(config: Config, ui_writer: W, readme_content: Option) -> Result { + pub async fn new_with_readme( + config: Config, + ui_writer: W, + readme_content: Option, + ) -> Result { Self::new_with_mode_and_readme(config, ui_writer, false, readme_content).await } - pub async fn new_autonomous_with_readme(config: Config, ui_writer: W, readme_content: Option) -> Result { + pub async fn new_autonomous_with_readme( + config: Config, + ui_writer: W, + readme_content: Option, + ) -> Result { Self::new_with_mode_and_readme(config, ui_writer, true, readme_content).await } @@ -404,7 +412,12 @@ impl Agent { Self::new_with_mode_and_readme(config, ui_writer, is_autonomous, None).await } - async fn new_with_mode_and_readme(config: Config, ui_writer: W, is_autonomous: bool, readme_content: Option) -> Result { + async fn new_with_mode_and_readme( + config: Config, + ui_writer: W, + is_autonomous: bool, + readme_content: Option, + ) -> Result { let mut providers = ProviderRegistry::new(); // Only register providers that are configured AND selected as the default provider @@ -917,7 +930,8 @@ The tool will execute immediately and you'll receive the result (success or erro request: CompletionRequest, show_timing: bool, ) -> Result { - self.stream_completion_with_tools(request, show_timing).await + self.stream_completion_with_tools(request, show_timing) + .await } /// Create tool definitions for native tool calling providers @@ -1301,7 +1315,7 @@ The tool will execute immediately and you'll receive the result (success or erro Ok(chunk) => { // Notify UI about SSE received (including pings) self.ui_writer.notify_sse_received(); - + // Capture usage data if available if let Some(ref usage) = chunk.usage { accumulated_usage = Some(usage.clone()); @@ -1310,7 +1324,7 @@ The tool will execute immediately and you'll receive the result (success or erro usage.prompt_tokens, usage.completion_tokens, usage.total_tokens ); } - + // Store raw chunk for debugging (limit to first 20 and last 5) if chunks_received < 20 || chunk.finished { raw_chunks.push(format!( @@ -1464,7 +1478,8 @@ The tool will execute immediately and you'll receive the result (success or erro // Don't add the "=> " prefix in autonomous mode // as it interferes with coach feedback parsing if !self.is_autonomous { - full_response.push_str(&format!("\n\n=> {}", summary_str)); + full_response + .push_str(&format!("\n\n=> {}", summary_str)); } else { full_response.push_str(&format!("\n\n{}", summary_str)); } @@ -1473,15 +1488,23 @@ The tool will execute immediately and you'll receive the result (success or erro self.ui_writer.println(""); let _ttft = first_token_time.unwrap_or_else(|| stream_start.elapsed()); - + // Add timing if needed let final_response = if show_timing { - format!("{}\n\n⏱️ {} | 💭 {}", full_response, Self::format_duration(total_execution_time), Self::format_duration(_ttft)) + format!( + "{}\n\n⏱️ {} | 💭 {}", + full_response, + Self::format_duration(total_execution_time), + Self::format_duration(_ttft) + ) } else { full_response }; - - return Ok(TaskResult::new(final_response, self.context_window.clone())); + + return Ok(TaskResult::new( + final_response, + self.context_window.clone(), + )); } // Closure marker with timing @@ -1576,14 +1599,16 @@ The tool will execute immediately and you'll receive the result (success or erro // We need to check the parser's text buffer as well, since the LLM // might have responded with text but no final_output tool call let text_content = parser.get_text_content(); - let has_text_response = !text_content.trim().is_empty() || !current_response.trim().is_empty(); - + let has_text_response = !text_content.trim().is_empty() + || !current_response.trim().is_empty(); + // If we have text in the parser buffer but not in current_response, // we should add it to the response if !text_content.trim().is_empty() && current_response.is_empty() { - current_response = filter_json_tool_calls(text_content).trim().to_string(); + current_response = + filter_json_tool_calls(text_content).trim().to_string(); } - + if !has_text_response && full_response.is_empty() { // Log detailed error information before failing error!( @@ -1688,15 +1713,23 @@ The tool will execute immediately and you'll receive the result (success or erro self.ui_writer.println(""); let _ttft = first_token_time.unwrap_or_else(|| stream_start.elapsed()); - + // Add timing if needed let final_response = if show_timing { - format!("{}\n\n⏱️ {} | 💭 {}", full_response, Self::format_duration(total_execution_time), Self::format_duration(_ttft)) + format!( + "{}\n\n⏱️ {} | 💭 {}", + full_response, + Self::format_duration(total_execution_time), + Self::format_duration(_ttft) + ) } else { full_response }; - - return Ok(TaskResult::new(final_response, self.context_window.clone())); + + return Ok(TaskResult::new( + final_response, + self.context_window.clone(), + )); } break; // Tool was executed, break to continue outer loop } @@ -1727,7 +1760,7 @@ The tool will execute immediately and you'll receive the result (success or erro } } } - + // Update context window with actual usage if available if let Some(usage) = accumulated_usage { debug!("Updating context window with actual usage from stream"); @@ -1748,9 +1781,9 @@ The tool will execute immediately and you'll receive the result (success or erro // Add the text to the response current_response = filter_json_tool_calls(text_content).trim().to_string(); } - + let has_response = !current_response.is_empty() || !full_response.is_empty(); - + if !has_response { warn!( "Loop exited without any response after {} iterations", @@ -1762,14 +1795,19 @@ The tool will execute immediately and you'll receive the result (success or erro } let _ttft = first_token_time.unwrap_or_else(|| stream_start.elapsed()); - + // Add timing if needed let final_response = if show_timing { - format!("{}\n\n⏱️ {} | 💭 {}", full_response, Self::format_duration(total_execution_time), Self::format_duration(_ttft)) + format!( + "{}\n\n⏱️ {} | 💭 {}", + full_response, + Self::format_duration(total_execution_time), + Self::format_duration(_ttft) + ) } else { full_response }; - + return Ok(TaskResult::new(final_response, self.context_window.clone())); } @@ -1778,14 +1816,19 @@ The tool will execute immediately and you'll receive the result (success or erro // If we exit the loop due to max iterations let _ttft = first_token_time.unwrap_or_else(|| stream_start.elapsed()); - + // Add timing if needed let final_response = if show_timing { - format!("{}\n\n⏱️ {} | 💭 {}", full_response, Self::format_duration(total_execution_time), Self::format_duration(_ttft)) + format!( + "{}\n\n⏱️ {} | 💭 {}", + full_response, + Self::format_duration(total_execution_time), + Self::format_duration(_ttft) + ) } else { full_response }; - + Ok(TaskResult::new(final_response, self.context_window.clone())) } @@ -1846,37 +1889,55 @@ The tool will execute immediately and you'll receive the result (success or erro if let Some(file_path) = tool_call.args.get("file_path") { if let Some(path_str) = file_path.as_str() { // Extract optional start and end positions - let start_char = tool_call.args.get("start") + let start_char = tool_call + .args + .get("start") .and_then(|v| v.as_u64()) .map(|n| n as usize); - let end_char = tool_call.args.get("end") + let end_char = tool_call + .args + .get("end") .and_then(|v| v.as_u64()) .map(|n| n as usize); - - debug!("Reading file: {}, start={:?}, end={:?}", path_str, start_char, end_char); - + + debug!( + "Reading file: {}, start={:?}, end={:?}", + path_str, start_char, end_char + ); + match std::fs::read_to_string(path_str) { Ok(content) => { // Validate and apply range if specified let start = start_char.unwrap_or(0); let end = end_char.unwrap_or(content.len()); - + // Validation if start > content.len() { - return Ok(format!("❌ Start position {} exceeds file length {}", start, content.len())); + return Ok(format!( + "❌ Start position {} exceeds file length {}", + start, + content.len() + )); } if end > content.len() { - return Ok(format!("❌ End position {} exceeds file length {}", end, content.len())); + return Ok(format!( + "❌ End position {} exceeds file length {}", + end, + content.len() + )); } if start > end { - return Ok(format!("❌ Start position {} is greater than end position {}", start, end)); + return Ok(format!( + "❌ Start position {} is greater than end position {}", + start, end + )); } - + // Extract the requested portion let partial_content = &content[start..end]; let line_count = partial_content.lines().count(); let total_lines = content.lines().count(); - + // Format output with range info if partial if start_char.is_some() || end_char.is_some() { Ok(format!( @@ -1884,7 +1945,10 @@ The tool will execute immediately and you'll receive the result (success or erro start, end, line_count, total_lines, partial_content )) } else { - Ok(format!("📄 File content ({} lines):\n{}", line_count, content)) + Ok(format!( + "📄 File content ({} lines):\n{}", + line_count, content + )) } } Err(e) => Ok(format!("❌ Failed to read file '{}': {}", path_str, e)), @@ -2057,8 +2121,8 @@ The tool will execute immediately and you'll receive the result (success or erro let line_count = content.lines().count(); let char_count = content.len(); Ok(format!( - "✅ Successfully wrote {} lines ({} characters) to '{}'", - line_count, char_count, path + "✅ Successfully wrote {} lines ({} characters)", + line_count, char_count )) } Err(e) => Ok(format!("❌ Failed to write to file '{}': {}", path, e)), @@ -2240,10 +2304,7 @@ The tool will execute immediately and you'll receive the result (success or erro // Write the result back to the file match std::fs::write(file_path, &result) { - Ok(()) => Ok(format!( - "✅ Successfully applied unified diff to '{}'", - file_path - )), + Ok(()) => Ok(format!("✅ Successfully applied unified diff")), Err(e) => Ok(format!("❌ Failed to write to file '{}': {}", file_path, e)), } }