Skip to main content

dot/tui/
input.rs

1use std::time::{Duration, Instant};
2
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
4
5use crate::tui::app::{App, AppMode};
6use crate::tui::widgets::ThinkingLevel;
7
8pub enum InputAction {
9    AnswerQuestion(String),
10    AnswerPermission(String),
11    None,
12    SendMessage(String),
13    Quit,
14    CancelStream,
15    ScrollUp(u16),
16    ScrollDown(u16),
17    ScrollToTop,
18    ScrollToBottom,
19    ClearConversation,
20    NewConversation,
21    OpenModelSelector,
22    OpenAgentSelector,
23    OpenThinkingSelector,
24    OpenSessionSelector,
25    SelectModel { provider: String, model: String },
26    SelectAgent { name: String },
27    ResumeSession { id: String },
28    SetThinkingLevel(u32),
29    ToggleThinking,
30    CycleThinkingLevel,
31    TruncateToMessage(usize),
32    ForkFromMessage(usize),
33}
34
35pub fn handle_paste(app: &mut App, text: String) -> InputAction {
36    if app.vim_mode && app.mode != AppMode::Insert {
37        return InputAction::None;
38    }
39
40    let trimmed = text.trim_end_matches('\n').to_string();
41    if trimmed.is_empty() {
42        return InputAction::None;
43    }
44
45    if crate::tui::app::is_image_path(trimmed.trim()) {
46        let path = trimmed.trim().trim_matches('"').trim_matches('\'');
47        match app.add_image_attachment(path) {
48            Ok(()) => {}
49            Err(e) => app.error_message = Some(e),
50        }
51        return InputAction::None;
52    }
53
54    app.handle_paste(trimmed);
55    InputAction::None
56}
57
58pub fn handle_key(app: &mut App, key: KeyEvent) -> InputAction {
59    if app.selection.anchor.is_some() {
60        app.selection.clear();
61    }
62
63    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
64        if app.model_selector.visible {
65            app.model_selector.close();
66            return InputAction::None;
67        }
68        if app.agent_selector.visible {
69            app.agent_selector.close();
70            return InputAction::None;
71        }
72        if app.command_palette.visible {
73            app.command_palette.close();
74            return InputAction::None;
75        }
76        if app.thinking_selector.visible {
77            app.thinking_selector.close();
78            return InputAction::None;
79        }
80        if app.session_selector.visible {
81            app.session_selector.close();
82            return InputAction::None;
83        }
84        if app.help_popup.visible {
85            app.help_popup.close();
86            return InputAction::None;
87        }
88        if app.is_streaming {
89            return InputAction::CancelStream;
90        }
91        if !app.input.is_empty() || !app.attachments.is_empty() {
92            app.input.clear();
93            app.cursor_pos = 0;
94            app.paste_blocks.clear();
95            app.attachments.clear();
96            return InputAction::None;
97        }
98        return InputAction::Quit;
99    }
100
101    if key.code == KeyCode::Esc && app.is_streaming {
102        let now = Instant::now();
103        if let Some(hint_until) = app.esc_hint_until {
104            if now < hint_until {
105                app.esc_hint_until = None;
106                app.last_escape_time = None;
107                return InputAction::CancelStream;
108            }
109        }
110        app.esc_hint_until = Some(now + Duration::from_secs(3));
111        app.last_escape_time = Some(now);
112        return InputAction::None;
113    }
114
115    if app.model_selector.visible {
116        return handle_model_selector(app, key);
117    }
118
119    if app.agent_selector.visible {
120        return handle_agent_selector(app, key);
121    }
122
123    if app.thinking_selector.visible {
124        return handle_thinking_selector(app, key);
125    }
126
127    if app.session_selector.visible {
128        return handle_session_selector(app, key);
129    }
130
131    if app.help_popup.visible {
132        if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
133            app.help_popup.close();
134        }
135        return InputAction::None;
136    }
137
138    if app.pending_question.is_some() {
139        return handle_question_popup(app, key);
140    }
141
142    if app.pending_permission.is_some() {
143        return handle_permission_popup(app, key);
144    }
145
146    if app.context_menu.visible {
147        return handle_context_menu(app, key);
148    }
149
150    if app.command_palette.visible {
151        return handle_command_palette(app, key);
152    }
153
154    if app.vim_mode {
155        match app.mode {
156            AppMode::Normal => handle_normal(app, key),
157            AppMode::Insert => handle_insert(app, key),
158        }
159    } else {
160        handle_simple(app, key)
161    }
162}
163
164fn handle_model_selector(app: &mut App, key: KeyEvent) -> InputAction {
165    match key.code {
166        KeyCode::Esc => {
167            app.model_selector.close();
168            InputAction::None
169        }
170        KeyCode::Up => {
171            app.model_selector.up();
172            InputAction::None
173        }
174        KeyCode::Down | KeyCode::Tab => {
175            app.model_selector.down();
176            InputAction::None
177        }
178        KeyCode::Enter => {
179            if let Some(entry) = app.model_selector.confirm() {
180                app.model_name = entry.model.clone();
181                app.provider_name = entry.provider.clone();
182                InputAction::SelectModel {
183                    provider: entry.provider,
184                    model: entry.model,
185                }
186            } else {
187                InputAction::None
188            }
189        }
190        KeyCode::Backspace => {
191            app.model_selector.query.pop();
192            app.model_selector.apply_filter();
193            InputAction::None
194        }
195        KeyCode::Char(c) => {
196            app.model_selector.query.push(c);
197            app.model_selector.apply_filter();
198            InputAction::None
199        }
200        _ => InputAction::None,
201    }
202}
203
204fn handle_agent_selector(app: &mut App, key: KeyEvent) -> InputAction {
205    match key.code {
206        KeyCode::Esc => {
207            app.agent_selector.close();
208            InputAction::None
209        }
210        KeyCode::Up => {
211            app.agent_selector.up();
212            InputAction::None
213        }
214        KeyCode::Down | KeyCode::Tab => {
215            app.agent_selector.down();
216            InputAction::None
217        }
218        KeyCode::Enter => {
219            if let Some(entry) = app.agent_selector.confirm() {
220                app.agent_name = entry.name.clone();
221                InputAction::SelectAgent { name: entry.name }
222            } else {
223                InputAction::None
224            }
225        }
226        _ => InputAction::None,
227    }
228}
229
230fn handle_thinking_selector(app: &mut App, key: KeyEvent) -> InputAction {
231    match key.code {
232        KeyCode::Esc => {
233            app.thinking_selector.close();
234            InputAction::None
235        }
236        KeyCode::Up => {
237            app.thinking_selector.up();
238            InputAction::None
239        }
240        KeyCode::Down | KeyCode::Tab => {
241            app.thinking_selector.down();
242            InputAction::None
243        }
244        KeyCode::Enter => {
245            if let Some(level) = app.thinking_selector.confirm() {
246                let budget = level.budget_tokens();
247                app.thinking_budget = budget;
248                InputAction::SetThinkingLevel(budget)
249            } else {
250                InputAction::None
251            }
252        }
253        _ => InputAction::None,
254    }
255}
256
257fn handle_session_selector(app: &mut App, key: KeyEvent) -> InputAction {
258    match key.code {
259        KeyCode::Esc => {
260            app.session_selector.close();
261            InputAction::None
262        }
263        KeyCode::Up => {
264            app.session_selector.up();
265            InputAction::None
266        }
267        KeyCode::Down | KeyCode::Tab => {
268            app.session_selector.down();
269            InputAction::None
270        }
271        KeyCode::Enter => {
272            if let Some(id) = app.session_selector.confirm() {
273                InputAction::ResumeSession { id }
274            } else {
275                InputAction::None
276            }
277        }
278        KeyCode::Backspace => {
279            app.session_selector.query.pop();
280            app.session_selector.apply_filter();
281            InputAction::None
282        }
283        KeyCode::Char(c) => {
284            app.session_selector.query.push(c);
285            app.session_selector.apply_filter();
286            InputAction::None
287        }
288        _ => InputAction::None,
289    }
290}
291
292fn handle_command_palette(app: &mut App, key: KeyEvent) -> InputAction {
293    match key.code {
294        KeyCode::Esc => {
295            app.command_palette.close();
296            InputAction::None
297        }
298        KeyCode::Up => {
299            app.command_palette.up();
300            InputAction::None
301        }
302        KeyCode::Down | KeyCode::Tab => {
303            app.command_palette.down();
304            InputAction::None
305        }
306        KeyCode::Enter => {
307            if let Some(cmd_name) = app.command_palette.confirm() {
308                app.input.clear();
309                app.cursor_pos = 0;
310                execute_command(app, cmd_name)
311            } else {
312                InputAction::None
313            }
314        }
315        KeyCode::Backspace => {
316            app.delete_char_before();
317            if app.input.is_empty() || !app.input.starts_with('/') {
318                app.command_palette.close();
319            } else {
320                app.command_palette.update_filter(&app.input);
321            }
322            InputAction::None
323        }
324        KeyCode::Char(c) => {
325            app.insert_char(c);
326            app.command_palette.update_filter(&app.input);
327            if app.command_palette.filtered.is_empty() {
328                app.command_palette.close();
329            }
330            InputAction::None
331        }
332        _ => InputAction::None,
333    }
334}
335
336fn execute_command(app: &mut App, cmd_name: &str) -> InputAction {
337    match cmd_name {
338        "model" => InputAction::OpenModelSelector,
339        "agent" => InputAction::OpenAgentSelector,
340        "thinking" => InputAction::OpenThinkingSelector,
341        "sessions" => InputAction::OpenSessionSelector,
342        "new" => InputAction::NewConversation,
343        "clear" => {
344            app.clear_conversation();
345            InputAction::None
346        }
347        "help" => {
348            app.help_popup.open();
349            InputAction::None
350        }
351        _ => InputAction::None,
352    }
353}
354
355fn handle_context_menu(app: &mut App, key: KeyEvent) -> InputAction {
356    match key.code {
357        KeyCode::Esc => {
358            app.context_menu.close();
359            InputAction::None
360        }
361        KeyCode::Up => {
362            app.context_menu.up();
363            InputAction::None
364        }
365        KeyCode::Down | KeyCode::Tab => {
366            app.context_menu.down();
367            InputAction::None
368        }
369        KeyCode::Enter => {
370            if let Some((action, msg_idx)) = app.context_menu.confirm() {
371                match action {
372                    0 => InputAction::TruncateToMessage(msg_idx),
373                    1 => InputAction::ForkFromMessage(msg_idx),
374                    _ => InputAction::None,
375                }
376            } else {
377                InputAction::None
378            }
379        }
380        _ => InputAction::None,
381    }
382}
383
384fn handle_normal(app: &mut App, key: KeyEvent) -> InputAction {
385    match key.code {
386        KeyCode::Char('q') => InputAction::Quit,
387        KeyCode::Char('i') | KeyCode::Enter => {
388            app.mode = AppMode::Insert;
389            InputAction::None
390        }
391        KeyCode::Char('j') | KeyCode::Down => InputAction::ScrollDown(1),
392        KeyCode::Char('k') | KeyCode::Up => InputAction::ScrollUp(1),
393        KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
394            InputAction::ScrollDown(10)
395        }
396        KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
397            InputAction::ScrollUp(10)
398        }
399        KeyCode::Char('g') => InputAction::ScrollToTop,
400        KeyCode::Char('G') => InputAction::ScrollToBottom,
401        KeyCode::PageUp => InputAction::ScrollUp(20),
402        KeyCode::PageDown => InputAction::ScrollDown(20),
403        KeyCode::Char('l') if key.modifiers.contains(KeyModifiers::CONTROL) => {
404            InputAction::ClearConversation
405        }
406        KeyCode::Tab => InputAction::OpenAgentSelector,
407        KeyCode::Char('t') => InputAction::ToggleThinking,
408        _ => InputAction::None,
409    }
410}
411
412fn handle_insert(app: &mut App, key: KeyEvent) -> InputAction {
413    if key.modifiers.contains(KeyModifiers::CONTROL) {
414        match key.code {
415            KeyCode::Char('t') => return InputAction::CycleThinkingLevel,
416            KeyCode::Char('a') => {
417                app.move_cursor_home();
418                return InputAction::None;
419            }
420            KeyCode::Char('e') => {
421                app.move_cursor_end();
422                return InputAction::None;
423            }
424            KeyCode::Char('w') => {
425                app.delete_word_before();
426                return InputAction::None;
427            }
428            KeyCode::Char('k') => {
429                app.delete_to_end();
430                return InputAction::None;
431            }
432            KeyCode::Char('u') => {
433                app.delete_to_start();
434                return InputAction::None;
435            }
436            _ => {}
437        }
438    }
439
440    if app.is_streaming {
441        return match key.code {
442            KeyCode::Esc => {
443                app.mode = AppMode::Normal;
444                InputAction::None
445            }
446            KeyCode::Enter => handle_send(app),
447            KeyCode::Char(c) => handle_char_input(app, c),
448            KeyCode::Backspace => handle_backspace(app),
449            KeyCode::Left => {
450                app.move_cursor_left();
451                InputAction::None
452            }
453            KeyCode::Right => {
454                app.move_cursor_right();
455                InputAction::None
456            }
457            KeyCode::Home => {
458                app.move_cursor_home();
459                InputAction::None
460            }
461            KeyCode::End => {
462                app.move_cursor_end();
463                InputAction::None
464            }
465            _ => InputAction::None,
466        };
467    }
468
469    match key.code {
470        KeyCode::Esc => {
471            app.mode = AppMode::Normal;
472            InputAction::None
473        }
474        KeyCode::Enter => handle_send(app),
475        KeyCode::Char(c) => handle_char_input(app, c),
476        KeyCode::Backspace => handle_backspace(app),
477        KeyCode::Left => {
478            app.move_cursor_left();
479            InputAction::None
480        }
481        KeyCode::Right => {
482            app.move_cursor_right();
483            InputAction::None
484        }
485        KeyCode::Home => {
486            app.move_cursor_home();
487            InputAction::None
488        }
489        KeyCode::End => {
490            app.move_cursor_end();
491            InputAction::None
492        }
493        _ => InputAction::None,
494    }
495}
496
497fn handle_simple(app: &mut App, key: KeyEvent) -> InputAction {
498    if key.modifiers.contains(KeyModifiers::CONTROL) {
499        match key.code {
500            KeyCode::Char('t') => return InputAction::CycleThinkingLevel,
501            KeyCode::Char('a') => {
502                app.move_cursor_home();
503                return InputAction::None;
504            }
505            KeyCode::Char('e') => {
506                app.move_cursor_end();
507                return InputAction::None;
508            }
509            KeyCode::Char('w') => {
510                app.delete_word_before();
511                return InputAction::None;
512            }
513            KeyCode::Char('k') => {
514                app.delete_to_end();
515                return InputAction::None;
516            }
517            KeyCode::Char('u') => {
518                app.delete_to_start();
519                return InputAction::None;
520            }
521            KeyCode::Char('d') => return InputAction::ScrollDown(10),
522            _ => {}
523        }
524    }
525
526    if app.is_streaming {
527        return match key.code {
528            KeyCode::Up => InputAction::ScrollUp(1),
529            KeyCode::Down => InputAction::ScrollDown(1),
530            KeyCode::PageUp => InputAction::ScrollUp(20),
531            KeyCode::PageDown => InputAction::ScrollDown(20),
532            KeyCode::Enter => handle_send(app),
533            KeyCode::Char(c) => handle_char_input(app, c),
534            KeyCode::Backspace => handle_backspace(app),
535            KeyCode::Left => {
536                app.move_cursor_left();
537                InputAction::None
538            }
539            KeyCode::Right => {
540                app.move_cursor_right();
541                InputAction::None
542            }
543            KeyCode::Home => {
544                app.move_cursor_home();
545                InputAction::None
546            }
547            KeyCode::End => {
548                app.move_cursor_end();
549                InputAction::None
550            }
551            _ => InputAction::None,
552        };
553    }
554
555    match key.code {
556        KeyCode::Esc => InputAction::None,
557        KeyCode::Enter => handle_send(app),
558        KeyCode::Up => InputAction::ScrollUp(1),
559        KeyCode::Down => InputAction::ScrollDown(1),
560        KeyCode::PageUp => InputAction::ScrollUp(20),
561        KeyCode::PageDown => InputAction::ScrollDown(20),
562        KeyCode::Tab => InputAction::OpenAgentSelector,
563        KeyCode::Char(c) => handle_char_input(app, c),
564        KeyCode::Backspace => handle_backspace(app),
565        KeyCode::Left => {
566            app.move_cursor_left();
567            InputAction::None
568        }
569        KeyCode::Right => {
570            app.move_cursor_right();
571            InputAction::None
572        }
573        KeyCode::Home => {
574            app.move_cursor_home();
575            InputAction::None
576        }
577        KeyCode::End => {
578            app.move_cursor_end();
579            InputAction::None
580        }
581        _ => InputAction::None,
582    }
583}
584
585fn handle_send(app: &mut App) -> InputAction {
586    parse_at_references(app);
587    if app.is_streaming {
588        app.queue_input();
589        return InputAction::None;
590    }
591    if let Some(msg) = app.take_input() {
592        InputAction::SendMessage(msg)
593    } else {
594        InputAction::None
595    }
596}
597
598fn handle_char_input(app: &mut App, c: char) -> InputAction {
599    app.insert_char(c);
600    if app.input == "/" {
601        app.command_palette.open(&app.input);
602    } else if app.input.starts_with('/') && app.command_palette.visible {
603        app.command_palette.update_filter(&app.input);
604        if app.command_palette.filtered.is_empty() {
605            app.command_palette.close();
606        }
607    }
608    InputAction::None
609}
610
611fn handle_backspace(app: &mut App) -> InputAction {
612    if let Some(pb_idx) = app.paste_block_at_cursor() {
613        app.delete_paste_block(pb_idx);
614    } else {
615        app.delete_char_before();
616    }
617    if app.input.starts_with('/') && !app.input.is_empty() {
618        if !app.command_palette.visible {
619            app.command_palette.open(&app.input);
620        } else {
621            app.command_palette.update_filter(&app.input);
622        }
623    } else if app.command_palette.visible {
624        app.command_palette.close();
625    }
626    InputAction::None
627}
628
629fn rect_contains(r: ratatui::layout::Rect, col: u16, row: u16) -> bool {
630    col >= r.x && col < r.x + r.width && row >= r.y && row < r.y + r.height
631}
632
633pub fn handle_mouse(app: &mut App, mouse: MouseEvent) -> InputAction {
634    let col = mouse.column;
635    let row = mouse.row;
636
637    match mouse.kind {
638        MouseEventKind::ScrollUp => {
639            app.selection.clear();
640            if app.model_selector.visible
641                && let Some(popup) = app.layout.model_selector
642                && rect_contains(popup, col, row)
643            {
644                app.model_selector.up();
645                return InputAction::None;
646            }
647            InputAction::ScrollUp(1)
648        }
649        MouseEventKind::ScrollDown => {
650            app.selection.clear();
651            if app.model_selector.visible
652                && let Some(popup) = app.layout.model_selector
653                && rect_contains(popup, col, row)
654            {
655                app.model_selector.down();
656                return InputAction::None;
657            }
658            InputAction::ScrollDown(1)
659        }
660        MouseEventKind::Down(MouseButton::Left) => {
661            if app.selection.anchor.is_some() && !app.selection.active {
662                app.selection.clear();
663            }
664
665            if app.context_menu.visible {
666                if let Some(popup) = app.layout.context_menu {
667                    if rect_contains(popup, col, row) {
668                        let relative_row = row.saturating_sub(popup.y + 1) as usize;
669                        app.context_menu.selected = relative_row.min(1);
670                        if let Some((action, msg_idx)) = app.context_menu.confirm() {
671                            return match action {
672                                0 => InputAction::TruncateToMessage(msg_idx),
673                                1 => InputAction::ForkFromMessage(msg_idx),
674                                _ => InputAction::None,
675                            };
676                        }
677                    }
678                }
679                app.context_menu.close();
680                return InputAction::None;
681            }
682
683            if app.model_selector.visible
684                && let Some(popup) = app.layout.model_selector
685            {
686                if !rect_contains(popup, col, row) {
687                    app.model_selector.close();
688                }
689                return InputAction::None;
690            }
691
692            if app.agent_selector.visible
693                && let Some(popup) = app.layout.agent_selector
694            {
695                if !rect_contains(popup, col, row) {
696                    app.agent_selector.close();
697                }
698                return InputAction::None;
699            }
700
701            if app.help_popup.visible
702                && let Some(popup) = app.layout.help_popup
703            {
704                if !rect_contains(popup, col, row) {
705                    app.help_popup.close();
706                }
707                return InputAction::None;
708            }
709
710            if app.thinking_selector.visible
711                && let Some(popup) = app.layout.thinking_selector
712                && rect_contains(popup, col, row)
713            {
714                let relative_row = row.saturating_sub(popup.y + 1) as usize;
715                if relative_row < ThinkingLevel::all().len() {
716                    app.thinking_selector.selected = relative_row;
717                    if let Some(level) = app.thinking_selector.confirm() {
718                        let budget = level.budget_tokens();
719                        app.thinking_budget = budget;
720                        return InputAction::SetThinkingLevel(budget);
721                    }
722                }
723            } else if app.thinking_selector.visible
724                && let Some(popup) = app.layout.thinking_selector
725            {
726                if !rect_contains(popup, col, row) {
727                    app.thinking_selector.close();
728                }
729                return InputAction::None;
730            }
731
732            if app.session_selector.visible
733                && let Some(popup) = app.layout.session_selector
734                && !rect_contains(popup, col, row)
735            {
736                app.session_selector.close();
737                return InputAction::None;
738            }
739
740            if app.command_palette.visible
741                && let Some(popup) = app.layout.command_palette
742            {
743                if rect_contains(popup, col, row) {
744                    let relative_row = row.saturating_sub(popup.y) as usize;
745                    if relative_row < app.command_palette.filtered.len() {
746                        app.command_palette.selected = relative_row;
747                        if let Some(cmd_name) = app.command_palette.confirm() {
748                            app.input.clear();
749                            app.cursor_pos = 0;
750                            return execute_command(app, cmd_name);
751                        }
752                    }
753                    return InputAction::None;
754                } else {
755                    app.command_palette.close();
756                    return InputAction::None;
757                }
758            }
759
760            if rect_contains(app.layout.input, col, row) {
761                if app.vim_mode {
762                    app.mode = AppMode::Insert;
763                }
764                let inner_x = col.saturating_sub(app.layout.input.x + 3);
765                let inner_y = row.saturating_sub(app.layout.input.y + 1);
766                let target_offset =
767                    compute_click_cursor_pos(&app.input, inner_x as usize, inner_y as usize);
768                app.cursor_pos = target_offset;
769                InputAction::None
770            } else if rect_contains(app.layout.messages, col, row) {
771                let content_y = app.layout.messages.y + 1;
772                if row >= content_y {
773                    let content_col = col.saturating_sub(app.layout.messages.x);
774                    let content_row = row - content_y;
775                    let visual_row = app.scroll_offset + content_row;
776                    app.selection.start(content_col, visual_row);
777                }
778                if app.vim_mode && app.mode == AppMode::Insert && app.input.is_empty() {
779                    app.mode = AppMode::Normal;
780                }
781                InputAction::None
782            } else {
783                InputAction::None
784            }
785        }
786        MouseEventKind::Drag(MouseButton::Left) => {
787            if app.selection.active {
788                let content_y = app.layout.messages.y + 1;
789                let content_height = app.layout.messages.height.saturating_sub(1);
790                let content_col = col.saturating_sub(app.layout.messages.x);
791                let content_row = if row >= content_y {
792                    (row - content_y).min(content_height.saturating_sub(1))
793                } else {
794                    0
795                };
796                let visual_row = app.scroll_offset + content_row;
797                app.selection.update(content_col, visual_row);
798            }
799            InputAction::None
800        }
801        MouseEventKind::Up(MouseButton::Left) => {
802            if app.selection.active {
803                let content_y = app.layout.messages.y + 1;
804                let content_height = app.layout.messages.height.saturating_sub(1);
805                let content_col = col.saturating_sub(app.layout.messages.x);
806                let content_row = if row >= content_y {
807                    (row - content_y).min(content_height.saturating_sub(1))
808                } else {
809                    0
810                };
811                let visual_row = app.scroll_offset + content_row;
812                app.selection.update(content_col, visual_row);
813                app.selection.active = false;
814                if !app.selection.is_empty_selection() {
815                    if let Some(text) = app.extract_selected_text()
816                        && !text.trim().is_empty()
817                    {
818                        crate::tui::app::copy_to_clipboard(&text);
819                    }
820                } else {
821                    app.selection.clear();
822                }
823            }
824            InputAction::None
825        }
826        MouseEventKind::Down(MouseButton::Right) => {
827            if app.context_menu.visible {
828                app.context_menu.close();
829                return InputAction::None;
830            }
831            if rect_contains(app.layout.messages, col, row) && !app.is_streaming {
832                let content_y = app.layout.messages.y + 1;
833                if row >= content_y {
834                    let visual_row = (app.scroll_offset + (row - content_y)) as usize;
835                    if let Some(&msg_idx) = app.message_line_map.get(visual_row) {
836                        app.context_menu.open(msg_idx, col, row);
837                    }
838                }
839            }
840            InputAction::None
841        }
842        _ => InputAction::None,
843    }
844}
845
846fn parse_at_references(app: &mut App) {
847    let words: Vec<String> = app.input.split_whitespace().map(String::from).collect();
848    for word in &words {
849        if let Some(path) = word.strip_prefix('@')
850            && !path.is_empty()
851            && crate::tui::app::is_image_path(path)
852        {
853            match app.add_image_attachment(path) {
854                Ok(()) => {}
855                Err(e) => {
856                    app.error_message = Some(e);
857                }
858            }
859        }
860    }
861}
862
863fn compute_click_cursor_pos(input: &str, target_col: usize, target_row: usize) -> usize {
864    let mut row: usize = 0;
865    let mut col: usize = 0;
866    let mut byte_pos: usize = 0;
867
868    for ch in input.chars() {
869        if row == target_row && col >= target_col {
870            return byte_pos;
871        }
872        if ch == '\n' {
873            if row == target_row {
874                return byte_pos;
875            }
876            row += 1;
877            col = 0;
878        } else {
879            col += 1;
880        }
881        byte_pos += ch.len_utf8();
882    }
883
884    byte_pos
885}
886
887fn handle_question_popup(app: &mut App, key: KeyEvent) -> InputAction {
888    let pq = app.pending_question.as_mut().unwrap();
889    match key.code {
890        KeyCode::Esc => {
891            if let Some(responder) = pq.responder.take() {
892                let _ = responder.0.send("[cancelled]".to_string());
893            }
894            app.pending_question = None;
895            InputAction::None
896        }
897        KeyCode::Up => {
898            if pq.selected > 0 {
899                pq.selected -= 1;
900            }
901            InputAction::None
902        }
903        KeyCode::Down | KeyCode::Tab => {
904            let max = if pq.options.is_empty() {
905                0
906            } else {
907                pq.options.len()
908            };
909            if pq.selected < max {
910                pq.selected += 1;
911            }
912            InputAction::None
913        }
914        KeyCode::Enter => {
915            let answer = if pq.options.is_empty() || pq.selected >= pq.options.len() {
916                if pq.custom_input.is_empty() {
917                    "ok".to_string()
918                } else {
919                    pq.custom_input.clone()
920                }
921            } else {
922                pq.options[pq.selected].clone()
923            };
924            if let Some(responder) = pq.responder.take() {
925                let _ = responder.0.send(answer.clone());
926            }
927            app.pending_question = None;
928            InputAction::AnswerQuestion(answer)
929        }
930        KeyCode::Char(c) => {
931            pq.custom_input.push(c);
932            // Select custom input row
933            pq.selected = pq.options.len();
934            InputAction::None
935        }
936        KeyCode::Backspace => {
937            pq.custom_input.pop();
938            InputAction::None
939        }
940        _ => InputAction::None,
941    }
942}
943
944fn handle_permission_popup(app: &mut App, key: KeyEvent) -> InputAction {
945    let pp = app.pending_permission.as_mut().unwrap();
946    match key.code {
947        KeyCode::Esc => {
948            if let Some(responder) = pp.responder.take() {
949                let _ = responder.0.send("deny".to_string());
950            }
951            app.pending_permission = None;
952            InputAction::None
953        }
954        KeyCode::Up => {
955            if pp.selected > 0 {
956                pp.selected -= 1;
957            }
958            InputAction::None
959        }
960        KeyCode::Down | KeyCode::Tab => {
961            if pp.selected < 1 {
962                pp.selected += 1;
963            }
964            InputAction::None
965        }
966        KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y') => {
967            let answer = if pp.selected == 0 { "allow" } else { "deny" };
968            if let Some(responder) = pp.responder.take() {
969                let _ = responder.0.send(answer.to_string());
970            }
971            app.pending_permission = None;
972            InputAction::AnswerPermission(answer.to_string())
973        }
974        KeyCode::Char('n') | KeyCode::Char('N') => {
975            if let Some(responder) = pp.responder.take() {
976                let _ = responder.0.send("deny".to_string());
977            }
978            app.pending_permission = None;
979            InputAction::AnswerPermission("deny".to_string())
980        }
981        _ => InputAction::None,
982    }
983}