Skip to main content

vtcode_ui/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::tui::core_tui::app::types::{
11    DiffOverlayRequest, DiffPreviewState, InlineCommand, InlineEvent, LocalAgentsTransientRequest,
12    SlashCommandItem, TaskPanelTransientRequest, TransientRequest,
13};
14use crate::tui::core_tui::runner::TuiSessionDriver;
15use crate::tui::core_tui::session::Session as CoreSessionState;
16use crate::tui::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::tui::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::tui::core_tui::types::InlineTheme,
71        placeholder: Option<String>,
72        view_rows: u16,
73        show_logs: bool,
74        appearance: Option<crate::tui::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::tui::core_tui::types::InlineTheme,
109        placeholder: Option<String>,
110        view_rows: u16,
111        show_logs: bool,
112        appearance: Option<crate::tui::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::tui::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::tui::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::tui::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::tui::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::tui::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::tui::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::tui::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::tui::core_tui::types::OverlayRequest::List(
441                        request.into(),
442                    ));
443                self.show_transient_surface(TransientSurface::FloatingOverlay);
444            }
445            TransientRequest::Wizard(request) => {
446                self.core
447                    .show_overlay(crate::tui::core_tui::types::OverlayRequest::Wizard(
448                        request.into(),
449                    ));
450                self.show_transient_surface(TransientSurface::FloatingOverlay);
451            }
452            TransientRequest::Diff(request) => {
453                self.show_diff_overlay(request);
454            }
455            TransientRequest::FilePalette(request) => {
456                self.load_file_palette(request.files, request.workspace);
457                match request.visible {
458                    Some(true) => {
459                        self.ensure_inline_lists_visible_for_trigger();
460                        self.file_palette_active = true;
461                        self.show_transient_surface(TransientSurface::FilePalette);
462                    }
463                    Some(false) => {
464                        self.close_file_palette();
465                    }
466                    None => {}
467                }
468            }
469            TransientRequest::AgentPalette(request) => {
470                self.load_agent_palette(request.agents);
471                match request.visible {
472                    Some(true) => {
473                        self.ensure_inline_lists_visible_for_trigger();
474                        self.agent_palette_active = true;
475                        self.show_transient_surface(TransientSurface::AgentPalette);
476                    }
477                    Some(false) => {
478                        self.close_agent_palette();
479                    }
480                    None => {}
481                }
482            }
483            TransientRequest::HistoryPicker => {
484                events::open_history_picker(self);
485            }
486            TransientRequest::SlashPalette => {
487                self.ensure_inline_lists_visible_for_trigger();
488                self.show_transient_surface(TransientSurface::SlashPalette);
489            }
490            TransientRequest::TaskPanel(TaskPanelTransientRequest { lines, visible }) => {
491                self.core.set_task_panel_lines(lines.clone());
492                self.task_panel_lines = lines;
493                if let Some(visible) = visible {
494                    self.set_task_panel_visible(visible);
495                } else {
496                    self.core.mark_dirty();
497                }
498            }
499            TransientRequest::LocalAgents(LocalAgentsTransientRequest { visible }) => {
500                if let Some(visible) = visible {
501                    if visible {
502                        self.ensure_inline_lists_visible_for_trigger();
503                        self.open_local_agents_drawer(false);
504                    } else {
505                        self.close_local_agents_drawer(true);
506                    }
507                } else {
508                    self.core.mark_dirty();
509                }
510            }
511        }
512        self.core.mark_dirty();
513    }
514
515    /// Show a help modal using the ratatui-cheese Help widget
516    pub(crate) fn show_help_modal(&mut self) {
517        self.show_transient(TransientRequest::Modal(
518            crate::tui::core_tui::app::types::ModalOverlayRequest {
519                title: "Keyboard Shortcuts".to_string(),
520                lines: Vec::new(),
521                secure_prompt: None,
522            },
523        ));
524        if let Some(state) = self.core.modal_state_mut() {
525            state.is_help_modal = true;
526        }
527    }
528
529    fn should_auto_open_local_agents(&self) -> bool {
530        if self.has_active_overlay() {
531            return false;
532        }
533
534        matches!(
535            self.visible_transient_surface(),
536            None | Some(TransientSurface::TaskPanel | TransientSurface::LocalAgents)
537        )
538    }
539
540    pub(crate) fn close_transient(&mut self) {
541        match self.visible_transient_surface() {
542            Some(TransientSurface::FloatingOverlay) => self.close_overlay(),
543            Some(TransientSurface::DiffPreview) => self.close_diff_overlay(),
544            Some(TransientSurface::TranscriptReview) => self.close_transcript_review(),
545            Some(TransientSurface::HistoryPicker) => self.close_history_picker(),
546            Some(TransientSurface::AgentPalette) => self.close_agent_palette(),
547            Some(TransientSurface::FilePalette) => self.close_file_palette(),
548            Some(TransientSurface::SlashPalette) => slash::clear_slash_suggestions(self),
549            Some(TransientSurface::TaskPanel) => self.set_task_panel_visible(false),
550            Some(TransientSurface::LocalAgents) => {
551                self.close_local_agents_drawer(true);
552            }
553            None => {}
554        }
555    }
556
557    pub(super) fn open_local_agents_drawer(&mut self, auto_opened: bool) {
558        self.local_agents_auto_opened = auto_opened;
559        self.show_transient_surface(TransientSurface::LocalAgents);
560    }
561
562    pub(super) fn close_local_agents_drawer(&mut self, clear_auto_opened: bool) {
563        if clear_auto_opened {
564            self.local_agents_auto_opened = false;
565        }
566        self.close_transient_surface(TransientSurface::LocalAgents);
567    }
568
569    pub(crate) fn sync_transient_focus(&mut self) {
570        let Some(surface) = self.visible_transient_surface() else {
571            self.core.set_input_enabled(true);
572            self.core.set_cursor_visible(true);
573            return;
574        };
575
576        match surface.focus_policy() {
577            TransientFocusPolicy::Modal | TransientFocusPolicy::CapturedInput => {
578                self.core.set_input_enabled(false);
579                self.core.set_cursor_visible(false);
580            }
581            TransientFocusPolicy::SharedInput | TransientFocusPolicy::Passive => {
582                self.core.set_input_enabled(true);
583                self.core.set_cursor_visible(true);
584            }
585        }
586    }
587
588    fn apply_transient_visibility_change(&mut self, change: TransientVisibilityChange) {
589        if matches!(
590            change.previous_visible,
591            Some(TransientSurface::FilePalette | TransientSurface::AgentPalette)
592        ) || matches!(
593            change.current_visible,
594            Some(TransientSurface::FilePalette | TransientSurface::AgentPalette)
595        ) {
596            self.core.needs_full_clear = true;
597        }
598        self.core.set_local_agents_drawer_visible(
599            change.current_visible == Some(TransientSurface::LocalAgents),
600        );
601        self.sync_transient_focus();
602    }
603
604    pub fn handle_command(&mut self, command: InlineCommand) {
605        match command {
606            InlineCommand::SetLocalAgents { entries } => {
607                let has_delegated_entries = entries.iter().any(|entry| {
608                    entry.kind == crate::tui::core_tui::types::LocalAgentKind::Delegated
609                });
610                let update = self.local_agents_state.set_entries(entries.clone());
611                self.core.set_local_agents(entries);
612                if update.has_new_delegated_entries && self.should_auto_open_local_agents() {
613                    self.ensure_inline_lists_visible_for_trigger();
614                    self.open_local_agents_drawer(true);
615                } else if self.local_agents_auto_opened && !has_delegated_entries {
616                    self.close_local_agents_drawer(true);
617                } else if !self.local_agents_visible() && !has_delegated_entries {
618                    self.local_agents_auto_opened = false;
619                }
620            }
621            InlineCommand::SetArchivedHistory { entries } => {
622                let archived = entries
623                    .into_iter()
624                    .map(|e| history_picker::ArchivedPrompt {
625                        content: e.content,
626                        created_at: e.created_at,
627                        session_label: e.session_label,
628                    })
629                    .collect();
630                self.history_picker_state.set_archived_prompts(archived);
631                self.core.mark_dirty();
632            }
633            InlineCommand::SetInput(value) => {
634                self.core
635                    .handle_command(crate::tui::core_tui::types::InlineCommand::SetInput(value));
636                self.update_input_triggers();
637            }
638            InlineCommand::ApplySuggestedPrompt(value) => {
639                self.core.handle_command(
640                    crate::tui::core_tui::types::InlineCommand::ApplySuggestedPrompt(value),
641                );
642                self.update_input_triggers();
643            }
644            InlineCommand::SetInlinePromptSuggestion {
645                suggestion,
646                llm_generated,
647            } => {
648                self.core.handle_command(
649                    crate::tui::core_tui::types::InlineCommand::SetInlinePromptSuggestion {
650                        suggestion,
651                        llm_generated,
652                    },
653                );
654                self.update_input_triggers();
655            }
656            InlineCommand::ClearInlinePromptSuggestion => {
657                self.core.handle_command(
658                    crate::tui::core_tui::types::InlineCommand::ClearInlinePromptSuggestion,
659                );
660                self.update_input_triggers();
661            }
662            InlineCommand::ClearInput => {
663                self.core
664                    .handle_command(crate::tui::core_tui::types::InlineCommand::ClearInput);
665                self.update_input_triggers();
666            }
667            InlineCommand::CloseTransient => self.close_transient(),
668            InlineCommand::ShowTransient { request } => self.show_transient(*request),
669            _ => {
670                if let Some(core_cmd) = to_core_command(&command) {
671                    self.core.handle_command(core_cmd);
672                }
673            }
674        }
675    }
676}
677
678impl std::ops::Deref for AppSession {
679    type Target = CoreSessionState;
680
681    fn deref(&self) -> &Self::Target {
682        &self.core
683    }
684}
685
686impl std::ops::DerefMut for AppSession {
687    fn deref_mut(&mut self) -> &mut Self::Target {
688        &mut self.core
689    }
690}
691
692fn to_core_command(command: &InlineCommand) -> Option<crate::tui::core_tui::types::InlineCommand> {
693    use crate::tui::core_tui::types::InlineCommand as CoreCommand;
694
695    Some(match command {
696        InlineCommand::AppendLine { kind, segments } => CoreCommand::AppendLine {
697            kind: *kind,
698            segments: segments.clone(),
699        },
700        InlineCommand::AppendPastedMessage {
701            kind,
702            text,
703            line_count,
704        } => CoreCommand::AppendPastedMessage {
705            kind: *kind,
706            text: text.clone(),
707            line_count: *line_count,
708        },
709        InlineCommand::Inline { kind, segment } => CoreCommand::Inline {
710            kind: *kind,
711            segment: segment.clone(),
712        },
713        InlineCommand::ReplaceLast {
714            count,
715            kind,
716            lines,
717            link_ranges,
718        } => CoreCommand::ReplaceLast {
719            count: *count,
720            kind: *kind,
721            lines: lines.clone(),
722            link_ranges: link_ranges.clone(),
723        },
724        InlineCommand::SetPrompt { prefix, style } => CoreCommand::SetPrompt {
725            prefix: prefix.clone(),
726            style: style.clone(),
727        },
728        InlineCommand::SetPlaceholder { hint, style } => CoreCommand::SetPlaceholder {
729            hint: hint.clone(),
730            style: style.clone(),
731        },
732        InlineCommand::SetMessageLabels { agent, user } => CoreCommand::SetMessageLabels {
733            agent: agent.clone(),
734            user: user.clone(),
735        },
736        InlineCommand::SetHeaderContext { context } => CoreCommand::SetHeaderContext {
737            context: context.clone(),
738        },
739        InlineCommand::SetInputStatus { left, right } => CoreCommand::SetInputStatus {
740            left: left.clone(),
741            right: right.clone(),
742        },
743        InlineCommand::SetTerminalTitleItems { items } => CoreCommand::SetTerminalTitleItems {
744            items: items.clone(),
745        },
746        InlineCommand::SetTerminalTitleThreadLabel { label } => {
747            CoreCommand::SetTerminalTitleThreadLabel {
748                label: label.clone(),
749            }
750        }
751        InlineCommand::SetTerminalTitleGitBranch { branch } => {
752            CoreCommand::SetTerminalTitleGitBranch {
753                branch: branch.clone(),
754            }
755        }
756        InlineCommand::SetTheme { theme } => CoreCommand::SetTheme {
757            theme: theme.clone(),
758        },
759        InlineCommand::SetAppearance { appearance } => CoreCommand::SetAppearance {
760            appearance: appearance.clone(),
761        },
762        InlineCommand::SetVimModeEnabled(enabled) => CoreCommand::SetVimModeEnabled(*enabled),
763        InlineCommand::SetQueuedInputs { entries } => CoreCommand::SetQueuedInputs {
764            entries: entries.clone(),
765        },
766        InlineCommand::SetSubprocessEntries { entries } => CoreCommand::SetSubprocessEntries {
767            entries: entries.clone(),
768        },
769        InlineCommand::SetSubagentPreview { text } => {
770            CoreCommand::SetSubagentPreview { text: text.clone() }
771        }
772        InlineCommand::SetLocalAgents { .. } => return None,
773        InlineCommand::SetArchivedHistory { .. } => return None,
774        InlineCommand::SetPrimaryAgent { name } => {
775            CoreCommand::SetPrimaryAgent { name: name.clone() }
776        }
777        InlineCommand::SetCursorVisible(value) => CoreCommand::SetCursorVisible(*value),
778        InlineCommand::SetInputEnabled(value) => CoreCommand::SetInputEnabled(*value),
779        InlineCommand::SetInput(value) => CoreCommand::SetInput(value.clone()),
780        InlineCommand::ApplySuggestedPrompt(value) => {
781            CoreCommand::ApplySuggestedPrompt(value.clone())
782        }
783        InlineCommand::SetInlinePromptSuggestion {
784            suggestion,
785            llm_generated,
786        } => CoreCommand::SetInlinePromptSuggestion {
787            suggestion: suggestion.clone(),
788            llm_generated: *llm_generated,
789        },
790        InlineCommand::ClearInlinePromptSuggestion => CoreCommand::ClearInlinePromptSuggestion,
791        InlineCommand::ClearInput => CoreCommand::ClearInput,
792        InlineCommand::ForceRedraw => CoreCommand::ForceRedraw,
793        InlineCommand::ClearScreen => CoreCommand::ClearScreen,
794        InlineCommand::SuspendEventLoop => CoreCommand::SuspendEventLoop,
795        InlineCommand::ResumeEventLoop => CoreCommand::ResumeEventLoop,
796        InlineCommand::ClearInputQueue => CoreCommand::ClearInputQueue,
797        InlineCommand::StopEventStream => CoreCommand::StopEventStream,
798        InlineCommand::StartEventStream => CoreCommand::StartEventStream,
799        InlineCommand::SetEditingMode(mode) => CoreCommand::SetEditingMode(*mode),
800        InlineCommand::SetAutonomousMode(enabled) => CoreCommand::SetAutonomousMode(*enabled),
801        InlineCommand::SetSkipConfirmations(skip) => CoreCommand::SetSkipConfirmations(*skip),
802        InlineCommand::Shutdown => CoreCommand::Shutdown,
803        InlineCommand::SetReasoningStage(stage) => CoreCommand::SetReasoningStage(stage.clone()),
804        InlineCommand::ShowTransient { .. } | InlineCommand::CloseTransient => return None,
805    })
806}
807
808impl TuiSessionDriver for AppSession {
809    type Command = InlineCommand;
810    type Event = InlineEvent;
811
812    fn handle_command(&mut self, command: Self::Command) {
813        AppSession::handle_command(self, command);
814    }
815
816    fn handle_event(
817        &mut self,
818        event: CrosstermEvent,
819        events: &UnboundedSender<Self::Event>,
820        callback: Option<&(dyn Fn(&Self::Event) + Send + Sync + 'static)>,
821    ) {
822        AppSession::handle_event(self, event, events, callback);
823    }
824
825    fn handle_tick(&mut self) {
826        self.core.handle_tick();
827        if self.local_agents_loading_active()
828            && self.core.appearance.should_animate_progress_status()
829            && !self.core.is_shimmer_active()
830            && self.core.shimmer_state.update()
831        {
832            self.core.mark_dirty();
833        }
834    }
835
836    fn render(&mut self, frame: &mut Frame<'_>) {
837        AppSession::render(self, frame);
838    }
839
840    fn take_redraw(&mut self) -> bool {
841        self.core.take_redraw()
842    }
843
844    fn use_steady_cursor(&self) -> bool {
845        self.core.use_steady_cursor()
846    }
847
848    fn is_hovering_link(&self) -> bool {
849        self.core.is_hovering_link()
850    }
851
852    fn is_selecting_text(&self) -> bool {
853        self.core.is_selecting_text()
854    }
855
856    fn should_exit(&self) -> bool {
857        self.core.should_exit()
858    }
859
860    fn request_exit(&mut self) {
861        self.core.request_exit();
862    }
863
864    fn mark_dirty(&mut self) {
865        self.core.mark_dirty();
866    }
867
868    fn update_terminal_title(&mut self) {
869        self.core.update_terminal_title();
870    }
871
872    fn clear_terminal_title(&mut self) {
873        self.core.clear_terminal_title();
874    }
875
876    fn is_running_activity(&self) -> bool {
877        self.core.is_running_activity() || self.local_agents_loading_active()
878    }
879
880    fn has_status_spinner(&self) -> bool {
881        self.core.has_status_spinner() || self.local_agents_loading_active()
882    }
883
884    fn thinking_spinner_active(&self) -> bool {
885        self.core.thinking_spinner.is_active
886    }
887
888    fn has_active_navigation_ui(&self) -> bool {
889        self.transient_host.has_active_navigation_surface()
890    }
891
892    fn apply_coalesced_scroll(&mut self, line_delta: i32, page_delta: i32) {
893        self.core.apply_coalesced_scroll(line_delta, page_delta);
894    }
895
896    fn set_show_logs(&mut self, show: bool) {
897        self.core.show_logs = show;
898    }
899
900    fn set_active_pty_sessions(
901        &mut self,
902        sessions: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
903    ) {
904        self.core.active_pty_sessions = sessions;
905    }
906
907    fn set_workspace_root(&mut self, root: Option<std::path::PathBuf>) {
908        self.core.set_workspace_root(root);
909    }
910
911    fn set_log_receiver(
912        &mut self,
913        receiver: UnboundedReceiver<crate::tui::core_tui::log::LogEntry>,
914    ) {
915        self.core.set_log_receiver(receiver);
916    }
917
918    fn set_fullscreen_active(&mut self, active: bool) {
919        self.core.set_fullscreen_active(active);
920    }
921
922    fn set_fullscreen_interaction(&mut self, config: FullscreenInteractionSettings) {
923        self.core.set_fullscreen_interaction(config);
924    }
925}