From 57ba758df533ffceedaeee3c3eef5ea1b281fa1d Mon Sep 17 00:00:00 2001 From: charley-oai Date: Tue, 13 Jan 2026 11:23:22 -0800 Subject: [PATCH] Fix queued messages during /review (#9122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sending a message during /review interrupts the review, whereas during normal operation, sending a message while the agent is running will queue the message. This is unexpected behavior, and since /review usually takes a while, it takes away a potentially useful operation. Summary - Treat review mode as an active task for message queuing so inputs don’t inject into the running review turn. - Prevents user submissions from rendering immediately in the transcript while the review continues streaming. - Keeps review UX consistent with normal “task running” behavior and avoids accidental interrupt/replacement. Notes - This change only affects UI queuing logic; core review flow and task lifecycle remain unchanged. --- codex-rs/tui/src/chatwidget.rs | 15 +++++++--- ..._review_queues_user_messages_snapshot.snap | 23 ++++++++++++++ codex-rs/tui/src/chatwidget/tests.rs | 30 +++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 6ba7cb0d4..d62329024 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2077,7 +2077,7 @@ impl ChatWidget { } fn queue_user_message(&mut self, user_message: UserMessage) { - if self.bottom_pane.is_task_running() { + if self.bottom_pane.is_task_running() || self.is_review_mode { self.queued_user_messages.push_back(user_message); self.refresh_queued_user_messages(); } else { @@ -2286,7 +2286,7 @@ impl ChatWidget { } } EventMsg::EnteredReviewMode(review_request) => { - self.on_entered_review_mode(review_request) + self.on_entered_review_mode(review_request, from_replay) } EventMsg::ExitedReviewMode(review) => self.on_exited_review_mode(review), EventMsg::ContextCompacted(_) => self.on_agent_message("Context compacted".to_owned()), @@ -2300,11 +2300,15 @@ impl ChatWidget { } } - fn on_entered_review_mode(&mut self, review: ReviewRequest) { + fn on_entered_review_mode(&mut self, review: ReviewRequest, from_replay: bool) { // Enter review mode and emit a concise banner if self.pre_review_token_info.is_none() { self.pre_review_token_info = Some(self.token_info.clone()); } + // Avoid toggling running state for replayed history events on resume. + if !from_replay && !self.bottom_pane.is_task_running() { + self.bottom_pane.set_task_running(true); + } self.is_review_mode = true; let hint = review .user_facing_hint @@ -3787,9 +3791,12 @@ impl ChatWidget { self.bottom_pane.clear_esc_backtrack_hint(); } /// Forward an `Op` directly to codex. - pub(crate) fn submit_op(&self, op: Op) { + pub(crate) fn submit_op(&mut self, op: Op) { // Record outbound operation for session replay fidelity. crate::session_log::log_outbound_op(&op); + if matches!(&op, Op::Review { .. }) && !self.bottom_pane.is_task_running() { + self.bottom_pane.set_task_running(true); + } if let Err(e) = self.codex_op_tx.send(op) { tracing::error!("failed to submit op: {e}"); } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap new file mode 100644 index 000000000..2a7717df7 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap @@ -0,0 +1,23 @@ +--- +source: tui/src/chatwidget/tests.rs +assertion_line: 3840 +expression: term.backend().vt100().screen().contents() +--- + + + + + + + + + + +• Working (0s • esc to interrupt) + ↳ Queued while /review is running. + ⌥ + ↑ edit + + +› Ask Codex to do anything + + 100% context left · ? for shortcuts diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index f51392fa9..4567c56a9 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -3871,3 +3871,33 @@ async fn chatwidget_tall() { .unwrap(); assert_snapshot!(term.backend().vt100().screen().contents()); } + +#[tokio::test] +async fn review_queues_user_messages_snapshot() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + + chat.handle_codex_event(Event { + id: "review-1".into(), + msg: EventMsg::EnteredReviewMode(ReviewRequest { + target: ReviewTarget::UncommittedChanges, + user_facing_hint: Some("current changes".to_string()), + }), + }); + let _ = drain_insert_history(&mut rx); + + chat.queue_user_message(UserMessage::from( + "Queued while /review is running.".to_string(), + )); + + let width: u16 = 80; + let height: u16 = 18; + let backend = VT100Backend::new(width, height); + let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal"); + let desired_height = chat.desired_height(width).min(height); + term.set_viewport_area(Rect::new(0, height - desired_height, width, desired_height)); + term.draw(|f| { + chat.render(f.area(), f.buffer_mut()); + }) + .unwrap(); + assert_snapshot!(term.backend().vt100().screen().contents()); +}