use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use arrrg::CommandLine;
use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
use claudius::Renderer;
use claudius::chat::{
ChatAgent, ChatArgs, ChatCommand, ChatConfig, ChatSession, PlainTextRenderer, help_text,
parse_command,
};
use claudius::{Anthropic, Model, SystemPrompt, ThinkingConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (args, _) = ChatArgs::from_command_line_relaxed("claudius-chat [OPTIONS]");
let config = ChatConfig::try_from(args)?;
let use_color = config.use_color;
let client = Anthropic::new(None)?;
let mut session = ChatSession::new(client, config);
let mut rl = DefaultEditor::new()?;
let interrupted = Arc::new(AtomicBool::new(false));
let mut renderer = PlainTextRenderer::with_color_and_interrupt(use_color, interrupted.clone());
let context = ();
let interrupted_clone = interrupted.clone();
ctrlc::set_handler(move || {
interrupted_clone.store(true, Ordering::Relaxed);
})?;
println!("Claude Chat (model: {})", session.config().model());
println!("Type /help for commands, /quit to exit\n");
loop {
interrupted.store(false, Ordering::Relaxed);
let readline = rl.readline("You: ");
match readline {
Ok(line) => {
let line = line.trim();
if line.is_empty() {
continue;
}
let _ = rl.add_history_entry(line);
if let Some(cmd) = parse_command(line) {
match cmd {
ChatCommand::Quit => {
println!("Goodbye!");
break;
}
ChatCommand::Clear => {
session.clear();
renderer.print_info(&context, "Conversation cleared.");
}
ChatCommand::Help => {
for line in help_text().lines() {
println!(" {}", line);
}
}
ChatCommand::Model(model_name) => {
let model = model_name
.parse()
.unwrap_or_else(|_| Model::Custom(model_name.clone()));
session.template_mut().model = Some(model);
renderer
.print_info(&context, &format!("Model changed to: {}", model_name));
}
ChatCommand::System(prompt) => {
session.template_mut().system = prompt.clone().map(SystemPrompt::from);
match prompt {
Some(p) => renderer
.print_info(&context, &format!("System prompt set to: {}", p)),
None => renderer.print_info(&context, "System prompt cleared."),
}
}
ChatCommand::MaxTokens(value) => {
session.template_mut().max_tokens = Some(value);
renderer.print_info(&context, &format!("max_tokens set to {value}"));
}
ChatCommand::Temperature(value) => {
session.template_mut().temperature = Some(value);
renderer
.print_info(&context, &format!("temperature set to {:.2}", value));
}
ChatCommand::ClearTemperature => {
session.template_mut().temperature = None;
renderer.print_info(&context, "temperature reset to model default");
}
ChatCommand::TopP(value) => {
session.template_mut().top_p = Some(value);
renderer.print_info(&context, &format!("top_p set to {:.2}", value));
}
ChatCommand::ClearTopP => {
session.template_mut().top_p = None;
renderer.print_info(&context, "top_p reset to model default");
}
ChatCommand::TopK(value) => {
session.template_mut().top_k = Some(value);
renderer.print_info(&context, &format!("top_k set to {value}"));
}
ChatCommand::ClearTopK => {
session.template_mut().top_k = None;
renderer.print_info(&context, "top_k reset to model default");
}
ChatCommand::AddStopSequence(sequence) => {
let stop_sequences = session
.template_mut()
.stop_sequences
.get_or_insert_with(Vec::new);
if !stop_sequences.iter().any(|s| s == &sequence) {
stop_sequences.push(sequence.clone());
}
renderer
.print_info(&context, &format!("Added stop sequence: {sequence}"));
}
ChatCommand::ClearStopSequences => {
session.template_mut().stop_sequences = None;
renderer.print_info(&context, "Stop sequences cleared.");
}
ChatCommand::ListStopSequences => {
let sequences =
session.template().stop_sequences.as_deref().unwrap_or(&[]);
print_stop_sequences(sequences);
}
ChatCommand::Thinking(budget) => {
session.template_mut().thinking = budget.map(ThinkingConfig::enabled);
match budget {
Some(tokens) => {
renderer.print_info(
&context,
&format!(
"Extended thinking enabled with {} token budget.",
tokens
),
);
}
None => {
renderer.print_info(&context, "Extended thinking disabled.");
}
}
}
ChatCommand::Budget(_tokens) => {
renderer.print_error(&context, "budget not supported");
}
ChatCommand::ClearBudget => {
session.config_mut().session_budget = None;
renderer.print_info(&context, "Session budget cleared.");
}
ChatCommand::Caching(enabled) => {
session.config_mut().caching_enabled = enabled;
if enabled {
renderer.print_info(&context, "Prompt caching enabled.");
} else {
renderer.print_info(&context, "Prompt caching disabled.");
}
}
ChatCommand::TranscriptPath(path) => {
session.config_mut().transcript_path = Some(PathBuf::from(&path));
renderer.print_info(
&context,
&format!("Transcript auto-save set to {}", path),
);
}
ChatCommand::ClearTranscriptPath => {
session.config_mut().transcript_path = None;
renderer.print_info(&context, "Transcript auto-save disabled.");
}
ChatCommand::SaveTranscript(path) => {
match session.save_transcript_to(&path) {
Ok(_) => renderer
.print_info(&context, &format!("Transcript saved to {}", path)),
Err(err) => renderer.print_error(
&context,
&format!("Failed to save transcript: {}", err),
),
}
}
ChatCommand::LoadTranscript(path) => {
match session.load_transcript_from(&path) {
Ok(_) => renderer.print_info(
&context,
&format!("Transcript loaded from {}", path),
),
Err(err) => renderer.print_error(
&context,
&format!("Failed to load transcript: {}", err),
),
}
}
ChatCommand::Stats => {
print_stats(&session);
}
ChatCommand::ShowConfig => {
print_config(&session);
}
ChatCommand::Invalid(message) => {
renderer.print_error(&context, &message);
}
}
continue;
}
println!("Claude:");
let message = claudius::MessageParam::user(line);
if let Err(e) = session.send_message(message, &mut renderer).await {
renderer.print_error(&context, &e.to_string());
}
}
Err(ReadlineError::Interrupted) => {
println!();
continue;
}
Err(ReadlineError::Eof) => {
println!("\nGoodbye!");
break;
}
Err(err) => {
renderer.print_error(&context, &format!("Input error: {}", err));
break;
}
}
}
Ok(())
}
fn print_stats<A: ChatAgent>(session: &ChatSession<A>) {
let stats = session.stats();
println!(" Session Statistics:");
println!(" Model: {}", stats.model);
println!(" Messages: {}", stats.message_count);
println!(" Max tokens: {}", stats.max_tokens);
println!(" Temperature: {}", describe_float(stats.temperature));
println!(" Top-p: {}", describe_float(stats.top_p));
println!(" Top-k: {}", describe_top_k(stats.top_k));
if let Some(prompt) = stats.system_prompt.as_deref() {
println!(" System prompt: {}", prompt);
} else {
println!(" System prompt: (none)");
}
println!(
" Thinking: {}",
match stats.thinking_budget {
Some(budget) => format!("enabled ({} tokens)", budget),
None => "disabled".to_string(),
}
);
print_stop_sequences(&stats.stop_sequences);
println!(
" Total tokens: {} in / {} out ({} requests)",
stats.total_input_tokens, stats.total_output_tokens, stats.total_requests
);
if stats.caching_enabled {
println!(
" Cache tokens: {} created / {} read",
stats.total_cache_creation_tokens, stats.total_cache_read_tokens
);
}
if let Some(input) = stats.last_turn_input_tokens {
let output = stats.last_turn_output_tokens.unwrap_or(0);
println!(" Last turn tokens: {input} in / {output} out");
}
if let Some(limit) = stats.session_budget_tokens {
let remaining = limit.saturating_sub(stats.budget_spent_tokens);
println!(
" Budget: {}/{} tokens ({} remaining)",
stats.budget_spent_tokens, limit, remaining
);
} else {
println!(" Budget: (not set)");
}
match stats.transcript_path {
Some(ref path) => println!(" Transcript file: {}", path.display()),
None => println!(" Transcript file: (disabled)"),
}
}
fn print_config<A: ChatAgent>(session: &ChatSession<A>) {
let stats = session.stats();
println!(" Current Configuration:");
println!(" Model: {}", stats.model);
println!(" Max tokens: {}", stats.max_tokens);
println!(" Temperature: {}", describe_float(stats.temperature));
println!(" Top-p: {}", describe_float(stats.top_p));
println!(" Top-k: {}", describe_top_k(stats.top_k));
println!(
" Thinking: {}",
match stats.thinking_budget {
Some(budget) => format!("enabled ({} tokens)", budget),
None => "disabled".to_string(),
}
);
println!(
" Caching: {}",
if stats.caching_enabled {
"enabled"
} else {
"disabled"
}
);
if let Some(prompt) = stats.system_prompt.as_deref() {
println!(" System prompt: {}", prompt);
} else {
println!(" System prompt: (none)");
}
print_stop_sequences(&stats.stop_sequences);
match stats.transcript_path {
Some(ref path) => println!(" Transcript file: {}", path.display()),
None => println!(" Transcript file: (disabled)"),
}
}
fn print_stop_sequences(stop_sequences: &[String]) {
if stop_sequences.is_empty() {
println!(" Stop sequences: (none)");
} else {
println!(" Stop sequences:");
for seq in stop_sequences {
println!(" - {}", seq);
}
}
}
fn describe_float(value: Option<f32>) -> String {
value
.map(|v| format!("{v:.2}"))
.unwrap_or_else(|| "default".to_string())
}
fn describe_top_k(value: Option<u32>) -> String {
value
.map(|v| v.to_string())
.unwrap_or_else(|| "default".to_string())
}