diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 227c816e1d..475961a8d1 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -6864,6 +6864,11 @@ impl ChatWidget { if self.bottom_pane.is_task_running() { return; } + if !self.bottom_pane.no_modal_or_popup_active() { + self.resume_queued_inputs_when_idle = !self.queued_user_messages.is_empty(); + self.refresh_pending_input_preview(); + return; + } let mut resume_when_idle = false; while !self.bottom_pane.is_task_running() { let Some(queued_message) = self.queued_user_messages.pop_front() else { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 737fa29c19..52e1d9a1f1 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -8488,6 +8488,72 @@ async fn theme_slash_command_while_task_running_opens_popup_instead_of_queueing( assert!(drain_insert_history(&mut rx).is_empty()); } +#[tokio::test] +async fn queued_followup_waits_for_popup_opened_during_running_turn() { + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None).await; + chat.handle_codex_event(Event { + id: "configured".into(), + msg: EventMsg::SessionConfigured(SessionConfiguredEvent { + session_id: ThreadId::new(), + forked_from_id: None, + thread_name: None, + model: "test-model".to_string(), + model_provider_id: "test-provider".to_string(), + service_tier: None, + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + cwd: PathBuf::from("/home/user/project"), + reasoning_effort: Some(ReasoningEffortConfig::default()), + history_log_id: 0, + history_entry_count: 0, + initial_messages: None, + network_proxy: None, + rollout_path: None, + }), + }); + drain_insert_history(&mut rx); + chat.on_task_started(); + chat.dispatch_command(SlashCommand::Theme); + assert!(chat.has_active_view(), "expected /theme popup to stay open"); + + chat.queued_user_messages + .push_back(UserMessage::from("followup".to_string())); + + chat.on_task_complete(None, false); + + assert!( + chat.has_active_view(), + "expected theme popup to remain open" + ); + assert_eq!( + chat.queued_user_message_texts(), + vec!["followup".to_string()] + ); + assert!(chat.resume_queued_inputs_when_idle); + assert_no_submit_op(&mut op_rx); + + let _ = chat + .bottom_pane + .handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + assert_matches!(rx.try_recv(), Ok(AppEvent::BottomPaneViewCompleted)); + assert!(!chat.has_active_view(), "expected popup to dismiss"); + + chat.maybe_resume_queued_inputs_when_idle(); + + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "followup".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected Op::UserTurn, got {other:?}"), + } + assert!(chat.queued_user_messages.is_empty()); + assert!(!chat.resume_queued_inputs_when_idle); +} + #[tokio::test] async fn model_selection_queues_selected_action_while_task_running() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.1-codex")).await;