1mod approval;
4mod commands;
5mod core;
6mod formatter;
7mod input;
8mod presentation;
9mod status;
10
11use crate::error::CliError;
12use commands::{handle_special_command, SpecialCommandResult};
13use core::{input_prompt, print_input_padding, print_welcome, reset_input_style};
14use input::InputStyleHelper;
15use rustyline::config::Config;
16use rustyline::error::ReadlineError;
17use rustyline::{Cmd, Editor, KeyEvent};
18use status::{clear_status_line, update_status_line};
19
20use mixtape_core::Agent;
21use std::io::Write;
22use std::sync::{Arc, Mutex};
23
24pub use approval::{
25 print_confirmation, print_tool_header, prompt_for_approval, read_input, ApprovalPrompter,
26 DefaultPrompter, PermissionRequest, SimplePrompter,
27};
28pub use commands::Verbosity;
29pub use presentation::{indent_lines, PresentationHook};
30
31pub async fn run_cli(agent: Agent) -> Result<(), CliError> {
63 let agent = Arc::new(agent);
64
65 let verbosity = Arc::new(Mutex::new(Verbosity::Normal));
67 agent.add_hook(PresentationHook::new(
68 Arc::clone(&agent),
69 Arc::clone(&verbosity),
70 ));
71 print_welcome(&agent).await?;
72
73 let config = Config::default();
74 let mut rl: Editor<InputStyleHelper, rustyline::history::DefaultHistory> =
75 Editor::with_config(config)?;
76 rl.set_helper(Some(InputStyleHelper));
77
78 rl.bind_sequence(KeyEvent::ctrl('J'), Cmd::Newline);
80
81 let history_path = dirs::cache_dir()
82 .map(|p| p.join("mixtape/history.txt"))
83 .unwrap_or_else(|| ".mixtape/history.txt".into());
84
85 if history_path.exists() {
87 rl.load_history(&history_path).ok();
88 }
89
90 loop {
91 update_status_line(&agent);
93
94 print_input_padding();
95 let readline = rl.readline(input_prompt());
96 reset_input_style();
97
98 match readline {
99 Ok(line) => {
100 let line = line.trim();
101
102 if line.is_empty() {
103 continue;
104 }
105
106 rl.add_history_entry(line)?;
107
108 if let Some(result) = handle_special_command(line, &agent, &verbosity).await? {
110 match result {
111 SpecialCommandResult::Exit => break,
112 SpecialCommandResult::Continue => continue,
113 }
114 }
115
116 print!("\n\x1b[2m⋯ thinking\x1b[0m");
118 let _ = std::io::stdout().flush();
119
120 match agent.run(line).await {
122 Ok(response) => {
123 print!("\r\x1b[2K");
125 println!("\n{}\n", response);
126
127 update_status_line(&agent);
129 }
130 Err(e) => {
131 print!("\r\x1b[2K");
133 eprintln!("❌ Error: {}\n", e);
134
135 update_status_line(&agent);
137 }
138 }
139 }
140 Err(ReadlineError::Interrupted) => {
141 println!("^C");
143 continue;
144 }
145 Err(ReadlineError::Eof) => {
146 break;
148 }
149 Err(err) => {
150 eprintln!("Error: {:?}", err);
151 break;
152 }
153 }
154 }
155
156 clear_status_line();
158
159 agent.shutdown().await;
161
162 if let Some(parent) = history_path.parent() {
164 std::fs::create_dir_all(parent).ok();
165 }
166 rl.save_history(&history_path)?;
167
168 println!("\n👋 Goodbye!\n");
169 Ok(())
170}