diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c967abf9c8..bbea3fedee 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -6301,6 +6301,8 @@ impl ChatWidget { notification.error.additional_details, ); } + } else if from_replay { + self.last_non_retry_error = None; } else { self.last_non_retry_error = Some(( notification.turn_id.clone(), @@ -6498,6 +6500,10 @@ impl ChatWidget { self.on_interrupted_turn(TurnAbortReason::Interrupted); } TurnStatus::Failed => { + if replay_kind.is_some() { + self.last_non_retry_error = None; + return; + } if let Some(error) = notification.turn.error { if self.last_non_retry_error.as_ref() == Some(&(notification.turn.id.clone(), error.message.clone())) @@ -6835,24 +6841,26 @@ impl ChatWidget { message, codex_error_info, }) => { - if codex_error_info - .as_ref() - .is_some_and(|info| self.handle_steer_rejected_error(info)) + if !from_replay + && !codex_error_info + .as_ref() + .is_some_and(|info| self.handle_steer_rejected_error(info)) { - } else if let Some(kind) = codex_error_info - .as_ref() - .and_then(core_rate_limit_error_kind) - { - match kind { - RateLimitErrorKind::ServerOverloaded => { - self.on_server_overloaded_error(message) - } - RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => { - self.on_error(message) + if let Some(kind) = codex_error_info + .as_ref() + .and_then(core_rate_limit_error_kind) + { + match kind { + RateLimitErrorKind::ServerOverloaded => { + self.on_server_overloaded_error(message) + } + RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => { + self.on_error(message) + } } + } else { + self.on_error(message); } - } else { - self.on_error(message); } } EventMsg::McpStartupUpdate(ev) => self.on_mcp_startup_update(ev), diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 9b7da74f8b..499283914c 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -898,6 +898,61 @@ async fn replayed_buffered_shell_turn_completion_clears_pending_shell_marker() { assert!(!chat.pending_standalone_user_shell_command); } +#[tokio::test] +async fn replayed_non_retry_error_preserves_pending_shell_submit_markers() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.pending_turn_start_after_submit = true; + chat.pending_standalone_user_shell_command = true; + + chat.handle_server_notification( + ServerNotification::Error(ErrorNotification { + error: AppServerTurnError { + message: "permission denied".to_string(), + codex_error_info: None, + additional_details: None, + }, + will_retry: false, + thread_id: "thread-1".to_string(), + turn_id: "old-turn".to_string(), + }), + Some(ReplayKind::ThreadSnapshot), + ); + + assert!(drain_insert_history(&mut rx).is_empty()); + assert!(chat.pending_turn_start_after_submit); + assert!(chat.pending_standalone_user_shell_command); + assert!(chat.standalone_user_shell_turn_id.is_none()); +} + +#[tokio::test] +async fn replayed_failed_completed_turn_preserves_pending_shell_submit_markers() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.pending_turn_start_after_submit = true; + chat.pending_standalone_user_shell_command = true; + + chat.replay_thread_turns( + vec![AppServerTurn { + id: "old-turn".to_string(), + items: Vec::new(), + status: AppServerTurnStatus::Failed, + error: Some(AppServerTurnError { + message: "permission denied".to_string(), + codex_error_info: None, + additional_details: None, + }), + started_at: None, + completed_at: Some(0), + duration_ms: None, + }], + ReplayKind::ThreadSnapshot, + ); + + assert!(drain_insert_history(&mut rx).is_empty()); + assert!(chat.pending_turn_start_after_submit); + assert!(chat.pending_standalone_user_shell_command); + assert!(chat.standalone_user_shell_turn_id.is_none()); +} + #[tokio::test] async fn replayed_stream_error_does_not_set_retry_status_or_status_indicator() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;