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