use std::sync::atomic::Ordering;
use std::time::Instant;
use tracing::{debug, info, warn};
use reedline::HistoryItem;
use std::path::PathBuf;
use crate::cli::prompt::starship::CMD_DURATION_NONE;
use crate::cli::jarvis::{jarvis_ask_typo_correction, TypoAction};
use crate::engine::builtins::{alias, cd, dirstack, source, unalias, which_type};
use crate::engine::classifier::{is_ai_goodbye_response, InputType};
use crate::engine::dispatch::{AiPipeMode, AiPipeRequest};
use crate::engine::expand;
use crate::engine::typo;
use crate::engine::{execute, try_builtin, try_execute_ai_pipe, CommandResult, LoopAction};
use super::Shell;
impl Shell {
pub(super) async fn handle_input(&mut self, line: &str) -> bool {
info!("\n\n==== USER INPUT RECEIVED, START PROCESS ====");
let line = line.trim().to_string();
if line.is_empty() {
return true;
}
let original_line = line.clone();
let line = if let Some(expanded) = expand::expand_alias(&line, &self.aliases) {
debug!(original = %line, expanded = %expanded, "Alias expanded");
expanded
} else {
line
};
debug!(input = %line, "User input received");
if let Some(result) = self.try_shell_builtins(&line) {
return self.handle_builtin(&original_line, &line, result);
}
if let Some(result) = try_builtin(&line) {
return self.handle_builtin(&original_line, &line, result);
}
let input_type = self.classifier.classify(&line);
debug!(input = %line, classification = ?input_type, "Input classified");
let (line, input_type) = if input_type == InputType::NaturalLanguage {
match check_typo_correction(&line) {
TypoCorrectionOutcome::UseCommand(corrected) => {
let new_type = self.classifier.classify(&corrected);
(corrected, new_type)
}
TypoCorrectionOutcome::Abort => return true,
TypoCorrectionOutcome::Proceed => (line, InputType::NaturalLanguage),
}
} else {
(line, input_type)
};
let start = Instant::now();
let (result, from_tool_call, should_update_exit_code, executed_command) = match input_type {
InputType::Goodbye => {
info!("Goodbye input detected, exiting shell");
return false;
}
InputType::Command => {
if let Some(ai_pipe_req) = try_execute_ai_pipe(&line) {
debug!(input = %line, mode = ?ai_pipe_req.mode, "AI pipe/redirect detected");
let result = self.handle_ai_pipe(ai_pipe_req).await;
(result, false, true, None)
} else {
debug!(input = %line, "Executing as command (no AI)");
(execute(&line), false, true, None)
}
}
InputType::NaturalLanguage => {
let ai_result = self.route_to_ai(&line).await;
(
ai_result.result,
ai_result.from_tool_call,
ai_result.should_update_exit_code,
ai_result.executed_command,
)
}
};
let elapsed_ms = start.elapsed().as_millis() as u64;
self.cmd_duration_ms.store(elapsed_ms, Ordering::Relaxed);
if should_update_exit_code {
self.last_exit_code
.store(result.exit_code, Ordering::Relaxed);
}
if result.used_alt_screen {
println!();
}
println!();
self.record_history(&original_line, &result);
if let Some(ref cmd) = executed_command {
if let Err(e) = self
.editor
.history_mut()
.save(HistoryItem::from_command_line(cmd))
{
warn!("Failed to save AI-executed command to reedline history: {e}");
}
}
if result.exit_code != 0 {
self.investigate_error(&line, &result, from_tool_call).await;
}
if !from_tool_call && is_ai_goodbye_response(&result.stdout) {
info!("AI goodbye response detected, exiting shell");
self.farewell_shown = true;
return false;
}
info!("\n==== FINISHED PROCESS ====\n\n");
true
}
fn handle_builtin(&mut self, original_line: &str, line: &str, result: CommandResult) -> bool {
debug!(
command = %line,
exit_code = result.exit_code,
action = ?result.action,
"Builtin command executed"
);
self.last_exit_code
.store(result.exit_code, Ordering::Relaxed);
self.cmd_duration_ms
.store(CMD_DURATION_NONE, Ordering::Relaxed);
println!();
match result.action {
LoopAction::Continue => {
self.record_history(original_line, &result);
true
}
LoopAction::Exit => {
info!("Exit command received");
false
}
LoopAction::Restart => {
info!("Restart command received");
self.restart_requested.store(true, Ordering::Relaxed);
false
}
}
}
fn try_shell_builtins(&mut self, input: &str) -> Option<CommandResult> {
let first_word = input.split_whitespace().next().unwrap_or("");
if !matches!(
first_word,
"alias" | "unalias" | "source" | "cd" | "pushd" | "popd" | "dirs" | "which" | "type"
) {
return None;
}
let tokens = match shell_words::split(input) {
Ok(t) => t,
Err(e) => {
let msg = format!("jarvish: parse error: {e}\n");
eprint!("{msg}");
return Some(CommandResult::error(msg, 1));
}
};
if tokens.is_empty() {
return Some(CommandResult::success(String::new()));
}
if tokens
.iter()
.any(|t| matches!(t.as_str(), "|" | ">" | ">>" | "<" | "&&" | "||" | ";"))
{
return None;
}
let expanded: Vec<String> = tokens
.into_iter()
.map(|t| expand::expand_token(&t))
.collect();
let args: Vec<&str> = expanded[1..].iter().map(|s| s.as_str()).collect();
let result = match first_word {
"alias" => alias::execute_with_aliases(&args, &mut self.aliases),
"unalias" => unalias::execute_with_aliases(&args, &mut self.aliases),
"source" => {
let path_str = match source::parse(&args) {
Ok(p) => p,
Err(cmd_result) => return Some(cmd_result),
};
let path = PathBuf::from(&path_str);
self.reload_config(&path)
}
"cd" => cd::execute(&args, &mut self.dir_stack),
"pushd" => dirstack::execute_pushd(&args, &mut self.dir_stack),
"popd" => dirstack::execute_popd(&args, &mut self.dir_stack),
"dirs" => dirstack::execute_dirs(&args, &mut self.dir_stack),
"which" => which_type::execute_which(&args, &self.aliases),
"type" => which_type::execute_type(&args, &self.aliases),
_ => unreachable!(),
};
debug!(
command = %first_word,
exit_code = result.exit_code,
"shell builtin executed"
);
Some(result)
}
fn record_history(&self, line: &str, result: &CommandResult) {
if result.action == LoopAction::Continue {
if let Some(ref bb) = self.black_box {
if let Err(e) = bb.record(line, result) {
warn!("Failed to record history: {e}");
eprintln!("jarvish: warning: failed to record history: {e}");
}
}
}
}
async fn handle_ai_pipe(&self, req: AiPipeRequest) -> CommandResult {
let ai = match self.ai_client {
Some(ref ai) => ai,
None => {
let msg = "jarvish: AI pipe requires OPENAI_API_KEY to be set.\n";
eprint!("{msg}");
return CommandResult::error(msg.to_string(), 1);
}
};
if req.stdin_text.is_empty() {
debug!(
exit_code = req.exit_code,
mode = ?req.mode,
"AI pipe: source pipeline produced no stdout, skipping AI"
);
let msg = "jarvish: AI pipe: no input received from the source pipeline.\n";
eprint!("{msg}");
return CommandResult::error(msg.to_string(), req.exit_code.max(1));
}
debug!(
prompt = %req.prompt,
input_chars = req.stdin_text.chars().count(),
source_exit_code = req.exit_code,
mode = ?req.mode,
"Processing AI pipe"
);
let result = match req.mode {
AiPipeMode::Filter => ai.process_ai_pipe(&req.stdin_text, &req.prompt).await,
AiPipeMode::Redirect => ai.process_ai_redirect(&req.stdin_text, &req.prompt).await,
};
match result {
Ok(output) => CommandResult::success(output),
Err(e) => {
let msg = format!("jarvish: {e}\n");
eprint!("{msg}");
CommandResult::error(msg, 1)
}
}
}
}
enum TypoCorrectionOutcome {
UseCommand(String),
Proceed,
Abort,
}
fn check_typo_correction(line: &str) -> TypoCorrectionOutcome {
let first_token = line.split_whitespace().next().unwrap_or("");
if !typo::is_command_like(first_token) {
return TypoCorrectionOutcome::Proceed;
}
let Some(suggestion) = typo::find_correction(first_token) else {
return TypoCorrectionOutcome::Proceed;
};
match jarvis_ask_typo_correction(first_token, &suggestion) {
TypoAction::Accept => {
let rest = &line[first_token.len()..];
TypoCorrectionOutcome::UseCommand(format!("{suggestion}{rest}"))
}
TypoAction::Reject => TypoCorrectionOutcome::Abort,
TypoAction::Abort => TypoCorrectionOutcome::Abort,
}
}