use std::io::IsTerminal;
use std::sync::atomic::Ordering;
use tracing::{debug, info, warn};
use crate::ai::AiResponse;
use crate::cli::jarvis::{jarvis_ask_investigate, jarvis_notice};
use crate::engine::{execute, CommandResult, LoopAction};
use super::Shell;
fn matches_ignore_pattern(line: &str, patterns: &[String]) -> bool {
patterns
.iter()
.any(|pattern| line == pattern || line.starts_with(&format!("{pattern} ")))
}
impl Shell {
pub(super) async fn investigate_error(
&mut self,
line: &str,
result: &CommandResult,
from_tool_call: bool,
) {
let ai = match self.ai_client {
Some(ref ai) => ai,
None => return,
};
if !from_tool_call && !std::io::stdin().is_terminal() {
info!("Skipping investigation (non-interactive mode)");
return;
}
if !from_tool_call && matches_ignore_pattern(line, &self.ignore_auto_investigation_cmds) {
info!(command = %line, "Skipping investigation (matched ignore_auto_investigation_cmds)");
return;
}
let should_investigate = if from_tool_call {
info!("Tool Call command failed, auto-investigating");
true
} else {
jarvis_ask_investigate(result.exit_code)
};
if !should_investigate {
return;
}
if from_tool_call {
if let Some(mut conv) = self.conversation_state.take() {
debug!("Continuing existing conversation for error investigation");
let error_msg = build_error_follow_up(line, result);
match ai.continue_conversation(&mut conv, &error_msg).await {
Ok(response) => {
self.handle_investigation_response(response, Some(conv));
return;
}
Err(e) => {
warn!(error = %e, "Conversation continuation for investigation failed, falling back to new investigation");
eprintln!("jarvish: investigation follow-up failed: {e}");
}
}
}
}
let bb_context = self
.black_box
.as_ref()
.and_then(|bb| bb.get_recent_context(5).ok())
.unwrap_or_default();
let cwd = std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "unknown".to_string());
let context = format!("Current working directory: {cwd}\n\n{bb_context}");
match ai.investigate_error(line, result, &context).await {
Ok(conv_result) => {
let response = conv_result.response.clone();
self.handle_investigation_response(response, Some(conv_result.conversation));
}
Err(e) => {
warn!(error = %e, "Error investigation failed");
eprintln!("jarvish: investigation failed: {e}");
}
}
}
fn handle_investigation_response(
&mut self,
response: AiResponse,
conversation: Option<crate::ai::ConversationState>,
) {
match response {
AiResponse::Command(ref fix_cmd) => {
jarvis_notice(fix_cmd);
let fix_result = execute(fix_cmd);
self.last_exit_code
.store(fix_result.exit_code, Ordering::Relaxed);
println!();
if fix_result.action == LoopAction::Continue {
if let Some(ref bb) = self.black_box {
if let Err(e) = bb.record(fix_cmd, &fix_result) {
warn!("Failed to record fix command history: {e}");
}
}
}
self.conversation_state = conversation;
}
AiResponse::NaturalLanguage(_) => {
self.conversation_state = conversation;
println!();
}
}
}
}
fn build_error_follow_up(command: &str, result: &CommandResult) -> String {
let mut msg = format!(
"The fix command I just ran has failed.\n\
Command: {command}\n\
Exit code: {}\n",
result.exit_code
);
if !result.stdout.is_empty() {
msg.push_str(&format!("\nstdout:\n{}\n", result.stdout));
}
if !result.stderr.is_empty() {
msg.push_str(&format!("\nstderr:\n{}\n", result.stderr));
}
msg.push_str("\nPlease investigate and try a different approach to fix this.");
msg
}
#[cfg(test)]
mod tests {
use super::*;
fn patterns(strs: &[&str]) -> Vec<String> {
strs.iter().map(|s| s.to_string()).collect()
}
#[test]
fn empty_patterns_never_matches() {
assert!(!matches_ignore_pattern("git log", &[]));
}
#[test]
fn exact_match() {
let p = patterns(&["git log"]);
assert!(matches_ignore_pattern("git log", &p));
}
#[test]
fn prefix_match_with_args() {
let p = patterns(&["git log"]);
assert!(matches_ignore_pattern("git log --oneline", &p));
}
#[test]
fn no_match_without_word_boundary() {
let p = patterns(&["git log"]);
assert!(!matches_ignore_pattern("git logx", &p));
}
#[test]
fn broad_pattern_matches_all_subcommands() {
let p = patterns(&["git"]);
assert!(matches_ignore_pattern("git log", &p));
assert!(matches_ignore_pattern("git status", &p));
assert!(matches_ignore_pattern("git", &p));
}
#[test]
fn multiple_patterns() {
let p = patterns(&["git log", "make test"]);
assert!(matches_ignore_pattern("git log --oneline", &p));
assert!(matches_ignore_pattern("make test", &p));
assert!(!matches_ignore_pattern("cargo test", &p));
}
#[test]
fn no_partial_match_in_middle() {
let p = patterns(&["log"]);
assert!(!matches_ignore_pattern("git log", &p));
assert!(matches_ignore_pattern("log something", &p));
}
}