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