From 4a377df0ec58f1bb11d4af65affdd8bc9ad49653 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 2 Mar 2026 13:11:55 -0800 Subject: [PATCH] tui: reset plan popup state after steer --- codex-rs/tui/src/chatwidget.rs | 13 +--- codex-rs/tui/src/chatwidget/tests.rs | 89 +++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index d59a8669db..a4a44e5979 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -624,7 +624,6 @@ pub(crate) struct ChatWidget { // corresponding user message item. pending_steers: VecDeque, // Whether a steer was submitted during the current turn. - steer_submitted_this_turn: bool, /// Terminal-appropriate keybinding for popping the most-recently queued /// message back into the composer. Determined once at construction time via /// [`queued_message_edit_binding_for_terminal`] and propagated to @@ -1443,7 +1442,6 @@ impl ChatWidget { self.turn_sleep_inhibitor.set_turn_running(true); self.saw_plan_update_this_turn = false; self.saw_plan_item_this_turn = false; - self.steer_submitted_this_turn = false; self.plan_delta_buffer.clear(); self.plan_item_active = false; self.adaptive_chunking.reset(); @@ -1530,11 +1528,7 @@ impl ChatWidget { } self.refresh_pending_input_preview(); - if !from_replay - && self.queued_user_messages.is_empty() - && !self.steer_submitted_this_turn - && !had_pending_steers - { + if !from_replay && self.queued_user_messages.is_empty() && !had_pending_steers { self.maybe_prompt_plan_implementation(); } // Keep this flag for replayed completion events so a subsequent live TurnComplete can @@ -3005,7 +2999,6 @@ impl ChatWidget { forked_from: None, queued_user_messages: VecDeque::new(), pending_steers: VecDeque::new(), - steer_submitted_this_turn: false, queued_message_edit_binding, show_welcome_banner: is_first_run, startup_tooltip_override, @@ -3190,7 +3183,6 @@ impl ChatWidget { plan_item_active: false, queued_user_messages: VecDeque::new(), pending_steers: VecDeque::new(), - steer_submitted_this_turn: false, queued_message_edit_binding, show_welcome_banner: is_first_run, startup_tooltip_override, @@ -3356,7 +3348,6 @@ impl ChatWidget { forked_from: None, queued_user_messages: VecDeque::new(), pending_steers: VecDeque::new(), - steer_submitted_this_turn: false, queued_message_edit_binding, show_welcome_banner: false, startup_tooltip_override: None, @@ -4363,7 +4354,7 @@ impl ChatWidget { if let Some(pending_steer) = pending_steer { self.pending_steers.push_back(pending_steer); - self.steer_submitted_this_turn = true; + self.saw_plan_item_this_turn = false; self.refresh_pending_input_preview(); } diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 0ad6a47738..dd79fc8005 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1710,7 +1710,6 @@ async fn make_chatwidget_manual( startup_tooltip_override: None, queued_user_messages: VecDeque::new(), pending_steers: VecDeque::new(), - steer_submitted_this_turn: false, queued_message_edit_binding: crate::key_hint::alt(KeyCode::Up), suppress_session_configured_redraw: false, pending_notification: None, @@ -2744,6 +2743,94 @@ async fn plan_implementation_popup_shows_after_proposed_plan_output() { ); } +#[tokio::test] +async fn plan_implementation_popup_skips_when_steer_follows_proposed_plan() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5")).await; + chat.set_feature_enabled(Feature::CollaborationModes, true); + let plan_mask = + collaboration_modes::mask_for_kind(chat.models_manager.as_ref(), ModeKind::Plan) + .expect("expected plan collaboration mask"); + chat.set_collaboration_mask(plan_mask); + chat.thread_id = Some(ThreadId::new()); + + chat.on_task_started(); + chat.on_plan_item_completed( + "- Step 1 +- Step 2 +" + .to_string(), + ); + chat.bottom_pane + .set_composer_text("Please continue.".to_string(), Vec::new(), Vec::new()); + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "Please continue.".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected Op::UserTurn, got {other:?}"), + } + + complete_user_message(&mut chat, "user-1", "Please continue."); + chat.on_task_complete(None, false); + + let popup = render_bottom_popup(&chat, 80); + assert!( + !popup.contains(PLAN_IMPLEMENTATION_TITLE), + "expected no plan popup after a steer follows the plan, got {popup:?}" + ); +} + +#[tokio::test] +async fn plan_implementation_popup_shows_after_new_plan_follows_steer() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5")).await; + chat.set_feature_enabled(Feature::CollaborationModes, true); + let plan_mask = + collaboration_modes::mask_for_kind(chat.models_manager.as_ref(), ModeKind::Plan) + .expect("expected plan collaboration mask"); + chat.set_collaboration_mask(plan_mask); + chat.thread_id = Some(ThreadId::new()); + + chat.on_task_started(); + chat.on_plan_item_completed( + "- Initial plan +" + .to_string(), + ); + chat.bottom_pane + .set_composer_text("Please revise.".to_string(), Vec::new(), Vec::new()); + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "Please revise.".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected Op::UserTurn, got {other:?}"), + } + + complete_user_message(&mut chat, "user-1", "Please revise."); + chat.on_plan_item_completed( + "- Revised plan +" + .to_string(), + ); + chat.on_task_complete(None, false); + + let popup = render_bottom_popup(&chat, 80); + assert!( + popup.contains(PLAN_IMPLEMENTATION_TITLE), + "expected plan popup after a newer plan follows the steer, got {popup:?}" + ); +} + #[tokio::test] async fn plan_implementation_popup_skips_when_rate_limit_prompt_pending() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;