Skip to main content

vtcode_tui/core_tui/app/session/
impl_events.rs

1use super::*;
2use crate::config::constants::ui;
3use crate::core_tui::session::MouseDragTarget;
4use crate::core_tui::session::render::modal_render_styles;
5use crate::core_tui::session::{TranscriptLinkClickAction, inline_list, list_panel, modal};
6use std::time::Instant;
7
8impl Session {
9    #[cfg(test)]
10    pub(crate) fn process_key(&mut self, key: KeyEvent) -> Option<InlineEvent> {
11        events::process_key(self, key)
12    }
13
14    fn input_area_contains(&self, column: u16, row: u16) -> bool {
15        self.core.input_area().is_some_and(|area| {
16            row >= area.y
17                && row < area.y.saturating_add(area.height)
18                && column >= area.x
19                && column < area.x.saturating_add(area.width)
20        })
21    }
22
23    fn bottom_panel_contains(&self, column: u16, row: u16) -> bool {
24        self.core.bottom_panel_area().is_some_and(|area| {
25            row >= area.y
26                && row < area.y.saturating_add(area.height)
27                && column >= area.x
28                && column < area.x.saturating_add(area.width)
29        })
30    }
31
32    fn panel_row_index(
33        &self,
34        layout: &list_panel::ListPanelLayout,
35        column: u16,
36        row: u16,
37    ) -> Option<usize> {
38        let area = self.core.bottom_panel_area()?;
39        layout.row_index(area, column, row)
40    }
41
42    fn handle_modal_list_result(
43        &mut self,
44        result: modal::ModalListKeyResult,
45        events: &UnboundedSender<InlineEvent>,
46        callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
47    ) -> bool {
48        match result {
49            modal::ModalListKeyResult::NotHandled => false,
50            modal::ModalListKeyResult::HandledNoRedraw => true,
51            modal::ModalListKeyResult::Redraw => {
52                self.mark_dirty();
53                true
54            }
55            modal::ModalListKeyResult::Emit(event) => {
56                self.mark_dirty();
57                let outbound: InlineEvent = event.into();
58                events::emit_inline_event(&outbound, events, callback);
59                true
60            }
61            modal::ModalListKeyResult::Submit(event) | modal::ModalListKeyResult::Cancel(event) => {
62                self.close_overlay();
63                self.mark_dirty();
64                let outbound: InlineEvent = event.into();
65                events::emit_inline_event(&outbound, events, callback);
66                true
67            }
68        }
69    }
70
71    fn handle_link_click_action(
72        &mut self,
73        action: TranscriptLinkClickAction,
74        clear_drag_target: bool,
75        events: &UnboundedSender<InlineEvent>,
76        callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
77    ) -> bool {
78        match action {
79            TranscriptLinkClickAction::Open(outbound) => {
80                if clear_drag_target {
81                    self.core.mouse_drag_target = MouseDragTarget::None;
82                }
83                self.mark_dirty();
84                let outbound: InlineEvent = outbound.into();
85                events::emit_inline_event(&outbound, events, callback);
86                self.core.mouse_selection.clear_click_history();
87                true
88            }
89            TranscriptLinkClickAction::Consume => {
90                if clear_drag_target {
91                    self.core.mouse_drag_target = MouseDragTarget::None;
92                }
93                self.core.mouse_selection.clear_click_history();
94                true
95            }
96            TranscriptLinkClickAction::Ignore => false,
97        }
98    }
99
100    fn modal_visible_index_at(&self, row: u16) -> Option<usize> {
101        let area = self.core.modal_list_area()?;
102        if row < area.y || row >= area.y.saturating_add(area.height) {
103            return None;
104        }
105
106        let styles = modal_render_styles(self);
107        let content_width =
108            area.width
109                .saturating_sub(inline_list::selection_padding_width() as u16) as usize;
110        let relative_row = usize::from(row.saturating_sub(area.y));
111
112        if let Some(wizard) = self.wizard_overlay() {
113            let step = wizard.steps.get(wizard.current_step)?;
114            let offset = step.list.list_state.offset();
115            let visible_indices = &step.list.visible_indices;
116            let mut consumed_rows = 0usize;
117            for (visible_index, &item_index) in visible_indices.iter().enumerate().skip(offset) {
118                let lines = modal::modal_list_item_lines(
119                    &step.list,
120                    visible_index,
121                    item_index,
122                    &styles,
123                    content_width,
124                    None,
125                    false,
126                );
127                let height = usize::from(inline_list::row_height(&lines));
128                if relative_row < consumed_rows + height {
129                    return Some(visible_index);
130                }
131                consumed_rows += height;
132                if consumed_rows >= usize::from(area.height) {
133                    break;
134                }
135            }
136            return None;
137        }
138
139        let modal = self.modal_state()?;
140        let list = modal.list.as_ref()?;
141        let offset = list.list_state.offset();
142        let mut consumed_rows = 0usize;
143        for (visible_index, &item_index) in list.visible_indices.iter().enumerate().skip(offset) {
144            let lines = modal::modal_list_item_lines(
145                list,
146                visible_index,
147                item_index,
148                &styles,
149                content_width,
150                None,
151                false,
152            );
153            let height = usize::from(inline_list::row_height(&lines));
154            if relative_row < consumed_rows + height {
155                return Some(visible_index);
156            }
157            consumed_rows += height;
158            if consumed_rows >= usize::from(area.height) {
159                break;
160            }
161        }
162
163        None
164    }
165
166    fn handle_active_overlay_click(
167        &mut self,
168        mouse_event: MouseEvent,
169        events: &UnboundedSender<InlineEvent>,
170        callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
171    ) -> bool {
172        let column = mouse_event.column;
173        let row = mouse_event.row;
174        let in_modal_list = self.core.modal_list_area().is_some_and(|area| {
175            row >= area.y
176                && row < area.y.saturating_add(area.height)
177                && column >= area.x
178                && column < area.x.saturating_add(area.width)
179        });
180        if !in_modal_list {
181            return self.has_active_overlay();
182        }
183
184        let Some(visible_index) = self.modal_visible_index_at(row) else {
185            return true;
186        };
187
188        if let Some(wizard) = self.wizard_overlay_mut() {
189            let result = wizard.handle_mouse_click(visible_index);
190            return self.handle_modal_list_result(result, events, callback);
191        }
192
193        if let Some(modal) = self.modal_state_mut() {
194            let result = modal.handle_list_mouse_click(visible_index);
195            return self.handle_modal_list_result(result, events, callback);
196        }
197
198        true
199    }
200
201    fn modal_text_area_contains(&self, column: u16, row: u16) -> bool {
202        self.core.modal_text_areas().iter().any(|area| {
203            row >= area.y
204                && row < area.y.saturating_add(area.height)
205                && column >= area.x
206                && column < area.x.saturating_add(area.width)
207        })
208    }
209
210    fn handle_active_overlay_scroll(
211        &mut self,
212        mouse_event: MouseEvent,
213        down: bool,
214        events: &UnboundedSender<InlineEvent>,
215        callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
216    ) -> bool {
217        if !self.has_active_overlay() {
218            return false;
219        }
220
221        let column = mouse_event.column;
222        let row = mouse_event.row;
223        let in_modal_list = self.core.modal_list_area().is_some_and(|area| {
224            row >= area.y
225                && row < area.y.saturating_add(area.height)
226                && column >= area.x
227                && column < area.x.saturating_add(area.width)
228        });
229
230        if !in_modal_list {
231            return true;
232        }
233
234        if let Some(wizard) = self.wizard_overlay_mut() {
235            let result = wizard.handle_mouse_scroll(down);
236            return self.handle_modal_list_result(result, events, callback);
237        }
238
239        if let Some(modal) = self.modal_state_mut() {
240            let result = modal.handle_list_mouse_scroll(down);
241            return self.handle_modal_list_result(result, events, callback);
242        }
243
244        true
245    }
246
247    fn handle_bottom_panel_scroll(&mut self, down: bool) -> bool {
248        if self.core.bottom_panel_area().is_none() {
249            return false;
250        }
251
252        if self.agent_palette_visible() {
253            let Some(palette) = self.agent_palette.as_mut() else {
254                return true;
255            };
256            if down {
257                palette.move_selection_down();
258            } else {
259                palette.move_selection_up();
260            }
261            self.mark_dirty();
262            return true;
263        }
264
265        if self.file_palette_visible() {
266            let Some(palette) = self.file_palette.as_mut() else {
267                return true;
268            };
269            if down {
270                palette.move_selection_down();
271            } else {
272                palette.move_selection_up();
273            }
274            self.mark_dirty();
275            return true;
276        }
277
278        if self.history_picker_visible() {
279            if down {
280                self.history_picker_state.move_down();
281            } else {
282                self.history_picker_state.move_up();
283            }
284            self.mark_dirty();
285            return true;
286        }
287
288        if self.local_agents_visible() {
289            let changed = if down {
290                self.local_agents_state.move_selection_down()
291            } else {
292                self.local_agents_state.move_selection_up()
293            };
294            if changed {
295                self.mark_dirty();
296            }
297            return true;
298        }
299
300        if slash::slash_navigation_available(self) {
301            if down {
302                slash::move_slash_selection_down(self);
303            } else {
304                slash::move_slash_selection_up(self);
305            }
306            return true;
307        }
308
309        false
310    }
311
312    fn handle_bottom_panel_click(&mut self, mouse_event: MouseEvent) -> bool {
313        let column = mouse_event.column;
314        let row = mouse_event.row;
315        if !self.bottom_panel_contains(column, row) {
316            return false;
317        }
318
319        if self.agent_palette_visible() {
320            let Some(layout) = render::agent_palette_panel_layout(self) else {
321                return true;
322            };
323            let bottom_area = self.core.bottom_panel_area();
324            let Some(palette) = self.agent_palette.as_mut() else {
325                return true;
326            };
327            let local_index = bottom_area.and_then(|area| layout.row_index(area, column, row));
328            let mut apply_name = None;
329            let mut should_mark_dirty = false;
330            if !palette.has_agents() {
331                return true;
332            }
333
334            let page_items = palette.current_page_items();
335            if let Some(local_index) = local_index
336                && let Some((global_index, entry, selected)) = page_items.get(local_index)
337            {
338                if *selected {
339                    apply_name = Some(entry.name.clone());
340                } else if palette.select_index(*global_index) {
341                    should_mark_dirty = true;
342                }
343            }
344
345            if let Some(name) = apply_name {
346                self.insert_agent_reference(&name);
347                self.close_agent_palette();
348                self.mark_dirty();
349            } else if should_mark_dirty {
350                self.mark_dirty();
351            }
352            return true;
353        }
354
355        if self.file_palette_visible() {
356            let Some(layout) = render::file_palette_panel_layout(self) else {
357                return true;
358            };
359            let bottom_area = self.core.bottom_panel_area();
360            let Some(palette) = self.file_palette.as_mut() else {
361                return true;
362            };
363            let local_index = bottom_area.and_then(|area| layout.row_index(area, column, row));
364            let mut apply_path = None;
365            let mut should_mark_dirty = false;
366            if !palette.has_files() {
367                return true;
368            }
369
370            let page_items = palette.current_page_items();
371            if let Some(local_index) = local_index
372                && let Some((global_index, entry, selected)) = page_items.get(local_index)
373            {
374                if *selected {
375                    if palette.selected_is_expandable_group() {
376                        palette.toggle_selected();
377                        should_mark_dirty = true;
378                    } else {
379                        apply_path = Some(entry.relative_path.clone());
380                    }
381                } else if palette.select_index(*global_index) {
382                    should_mark_dirty = true;
383                }
384            }
385
386            if let Some(path) = apply_path {
387                self.insert_file_reference(&path);
388                self.close_file_palette();
389                self.mark_dirty();
390            } else if should_mark_dirty {
391                self.mark_dirty();
392            }
393            return true;
394        }
395
396        if self.history_picker_visible() {
397            let Some(layout) = render::history_picker_panel_layout(self) else {
398                return true;
399            };
400            if let Some(local_index) = self.panel_row_index(&layout, column, row)
401                && !self.history_picker_state.matches.is_empty()
402            {
403                let actual_index = self
404                    .history_picker_state
405                    .scroll_offset()
406                    .saturating_add(local_index);
407                if self.history_picker_state.selected_index() == Some(actual_index) {
408                    let was_active = self.history_picker_visible();
409                    self.history_picker_state
410                        .accept(&mut self.core.input_manager);
411                    self.finish_history_picker_interaction(was_active);
412                    self.mark_dirty();
413                } else if self.history_picker_state.select_index(actual_index) {
414                    self.mark_dirty();
415                }
416            }
417            return true;
418        }
419
420        if self.local_agents_visible() {
421            let Some(layout) = render::local_agents_panel_layout(self) else {
422                return true;
423            };
424            if let Some(local_index) = self.panel_row_index(&layout, column, row) {
425                let actual_index = self
426                    .local_agents_state
427                    .scroll_offset()
428                    .saturating_add(local_index);
429                if self.local_agents_state.select_index(actual_index) {
430                    self.mark_dirty();
431                }
432            }
433            return true;
434        }
435
436        if slash::slash_navigation_available(self) {
437            let Some(layout) = slash::slash_panel_layout(self) else {
438                return true;
439            };
440            if let Some(local_index) = self.panel_row_index(&layout, column, row) {
441                let actual_index = self
442                    .slash_palette
443                    .scroll_offset()
444                    .saturating_add(local_index);
445                if self.slash_palette.selected_index() == Some(actual_index) {
446                    slash::apply_selected_slash_suggestion(self);
447                } else {
448                    slash::select_slash_suggestion_index(self, actual_index);
449                }
450            }
451            return true;
452        }
453
454        true
455    }
456
457    pub fn handle_event(
458        &mut self,
459        event: CrosstermEvent,
460        events: &UnboundedSender<InlineEvent>,
461        callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
462    ) {
463        match event {
464            CrosstermEvent::Key(key) => {
465                self.update_held_key_modifiers(&key);
466                // Only process Press events to avoid duplicate character insertion
467                // Repeat events can cause characters to be inserted multiple times
468                if matches!(key.kind, KeyEventKind::Press)
469                    && let Some(outbound) = events::process_key(self, key)
470                {
471                    events::emit_inline_event(&outbound, events, callback);
472                }
473            }
474            CrosstermEvent::Mouse(mouse_event) => {
475                if !self.core.fullscreen.interaction.mouse_capture {
476                    return;
477                }
478
479                match mouse_event.kind {
480                    MouseEventKind::Moved => {
481                        if self
482                            .update_transcript_file_link_hover(mouse_event.column, mouse_event.row)
483                        {
484                            self.mark_dirty();
485                        }
486                    }
487                    MouseEventKind::ScrollDown => {
488                        self.core.clear_pending_link_click();
489                        self.core.mouse_selection.clear_click_history();
490                        if !self.handle_active_overlay_scroll(mouse_event, true, events, callback)
491                            && !self.handle_bottom_panel_scroll(true)
492                        {
493                            self.scroll_line_down();
494                            self.mark_dirty();
495                        }
496                    }
497                    MouseEventKind::ScrollUp => {
498                        self.core.clear_pending_link_click();
499                        self.core.mouse_selection.clear_click_history();
500                        if !self.handle_active_overlay_scroll(mouse_event, false, events, callback)
501                            && !self.handle_bottom_panel_scroll(false)
502                        {
503                            self.scroll_line_up();
504                            self.mark_dirty();
505                        }
506                    }
507                    MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
508                        self.core.clear_pending_link_click();
509                        if self.core.queue_link_click_action(
510                            self.transcript_file_link_click_action(
511                                mouse_event.column,
512                                mouse_event.row,
513                                mouse_event.modifiers,
514                            ),
515                        ) {
516                            self.core.mouse_selection.clear_click_history();
517                            return;
518                        }
519
520                        if self.has_active_overlay() {
521                            let in_modal_list = self.core.modal_list_area().is_some_and(|area| {
522                                mouse_event.row >= area.y
523                                    && mouse_event.row < area.y.saturating_add(area.height)
524                                    && mouse_event.column >= area.x
525                                    && mouse_event.column < area.x.saturating_add(area.width)
526                            });
527                            if self
528                                .core
529                                .queue_link_click_action(self.modal_link_click_action(
530                                    mouse_event.column,
531                                    mouse_event.row,
532                                    mouse_event.modifiers,
533                                ))
534                            {
535                                self.core.mouse_selection.clear_click_history();
536                                return;
537                            }
538
539                            if self.modal_text_area_contains(mouse_event.column, mouse_event.row)
540                                && !in_modal_list
541                            {
542                                let is_double_click = self.core.mouse_selection.register_click(
543                                    mouse_event.column,
544                                    mouse_event.row,
545                                    Instant::now(),
546                                );
547                                if is_double_click {
548                                    let modal_double_click_action =
549                                        self.core.throttle_link_click_action(
550                                            self.modal_link_double_click_action(
551                                                mouse_event.column,
552                                                mouse_event.row,
553                                            ),
554                                        );
555                                    if !matches!(
556                                        modal_double_click_action,
557                                        TranscriptLinkClickAction::Ignore
558                                    ) {
559                                        self.core.clear_pending_link_click();
560                                    }
561                                    if self.handle_link_click_action(
562                                        modal_double_click_action,
563                                        true,
564                                        events,
565                                        callback,
566                                    ) {
567                                        return;
568                                    }
569                                }
570
571                                self.core.mouse_drag_target = MouseDragTarget::ModalText;
572                                self.core
573                                    .mouse_selection
574                                    .start_selection(mouse_event.column, mouse_event.row);
575                                self.mark_dirty();
576                                return;
577                            }
578                        }
579
580                        if self.has_active_overlay()
581                            && self.handle_active_overlay_click(mouse_event, events, callback)
582                        {
583                            self.core.mouse_selection.clear_click_history();
584                            return;
585                        }
586
587                        if self.handle_bottom_panel_click(mouse_event) {
588                            self.core.mouse_selection.clear_click_history();
589                            return;
590                        }
591
592                        if self.handle_input_click(mouse_event) {
593                            self.core.mouse_drag_target = MouseDragTarget::Input;
594                            self.core.mouse_selection.clear();
595                            return;
596                        }
597
598                        let is_double_click = self.core.mouse_selection.register_click(
599                            mouse_event.column,
600                            mouse_event.row,
601                            Instant::now(),
602                        );
603                        if is_double_click {
604                            let transcript_double_click_action =
605                                self.core.throttle_link_click_action(
606                                    self.transcript_file_link_double_click_action(
607                                        mouse_event.column,
608                                        mouse_event.row,
609                                    ),
610                                );
611                            if !matches!(
612                                transcript_double_click_action,
613                                TranscriptLinkClickAction::Ignore
614                            ) {
615                                self.core.clear_pending_link_click();
616                            }
617                            if self.handle_link_click_action(
618                                transcript_double_click_action,
619                                true,
620                                events,
621                                callback,
622                            ) {
623                                return;
624                            }
625
626                            self.core.mouse_drag_target = MouseDragTarget::None;
627                            let _ = self.handle_transcript_click(mouse_event);
628                            if self
629                                .core
630                                .select_transcript_word_at(mouse_event.column, mouse_event.row)
631                            {
632                                self.mark_dirty();
633                            } else {
634                                self.core.mouse_selection.clear();
635                            }
636                            self.core.mouse_selection.clear_click_history();
637                            return;
638                        }
639
640                        self.core.mouse_drag_target = MouseDragTarget::Transcript;
641                        self.core
642                            .mouse_selection
643                            .start_selection(mouse_event.column, mouse_event.row);
644                        self.mark_dirty();
645                        self.handle_transcript_click(mouse_event);
646                    }
647                    MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
648                        self.core.clear_pending_link_click();
649                        match self.core.mouse_drag_target {
650                            MouseDragTarget::Input => {
651                                if let Some(cursor) = self.cursor_index_for_input_point(
652                                    mouse_event.column,
653                                    mouse_event.row,
654                                ) && self.core.input_manager.cursor() != cursor
655                                {
656                                    self.core.input_manager.set_cursor_with_selection(cursor);
657                                    self.mark_dirty();
658                                }
659                            }
660                            MouseDragTarget::Transcript => {
661                                self.core
662                                    .mouse_selection
663                                    .update_selection(mouse_event.column, mouse_event.row);
664                                self.mark_dirty();
665                            }
666                            MouseDragTarget::ModalText => {
667                                self.core
668                                    .mouse_selection
669                                    .update_selection(mouse_event.column, mouse_event.row);
670                                self.mark_dirty();
671                            }
672                            MouseDragTarget::None => {}
673                        }
674                    }
675                    MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
676                        let transcript_link_action = self.core.pending_link_click_action(
677                            self.transcript_file_link_click_action(
678                                mouse_event.column,
679                                mouse_event.row,
680                                mouse_event.modifiers,
681                            ),
682                        );
683                        let modal_link_action =
684                            self.core
685                                .pending_link_click_action(self.modal_link_click_action(
686                                    mouse_event.column,
687                                    mouse_event.row,
688                                    mouse_event.modifiers,
689                                ));
690                        match self.core.mouse_drag_target {
691                            MouseDragTarget::Input => {
692                                if let Some(cursor) = self.cursor_index_for_input_point(
693                                    mouse_event.column,
694                                    mouse_event.row,
695                                ) && self.core.input_manager.cursor() != cursor
696                                {
697                                    self.core.input_manager.set_cursor_with_selection(cursor);
698                                    self.mark_dirty();
699                                }
700                            }
701                            MouseDragTarget::Transcript => {
702                                self.core
703                                    .mouse_selection
704                                    .finish_selection(mouse_event.column, mouse_event.row);
705                                self.mark_dirty();
706                            }
707                            MouseDragTarget::ModalText => {
708                                self.core
709                                    .mouse_selection
710                                    .finish_selection(mouse_event.column, mouse_event.row);
711                                self.mark_dirty();
712                            }
713                            MouseDragTarget::None => {}
714                        }
715                        self.core.mouse_drag_target = MouseDragTarget::None;
716                        self.core.clear_pending_link_click();
717                        if self.handle_link_click_action(
718                            transcript_link_action,
719                            false,
720                            events,
721                            callback,
722                        ) {
723                            return;
724                        }
725                        if self.handle_link_click_action(modal_link_action, false, events, callback)
726                        {
727                        }
728                    }
729                    _ => {}
730                }
731            }
732            CrosstermEvent::Paste(content) => {
733                if let Some(event) = events::handle_paste(self, &content) {
734                    events::emit_inline_event(&event, events, callback);
735                }
736            }
737            CrosstermEvent::Resize(_, rows) => {
738                self.apply_view_rows(rows);
739                self.mark_dirty();
740            }
741            CrosstermEvent::FocusGained => {
742                // No-op: focus tracking is host/application concern.
743            }
744            CrosstermEvent::FocusLost => {
745                self.clear_held_key_modifiers();
746            }
747        }
748    }
749
750    pub(crate) fn handle_transcript_click(&mut self, mouse_event: MouseEvent) -> bool {
751        if !matches!(
752            mouse_event.kind,
753            MouseEventKind::Down(crossterm::event::MouseButton::Left)
754        ) {
755            return false;
756        }
757
758        let Some(area) = self.core.transcript_area() else {
759            return false;
760        };
761
762        if mouse_event.row < area.y
763            || mouse_event.row >= area.y.saturating_add(area.height)
764            || mouse_event.column < area.x
765            || mouse_event.column >= area.x.saturating_add(area.width)
766        {
767            return false;
768        }
769
770        if self.core.transcript_width == 0 || self.core.transcript_rows == 0 {
771            return false;
772        }
773
774        let row_in_view = (mouse_event.row - area.y) as usize;
775        if row_in_view >= self.core.transcript_rows as usize {
776            return false;
777        }
778
779        let viewport_rows = self.core.transcript_rows.max(1) as usize;
780        let transcript_width = self.core.transcript_width;
781        let padding = usize::from(ui::INLINE_TRANSCRIPT_BOTTOM_PADDING);
782        let effective_padding = padding.min(viewport_rows.saturating_sub(1));
783        let total_rows = self.total_transcript_rows(transcript_width) + effective_padding;
784        let (top_offset, _clamped_total_rows) =
785            self.prepare_transcript_scroll(total_rows, viewport_rows);
786        let view_top = top_offset.min(self.core.scroll_manager.max_offset());
787        self.core.transcript_view_top = view_top;
788
789        let clicked_row = view_top.saturating_add(row_in_view);
790        let expanded = self.expand_collapsed_paste_at_row(transcript_width, clicked_row);
791        if expanded {
792            self.mark_dirty();
793        }
794        expanded
795    }
796
797    pub(crate) fn handle_input_click(&mut self, mouse_event: MouseEvent) -> bool {
798        if !matches!(
799            mouse_event.kind,
800            MouseEventKind::Down(crossterm::event::MouseButton::Left)
801        ) {
802            return false;
803        }
804
805        if !self.input_area_contains(mouse_event.column, mouse_event.row) {
806            return false;
807        }
808
809        let cursor_at_end =
810            self.core.input_manager.cursor() == self.core.input_manager.content().len();
811        if self.core.input_compact_mode()
812            && cursor_at_end
813            && self.input_compact_placeholder().is_some()
814        {
815            self.core.set_input_compact_mode(false);
816            self.mark_dirty();
817            return true;
818        }
819
820        if let Some(cursor) = self.cursor_index_for_input_point(mouse_event.column, mouse_event.row)
821        {
822            if self.core.input_manager.cursor() != cursor {
823                self.core.input_manager.set_cursor(cursor);
824                self.mark_dirty();
825            }
826            return true;
827        }
828
829        false
830    }
831}