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.
This commit is contained in:
Eric Traut
2026-04-13 08:52:14 -07:00
committed by GitHub
Unverified
parent a5783f90c9
commit ce5ad7b295
2 changed files with 18 additions and 64 deletions
+14 -35
View File
@@ -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<PathBuf>,
},
ElicitationRequested {
server_name: String,
},
PlanModePrompt {
title: String,
},
UserInputRequested {
question_count: usize,
summary: Option<String>,
},
AgentTurnComplete { response: String },
ExecApprovalRequested { command: String },
EditApprovalRequested { cwd: PathBuf, changes: Vec<PathBuf> },
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,
}
}
+4 -29
View File
@@ -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"
);
}