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            KeyCode::Char('j') => {
437                if !app.input.is_empty() {
438                    app.insert_char('\n');
439                }
440                return InputAction::None;
441            }
442            _ => {}
443        }
444    }
445
446    if app.is_streaming {
447        return match key.code {
448            KeyCode::Esc => {
449                app.mode = AppMode::Normal;
450                InputAction::None
451            }
452            KeyCode::Enter if key.modifiers.contains(KeyModifiers::SHIFT) => {
453                if !app.input.is_empty() {
454                    app.insert_char('\n');
455                }
456                InputAction::None
457            }
458            KeyCode::Enter => handle_send(app),
459            KeyCode::Char(c) => handle_char_input(app, c),
460            KeyCode::Backspace => handle_backspace(app),
461            KeyCode::Up => {
462                if !app.move_cursor_up() {
463                    app.history_prev();
464                }
465                InputAction::None
466            }
467            KeyCode::Down => {
468                if !app.move_cursor_down() {
469                    app.history_next();
470                }
471                InputAction::None
472            }
473            KeyCode::Left => {
474                app.move_cursor_left();
475                InputAction::None
476            }
477            KeyCode::Right => {
478                app.move_cursor_right();
479                InputAction::None
480            }
481            KeyCode::Home => {
482                app.move_cursor_home();
483                InputAction::None
484            }
485            KeyCode::End => {
486                app.move_cursor_end();
487                InputAction::None
488            }
489            _ => InputAction::None,
490        };
491    }
492
493    match key.code {
494        KeyCode::Esc => {
495            app.mode = AppMode::Normal;
496            InputAction::None
497        }
498        KeyCode::Enter if key.modifiers.contains(KeyModifiers::SHIFT) => {
499            if !app.input.is_empty() {
500                app.insert_char('\n');
501            }
502            InputAction::None
503        }
504        KeyCode::Enter => handle_send(app),
505        KeyCode::Char(c) => handle_char_input(app, c),
506        KeyCode::Backspace => handle_backspace(app),
507        KeyCode::Up => {
508            if !app.move_cursor_up() {
509                app.history_prev();
510            }
511            InputAction::None
512        }
513        KeyCode::Down => {
514            if !app.move_cursor_down() {
515                app.history_next();
516            }
517            InputAction::None
518        }
519        KeyCode::Left => {
520            app.move_cursor_left();
521            InputAction::None
522        }
523        KeyCode::Right => {
524            app.move_cursor_right();
525            InputAction::None
526        }
527        KeyCode::Home => {
528            app.move_cursor_home();
529            InputAction::None
530        }
531        KeyCode::End => {
532            app.move_cursor_end();
533            InputAction::None
534        }
535        _ => InputAction::None,
536    }
537}
538
539fn handle_simple(app: &mut App, key: KeyEvent) -> InputAction {
540    if key.modifiers.contains(KeyModifiers::CONTROL) {
541        match key.code {
542            KeyCode::Char('t') => return InputAction::CycleThinkingLevel,
543            KeyCode::Char('a') => {
544                app.move_cursor_home();
545                return InputAction::None;
546            }
547            KeyCode::Char('e') => {
548                app.move_cursor_end();
549                return InputAction::None;
550            }
551            KeyCode::Char('w') => {
552                app.delete_word_before();
553                return InputAction::None;
554            }
555            KeyCode::Char('k') => {
556                app.delete_to_end();
557                return InputAction::None;
558            }
559            KeyCode::Char('u') => {
560                app.delete_to_start();
561                return InputAction::None;
562            }
563            KeyCode::Char('d') => return InputAction::ScrollDown(10),
564            KeyCode::Char('j') => {
565                if !app.input.is_empty() {
566                    app.insert_char('\n');
567                }
568                return InputAction::None;
569            }
570            _ => {}
571        }
572    }
573
574    if app.is_streaming {
575        return match key.code {
576            KeyCode::Up => {
577                if !app.move_cursor_up() {
578                    app.history_prev();
579                }
580                InputAction::None
581            }
582            KeyCode::Down => {
583                if !app.move_cursor_down() {
584                    app.history_next();
585                }
586                InputAction::None
587            }
588            KeyCode::PageUp => InputAction::ScrollUp(20),
589            KeyCode::PageDown => InputAction::ScrollDown(20),
590            KeyCode::Enter if key.modifiers.contains(KeyModifiers::SHIFT) => {
591                if !app.input.is_empty() {
592                    app.insert_char('\n');
593                }
594                InputAction::None
595            }
596            KeyCode::Enter => handle_send(app),
597            KeyCode::Char(c) => handle_char_input(app, c),
598            KeyCode::Backspace => handle_backspace(app),
599            KeyCode::Left => {
600                app.move_cursor_left();
601                InputAction::None
602            }
603            KeyCode::Right => {
604                app.move_cursor_right();
605                InputAction::None
606            }
607            KeyCode::Home => {
608                app.move_cursor_home();
609                InputAction::None
610            }
611            KeyCode::End => {
612                app.move_cursor_end();
613                InputAction::None
614            }
615            _ => InputAction::None,
616        };
617    }
618
619    match key.code {
620        KeyCode::Esc => InputAction::None,
621        KeyCode::Enter if key.modifiers.contains(KeyModifiers::SHIFT) => {
622            if !app.input.is_empty() {
623                app.insert_char('\n');
624            }
625            InputAction::None
626        }
627        KeyCode::Enter => handle_send(app),
628        KeyCode::Up => {
629            if !app.move_cursor_up() {
630                app.history_prev();
631            }
632            InputAction::None
633        }
634        KeyCode::Down => {
635            if !app.move_cursor_down() {
636                app.history_next();
637            }
638            InputAction::None
639        }
640        KeyCode::PageUp => InputAction::ScrollUp(20),
641        KeyCode::PageDown => InputAction::ScrollDown(20),
642        KeyCode::Tab => InputAction::OpenAgentSelector,
643        KeyCode::Char(c) => handle_char_input(app, c),
644        KeyCode::Backspace => handle_backspace(app),
645        KeyCode::Left => {
646            app.move_cursor_left();
647            InputAction::None
648        }
649        KeyCode::Right => {
650            app.move_cursor_right();
651            InputAction::None
652        }
653        KeyCode::Home => {
654            app.move_cursor_home();
655            InputAction::None
656        }
657        KeyCode::End => {
658            app.move_cursor_end();
659            InputAction::None
660        }
661        _ => InputAction::None,
662    }
663}
664
665fn handle_send(app: &mut App) -> InputAction {
666    parse_at_references(app);
667    if app.is_streaming {
668        app.queue_input();
669        return InputAction::None;
670    }
671    if let Some(msg) = app.take_input() {
672        InputAction::SendMessage(msg)
673    } else {
674        InputAction::None
675    }
676}
677
678fn handle_char_input(app: &mut App, c: char) -> InputAction {
679    app.insert_char(c);
680    if app.input == "/" {
681        app.command_palette.open(&app.input);
682    } else if app.input.starts_with('/') && app.command_palette.visible {
683        app.command_palette.update_filter(&app.input);
684        if app.command_palette.filtered.is_empty() {
685            app.command_palette.close();
686        }
687    }
688    InputAction::None
689}
690
691fn handle_backspace(app: &mut App) -> InputAction {
692    if let Some(pb_idx) = app.paste_block_at_cursor() {
693        app.delete_paste_block(pb_idx);
694    } else {
695        app.delete_char_before();
696    }
697    if app.input.starts_with('/') && !app.input.is_empty() {
698        if !app.command_palette.visible {
699            app.command_palette.open(&app.input);
700        } else {
701            app.command_palette.update_filter(&app.input);
702        }
703    } else if app.command_palette.visible {
704        app.command_palette.close();
705    }
706    InputAction::None
707}
708
709fn rect_contains(r: ratatui::layout::Rect, col: u16, row: u16) -> bool {
710    col >= r.x && col < r.x + r.width && row >= r.y && row < r.y + r.height
711}
712
713pub fn handle_mouse(app: &mut App, mouse: MouseEvent) -> InputAction {
714    let col = mouse.column;
715    let row = mouse.row;
716
717    match mouse.kind {
718        MouseEventKind::ScrollUp => {
719            app.selection.clear();
720            if app.model_selector.visible
721                && let Some(popup) = app.layout.model_selector
722                && rect_contains(popup, col, row)
723            {
724                app.model_selector.up();
725                return InputAction::None;
726            }
727            InputAction::ScrollUp(1)
728        }
729        MouseEventKind::ScrollDown => {
730            app.selection.clear();
731            if app.model_selector.visible
732                && let Some(popup) = app.layout.model_selector
733                && rect_contains(popup, col, row)
734            {
735                app.model_selector.down();
736                return InputAction::None;
737            }
738            InputAction::ScrollDown(1)
739        }
740        MouseEventKind::Down(MouseButton::Left) => {
741            if app.selection.anchor.is_some() && !app.selection.active {
742                app.selection.clear();
743            }
744
745            if app.context_menu.visible {
746                if let Some(popup) = app.layout.context_menu {
747                    if rect_contains(popup, col, row) {
748                        let relative_row = row.saturating_sub(popup.y + 1) as usize;
749                        app.context_menu.selected = relative_row.min(1);
750                        if let Some((action, msg_idx)) = app.context_menu.confirm() {
751                            return match action {
752                                0 => InputAction::TruncateToMessage(msg_idx),
753                                1 => InputAction::ForkFromMessage(msg_idx),
754                                _ => InputAction::None,
755                            };
756                        }
757                    }
758                }
759                app.context_menu.close();
760                return InputAction::None;
761            }
762
763            if app.model_selector.visible
764                && let Some(popup) = app.layout.model_selector
765            {
766                if !rect_contains(popup, col, row) {
767                    app.model_selector.close();
768                }
769                return InputAction::None;
770            }
771
772            if app.agent_selector.visible
773                && let Some(popup) = app.layout.agent_selector
774            {
775                if !rect_contains(popup, col, row) {
776                    app.agent_selector.close();
777                }
778                return InputAction::None;
779            }
780
781            if app.help_popup.visible
782                && let Some(popup) = app.layout.help_popup
783            {
784                if !rect_contains(popup, col, row) {
785                    app.help_popup.close();
786                }
787                return InputAction::None;
788            }
789
790            if app.thinking_selector.visible
791                && let Some(popup) = app.layout.thinking_selector
792                && rect_contains(popup, col, row)
793            {
794                let relative_row = row.saturating_sub(popup.y + 1) as usize;
795                if relative_row < ThinkingLevel::all().len() {
796                    app.thinking_selector.selected = relative_row;
797                    if let Some(level) = app.thinking_selector.confirm() {
798                        let budget = level.budget_tokens();
799                        app.thinking_budget = budget;
800                        return InputAction::SetThinkingLevel(budget);
801                    }
802                }
803            } else if app.thinking_selector.visible
804                && let Some(popup) = app.layout.thinking_selector
805            {
806                if !rect_contains(popup, col, row) {
807                    app.thinking_selector.close();
808                }
809                return InputAction::None;
810            }
811
812            if app.session_selector.visible
813                && let Some(popup) = app.layout.session_selector
814                && !rect_contains(popup, col, row)
815            {
816                app.session_selector.close();
817                return InputAction::None;
818            }
819
820            if app.command_palette.visible
821                && let Some(popup) = app.layout.command_palette
822            {
823                if rect_contains(popup, col, row) {
824                    let relative_row = row.saturating_sub(popup.y) as usize;
825                    if relative_row < app.command_palette.filtered.len() {
826                        app.command_palette.selected = relative_row;
827                        if let Some(cmd_name) = app.command_palette.confirm() {
828                            app.input.clear();
829                            app.cursor_pos = 0;
830                            return execute_command(app, cmd_name);
831                        }
832                    }
833                    return InputAction::None;
834                } else {
835                    app.command_palette.close();
836                    return InputAction::None;
837                }
838            }
839
840            if rect_contains(app.layout.input, col, row) {
841                if app.vim_mode {
842                    app.mode = AppMode::Insert;
843                }
844                let inner_x = col.saturating_sub(app.layout.input.x + 3);
845                let inner_y = row.saturating_sub(app.layout.input.y + 1);
846                let target_offset =
847                    compute_click_cursor_pos(&app.input, inner_x as usize, inner_y as usize);
848                app.cursor_pos = target_offset;
849                InputAction::None
850            } else if rect_contains(app.layout.messages, col, row) {
851                let content_y = app.layout.messages.y + 1;
852                if row >= content_y {
853                    let content_col = col.saturating_sub(app.layout.messages.x);
854                    let content_row = row - content_y;
855                    let visual_row = app.scroll_offset + content_row;
856                    app.selection.start(content_col, visual_row);
857                }
858                if app.vim_mode && app.mode == AppMode::Insert && app.input.is_empty() {
859                    app.mode = AppMode::Normal;
860                }
861                InputAction::None
862            } else {
863                InputAction::None
864            }
865        }
866        MouseEventKind::Drag(MouseButton::Left) => {
867            if app.selection.active {
868                let content_y = app.layout.messages.y + 1;
869                let content_height = app.layout.messages.height.saturating_sub(1);
870                let content_col = col.saturating_sub(app.layout.messages.x);
871                let content_row = if row >= content_y {
872                    (row - content_y).min(content_height.saturating_sub(1))
873                } else {
874                    0
875                };
876                let visual_row = app.scroll_offset + content_row;
877                app.selection.update(content_col, visual_row);
878            }
879            InputAction::None
880        }
881        MouseEventKind::Up(MouseButton::Left) => {
882            if app.selection.active {
883                let content_y = app.layout.messages.y + 1;
884                let content_height = app.layout.messages.height.saturating_sub(1);
885                let content_col = col.saturating_sub(app.layout.messages.x);
886                let content_row = if row >= content_y {
887                    (row - content_y).min(content_height.saturating_sub(1))
888                } else {
889                    0
890                };
891                let visual_row = app.scroll_offset + content_row;
892                app.selection.update(content_col, visual_row);
893                app.selection.active = false;
894                if !app.selection.is_empty_selection() {
895                    if let Some(text) = app.extract_selected_text()
896                        && !text.trim().is_empty()
897                    {
898                        crate::tui::app::copy_to_clipboard(&text);
899                    }
900                } else {
901                    app.selection.clear();
902                }
903            }
904            InputAction::None
905        }
906        MouseEventKind::Down(MouseButton::Right) => {
907            if app.context_menu.visible {
908                app.context_menu.close();
909                return InputAction::None;
910            }
911            if rect_contains(app.layout.messages, col, row) && !app.is_streaming {
912                let content_y = app.layout.messages.y + 1;
913                if row >= content_y {
914                    let visual_row = (app.scroll_offset + (row - content_y)) as usize;
915                    if let Some(&msg_idx) = app.message_line_map.get(visual_row) {
916                        app.context_menu.open(msg_idx, col, row);
917                    }
918                }
919            }
920            InputAction::None
921        }
922        _ => InputAction::None,
923    }
924}
925
926fn parse_at_references(app: &mut App) {
927    let words: Vec<String> = app.input.split_whitespace().map(String::from).collect();
928    for word in &words {
929        if let Some(path) = word.strip_prefix('@')
930            && !path.is_empty()
931            && crate::tui::app::is_image_path(path)
932        {
933            match app.add_image_attachment(path) {
934                Ok(()) => {}
935                Err(e) => {
936                    app.error_message = Some(e);
937                }
938            }
939        }
940    }
941}
942
943fn compute_click_cursor_pos(input: &str, target_col: usize, target_row: usize) -> usize {
944    let mut row: usize = 0;
945    let mut col: usize = 0;
946    let mut byte_pos: usize = 0;
947
948    for ch in input.chars() {
949        if row == target_row && col >= target_col {
950            return byte_pos;
951        }
952        if ch == '\n' {
953            if row == target_row {
954                return byte_pos;
955            }
956            row += 1;
957            col = 0;
958        } else {
959            col += 1;
960        }
961        byte_pos += ch.len_utf8();
962    }
963
964    byte_pos
965}
966
967fn handle_question_popup(app: &mut App, key: KeyEvent) -> InputAction {
968    let pq = app.pending_question.as_mut().unwrap();
969    match key.code {
970        KeyCode::Esc => {
971            if let Some(responder) = pq.responder.take() {
972                let _ = responder.0.send("[cancelled]".to_string());
973            }
974            app.pending_question = None;
975            InputAction::None
976        }
977        KeyCode::Up => {
978            if pq.selected > 0 {
979                pq.selected -= 1;
980            }
981            InputAction::None
982        }
983        KeyCode::Down | KeyCode::Tab => {
984            let max = if pq.options.is_empty() {
985                0
986            } else {
987                pq.options.len()
988            };
989            if pq.selected < max {
990                pq.selected += 1;
991            }
992            InputAction::None
993        }
994        KeyCode::Enter => {
995            let answer = if pq.options.is_empty() || pq.selected >= pq.options.len() {
996                if pq.custom_input.is_empty() {
997                    "ok".to_string()
998                } else {
999                    pq.custom_input.clone()
1000                }
1001            } else {
1002                pq.options[pq.selected].clone()
1003            };
1004            if let Some(responder) = pq.responder.take() {
1005                let _ = responder.0.send(answer.clone());
1006            }
1007            app.pending_question = None;
1008            InputAction::AnswerQuestion(answer)
1009        }
1010        KeyCode::Char(c) => {
1011            pq.custom_input.push(c);
1012            // Select custom input row
1013            pq.selected = pq.options.len();
1014            InputAction::None
1015        }
1016        KeyCode::Backspace => {
1017            pq.custom_input.pop();
1018            InputAction::None
1019        }
1020        _ => InputAction::None,
1021    }
1022}
1023
1024fn handle_permission_popup(app: &mut App, key: KeyEvent) -> InputAction {
1025    let pp = app.pending_permission.as_mut().unwrap();
1026    match key.code {
1027        KeyCode::Esc => {
1028            if let Some(responder) = pp.responder.take() {
1029                let _ = responder.0.send("deny".to_string());
1030            }
1031            app.pending_permission = None;
1032            InputAction::None
1033        }
1034        KeyCode::Up => {
1035            if pp.selected > 0 {
1036                pp.selected -= 1;
1037            }
1038            InputAction::None
1039        }
1040        KeyCode::Down | KeyCode::Tab => {
1041            if pp.selected < 1 {
1042                pp.selected += 1;
1043            }
1044            InputAction::None
1045        }
1046        KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y') => {
1047            let answer = if pp.selected == 0 { "allow" } else { "deny" };
1048            if let Some(responder) = pp.responder.take() {
1049                let _ = responder.0.send(answer.to_string());
1050            }
1051            app.pending_permission = None;
1052            InputAction::AnswerPermission(answer.to_string())
1053        }
1054        KeyCode::Char('n') | KeyCode::Char('N') => {
1055            if let Some(responder) = pp.responder.take() {
1056                let _ = responder.0.send("deny".to_string());
1057            }
1058            app.pending_permission = None;
1059            InputAction::AnswerPermission("deny".to_string())
1060        }
1061        _ => InputAction::None,
1062    }
1063}