pub(crate) mod anthropic;
mod banner;
mod circuit_breaker;
pub(crate) mod history;
mod input;
mod ollama_native;
pub(crate) mod openai_compat;
mod settings;
pub(crate) mod provider;
mod terminal_guard;
pub(crate) mod tool_types;
mod tools;
mod tui;
use anthropic::AnthropicProvider;
use provider::ChatProvider;
use std::sync::Arc;
use uuid::Uuid;
pub(crate) use crate::model::catalog::try_select_provider;
fn select_provider(name: Option<&str>) -> Arc<dyn ChatProvider> {
match name {
Some(n) => match try_select_provider(n) {
Ok(p) => p,
Err(msg) => {
eprintln!("[chat] {msg}");
std::process::exit(2);
}
},
None => {
if std::env::var("ANTHROPIC_API_KEY").is_ok() {
Arc::new(AnthropicProvider)
} else if std::env::var("OPENAI_API_KEY").is_ok() {
Arc::new(openai_compat::openai())
} else {
Arc::new(openai_compat::ollama())
}
}
}
}
fn resolve_default_model(provider: &Arc<dyn ChatProvider>) -> String {
if provider.name() == "ollama" {
if let Some(detected) = openai_compat::detect_ollama_model() {
eprintln!("[chat] auto-detected Ollama model: {detected}");
return detected;
}
eprintln!(
"[chat] no Ollama model detected at 127.0.0.1:11434 (daemon not running, or nothing \
pulled) — falling back to '{}', which may not actually be pulled. Run `ollama pull \
{}`, or pass --model / use /model to name one that is.",
provider.default_model(),
provider.default_model()
);
}
provider.default_model().to_string()
}
#[allow(clippy::too_many_arguments)]
pub fn dispatch(
provider_name: Option<String>,
model: Option<String>,
system: Option<String>,
resume: Option<String>,
verbose: bool,
use_sandbox: bool,
) {
let repo_root = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let chat_settings = settings::load(&repo_root).unwrap_or_default();
let use_configured_model = provider_name.is_none();
let provider_name = provider_name.or_else(|| Some(chat_settings.default_provider.clone()));
let provider = select_provider(provider_name.as_deref());
let model = model
.or_else(|| {
use_configured_model
.then_some(chat_settings.default_model)
.flatten()
})
.unwrap_or_else(|| resolve_default_model(&provider));
let api_key = if provider.requires_key() {
match std::env::var(provider.env_var()) {
Ok(k) if !k.is_empty() => Some(k),
_ => {
eprintln!(
"[chat] {} not set — export it, or run with --provider ollama for a local model",
provider.env_var()
);
std::process::exit(2);
}
}
} else {
None
};
let resumed = resume.is_some();
let (session_id, history) = match &resume {
Some(id) => match history::load(id) {
Ok(mut msgs) => {
match history::repair_dangling_tool_call(id, &mut msgs) {
Ok(true) => eprintln!(
"[chat] repaired a tool call left unresolved by a previous crash/force-quit"
),
Ok(false) => {}
Err(e) => eprintln!("[chat] warning: failed to repair dangling tool call: {e}"),
}
(id.clone(), msgs)
}
Err(e) => {
eprintln!("[chat] {e}");
std::process::exit(2);
}
},
None => (Uuid::new_v4().to_string(), Vec::new()),
};
let mut guard = match terminal_guard::TerminalGuard::new() {
Ok(g) => g,
Err(e) => {
eprintln!("[chat] {e:#}");
std::process::exit(1);
}
};
let app = tui::App::new(
provider,
model,
system,
api_key,
session_id,
history,
verbose,
resumed,
use_sandbox,
);
if let Err(e) = tui::run(&mut guard, app) {
drop(guard); eprintln!("[chat] fatal: {e:#}");
std::process::exit(1);
}
}