Skip to main content

vtcode_tui/core_tui/session/
impl_render.rs

1use super::*;
2
3impl Session {
4    pub fn render(&mut self, frame: &mut Frame<'_>) {
5        let viewport = frame.area();
6        if viewport.height == 0 || viewport.width == 0 {
7            return;
8        }
9
10        // Clear entire frame if modal was just closed to remove artifacts
11        if self.needs_full_clear {
12            frame.render_widget(Clear, viewport);
13            self.needs_full_clear = false;
14        }
15
16        // Calculate layout constraints
17        let header_lines = self.header_lines();
18        let header_height = self.header_height_from_lines(viewport.width, &header_lines);
19        if header_height != self.header_rows {
20            self.header_rows = header_height;
21            self.recalculate_transcript_rows();
22        }
23
24        let mode = self.resolved_layout_mode(viewport);
25        // Always reserve 1 row for the inline status line to prevent layout
26        // jumping when status content appears/disappears during task execution.
27        let status_height = if viewport.width > 0 && !mode.show_footer() {
28            1
29        } else {
30            0
31        };
32        let inner_width = viewport.width.saturating_sub(2);
33        let desired_lines = self.desired_input_lines(inner_width);
34        let block_height = Self::input_block_height_for_lines(desired_lines);
35        let input_height = block_height.saturating_add(status_height);
36        self.apply_input_height(input_height);
37
38        let mut constraints = vec![Constraint::Length(header_height), Constraint::Min(1)];
39        constraints.push(Constraint::Length(input_height));
40
41        let segments = Layout::vertical(constraints).split(viewport);
42
43        let header_area = segments[0];
44        let main_area = segments[1];
45        let input_index = segments.len().saturating_sub(1);
46        let input_area = segments[input_index];
47
48        let _available_width = main_area.width;
49        let _horizontal_minimum = ui::INLINE_CONTENT_MIN_WIDTH + ui::INLINE_NAVIGATION_MIN_WIDTH;
50
51        let (transcript_area, modal_area) = render::split_inline_modal_area(self, main_area);
52        let (transcript_area, file_palette_area) =
53            render::split_inline_file_palette_area(self, transcript_area);
54        let (transcript_area, history_picker_area) =
55            render::split_inline_history_picker_area(self, transcript_area);
56        let (transcript_area, slash_area) = slash::split_inline_slash_area(self, transcript_area);
57        let navigation_area = Rect::new(main_area.x, main_area.y, 0, 0); // No navigation area since timeline pane is removed
58
59        // Use SessionWidget for buffer-based rendering (header, transcript, overlays)
60        SessionWidget::new(self)
61            .header_lines(header_lines.clone())
62            .header_area(header_area)
63            .transcript_area(transcript_area)
64            .navigation_area(navigation_area) // Pass empty navigation area
65            .render(viewport, frame.buffer_mut());
66
67        // Handle frame-based rendering for components that need it
68        // Note: header, transcript, and overlays are handled by SessionWidget
69        // Timeline pane has been removed, so no navigation rendering
70        self.render_input(frame, input_area);
71        if let Some(modal_area) = modal_area {
72            render::render_modal(self, frame, modal_area);
73        } else {
74            render::render_modal(self, frame, viewport);
75        }
76        if let Some(file_palette_area) = file_palette_area {
77            render::render_file_palette(self, frame, file_palette_area);
78        }
79        if let Some(history_picker_area) = history_picker_area {
80            render::render_history_picker(self, frame, history_picker_area);
81        }
82        if let Some(slash_area) = slash_area {
83            slash::render_slash_palette(self, frame, slash_area);
84        }
85
86        // Render diff preview modal if active
87        if self.diff_preview.is_some() {
88            diff_preview::render_diff_preview(self, frame, viewport);
89        }
90
91        // Apply mouse text selection highlight
92        if self.mouse_selection.has_selection || self.mouse_selection.is_selecting {
93            self.mouse_selection
94                .apply_highlight(frame.buffer_mut(), viewport);
95
96            // Copy to clipboard once when selection is finalized
97            if self.mouse_selection.needs_copy() {
98                let text = self
99                    .mouse_selection
100                    .extract_text(frame.buffer_mut(), viewport);
101                if !text.is_empty() {
102                    MouseSelectionState::copy_to_clipboard(&text);
103                }
104                self.mouse_selection.mark_copied();
105            }
106        }
107    }
108
109    #[allow(dead_code)]
110    pub(crate) fn render_message_spans(&self, index: usize) -> Vec<Span<'static>> {
111        let Some(line) = self.lines.get(index) else {
112            return vec![Span::raw(String::new())];
113        };
114        message_renderer::render_message_spans(
115            line,
116            &self.theme,
117            &self.labels,
118            |kind| self.prefix_text(kind),
119            |line| self.prefix_style(line),
120            |kind| self.text_fallback(kind),
121        )
122    }
123}