mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Notify users when asynchronous questions arrive in the TUI (#46574)
## What changed Send an `async-question` notification when new unanswered questions arrive, honoring existing notification settings and taking priority over turn completion. Show a shortened question title for a single question or a count for a batch. Empty batches, duplicate deliveries, and replayed history do not trigger notifications. ## Testing Add tests covering live arrival, replay and duplicate suppression, notification settings, priority over turn completion, batch summaries, and long-title truncation. GitOrigin-RevId: 205dc3ff5461c2166159af5143b0ca7279a87f42
This commit is contained in:
@@ -30,6 +30,7 @@ pub(super) enum Notification {
|
||||
EditApprovalRequested { cwd: PathBuf, changes: Vec<PathBuf> },
|
||||
ElicitationRequested { server_name: String },
|
||||
PlanModePrompt { title: String },
|
||||
AsyncQuestion { title: String },
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
@@ -62,6 +63,9 @@ impl Notification {
|
||||
Notification::PlanModePrompt { title } => {
|
||||
format!("Plan mode prompt: {title}")
|
||||
}
|
||||
Notification::AsyncQuestion { title } => {
|
||||
format!("Question: {title}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +76,7 @@ impl Notification {
|
||||
| Notification::EditApprovalRequested { .. }
|
||||
| Notification::ElicitationRequested { .. } => "approval-requested",
|
||||
Notification::PlanModePrompt { .. } => "plan-mode-prompt",
|
||||
Notification::AsyncQuestion { .. } => "async-question",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +86,8 @@ impl Notification {
|
||||
Notification::ExecApprovalRequested { .. }
|
||||
| Notification::EditApprovalRequested { .. }
|
||||
| Notification::ElicitationRequested { .. }
|
||||
| Notification::PlanModePrompt { .. } => 1,
|
||||
| Notification::PlanModePrompt { .. }
|
||||
| Notification::AsyncQuestion { .. } => 1,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,19 @@ impl ChatWidget {
|
||||
message_id: &str,
|
||||
questions: &[AsyncUserInputQuestion],
|
||||
) {
|
||||
let previous_count = self.bottom_pane.question_editor().unanswered_count();
|
||||
self.bottom_pane.push_async_questions(message_id, questions);
|
||||
let added_count = self.bottom_pane.question_editor().unanswered_count() - previous_count;
|
||||
if added_count > 0 {
|
||||
let title = match questions {
|
||||
[question] if !question.title.trim().is_empty() => {
|
||||
truncate_text(question.title.trim(), /*max_graphemes*/ 30)
|
||||
}
|
||||
_ if added_count == 1 => "Question requested".to_string(),
|
||||
_ => format!("{added_count} questions requested"),
|
||||
};
|
||||
self.notify(Notification::AsyncQuestion { title });
|
||||
}
|
||||
self.refresh_pending_input_preview();
|
||||
}
|
||||
|
||||
|
||||
@@ -297,3 +297,6 @@ pub(super) use helpers::*;
|
||||
|
||||
#[path = "tests/questions_tests.rs"]
|
||||
mod questions_tests;
|
||||
|
||||
#[path = "tests/question_notifications_tests.rs"]
|
||||
mod question_notifications_tests;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
//! Async questions notify once on arrival and honor the terminal notification settings.
|
||||
|
||||
use super::*;
|
||||
use codex_protocol::items::AsyncUserInputQuestion;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_async_question_notifies_once_and_takes_priority_over_turn_completion() {
|
||||
let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let item = AppServerThreadItem::AgentMessage {
|
||||
id: "question".into(),
|
||||
text: String::new(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: Some(vec![AsyncUserInputQuestion {
|
||||
title: "Which environment?".into(),
|
||||
options: None,
|
||||
}]),
|
||||
};
|
||||
for kind in [
|
||||
ReplayKind::ResumeInitialMessages,
|
||||
ReplayKind::ThreadSnapshot,
|
||||
] {
|
||||
chat.replay_thread_item(item.clone(), "turn".into(), kind);
|
||||
assert!(chat.pending_notification.is_none());
|
||||
}
|
||||
|
||||
let notification = ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
item,
|
||||
thread_id: "thread".into(),
|
||||
turn_id: "turn".into(),
|
||||
completed_at_ms: 0,
|
||||
});
|
||||
chat.handle_server_notification(notification.clone(), /*replay_kind*/ None);
|
||||
chat.notify(Notification::AgentTurnComplete {
|
||||
response: "Done".into(),
|
||||
});
|
||||
insta::assert_snapshot!(
|
||||
chat.pending_notification.take().unwrap().display(),
|
||||
@"Question: Which environment?"
|
||||
);
|
||||
|
||||
chat.handle_server_notification(notification.clone(), /*replay_kind*/ None);
|
||||
assert!(chat.pending_notification.is_none());
|
||||
chat.bottom_pane.clear_pending_questions();
|
||||
chat.handle_server_notification(notification, /*replay_kind*/ None);
|
||||
assert!(chat.pending_notification.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn async_question_notifications_respect_settings_and_ignore_empty_batches() {
|
||||
for (settings, expected) in [
|
||||
(Notifications::Enabled(true), true),
|
||||
(Notifications::Enabled(false), false),
|
||||
(Notifications::Custom(vec!["async-question".into()]), true),
|
||||
(
|
||||
Notifications::Custom(vec!["agent-turn-complete".into()]),
|
||||
false,
|
||||
),
|
||||
] {
|
||||
let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.local_settings.tui.notification_settings.notifications = settings;
|
||||
chat.add_async_questions("empty", &[]);
|
||||
assert!(chat.pending_notification.is_none());
|
||||
chat.add_async_questions(
|
||||
"question",
|
||||
&[AsyncUserInputQuestion {
|
||||
title: "Which way?".into(),
|
||||
options: None,
|
||||
}],
|
||||
);
|
||||
assert_eq!(chat.pending_notification.is_some(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn async_question_notification_summarizes_batches_and_bounds_long_titles() {
|
||||
let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let questions = vec![
|
||||
AsyncUserInputQuestion {
|
||||
title: "Which environment should we deploy this change to?".into(),
|
||||
options: None,
|
||||
},
|
||||
AsyncUserInputQuestion {
|
||||
title: "Any details?".into(),
|
||||
options: None,
|
||||
},
|
||||
];
|
||||
chat.add_async_questions("batch", &questions);
|
||||
insta::assert_snapshot!(
|
||||
chat.pending_notification.take().unwrap().display(),
|
||||
@"Question: 2 questions requested"
|
||||
);
|
||||
chat.add_async_questions("single", &questions[..1]);
|
||||
insta::assert_snapshot!(
|
||||
chat.pending_notification.take().unwrap().display(),
|
||||
@"Question: Which environment should we..."
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user