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
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::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 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}