1mod approval;
4mod commands;
5mod core;
6mod formatter;
7mod input;
8mod presentation;
9mod spinner;
10mod status;
11
12use crate::error::CliError;
13use commands::{handle_special_command, SpecialCommandResult};
14use core::{input_prompt, print_input_padding, print_welcome, reset_input_style};
15use input::InputStyleHelper;
16use rustyline::config::Config;
17use rustyline::error::ReadlineError;
18use rustyline::{Cmd, Editor, KeyEvent};
19use spinner::Spinner;
20use status::{clear_status_line, update_status_line};
21
22use mixtape_core::{Agent, AgentError, AgentEvent, AgentResponse, AuthorizationResponse};
23use serde_json::Value;
24use std::sync::{Arc, Mutex};
25use tokio::sync::mpsc;
26
27type PermissionData = (String, String, String, Value);
29
30pub use approval::{
31 print_confirmation, prompt_for_approval, read_input, ApprovalPrompter, DefaultPrompter,
32 PermissionRequest, SimplePrompter,
33};
34pub use commands::Verbosity;
35pub use presentation::{
36 indent_lines, new_event_queue, print_result_separator, print_tool_footer, print_tool_header,
37 EventPresenter, PresentationHook,
38};
39
40pub async fn run_cli(agent: Agent) -> Result<(), CliError> {
72 let agent = Arc::new(agent);
73
74 let event_queue = new_event_queue();
76
77 agent.add_hook(PresentationHook::new(Arc::clone(&event_queue)));
79
80 let verbosity = Arc::new(Mutex::new(Verbosity::Normal));
82 let presenter = EventPresenter::new(
83 Arc::clone(&agent),
84 Arc::clone(&verbosity),
85 Arc::clone(&event_queue),
86 );
87
88 let (perm_tx, perm_rx) = mpsc::unbounded_channel::<PermissionData>();
90 let perm_rx = Arc::new(tokio::sync::Mutex::new(perm_rx));
91 agent.add_hook(move |event: &AgentEvent| {
92 if let AgentEvent::PermissionRequired {
93 proposal_id,
94 tool_name,
95 params_hash,
96 params,
97 ..
98 } = event
99 {
100 let _ = perm_tx.send((
101 proposal_id.clone(),
102 tool_name.clone(),
103 params_hash.clone(),
104 params.clone(),
105 ));
106 }
107 });
108
109 print_welcome(&agent).await?;
110
111 let config = Config::default();
112 let mut rl: Editor<InputStyleHelper, rustyline::history::DefaultHistory> =
113 Editor::with_config(config)?;
114 rl.set_helper(Some(InputStyleHelper));
115
116 rl.bind_sequence(KeyEvent::ctrl('J'), Cmd::Newline);
118
119 let history_path = dirs::cache_dir()
120 .map(|p| p.join("mixtape/history.txt"))
121 .unwrap_or_else(|| ".mixtape/history.txt".into());
122
123 if history_path.exists() {
125 rl.load_history(&history_path).ok();
126 }
127
128 loop {
129 update_status_line(&agent);
131
132 print_input_padding();
133 let readline = rl.readline(input_prompt());
134 reset_input_style();
135
136 match readline {
137 Ok(line) => {
138 let line = line.trim();
139
140 if line.is_empty() {
141 continue;
142 }
143
144 rl.add_history_entry(line)?;
145
146 if let Some(result) = handle_special_command(line, &agent, &verbosity).await? {
148 match result {
149 SpecialCommandResult::Exit => break,
150 SpecialCommandResult::Continue => continue,
151 }
152 }
153
154 println!(); let spinner = Spinner::new("thinking");
157
158 let result = run_with_permissions(
160 Arc::clone(&agent),
161 line.to_string(),
162 spinner,
163 Arc::clone(&perm_rx),
164 &presenter,
165 )
166 .await;
167
168 match result {
169 Ok(response) => {
170 println!("\n{}\n", response);
171 update_status_line(&agent);
172 }
173 Err(e) => {
174 eprintln!("ā Error: {}\n", e);
175 update_status_line(&agent);
176 }
177 }
178 }
179 Err(ReadlineError::Interrupted) => {
180 println!("^C");
182 continue;
183 }
184 Err(ReadlineError::Eof) => {
185 break;
187 }
188 Err(err) => {
189 eprintln!("Error: {:?}", err);
190 break;
191 }
192 }
193 }
194
195 clear_status_line();
197
198 agent.shutdown().await;
200
201 if let Some(parent) = history_path.parent() {
203 std::fs::create_dir_all(parent).ok();
204 }
205 rl.save_history(&history_path)?;
206
207 println!("\nš Goodbye!\n");
208 Ok(())
209}
210
211async fn run_with_permissions<F: formatter::ToolFormatter>(
213 agent: Arc<Agent>,
214 input: String,
215 spinner: Spinner,
216 perm_rx: Arc<tokio::sync::Mutex<mpsc::UnboundedReceiver<PermissionData>>>,
217 presenter: &EventPresenter<F>,
218) -> Result<AgentResponse, AgentError> {
219 let agent_clone = Arc::clone(&agent);
221 let mut handle = tokio::spawn(async move { agent_clone.run(&input).await });
222
223 let mut rx = perm_rx.lock().await;
225
226 let mut spinner = Some(spinner);
228
229 loop {
231 tokio::select! {
232 biased; Some((proposal_id, tool_name, params_hash, params)) = rx.recv() => {
236 if let Some(s) = spinner.take() {
238 s.stop().await;
239 }
240
241 presenter.flush();
243
244 let formatted_display =
246 agent.format_tool_input(&tool_name, ¶ms, mixtape_core::Display::Cli);
247
248 let request = PermissionRequest {
249 tool_name: tool_name.clone(),
250 tool_use_id: proposal_id.clone(),
251 params_hash: params_hash.clone(),
252 formatted_display,
253 };
254
255 let response = approval::prompt_for_approval(&request);
256
257 match response {
258 AuthorizationResponse::Once => {
259 agent.authorize_once(&proposal_id).await.ok();
260 }
261 AuthorizationResponse::Trust { grant } => {
262 agent
263 .respond_to_authorization(
264 &proposal_id,
265 AuthorizationResponse::Trust { grant },
266 )
267 .await
268 .ok();
269 }
270 AuthorizationResponse::Deny { reason } => {
271 agent.deny_authorization(&proposal_id, reason).await.ok();
272 }
273 }
274
275 spinner = Some(Spinner::new("thinking"));
277 }
278
279 result = &mut handle => {
281 if let Some(s) = spinner.take() {
283 s.stop().await;
284 }
285 presenter.flush();
287 return result.unwrap_or_else(|e| Err(AgentError::Tool(e.to_string().into())));
288 }
289 }
290 }
291}