vtcode_tui/core_tui/session/render/
modal_renderer.rs1use super::*;
2use crate::ui::tui::session::modal::{
3 ModalBodyContext, ModalListState, ModalRenderStyles, render_modal_body,
4 render_wizard_modal_body,
5};
6
7const MAX_INLINE_MODAL_HEIGHT: u16 = 20;
8const MAX_INLINE_MODAL_HEIGHT_MULTILINE: u16 = 32;
9const MAX_INLINE_INSTRUCTION_ROWS: usize = 6;
10
11fn list_has_two_line_items(list: &ModalListState) -> bool {
12 list.visible_indices.iter().any(|&index| {
13 list.items.get(index).is_some_and(|item| {
14 item.subtitle
15 .as_ref()
16 .is_some_and(|subtitle| !subtitle.trim().is_empty())
17 })
18 })
19}
20
21fn list_row_cap(list: &ModalListState) -> usize {
22 if list_has_two_line_items(list) {
23 ui::INLINE_LIST_MAX_ROWS_MULTILINE
24 } else {
25 ui::INLINE_LIST_MAX_ROWS
26 }
27}
28
29fn list_desired_rows(list: &ModalListState) -> usize {
30 list.visible_indices.len().clamp(1, list_row_cap(list))
31}
32
33pub fn split_inline_modal_area(session: &Session, area: Rect) -> (Rect, Option<Rect>) {
34 if area.width == 0 || area.height == 0 {
35 return (area, None);
36 }
37
38 let multiline_list_present = if let Some(wizard) = session.wizard_modal.as_ref() {
39 wizard
40 .steps
41 .get(wizard.current_step)
42 .is_some_and(|step| list_has_two_line_items(&step.list))
43 } else if let Some(modal) = session.modal.as_ref() {
44 modal.list.as_ref().is_some_and(list_has_two_line_items)
45 } else {
46 false
47 };
48
49 let desired_lines = if let Some(wizard) = session.wizard_modal.as_ref() {
50 let mut lines = 0usize;
51 lines = lines.saturating_add(1); if wizard.search.is_some() {
53 lines = lines.saturating_add(1);
54 }
55 lines = lines.saturating_add(2); let list_rows = wizard
57 .steps
58 .get(wizard.current_step)
59 .map(|step| list_desired_rows(&step.list))
60 .unwrap_or(1);
61 lines = lines.saturating_add(list_rows);
62 if wizard
63 .steps
64 .get(wizard.current_step)
65 .is_some_and(|step| step.notes_active || !step.notes.is_empty())
66 {
67 lines = lines.saturating_add(1);
68 }
69 lines = lines.saturating_add(
70 wizard
71 .instruction_lines()
72 .len()
73 .min(MAX_INLINE_INSTRUCTION_ROWS),
74 );
75 lines
76 } else if let Some(modal) = session.modal.as_ref() {
77 let mut lines = modal.lines.len().clamp(1, MAX_INLINE_INSTRUCTION_ROWS);
78 if modal.search.is_some() {
79 lines = lines.saturating_add(1);
80 }
81 if modal.secure_prompt.is_some() {
82 lines = lines.saturating_add(2);
83 }
84 if let Some(list) = modal.list.as_ref() {
85 lines = lines.saturating_add(list_desired_rows(list));
86 } else {
87 lines = lines.saturating_add(1);
88 }
89 lines
90 } else {
91 return (area, None);
92 };
93
94 let max_panel_height = area.height.saturating_sub(1);
95 if max_panel_height == 0 {
96 return (area, None);
97 }
98
99 let min_height = ui::MODAL_MIN_HEIGHT.min(max_panel_height).max(1);
100 let modal_height_cap = if multiline_list_present {
101 MAX_INLINE_MODAL_HEIGHT_MULTILINE
102 } else {
103 MAX_INLINE_MODAL_HEIGHT
104 };
105 let capped_max = modal_height_cap.min(max_panel_height).max(min_height);
106 let desired_height = (desired_lines.min(u16::MAX as usize) as u16)
107 .max(min_height)
108 .min(capped_max);
109
110 let chunks =
111 Layout::vertical([Constraint::Min(1), Constraint::Length(desired_height)]).split(area);
112 (chunks[0], Some(chunks[1]))
113}
114
115pub fn render_modal(session: &mut Session, frame: &mut Frame<'_>, area: Rect) {
116 if area.width == 0 || area.height == 0 {
117 return;
118 }
119
120 if session.skip_confirmations
122 && let Some(mut modal) = session.modal.take()
123 {
124 if let Some(list) = &mut modal.list
125 && let Some(_selection) = list.current_selection()
126 {
127 }
132 session.input_enabled = modal.restore_input;
133 session.cursor_visible = modal.restore_cursor;
134 session.needs_full_clear = true;
135 session.needs_redraw = true;
136 return;
137 }
138
139 let styles = modal_render_styles(session);
140 if let Some(wizard) = session.wizard_modal.as_mut() {
141 frame.render_widget(Clear, area);
142 if area.width == 0 || area.height == 0 {
143 return;
144 }
145 render_wizard_modal_body(frame, area, wizard, &styles);
146 return;
147 }
148
149 let Some(modal) = session.modal.as_mut() else {
150 return;
151 };
152
153 frame.render_widget(Clear, area);
154 if area.width == 0 || area.height == 0 {
155 return;
156 }
157 render_modal_body(
158 frame,
159 area,
160 ModalBodyContext {
161 instructions: &modal.lines,
162 footer_hint: modal.footer_hint.as_deref(),
163 list: modal.list.as_mut(),
164 styles: &styles,
165 secure_prompt: modal.secure_prompt.as_ref(),
166 search: modal.search.as_ref(),
167 input: session.input_manager.content(),
168 cursor: session.input_manager.cursor(),
169 },
170 );
171}
172
173fn modal_render_styles(session: &Session) -> ModalRenderStyles {
174 ModalRenderStyles {
175 border: border_style(session),
176 highlight: modal_list_highlight_style(session),
177 badge: border_style(session).add_modifier(Modifier::DIM | Modifier::BOLD),
178 header: accent_style(session).add_modifier(Modifier::BOLD),
179 selectable: default_style(session),
180 detail: default_style(session).add_modifier(Modifier::DIM),
181 search_match: accent_style(session).add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
182 title: Style::default().add_modifier(Modifier::BOLD),
183 divider: default_style(session).add_modifier(Modifier::DIM | Modifier::ITALIC),
184 instruction_border: border_style(session),
185 instruction_title: session.section_title_style(),
186 instruction_bullet: accent_style(session).add_modifier(Modifier::BOLD),
187 instruction_body: default_style(session),
188 hint: default_style(session).add_modifier(Modifier::DIM | Modifier::ITALIC),
189 }
190}
191
192#[allow(dead_code)]
193pub(super) fn handle_tool_code_fence_marker(session: &mut Session, text: &str) -> bool {
194 let trimmed = text.trim();
195 let stripped = trimmed
196 .strip_prefix("```")
197 .or_else(|| trimmed.strip_prefix("~~~"));
198
199 let Some(rest) = stripped else {
200 return false;
201 };
202
203 if rest.contains("```") || rest.contains("~~~") {
204 return false;
205 }
206
207 if session.in_tool_code_fence {
208 session.in_tool_code_fence = false;
209 remove_trailing_empty_tool_line(session);
210 } else {
211 session.in_tool_code_fence = true;
212 }
213
214 true
215}
216
217#[allow(dead_code)]
218fn remove_trailing_empty_tool_line(session: &mut Session) {
219 let should_remove = session
220 .lines
221 .last()
222 .map(|line| line.kind == InlineMessageKind::Tool && line.segments.is_empty())
223 .unwrap_or(false);
224 if should_remove {
225 session.lines.pop();
226 invalidate_scroll_metrics(session);
227 }
228}