diff --git a/codex-rs/core/src/codex/compact.rs b/codex-rs/core/src/codex/compact.rs index 304452b060..236b024139 100644 --- a/codex-rs/core/src/codex/compact.rs +++ b/codex-rs/core/src/codex/compact.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use super::Session; use super::TurnContext; +use super::filter_model_visible_history; use super::get_last_assistant_message_from_turn; use crate::Prompt; use crate::client_common::ResponseEvent; @@ -86,8 +87,9 @@ async fn run_compact_task_inner( loop { let turn_input = history.get_history(); + let prompt_input = filter_model_visible_history(turn_input.clone()); let prompt = Prompt { - input: turn_input.clone(), + input: prompt_input.clone(), ..Default::default() }; let attempt_result = drain_to_completed(&sess, turn_context.as_ref(), &prompt).await; @@ -109,7 +111,7 @@ async fn run_compact_task_inner( return; } Err(e @ CodexErr::ContextWindowExceeded) => { - if turn_input.len() > 1 { + if prompt_input.len() > 1 { // Trim from the beginning to preserve cache (prefix-based) and keep recent messages intact. error!( "Context window exceeded while compacting; removing oldest history item. Error: {e}" @@ -152,7 +154,13 @@ async fn run_compact_task_inner( let summary_text = get_last_assistant_message_from_turn(&history_snapshot).unwrap_or_default(); let user_messages = collect_user_messages(&history_snapshot); let initial_context = sess.build_initial_context(turn_context.as_ref()); - let new_history = build_compacted_history(initial_context, &user_messages, &summary_text); + let mut new_history = build_compacted_history(initial_context, &user_messages, &summary_text); + let ghost_snapshots: Vec = history_snapshot + .iter() + .filter(|item| matches!(item, ResponseItem::GhostSnapshot { .. })) + .cloned() + .collect(); + new_history.extend(ghost_snapshots); sess.replace_history(new_history).await; let rollout_item = RolloutItem::Compacted(CompactedItem { diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 2d06c881a8..4db6a50461 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -499,6 +499,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { | EventMsg::GetHistoryEntryResponse(_) | EventMsg::McpListToolsResponse(_) | EventMsg::ListCustomPromptsResponse(_) + | EventMsg::RawResponseItem(_) | EventMsg::UserMessage(_) | EventMsg::EnteredReviewMode(_) | EventMsg::ExitedReviewMode(_) @@ -507,7 +508,6 @@ impl EventProcessor for EventProcessorWithHumanOutput { | EventMsg::AgentReasoningRawContentDelta(_) | EventMsg::ItemStarted(_) | EventMsg::ItemCompleted(_) - | EventMsg::RawResponseItem(_) | EventMsg::UndoCompleted(_) | EventMsg::UndoStarted(_) => {} } diff --git a/codex-rs/git-tooling/src/ghost_commits.rs b/codex-rs/git-tooling/src/ghost_commits.rs index c5ebd7c02c..ec211ac3e1 100644 --- a/codex-rs/git-tooling/src/ghost_commits.rs +++ b/codex-rs/git-tooling/src/ghost_commits.rs @@ -159,13 +159,13 @@ pub fn restore_ghost_commit(repo_path: &Path, commit: &GhostCommit) -> Result<() let repo_prefix = repo_subdir(repo_root.as_path(), repo_path); let current_untracked = capture_existing_untracked(repo_root.as_path(), repo_prefix.as_deref())?; + restore_to_commit_inner(repo_root.as_path(), repo_prefix.as_deref(), commit.id())?; remove_new_untracked( repo_root.as_path(), commit.preexisting_untracked_files(), commit.preexisting_untracked_dirs(), current_untracked, - )?; - restore_to_commit_inner(repo_root.as_path(), repo_prefix.as_deref(), commit.id()) + ) } /// Restore the working tree to match the given commit ID. diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 54d2f7912e..c23709fe07 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -348,6 +348,7 @@ impl BottomPane { )); } if let Some(status) = self.status.as_mut() { + status.set_interrupt_hint_visible(true); status.set_queued_messages(self.queued_user_messages.clone()); } self.request_redraw(); @@ -374,6 +375,13 @@ impl BottomPane { } } + pub(crate) fn set_interrupt_hint_visible(&mut self, visible: bool) { + if let Some(status) = self.status.as_mut() { + status.set_interrupt_hint_visible(visible); + self.request_redraw(); + } + } + pub(crate) fn set_context_window_percent(&mut self, percent: Option) { if self.context_window_percent == percent { return; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 7b83eedcc9..13411df404 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -304,9 +304,6 @@ impl ChatWidget { } fn set_status_header(&mut self, header: String) { - if self.current_status_header == header { - return; - } self.current_status_header = header.clone(); self.bottom_pane.update_status_header(header); } @@ -429,6 +426,7 @@ impl ChatWidget { self.bottom_pane.clear_ctrl_c_quit_hint(); self.bottom_pane.set_task_running(true); self.retry_status_header = None; + self.bottom_pane.set_interrupt_hint_visible(true); self.set_status_header(String::from("Working")); self.full_reasoning_buffer.clear(); self.reasoning_buffer.clear(); @@ -666,6 +664,7 @@ impl ChatWidget { fn on_undo_started(&mut self, event: UndoStartedEvent) { self.bottom_pane.ensure_status_indicator(); + self.bottom_pane.set_interrupt_hint_visible(false); let message = event .message .unwrap_or_else(|| "Undo in progress...".to_string()); diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index e874c6060a..75796d7d62 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -933,6 +933,25 @@ fn undo_failure_events_render_error_message() { ); } +#[test] +fn undo_started_hides_interrupt_hint() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); + + chat.handle_codex_event(Event { + id: "turn-hint".to_string(), + msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }), + }); + + let status = chat + .bottom_pane + .status_widget() + .expect("status indicator should be active"); + assert!( + !status.interrupt_hint_visible(), + "undo should hide the interrupt hint because the operation cannot be cancelled" + ); +} + /// The commit picker shows only commit subjects (no timestamps). #[test] fn review_commit_picker_shows_subjects_without_timestamps() { diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index 81efde71f2..cda52ffee7 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -25,6 +25,8 @@ pub(crate) struct StatusIndicatorWidget { header: String, /// Queued user messages to display under the status line. queued_messages: Vec, + /// Whether to show the interrupt hint (Esc). + show_interrupt_hint: bool, elapsed_running: Duration, last_resume_at: Instant, @@ -55,6 +57,7 @@ impl StatusIndicatorWidget { Self { header: String::from("Working"), queued_messages: Vec::new(), + show_interrupt_hint: true, elapsed_running: Duration::ZERO, last_resume_at: Instant::now(), is_paused: false, @@ -98,9 +101,11 @@ impl StatusIndicatorWidget { /// Update the animated header label (left of the brackets). pub(crate) fn update_header(&mut self, header: String) { - if self.header != header { - self.header = header; - } + self.header = header; + } + + pub(crate) fn set_interrupt_hint_visible(&mut self, visible: bool) { + self.show_interrupt_hint = visible; } #[cfg(test)] @@ -108,6 +113,11 @@ impl StatusIndicatorWidget { &self.header } + #[cfg(test)] + pub(crate) fn interrupt_hint_visible(&self) -> bool { + self.show_interrupt_hint + } + /// Replace the queued messages displayed beneath the header. pub(crate) fn set_queued_messages(&mut self, queued: Vec) { self.queued_messages = queued; @@ -175,12 +185,16 @@ impl WidgetRef for StatusIndicatorWidget { spans.push(spinner(Some(self.last_resume_at))); spans.push(" ".into()); spans.extend(shimmer_spans(&self.header)); - spans.extend(vec![ - " ".into(), - format!("({pretty_elapsed} • ").dim(), - key_hint::plain(KeyCode::Esc).into(), - " to interrupt)".dim(), - ]); + spans.push(" ".into()); + if self.show_interrupt_hint { + spans.extend(vec![ + format!("({pretty_elapsed} • ").dim(), + key_hint::plain(KeyCode::Esc).into(), + " to interrupt)".dim(), + ]); + } else { + spans.push(format!("({pretty_elapsed})").dim()); + } // Build lines: status, then queued messages, then spacer. let mut lines: Vec> = Vec::new();