use anyhow::Result;
use std::io::{stdout, BufRead, Write};
use tokio::sync::mpsc;
use crate::commands::{self, CommandResult};
use crate::config::{Config, HookTrigger};
use crate::context;
use crate::permissions::PermissionResponse;
use crate::plugin::PluginRegistry;
use crate::query::{Engine, StreamEvent};
use crate::session;
use crate::utils::diff::colorize_diff;
pub async fn run(mut engine: Engine, config: &Config, plugins: &PluginRegistry) -> Result<()> {
let system_prompt = context::build_system_prompt_for_model(
engine.model(),
Some(plugins),
&HookTrigger::OnContextBuild,
config.is_anthropic(),
)
.await?;
engine.set_system_prompt(system_prompt);
let (_session_id, session_path) = session::create_session(engine.model())?;
println!("\x1b[1;36mclaux\x1b[0m v{}", env!("CARGO_PKG_VERSION"));
println!("Model: \x1b[33m{}\x1b[0m", engine.model());
println!("Type /help for commands, Ctrl+D to exit.\n");
loop {
let input = match read_input()? {
Some(input) => input,
None => break, };
let trimmed = input.trim();
if trimmed.is_empty() {
continue;
}
if let Some(result) = commands::parse_command(trimmed) {
match result {
CommandResult::Text(ref text) if text == "__cost__" => {
println!("{}", commands::format_cost(&engine));
}
CommandResult::Text(text) => println!("{text}"),
CommandResult::Exit => break,
CommandResult::Async(async_cmd) => {
match commands::execute_async(async_cmd, &mut engine).await {
Ok(output) => println!("{output}"),
Err(e) => eprintln!("\x1b[31mError: {e}\x1b[0m"),
}
}
}
continue;
}
let user_msg = crate::api::Message::user(trimmed);
let _ = session::append_message(&session_path, &user_msg);
print!("\n\x1b[1;32m❯\x1b[0m ");
stdout().flush()?;
let (tx, mut rx) = mpsc::channel::<StreamEvent>(256);
let model_name = engine.model().to_string();
print!("\n \x1b[2mthinking...\x1b[0m");
let _ = stdout().flush();
let display_handle = tokio::spawn(async move {
let mut in_tool = false;
let mut first_text = true;
while let Some(event) = rx.recv().await {
match event {
StreamEvent::Text(t) => {
if in_tool {
println!();
in_tool = false;
}
if first_text {
print!("\r\x1b[2m● {model_name} \x1b[0m");
let _ = stdout().flush();
first_text = false;
}
print!("{t}");
let _ = stdout().flush();
}
StreamEvent::ToolStart { name, summary, .. } => {
print!("\n \x1b[2m[{name}]\x1b[0m {summary} ");
let _ = stdout().flush();
in_tool = true;
}
StreamEvent::ToolResult { is_error, .. } => {
if is_error {
print!("\x1b[31m✗\x1b[0m");
} else {
print!("\x1b[32m✓\x1b[0m");
}
let _ = stdout().flush();
in_tool = false;
}
StreamEvent::PermissionRequest {
tool_name,
summary,
respond,
} => {
if in_tool {
println!();
in_tool = false;
}
let response = prompt_permission(&tool_name, &summary);
let _ = respond.send(response);
}
StreamEvent::PermissionRequestWithDiff {
tool_name,
summary,
diff,
respond,
} => {
if in_tool {
println!();
in_tool = false;
}
let response = prompt_permission_with_diff(&tool_name, &summary, &diff);
let _ = respond.send(response);
}
StreamEvent::Error(e) => {
eprintln!("\n\x1b[31mError: {e}\x1b[0m");
}
StreamEvent::Done => {
println!("\n");
break;
}
}
}
});
if let Err(e) = engine.submit_streaming(trimmed, tx).await {
eprintln!("\n\x1b[31mError: {e}\x1b[0m\n");
}
display_handle.await?;
if let Some(last) = engine.messages().last() {
let _ = session::append_message(&session_path, last);
}
}
println!("\n{}", engine.cost.format_summary());
println!("Goodbye!");
Ok(())
}
fn prompt_permission(tool_name: &str, summary: &str) -> PermissionResponse {
if tool_name == "Bash" {
print!(
"\n \x1b[33m⚡ {summary}\x1b[0m \x1b[2m(y)es / (n)o / (a)lways this command / (A)lways all bash\x1b[0m "
);
} else {
print!("\n \x1b[33m⚡ {summary}\x1b[0m \x1b[2m(y)es / (n)o / (a)lways\x1b[0m ");
}
let _ = stdout().flush();
let mut input = String::new();
if std::io::stdin().lock().read_line(&mut input).is_err() {
return PermissionResponse::Deny;
}
let trimmed = input.trim().to_lowercase();
if tool_name == "Bash" && (trimmed == "a" || trimmed == "always") {
if let Some(cmd) = summary.strip_prefix("bash: ") {
return PermissionResponse::AlwaysAllowCommand(cmd.trim().to_string());
}
return PermissionResponse::AlwaysAllowCommand(summary.to_string());
}
match trimmed.as_str() {
"y" | "yes" | "" => PermissionResponse::Allow,
"a" | "always" => PermissionResponse::AlwaysAllow,
_ => PermissionResponse::Deny,
}
}
fn prompt_permission_with_diff(tool_name: &str, summary: &str, diff: &str) -> PermissionResponse {
if tool_name == "Bash" {
println!("\n \x1b[33m⚡ {summary}\x1b[0m \x1b[2m(y)es / (n)o / (a)lways this command / (A)lways all bash\x1b[0m");
} else {
println!("\n \x1b[33m⚡ {summary}\x1b[0m \x1b[2m(y)es / (n)o / (a)lways\x1b[0m");
}
println!("\n \x1b[2m--- Diff Preview ---\x1b[0m");
let colored_diff = colorize_diff(diff);
for line in colored_diff.lines() {
println!(" {line}");
}
println!(" \x1b[2m--- End Diff ---\x1b[0m\n");
print!(" Allow? ");
let _ = stdout().flush();
let mut input = String::new();
if std::io::stdin().lock().read_line(&mut input).is_err() {
return PermissionResponse::Deny;
}
let trimmed = input.trim().to_lowercase();
if tool_name == "Bash" && (trimmed == "a" || trimmed == "always") {
if let Some(cmd) = summary.strip_prefix("bash: ") {
return PermissionResponse::AlwaysAllowCommand(cmd.trim().to_string());
}
return PermissionResponse::AlwaysAllowCommand(summary.to_string());
}
match trimmed.as_str() {
"y" | "yes" | "" => PermissionResponse::Allow,
"a" | "always" => PermissionResponse::AlwaysAllow,
_ => PermissionResponse::Deny,
}
}
fn read_input() -> Result<Option<String>> {
print!("\x1b[1;34m>\x1b[0m ");
stdout().flush()?;
let mut line = String::new();
match std::io::stdin().read_line(&mut line) {
Ok(0) => Ok(None), Ok(_) => Ok(Some(line)),
Err(e) => Err(e.into()),
}
}