use std::path::PathBuf; use crate::app::App; use crate::history_cell::UserHistoryCell; use crate::pager_overlay::Overlay; use crate::tui; use crate::tui::TuiEvent; use codex_core::protocol::ConversationPathResponseEvent; use codex_protocol::mcp_protocol::ConversationId; use color_eyre::eyre::Result; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; /// Aggregates all backtrack-related state used by the App. #[derive(Default)] pub(crate) struct BacktrackState { /// True when Esc has primed backtrack mode in the main view. pub(crate) primed: bool, /// Session id of the base conversation to fork from. pub(crate) base_id: Option, /// Index in the transcript of the last user message. pub(crate) nth_user_message: usize, /// True when the transcript overlay is showing a backtrack preview. pub(crate) overlay_preview_active: bool, /// Pending fork request: (base_id, nth_user_message, prefill). pub(crate) pending: Option<(ConversationId, usize, String)>, } impl App { /// Route overlay events when transcript overlay is active. /// - If backtrack preview is active: Esc steps selection; Enter confirms. /// - Otherwise: Esc begins preview; all other events forward to overlay. /// interactions (Esc to step target, Enter to confirm) and overlay lifecycle. pub(crate) async fn handle_backtrack_overlay_event( &mut self, tui: &mut tui::Tui, event: TuiEvent, ) -> Result { if self.backtrack.overlay_preview_active { match event { TuiEvent::Key(KeyEvent { code: KeyCode::Esc, kind: KeyEventKind::Press | KeyEventKind::Repeat, .. }) => { self.overlay_step_backtrack(tui, event)?; Ok(true) } TuiEvent::Key(KeyEvent { code: KeyCode::Enter, kind: KeyEventKind::Press, .. }) => { self.overlay_confirm_backtrack(tui); Ok(true) } // Catchall: forward any other events to the overlay widget. _ => { self.overlay_forward_event(tui, event)?; Ok(true) } } } else if let TuiEvent::Key(KeyEvent { code: KeyCode::Esc, kind: KeyEventKind::Press | KeyEventKind::Repeat, .. }) = event { // First Esc in transcript overlay: begin backtrack preview at latest user message. self.begin_overlay_backtrack_preview(tui); Ok(true) } else { // Not in backtrack mode: forward events to the overlay widget. self.overlay_forward_event(tui, event)?; Ok(true) } } /// Handle global Esc presses for backtracking when no overlay is present. pub(crate) fn handle_backtrack_esc_key(&mut self, tui: &mut tui::Tui) { // Only handle backtracking when composer is empty to avoid clobbering edits. if self.chat_widget.composer_is_empty() { if !self.backtrack.primed { self.prime_backtrack(); } else if self.overlay.is_none() { self.open_backtrack_preview(tui); } else if self.backtrack.overlay_preview_active { self.step_backtrack_and_highlight(tui); } } } /// Stage a backtrack and request conversation history from the agent. pub(crate) fn request_backtrack( &mut self, prefill: String, base_id: ConversationId, nth_user_message: usize, ) { self.backtrack.pending = Some((base_id, nth_user_message, prefill)); self.app_event_tx.send(crate::app_event::AppEvent::CodexOp( codex_core::protocol::Op::GetPath, )); } /// Open transcript overlay (enters alternate screen and shows full transcript). pub(crate) fn open_transcript_overlay(&mut self, tui: &mut tui::Tui) { let _ = tui.enter_alt_screen(); self.overlay = Some(Overlay::new_transcript(self.transcript_cells.clone())); tui.frame_requester().schedule_frame(); } /// Close transcript overlay and restore normal UI. pub(crate) fn close_transcript_overlay(&mut self, tui: &mut tui::Tui) { let _ = tui.leave_alt_screen(); let was_backtrack = self.backtrack.overlay_preview_active; if !self.deferred_history_lines.is_empty() { let lines = std::mem::take(&mut self.deferred_history_lines); tui.insert_history_lines(lines); } self.overlay = None; self.backtrack.overlay_preview_active = false; if was_backtrack { // Ensure backtrack state is fully reset when overlay closes (e.g. via 'q'). self.reset_backtrack_state(); } } /// Re-render the full transcript into the terminal scrollback in one call. /// Useful when switching sessions to ensure prior history remains visible. pub(crate) fn render_transcript_once(&mut self, tui: &mut tui::Tui) { if !self.transcript_cells.is_empty() { for cell in &self.transcript_cells { tui.insert_history_lines(cell.transcript_lines()); } } } /// Initialize backtrack state and show composer hint. fn prime_backtrack(&mut self) { self.backtrack.primed = true; self.backtrack.nth_user_message = usize::MAX; self.backtrack.base_id = self.chat_widget.conversation_id(); self.chat_widget.show_esc_backtrack_hint(); } /// Open overlay and begin backtrack preview flow (first step + highlight). fn open_backtrack_preview(&mut self, tui: &mut tui::Tui) { self.open_transcript_overlay(tui); self.backtrack.overlay_preview_active = true; // Composer is hidden by overlay; clear its hint. self.chat_widget.clear_esc_backtrack_hint(); self.step_backtrack_and_highlight(tui); } /// When overlay is already open, begin preview mode and select latest user message. fn begin_overlay_backtrack_preview(&mut self, tui: &mut tui::Tui) { self.backtrack.primed = true; self.backtrack.base_id = self.chat_widget.conversation_id(); self.backtrack.overlay_preview_active = true; let last_user_cell_position = self .transcript_cells .iter() .filter_map(|c| c.as_any().downcast_ref::()) .count() as i64 - 1; if last_user_cell_position >= 0 { self.apply_backtrack_selection(last_user_cell_position as usize); } tui.frame_requester().schedule_frame(); } /// Step selection to the next older user message and update overlay. fn step_backtrack_and_highlight(&mut self, tui: &mut tui::Tui) { let last_user_cell_position = self .transcript_cells .iter() .filter(|c| c.as_any().is::()) .take(self.backtrack.nth_user_message) .count() .saturating_sub(1); self.apply_backtrack_selection(last_user_cell_position); tui.frame_requester().schedule_frame(); } /// Apply a computed backtrack selection to the overlay and internal counter. fn apply_backtrack_selection(&mut self, nth_user_message: usize) { self.backtrack.nth_user_message = nth_user_message; if let Some(Overlay::Transcript(t)) = &mut self.overlay { let cell = self .transcript_cells .iter() .enumerate() .filter(|(_, c)| c.as_any().is::()) .nth(nth_user_message); if let Some((idx, _)) = cell { t.set_highlight_cell(Some(idx)); } } } /// Forward any event to the overlay and close it if done. fn overlay_forward_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()> { if let Some(overlay) = &mut self.overlay { overlay.handle_event(tui, event)?; if overlay.is_done() { self.close_transcript_overlay(tui); tui.frame_requester().schedule_frame(); } } Ok(()) } /// Handle Enter in overlay backtrack preview: confirm selection and reset state. fn overlay_confirm_backtrack(&mut self, tui: &mut tui::Tui) { let nth_user_message = self.backtrack.nth_user_message; if let Some(base_id) = self.backtrack.base_id { let user_cells = self .transcript_cells .iter() .filter_map(|c| c.as_any().downcast_ref::()) .collect::>(); let prefill = user_cells .get(nth_user_message) .map(|c| c.message.clone()) .unwrap_or_default(); self.close_transcript_overlay(tui); self.request_backtrack(prefill, base_id, nth_user_message); } self.reset_backtrack_state(); } /// Handle Esc in overlay backtrack preview: step selection if armed, else forward. fn overlay_step_backtrack(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()> { if self.backtrack.base_id.is_some() { self.step_backtrack_and_highlight(tui); } else { self.overlay_forward_event(tui, event)?; } Ok(()) } /// Confirm a primed backtrack from the main view (no overlay visible). /// Computes the prefill from the selected user message and requests history. pub(crate) fn confirm_backtrack_from_main(&mut self) { if let Some(base_id) = self.backtrack.base_id { let prefill = self .transcript_cells .iter() .filter(|c| c.as_any().is::()) .nth(self.backtrack.nth_user_message) .and_then(|c| c.as_any().downcast_ref::()) .map(|c| c.message.clone()) .unwrap_or_default(); self.request_backtrack(prefill, base_id, self.backtrack.nth_user_message); } self.reset_backtrack_state(); } /// Clear all backtrack-related state and composer hints. pub(crate) fn reset_backtrack_state(&mut self) { self.backtrack.primed = false; self.backtrack.base_id = None; self.backtrack.nth_user_message = usize::MAX; // In case a hint is somehow still visible (e.g., race with overlay open/close). self.chat_widget.clear_esc_backtrack_hint(); } /// Handle a ConversationHistory response while a backtrack is pending. /// If it matches the primed base session, fork and switch to the new conversation. pub(crate) async fn on_conversation_history_for_backtrack( &mut self, tui: &mut tui::Tui, ev: ConversationPathResponseEvent, ) -> Result<()> { if let Some((base_id, _, _)) = self.backtrack.pending.as_ref() && ev.conversation_id == *base_id && let Some((_, nth_user_message, prefill)) = self.backtrack.pending.take() { self.fork_and_switch_to_new_conversation(tui, ev, nth_user_message, prefill) .await; } Ok(()) } /// Fork the conversation using provided history and switch UI/state accordingly. async fn fork_and_switch_to_new_conversation( &mut self, tui: &mut tui::Tui, ev: ConversationPathResponseEvent, nth_user_message: usize, prefill: String, ) { let cfg = self.chat_widget.config_ref().clone(); // Perform the fork via a thin wrapper for clarity/testability. let result = self .perform_fork(ev.path.clone(), nth_user_message, cfg.clone()) .await; match result { Ok(new_conv) => { self.install_forked_conversation(tui, cfg, new_conv, nth_user_message, &prefill) } Err(e) => tracing::error!("error forking conversation: {e:#}"), } } /// Thin wrapper around ConversationManager::fork_conversation. async fn perform_fork( &self, path: PathBuf, nth_user_message: usize, cfg: codex_core::config::Config, ) -> codex_core::error::Result { self.server .fork_conversation(nth_user_message, cfg, path) .await } /// Install a forked conversation into the ChatWidget and update UI to reflect selection. fn install_forked_conversation( &mut self, tui: &mut tui::Tui, cfg: codex_core::config::Config, new_conv: codex_core::NewConversation, nth_user_message: usize, prefill: &str, ) { let conv = new_conv.conversation; let session_configured = new_conv.session_configured; let init = crate::chatwidget::ChatWidgetInit { config: cfg, frame_requester: tui.frame_requester(), app_event_tx: self.app_event_tx.clone(), initial_prompt: None, initial_images: Vec::new(), enhanced_keys_supported: self.enhanced_keys_supported, }; self.chat_widget = crate::chatwidget::ChatWidget::new_from_existing(init, conv, session_configured); // Trim transcript up to the selected user message and re-render it. self.trim_transcript_for_backtrack(nth_user_message); self.render_transcript_once(tui); if !prefill.is_empty() { self.chat_widget.set_composer_text(prefill.to_string()); } tui.frame_requester().schedule_frame(); } /// Trim transcript_cells to preserve only content up to the selected user message. fn trim_transcript_for_backtrack(&mut self, nth_user_message: usize) { let cut_idx = self .transcript_cells .iter() .enumerate() .filter_map(|(idx, cell)| { if cell.as_any().is::() { Some(idx) } else { None } }) .nth(nth_user_message - 1) .unwrap_or(self.transcript_cells.len()); self.transcript_cells.truncate(cut_idx); } }