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, LocalAgentsTransientRequest,
12    SlashCommandItem, TaskPanelTransientRequest, TransientRequest,
13};
14use crate::core_tui::runner::TuiSessionDriver;
15use crate::core_tui::session::Session as CoreSessionState;
16use crate::core_tui::session::action::BindingStore;
17
18mod agent_palette;
19pub mod diff_preview;
20mod events;
21pub mod file_palette;
22pub mod history_picker;
23mod impl_events;
24mod impl_render;
25mod layout;
26mod local_agents;
27mod palette;
28pub mod render;
29pub mod slash;
30pub mod slash_palette;
31mod transcript_review;
32mod transient;
33pub mod trust;
34
35use self::file_palette::FilePalette;
36use self::history_picker::HistoryPickerState;
37use self::local_agents::LocalAgentsState;
38use self::slash_palette::SlashPalette;
39use self::transcript_review::TranscriptReviewState;
40use self::transient::{
41    TransientFocusPolicy, TransientHost, TransientSurface, TransientVisibilityChange,
42};
43use crate::options::FullscreenInteractionSettings;
44use agent_palette::AgentPalette;
45
46/// App-level session that layers VT Code features on top of the core session.
47pub struct AppSession {
48    pub(crate) core: CoreSessionState,
49    pub(crate) agent_palette: Option<AgentPalette>,
50    pub(crate) agent_palette_active: bool,
51    pub(crate) file_palette: Option<FilePalette>,
52    pub(crate) file_palette_active: bool,
53    pub(crate) inline_lists_visible: bool,
54    pub(crate) slash_palette: SlashPalette,
55    pub(crate) history_picker_state: HistoryPickerState,
56    local_agents_state: LocalAgentsState,
57    local_agents_auto_opened: bool,
58    pub(crate) show_task_panel: bool,
59    pub(crate) task_panel_lines: Vec<String>,
60    pub(crate) diff_preview_state: Option<DiffPreviewState>,
61    pub(crate) transcript_review_state: Option<TranscriptReviewState>,
62    pub(crate) diff_overlay_queue: VecDeque<DiffOverlayRequest>,
63    pub(crate) transient_host: TransientHost,
64}
65
66pub(super) type Session = AppSession;
67
68impl AppSession {
69    pub fn new_with_logs(
70        theme: crate::core_tui::types::InlineTheme,
71        placeholder: Option<String>,
72        view_rows: u16,
73        show_logs: bool,
74        appearance: Option<crate::core_tui::session::config::AppearanceConfig>,
75        slash_commands: Vec<SlashCommandItem>,
76        app_name: String,
77    ) -> Self {
78        let core = CoreSessionState::new_with_logs(
79            theme,
80            placeholder,
81            view_rows,
82            show_logs,
83            appearance,
84            app_name,
85        );
86
87        Self {
88            core,
89            agent_palette: None,
90            agent_palette_active: false,
91            file_palette: None,
92            file_palette_active: false,
93            inline_lists_visible: true,
94            slash_palette: SlashPalette::with_commands(slash_commands),
95            history_picker_state: HistoryPickerState::new(),
96            local_agents_state: LocalAgentsState::default(),
97            local_agents_auto_opened: false,
98            show_task_panel: false,
99            task_panel_lines: Vec::new(),
100            diff_preview_state: None,
101            transcript_review_state: None,
102            diff_overlay_queue: VecDeque::new(),
103            transient_host: TransientHost::default(),
104        }
105    }
106
107    pub fn new_with_logs_and_bindings(
108        theme: crate::core_tui::types::InlineTheme,
109        placeholder: Option<String>,
110        view_rows: u16,
111        show_logs: bool,
112        appearance: Option<crate::core_tui::session::config::AppearanceConfig>,
113        slash_commands: Vec<SlashCommandItem>,
114        app_name: String,
115        bindings: BindingStore,
116    ) -> Self {
117        let core = CoreSessionState::new_with_bindings(
118            theme,
119            placeholder,
120            view_rows,
121            show_logs,
122            appearance,
123            app_name,
124            bindings,
125        );
126
127        Self {
128            core,
129            agent_palette: None,
130            agent_palette_active: false,
131            file_palette: None,
132            file_palette_active: false,
133            inline_lists_visible: true,
134            slash_palette: SlashPalette::with_commands(slash_commands),
135            history_picker_state: HistoryPickerState::new(),
136            local_agents_state: LocalAgentsState::default(),
137            local_agents_auto_opened: false,
138            show_task_panel: false,
139            task_panel_lines: Vec::new(),
140            diff_preview_state: None,
141            transcript_review_state: None,
142            diff_overlay_queue: VecDeque::new(),
143            transient_host: TransientHost::default(),
144        }
145    }
146
147    pub fn new(
148        theme: crate::core_tui::types::InlineTheme,
149        placeholder: Option<String>,
150        view_rows: u16,
151    ) -> Self {
152        Self::new_with_logs(
153            theme,
154            placeholder,
155            view_rows,
156            true,
157            None,
158            Vec::new(),
159            "Agent TUI".to_string(),
160        )
161    }
162
163    pub fn core(&self) -> &CoreSessionState {
164        &self.core
165    }
166
167    pub fn core_mut(&mut self) -> &mut CoreSessionState {
168        &mut self.core
169    }
170
171    pub(crate) fn inline_lists_visible(&self) -> bool {
172        self.inline_lists_visible
173    }
174
175    pub(crate) fn toggle_inline_lists_visibility(&mut self) {
176        self.inline_lists_visible = !self.inline_lists_visible;
177        self.core.mark_dirty();
178    }
179
180    pub(crate) fn ensure_inline_lists_visible_for_trigger(&mut self) {
181        if !self.inline_lists_visible {
182            self.inline_lists_visible = true;
183            self.core.mark_dirty();
184        }
185    }
186
187    pub(crate) fn update_input_triggers(&mut self) {
188        if !self.core.input_enabled() {
189            return;
190        }
191
192        self.check_agent_reference_trigger();
193        self.check_file_reference_trigger();
194        slash::update_slash_suggestions(self);
195    }
196
197    pub(super) fn show_transient_surface(&mut self, surface: TransientSurface) -> bool {
198        let change = self.transient_host.show(surface);
199        if !change.changed() {
200            return false;
201        }
202
203        self.apply_transient_visibility_change(change);
204        true
205    }
206
207    pub(super) fn close_transient_surface(&mut self, surface: TransientSurface) -> bool {
208        let change = self.transient_host.hide(surface);
209        if !change.changed() {
210            return false;
211        }
212
213        self.apply_transient_visibility_change(change);
214        true
215    }
216
217    pub(super) fn finish_history_picker_interaction(&mut self, was_active: bool) {
218        if was_active && !self.history_picker_state.active {
219            self.close_transient_surface(TransientSurface::HistoryPicker);
220            self.update_input_triggers();
221        }
222    }
223
224    pub(crate) fn set_task_panel_visible(&mut self, visible: bool) {
225        if self.show_task_panel != visible {
226            self.show_task_panel = visible;
227            if visible {
228                self.show_transient_surface(TransientSurface::TaskPanel);
229            } else {
230                self.close_transient_surface(TransientSurface::TaskPanel);
231            }
232            self.core.mark_dirty();
233        }
234    }
235
236    pub(crate) fn visible_transient_surface(&self) -> Option<TransientSurface> {
237        self.transient_host.top()
238    }
239
240    pub(crate) fn visible_bottom_docked_surface(&self) -> Option<TransientSurface> {
241        self.transient_host.visible_bottom_docked()
242    }
243
244    pub(crate) fn history_picker_visible(&self) -> bool {
245        self.history_picker_state.active
246            && self
247                .transient_host
248                .is_visible(TransientSurface::HistoryPicker)
249    }
250
251    pub(crate) fn local_agents_visible(&self) -> bool {
252        self.transient_host
253            .is_visible(TransientSurface::LocalAgents)
254    }
255
256    pub(super) fn local_agents_loading_active(&self) -> bool {
257        self.local_agents_visible()
258            && self
259                .local_agents_state
260                .entries()
261                .iter()
262                .any(crate::core_tui::types::LocalAgentEntry::is_loading)
263    }
264
265    pub(crate) fn file_palette_visible(&self) -> bool {
266        self.file_palette_active
267            && self
268                .transient_host
269                .is_visible(TransientSurface::FilePalette)
270    }
271
272    pub(crate) fn agent_palette_visible(&self) -> bool {
273        self.agent_palette_active
274            && self
275                .transient_host
276                .is_visible(TransientSurface::AgentPalette)
277    }
278
279    pub(crate) fn slash_palette_visible(&self) -> bool {
280        !self.slash_palette.is_empty()
281            && self
282                .transient_host
283                .is_visible(TransientSurface::SlashPalette)
284    }
285
286    pub(crate) fn has_active_overlay(&self) -> bool {
287        self.core.has_active_overlay()
288            && self
289                .transient_host
290                .is_visible(TransientSurface::FloatingOverlay)
291    }
292
293    pub(crate) fn modal_state(&self) -> Option<&crate::core_tui::session::modal::ModalState> {
294        self.has_active_overlay()
295            .then(|| self.core.modal_state())
296            .flatten()
297    }
298
299    pub(crate) fn modal_state_mut(
300        &mut self,
301    ) -> Option<&mut crate::core_tui::session::modal::ModalState> {
302        if !self.has_active_overlay() {
303            return None;
304        }
305        self.core.modal_state_mut()
306    }
307
308    pub(crate) fn wizard_overlay(
309        &self,
310    ) -> Option<&crate::core_tui::session::modal::WizardModalState> {
311        self.has_active_overlay()
312            .then(|| self.core.wizard_overlay())
313            .flatten()
314    }
315
316    pub(crate) fn wizard_overlay_mut(
317        &mut self,
318    ) -> Option<&mut crate::core_tui::session::modal::WizardModalState> {
319        if !self.has_active_overlay() {
320            return None;
321        }
322        self.core.wizard_overlay_mut()
323    }
324
325    pub(crate) fn close_overlay(&mut self) {
326        if !self.has_active_overlay() {
327            return;
328        }
329
330        self.core.close_overlay();
331        if !self.core.has_active_overlay() {
332            self.close_transient_surface(TransientSurface::FloatingOverlay);
333        }
334    }
335
336    pub(crate) fn diff_preview_state(&self) -> Option<&DiffPreviewState> {
337        self.transient_host
338            .is_visible(TransientSurface::DiffPreview)
339            .then_some(())
340            .and(self.diff_preview_state.as_ref())
341    }
342
343    pub(crate) fn diff_preview_state_mut(&mut self) -> Option<&mut DiffPreviewState> {
344        if !self
345            .transient_host
346            .is_visible(TransientSurface::DiffPreview)
347        {
348            return None;
349        }
350        self.diff_preview_state.as_mut()
351    }
352
353    pub(crate) fn transcript_review_state(&self) -> Option<&TranscriptReviewState> {
354        self.transient_host
355            .is_visible(TransientSurface::TranscriptReview)
356            .then_some(())
357            .and(self.transcript_review_state.as_ref())
358    }
359
360    pub(crate) fn transcript_review_state_mut(&mut self) -> Option<&mut TranscriptReviewState> {
361        if !self
362            .transient_host
363            .is_visible(TransientSurface::TranscriptReview)
364        {
365            return None;
366        }
367        self.transcript_review_state.as_mut()
368    }
369
370    pub(crate) fn show_diff_overlay(&mut self, request: DiffOverlayRequest) {
371        if self.diff_preview_state.is_some() {
372            self.diff_overlay_queue.push_back(request);
373            return;
374        }
375
376        let mut state = DiffPreviewState::new_with_mode(
377            request.file_path,
378            request.before,
379            request.after,
380            request.hunks,
381            request.mode,
382        );
383        state.current_hunk = request.current_hunk;
384        self.diff_preview_state = Some(state);
385        self.show_transient_surface(TransientSurface::DiffPreview);
386        self.core.mark_dirty();
387    }
388
389    pub(crate) fn close_diff_overlay(&mut self) {
390        if self.diff_preview_state.is_none() {
391            return;
392        }
393        self.diff_preview_state = None;
394        if let Some(next) = self.diff_overlay_queue.pop_front() {
395            self.show_diff_overlay(next);
396            return;
397        }
398        self.close_transient_surface(TransientSurface::DiffPreview);
399        self.core.mark_dirty();
400    }
401
402    pub(crate) fn close_history_picker(&mut self) {
403        if !self.history_picker_state.active {
404            return;
405        }
406        self.history_picker_state
407            .cancel(&mut self.core.input_manager);
408        self.close_transient_surface(TransientSurface::HistoryPicker);
409        self.update_input_triggers();
410        self.mark_dirty();
411    }
412
413    pub(crate) fn open_transcript_review(&mut self, width: u16, height: u16) {
414        self.transcript_review_state = Some(TranscriptReviewState::open(self, width, height));
415        self.show_transient_surface(TransientSurface::TranscriptReview);
416        self.core.mark_dirty();
417    }
418
419    pub(crate) fn close_transcript_review(&mut self) {
420        if self.transcript_review_state.is_none() {
421            return;
422        }
423        self.transcript_review_state = None;
424        self.close_transient_surface(TransientSurface::TranscriptReview);
425        self.core.mark_dirty();
426    }
427
428    pub(crate) fn show_transient(&mut self, request: TransientRequest) {
429        self.core.clear_inline_prompt_suggestion();
430        match request {
431            TransientRequest::Modal(request) => {
432                self.core
433                    .show_overlay(crate::core_tui::types::OverlayRequest::Modal(
434                        request.into(),
435                    ));
436                self.show_transient_surface(TransientSurface::FloatingOverlay);
437            }
438            TransientRequest::List(request) => {
439                self.core
440                    .show_overlay(crate::core_tui::types::OverlayRequest::List(request.into()));
441                self.show_transient_surface(TransientSurface::FloatingOverlay);
442            }
443            TransientRequest::Wizard(request) => {
444                self.core
445                    .show_overlay(crate::core_tui::types::OverlayRequest::Wizard(
446                        request.into(),
447                    ));
448                self.show_transient_surface(TransientSurface::FloatingOverlay);
449            }
450            TransientRequest::Diff(request) => {
451                self.show_diff_overlay(request);
452            }
453            TransientRequest::FilePalette(request) => {
454                self.load_file_palette(request.files, request.workspace);
455                match request.visible {
456                    Some(true) => {
457                        self.ensure_inline_lists_visible_for_trigger();
458                        self.file_palette_active = true;
459                        self.show_transient_surface(TransientSurface::FilePalette);
460                    }
461                    Some(false) => {
462                        self.close_file_palette();
463                    }
464                    None => {}
465                }
466            }
467            TransientRequest::AgentPalette(request) => {
468                self.load_agent_palette(request.agents);
469                match request.visible {
470                    Some(true) => {
471                        self.ensure_inline_lists_visible_for_trigger();
472                        self.agent_palette_active = true;
473                        self.show_transient_surface(TransientSurface::AgentPalette);
474                    }
475                    Some(false) => {
476                        self.close_agent_palette();
477                    }
478                    None => {}
479                }
480            }
481            TransientRequest::HistoryPicker => {
482                events::open_history_picker(self);
483            }
484            TransientRequest::SlashPalette => {
485                self.ensure_inline_lists_visible_for_trigger();
486                self.show_transient_surface(TransientSurface::SlashPalette);
487            }
488            TransientRequest::TaskPanel(TaskPanelTransientRequest { lines, visible }) => {
489                self.core.set_task_panel_lines(lines.clone());
490                self.task_panel_lines = lines;
491                if let Some(visible) = visible {
492                    self.set_task_panel_visible(visible);
493                } else {
494                    self.core.mark_dirty();
495                }
496            }
497            TransientRequest::LocalAgents(LocalAgentsTransientRequest { visible }) => {
498                if let Some(visible) = visible {
499                    if visible {
500                        self.ensure_inline_lists_visible_for_trigger();
501                        self.open_local_agents_drawer(false);
502                    } else {
503                        self.close_local_agents_drawer(true);
504                    }
505                } else {
506                    self.core.mark_dirty();
507                }
508            }
509        }
510        self.core.mark_dirty();
511    }
512
513    /// Show a help modal using the ratatui-cheese Help widget
514    pub(crate) fn show_help_modal(&mut self) {
515        self.show_transient(TransientRequest::Modal(
516            crate::core_tui::app::types::ModalOverlayRequest {
517                title: "Keyboard Shortcuts".to_string(),
518                lines: Vec::new(),
519                secure_prompt: None,
520            },
521        ));
522        if let Some(state) = self.core.modal_state_mut() {
523            state.is_help_modal = true;
524        }
525    }
526
527    fn should_auto_open_local_agents(&self) -> bool {
528        if self.has_active_overlay() {
529            return false;
530        }
531
532        matches!(
533            self.visible_transient_surface(),
534            None | Some(TransientSurface::TaskPanel | TransientSurface::LocalAgents)
535        )
536    }
537
538    pub(crate) fn close_transient(&mut self) {
539        match self.visible_transient_surface() {
540            Some(TransientSurface::FloatingOverlay) => self.close_overlay(),
541            Some(TransientSurface::DiffPreview) => self.close_diff_overlay(),
542            Some(TransientSurface::TranscriptReview) => self.close_transcript_review(),
543            Some(TransientSurface::HistoryPicker) => self.close_history_picker(),
544            Some(TransientSurface::AgentPalette) => self.close_agent_palette(),
545            Some(TransientSurface::FilePalette) => self.close_file_palette(),
546            Some(TransientSurface::SlashPalette) => slash::clear_slash_suggestions(self),
547            Some(TransientSurface::TaskPanel) => self.set_task_panel_visible(false),
548            Some(TransientSurface::LocalAgents) => {
549                self.close_local_agents_drawer(true);
550            }
551            None => {}
552        }
553    }
554
555    pub(super) fn open_local_agents_drawer(&mut self, auto_opened: bool) {
556        self.local_agents_auto_opened = auto_opened;
557        self.show_transient_surface(TransientSurface::LocalAgents);
558    }
559
560    pub(super) fn close_local_agents_drawer(&mut self, clear_auto_opened: bool) {
561        if clear_auto_opened {
562            self.local_agents_auto_opened = false;
563        }
564        self.close_transient_surface(TransientSurface::LocalAgents);
565    }
566
567    pub(crate) fn sync_transient_focus(&mut self) {
568        let Some(surface) = self.visible_transient_surface() else {
569            self.core.set_input_enabled(true);
570            self.core.set_cursor_visible(true);
571            return;
572        };
573
574        match surface.focus_policy() {
575            TransientFocusPolicy::Modal | TransientFocusPolicy::CapturedInput => {
576                self.core.set_input_enabled(false);
577                self.core.set_cursor_visible(false);
578            }
579            TransientFocusPolicy::SharedInput | TransientFocusPolicy::Passive => {
580                self.core.set_input_enabled(true);
581                self.core.set_cursor_visible(true);
582            }
583        }
584    }
585
586    fn apply_transient_visibility_change(&mut self, change: TransientVisibilityChange) {
587        if matches!(
588            change.previous_visible,
589            Some(TransientSurface::FilePalette | TransientSurface::AgentPalette)
590        ) || matches!(
591            change.current_visible,
592            Some(TransientSurface::FilePalette | TransientSurface::AgentPalette)
593        ) {
594            self.core.needs_full_clear = true;
595        }
596        self.core.set_local_agents_drawer_visible(
597            change.current_visible == Some(TransientSurface::LocalAgents),
598        );
599        self.sync_transient_focus();
600    }
601
602    pub fn handle_command(&mut self, command: InlineCommand) {
603        match command {
604            InlineCommand::SetLocalAgents { entries } => {
605                let has_delegated_entries = entries
606                    .iter()
607                    .any(|entry| entry.kind == crate::core_tui::types::LocalAgentKind::Delegated);
608                let update = self.local_agents_state.set_entries(entries.clone());
609                self.core.set_local_agents(entries);
610                if update.has_new_delegated_entries && self.should_auto_open_local_agents() {
611                    self.ensure_inline_lists_visible_for_trigger();
612                    self.open_local_agents_drawer(true);
613                } else if self.local_agents_auto_opened && !has_delegated_entries {
614                    self.close_local_agents_drawer(true);
615                } else if !self.local_agents_visible() && !has_delegated_entries {
616                    self.local_agents_auto_opened = false;
617                }
618            }
619            InlineCommand::SetInput(value) => {
620                self.core
621                    .handle_command(crate::core_tui::types::InlineCommand::SetInput(value));
622                self.update_input_triggers();
623            }
624            InlineCommand::ApplySuggestedPrompt(value) => {
625                self.core.handle_command(
626                    crate::core_tui::types::InlineCommand::ApplySuggestedPrompt(value),
627                );
628                self.update_input_triggers();
629            }
630            InlineCommand::SetInlinePromptSuggestion {
631                suggestion,
632                llm_generated,
633            } => {
634                self.core.handle_command(
635                    crate::core_tui::types::InlineCommand::SetInlinePromptSuggestion {
636                        suggestion,
637                        llm_generated,
638                    },
639                );
640                self.update_input_triggers();
641            }
642            InlineCommand::ClearInlinePromptSuggestion => {
643                self.core.handle_command(
644                    crate::core_tui::types::InlineCommand::ClearInlinePromptSuggestion,
645                );
646                self.update_input_triggers();
647            }
648            InlineCommand::ClearInput => {
649                self.core
650                    .handle_command(crate::core_tui::types::InlineCommand::ClearInput);
651                self.update_input_triggers();
652            }
653            InlineCommand::CloseTransient => self.close_transient(),
654            InlineCommand::ShowTransient { request } => self.show_transient(*request),
655            _ => {
656                if let Some(core_cmd) = to_core_command(&command) {
657                    self.core.handle_command(core_cmd);
658                }
659            }
660        }
661    }
662}
663
664impl std::ops::Deref for AppSession {
665    type Target = CoreSessionState;
666
667    fn deref(&self) -> &Self::Target {
668        &self.core
669    }
670}
671
672impl std::ops::DerefMut for AppSession {
673    fn deref_mut(&mut self) -> &mut Self::Target {
674        &mut self.core
675    }
676}
677
678fn to_core_command(command: &InlineCommand) -> Option<crate::core_tui::types::InlineCommand> {
679    use crate::core_tui::types::InlineCommand as CoreCommand;
680
681    Some(match command {
682        InlineCommand::AppendLine { kind, segments } => CoreCommand::AppendLine {
683            kind: *kind,
684            segments: segments.clone(),
685        },
686        InlineCommand::AppendPastedMessage {
687            kind,
688            text,
689            line_count,
690        } => CoreCommand::AppendPastedMessage {
691            kind: *kind,
692            text: text.clone(),
693            line_count: *line_count,
694        },
695        InlineCommand::Inline { kind, segment } => CoreCommand::Inline {
696            kind: *kind,
697            segment: segment.clone(),
698        },
699        InlineCommand::ReplaceLast {
700            count,
701            kind,
702            lines,
703            link_ranges,
704        } => CoreCommand::ReplaceLast {
705            count: *count,
706            kind: *kind,
707            lines: lines.clone(),
708            link_ranges: link_ranges.clone(),
709        },
710        InlineCommand::SetPrompt { prefix, style } => CoreCommand::SetPrompt {
711            prefix: prefix.clone(),
712            style: style.clone(),
713        },
714        InlineCommand::SetPlaceholder { hint, style } => CoreCommand::SetPlaceholder {
715            hint: hint.clone(),
716            style: style.clone(),
717        },
718        InlineCommand::SetMessageLabels { agent, user } => CoreCommand::SetMessageLabels {
719            agent: agent.clone(),
720            user: user.clone(),
721        },
722        InlineCommand::SetHeaderContext { context } => CoreCommand::SetHeaderContext {
723            context: context.clone(),
724        },
725        InlineCommand::SetInputStatus { left, right } => CoreCommand::SetInputStatus {
726            left: left.clone(),
727            right: right.clone(),
728        },
729        InlineCommand::SetTerminalTitleItems { items } => CoreCommand::SetTerminalTitleItems {
730            items: items.clone(),
731        },
732        InlineCommand::SetTerminalTitleThreadLabel { label } => {
733            CoreCommand::SetTerminalTitleThreadLabel {
734                label: label.clone(),
735            }
736        }
737        InlineCommand::SetTerminalTitleGitBranch { branch } => {
738            CoreCommand::SetTerminalTitleGitBranch {
739                branch: branch.clone(),
740            }
741        }
742        InlineCommand::SetTheme { theme } => CoreCommand::SetTheme {
743            theme: theme.clone(),
744        },
745        InlineCommand::SetAppearance { appearance } => CoreCommand::SetAppearance {
746            appearance: appearance.clone(),
747        },
748        InlineCommand::SetVimModeEnabled(enabled) => CoreCommand::SetVimModeEnabled(*enabled),
749        InlineCommand::SetQueuedInputs { entries } => CoreCommand::SetQueuedInputs {
750            entries: entries.clone(),
751        },
752        InlineCommand::SetSubprocessEntries { entries } => CoreCommand::SetSubprocessEntries {
753            entries: entries.clone(),
754        },
755        InlineCommand::SetSubagentPreview { text } => {
756            CoreCommand::SetSubagentPreview { text: text.clone() }
757        }
758        InlineCommand::SetLocalAgents { .. } => return None,
759        InlineCommand::SetPrimaryAgent { name } => {
760            CoreCommand::SetPrimaryAgent { name: name.clone() }
761        }
762        InlineCommand::SetCursorVisible(value) => CoreCommand::SetCursorVisible(*value),
763        InlineCommand::SetInputEnabled(value) => CoreCommand::SetInputEnabled(*value),
764        InlineCommand::SetInput(value) => CoreCommand::SetInput(value.clone()),
765        InlineCommand::ApplySuggestedPrompt(value) => {
766            CoreCommand::ApplySuggestedPrompt(value.clone())
767        }
768        InlineCommand::SetInlinePromptSuggestion {
769            suggestion,
770            llm_generated,
771        } => CoreCommand::SetInlinePromptSuggestion {
772            suggestion: suggestion.clone(),
773            llm_generated: *llm_generated,
774        },
775        InlineCommand::ClearInlinePromptSuggestion => CoreCommand::ClearInlinePromptSuggestion,
776        InlineCommand::ClearInput => CoreCommand::ClearInput,
777        InlineCommand::ForceRedraw => CoreCommand::ForceRedraw,
778        InlineCommand::ClearScreen => CoreCommand::ClearScreen,
779        InlineCommand::SuspendEventLoop => CoreCommand::SuspendEventLoop,
780        InlineCommand::ResumeEventLoop => CoreCommand::ResumeEventLoop,
781        InlineCommand::ClearInputQueue => CoreCommand::ClearInputQueue,
782        InlineCommand::StopEventStream => CoreCommand::StopEventStream,
783        InlineCommand::StartEventStream => CoreCommand::StartEventStream,
784        InlineCommand::SetEditingMode(mode) => CoreCommand::SetEditingMode(*mode),
785        InlineCommand::SetAutonomousMode(enabled) => CoreCommand::SetAutonomousMode(*enabled),
786        InlineCommand::SetSkipConfirmations(skip) => CoreCommand::SetSkipConfirmations(*skip),
787        InlineCommand::Shutdown => CoreCommand::Shutdown,
788        InlineCommand::SetReasoningStage(stage) => CoreCommand::SetReasoningStage(stage.clone()),
789        InlineCommand::ShowTransient { .. } | InlineCommand::CloseTransient => return None,
790    })
791}
792
793impl TuiSessionDriver for AppSession {
794    type Command = InlineCommand;
795    type Event = InlineEvent;
796
797    fn handle_command(&mut self, command: Self::Command) {
798        AppSession::handle_command(self, command);
799    }
800
801    fn handle_event(
802        &mut self,
803        event: CrosstermEvent,
804        events: &UnboundedSender<Self::Event>,
805        callback: Option<&(dyn Fn(&Self::Event) + Send + Sync + 'static)>,
806    ) {
807        AppSession::handle_event(self, event, events, callback);
808    }
809
810    fn handle_tick(&mut self) {
811        self.core.handle_tick();
812        if self.local_agents_loading_active()
813            && self.core.appearance.should_animate_progress_status()
814            && !self.core.is_shimmer_active()
815            && self.core.shimmer_state.update()
816        {
817            self.core.mark_dirty();
818        }
819    }
820
821    fn render(&mut self, frame: &mut Frame<'_>) {
822        AppSession::render(self, frame);
823    }
824
825    fn take_redraw(&mut self) -> bool {
826        self.core.take_redraw()
827    }
828
829    fn use_steady_cursor(&self) -> bool {
830        self.core.use_steady_cursor()
831    }
832
833    fn is_hovering_link(&self) -> bool {
834        self.core.is_hovering_link()
835    }
836
837    fn is_selecting_text(&self) -> bool {
838        self.core.is_selecting_text()
839    }
840
841    fn should_exit(&self) -> bool {
842        self.core.should_exit()
843    }
844
845    fn request_exit(&mut self) {
846        self.core.request_exit();
847    }
848
849    fn mark_dirty(&mut self) {
850        self.core.mark_dirty();
851    }
852
853    fn update_terminal_title(&mut self) {
854        self.core.update_terminal_title();
855    }
856
857    fn clear_terminal_title(&mut self) {
858        self.core.clear_terminal_title();
859    }
860
861    fn is_running_activity(&self) -> bool {
862        self.core.is_running_activity() || self.local_agents_loading_active()
863    }
864
865    fn has_status_spinner(&self) -> bool {
866        self.core.has_status_spinner() || self.local_agents_loading_active()
867    }
868
869    fn thinking_spinner_active(&self) -> bool {
870        self.core.thinking_spinner.is_active
871    }
872
873    fn has_active_navigation_ui(&self) -> bool {
874        self.transient_host.has_active_navigation_surface()
875    }
876
877    fn apply_coalesced_scroll(&mut self, line_delta: i32, page_delta: i32) {
878        self.core.apply_coalesced_scroll(line_delta, page_delta);
879    }
880
881    fn set_show_logs(&mut self, show: bool) {
882        self.core.show_logs = show;
883    }
884
885    fn set_active_pty_sessions(
886        &mut self,
887        sessions: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
888    ) {
889        self.core.active_pty_sessions = sessions;
890    }
891
892    fn set_workspace_root(&mut self, root: Option<std::path::PathBuf>) {
893        self.core.set_workspace_root(root);
894    }
895
896    fn set_log_receiver(&mut self, receiver: UnboundedReceiver<crate::core_tui::log::LogEntry>) {
897        self.core.set_log_receiver(receiver);
898    }
899
900    fn set_fullscreen_active(&mut self, active: bool) {
901        self.core.set_fullscreen_active(active);
902    }
903
904    fn set_fullscreen_interaction(&mut self, config: FullscreenInteractionSettings) {
905        self.core.set_fullscreen_interaction(config);
906    }
907}