Skip to main content

vtcode_tui/core_tui/app/session/
mod.rs

1use std::collections::VecDeque;
2
3pub(super) use ratatui::crossterm::event::{
4    Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, MouseEvent, MouseEventKind,
5};
6pub(super) use ratatui::prelude::*;
7pub(super) use ratatui::widgets::Clear;
8pub(super) use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
9
10use crate::core_tui::app::types::{
11    DiffOverlayRequest, DiffPreviewState, InlineCommand, InlineEvent, SlashCommandItem,
12};
13use crate::core_tui::runner::TuiSessionDriver;
14use crate::core_tui::session::Session as CoreSessionState;
15
16pub mod diff_preview;
17mod events;
18pub mod file_palette;
19pub mod history_picker;
20mod impl_events;
21mod impl_render;
22mod layout;
23mod palette;
24pub mod render;
25pub mod slash;
26pub mod slash_palette;
27pub mod trust;
28
29use self::file_palette::FilePalette;
30use self::history_picker::HistoryPickerState;
31use self::slash_palette::SlashPalette;
32
33/// App-level session that layers VT Code features on top of the core session.
34pub struct AppSession {
35    pub(crate) core: CoreSessionState,
36    pub(crate) file_palette: Option<FilePalette>,
37    pub(crate) file_palette_active: bool,
38    pub(crate) inline_lists_visible: bool,
39    pub(crate) slash_palette: SlashPalette,
40    pub(crate) history_picker_state: HistoryPickerState,
41    pub(crate) show_task_panel: bool,
42    pub(crate) task_panel_lines: Vec<String>,
43    pub(crate) diff_preview_state: Option<DiffPreviewState>,
44    pub(crate) diff_overlay_queue: VecDeque<DiffOverlayRequest>,
45}
46
47pub(super) type Session = AppSession;
48
49impl AppSession {
50    pub fn new_with_logs(
51        theme: crate::core_tui::types::InlineTheme,
52        placeholder: Option<String>,
53        view_rows: u16,
54        show_logs: bool,
55        appearance: Option<crate::core_tui::session::config::AppearanceConfig>,
56        slash_commands: Vec<SlashCommandItem>,
57        app_name: String,
58    ) -> Self {
59        let core = CoreSessionState::new_with_logs(
60            theme,
61            placeholder,
62            view_rows,
63            show_logs,
64            appearance,
65            app_name,
66        );
67
68        Self {
69            core,
70            file_palette: None,
71            file_palette_active: false,
72            inline_lists_visible: true,
73            slash_palette: SlashPalette::with_commands(slash_commands),
74            history_picker_state: HistoryPickerState::new(),
75            show_task_panel: false,
76            task_panel_lines: Vec::new(),
77            diff_preview_state: None,
78            diff_overlay_queue: VecDeque::new(),
79        }
80    }
81
82    pub fn new(
83        theme: crate::core_tui::types::InlineTheme,
84        placeholder: Option<String>,
85        view_rows: u16,
86    ) -> Self {
87        Self::new_with_logs(
88            theme,
89            placeholder,
90            view_rows,
91            true,
92            None,
93            Vec::new(),
94            "Agent TUI".to_string(),
95        )
96    }
97
98    pub fn core(&self) -> &CoreSessionState {
99        &self.core
100    }
101
102    pub fn core_mut(&mut self) -> &mut CoreSessionState {
103        &mut self.core
104    }
105
106    pub(crate) fn inline_lists_visible(&self) -> bool {
107        self.inline_lists_visible
108    }
109
110    pub(crate) fn toggle_inline_lists_visibility(&mut self) {
111        self.inline_lists_visible = !self.inline_lists_visible;
112        self.core.mark_dirty();
113    }
114
115    pub(crate) fn ensure_inline_lists_visible_for_trigger(&mut self) {
116        if !self.inline_lists_visible {
117            self.inline_lists_visible = true;
118            self.core.mark_dirty();
119        }
120    }
121
122    pub(crate) fn update_input_triggers(&mut self) {
123        if !self.core.input_enabled() {
124            if self.file_palette_active {
125                self.close_file_palette();
126            }
127            slash::clear_slash_suggestions(self);
128            return;
129        }
130
131        self.check_file_reference_trigger();
132        slash::update_slash_suggestions(self);
133    }
134
135    pub(crate) fn set_task_panel_visible(&mut self, visible: bool) {
136        if self.show_task_panel != visible {
137            self.show_task_panel = visible;
138            self.core.mark_dirty();
139        }
140    }
141
142    pub(crate) fn set_task_panel_lines(&mut self, lines: Vec<String>) {
143        self.task_panel_lines = lines;
144        self.core.mark_dirty();
145    }
146
147    pub(crate) fn diff_preview_state(&self) -> Option<&DiffPreviewState> {
148        self.diff_preview_state.as_ref()
149    }
150
151    pub(crate) fn diff_preview_state_mut(&mut self) -> Option<&mut DiffPreviewState> {
152        self.diff_preview_state.as_mut()
153    }
154
155    pub(crate) fn show_diff_overlay(&mut self, request: DiffOverlayRequest) {
156        if self.diff_preview_state.is_some() {
157            self.diff_overlay_queue.push_back(request);
158            return;
159        }
160
161        let mut state = DiffPreviewState::new_with_mode(
162            request.file_path,
163            request.before,
164            request.after,
165            request.hunks,
166            request.mode,
167        );
168        state.current_hunk = request.current_hunk;
169        self.diff_preview_state = Some(state);
170        self.core.set_input_enabled(false);
171        self.core.set_cursor_visible(false);
172        self.core.mark_dirty();
173    }
174
175    pub(crate) fn close_diff_overlay(&mut self) {
176        if self.diff_preview_state.is_none() {
177            return;
178        }
179        self.diff_preview_state = None;
180        self.core.set_input_enabled(true);
181        self.core.set_cursor_visible(true);
182        if let Some(next) = self.diff_overlay_queue.pop_front() {
183            self.show_diff_overlay(next);
184            return;
185        }
186        self.core.mark_dirty();
187    }
188
189    pub fn handle_command(&mut self, command: InlineCommand) {
190        match command {
191            InlineCommand::SetTaskPanelVisible(visible) => {
192                self.set_task_panel_visible(visible);
193            }
194            InlineCommand::SetTaskPanelLines(lines) => {
195                self.set_task_panel_lines(lines);
196            }
197            InlineCommand::LoadFilePalette { files, workspace } => {
198                self.load_file_palette(files, workspace);
199            }
200            InlineCommand::SetInput(value) => {
201                self.core
202                    .handle_command(crate::core_tui::types::InlineCommand::SetInput(value));
203                self.update_input_triggers();
204            }
205            InlineCommand::ApplySuggestedPrompt(value) => {
206                self.core.handle_command(
207                    crate::core_tui::types::InlineCommand::ApplySuggestedPrompt(value),
208                );
209                self.update_input_triggers();
210            }
211            InlineCommand::ClearInput => {
212                self.core
213                    .handle_command(crate::core_tui::types::InlineCommand::ClearInput);
214                self.update_input_triggers();
215            }
216            InlineCommand::OpenHistoryPicker => {
217                events::open_history_picker(self);
218            }
219            InlineCommand::CloseOverlay => {
220                if self.diff_preview_state.is_some() {
221                    self.close_diff_overlay();
222                } else {
223                    self.core
224                        .handle_command(crate::core_tui::types::InlineCommand::CloseOverlay);
225                }
226            }
227            InlineCommand::ShowOverlay { request } => match *request {
228                crate::core_tui::app::types::OverlayRequest::Diff(request) => {
229                    self.show_diff_overlay(request);
230                }
231                crate::core_tui::app::types::OverlayRequest::Modal(request) => {
232                    self.core
233                        .show_overlay(crate::core_tui::types::OverlayRequest::Modal(
234                            request.into(),
235                        ));
236                }
237                crate::core_tui::app::types::OverlayRequest::List(request) => {
238                    self.core
239                        .show_overlay(crate::core_tui::types::OverlayRequest::List(request.into()));
240                }
241                crate::core_tui::app::types::OverlayRequest::Wizard(request) => {
242                    self.core
243                        .show_overlay(crate::core_tui::types::OverlayRequest::Wizard(
244                            request.into(),
245                        ));
246                }
247            },
248            _ => {
249                if let Some(core_cmd) = to_core_command(&command) {
250                    self.core.handle_command(core_cmd);
251                }
252            }
253        }
254    }
255}
256
257impl std::ops::Deref for AppSession {
258    type Target = CoreSessionState;
259
260    fn deref(&self) -> &Self::Target {
261        &self.core
262    }
263}
264
265impl std::ops::DerefMut for AppSession {
266    fn deref_mut(&mut self) -> &mut Self::Target {
267        &mut self.core
268    }
269}
270
271fn to_core_command(command: &InlineCommand) -> Option<crate::core_tui::types::InlineCommand> {
272    use crate::core_tui::types::InlineCommand as CoreCommand;
273
274    Some(match command {
275        InlineCommand::AppendLine { kind, segments } => CoreCommand::AppendLine {
276            kind: *kind,
277            segments: segments.clone(),
278        },
279        InlineCommand::AppendPastedMessage {
280            kind,
281            text,
282            line_count,
283        } => CoreCommand::AppendPastedMessage {
284            kind: *kind,
285            text: text.clone(),
286            line_count: *line_count,
287        },
288        InlineCommand::Inline { kind, segment } => CoreCommand::Inline {
289            kind: *kind,
290            segment: segment.clone(),
291        },
292        InlineCommand::ReplaceLast {
293            count,
294            kind,
295            lines,
296            link_ranges,
297        } => CoreCommand::ReplaceLast {
298            count: *count,
299            kind: *kind,
300            lines: lines.clone(),
301            link_ranges: link_ranges.clone(),
302        },
303        InlineCommand::SetPrompt { prefix, style } => CoreCommand::SetPrompt {
304            prefix: prefix.clone(),
305            style: style.clone(),
306        },
307        InlineCommand::SetPlaceholder { hint, style } => CoreCommand::SetPlaceholder {
308            hint: hint.clone(),
309            style: style.clone(),
310        },
311        InlineCommand::SetMessageLabels { agent, user } => CoreCommand::SetMessageLabels {
312            agent: agent.clone(),
313            user: user.clone(),
314        },
315        InlineCommand::SetHeaderContext { context } => CoreCommand::SetHeaderContext {
316            context: context.clone(),
317        },
318        InlineCommand::SetInputStatus { left, right } => CoreCommand::SetInputStatus {
319            left: left.clone(),
320            right: right.clone(),
321        },
322        InlineCommand::SetTheme { theme } => CoreCommand::SetTheme {
323            theme: theme.clone(),
324        },
325        InlineCommand::SetAppearance { appearance } => CoreCommand::SetAppearance {
326            appearance: appearance.clone(),
327        },
328        InlineCommand::SetVimModeEnabled(enabled) => CoreCommand::SetVimModeEnabled(*enabled),
329        InlineCommand::SetQueuedInputs { entries } => CoreCommand::SetQueuedInputs {
330            entries: entries.clone(),
331        },
332        InlineCommand::SetCursorVisible(value) => CoreCommand::SetCursorVisible(*value),
333        InlineCommand::SetInputEnabled(value) => CoreCommand::SetInputEnabled(*value),
334        InlineCommand::SetInput(value) => CoreCommand::SetInput(value.clone()),
335        InlineCommand::ApplySuggestedPrompt(value) => {
336            CoreCommand::ApplySuggestedPrompt(value.clone())
337        }
338        InlineCommand::ClearInput => CoreCommand::ClearInput,
339        InlineCommand::ForceRedraw => CoreCommand::ForceRedraw,
340        InlineCommand::CloseOverlay => CoreCommand::CloseOverlay,
341        InlineCommand::ClearScreen => CoreCommand::ClearScreen,
342        InlineCommand::SuspendEventLoop => CoreCommand::SuspendEventLoop,
343        InlineCommand::ResumeEventLoop => CoreCommand::ResumeEventLoop,
344        InlineCommand::ClearInputQueue => CoreCommand::ClearInputQueue,
345        InlineCommand::SetEditingMode(mode) => CoreCommand::SetEditingMode(*mode),
346        InlineCommand::SetAutonomousMode(enabled) => CoreCommand::SetAutonomousMode(*enabled),
347        InlineCommand::SetSkipConfirmations(skip) => CoreCommand::SetSkipConfirmations(*skip),
348        InlineCommand::Shutdown => CoreCommand::Shutdown,
349        InlineCommand::SetReasoningStage(stage) => CoreCommand::SetReasoningStage(stage.clone()),
350        InlineCommand::SetTaskPanelVisible(_)
351        | InlineCommand::SetTaskPanelLines(_)
352        | InlineCommand::LoadFilePalette { .. }
353        | InlineCommand::OpenHistoryPicker
354        | InlineCommand::ShowOverlay { .. } => return None,
355    })
356}
357
358impl TuiSessionDriver for AppSession {
359    type Command = InlineCommand;
360    type Event = InlineEvent;
361
362    fn handle_command(&mut self, command: Self::Command) {
363        AppSession::handle_command(self, command);
364    }
365
366    fn handle_event(
367        &mut self,
368        event: CrosstermEvent,
369        events: &UnboundedSender<Self::Event>,
370        callback: Option<&(dyn Fn(&Self::Event) + Send + Sync + 'static)>,
371    ) {
372        AppSession::handle_event(self, event, events, callback);
373    }
374
375    fn handle_tick(&mut self) {
376        self.core.handle_tick();
377    }
378
379    fn render(&mut self, frame: &mut Frame<'_>) {
380        AppSession::render(self, frame);
381    }
382
383    fn take_redraw(&mut self) -> bool {
384        self.core.take_redraw()
385    }
386
387    fn use_steady_cursor(&self) -> bool {
388        self.core.use_steady_cursor()
389    }
390
391    fn should_exit(&self) -> bool {
392        self.core.should_exit()
393    }
394
395    fn request_exit(&mut self) {
396        self.core.request_exit();
397    }
398
399    fn mark_dirty(&mut self) {
400        self.core.mark_dirty();
401    }
402
403    fn update_terminal_title(&mut self) {
404        self.core.update_terminal_title();
405    }
406
407    fn clear_terminal_title(&mut self) {
408        self.core.clear_terminal_title();
409    }
410
411    fn is_running_activity(&self) -> bool {
412        self.core.is_running_activity()
413    }
414
415    fn has_status_spinner(&self) -> bool {
416        self.core.has_status_spinner()
417    }
418
419    fn thinking_spinner_active(&self) -> bool {
420        self.core.thinking_spinner.is_active
421    }
422
423    fn has_active_navigation_ui(&self) -> bool {
424        self.core.has_active_overlay()
425            || self.diff_preview_state.is_some()
426            || self.file_palette_active
427            || self.history_picker_state.active
428            || slash::slash_navigation_available(self)
429    }
430
431    fn apply_coalesced_scroll(&mut self, line_delta: i32, page_delta: i32) {
432        self.core.apply_coalesced_scroll(line_delta, page_delta);
433    }
434
435    fn set_show_logs(&mut self, show: bool) {
436        self.core.show_logs = show;
437    }
438
439    fn set_active_pty_sessions(
440        &mut self,
441        sessions: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
442    ) {
443        self.core.active_pty_sessions = sessions;
444    }
445
446    fn set_workspace_root(&mut self, root: Option<std::path::PathBuf>) {
447        self.core.set_workspace_root(root);
448    }
449
450    fn set_log_receiver(&mut self, receiver: UnboundedReceiver<crate::core_tui::log::LogEntry>) {
451        self.core.set_log_receiver(receiver);
452    }
453}