Skip to main content

vtcode_ui/tui/core_tui/session/
impl_input.rs

1use super::*;
2#[cfg_attr(not(vendored_crossterm), allow(unused_imports))]
3use vtcode_commons::ansi_capabilities::ColorScheme;
4
5impl Session {
6    pub(crate) fn cursor(&self) -> usize {
7        self.input_manager.cursor()
8    }
9
10    pub(crate) fn set_input(&mut self, text: impl Into<String>) {
11        self.input_manager.set_content(text.into());
12        self.input_compact_mode = self.input_compact_placeholder().is_some();
13        self.mark_dirty();
14    }
15
16    pub(crate) fn set_cursor(&mut self, pos: usize) {
17        self.input_manager.set_cursor(pos);
18        self.mark_dirty();
19    }
20
21    #[cfg_attr(feature = "profiling", hotpath::measure)]
22    pub(crate) fn process_key(&mut self, key: KeyEvent) -> Option<InlineEvent> {
23        events::process_key(self, key)
24    }
25
26    pub fn handle_command(&mut self, command: InlineCommand) {
27        let mut command_needs_redraw = true;
28
29        // Track streaming state: set when agent starts responding
30        if matches!(
31            &command,
32            InlineCommand::AppendLine { kind: InlineMessageKind::Agent, segments }
33                if !segments.is_empty()
34        ) || matches!(
35            &command,
36            InlineCommand::AppendPastedMessage { kind: InlineMessageKind::Agent, text, .. }
37                if !text.is_empty()
38        ) || matches!(
39            &command,
40            InlineCommand::Inline { kind: InlineMessageKind::Agent, segment }
41                if !segment.text.is_empty()
42        ) {
43            self.is_streaming_final_answer = true;
44        }
45
46        // Clear streaming state on turn completion (status cleared)
47        if let InlineCommand::SetInputStatus { left, right } | InlineCommand::SetConfiguredInputStatus { left, right } =
48            &command
49            && self.is_streaming_final_answer
50            && left.is_none()
51            && right.is_none()
52        {
53            self.is_streaming_final_answer = false;
54        }
55
56        match command {
57            InlineCommand::AppendLine { kind, segments } => {
58                let previous_max_offset = self.current_max_scroll_offset();
59                self.clear_thinking_spinner_if_active(kind);
60                self.push_line(kind, segments);
61                self.adjust_scroll_after_change(previous_max_offset);
62                self.request_transcript_clear();
63            }
64            InlineCommand::AppendPastedMessage { kind, text, line_count } => {
65                self.clear_thinking_spinner_if_active(kind);
66                self.append_pasted_message(kind, text, line_count);
67                self.request_transcript_clear();
68            }
69            InlineCommand::Inline { kind, segment } => {
70                self.clear_thinking_spinner_if_active(kind);
71                self.append_inline(kind, segment);
72                self.request_transcript_clear();
73            }
74            InlineCommand::ReplaceLast { count, kind, lines, link_ranges } => {
75                self.clear_thinking_spinner_if_active(kind);
76                self.replace_last(count, kind, lines, link_ranges);
77                self.request_transcript_clear();
78            }
79            InlineCommand::SetPrompt { prefix, style } => {
80                self.prompt_prefix = prefix;
81                self.prompt_style = style;
82                self.ensure_prompt_style_color();
83                self.input_render_cache = None;
84            }
85            InlineCommand::SetPlaceholder { hint, style } => {
86                self.placeholder = hint;
87                self.placeholder_style = style;
88                self.input_render_cache = None;
89            }
90            InlineCommand::SetMessageLabels { agent, user } => {
91                self.labels.agent = agent.filter(|label| !label.is_empty());
92                self.labels.user = user.filter(|label| !label.is_empty());
93                self.invalidate_transcript_cache();
94                self.invalidate_scroll_metrics();
95            }
96            InlineCommand::SetHeaderContext { context } => {
97                let mut next_context = *context;
98                next_context.reasoning_stage = self.header_context.reasoning_stage.clone();
99                next_context.primary_agent = self.header_context.primary_agent.clone();
100                next_context.primary_agent_color = self.header_context.primary_agent_color.clone();
101                self.header_context = next_context;
102                self.invalidate_header_cache();
103            }
104            InlineCommand::SetInputStatus { left, right } => {
105                self.apply_input_status(left, right);
106            }
107            InlineCommand::SetConfiguredInputStatus { left, right } => {
108                self.footer_context_configured = true;
109                self.footer_context_right = right.clone();
110                self.footer_context_status = left
111                    .as_deref()
112                    .map(str::trim)
113                    .filter(|value| !value.is_empty())
114                    .map(str::to_owned);
115                self.apply_input_status(left, right);
116            }
117            InlineCommand::ProgramStatus(update) => {
118                self.program_status.apply(update);
119                if matches!(update, vtcode_commons::program_status::ProgramStatusUpdate::Configure { enabled: true }) {
120                    self.program_status.children(&self.local_agents);
121                }
122                command_needs_redraw = false;
123            }
124            InlineCommand::UpdateProgress(update) => {
125                self.program_status.progress(update);
126                if self.progress.apply(update) {
127                    self.request_transcript_clear();
128                } else {
129                    command_needs_redraw = false;
130                }
131            }
132            InlineCommand::SetActivityState(state) => {
133                self.program_status.activity(state);
134                self.activity_state = state;
135                self.input_status_left = state.status().map(ToOwned::to_owned);
136                let enabled = !state.is_busy();
137                self.set_input_enabled(enabled);
138                if let Some(ActiveOverlay::Modal(overlay)) = self.active_overlay.as_mut() {
139                    overlay.restore_cursor = enabled;
140                }
141                self.cursor_visible = enabled && !self.has_active_overlay();
142                // ActivityState contributes the cached header badge as well as
143                // the uncached footer/status presentation.
144                self.invalidate_header_cache();
145            }
146            InlineCommand::SetTerminalTitleItems { items } => {
147                if self.terminal_title_items != items {
148                    self.terminal_title_items = items;
149                    self.mark_visual_dirty();
150                } else {
151                    command_needs_redraw = false;
152                }
153            }
154            InlineCommand::SetTerminalTitleThreadLabel { label } => {
155                let label = label.filter(|value| !value.trim().is_empty());
156                if self.terminal_title_thread_label != label {
157                    self.terminal_title_thread_label = label;
158                    self.mark_visual_dirty();
159                } else {
160                    command_needs_redraw = false;
161                }
162            }
163            InlineCommand::SetTerminalTitleGitBranch { branch } => {
164                let branch = branch.filter(|value| !value.trim().is_empty());
165                if self.terminal_title_git_branch != branch {
166                    self.terminal_title_git_branch = branch;
167                    self.mark_visual_dirty();
168                } else {
169                    command_needs_redraw = false;
170                }
171            }
172            InlineCommand::SetTheme { theme } => {
173                let previous_theme = self.theme.clone();
174                self.theme = theme.clone();
175                self.styles.set_theme(theme);
176                self.retint_lines_for_theme_change(&previous_theme);
177                self.ensure_prompt_style_color();
178                self.invalidate_header_cache();
179                self.invalidate_transcript_cache();
180            }
181            InlineCommand::SetColorSchemeAuto { enabled } => {
182                self.auto_color_scheme = enabled;
183                command_needs_redraw = false;
184            }
185            InlineCommand::SetAppearance { appearance } => {
186                self.appearance = appearance;
187                self.invalidate_header_cache();
188                self.invalidate_transcript_cache();
189                self.invalidate_scroll_metrics();
190            }
191            InlineCommand::SetFullscreenInteraction { interaction } => {
192                self.fullscreen.interaction = interaction;
193                self.mark_visual_dirty();
194            }
195            InlineCommand::SetVimModeEnabled(enabled) => {
196                self.vim_state.set_enabled(enabled);
197                self.mark_visual_dirty();
198            }
199            InlineCommand::SetQueuedInputs { entries } => {
200                self.set_queued_inputs_entries(entries);
201                self.mark_visual_dirty();
202            }
203            InlineCommand::SetSubprocessEntries { entries } => {
204                self.subprocess_entries = entries;
205                self.invalidate_sidebar_cache();
206            }
207            InlineCommand::SetSubagentPreview { text } => {
208                self.subagent_preview = text.filter(|value| !value.trim().is_empty());
209                self.invalidate_sidebar_cache();
210            }
211            InlineCommand::SetPrimaryAgent { name, color } => {
212                self.header_context.primary_agent = name.filter(|value| !value.trim().is_empty());
213                self.header_context.primary_agent_color = color.filter(|value| !value.trim().is_empty());
214                self.invalidate_header_cache();
215            }
216            InlineCommand::SetCursorVisible(value) => {
217                self.cursor_visible = value;
218            }
219            InlineCommand::SetInputEnabled(value) => {
220                self.set_input_enabled(value);
221            }
222            InlineCommand::SetImageInputEnabled(value) => {
223                self.image_input_enabled = value;
224            }
225            InlineCommand::SetInput(content) => {
226                // Check if the content appears to be an error message
227                // If it looks like an error, redirect to transcript instead
228                if Self::is_error_content(&content) {
229                    // Add error to transcript instead of input field
230                    crate::tui::utils::transcript::display_error(&content);
231                } else {
232                    self.clear_suggested_prompt_state();
233                    self.clear_inline_prompt_suggestion();
234                    self.input_manager.set_content(content);
235                    self.input_compact_mode = self.input_compact_placeholder().is_some();
236                    self.scroll_manager.set_offset(0);
237                }
238            }
239            InlineCommand::RestoreInputDraft(input) => {
240                self.clear_suggested_prompt_state();
241                self.clear_inline_prompt_suggestion();
242                self.input_manager.set_content(input.text);
243                self.input_manager.set_attachments(input.attachments);
244                self.input_compact_mode = self.input_compact_placeholder().is_some();
245                self.scroll_manager.set_offset(0);
246            }
247            InlineCommand::ApplySuggestedPrompt(content) => {
248                self.apply_suggested_prompt(content);
249                self.scroll_manager.set_offset(0);
250            }
251            InlineCommand::SetInlinePromptSuggestion { suggestion, llm_generated } => {
252                self.set_inline_prompt_suggestion(suggestion, llm_generated);
253            }
254            InlineCommand::ClearInlinePromptSuggestion => {
255                self.clear_inline_prompt_suggestion();
256            }
257            InlineCommand::ClearInput => {
258                command::clear_input(self);
259            }
260            InlineCommand::ForceRedraw => {
261                self.mark_dirty();
262            }
263            InlineCommand::ShowOverlay { request } => {
264                self.clear_inline_prompt_suggestion();
265                self.show_overlay(*request);
266            }
267            InlineCommand::CloseOverlay => {
268                self.close_overlay();
269            }
270            InlineCommand::ClearScreen => {
271                self.clear_screen();
272            }
273            InlineCommand::SuspendEventLoop
274            | InlineCommand::ResumeEventLoop
275            | InlineCommand::ClearInputQueue
276            | InlineCommand::StopEventStream
277            | InlineCommand::StartEventStream => {
278                // Handled by drive_terminal
279            }
280            InlineCommand::SetSkipConfirmations(skip) => {
281                if skip {
282                    // Permission bypass is handled by the runtime. Close only
283                    // an approval overlay that was already open; user-started
284                    // selection modals must remain interactive after this.
285                    self.close_overlay();
286                }
287            }
288            InlineCommand::Shutdown => {
289                self.request_exit();
290            }
291            InlineCommand::SetReasoningStage(stage) => {
292                self.header_context.reasoning_stage = stage;
293                self.invalidate_header_cache();
294            }
295        }
296        if command_needs_redraw {
297            self.mark_visual_dirty();
298        }
299    }
300
301    fn apply_input_status(&mut self, left: Option<String>, right: Option<String>) {
302        self.input_status_left = left;
303        self.input_status_right = right;
304        if self.thinking_spinner.is_active {
305            self.thinking_spinner.stop();
306        }
307        // The blocked/recovery badge can be derived from either status field.
308        self.invalidate_header_cache();
309    }
310
311    /// Apply an unsolicited terminal color-scheme report (Contour VT extension,
312    /// `CSI ? 997 ; Ps n`). No-op unless automatic color-scheme following is
313    /// enabled via `SetColorSchemeAuto`, which the host wires from
314    /// `ui.color_scheme_mode = "auto"`.
315    ///
316    /// The shared scheme override is updated first so theme matching and
317    /// suggestions agree with the reported scheme; the switch itself goes
318    /// through the regular `SetTheme` path so retinting and caches stay
319    /// consistent. The user's persisted theme preference is never touched.
320    #[cfg_attr(not(vendored_crossterm), allow(dead_code))]
321    pub(crate) fn apply_terminal_color_scheme_report(&mut self, dark: bool) {
322        if !self.auto_color_scheme {
323            return;
324        }
325        let scheme = if dark { ColorScheme::Dark } else { ColorScheme::Light };
326        vtcode_commons::ansi_capabilities::set_color_scheme_override(Some(scheme));
327        let active = crate::theme::active_theme_id();
328        if crate::theme::theme_matches_terminal_scheme(&active) {
329            return;
330        }
331        let next = crate::theme::theme_for_terminal_scheme_change(&active, dark);
332        if crate::theme::set_active_theme(next).is_err() {
333            return;
334        }
335        let theme = crate::tui::core_tui::style::theme_from_styles(&crate::theme::active_styles());
336        self.handle_command(InlineCommand::SetTheme { theme });
337    }
338}