diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e025dd1135..77939ad5c7 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2379,6 +2379,9 @@ impl ChatWidget { if should_clear { self.standalone_user_shell_turn_id = None; } + if self.standalone_user_shell_turn_id.is_none() { + self.pending_standalone_user_shell_command = false; + } } fn should_hold_user_input_for_standalone_shell_turn(&self) -> bool { @@ -2860,6 +2863,9 @@ impl ChatWidget { /// This does not clear MCP startup tracking, because MCP startup can overlap with turn cleanup /// and should continue to drive the bottom-pane running indicator while it is in progress. fn finalize_turn(&mut self) { + self.pending_turn_start_after_submit = false; + self.pending_standalone_user_shell_command = false; + self.standalone_user_shell_turn_id = None; // Ensure any spinner is replaced by a red ✗ and flushed into history. self.finalize_active_cell_as_failed(); // Reset running state and clear streaming buffers. diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 4d5c0f9cb4..cf6ca84d10 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -1198,6 +1198,43 @@ async fn bang_after_user_turn_submit_before_turn_started_does_not_mark_standalon assert!(!chat.pending_standalone_user_shell_command); } +#[tokio::test] +async fn completion_without_seen_shell_start_clears_pending_marker() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.thread_id = Some(ThreadId::new()); + chat.pending_standalone_user_shell_command = true; + chat.queued_user_messages + .push_back(UserMessage::from("follow up".to_string())); + + chat.handle_server_notification( + ServerNotification::TurnCompleted(TurnCompletedNotification { + thread_id: "thread-1".to_string(), + turn: AppServerTurn { + id: "shell-turn".to_string(), + items: Vec::new(), + status: AppServerTurnStatus::Completed, + error: None, + started_at: None, + completed_at: Some(0), + duration_ms: None, + }, + }), + /*replay_kind*/ None, + ); + + assert!(!chat.pending_standalone_user_shell_command); + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "follow up".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected queued follow-up submit, got {other:?}"), + } +} + #[tokio::test] async fn disabled_slash_command_while_task_running_snapshot() { // Build a chat widget and simulate an active task