use anyhow::{Context, Result};
use owo_colors::OwoColorize;
use std::sync::{Arc, Mutex};
use indicatif::{ProgressBar, ProgressStyle};
use crate::cache::CacheManager;
pub(super) fn handle_ask(
question: Option<String>,
_auto_execute: bool,
provider_override: Option<String>,
as_json: bool,
pretty_json: bool,
additional_context: Option<String>,
configure: bool,
agentic: bool,
max_iterations: usize,
no_eval: bool,
show_reasoning: bool,
verbose: bool,
quiet: bool,
answer: bool,
interactive: bool,
debug: bool,
) -> Result<()> {
if configure {
eprintln!("Note: --configure is deprecated, use `rfx llm config` instead");
log::info!("Launching configuration wizard");
return crate::semantic::run_configure_wizard();
}
if !crate::semantic::is_any_api_key_configured() {
anyhow::bail!(
"No API key configured.\n\
\n\
Please run 'rfx ask --configure' to set up your API provider and key.\n\
\n\
Alternatively, you can set an environment variable:\n\
- OPENAI_API_KEY\n\
- ANTHROPIC_API_KEY\n\
- OPENROUTER_API_KEY"
);
}
if interactive || question.is_none() {
log::info!("Launching interactive chat mode");
let cache = CacheManager::new(".");
if !cache.exists() {
anyhow::bail!(
"No index found in current directory.\n\
\n\
Run 'rfx index' to build the code search index first.\n\
\n\
Example:\n\
$ rfx index # Index current directory\n\
$ rfx ask # Launch interactive chat"
);
}
return crate::semantic::run_chat_mode(cache, provider_override, None);
}
let question = question.unwrap();
log::info!("Starting ask command");
let cache = CacheManager::new(".");
if !cache.exists() {
anyhow::bail!(
"No index found in current directory.\n\
\n\
Run 'rfx index' to build the code search index first.\n\
\n\
Example:\n\
$ rfx index # Index current directory\n\
$ rfx ask \"Find all TODOs\" # Ask questions"
);
}
let runtime = tokio::runtime::Runtime::new()
.context("Failed to create async runtime")?;
let quiet = quiet || as_json;
let spinner = if !as_json {
let s = ProgressBar::new_spinner();
s.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])
);
s.set_message("Generating queries...".to_string());
s.enable_steady_tick(std::time::Duration::from_millis(80));
Some(s)
} else {
None
};
let (queries, results, total_count, count_only, gathered_context) = if agentic {
let spinner_shared = if !quiet {
spinner.as_ref().map(|s| Arc::new(Mutex::new(s.clone())))
} else {
None
};
let reporter: Box<dyn crate::semantic::AgenticReporter> = if quiet {
Box::new(crate::semantic::QuietReporter)
} else {
Box::new(crate::semantic::ConsoleReporter::new(show_reasoning, verbose, debug, spinner_shared))
};
if let Some(ref s) = spinner {
s.set_message("Starting agentic mode...".to_string());
s.enable_steady_tick(std::time::Duration::from_millis(80));
}
let agentic_config = crate::semantic::AgenticConfig {
max_iterations,
max_tools_per_phase: 5,
enable_evaluation: !no_eval,
eval_config: Default::default(),
provider_override: provider_override.clone(),
model_override: None,
show_reasoning,
verbose,
debug,
};
let agentic_response = runtime.block_on(async {
crate::semantic::run_agentic_loop(&question, &cache, agentic_config, &*reporter).await
}).context("Failed to run agentic loop")?;
if let Some(ref s) = spinner {
s.finish_and_clear();
}
if !as_json {
reporter.clear_all();
}
log::info!("Agentic loop completed: {} queries generated", agentic_response.queries.len());
let count_only_mode = agentic_response.total_count.is_none();
let count = agentic_response.total_count.unwrap_or(0);
(agentic_response.queries, agentic_response.results, count, count_only_mode, agentic_response.gathered_context)
} else {
if let Some(ref s) = spinner {
s.set_message("Generating queries...".to_string());
s.enable_steady_tick(std::time::Duration::from_millis(80));
}
let semantic_response = runtime.block_on(async {
crate::semantic::ask_question(&question, &cache, provider_override.clone(), additional_context, debug).await
}).context("Failed to generate semantic queries")?;
if let Some(ref s) = spinner {
s.finish_and_clear();
}
log::info!("LLM generated {} queries", semantic_response.queries.len());
let (exec_results, exec_total, exec_count_only) = runtime.block_on(async {
crate::semantic::execute_queries(semantic_response.queries.clone(), &cache).await
}).context("Failed to execute queries")?;
(semantic_response.queries, exec_results, exec_total, exec_count_only, None)
};
let generated_answer = if answer {
let answer_spinner = if !as_json {
let s = ProgressBar::new_spinner();
s.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.unwrap()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])
);
s.set_message("Generating answer...".to_string());
s.enable_steady_tick(std::time::Duration::from_millis(80));
Some(s)
} else {
None
};
let mut config = crate::semantic::config::load_config(cache.path())?;
if let Some(provider) = &provider_override {
config.provider = provider.clone();
}
let api_key = crate::semantic::config::get_api_key(&config.provider)?;
let model = if config.model.is_some() {
config.model.clone()
} else {
crate::semantic::config::get_user_model(&config.provider)
};
let provider_instance = crate::semantic::providers::create_provider(
&config.provider,
api_key,
model,
crate::semantic::config::get_provider_options(&config.provider),
config.timeout_seconds,
)?;
let codebase_context_str = crate::semantic::context::CodebaseContext::extract(&cache)
.ok()
.map(|ctx| ctx.to_prompt_string());
let answer_result = runtime.block_on(async {
crate::semantic::generate_answer(
&question,
&results,
total_count,
gathered_context.as_deref(),
codebase_context_str.as_deref(),
&*provider_instance,
).await
}).context("Failed to generate answer")?;
if let Some(s) = answer_spinner {
s.finish_and_clear();
}
Some(answer_result)
} else {
None
};
if as_json {
let json_response = crate::semantic::AgenticQueryResponse {
queries: queries.clone(),
results: results.clone(),
total_count: if count_only { None } else { Some(total_count) },
gathered_context: gathered_context.clone(),
tools_executed: None, answer: generated_answer,
};
let json_str = if pretty_json {
serde_json::to_string_pretty(&json_response)?
} else {
serde_json::to_string(&json_response)?
};
println!("{}", json_str);
return Ok(());
}
if !answer {
println!("\n{}", "Generated Queries:".bold().cyan());
println!("{}", "==================".cyan());
for (idx, query_cmd) in queries.iter().enumerate() {
println!(
"{}. {} {} {}",
(idx + 1).to_string().bright_white().bold(),
format!("[order: {}, merge: {}]", query_cmd.order, query_cmd.merge).dimmed(),
"rfx".bright_green().bold(),
query_cmd.command.bright_white()
);
}
println!();
}
println!();
if let Some(answer_text) = generated_answer {
println!("{}", "Answer:".bold().green());
println!("{}", "=======".green());
println!();
termimad::print_text(&answer_text);
println!();
if !results.is_empty() {
println!(
"{}",
format!(
"(Based on {} matches across {} files)",
total_count,
results.len()
).dimmed()
);
}
} else {
if count_only {
println!("{} {}", "Found".bright_green().bold(), format!("{} results", total_count).bright_white().bold());
} else if results.is_empty() {
println!("{}", "No results found.".yellow());
} else {
println!(
"{} {} {} {} {}",
"Found".bright_green().bold(),
total_count.to_string().bright_white().bold(),
"total results across".dimmed(),
results.len().to_string().bright_white().bold(),
"files:".dimmed()
);
println!();
for file_group in &results {
println!("{}:", file_group.path.bright_cyan().bold());
for match_result in &file_group.matches {
println!(
" {} {}-{}: {}",
"Line".dimmed(),
match_result.span.start_line.to_string().bright_yellow(),
match_result.span.end_line.to_string().bright_yellow(),
match_result.preview.lines().next().unwrap_or("")
);
}
println!();
}
}
}
Ok(())
}