1use std::io;
2
3use anyhow::Result;
4use crossterm::{
5 event::{KeyCode, KeyEventKind},
6 style::Stylize,
7};
8
9use crate::{
10 agent::types::{AgentEvent, ApprovalResult},
11 tui::{
12 app::{save_global_history, App},
13 colorizer::{CodeColorizer, StreamColorizer},
14 event_loop::EventLoop,
15 render::write_to_output,
16 utils::{detect_lang_for_result, format_tool_args},
17 },
18};
19
20impl EventLoop {
21 pub fn handle_input(
22 &self,
23 app: &mut App,
24 stdout: &mut io::Stdout,
25 key: crossterm::event::KeyEvent,
26 ) -> Result<bool> {
27 if key.kind != KeyEventKind::Press {
28 return Ok(false);
29 }
30
31 let now = std::time::Instant::now();
32 let is_rapid = if let Some(last) = app.last_key_time {
33 now.duration_since(last) < std::time::Duration::from_millis(5)
34 } else {
35 false
36 };
37 app.last_key_time = Some(now);
38
39 if app.awaiting_approval {
40 if (key.code == KeyCode::Char('c') || key.code == KeyCode::Char('C'))
41 && key
42 .modifiers
43 .contains(crossterm::event::KeyModifiers::CONTROL)
44 {
45 return Ok(true);
46 }
47 match key.code {
48 KeyCode::Char('y') | KeyCode::Char('Y') => {
49 app.awaiting_approval = false;
50 app.current_task = None;
51 write_to_output(stdout, app, "ā
Approved\n".green().to_string())?;
52 let _ = self.app_tx.try_send(ApprovalResult::Yes);
53 }
54 KeyCode::Char('n') | KeyCode::Char('N') => {
55 app.awaiting_approval = false;
56 app.current_task = None;
57 write_to_output(stdout, app, "ā Rejected\n".red().to_string())?;
58 let _ = self.app_tx.try_send(ApprovalResult::No);
59 }
60 KeyCode::Char('a') | KeyCode::Char('A') => {
61 if app.is_path_traversal_warning {
62 return Ok(false);
63 }
64 app.awaiting_approval = false;
65 app.current_task = None;
66 write_to_output(stdout, app, "š”ļø Always Approved\n".blue().to_string())?;
67 let _ = self.app_tx.try_send(ApprovalResult::Always);
68 }
69 _ => {}
70 }
71 return Ok(false);
72 }
73
74 match key.code {
75 KeyCode::Enter => {
76 if is_rapid {
77 let byte_pos = app.cursor_pos.min(app.input.len());
78 app.input.insert(byte_pos, '\n');
79 app.cursor_pos = byte_pos + 1;
80 } else if !app.input.is_empty() {
81 let cmd = app.input.clone();
82 app.reasoning_started = false;
83 app.content_started = false;
84 let separator = format!("\n{}\n", "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā".dim());
85 let prompt = format!("> {}\n", cmd).cyan().to_string();
86 write_to_output(stdout, app, format!("{}{}", separator, prompt))?;
87
88 if cmd == "exit" || cmd == "quit" || cmd == "/exit" || cmd == "/quit" {
89 return Ok(true);
90 }
91 if app.history.last() != Some(&cmd) {
92 app.history.push(cmd.clone());
93 if app.history.len() > 1000 {
94 app.history.remove(0);
95 }
96 save_global_history(&app.history);
97 }
98 app.history_index = None;
99 app.aborted = false;
100 app.queued_commands.push(cmd.clone());
101 let current_run_id = self.run_id.load(std::sync::atomic::Ordering::SeqCst);
102 let _ = self.cmd_tx.try_send((current_run_id, cmd));
103 app.input.clear();
104 app.cursor_pos = 0;
105 }
106 }
107 KeyCode::Char('c') | KeyCode::Char('C')
108 if key
109 .modifiers
110 .contains(crossterm::event::KeyModifiers::CONTROL) =>
111 {
112 return Ok(true);
113 }
114 KeyCode::Char(c) => {
115 let byte_pos = app.cursor_pos.min(app.input.len());
116 app.input.insert(byte_pos, c);
117 app.cursor_pos = byte_pos + c.len_utf8();
118 }
119 KeyCode::Backspace if app.cursor_pos > 0 => {
120 let mut prev = app.cursor_pos - 1;
121 while prev > 0 && !app.input.is_char_boundary(prev) {
122 prev -= 1;
123 }
124 app.input.replace_range(prev..app.cursor_pos, "");
125 app.cursor_pos = prev;
126 }
127 KeyCode::Delete if app.cursor_pos < app.input.len() => {
128 let mut next = app.cursor_pos + 1;
129 while next < app.input.len() && !app.input.is_char_boundary(next) {
130 next += 1;
131 }
132 app.input.replace_range(app.cursor_pos..next, "");
133 }
134 KeyCode::Left if app.cursor_pos > 0 => {
135 let mut prev = app.cursor_pos - 1;
136 while prev > 0 && !app.input.is_char_boundary(prev) {
137 prev -= 1;
138 }
139 app.cursor_pos = prev;
140 }
141 KeyCode::Right if app.cursor_pos < app.input.len() => {
142 let mut next = app.cursor_pos + 1;
143 while next < app.input.len() && !app.input.is_char_boundary(next) {
144 next += 1;
145 }
146 app.cursor_pos = next;
147 }
148 KeyCode::Home => {
149 app.cursor_pos = 0;
150 }
151 KeyCode::End => {
152 app.cursor_pos = app.input.len();
153 }
154 KeyCode::Up => {
155 app.next_history();
156 }
157 KeyCode::Down => {
158 app.prev_history();
159 }
160 _ => {}
161 }
162 Ok(false)
163 }
164
165 pub fn handle_agent_event(
166 &self,
167 app: &mut App,
168 stdout: &mut io::Stdout,
169 agent_event: AgentEvent,
170 full_message: &mut String,
171 reasoning_colorizer: &mut StreamColorizer,
172 content_colorizer: &mut StreamColorizer,
173 ) -> Result<()> {
174 if app.aborted {
175 match &agent_event {
176 AgentEvent::Aborted { token_usage } | AgentEvent::Done { token_usage } => {
177 let flush = reasoning_colorizer.finish();
178 if !flush.is_empty() {
179 write_to_output(stdout, app, flush)?;
180 }
181 let flush = content_colorizer.finish();
182 if !flush.is_empty() {
183 write_to_output(stdout, app, flush)?;
184 }
185 app.token_usage = token_usage.clone();
186 app.finish_task();
187 }
188 _ => return Ok(()),
189 }
190 return Ok(());
191 }
192
193 match agent_event {
194 AgentEvent::Reasoning { content } => {
195 app.start_task("Reasoning".to_string());
196 if !content.is_empty() {
197 if !app.reasoning_started {
198 let separator = "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā".dim().to_string();
199 let header = "š§ Thinking Process:\n".yellow().italic().to_string();
200 write_to_output(stdout, app, format!("\n{}\n{}", separator, header))?;
201 app.reasoning_started = true;
202 app.content_started = false;
203 }
204 let colored = reasoning_colorizer.feed(&content);
205 write_to_output(stdout, app, colored)?;
206 }
207 }
208 AgentEvent::Content { content } => {
209 app.start_task("Generating".to_string());
210 full_message.push_str(&content);
211 if !content.is_empty() {
212 if !app.content_started {
213 let separator = "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā".dim().to_string();
214 let header = "š¬ Response:\n".cyan().bold().to_string();
215 write_to_output(stdout, app, format!("\n{}\n{}", separator, header))?;
216 app.content_started = true;
217 app.reasoning_started = false;
218 }
219 let colored = content_colorizer.feed(&content);
220 write_to_output(stdout, app, colored)?;
221 }
222 }
223 AgentEvent::ToolStart { name, args } => {
224 app.start_task(format!("Tool: {}", name));
225 app.reasoning_started = false;
226 app.content_started = false;
227 let separator = "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā".dim().to_string();
228 let formatted_args = format_tool_args(&name, &args);
229 write_to_output(
230 stdout,
231 app,
232 format!(
233 "\n{}\nš§ {} \n{}\n",
234 separator,
235 name.cyan().bold(),
236 formatted_args.dim()
237 ),
238 )?;
239 }
240 AgentEvent::ToolEnd { name, result } => {
241 app.reasoning_started = false;
242 app.content_started = false;
243 let separator = "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā".dim().to_string();
244 if let Some(ref res) = result {
245 let lang = detect_lang_for_result(&name, res);
246 let max_lines = if name == "read_local_file" || name == "execute_shell_command"
247 {
248 Some(20)
249 } else {
250 Some(10)
251 };
252 let colored_result = CodeColorizer::highlight(res, lang, max_lines);
253 write_to_output(
254 stdout,
255 app,
256 format!(
257 "\n{}\nā
{} executed:\n{}\n",
258 separator,
259 name.green().bold(),
260 colored_result
261 ),
262 )?;
263 } else {
264 write_to_output(
265 stdout,
266 app,
267 format!("\n{}\nā
{} executed.\n", separator, name.green().bold()),
268 )?;
269 }
270 }
271 AgentEvent::ApprovalRequest { name, args } => {
272 app.start_task("Awaiting Approval".to_string());
273 app.awaiting_approval = true;
274 app.reasoning_started = false;
275 app.content_started = false;
276 let separator = "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā".dim().to_string();
277
278 let (display_name, is_traversal) =
279 if let Some(stripped) = name.strip_prefix("path_traversal_warning:") {
280 (stripped.to_string(), true)
281 } else {
282 (name.clone(), false)
283 };
284
285 app.is_path_traversal_warning = is_traversal;
286 let header = if is_traversal {
287 format!(
288 "ā ļø WARNING: Path traversal detected for tool: {}\n",
289 display_name
290 )
291 .red()
292 .bold()
293 .to_string()
294 } else {
295 format!("ā ļø Approval Required for tool: {}\n", display_name)
296 .yellow()
297 .to_string()
298 };
299
300 write_to_output(stdout, app, format!("\n{}\n{}", separator, header))?;
301 write_to_output(
302 stdout,
303 app,
304 format!("Arguments: {}\n", args).dim().to_string(),
305 )?;
306
307 let prompt_str = if is_traversal {
308 "? Press 'y' to approve this path traversal, 'n' to reject. (Always Approve is \
309 disabled for security)\n"
310 .red()
311 .to_string()
312 } else {
313 "? Press 'y' to approve, 'n' to reject, 'a' to allow all.\n"
314 .red()
315 .to_string()
316 };
317 write_to_output(stdout, app, prompt_str)?;
318 }
319 AgentEvent::Error { content } => {
320 let flush = reasoning_colorizer.finish();
321 if !flush.is_empty() {
322 write_to_output(stdout, app, flush)?;
323 }
324 let flush = content_colorizer.finish();
325 if !flush.is_empty() {
326 write_to_output(stdout, app, flush)?;
327 }
328 app.finish_task();
329 app.reasoning_started = false;
330 app.content_started = false;
331 let separator = "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā".dim().to_string();
332 write_to_output(
333 stdout,
334 app,
335 format!("\n{}\nā Error: {}\n", separator, content)
336 .red()
337 .to_string(),
338 )?;
339 }
340 AgentEvent::Done { token_usage } => {
341 let flush = reasoning_colorizer.finish();
342 if !flush.is_empty() {
343 write_to_output(stdout, app, flush)?;
344 }
345 let flush = content_colorizer.finish();
346 if !flush.is_empty() {
347 write_to_output(stdout, app, flush)?;
348 }
349 app.token_usage = token_usage;
350 app.finish_task();
351 app.reasoning_started = false;
352 app.content_started = false;
353 let separator = "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā".dim().to_string();
354 write_to_output(
355 stdout,
356 app,
357 format!("\n{}\nā
Operation Complete\n", separator)
358 .green()
359 .to_string(),
360 )?;
361 full_message.clear();
362 }
363 AgentEvent::Aborted { token_usage } => {
364 let flush = reasoning_colorizer.finish();
365 if !flush.is_empty() {
366 write_to_output(stdout, app, flush)?;
367 }
368 let flush = content_colorizer.finish();
369 if !flush.is_empty() {
370 write_to_output(stdout, app, flush)?;
371 }
372 app.token_usage = token_usage;
373 app.finish_task();
374 app.reasoning_started = false;
375 app.content_started = false;
376 let separator = "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā".dim().to_string();
377 write_to_output(
378 stdout,
379 app,
380 format!("\n{}\nš Operation aborted by user.\n", separator).to_string(),
381 )?;
382 }
383 }
384 Ok(())
385 }
386}