From ce5ad7b295afbaa763b595eef5501ce4c3eb84ab Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 13 Apr 2026 08:52:14 -0700 Subject: [PATCH] Emit plan-mode prompt notifications for questionnaires (#17417) Addresses #17252 Problem: Plan-mode clarification questionnaires used the generic user-input notification type, so configs listening for plan-mode-prompt did not fire when request_user_input waited for an answer. Solution: Map request_user_input prompts to the plan-mode-prompt notification and remove the obsolete user-input TUI notification variant. --- codex-rs/tui/src/chatwidget.rs | 49 ++++++------------- .../tui/src/chatwidget/tests/plan_mode.rs | 33 ++----------- 2 files changed, 18 insertions(+), 64 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 413ea3b8a..5f1196c94 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -4555,10 +4555,14 @@ impl ChatWidget { pub(crate) fn handle_request_user_input_now(&mut self, ev: RequestUserInputEvent) { self.flush_answer_stream_with_separator(); - self.notify(Notification::UserInputRequested { - question_count: ev.questions.len(), - summary: Notification::user_input_request_summary(&ev.questions), - }); + let question_count = ev.questions.len(); + let summary = Notification::user_input_request_summary(&ev.questions); + let title = match (question_count, summary.as_deref()) { + (1, Some(summary)) => summary.to_string(), + (1, None) => "Question requested".to_string(), + (count, _) => format!("{count} questions requested"), + }; + self.notify(Notification::PlanModePrompt { title }); self.bottom_pane.push_user_input_request(ev); self.request_redraw(); } @@ -10668,26 +10672,11 @@ impl Renderable for ChatWidget { #[derive(Debug)] enum Notification { - AgentTurnComplete { - response: String, - }, - ExecApprovalRequested { - command: String, - }, - EditApprovalRequested { - cwd: PathBuf, - changes: Vec, - }, - ElicitationRequested { - server_name: String, - }, - PlanModePrompt { - title: String, - }, - UserInputRequested { - question_count: usize, - summary: Option, - }, + AgentTurnComplete { response: String }, + ExecApprovalRequested { command: String }, + EditApprovalRequested { cwd: PathBuf, changes: Vec }, + ElicitationRequested { server_name: String }, + PlanModePrompt { title: String }, } impl Notification { @@ -10720,14 +10709,6 @@ impl Notification { Notification::PlanModePrompt { title } => { format!("Plan mode prompt: {title}") } - Notification::UserInputRequested { - question_count, - summary, - } => match (*question_count, summary.as_deref()) { - (1, Some(summary)) => format!("Question requested: {summary}"), - (1, None) => "Question requested".to_string(), - (count, _) => format!("Questions requested: {count}"), - }, } } @@ -10738,7 +10719,6 @@ impl Notification { | Notification::EditApprovalRequested { .. } | Notification::ElicitationRequested { .. } => "approval-requested", Notification::PlanModePrompt { .. } => "plan-mode-prompt", - Notification::UserInputRequested { .. } => "user-input-requested", } } @@ -10748,8 +10728,7 @@ impl Notification { Notification::ExecApprovalRequested { .. } | Notification::EditApprovalRequested { .. } | Notification::ElicitationRequested { .. } - | Notification::PlanModePrompt { .. } - | Notification::UserInputRequested { .. } => 1, + | Notification::PlanModePrompt { .. } => 1, } } diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index ecdb15a39..083cb8069 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -265,25 +265,6 @@ fn plan_mode_prompt_notification_uses_dedicated_type_name() { ); } -#[test] -fn user_input_requested_notification_uses_dedicated_type_name() { - let notification = Notification::UserInputRequested { - question_count: 1, - summary: Some("Reasoning scope".to_string()), - }; - - assert!(notification.allowed_for(&Notifications::Custom(vec![ - "user-input-requested".to_string(), - ]))); - assert!(!notification.allowed_for(&Notifications::Custom(vec![ - "approval-requested".to_string(), - ]))); - assert_eq!( - notification.display(), - "Question requested: Reasoning scope" - ); -} - #[tokio::test] async fn open_plan_implementation_prompt_sets_pending_notification() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.1-codex-max")).await; @@ -331,7 +312,7 @@ async fn agent_turn_complete_does_not_override_pending_plan_mode_prompt_notifica } #[tokio::test] -async fn user_input_notification_overrides_pending_agent_turn_complete_notification() { +async fn request_user_input_notification_overrides_pending_agent_turn_complete_notification() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.1-codex-max")).await; chat.notify(Notification::AgentTurnComplete { @@ -355,10 +336,7 @@ async fn user_input_notification_overrides_pending_agent_turn_complete_notificat assert_matches!( chat.pending_notification, - Some(Notification::UserInputRequested { - question_count: 1, - summary: Some(ref summary), - }) if summary == "Reasoning scope" + Some(Notification::PlanModePrompt { ref title }) if title == "Reasoning scope" ); } @@ -366,7 +344,7 @@ async fn user_input_notification_overrides_pending_agent_turn_complete_notificat async fn handle_request_user_input_sets_pending_notification() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.1-codex-max")).await; chat.config.tui_notifications.notifications = - Notifications::Custom(vec!["user-input-requested".to_string()]); + Notifications::Custom(vec!["plan-mode-prompt".to_string()]); chat.handle_request_user_input_now(RequestUserInputEvent { call_id: "call-1".to_string(), @@ -386,10 +364,7 @@ async fn handle_request_user_input_sets_pending_notification() { assert_matches!( chat.pending_notification, - Some(Notification::UserInputRequested { - question_count: 1, - summary: Some(ref summary), - }) if summary == "Reasoning scope" + Some(Notification::PlanModePrompt { ref title }) if title == "Reasoning scope" ); }