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
46pub 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 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}