mirror of
https://github.com/openai/codex.git
synced 2026-05-02 18:37:01 +00:00
tui: clarify pending steer follow-ups (#13841)
## Summary - split the pending input preview into labeled pending-steer and queued follow-up sections - explain that pending steers submit after the next tool call and that Esc can interrupt and send them immediately - treat Esc as an interrupt-plus-resubmit path when pending steers exist, with updated TUI snapshots and tests Queues and steers: <img width="1038" height="263" alt="Screenshot 2026-03-07 at 10 17 17 PM" src="https://github.com/user-attachments/assets/4ef433ef-27a3-4b7c-ad69-2046f6eb89e6" /> After pressing Esc: <img width="1046" height="320" alt="Screenshot 2026-03-07 at 10 17 21 PM" src="https://github.com/user-attachments/assets/0f4d89e0-b6b9-486a-9f04-b6021f169ba7" /> ## Codex author `codex resume 019cc6f4-2cca-7803-b717-8264526dbd97` --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
parent
f41b1638c9
commit
4ad3b59de3
@@ -1866,6 +1866,7 @@ async fn make_chatwidget_manual(
|
||||
startup_tooltip_override: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
pending_steers: VecDeque::new(),
|
||||
submit_pending_steers_after_interrupt: false,
|
||||
queued_message_edit_binding: crate::key_hint::alt(KeyCode::Up),
|
||||
suppress_session_configured_redraw: false,
|
||||
pending_notification: None,
|
||||
@@ -1913,6 +1914,17 @@ fn next_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver<Op>) -> Op {
|
||||
}
|
||||
}
|
||||
|
||||
fn next_interrupt_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver<Op>) {
|
||||
loop {
|
||||
match op_rx.try_recv() {
|
||||
Ok(Op::Interrupt) => return,
|
||||
Ok(_) => continue,
|
||||
Err(TryRecvError::Empty) => panic!("expected interrupt op but queue was empty"),
|
||||
Err(TryRecvError::Disconnected) => panic!("expected interrupt op but channel closed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_no_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver<Op>) {
|
||||
while let Ok(op) = op_rx.try_recv() {
|
||||
assert!(
|
||||
@@ -4378,6 +4390,107 @@ async fn manual_interrupt_restores_pending_steers_to_composer() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn esc_interrupt_sends_all_pending_steers_immediately_and_keeps_existing_draft() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.on_task_started();
|
||||
chat.on_agent_message_delta("Final answer line\n".to_string());
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("first pending steer".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: "first pending steer".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
),
|
||||
other => panic!("expected Op::UserTurn, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("second pending steer".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: "second pending steer".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
),
|
||||
other => panic!("expected Op::UserTurn, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.queued_user_messages
|
||||
.push_back(UserMessage::from("queued draft".to_string()));
|
||||
chat.refresh_pending_input_preview();
|
||||
chat.bottom_pane
|
||||
.set_composer_text("still editing".to_string(), Vec::new(), Vec::new());
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
next_interrupt_op(&mut op_rx);
|
||||
|
||||
chat.on_interrupted_turn(TurnAbortReason::Interrupted);
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
items,
|
||||
vec![UserInput::Text {
|
||||
text: "first pending steer\nsecond pending steer".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
),
|
||||
other => panic!("expected merged pending steers to submit, got {other:?}"),
|
||||
}
|
||||
|
||||
assert!(chat.pending_steers.is_empty());
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "still editing");
|
||||
assert_eq!(chat.queued_user_messages.len(), 1);
|
||||
assert_eq!(
|
||||
chat.queued_user_messages.front().unwrap().text,
|
||||
"queued draft"
|
||||
);
|
||||
|
||||
let inserted = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
inserted
|
||||
.iter()
|
||||
.any(|cell| lines_to_single_string(cell).contains("first pending steer"))
|
||||
);
|
||||
assert!(
|
||||
inserted
|
||||
.iter()
|
||||
.any(|cell| lines_to_single_string(cell).contains("second pending steer"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn esc_with_pending_steers_overrides_agent_command_interrupt_behavior() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.on_task_started();
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("pending steer".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 { .. } => {}
|
||||
other => panic!("expected Op::UserTurn, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("/agent ".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
next_interrupt_op(&mut op_rx);
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "/agent ");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manual_interrupt_restores_pending_steer_mention_bindings_to_composer() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
@@ -6249,6 +6362,42 @@ async fn interrupted_turn_error_message_snapshot() {
|
||||
assert_snapshot!("interrupted_turn_error_message", last);
|
||||
}
|
||||
|
||||
// Snapshot test: interrupting specifically to submit pending steers shows an
|
||||
// informational banner instead of the generic "tell the model what to do
|
||||
// differently" error prompt.
|
||||
#[tokio::test]
|
||||
async fn interrupted_turn_pending_steers_message_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.pending_steers.push_back(pending_steer("steer 1"));
|
||||
chat.submit_pending_steers_after_interrupt = true;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
}),
|
||||
});
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let info = cells
|
||||
.iter()
|
||||
.map(|cell| lines_to_single_string(cell))
|
||||
.find(|line| line.contains("Model interrupted to submit steer instructions."))
|
||||
.expect("expected steer interrupt info message to be inserted");
|
||||
assert_snapshot!("interrupted_turn_pending_steers_message", info);
|
||||
}
|
||||
|
||||
/// Opening custom prompt from the review popup, pressing Esc returns to the
|
||||
/// parent popup, pressing Esc again dismisses all panels (back to normal mode).
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user