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