vtcode_tui/core_tui/app/session/
impl_render.rs1use 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}