From fca8c00f11bdbfaabed6bae2cb3765fcbd106fe8 Mon Sep 17 00:00:00 2001 From: rakan-oai Date: Tue, 14 Jul 2026 16:25:14 +0000 Subject: [PATCH] Fix TUI status visibility around streamed output (#33105) ## What changed - Hide the working-status indicator when finalized assistant output is committed to the transcript, including single-line final answers. - Restore the indicator when image generation begins during a running task after its streamed preamble has been flushed. ## Testing - Add snapshot coverage for a single-line final answer and for image generation following a single-line preamble. GitOrigin-RevId: e294a11cda3f0ce46f2bbed1be2a1380c9c73085 --- ...eration_begin_restores_working_status.snap | 11 ++++++ ...ine_final_answer_hides_working_status.snap | 14 +++++++ codex-rs/tui/src/chatwidget/streaming.rs | 5 +++ .../tui/src/chatwidget/tests/exec_flow.rs | 24 ++++++++++++ .../src/chatwidget/tests/status_and_layout.rs | 39 +++++++++++++++++++ codex-rs/tui/src/chatwidget/tool_lifecycle.rs | 3 ++ 6 files changed, 96 insertions(+) create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__image_generation_begin_restores_working_status.snap create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__single_line_final_answer_hides_working_status.snap diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__image_generation_begin_restores_working_status.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__image_generation_begin_restores_working_status.snap new file mode 100644 index 0000000000..65070af637 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__image_generation_begin_restores_working_status.snap @@ -0,0 +1,11 @@ +--- +source: tui/src/chatwidget/tests/exec_flow.rs +expression: normalized_backend_snapshot(terminal.backend()) +--- +" " +"• Working (0s • esc to interrupt) " +" " +" " +"› Ask Codex to do anything " +" " +" gpt-5.6-sol default · /tmp/project " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__single_line_final_answer_hides_working_status.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__single_line_final_answer_hides_working_status.snap new file mode 100644 index 0000000000..dfa6b7a09b --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__single_line_final_answer_hides_working_status.snap @@ -0,0 +1,14 @@ +--- +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: normalize_snapshot_paths(terminal.backend().vt100().screen().contents()) +--- + +› count to 1 + + +• 1 + + +› Ask Codex to do anything + + gpt-5 default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/streaming.rs b/codex-rs/tui/src/chatwidget/streaming.rs index e0d90dad9b..010d41cdb2 100644 --- a/codex-rs/tui/src/chatwidget/streaming.rs +++ b/codex-rs/tui/src/chatwidget/streaming.rs @@ -26,6 +26,11 @@ impl ChatWidget { }; self.clear_active_stream_tail(); let (cell, source) = controller.finalize(); + // Match newline-committed streaming behavior: once assistant output is ready to be + // committed into history, hide the inline status row so transcript content replaces it. + if cell.is_some() { + self.bottom_pane.hide_status_indicator(); + } let deferred_history_cell = if scrollback_reflow == crate::app_event::ConsolidationScrollbackReflow::Required { cell diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 99190ec726..41cd7dbc84 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -877,6 +877,30 @@ async fn view_image_tool_call_preserves_foreign_path() { assert_chatwidget_snapshot!("foreign_image_attachment_history_snapshot", combined); } +#[tokio::test] +async fn image_generation_begin_restores_working_status_after_single_line_preamble() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + chat.on_task_started(); + chat.on_agent_message_delta("Generating an image.".to_string()); + chat.on_image_generation_begin(); + + assert!(chat.bottom_pane.is_task_running()); + assert!(chat.bottom_pane.status_indicator_visible()); + + let width: u16 = 80; + let height = chat.desired_height(width); + let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(width, height)) + .expect("create terminal"); + terminal + .draw(|frame| chat.render(frame.area(), frame.buffer_mut())) + .expect("draw image generation status"); + assert_chatwidget_snapshot!( + "image_generation_begin_restores_working_status", + normalized_backend_snapshot(terminal.backend()) + ); +} + #[tokio::test] async fn image_generation_call_adds_history_cell() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index fe17933f52..7c52269adb 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -1525,6 +1525,45 @@ async fn streaming_final_answer_keeps_task_running_state() { assert!(!chat.bottom_pane.quit_shortcut_hint_visible()); } +#[tokio::test] +async fn single_line_final_answer_hides_working_status_snapshot() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; + chat.thread_id = Some(ThreadId::new()); + + complete_user_message(&mut chat, "user-1", "count to 1"); + chat.on_task_started(); + complete_assistant_message( + &mut chat, + "msg-final-single-line", + "1", + Some(MessagePhase::FinalAnswer), + ); + + assert!(chat.bottom_pane.is_task_running()); + assert!(!chat.bottom_pane.status_indicator_visible()); + + let width: u16 = 40; + let vt_height: u16 = 10; + let ui_height = chat.desired_height(width); + let viewport = Rect::new(0, vt_height - ui_height - 1, width, ui_height); + let backend = VT100Backend::new(width, vt_height); + let mut terminal = crate::custom_terminal::Terminal::with_options(backend).expect("terminal"); + terminal.set_viewport_area(viewport); + + for lines in drain_insert_history(&mut rx) { + crate::insert_history::insert_history_lines(&mut terminal, lines) + .expect("insert history lines"); + } + + terminal + .draw(|frame| chat.render(frame.area(), frame.buffer_mut())) + .expect("draw final answer"); + assert_chatwidget_snapshot!( + "single_line_final_answer_hides_working_status", + normalize_snapshot_paths(terminal.backend().vt100().screen().contents()) + ); +} + #[tokio::test] async fn ctrl_c_interrupt_pauses_active_goal_turn() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/chatwidget/tool_lifecycle.rs b/codex-rs/tui/src/chatwidget/tool_lifecycle.rs index 99d84d155b..2b8f5b1bc8 100644 --- a/codex-rs/tui/src/chatwidget/tool_lifecycle.rs +++ b/codex-rs/tui/src/chatwidget/tool_lifecycle.rs @@ -25,6 +25,9 @@ impl ChatWidget { pub(super) fn on_image_generation_begin(&mut self) { self.record_visible_turn_activity(); self.flush_answer_stream_with_separator(); + if self.bottom_pane.is_task_running() { + self.bottom_pane.ensure_status_indicator(); + } } pub(super) fn on_image_generation_end(