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        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}