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::config::constants::ui;
11use crate::tui::core_tui::app::types::{
12 CompactActivityMetadata, DiffOverlayRequest, DiffPreviewMode, DiffPreviewState, InlineCommand, InlineEvent,
13 InlineMessageKind, InlineSegment, LocalAgentsTransientRequest, SlashCommandItem, TaskPanelMetadata,
14 TaskPanelTransientRequest, ToolOutputId, TransientActivitySignal, TransientRequest,
15};
16use crate::tui::core_tui::runner::TuiSessionDriver;
17use crate::tui::core_tui::session::Session as CoreSessionState;
18use crate::tui::core_tui::session::action::BindingStore;
19use vtcode_commons::ui_protocol::TaskItemStatus;
20
21mod agent_palette;
22pub(crate) mod diff_preview;
24mod events;
25pub mod file_palette;
27pub mod history_picker;
29mod impl_events;
30mod impl_render;
31mod layout;
32mod local_agents;
33mod palette;
34pub(crate) mod render;
36pub(crate) mod slash;
38pub mod slash_palette;
40mod task_panel;
41#[path = "transcript_review/mod.rs"]
42mod tool_output_viewer;
43mod transient;
44pub mod trust;
46
47use self::file_palette::FilePalette;
48use self::history_picker::HistoryPickerState;
49use self::local_agents::LocalAgentsState;
50use self::slash_palette::SlashPalette;
51use self::tool_output_viewer::ToolOutputViewerState;
52use self::transient::{TransientFocusPolicy, TransientHost, TransientSurface, TransientVisibilityChange};
53use crate::tui::core_tui::style::theme_from_styles;
54use crate::tui::options::FullscreenInteractionSettings;
55use crate::tui::ui::theme;
56use agent_palette::AgentPalette;
57use std::sync::{
58 Arc,
59 atomic::{AtomicUsize, Ordering},
60};
61
62#[derive(Clone, Debug, Default)]
63pub(crate) struct ToolOutputBlock {
64 pub(crate) id: u64,
67 pub(crate) anchor_line: Option<usize>,
71 pub(crate) anchor_search_start: usize,
75 pub(crate) recorded_at_line: Option<usize>,
79 pub(crate) lines: Vec<String>,
80}
81
82#[derive(Clone, Debug)]
83pub(crate) struct CompactActivityEntry {
84 pub(crate) line_index: usize,
85 pub(crate) metadata: CompactActivityMetadata,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub(crate) struct CompactActivityHitRegion {
90 pub(crate) area: Rect,
91 pub(crate) review_anchor: ToolOutputId,
92}
93
94pub struct AppSession {
96 pub(crate) core: CoreSessionState,
97 agent_palette: Option<AgentPalette>,
98 agent_palette_active: bool,
99 file_palette: Option<FilePalette>,
100 pub(crate) file_palette_active: bool,
101 inline_lists_visible: bool,
102 pub(crate) slash_palette: SlashPalette,
103 pub(crate) history_picker_state: HistoryPickerState,
104 local_agents_state: LocalAgentsState,
105 local_agents_auto_opened: bool,
106 pub(crate) show_task_panel: bool,
107 pub(crate) task_panel_lines: Vec<String>,
108 pub(crate) task_panel_statuses: Vec<TaskItemStatus>,
109 pub(crate) task_panel_current: Option<usize>,
110 pub(crate) task_panel_metadata: Option<TaskPanelMetadata>,
111 diff_preview_state: Option<DiffPreviewState>,
112 tool_output_viewer_state: Option<ToolOutputViewerState>,
113 pub(crate) tool_output_blocks: Vec<ToolOutputBlock>,
114 pub(crate) tool_output_revision: u64,
115 pub(crate) compact_activity_entries: Vec<CompactActivityEntry>,
116 pub(crate) compact_activity_hit_regions: Vec<CompactActivityHitRegion>,
117 pub(crate) diff_review_anchors: Vec<vtcode_commons::ui_protocol::DiffReviewAnchor>,
118 diff_overlay_queue: VecDeque<DiffOverlayRequest>,
119 transient_host: TransientHost,
120 transient_active_signal: Option<Arc<TransientActivitySignal>>,
121 preview_callback: Option<crate::tui::core_tui::types::PreviewCallback>,
122}
123
124pub(super) type Session = AppSession;
125
126impl AppSession {
127 pub(crate) fn new_with_logs(
129 theme: crate::tui::core_tui::types::InlineTheme,
130 placeholder: Option<String>,
131 view_rows: u16,
132 show_logs: bool,
133 appearance: Option<crate::tui::core_tui::session::config::AppearanceConfig>,
134 slash_commands: Vec<SlashCommandItem>,
135 app_name: String,
136 ) -> Self {
137 let core = CoreSessionState::new_with_logs(theme, placeholder, view_rows, show_logs, appearance, app_name);
138
139 Self {
140 core,
141 agent_palette: None,
142 agent_palette_active: false,
143 file_palette: None,
144 file_palette_active: false,
145 inline_lists_visible: true,
146 slash_palette: SlashPalette::with_commands(slash_commands),
147 history_picker_state: HistoryPickerState::new(),
148 local_agents_state: LocalAgentsState::default(),
149 local_agents_auto_opened: false,
150 show_task_panel: false,
151 task_panel_lines: Vec::new(),
152 task_panel_statuses: Vec::new(),
153 task_panel_current: None,
154 task_panel_metadata: None,
155 diff_preview_state: None,
156 tool_output_viewer_state: None,
157 tool_output_blocks: Vec::new(),
158 tool_output_revision: 0,
159 compact_activity_entries: Vec::new(),
160 compact_activity_hit_regions: Vec::new(),
161 diff_review_anchors: Vec::new(),
162 diff_overlay_queue: VecDeque::new(),
163 transient_host: TransientHost::default(),
164 transient_active_signal: None,
165 preview_callback: None,
166 }
167 }
168
169 pub(crate) fn new_with_logs_and_bindings(
171 theme: crate::tui::core_tui::types::InlineTheme,
172 placeholder: Option<String>,
173 view_rows: u16,
174 show_logs: bool,
175 appearance: Option<crate::tui::core_tui::session::config::AppearanceConfig>,
176 slash_commands: Vec<SlashCommandItem>,
177 app_name: String,
178 bindings: BindingStore,
179 ) -> Self {
180 let core = CoreSessionState::new_with_bindings(
181 theme,
182 placeholder,
183 view_rows,
184 show_logs,
185 appearance,
186 app_name,
187 bindings,
188 );
189
190 Self {
191 core,
192 agent_palette: None,
193 agent_palette_active: false,
194 file_palette: None,
195 file_palette_active: false,
196 inline_lists_visible: true,
197 slash_palette: SlashPalette::with_commands(slash_commands),
198 history_picker_state: HistoryPickerState::new(),
199 local_agents_state: LocalAgentsState::default(),
200 local_agents_auto_opened: false,
201 show_task_panel: false,
202 task_panel_lines: Vec::new(),
203 task_panel_statuses: Vec::new(),
204 task_panel_current: None,
205 task_panel_metadata: None,
206 diff_preview_state: None,
207 tool_output_viewer_state: None,
208 tool_output_blocks: Vec::new(),
209 tool_output_revision: 0,
210 compact_activity_entries: Vec::new(),
211 compact_activity_hit_regions: Vec::new(),
212 diff_review_anchors: Vec::new(),
213 diff_overlay_queue: VecDeque::new(),
214 transient_host: TransientHost::default(),
215 transient_active_signal: None,
216 preview_callback: None,
217 }
218 }
219
220 pub fn new(theme: crate::tui::core_tui::types::InlineTheme, placeholder: Option<String>, view_rows: u16) -> Self {
221 Self::new_with_logs(theme, placeholder, view_rows, true, None, Vec::new(), "Agent TUI".to_string())
222 }
223
224 pub fn core(&self) -> &CoreSessionState {
225 &self.core
226 }
227
228 pub fn core_mut(&mut self) -> &mut CoreSessionState {
229 &mut self.core
230 }
231
232 pub(crate) fn set_transient_activity_signal(&mut self, signal: Arc<TransientActivitySignal>) {
233 self.transient_active_signal = Some(signal);
234 self.sync_transient_activity_signal();
235 }
236
237 pub(crate) fn inline_lists_visible(&self) -> bool {
238 self.inline_lists_visible
239 }
240
241 fn toggle_inline_lists_visibility(&mut self) {
242 self.inline_lists_visible = !self.inline_lists_visible;
243 self.core.mark_dirty();
244 }
245
246 fn ensure_inline_lists_visible_for_trigger(&mut self) {
247 if !self.inline_lists_visible {
248 self.inline_lists_visible = true;
249 self.core.mark_dirty();
250 }
251 }
252
253 fn update_input_triggers(&mut self) {
254 if !self.core.input_enabled() {
255 return;
256 }
257
258 self.check_agent_reference_trigger();
259 self.check_file_reference_trigger();
260 slash::update_slash_suggestions(self);
261 }
262
263 fn show_transient_surface(&mut self, surface: TransientSurface) -> bool {
264 let change = self.transient_host.show(surface);
265 if !change.changed() {
266 return false;
267 }
268
269 self.apply_transient_visibility_change(change);
270 true
271 }
272
273 fn close_transient_surface(&mut self, surface: TransientSurface) -> bool {
274 let change = self.transient_host.hide(surface);
275 if !change.changed() {
276 return false;
277 }
278
279 self.apply_transient_visibility_change(change);
280 true
281 }
282
283 fn finish_history_picker_interaction(&mut self, was_active: bool) {
284 if was_active && !self.history_picker_state.active {
285 self.close_transient_surface(TransientSurface::HistoryPicker);
286 self.update_input_triggers();
287 }
288 }
289
290 pub(crate) fn set_task_panel_visible(&mut self, visible: bool) {
291 if self.show_task_panel != visible {
292 self.show_task_panel = visible;
293 if visible {
294 self.show_transient_surface(TransientSurface::TaskPanel);
295 } else {
296 self.close_transient_surface(TransientSurface::TaskPanel);
297 }
298 self.core.mark_dirty();
299 }
300 }
301
302 pub(crate) fn toggle_task_panel(&mut self) {
303 let target = !self.show_task_panel;
304 self.set_task_panel_visible(target);
305 }
306
307 fn visible_transient_surface(&self) -> Option<TransientSurface> {
308 self.transient_host.top()
309 }
310
311 fn visible_bottom_docked_surface(&self) -> Option<TransientSurface> {
312 self.transient_host.visible_bottom_docked()
313 }
314
315 fn history_picker_visible(&self) -> bool {
316 self.history_picker_state.active && self.transient_host.is_visible(TransientSurface::HistoryPicker)
317 }
318
319 pub(crate) fn local_agents_visible(&self) -> bool {
320 self.transient_host.is_visible(TransientSurface::LocalAgents)
321 }
322
323 fn background_activity_active(&self) -> bool {
328 self.local_agents_state.loading_count() > 0
329 }
330
331 pub(crate) fn file_palette_visible(&self) -> bool {
332 self.file_palette_active && self.transient_host.is_visible(TransientSurface::FilePalette)
333 }
334
335 fn agent_palette_visible(&self) -> bool {
336 self.agent_palette_active && self.transient_host.is_visible(TransientSurface::AgentPalette)
337 }
338
339 pub(crate) fn slash_palette_visible(&self) -> bool {
340 !self.slash_palette.is_empty() && self.transient_host.is_visible(TransientSurface::SlashPalette)
341 }
342
343 pub(crate) fn has_active_overlay(&self) -> bool {
344 self.core.has_active_overlay() && self.transient_host.is_visible(TransientSurface::FloatingOverlay)
345 }
346
347 pub(crate) fn modal_state(&self) -> Option<&crate::tui::core_tui::session::modal::ModalState> {
348 self.has_active_overlay().then(|| self.core.modal_state()).flatten()
349 }
350
351 fn modal_state_mut(&mut self) -> Option<&mut crate::tui::core_tui::session::modal::ModalState> {
352 if !self.has_active_overlay() {
353 return None;
354 }
355 self.core.modal_state_mut()
356 }
357
358 fn wizard_overlay(&self) -> Option<&crate::tui::core_tui::session::modal::WizardModalState> {
359 self.has_active_overlay().then(|| self.core.wizard_overlay()).flatten()
360 }
361
362 fn wizard_overlay_mut(&mut self) -> Option<&mut crate::tui::core_tui::session::modal::WizardModalState> {
363 if !self.has_active_overlay() {
364 return None;
365 }
366 self.core.wizard_overlay_mut()
367 }
368
369 fn close_overlay(&mut self) {
370 if !self.has_active_overlay() {
371 return;
372 }
373
374 self.core.close_overlay();
375 if !self.core.has_active_overlay() {
376 self.close_transient_surface(TransientSurface::FloatingOverlay);
377 }
378 }
379
380 fn sync_theme_from_runtime(&mut self) {
383 let inline_theme = theme_from_styles(&theme::active_styles());
384 self.core.theme = inline_theme;
385 self.core.styles.set_theme(self.core.theme.clone());
386 }
387
388 fn cancel_theme_preview(&mut self) {
394 if !theme::has_preview_theme() {
395 return;
396 }
397
398 if let Some(callback) = self.preview_callback.as_ref() {
399 let _ = callback(None);
400 }
401 theme::clear_preview_theme();
402 self.sync_theme_from_runtime();
403 }
404
405 pub(crate) fn diff_preview_state(&self) -> Option<&DiffPreviewState> {
406 self.transient_host
407 .is_visible(TransientSurface::DiffPreview)
408 .then_some(())
409 .and(self.diff_preview_state.as_ref())
410 }
411
412 fn diff_preview_state_mut(&mut self) -> Option<&mut DiffPreviewState> {
413 if !self.transient_host.is_visible(TransientSurface::DiffPreview) {
414 return None;
415 }
416 self.diff_preview_state.as_mut()
417 }
418
419 fn tool_output_viewer_state(&self) -> Option<&ToolOutputViewerState> {
420 self.transient_host
421 .is_visible(TransientSurface::ToolOutputViewer)
422 .then_some(())
423 .and(self.tool_output_viewer_state.as_ref())
424 }
425
426 fn tool_output_viewer_state_mut(&mut self) -> Option<&mut ToolOutputViewerState> {
427 if !self.transient_host.is_visible(TransientSurface::ToolOutputViewer) {
428 return None;
429 }
430 self.tool_output_viewer_state.as_mut()
431 }
432
433 pub(crate) fn show_diff_overlay(&mut self, request: DiffOverlayRequest) {
434 if self.diff_preview_state.is_some() {
435 self.diff_overlay_queue.push_back(request);
436 return;
437 }
438
439 let mut state = match request.unified.as_deref() {
440 Some(unified) => DiffPreviewState::from_unified(request.file_path, unified, request.mode),
441 None => DiffPreviewState::new_with_mode(
442 request.file_path,
443 request.before,
444 request.after,
445 request.hunks,
446 request.mode,
447 ),
448 };
449 state.focus_hunk(request.current_hunk);
450 self.diff_preview_state = Some(state);
451 self.show_transient_surface(TransientSurface::DiffPreview);
452 self.core.mark_dirty();
453 }
454
455 pub(crate) fn close_diff_overlay(&mut self) {
456 if self.diff_preview_state.is_none() {
457 return;
458 }
459 self.diff_preview_state = None;
460 if let Some(next) = self.diff_overlay_queue.pop_front() {
461 self.show_diff_overlay(next);
462 return;
463 }
464 self.close_transient_surface(TransientSurface::DiffPreview);
465 self.core.mark_dirty();
466 }
467
468 pub(crate) fn record_diff_review(&mut self, anchor: vtcode_commons::ui_protocol::DiffReviewAnchor) {
469 self.diff_review_anchors.push(anchor);
470 const MAX_DIFF_REVIEW_ANCHORS: usize = 32;
473 if self.diff_review_anchors.len() > MAX_DIFF_REVIEW_ANCHORS {
474 let excess = self.diff_review_anchors.len() - MAX_DIFF_REVIEW_ANCHORS;
475 self.diff_review_anchors.drain(..excess);
476 }
477 }
481
482 pub(crate) fn open_diff_review_for_notice(&mut self, notice_text: &str) -> bool {
494 if !notice_text.contains("review full diff") {
495 return false;
496 }
497
498 fn is_generic_label(path: &str) -> bool {
499 vtcode_commons::ui_protocol::is_generic_diff_review_path(path)
500 }
501
502 let by_notice = self
503 .diff_review_anchors
504 .iter()
505 .enumerate()
506 .rev()
507 .find(|(_, anchor)| !anchor.notice.is_empty() && notice_text.contains(anchor.notice.as_str()))
508 .map(|(index, _)| index);
509
510 let mut by_path: Option<usize> = None;
511 for (index, anchor) in self.diff_review_anchors.iter().enumerate() {
512 if is_generic_label(anchor.file_path.as_str()) {
513 continue;
514 }
515 if !notice_text.contains(anchor.file_path.as_str()) {
516 continue;
517 }
518 let better = match by_path {
519 None => true,
520 Some(prev_index) => anchor.file_path.len() > self.diff_review_anchors[prev_index].file_path.len(),
521 };
522 if better {
523 by_path = Some(index);
524 }
525 }
526
527 let index = by_notice
528 .or(by_path)
529 .or_else(|| (self.diff_review_anchors.len() == 1).then_some(0));
530 let Some(index) = index else {
531 return false;
532 };
533 let anchor = self.diff_review_anchors.swap_remove(index);
534 self.open_diff_review_anchor(anchor)
535 }
536
537 fn open_diff_review_anchor(&mut self, anchor: vtcode_commons::ui_protocol::DiffReviewAnchor) -> bool {
538 self.show_diff_overlay(DiffOverlayRequest {
539 file_path: anchor.file_path,
540 before: String::new(),
541 after: String::new(),
542 hunks: Vec::new(),
543 current_hunk: 0,
544 mode: DiffPreviewMode::ReadonlyReview,
545 unified: Some(anchor.unified),
546 });
547 true
548 }
549
550 fn close_history_picker(&mut self) {
551 if !self.history_picker_state.active {
552 return;
553 }
554 self.history_picker_state.cancel(&mut self.core.input_manager);
555 self.close_transient_surface(TransientSurface::HistoryPicker);
556 self.update_input_triggers();
557 self.mark_dirty();
558 }
559
560 fn open_tool_output_viewer(&mut self, width: u16, height: u16, review_anchor: Option<ToolOutputId>) {
561 self.tool_output_viewer_state = Some(ToolOutputViewerState::open_focused(self, width, height, review_anchor));
562 self.show_transient_surface(TransientSurface::ToolOutputViewer);
563 self.core.mark_dirty();
564 }
565
566 pub(crate) fn record_tool_output_block(&mut self, id: ToolOutputId, mut lines: Vec<String>) {
567 if lines.is_empty() {
568 return;
569 }
570 let max_lines = ui::TUI_TOOL_OUTPUT_CAPTURE_MAX_LINES;
573 if lines.len() > max_lines {
574 let drop = lines.len() - max_lines;
575 lines.drain(..drop);
576 }
577
578 let recorded_at_line = self.core.lines.len();
583 let mut anchor_search_start = recorded_at_line;
584 while anchor_search_start > 0
585 && self
586 .core
587 .lines
588 .get(anchor_search_start - 1)
589 .is_some_and(|line| line.kind == InlineMessageKind::Pty)
590 {
591 anchor_search_start -= 1;
592 }
593 let anchor_line = self.find_live_pty_anchor(lines.first(), anchor_search_start);
594
595 self.tool_output_blocks.push(ToolOutputBlock {
596 id,
597 anchor_line,
598 anchor_search_start,
599 recorded_at_line: Some(recorded_at_line),
600 lines,
601 });
602 self.trim_tool_output_blocks();
603 self.tool_output_revision = self.tool_output_revision.wrapping_add(1);
604 self.core.mark_dirty();
605 }
606
607 fn trim_tool_output_blocks(&mut self) {
611 let max_blocks = ui::TUI_TOOL_OUTPUT_BLOCKS_MAX;
612 if self.tool_output_blocks.len() <= max_blocks {
613 return;
614 }
615 let pinned = self
616 .tool_output_viewer_state
617 .as_ref()
618 .map(|viewer| viewer.retained_tool_ids())
619 .unwrap_or_default();
620 while self.tool_output_blocks.len() > max_blocks {
621 let newest = self.tool_output_blocks.len() - 1;
622 let Some(pos) = self
623 .tool_output_blocks
624 .iter()
625 .take(newest)
626 .position(|block| !pinned.contains(&block.id))
627 else {
628 break;
629 };
630 self.tool_output_blocks.remove(pos);
631 }
632 }
633
634 fn find_live_pty_anchor(&self, header: Option<&String>, search_start: usize) -> Option<usize> {
635 let header = header.map(|header| normalize_tool_output_header(header))?;
636 let mut index = self.core.lines.len();
637 while index > search_start {
638 let line_index = index - 1;
639 let line = self.core.lines.get(line_index)?;
640 if line.kind != InlineMessageKind::Pty {
641 break;
642 }
643 if folded_pty_header(&self.core, line_index).trim_end() == header.trim_end() {
644 return Some(line_index);
645 }
646 index = line_index;
647 }
648 None
649 }
650
651 fn handle_core_command(&mut self, command: crate::tui::core_tui::types::InlineCommand) {
652 let evicted_before = self.core.evicted_message_count;
653 self.core.handle_command(command);
654 let evicted = self.core.evicted_message_count.saturating_sub(evicted_before);
655 if evicted == 0 {
656 return;
657 }
658
659 self.compact_activity_entries.retain_mut(|entry| {
660 if entry.line_index < evicted {
661 return false;
662 }
663 entry.line_index -= evicted;
664 true
665 });
666 for block in &mut self.tool_output_blocks {
667 block.anchor_line = block.anchor_line.filter(|&line| line >= evicted).map(|line| line - evicted);
668 block.anchor_search_start = block.anchor_search_start.saturating_sub(evicted);
669 block.recorded_at_line = block.recorded_at_line.map(|line| line.saturating_sub(evicted));
670 }
671 self.compact_activity_hit_regions.clear();
672 self.tool_output_revision = self.tool_output_revision.wrapping_add(1);
673 }
674
675 fn append_tool_output_line(&mut self, id: ToolOutputId, kind: InlineMessageKind, segments: Vec<InlineSegment>) {
676 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::AppendLine { kind, segments });
677 let line_index = self.core.lines.len().saturating_sub(1);
678 let is_expand_notice = self
681 .core
682 .lines
683 .get(line_index)
684 .is_some_and(|line| line.segments.iter().any(|segment| segment.text.contains("click to expand")));
685 if let Some(block) = self.tool_output_blocks.iter_mut().find(|block| block.id == id)
686 && (block.anchor_line.is_none() || is_expand_notice)
687 {
688 block.anchor_line = Some(line_index);
689 self.tool_output_revision = self.tool_output_revision.wrapping_add(1);
690 self.core.mark_dirty();
691 }
692 }
693
694 fn update_tool_output_anchors(&mut self, metadata: &CompactActivityMetadata, line_index: usize) {
695 let mut ids = metadata.review_anchors.clone();
696 if ids.is_empty()
697 && let Some(review_anchor) = metadata.review_anchor
698 {
699 ids.push(review_anchor);
700 }
701 for id in ids {
702 if let Some(block) = self.tool_output_blocks.iter_mut().find(|block| block.id == id) {
703 block.anchor_line = Some(line_index);
704 }
705 }
706 }
707
708 fn refresh_compact_activity_presentations(&mut self) {
709 let entries = self.compact_activity_entries.clone();
710 for entry in entries {
711 let is_compact_line = self
712 .core
713 .lines
714 .get(entry.line_index)
715 .is_some_and(|line| line.kind == InlineMessageKind::Info);
716 if !is_compact_line {
717 continue;
718 }
719
720 let segments = tool_output_viewer::compact_activity_segments(self, &entry.metadata);
721 let revision = self.core.next_revision();
722 if let Some(line) = self.core.lines.get_mut(entry.line_index) {
723 line.segments = segments;
724 line.link_ranges.clear();
725 line.revision = revision;
726 self.core.mark_line_dirty(entry.line_index);
727 }
728 }
729 self.core.invalidate_scroll_metrics();
730 }
731
732 pub(crate) fn mark_visual_dirty(&mut self) {
734 self.core.mark_visual_dirty();
735 }
736
737 fn append_compact_activity(&mut self, metadata: CompactActivityMetadata) {
738 let segments = tool_output_viewer::compact_activity_segments(self, &metadata);
739 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::AppendLine {
740 kind: InlineMessageKind::Info,
741 segments,
742 });
743 let line_index = self.core.lines.len().saturating_sub(1);
744 self.compact_activity_entries
745 .push(CompactActivityEntry { line_index, metadata: metadata.clone() });
746 let max_entries = ui::TUI_COMPACT_ACTIVITY_MAX_ENTRIES;
747 if self.compact_activity_entries.len() > max_entries {
748 let drop = self.compact_activity_entries.len() - max_entries;
749 self.compact_activity_entries.drain(..drop);
750 }
751 self.update_tool_output_anchors(&metadata, line_index);
752 }
753
754 fn replace_compact_activity(&mut self, metadata: CompactActivityMetadata) {
755 let can_replace = self
756 .compact_activity_entries
757 .last()
758 .is_some_and(|entry| entry.line_index + 1 == self.core.lines.len());
759 if !can_replace {
760 self.append_compact_activity(metadata);
761 return;
762 }
763
764 let segments = tool_output_viewer::compact_activity_segments(self, &metadata);
765 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::ReplaceLast {
766 count: 1,
767 kind: InlineMessageKind::Info,
768 lines: vec![segments],
769 link_ranges: None,
770 });
771 if let Some(entry) = self.compact_activity_entries.last_mut() {
772 entry.metadata = metadata.clone();
773 entry.line_index = self.core.lines.len().saturating_sub(1);
774 }
775 let line_index = self.core.lines.len().saturating_sub(1);
776 self.update_tool_output_anchors(&metadata, line_index);
777 }
778
779 fn collapse_pty_block(&mut self, metadata: CompactActivityMetadata) {
780 let pty_count = self
781 .core
782 .lines
783 .iter()
784 .rev()
785 .take_while(|line| line.kind == InlineMessageKind::Pty)
786 .count();
787 if pty_count == 0 {
788 self.replace_compact_activity(metadata);
789 return;
790 }
791
792 let old_first_removed = self.core.lines.len().saturating_sub(pty_count);
793 let preceding_activity = self.compact_activity_entries.last().is_some_and(|entry| {
794 entry.line_index + 1 == old_first_removed && entry.metadata.group_id == metadata.group_id
795 });
796 let remove_count = pty_count + usize::from(preceding_activity);
797 let first_removed = self.core.lines.len().saturating_sub(remove_count);
798 let segments = tool_output_viewer::compact_activity_segments(self, &metadata);
799 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::ReplaceLast {
800 count: remove_count,
801 kind: InlineMessageKind::Info,
802 lines: vec![segments],
803 link_ranges: None,
804 });
805 self.compact_activity_entries.retain(|entry| entry.line_index < first_removed);
806 let line_index = self.core.lines.len().saturating_sub(1);
807 self.compact_activity_entries
808 .push(CompactActivityEntry { line_index, metadata: metadata.clone() });
809 self.update_tool_output_anchors(&metadata, line_index);
810 }
811
812 pub(crate) fn compact_activity_for_line(&self, line_index: usize) -> Option<&CompactActivityMetadata> {
813 self.compact_activity_entries
814 .iter()
815 .find(|entry| entry.line_index == line_index)
816 .map(|entry| &entry.metadata)
817 }
818
819 pub(crate) fn compact_activity_review_anchor_at(&self, column: u16, row: u16) -> Option<ToolOutputId> {
820 self.compact_activity_hit_regions
821 .iter()
822 .find(|region| region.area.contains(Position { x: column, y: row }))
823 .map(|region| region.review_anchor)
824 }
825
826 fn close_tool_output_viewer(&mut self) {
827 if self.tool_output_viewer_state.is_none() {
828 return;
829 }
830 self.tool_output_viewer_state = None;
831 self.close_transient_surface(TransientSurface::ToolOutputViewer);
832 self.trim_tool_output_blocks();
834 self.core.mark_dirty();
835 }
836
837 pub(crate) fn show_transient(&mut self, request: TransientRequest) {
838 self.core.clear_inline_prompt_suggestion();
839 match request {
840 TransientRequest::Modal(request) => {
841 self.core
842 .show_overlay(crate::tui::core_tui::types::OverlayRequest::Modal(request.into()));
843 self.show_transient_surface(TransientSurface::FloatingOverlay);
844 }
845 TransientRequest::List(request) => {
846 self.core
847 .show_overlay(crate::tui::core_tui::types::OverlayRequest::List(request.into()));
848 self.show_transient_surface(TransientSurface::FloatingOverlay);
849 }
850 TransientRequest::Wizard(request) => {
851 self.core
852 .show_overlay(crate::tui::core_tui::types::OverlayRequest::Wizard(request.into()));
853 self.show_transient_surface(TransientSurface::FloatingOverlay);
854 }
855 TransientRequest::Diff(request) => {
856 self.show_diff_overlay(request);
857 }
858 TransientRequest::FilePalette(request) => {
859 self.load_file_palette(request.dir_lister, request.workspace);
860 match request.visible {
861 Some(true) => {
862 self.ensure_inline_lists_visible_for_trigger();
863 self.file_palette_active = true;
864 self.show_transient_surface(TransientSurface::FilePalette);
865 }
866 Some(false) => {
867 self.close_file_palette();
868 }
869 None => {}
870 }
871 }
872 TransientRequest::AgentPalette(request) => {
873 self.load_agent_palette(request.agents);
874 match request.visible {
875 Some(true) => {
876 self.ensure_inline_lists_visible_for_trigger();
877 self.agent_palette_active = true;
878 self.show_transient_surface(TransientSurface::AgentPalette);
879 }
880 Some(false) => {
881 self.close_agent_palette();
882 }
883 None => {}
884 }
885 }
886 TransientRequest::HistoryPicker => {
887 events::open_history_picker(self);
888 }
889 TransientRequest::SlashPalette => {
890 self.ensure_inline_lists_visible_for_trigger();
891 self.show_transient_surface(TransientSurface::SlashPalette);
892 }
893 TransientRequest::TaskPanel(TaskPanelTransientRequest { lines, statuses, current, visible, metadata }) => {
894 if visible.is_none() {
898 let aligned = statuses.len() == lines.len();
901 self.task_panel_statuses = if aligned { statuses } else { Vec::new() };
902 self.task_panel_current =
903 (aligned.then_some(current).flatten()).filter(|index| *index < lines.len());
904 self.task_panel_lines = lines;
905 self.core.mark_task_panel_content_dirty();
906 match metadata {
907 Some(metadata) => {
908 self.core.set_task_panel_progress_label(Some(format!(
909 "{}/{}",
910 metadata.completed, metadata.total
911 )));
912 let percent = if metadata.total == 0 {
913 None
914 } else {
915 let completed = metadata.completed.min(metadata.total) as u64;
916 let total = metadata.total as u64;
917 let percent = completed.saturating_mul(100) / total.max(1);
918 Some(u8::try_from(percent.min(100)).unwrap_or(100))
919 };
920 self.core
921 .program_status
922 .apply(vtcode_commons::program_status::ProgramStatusUpdate::Progress { percent });
923 self.task_panel_metadata = Some(metadata);
924 }
925 None => {
926 self.task_panel_metadata = None;
930 self.core.set_task_panel_progress_label(None);
931 self.core
932 .program_status
933 .apply(vtcode_commons::program_status::ProgramStatusUpdate::Progress { percent: None });
934 }
935 }
936 }
937 if let Some(visible) = visible {
938 let suppressed = visible && !self.core.appearance.should_show_task_panel();
939 if !suppressed {
940 self.set_task_panel_visible(visible);
941 }
942 } else {
945 self.core.mark_dirty();
946 }
947 }
948 TransientRequest::LocalAgents(LocalAgentsTransientRequest { visible }) => {
949 if let Some(visible) = visible {
950 if visible {
951 self.ensure_inline_lists_visible_for_trigger();
952 self.open_local_agents_drawer(false);
953 } else {
954 self.close_local_agents_drawer(true);
955 }
956 } else {
957 self.core.mark_dirty();
958 }
959 }
960 }
961 self.core.mark_dirty();
962 }
963
964 fn show_help_modal(&mut self) {
970 self.show_transient(TransientRequest::Modal(crate::tui::core_tui::app::types::ModalOverlayRequest {
971 title: "Keyboard Shortcuts".to_string(),
972 lines: Vec::new(),
973 secure_prompt: None,
974 is_help_modal: true,
975 }));
976 }
977
978 fn should_auto_open_local_agents(&self) -> bool {
979 if self.has_active_overlay() {
980 return false;
981 }
982
983 matches!(
984 self.visible_transient_surface(),
985 None | Some(TransientSurface::TaskPanel | TransientSurface::LocalAgents)
986 )
987 }
988
989 pub(crate) fn close_transient(&mut self) {
990 match self.visible_transient_surface() {
991 Some(TransientSurface::FloatingOverlay) => self.close_overlay(),
992 Some(TransientSurface::DiffPreview) => self.close_diff_overlay(),
993 Some(TransientSurface::ToolOutputViewer) => self.close_tool_output_viewer(),
994 Some(TransientSurface::HistoryPicker) => self.close_history_picker(),
995 Some(TransientSurface::AgentPalette) => self.close_agent_palette(),
996 Some(TransientSurface::FilePalette) => self.close_file_palette(),
997 Some(TransientSurface::SlashPalette) => slash::clear_slash_suggestions(self),
998 Some(TransientSurface::TaskPanel) => self.set_task_panel_visible(false),
999 Some(TransientSurface::LocalAgents) => {
1000 self.close_local_agents_drawer(true);
1001 }
1002 None => {}
1003 }
1004 }
1005
1006 fn open_local_agents_drawer(&mut self, auto_opened: bool) {
1007 self.local_agents_auto_opened = auto_opened;
1008 self.show_transient_surface(TransientSurface::LocalAgents);
1009 }
1010
1011 fn close_local_agents_drawer(&mut self, clear_auto_opened: bool) {
1012 if clear_auto_opened {
1013 self.local_agents_auto_opened = false;
1014 }
1015 self.close_transient_surface(TransientSurface::LocalAgents);
1016 }
1017
1018 fn sync_transient_focus(&mut self) {
1019 if self.core.activity_state.is_busy() {
1020 self.core.set_input_enabled(false);
1021 self.core.set_cursor_visible(false);
1022 return;
1023 }
1024 let Some(surface) = self.visible_transient_surface() else {
1025 self.core.set_input_enabled(true);
1026 self.core.set_cursor_visible(true);
1027 return;
1028 };
1029
1030 match surface.focus_policy() {
1031 TransientFocusPolicy::Modal | TransientFocusPolicy::CapturedInput => {
1032 self.core.set_input_enabled(false);
1033 self.core.set_cursor_visible(false);
1034 }
1035 TransientFocusPolicy::SharedInput | TransientFocusPolicy::Passive => {
1036 self.core.set_input_enabled(true);
1037 self.core.set_cursor_visible(true);
1038 }
1039 }
1040 }
1041
1042 fn sync_transient_activity_signal(&self) {
1043 let Some(signal) = self.transient_active_signal.as_ref() else {
1044 return;
1045 };
1046 let active = self
1047 .visible_transient_surface()
1048 .is_some_and(|surface| !matches!(surface, TransientSurface::TaskPanel));
1049 signal.set_active(active);
1050 }
1051
1052 fn apply_transient_visibility_change(&mut self, change: TransientVisibilityChange) {
1053 if matches!(change.previous_visible, Some(TransientSurface::FilePalette | TransientSurface::AgentPalette))
1054 || matches!(change.current_visible, Some(TransientSurface::FilePalette | TransientSurface::AgentPalette))
1055 {
1056 self.core.request_full_clear();
1057 }
1058 self.core
1059 .set_local_agents_drawer_visible(change.current_visible == Some(TransientSurface::LocalAgents));
1060 self.sync_transient_focus();
1061 self.sync_transient_activity_signal();
1062 }
1063
1064 pub fn handle_command(&mut self, command: InlineCommand) {
1065 match command {
1066 InlineCommand::SetLocalAgents { entries } => {
1067 let has_delegated_entries = entries
1068 .iter()
1069 .any(|entry| entry.kind == crate::tui::core_tui::types::LocalAgentKind::Delegated);
1070 let has_live_delegated = entries.iter().any(|entry| {
1074 entry.kind == crate::tui::core_tui::types::LocalAgentKind::Delegated && entry.is_loading()
1075 });
1076 let update = self.local_agents_state.set_entries(entries.clone());
1077 let background_count = self.local_agents_state.loading_count();
1078 let finished_count = self.local_agents_state.finished_count();
1079 self.core.set_local_agents(entries);
1080 self.core.set_background_activity_count(background_count);
1081 self.core.set_background_finished_count(finished_count);
1082 if update.has_new_delegated_entries && self.should_auto_open_local_agents() {
1083 self.ensure_inline_lists_visible_for_trigger();
1084 self.open_local_agents_drawer(true);
1085 } else if self.local_agents_auto_opened && !has_live_delegated {
1086 self.close_local_agents_drawer(true);
1087 } else if !self.local_agents_visible() && !has_delegated_entries {
1088 self.local_agents_auto_opened = false;
1089 }
1090 }
1091 InlineCommand::SetArchivedHistory { entries } => {
1092 let mut input_entries = Vec::new();
1093 let archived = entries
1094 .into_iter()
1095 .map(|e| {
1096 input_entries.push(
1097 crate::tui::core_tui::session::input_manager::InputHistoryEntry::from_content_and_timestamp(
1098 e.content.clone(),
1099 e.created_at,
1100 ),
1101 );
1102 history_picker::ArchivedPrompt {
1103 content: e.content,
1104 created_at: e.created_at,
1105 session_label: e.session_label,
1106 }
1107 })
1108 .collect();
1109 self.history_picker_state.set_archived_prompts(archived);
1110 self.core.input_manager.prepend_archived_history(input_entries);
1111 self.core.mark_dirty();
1112 }
1113 InlineCommand::SetInput(value) => {
1114 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::SetInput(value));
1115 self.update_input_triggers();
1116 }
1117 InlineCommand::RestoreInputDraft(input) => {
1118 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::RestoreInputDraft(input));
1119 self.update_input_triggers();
1120 }
1121 InlineCommand::ApplySuggestedPrompt(value) => {
1122 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::ApplySuggestedPrompt(value));
1123 self.update_input_triggers();
1124 }
1125 InlineCommand::SetInlinePromptSuggestion { suggestion, llm_generated } => {
1126 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::SetInlinePromptSuggestion {
1127 suggestion,
1128 llm_generated,
1129 });
1130 self.update_input_triggers();
1131 }
1132 InlineCommand::ClearInlinePromptSuggestion => {
1133 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::ClearInlinePromptSuggestion);
1134 self.update_input_triggers();
1135 }
1136 InlineCommand::ClearInput => {
1137 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::ClearInput);
1138 self.update_input_triggers();
1139 }
1140 InlineCommand::RecordToolOutput { id, lines } => {
1141 self.record_tool_output_block(id, lines);
1142 }
1143 InlineCommand::AppendToolOutputLine { id, kind, segments } => {
1144 self.append_tool_output_line(id, kind, segments);
1145 }
1146 InlineCommand::AppendCompactActivity(metadata) => {
1147 self.append_compact_activity(metadata);
1148 }
1149 InlineCommand::ReplaceCompactActivity(metadata) => {
1150 self.replace_compact_activity(metadata);
1151 }
1152 InlineCommand::CollapsePtyBlock(metadata) => {
1153 self.collapse_pty_block(metadata);
1154 }
1155 InlineCommand::SetKeyBindings { bindings } => {
1156 self.core.set_bindings(BindingStore::new(bindings));
1157 self.refresh_compact_activity_presentations();
1158 }
1159 InlineCommand::SetAppearance { appearance } => {
1160 let enabling_screen_reader = appearance.screen_reader_mode && !self.core.appearance.screen_reader_mode;
1161 if enabling_screen_reader && let Some(viewer) = self.tool_output_viewer_state.as_mut() {
1162 viewer.set_render_mode(tool_output_viewer::TranscriptRenderMode::Raw);
1163 }
1164 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::SetAppearance { appearance });
1165 self.refresh_compact_activity_presentations();
1166 }
1167 InlineCommand::SetFullscreenInteraction { interaction } => {
1168 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::SetFullscreenInteraction {
1169 interaction,
1170 });
1171 }
1172 InlineCommand::ReplaceLast { count, kind, lines, link_ranges } => {
1173 let remove_count = count.min(self.core.lines.len());
1174 let first_removed = self.core.lines.len().saturating_sub(remove_count);
1175 self.compact_activity_entries.retain(|entry| entry.line_index < first_removed);
1176 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::ReplaceLast {
1177 count,
1178 kind,
1179 lines,
1180 link_ranges,
1181 });
1182 }
1183 InlineCommand::ClearScreen => {
1184 self.tool_output_blocks.clear();
1185 self.compact_activity_entries.clear();
1186 self.compact_activity_hit_regions.clear();
1187 self.diff_review_anchors.clear();
1188 self.tool_output_revision = self.tool_output_revision.wrapping_add(1);
1189 self.handle_core_command(crate::tui::core_tui::types::InlineCommand::ClearScreen);
1190 }
1191 InlineCommand::CloseTransient => self.close_transient(),
1192 InlineCommand::ShowTransient { request } => self.show_transient(*request),
1193 InlineCommand::RecordDiffReview(anchor) => self.record_diff_review(anchor),
1194 InlineCommand::FocusTranscriptReview { id } => self.open_tool_output_viewer(
1195 self.core.transcript_width.max(1),
1196 self.core.transcript_rows.max(1),
1197 Some(id),
1198 ),
1199 InlineCommand::UpdateFilePaletteSearch { files } => {
1200 if let Some(palette) = &mut self.file_palette {
1201 palette.set_search_index(files);
1202 }
1203 }
1204 InlineCommand::SetSlashCommands { commands } => {
1205 self.slash_palette.set_commands(commands);
1206 }
1207 _ => {
1208 if let Some(core_cmd) = to_core_command(&command) {
1209 self.handle_core_command(core_cmd);
1210 }
1211 }
1212 }
1213 }
1214}
1215
1216fn normalize_tool_output_header(header: &str) -> String {
1217 crate::tui::core_tui::session::text_utils::strip_ansi_codes(header)
1218 .trim_end()
1219 .to_owned()
1220}
1221
1222fn rendered_core_line_text(core: &CoreSessionState, line_index: usize) -> String {
1223 let Some(line) = core.lines.get(line_index) else {
1224 return String::new();
1225 };
1226 core.render_message_spans_for_line(line)
1227 .into_iter()
1228 .map(|span| crate::tui::core_tui::session::text_utils::strip_ansi_codes(span.content.as_ref()).into_owned())
1229 .collect()
1230}
1231
1232fn folded_pty_header(core: &CoreSessionState, line_index: usize) -> String {
1233 let first = rendered_core_line_text(core, line_index);
1234 if !first.trim_start().starts_with('•') {
1235 return first;
1236 }
1237
1238 let mut folded = first.trim_end().to_owned();
1239 let mut continuation_index = line_index + 1;
1240 while let Some(line) = core.lines.get(continuation_index) {
1241 if line.kind != InlineMessageKind::Pty {
1242 break;
1243 }
1244 let continuation = rendered_core_line_text(core, continuation_index);
1245 let Some(fragment) = continuation.trim_start().strip_prefix('│') else {
1246 break;
1247 };
1248 folded.push(' ');
1249 folded.push_str(fragment.trim());
1250 continuation_index += 1;
1251 }
1252 folded
1253}
1254
1255impl std::ops::Deref for AppSession {
1256 type Target = CoreSessionState;
1257
1258 fn deref(&self) -> &Self::Target {
1259 &self.core
1260 }
1261}
1262
1263impl std::ops::DerefMut for AppSession {
1264 fn deref_mut(&mut self) -> &mut Self::Target {
1265 &mut self.core
1266 }
1267}
1268
1269fn to_core_command(command: &InlineCommand) -> Option<crate::tui::core_tui::types::InlineCommand> {
1270 use crate::tui::core_tui::types::InlineCommand as CoreCommand;
1271
1272 Some(match command {
1273 InlineCommand::AppendLine { kind, segments } => {
1274 CoreCommand::AppendLine { kind: *kind, segments: segments.clone() }
1275 }
1276 InlineCommand::AppendPastedMessage { kind, text, line_count } => CoreCommand::AppendPastedMessage {
1277 kind: *kind,
1278 text: text.clone(),
1279 line_count: *line_count,
1280 },
1281 InlineCommand::Inline { kind, segment } => CoreCommand::Inline { kind: *kind, segment: segment.clone() },
1282 InlineCommand::ReplaceLast { count, kind, lines, link_ranges } => CoreCommand::ReplaceLast {
1283 count: *count,
1284 kind: *kind,
1285 lines: lines.clone(),
1286 link_ranges: link_ranges.clone(),
1287 },
1288 InlineCommand::RecordToolOutput { .. }
1289 | InlineCommand::FocusTranscriptReview { .. }
1290 | InlineCommand::AppendToolOutputLine { .. }
1291 | InlineCommand::AppendCompactActivity(_)
1292 | InlineCommand::ReplaceCompactActivity(_)
1293 | InlineCommand::CollapsePtyBlock(_)
1294 | InlineCommand::SetKeyBindings { .. } => return None,
1295 InlineCommand::SetPrompt { prefix, style } => {
1296 CoreCommand::SetPrompt { prefix: prefix.clone(), style: style.clone() }
1297 }
1298 InlineCommand::SetPlaceholder { hint, style } => {
1299 CoreCommand::SetPlaceholder { hint: hint.clone(), style: style.clone() }
1300 }
1301 InlineCommand::SetMessageLabels { agent, user } => {
1302 CoreCommand::SetMessageLabels { agent: agent.clone(), user: user.clone() }
1303 }
1304 InlineCommand::SetHeaderContext { context } => CoreCommand::SetHeaderContext { context: context.clone() },
1305 InlineCommand::SetInputStatus { left, right } => {
1306 CoreCommand::SetInputStatus { left: left.clone(), right: right.clone() }
1307 }
1308 InlineCommand::SetConfiguredInputStatus { left, right } => {
1309 CoreCommand::SetConfiguredInputStatus { left: left.clone(), right: right.clone() }
1310 }
1311 InlineCommand::ProgramStatus(update) => CoreCommand::ProgramStatus(*update),
1312 InlineCommand::SetActivityState(state) => CoreCommand::SetActivityState(*state),
1313 InlineCommand::UpdateProgress(update) => CoreCommand::UpdateProgress(*update),
1314 InlineCommand::SetTerminalTitleItems { items } => CoreCommand::SetTerminalTitleItems { items: items.clone() },
1315 InlineCommand::SetTerminalTitleThreadLabel { label } => {
1316 CoreCommand::SetTerminalTitleThreadLabel { label: label.clone() }
1317 }
1318 InlineCommand::SetTerminalTitleGitBranch { branch } => {
1319 CoreCommand::SetTerminalTitleGitBranch { branch: branch.clone() }
1320 }
1321 InlineCommand::SetTheme { theme } => CoreCommand::SetTheme { theme: theme.clone() },
1322 InlineCommand::SetColorSchemeAuto { enabled } => CoreCommand::SetColorSchemeAuto { enabled: *enabled },
1323 InlineCommand::SetAppearance { appearance } => CoreCommand::SetAppearance { appearance: appearance.clone() },
1324 InlineCommand::SetFullscreenInteraction { interaction } => {
1325 CoreCommand::SetFullscreenInteraction { interaction: *interaction }
1326 }
1327 InlineCommand::SetVimModeEnabled(enabled) => CoreCommand::SetVimModeEnabled(*enabled),
1328 InlineCommand::SetQueuedInputs { entries } => CoreCommand::SetQueuedInputs { entries: entries.clone() },
1329 InlineCommand::SetSubprocessEntries { entries } => {
1330 CoreCommand::SetSubprocessEntries { entries: entries.clone() }
1331 }
1332 InlineCommand::SetSubagentPreview { text } => CoreCommand::SetSubagentPreview { text: text.clone() },
1333 InlineCommand::SetLocalAgents { .. } => return None,
1334 InlineCommand::SetArchivedHistory { .. } => return None,
1335 InlineCommand::UpdateFilePaletteSearch { .. } => return None,
1336 InlineCommand::SetSlashCommands { .. } => return None,
1337 InlineCommand::SetPrimaryAgent { name, color } => {
1338 CoreCommand::SetPrimaryAgent { name: name.clone(), color: color.clone() }
1339 }
1340 InlineCommand::SetCursorVisible(value) => CoreCommand::SetCursorVisible(*value),
1341 InlineCommand::SetInputEnabled(value) => CoreCommand::SetInputEnabled(*value),
1342 InlineCommand::SetImageInputEnabled(value) => CoreCommand::SetImageInputEnabled(*value),
1343 InlineCommand::SetInput(value) => CoreCommand::SetInput(value.clone()),
1344 InlineCommand::RestoreInputDraft(input) => CoreCommand::RestoreInputDraft(input.clone()),
1345 InlineCommand::ApplySuggestedPrompt(value) => CoreCommand::ApplySuggestedPrompt(value.clone()),
1346 InlineCommand::SetInlinePromptSuggestion { suggestion, llm_generated } => {
1347 CoreCommand::SetInlinePromptSuggestion {
1348 suggestion: suggestion.clone(),
1349 llm_generated: *llm_generated,
1350 }
1351 }
1352 InlineCommand::ClearInlinePromptSuggestion => CoreCommand::ClearInlinePromptSuggestion,
1353 InlineCommand::ClearInput => CoreCommand::ClearInput,
1354 InlineCommand::ForceRedraw => CoreCommand::ForceRedraw,
1355 InlineCommand::ClearScreen => CoreCommand::ClearScreen,
1356 InlineCommand::SuspendEventLoop => CoreCommand::SuspendEventLoop,
1357 InlineCommand::ResumeEventLoop => CoreCommand::ResumeEventLoop,
1358 InlineCommand::ClearInputQueue => CoreCommand::ClearInputQueue,
1359 InlineCommand::StopEventStream => CoreCommand::StopEventStream,
1360 InlineCommand::StartEventStream => CoreCommand::StartEventStream,
1361 InlineCommand::SetSkipConfirmations(skip) => CoreCommand::SetSkipConfirmations(*skip),
1362 InlineCommand::Shutdown => CoreCommand::Shutdown,
1363 InlineCommand::SetReasoningStage(stage) => CoreCommand::SetReasoningStage(stage.clone()),
1364 InlineCommand::ShowTransient { .. } | InlineCommand::CloseTransient | InlineCommand::RecordDiffReview(_) => {
1365 return None;
1366 }
1367 })
1368}
1369
1370impl TuiSessionDriver for AppSession {
1371 type Command = InlineCommand;
1372 type Event = InlineEvent;
1373
1374 fn handle_command(&mut self, command: Self::Command) {
1375 AppSession::handle_command(self, command);
1376 }
1377
1378 fn handle_event(
1379 &mut self,
1380 event: CrosstermEvent,
1381 events: &UnboundedSender<Self::Event>,
1382 callback: Option<&(dyn Fn(&Self::Event) + Send + Sync + 'static)>,
1383 ) {
1384 AppSession::handle_event(self, event, events, callback);
1385 }
1386
1387 fn handle_tick(&mut self) {
1388 self.core.handle_tick();
1393 }
1394
1395 fn needs_animation_tick(&self) -> bool {
1396 self.core.needs_animation_tick()
1400 || (self.core.appearance.should_animate_progress_status() && self.background_activity_active())
1401 }
1402
1403 fn render(&mut self, frame: &mut Frame<'_>) {
1404 AppSession::render(self, frame);
1405 }
1406
1407 fn take_redraw(&mut self) -> bool {
1408 self.core.take_redraw()
1409 }
1410
1411 fn use_steady_cursor(&self) -> bool {
1412 self.core.use_steady_cursor()
1413 }
1414
1415 fn is_hovering_link(&self) -> bool {
1416 self.core.is_hovering_link()
1417 }
1418
1419 fn is_selecting_text(&self) -> bool {
1420 self.core.is_selecting_text()
1421 }
1422
1423 fn should_exit(&self) -> bool {
1424 self.core.should_exit()
1425 }
1426
1427 fn request_exit(&mut self) {
1428 self.core.request_exit();
1429 }
1430
1431 fn mark_dirty(&mut self) {
1432 self.core.mark_dirty();
1433 }
1434
1435 fn update_terminal_title(&mut self) {
1436 self.core.update_terminal_title();
1437 }
1438
1439 fn attach_program_status_terminal(&mut self) {
1440 self.core.program_status.attach_terminal();
1441 }
1442
1443 fn flush_program_status(&mut self) {
1444 self.core.program_status.flush();
1445 }
1446
1447 fn shutdown_program_status(&mut self) {
1448 self.core.program_status.shutdown();
1449 }
1450
1451 fn clear_terminal_title(&mut self) {
1452 self.core.clear_terminal_title();
1453 }
1454
1455 fn is_running_activity(&self) -> bool {
1456 self.core.is_running_activity()
1460 }
1461
1462 fn has_status_spinner(&self) -> bool {
1463 self.core.has_status_spinner() || self.background_activity_active()
1466 }
1467
1468 fn thinking_spinner_active(&self) -> bool {
1469 self.core.thinking_spinner.is_active
1470 }
1471
1472 fn has_active_navigation_ui(&self) -> bool {
1473 self.transient_host.has_active_navigation_surface()
1474 }
1475
1476 fn apply_coalesced_scroll(&mut self, line_delta: i32, page_delta: i32) {
1477 self.core.apply_coalesced_scroll(line_delta, page_delta);
1478 }
1479
1480 fn set_show_logs(&mut self, show: bool) {
1481 self.core.show_logs = show;
1482 }
1483
1484 fn set_active_pty_sessions(&mut self, sessions: Option<Arc<AtomicUsize>>) {
1485 self.core.active_pty_sessions = sessions;
1486 }
1487
1488 fn set_workspace_root(&mut self, root: Option<std::path::PathBuf>) {
1489 self.core.set_workspace_root(root);
1490 }
1491
1492 fn set_log_receiver(&mut self, receiver: UnboundedReceiver<crate::tui::core_tui::log::LogEntry>) {
1493 self.core.set_log_receiver(receiver);
1494 }
1495
1496 fn set_fullscreen_active(&mut self, active: bool) {
1497 self.core.set_fullscreen_active(active);
1498 }
1499
1500 fn set_fullscreen_interaction(&mut self, config: FullscreenInteractionSettings) {
1501 self.core.set_fullscreen_interaction(config);
1502 }
1503
1504 fn set_preview_callback(&mut self, callback: Option<crate::tui::core_tui::types::PreviewCallback>) {
1505 self.preview_callback = callback;
1506 }
1507}