Skip to main content

deepseek_rust_cli/tui/
events.rs

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}