mixtape_cli/repl/
mod.rs

1//! Interactive REPL for mixtape agents
2
3mod 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
31/// Run an interactive REPL for the agent
32///
33/// This provides a command-line interface with:
34/// - Up/down arrow history
35/// - Ctrl+R reverse search
36/// - Multi-line input support
37/// - Special commands (!shell, /help, etc)
38/// - Automatic session management
39/// - Rich tool presentation with CLIPresenter formatting
40/// - Tool approval prompts (when using Registry approval mode)
41///
42/// # Errors
43///
44/// Returns `CliError` which can be:
45/// - `Agent` - Agent execution errors
46/// - `Session` - Session storage errors
47/// - `Readline` - Input/readline errors
48/// - `Io` - Filesystem errors (history loading/saving)
49///
50/// # Example
51/// ```ignore
52/// use mixtape_core::{Agent, ClaudeSonnet4_5};
53/// use mixtape_cli::run_cli;
54///
55/// let agent = Agent::builder()
56///     .bedrock(ClaudeSonnet4_5)
57///     .build()
58///     .await?;
59///
60/// run_cli(agent).await?;
61/// ```
62pub async fn run_cli(agent: Agent) -> Result<(), CliError> {
63    let agent = Arc::new(agent);
64
65    // Add presentation hook for rich tool display
66    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    // Bind Ctrl-J to insert newline instead of submitting
79    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    // Load history
86    if history_path.exists() {
87        rl.load_history(&history_path).ok();
88    }
89
90    loop {
91        // Update persistent status line at bottom of terminal
92        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                // Handle special commands
109                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                // Show thinking indicator
117                print!("\n\x1b[2m⋯ thinking\x1b[0m");
118                let _ = std::io::stdout().flush();
119
120                // Regular agent interaction
121                match agent.run(line).await {
122                    Ok(response) => {
123                        // Clear the thinking indicator line and print response
124                        print!("\r\x1b[2K");
125                        println!("\n{}\n", response);
126
127                        // Update status line with new context usage
128                        update_status_line(&agent);
129                    }
130                    Err(e) => {
131                        // Clear the thinking indicator line and print error
132                        print!("\r\x1b[2K");
133                        eprintln!("❌ Error: {}\n", e);
134
135                        // Update status line even after error
136                        update_status_line(&agent);
137                    }
138                }
139            }
140            Err(ReadlineError::Interrupted) => {
141                // Ctrl+C - just continue
142                println!("^C");
143                continue;
144            }
145            Err(ReadlineError::Eof) => {
146                // Ctrl+D - exit
147                break;
148            }
149            Err(err) => {
150                eprintln!("Error: {:?}", err);
151                break;
152            }
153        }
154    }
155
156    // Clear persistent status line on exit
157    clear_status_line();
158
159    // Gracefully shutdown agent (disconnects MCP servers)
160    agent.shutdown().await;
161
162    // Save history
163    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}