1use std::io::{self, BufRead, Write};
6
7use tracing::{info, warn};
8
9use opendev_history::SessionManager;
10use opendev_runtime::AutonomyLevel;
11use opendev_tools_core::ToolRegistry;
12
13use crate::commands::{BuiltinCommands, CommandOutcome};
14use crate::error::ReplError;
15use crate::query_processor::QueryProcessor;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum OperationMode {
20 Normal,
22 Plan,
24}
25
26impl std::fmt::Display for OperationMode {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 OperationMode::Normal => write!(f, "NORMAL"),
30 OperationMode::Plan => write!(f, "PLAN"),
31 }
32 }
33}
34
35pub struct ReplState {
37 pub mode: OperationMode,
39 pub autonomy_level: AutonomyLevel,
41 pub running: bool,
43 pub last_prompt: String,
45 pub last_operation_summary: String,
47 pub last_error: Option<String>,
49 pub last_latency_ms: Option<u64>,
51 pub pending_plan_request: bool,
53 pub messages_cleared: bool,
55 pub compact_requested: bool,
57 pub init_prompt: Option<String>,
59}
60
61impl Default for ReplState {
62 fn default() -> Self {
63 Self {
64 mode: OperationMode::Normal,
65 autonomy_level: AutonomyLevel::default(),
66 running: true,
67 last_prompt: String::new(),
68 last_operation_summary: String::from("—"),
69 last_error: None,
70 last_latency_ms: None,
71 pending_plan_request: false,
72 messages_cleared: false,
73 compact_requested: false,
74 init_prompt: None,
75 }
76 }
77}
78
79pub struct Repl {
84 pub state: ReplState,
86 session_manager: SessionManager,
88 tool_registry: ToolRegistry,
90 query_processor: QueryProcessor,
92 commands: BuiltinCommands,
94}
95
96impl Repl {
97 pub fn new(session_manager: SessionManager, tool_registry: ToolRegistry) -> Self {
99 let query_processor = QueryProcessor::new();
100 let commands = BuiltinCommands::new();
101 Self {
102 state: ReplState::default(),
103 session_manager,
104 tool_registry,
105 query_processor,
106 commands,
107 }
108 }
109
110 pub fn set_initial_message(&mut self, message: String) {
115 self.state.last_prompt = message;
116 }
117
118 pub async fn run(&mut self) -> Result<(), ReplError> {
123 info!("Starting REPL");
124 self.print_welcome();
125
126 if !self.state.last_prompt.is_empty() {
128 let initial = self.state.last_prompt.clone();
129 info!(message = %initial, "Processing initial message");
130 self.process_query(&initial).await?;
131 }
132
133 let stdin = io::stdin();
134 let mut reader = stdin.lock();
135
136 while self.state.running {
137 self.print_prompt();
138
139 let mut line = String::new();
140 match reader.read_line(&mut line) {
141 Ok(0) => {
142 break;
144 }
145 Ok(_) => {}
146 Err(e) => {
147 warn!(error = %e, "Error reading input");
148 return Err(ReplError::Io(e));
149 }
150 }
151
152 let input = line.trim();
153 if input.is_empty() {
154 continue;
155 }
156
157 if input.starts_with('/') {
158 self.handle_command(input);
159
160 if self.state.messages_cleared {
162 self.state.messages_cleared = false;
163 if let Some(session) = self.session_manager.current_session_mut() {
164 session.messages.clear();
165 }
166 }
167 if self.state.compact_requested {
168 self.state.compact_requested = false;
169 info!("Compact flag consumed; compaction will run on next query.");
171 }
172
173 if let Some(query) = self.state.init_prompt.take() {
174 self.state.last_prompt = query.clone();
175 self.process_query(&query).await?;
176 }
177
178 continue;
179 }
180
181 self.state.last_prompt = input.to_string();
182 self.process_query(input).await?;
183 }
184
185 self.cleanup();
186 Ok(())
187 }
188
189 fn print_welcome(&self) {
191 println!("OpenDev -- AI-powered coding assistant");
192 println!("Type /help for commands, /exit to quit.");
193 println!(
194 "Mode: {} | Autonomy: {}",
195 self.state.mode, self.state.autonomy_level
196 );
197 println!();
198 }
199
200 fn print_prompt(&self) {
202 let mode_indicator = match self.state.mode {
203 OperationMode::Normal => ">",
204 OperationMode::Plan => "plan>",
205 };
206 print!("{} ", mode_indicator);
207 let _ = io::stdout().flush();
208 }
209
210 fn handle_command(&mut self, input: &str) {
212 let parts: Vec<&str> = input.splitn(2, ' ').collect();
213 let cmd = parts[0].to_lowercase();
214 let args = parts.get(1).copied().unwrap_or("");
215
216 match self.commands.dispatch(&cmd, args, &mut self.state) {
217 CommandOutcome::Handled => {}
218 CommandOutcome::Exit => {
219 self.state.running = false;
220 }
221 CommandOutcome::Unknown => {
222 eprintln!("Unknown command: {}", cmd);
223 eprintln!("Type /help for available commands");
224 }
225 }
226 }
227
228 async fn process_query(&mut self, query: &str) -> Result<(), ReplError> {
230 let plan_requested = self.state.pending_plan_request;
231 if plan_requested {
232 self.state.pending_plan_request = false;
233 }
234
235 let result = self
236 .query_processor
237 .process(
238 query,
239 &mut self.session_manager,
240 &self.tool_registry,
241 plan_requested,
242 )
243 .await?;
244
245 self.state.last_operation_summary = result.operation_summary;
246 self.state.last_error = result.error;
247 self.state.last_latency_ms = result.latency_ms;
248
249 if !result.content.is_empty() {
251 println!("{}", result.content);
252 }
253
254 Ok(())
255 }
256
257 fn cleanup(&mut self) {
259 info!("Cleaning up REPL resources");
260
261 self.session_manager
263 .set_metadata("mode", &self.state.mode.to_string());
264 self.session_manager
265 .set_metadata("autonomy_level", &self.state.autonomy_level.to_string());
266
267 if let Err(e) = self.session_manager.save_current() {
268 warn!(error = %e, "Failed to save session on exit");
269 }
270 println!("Goodbye!");
271 }
272}
273
274#[cfg(test)]
275#[path = "repl_tests.rs"]
276mod tests;