Skip to main content

vtcode_tui/core_tui/session/render/
modal_renderer.rs

1use super::*;
2
3pub fn render_modal(session: &mut Session, frame: &mut Frame<'_>, viewport: Rect) {
4    if viewport.width == 0 || viewport.height == 0 {
5        return;
6    }
7
8    // Auto-approve modals when skip_confirmations is set (for tests and headless mode)
9    if session.skip_confirmations
10        && let Some(mut modal) = session.modal.take()
11    {
12        if let Some(list) = &mut modal.list
13            && let Some(_selection) = list.current_selection()
14        {
15            // Note: We can't easily emit an event from here without access to the sender.
16            // Instead, we just clear the modal and assume the tool execution logic
17            // or whatever triggered the modal will check skip_confirmations as well.
18            // This is handled in ensure_tool_permission.
19        }
20        session.input_enabled = modal.restore_input;
21        session.cursor_visible = modal.restore_cursor;
22        session.needs_full_clear = true;
23        session.needs_redraw = true;
24        return;
25    }
26
27    let styles = modal_render_styles(session);
28    if let Some(wizard) = session.wizard_modal.as_mut() {
29        let _is_multistep = wizard.mode == crate::ui::tui::types::WizardModalMode::MultiStep;
30        let mut width_lines = Vec::new();
31        width_lines.push(wizard.question_header());
32        if let Some(step) = wizard.steps.get(wizard.current_step) {
33            width_lines.push(step.question.clone());
34        }
35        if let Some(notes) = wizard.notes_line() {
36            width_lines.push(notes);
37        }
38        width_lines.extend(wizard.instruction_lines());
39
40        let text_lines = width_lines.len();
41        let search_lines = wizard.search.as_ref().map(|_| 3).unwrap_or(0);
42        let area = compute_modal_area(viewport, text_lines, 0, search_lines, true);
43
44        let block = Block::bordered()
45            .title(Line::styled(wizard.title.clone(), styles.title))
46            .border_type(terminal_capabilities::get_border_type())
47            .border_style(styles.border);
48
49        frame.render_widget(Clear, area);
50        frame.render_widget(block, area);
51
52        if area.width <= 2 || area.height <= 2 {
53            return;
54        }
55
56        let inner = Rect {
57            x: area.x.saturating_add(1),
58            y: area.y.saturating_add(1),
59            width: area.width.saturating_sub(2),
60            height: area.height.saturating_sub(2),
61        };
62
63        if inner.width == 0 || inner.height == 0 {
64            return;
65        }
66
67        render_wizard_modal_body(frame, inner, wizard, &styles);
68        return;
69    }
70
71    let Some(modal) = session.modal.as_mut() else {
72        return;
73    };
74
75    let prompt_lines = if modal.secure_prompt.is_some() { 2 } else { 0 };
76    let search_lines = modal.search.as_ref().map(|_| 3).unwrap_or(0);
77    let area = compute_modal_area(
78        viewport,
79        modal.lines.len(),
80        prompt_lines,
81        search_lines,
82        modal.list.is_some(),
83    );
84
85    let block = Block::bordered()
86        .title(Line::styled(modal.title.clone(), styles.title))
87        .border_type(terminal_capabilities::get_border_type())
88        .border_style(styles.border);
89
90    frame.render_widget(Clear, area);
91    frame.render_widget(block, area);
92
93    if area.width <= 2 || area.height <= 2 {
94        return;
95    }
96
97    let inner = Rect {
98        x: area.x.saturating_add(1),
99        y: area.y.saturating_add(1),
100        width: area.width.saturating_sub(2),
101        height: area.height.saturating_sub(2),
102    };
103
104    if inner.width == 0 || inner.height == 0 {
105        return;
106    }
107
108    render_modal_body(
109        frame,
110        inner,
111        ModalBodyContext {
112            instructions: &modal.lines,
113            footer_hint: modal.footer_hint.as_deref(),
114            list: modal.list.as_mut(),
115            styles: &styles,
116            secure_prompt: modal.secure_prompt.as_ref(),
117            search: modal.search.as_ref(),
118            input: session.input_manager.content(),
119            cursor: session.input_manager.cursor(),
120        },
121    );
122}
123
124fn modal_render_styles(session: &Session) -> ModalRenderStyles {
125    ModalRenderStyles {
126        border: border_style(session),
127        highlight: modal_list_highlight_style(session),
128        badge: session.section_title_style().add_modifier(Modifier::DIM),
129        header: session.section_title_style(),
130        selectable: default_style(session).add_modifier(Modifier::BOLD),
131        detail: default_style(session).add_modifier(Modifier::DIM),
132        search_match: accent_style(session).add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
133        title: Style::default().add_modifier(Modifier::BOLD),
134        divider: default_style(session).add_modifier(Modifier::DIM | Modifier::ITALIC),
135        instruction_border: border_style(session),
136        instruction_title: session.section_title_style(),
137        instruction_bullet: accent_style(session).add_modifier(Modifier::BOLD),
138        instruction_body: default_style(session),
139        hint: default_style(session).add_modifier(Modifier::DIM | Modifier::ITALIC),
140    }
141}
142
143#[allow(dead_code)]
144pub(super) fn handle_tool_code_fence_marker(session: &mut Session, text: &str) -> bool {
145    let trimmed = text.trim();
146    let stripped = trimmed
147        .strip_prefix("```")
148        .or_else(|| trimmed.strip_prefix("~~~"));
149
150    let Some(rest) = stripped else {
151        return false;
152    };
153
154    if rest.contains("```") || rest.contains("~~~") {
155        return false;
156    }
157
158    if session.in_tool_code_fence {
159        session.in_tool_code_fence = false;
160        remove_trailing_empty_tool_line(session);
161    } else {
162        session.in_tool_code_fence = true;
163    }
164
165    true
166}
167
168#[allow(dead_code)]
169fn remove_trailing_empty_tool_line(session: &mut Session) {
170    let should_remove = session
171        .lines
172        .last()
173        .map(|line| line.kind == InlineMessageKind::Tool && line.segments.is_empty())
174        .unwrap_or(false);
175    if should_remove {
176        session.lines.pop();
177        invalidate_scroll_metrics(session);
178    }
179}