vtcode_ui/tui/core_tui/session/
impl_input.rs1use 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 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 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 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 if Self::is_error_content(&content) {
229 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 }
280 InlineCommand::SetSkipConfirmations(skip) => {
281 if skip {
282 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 self.invalidate_header_cache();
309 }
310
311 #[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}