Compare commits

...

1 Commits

Author SHA1 Message Date
Charles Cunningham
0b6fb310e3 tui: requeue leftover pending steers on turn complete
Co-authored-by: Codex <noreply@openai.com>
2026-03-19 15:18:50 -07:00
4 changed files with 254 additions and 0 deletions

View File

@@ -1790,6 +1790,19 @@ impl ChatWidget {
self.request_redraw();
let had_pending_steers = !self.pending_steers.is_empty();
if had_pending_steers {
// Some tasks, such as `/compact`, can finish without ever committing steer input back
// through core. When that happens, preserve FIFO by converting the leftover steers
// into normal queued follow-up turns ahead of any tab-queued drafts.
let pending_steers: Vec<UserMessage> = self
.pending_steers
.drain(..)
.map(|pending| pending.user_message)
.collect();
for user_message in pending_steers.into_iter().rev() {
self.queued_user_messages.push_front(user_message);
}
}
self.refresh_pending_input_preview();
if !from_replay && self.queued_user_messages.is_empty() && !had_pending_steers {

View File

@@ -4122,6 +4122,120 @@ async fn steer_enter_uses_pending_steers_while_final_answer_stream_is_active() {
assert!(lines_to_single_string(&inserted[0]).contains("queued while streaming"));
}
#[tokio::test]
async fn turn_complete_requeues_unconsumed_pending_steers_before_queued_drafts() {
let (mut chat, mut 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("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();
assert!(drain_insert_history(&mut rx).is_empty());
chat.handle_codex_event(Event {
id: "turn-1".into(),
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: 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 requeued pending steer to submit first, got {other:?}"),
}
assert!(chat.pending_steers.is_empty());
assert_eq!(
chat.queued_user_message_texts(),
vec![
"second pending steer".to_string(),
"queued draft".to_string()
]
);
}
#[tokio::test]
async fn turn_complete_requeues_pending_steer_after_final_message_streaming() {
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(
"queued while streaming".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: "queued while streaming".to_string(),
text_elements: Vec::new(),
}]
),
other => panic!("expected Op::UserTurn, got {other:?}"),
}
assert!(drain_insert_history(&mut rx).is_empty());
chat.handle_codex_event(Event {
id: "turn-1".into(),
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
}),
});
match next_submit_op(&mut op_rx) {
Op::UserTurn { items, .. } => assert_eq!(
items,
vec![UserInput::Text {
text: "queued while streaming".to_string(),
text_elements: Vec::new(),
}]
),
other => panic!("expected requeued streamed steer to submit, got {other:?}"),
}
assert!(chat.pending_steers.is_empty());
assert!(chat.queued_user_messages.is_empty());
}
#[tokio::test]
async fn failed_pending_steer_submit_does_not_add_pending_preview() {
let (mut chat, mut rx, op_rx) = make_chatwidget_manual(None).await;

View File

@@ -2150,6 +2150,19 @@ impl ChatWidget {
self.request_redraw();
let had_pending_steers = !self.pending_steers.is_empty();
if had_pending_steers {
// Some tasks, such as `/compact`, can finish without ever committing steer input back
// through core. When that happens, preserve FIFO by converting the leftover steers
// into normal queued follow-up turns ahead of any tab-queued drafts.
let pending_steers: Vec<UserMessage> = self
.pending_steers
.drain(..)
.map(|pending| pending.user_message)
.collect();
for user_message in pending_steers.into_iter().rev() {
self.queued_user_messages.push_front(user_message);
}
}
self.refresh_pending_input_preview();
if !from_replay && self.queued_user_messages.is_empty() && !had_pending_steers {

View File

@@ -4126,6 +4126,120 @@ async fn steer_enter_uses_pending_steers_while_final_answer_stream_is_active() {
assert!(lines_to_single_string(&inserted[0]).contains("queued while streaming"));
}
#[tokio::test]
async fn turn_complete_requeues_unconsumed_pending_steers_before_queued_drafts() {
let (mut chat, mut 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("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();
assert!(drain_insert_history(&mut rx).is_empty());
chat.handle_codex_event(Event {
id: "turn-1".into(),
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: 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 requeued pending steer to submit first, got {other:?}"),
}
assert!(chat.pending_steers.is_empty());
assert_eq!(
chat.queued_user_message_texts(),
vec![
"second pending steer".to_string(),
"queued draft".to_string()
]
);
}
#[tokio::test]
async fn turn_complete_requeues_pending_steer_after_final_message_streaming() {
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(
"queued while streaming".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: "queued while streaming".to_string(),
text_elements: Vec::new(),
}]
),
other => panic!("expected Op::UserTurn, got {other:?}"),
}
assert!(drain_insert_history(&mut rx).is_empty());
chat.handle_codex_event(Event {
id: "turn-1".into(),
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
}),
});
match next_submit_op(&mut op_rx) {
Op::UserTurn { items, .. } => assert_eq!(
items,
vec![UserInput::Text {
text: "queued while streaming".to_string(),
text_elements: Vec::new(),
}]
),
other => panic!("expected requeued streamed steer to submit, got {other:?}"),
}
assert!(chat.pending_steers.is_empty());
assert!(chat.queued_user_messages.is_empty());
}
#[tokio::test]
async fn failed_pending_steer_submit_does_not_add_pending_preview() {
let (mut chat, mut rx, op_rx) = make_chatwidget_manual(None).await;