Skip to content
14 changes: 11 additions & 3 deletions codex-rs/core/src/codex/compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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}"
Expand Down Expand Up @@ -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<ResponseItem> = 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 {
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/exec/src/event_processor_with_human_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
| EventMsg::GetHistoryEntryResponse(_)
| EventMsg::McpListToolsResponse(_)
| EventMsg::ListCustomPromptsResponse(_)
| EventMsg::RawResponseItem(_)
| EventMsg::UserMessage(_)
| EventMsg::EnteredReviewMode(_)
| EventMsg::ExitedReviewMode(_)
Expand All @@ -507,7 +508,6 @@ impl EventProcessor for EventProcessorWithHumanOutput {
| EventMsg::AgentReasoningRawContentDelta(_)
| EventMsg::ItemStarted(_)
| EventMsg::ItemCompleted(_)
| EventMsg::RawResponseItem(_)
| EventMsg::UndoCompleted(_)
| EventMsg::UndoStarted(_) => {}
}
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/git-tooling/src/ghost_commits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/tui/src/bottom_pane/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<i64>) {
if self.context_window_percent == percent {
return;
Expand Down
5 changes: 2 additions & 3 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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());
Expand Down
19 changes: 19 additions & 0 deletions codex-rs/tui/src/chatwidget/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
32 changes: 23 additions & 9 deletions codex-rs/tui/src/status_indicator_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub(crate) struct StatusIndicatorWidget {
header: String,
/// Queued user messages to display under the status line.
queued_messages: Vec<String>,
/// Whether to show the interrupt hint (Esc).
show_interrupt_hint: bool,

elapsed_running: Duration,
last_resume_at: Instant,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -98,16 +101,23 @@ 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)]
pub(crate) fn header(&self) -> &str {
&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<String>) {
self.queued_messages = queued;
Expand Down Expand Up @@ -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<Line<'static>> = Vec::new();
Expand Down
Loading