Readability improvements across streaming_parser, input_formatter, commands
- streaming_parser.rs: Reduced ~70 lines by removing redundant comments, consolidating doc comments, using slice syntax for TOOL_CALL_PATTERNS - input_formatter.rs: Lazy regex compilation via once_cell (performance), cleaner function structure, reduced comment noise - commands.rs: Extracted format_research_task_summary() and format_research_report_header() helpers, reduced ~40 lines of duplication - pending_research.rs: Fixed 2 unused variable warnings in tests All changes are behavior-preserving. 446 tests pass. Agent: carmack
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
//! Interactive command handlers for G3 CLI.
|
||||
//!
|
||||
//! Handles `/` commands in interactive mode.
|
||||
//! Handles `/` commands in interactive mode (help, compact, research, etc.).
|
||||
|
||||
use anyhow::Result;
|
||||
use rustyline::Editor;
|
||||
@@ -16,6 +16,33 @@ use crate::project::load_and_validate_project;
|
||||
use crate::template::process_template;
|
||||
use crate::task_execution::execute_task_with_retry;
|
||||
|
||||
// --- Research command helpers ---
|
||||
|
||||
fn format_research_task_summary(task: &g3_core::pending_research::ResearchTask) -> String {
|
||||
let status_emoji = match task.status {
|
||||
g3_core::pending_research::ResearchStatus::Pending => "🔄",
|
||||
g3_core::pending_research::ResearchStatus::Complete => "✅",
|
||||
g3_core::pending_research::ResearchStatus::Failed => "❌",
|
||||
};
|
||||
let injected = if task.injected { " (injected)" } else { "" };
|
||||
let query_preview = if task.query.len() > 60 {
|
||||
format!("{}...", task.query.chars().take(57).collect::<String>())
|
||||
} else {
|
||||
task.query.clone()
|
||||
};
|
||||
format!(
|
||||
" {} `{}` - {} ({}){}\n Query: {}",
|
||||
status_emoji, task.id, task.status, task.elapsed_display(), injected, query_preview
|
||||
)
|
||||
}
|
||||
|
||||
fn format_research_report_header(task: &g3_core::pending_research::ResearchTask) -> String {
|
||||
format!(
|
||||
"📋 Research Report: `{}`\n\nQuery: {}\n\nStatus: {} | Elapsed: {}\n\n{}",
|
||||
task.id, task.query, task.status, task.elapsed_display(), "─".repeat(60)
|
||||
)
|
||||
}
|
||||
|
||||
/// Handle a control command. Returns true if the command was handled and the loop should continue.
|
||||
pub async fn handle_command<W: UiWriter>(
|
||||
input: &str,
|
||||
@@ -135,89 +162,49 @@ pub async fn handle_command<W: UiWriter>(
|
||||
}
|
||||
cmd if cmd == "/research" || cmd.starts_with("/research ") => {
|
||||
let manager = agent.get_pending_research_manager();
|
||||
|
||||
// Parse argument: /research, /research latest, /research <id>
|
||||
let arg = cmd.strip_prefix("/research").unwrap_or("").trim();
|
||||
|
||||
if arg.is_empty() {
|
||||
// List all research tasks
|
||||
let all_tasks = manager.list_all();
|
||||
|
||||
if all_tasks.is_empty() {
|
||||
output.print("📋 No research tasks (pending or completed).");
|
||||
output.print("📋 No research tasks (pending or completed).");
|
||||
} else {
|
||||
output.print(&format!("📋 Research Tasks ({} total):\n", all_tasks.len()));
|
||||
|
||||
for task in all_tasks {
|
||||
let status_emoji = match task.status {
|
||||
g3_core::pending_research::ResearchStatus::Pending => "🔄",
|
||||
g3_core::pending_research::ResearchStatus::Complete => "✅",
|
||||
g3_core::pending_research::ResearchStatus::Failed => "❌",
|
||||
};
|
||||
|
||||
let injected_marker = if task.injected { " (injected)" } else { "" };
|
||||
|
||||
output.print(&format!(
|
||||
" {} `{}` - {} ({}){}\n Query: {}",
|
||||
status_emoji,
|
||||
task.id,
|
||||
task.status,
|
||||
task.elapsed_display(),
|
||||
injected_marker,
|
||||
if task.query.len() > 60 {
|
||||
format!("{}...", &task.query.chars().take(57).collect::<String>())
|
||||
} else {
|
||||
task.query.clone()
|
||||
}
|
||||
));
|
||||
output.print("");
|
||||
output.print(&format!("📋 Research Tasks ({} total):\n", all_tasks.len()));
|
||||
for task in all_tasks {
|
||||
output.print(&format_research_task_summary(&task));
|
||||
output.print("");
|
||||
}
|
||||
}
|
||||
} else if arg == "latest" {
|
||||
// Show the most recent research report
|
||||
let all_tasks = manager.list_all();
|
||||
|
||||
// Find the most recent completed task (smallest elapsed time = most recent)
|
||||
let latest = all_tasks.iter()
|
||||
.filter(|t| t.status != g3_core::pending_research::ResearchStatus::Pending)
|
||||
.min_by_key(|t| t.started_at.elapsed());
|
||||
|
||||
match latest {
|
||||
Some(task) => {
|
||||
output.print(&format!("📋 Research Report: `{}`\n", task.id));
|
||||
output.print(&format!("Query: {}\n", task.query));
|
||||
output.print(&format!("Status: {} | Elapsed: {}\n", task.status, task.elapsed_display()));
|
||||
output.print(&"─".repeat(60));
|
||||
if let Some(ref result) = task.result {
|
||||
output.print(result);
|
||||
} else {
|
||||
output.print("(No report content available)");
|
||||
}
|
||||
output.print(&format_research_report_header(task));
|
||||
output.print(task.result.as_deref().unwrap_or("(No report content available)"));
|
||||
}
|
||||
None => {
|
||||
output.print("📋 No completed research tasks yet.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// View a specific research report by ID
|
||||
let task_id = arg.to_string();
|
||||
|
||||
match manager.get(&task_id) {
|
||||
match manager.get(&arg.to_string()) {
|
||||
Some(task) => {
|
||||
output.print(&format!("📋 Research Report: `{}`\n", task.id));
|
||||
output.print(&format!("Query: {}\n", task.query));
|
||||
output.print(&format!("Status: {} | Elapsed: {}\n", task.status, task.elapsed_display()));
|
||||
output.print(&"─".repeat(60));
|
||||
if let Some(ref result) = task.result {
|
||||
output.print(result);
|
||||
output.print(&format_research_report_header(&task));
|
||||
let content = if let Some(ref result) = task.result {
|
||||
result.as_str()
|
||||
} else if task.status == g3_core::pending_research::ResearchStatus::Pending {
|
||||
output.print("(Research still in progress...)");
|
||||
"(Research still in progress...)"
|
||||
} else {
|
||||
output.print("(No report content available)");
|
||||
}
|
||||
"(No report content available)"
|
||||
};
|
||||
output.print(content);
|
||||
}
|
||||
None => {
|
||||
output.print(&format!("❓ No research task found with id: `{}`", task_id));
|
||||
output.print(&format!("❓ No research task found with id: `{}`", arg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,129 +1,102 @@
|
||||
//! Input formatting for interactive mode.
|
||||
//!
|
||||
//! Formats user input with markdown-style highlighting:
|
||||
//! - ALL CAPS words become bold
|
||||
//! - Quoted text ("..." or '...') becomes cyan
|
||||
//! - Standard markdown formatting (bold, italic, code) is applied
|
||||
//! Applies visual highlighting to user input:
|
||||
//! - ALL CAPS words (2+ chars) → bold green
|
||||
//! - Quoted text ("..." or '...') → cyan
|
||||
//! - Standard markdown (bold, italic, code) via termimad
|
||||
|
||||
use crossterm::terminal;
|
||||
use regex::Regex;
|
||||
use std::io::Write;
|
||||
use std::io::IsTerminal;
|
||||
use once_cell::sync::Lazy;
|
||||
use termimad::MadSkin;
|
||||
|
||||
use crate::streaming_markdown::StreamingMarkdownFormatter;
|
||||
|
||||
/// Pre-process input text to add markdown markers for special formatting.
|
||||
///
|
||||
/// This pass runs BEFORE markdown formatting:
|
||||
/// 1. ALL CAPS words (2+ chars) → wrapped in ** for bold
|
||||
/// 2. Quoted text "..." or '...' → wrapped in special markers for cyan
|
||||
///
|
||||
/// Returns the preprocessed text ready for markdown formatting.
|
||||
// Compiled regexes for preprocessing (compiled once, reused)
|
||||
static CAPS_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
// ALL CAPS words: 2+ uppercase letters, may include numbers, word boundaries
|
||||
Regex::new(r"\b([A-Z][A-Z0-9]{1,}[A-Z0-9]*)\b").unwrap()
|
||||
});
|
||||
static DOUBLE_QUOTE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#""([^"]+)""#).unwrap());
|
||||
static SINGLE_QUOTE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"'([^']+)'").unwrap());
|
||||
|
||||
/// Pre-process input to add markdown markers before formatting.
|
||||
/// ALL CAPS → **bold**, quoted text → special markers for cyan.
|
||||
pub fn preprocess_input(input: &str) -> String {
|
||||
let mut result = input.to_string();
|
||||
|
||||
// First, handle ALL CAPS words (2+ uppercase letters, may include numbers)
|
||||
// Must be a standalone word (word boundaries)
|
||||
let caps_re = Regex::new(r"\b([A-Z][A-Z0-9]{1,}[A-Z0-9]*)\b").unwrap();
|
||||
result = caps_re.replace_all(&result, "**$1**").to_string();
|
||||
// ALL CAPS → **bold**
|
||||
result = CAPS_RE.replace_all(&result, "**$1**").to_string();
|
||||
|
||||
// Then, handle quoted text - wrap in a special marker that we'll process after markdown
|
||||
// Use lowercase placeholders that won't be matched by the ALL CAPS regex
|
||||
let double_quote_re = Regex::new(r#""([^"]+)""#).unwrap();
|
||||
result = double_quote_re.replace_all(&result, "\x00qdbl\x00$1\x00qend\x00").to_string();
|
||||
|
||||
let single_quote_re = Regex::new(r"'([^']+)'").unwrap();
|
||||
result = single_quote_re.replace_all(&result, "\x00qsgl\x00$1\x00qend\x00").to_string();
|
||||
// Quoted text → markers (processed after markdown to apply cyan)
|
||||
result = DOUBLE_QUOTE_RE.replace_all(&result, "\x00qdbl\x00$1\x00qend\x00").to_string();
|
||||
result = SINGLE_QUOTE_RE.replace_all(&result, "\x00qsgl\x00$1\x00qend\x00").to_string();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Apply cyan highlighting to quoted text markers.
|
||||
/// This runs AFTER markdown formatting to apply the cyan color.
|
||||
// Regexes for post-processing quote markers into ANSI cyan
|
||||
static CYAN_DOUBLE_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r#"(\x1b\[36m")([^\x1b]*)\x1b\[0m"#).unwrap()
|
||||
});
|
||||
static CYAN_SINGLE_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(\x1b\[36m')([^\x1b]*)\x1b\[0m").unwrap()
|
||||
});
|
||||
|
||||
/// Apply cyan highlighting to quoted text markers (runs after markdown formatting).
|
||||
fn apply_quote_highlighting(text: &str) -> String {
|
||||
let mut result = text.to_string();
|
||||
|
||||
// Replace double-quote markers with cyan formatting
|
||||
// \x1b[36m = cyan, \x1b[0m = reset
|
||||
result = result.replace("\x00qdbl\x00", "\x1b[36m\"");
|
||||
result = result.replace("\x00qsgl\x00", "\x1b[36m'");
|
||||
result = result.replace("\x00qend\x00", "\x1b[0m");
|
||||
|
||||
// Add back the closing quotes
|
||||
// We need to insert them before the reset code
|
||||
let re = Regex::new(r#"(\x1b\[36m")([^\x1b]*)\x1b\[0m"#).unwrap();
|
||||
result = re.replace_all(&result, |caps: ®ex::Captures| {
|
||||
// Insert closing quotes before reset code
|
||||
result = CYAN_DOUBLE_RE.replace_all(&result, |caps: ®ex::Captures| {
|
||||
format!("{}{}\"\x1b[0m", &caps[1], &caps[2])
|
||||
}).to_string();
|
||||
|
||||
let re = Regex::new(r"(\x1b\[36m')([^\x1b]*)\x1b\[0m").unwrap();
|
||||
result = re.replace_all(&result, |caps: ®ex::Captures| {
|
||||
result = CYAN_SINGLE_RE.replace_all(&result, |caps: ®ex::Captures| {
|
||||
format!("{}{}'\x1b[0m", &caps[1], &caps[2])
|
||||
}).to_string();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Format user input with markdown and special highlighting.
|
||||
///
|
||||
/// Applies:
|
||||
/// 1. ALL CAPS → bold (green)
|
||||
/// 2. Quoted text → cyan
|
||||
/// 3. Standard markdown (bold, italic, inline code)
|
||||
/// Format user input with markdown and special highlighting (ALL CAPS, quotes).
|
||||
pub fn format_input(input: &str) -> String {
|
||||
// Pre-process to add markdown markers
|
||||
let preprocessed = preprocess_input(input);
|
||||
|
||||
// Apply markdown formatting using the streaming formatter
|
||||
let skin = MadSkin::default();
|
||||
let mut formatter = StreamingMarkdownFormatter::new(skin);
|
||||
let formatted = formatter.process(&preprocessed);
|
||||
let formatted = formatted + &formatter.finish();
|
||||
|
||||
// Apply quote highlighting (after markdown so colors don't interfere)
|
||||
apply_quote_highlighting(&formatted)
|
||||
}
|
||||
|
||||
/// Reprint user input in place with formatting.
|
||||
///
|
||||
/// This moves the cursor up to overwrite the original input line,
|
||||
/// then prints the formatted version.
|
||||
///
|
||||
/// Note: This function only performs formatting when stdout is a TTY.
|
||||
/// In non-TTY contexts (piped output, etc.), it does nothing to avoid
|
||||
/// corrupting terminal state for subsequent stdin operations.
|
||||
/// Reprint user input in place with formatting (TTY only).
|
||||
/// Moves cursor up to overwrite original input, then prints formatted version.
|
||||
pub fn reprint_formatted_input(input: &str, prompt: &str) {
|
||||
// Only reformat if stdout is a TTY - avoid corrupting terminal state otherwise
|
||||
if !std::io::stdout().is_terminal() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format the input
|
||||
let formatted = format_input(input);
|
||||
|
||||
// Get terminal width to calculate visual lines
|
||||
// The prompt + input may wrap across multiple terminal rows
|
||||
let term_width = terminal::size()
|
||||
.map(|(w, _)| w as usize)
|
||||
.unwrap_or(80);
|
||||
// Calculate visual lines (prompt + input may wrap across terminal rows)
|
||||
let term_width = terminal::size().map(|(w, _)| w as usize).unwrap_or(80);
|
||||
let visual_lines = (prompt.len() + input.len()).div_ceil(term_width).max(1);
|
||||
|
||||
// Calculate visual lines: prompt + input length divided by terminal width
|
||||
// This accounts for line wrapping in the terminal
|
||||
let total_chars = prompt.len() + input.len();
|
||||
let visual_lines = ((total_chars + term_width - 1) / term_width).max(1); // ceiling division
|
||||
|
||||
// Move cursor up by the number of lines and clear
|
||||
// Move up and clear each line
|
||||
for _ in 0..visual_lines {
|
||||
// Move up one line and clear it
|
||||
print!("\x1b[1A\x1b[2K");
|
||||
}
|
||||
|
||||
// Reprint with prompt and formatted input
|
||||
// Use dim color for the prompt to distinguish from the formatted input
|
||||
// Dim prompt + formatted input
|
||||
println!("\x1b[2m{}\x1b[0m{}", prompt, formatted);
|
||||
|
||||
// Ensure output is flushed
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user