Skip to main content

vtcode_tui/core_tui/app/session/
impl_render.rs

1use super::layout::{
2    BottomPanelKind, resolve_bottom_panel_spec, split_input_and_bottom_panel_area,
3};
4use super::*;
5use crate::config::constants::ui;
6use crate::core_tui::app::session::transient::TransientSurface;
7use crate::core_tui::session::render as core_render;
8use crate::core_tui::session::{inline_list, list_panel, message_renderer};
9
10impl Session {
11    pub fn render(&mut self, frame: &mut Frame<'_>) {
12        let Some(viewport) = self.core.begin_frame(frame) else {
13            return;
14        };
15        let mut metrics = self.core.measure_frame(viewport);
16        let local_agents_captures_input = self.inline_lists_visible()
17            && matches!(
18                self.visible_bottom_docked_surface(),
19                Some(TransientSurface::LocalAgents)
20            );
21        let panel = resolve_bottom_panel_spec(
22            self,
23            viewport,
24            metrics.header_height,
25            if local_agents_captures_input {
26                0
27            } else {
28                metrics.input_core_height
29            },
30        );
31        if local_agents_captures_input {
32            metrics.input_core_height = 0;
33        }
34        let layout = self
35            .core
36            .build_frame_layout(viewport, metrics, panel.height);
37        self.core.set_modal_list_area(None);
38        let transcript_area = layout.main_area;
39        let (input_area, bottom_panel_area) = if matches!(panel.kind, BottomPanelKind::LocalAgents)
40        {
41            (
42                Rect::new(
43                    layout.input_area.x,
44                    layout.input_area.y,
45                    layout.input_area.width,
46                    0,
47                ),
48                Some(layout.input_area),
49            )
50        } else {
51            split_input_and_bottom_panel_area(layout.input_area, panel.height)
52        };
53        self.core.set_bottom_panel_area(bottom_panel_area);
54        self.core.render_base_frame(frame, &layout, transcript_area);
55        self.core.render_input(frame, input_area);
56        if let Some(panel_area) = bottom_panel_area {
57            match panel.kind {
58                BottomPanelKind::AgentPalette => {
59                    render::render_agent_palette(self, frame, panel_area);
60                }
61                BottomPanelKind::FilePalette => {
62                    render::render_file_palette(self, frame, panel_area);
63                }
64                BottomPanelKind::HistoryPicker => {
65                    render::render_history_picker(self, frame, panel_area);
66                }
67                BottomPanelKind::SlashPalette => {
68                    slash::render_slash_palette(self, frame, panel_area);
69                }
70                BottomPanelKind::TaskPanel => {
71                    render_task_panel(self, frame, panel_area);
72                }
73                BottomPanelKind::LocalAgents => {
74                    render::render_local_agents(self, frame, panel_area);
75                }
76                BottomPanelKind::None => {
77                    frame.render_widget(Clear, panel_area);
78                }
79            }
80        }
81
82        if self.has_active_overlay() {
83            core_render::render_modal(
84                self,
85                frame,
86                core_render::floating_modal_area(layout.viewport),
87            );
88        }
89
90        if self.diff_preview_state().is_some() {
91            diff_preview::render_diff_preview(self, frame, layout.viewport);
92        }
93        if let Some(mut state) = self.transcript_review_state.take() {
94            let width = transcript_review::review_content_width(layout.viewport);
95            let height = layout.viewport.height.saturating_sub(4);
96            state.refresh(self, width, height);
97            transcript_review::render_transcript_review(self, frame, layout.viewport, &state);
98            self.transcript_review_state = Some(state);
99        }
100        self.core.finalize_mouse_selection(frame, layout.viewport);
101    }
102
103    #[expect(dead_code)]
104    pub(crate) fn render_message_spans(&self, index: usize) -> Vec<Span<'static>> {
105        let Some(line) = self.core.lines.get(index) else {
106            return vec![Span::raw(String::new())];
107        };
108        message_renderer::render_message_spans(
109            line,
110            &self.core.theme,
111            &self.core.labels,
112            |kind| self.core.prefix_text(kind),
113            |line| self.core.prefix_style(line),
114            |kind| self.core.text_fallback(kind),
115        )
116    }
117}
118
119fn render_task_panel(session: &mut Session, frame: &mut Frame<'_>, area: Rect) {
120    if area.width == 0 || area.height == 0 {
121        return;
122    }
123
124    let rows = if session.task_panel_lines.is_empty() {
125        vec![(
126            inline_list::InlineListRow::single(
127                ui::PLAN_STATUS_EMPTY.to_string().into(),
128                session.core.header_secondary_style(),
129            ),
130            1,
131        )]
132    } else {
133        session
134            .task_panel_lines
135            .iter()
136            .map(|line| {
137                (
138                    inline_list::InlineListRow::single(
139                        line.clone().into(),
140                        session.core.header_secondary_style(),
141                    ),
142                    1,
143                )
144            })
145            .collect()
146    };
147    let item_count = session.task_panel_lines.len();
148    let sections = list_panel::SharedListPanelSections {
149        header: vec![Line::from(vec![Span::styled(
150            ui::PLAN_BLOCK_TITLE.to_string(),
151            session.core.section_title_style(),
152        )])],
153        info: vec![Line::from(format!(
154            "{} item{}",
155            item_count,
156            if item_count == 1 { "" } else { "s" }
157        ))],
158        search: None,
159    };
160    let styles = list_panel::SharedListPanelStyles {
161        base_style: session.core.styles.default_style(),
162        selected_style: Some(session.core.styles.modal_list_highlight_style()),
163        text_style: session.core.header_secondary_style(),
164        divider_style: None,
165        input_styles: list_panel::input_styles_from_theme(&session.core.theme),
166    };
167    let mut model = list_panel::StaticRowsListPanelModel {
168        rows,
169        selected: None,
170        offset: 0,
171        visible_rows: area.height as usize,
172    };
173    list_panel::render_shared_list_panel(frame, area, sections, styles, &mut model);
174}