diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 7b2adaf155..8dc612ecbb 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -215,14 +215,14 @@ impl App { scrollback_reflow, deferred_history_cell, )?; - self.insert_completed_token_activity_output_if_ready(tui); + self.insert_completed_token_activity_output_after_stream_shutdown(tui); } AppEvent::ConsolidateProposedPlan(source) => { if !self.terminal_resize_reflow_enabled() { if !self.transcript_reflow.history_cell_refresh_requested() { self.transcript_reflow.clear(); } - self.insert_completed_token_activity_output(tui); + self.insert_completed_token_activity_output_after_stream_shutdown(tui); return Ok(AppRunControl::Continue); } let end = self.transcript_cells.len(); @@ -257,7 +257,7 @@ impl App { self.maybe_finish_stream_reflow(tui)?; } - self.insert_completed_token_activity_output_if_ready(tui); + self.insert_completed_token_activity_output_after_stream_shutdown(tui); } AppEvent::ApplyThreadRollback { num_turns } => { if self.apply_non_pending_thread_rollback(num_turns) { @@ -743,6 +743,9 @@ impl App { self.insert_completed_token_activity_output_if_ready(tui); } } + AppEvent::CommitCompletedTokenActivityOutput => { + self.insert_completed_token_activity_output_after_stream_shutdown(tui); + } AppEvent::ConnectorsLoaded { result, is_final } => { self.chat_widget.on_connectors_loaded(result, is_final); } diff --git a/codex-rs/tui/src/app/history_ui.rs b/codex-rs/tui/src/app/history_ui.rs index f07ee48f15..8595dda755 100644 --- a/codex-rs/tui/src/app/history_ui.rs +++ b/codex-rs/tui/src/app/history_ui.rs @@ -48,6 +48,16 @@ impl App { } } + pub(super) fn insert_completed_token_activity_output_after_stream_shutdown( + &mut self, + tui: &mut tui::Tui, + ) { + if self.chat_widget.token_activity_history_insertion_blocked() { + return; + } + self.insert_completed_token_activity_output(tui); + } + pub(super) fn open_url_in_browser(&mut self, url: String) { if let Err(err) = webbrowser::open(&url) { self.chat_widget diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 3e3a506fb7..3d9e9b711f 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -304,6 +304,9 @@ pub(crate) enum AppEvent { result: Result, }, + /// Commit a settled token activity card after a stream shutdown barrier. + CommitCompletedTokenActivityOutput, + /// Send a user-confirmed request to notify the workspace owner. SendAddCreditsNudgeEmail { credit_type: AddCreditsNudgeCreditType, diff --git a/codex-rs/tui/src/chatwidget/streaming.rs b/codex-rs/tui/src/chatwidget/streaming.rs index e6f5701d1c..faaeb1efc7 100644 --- a/codex-rs/tui/src/chatwidget/streaming.rs +++ b/codex-rs/tui/src/chatwidget/streaming.rs @@ -51,6 +51,9 @@ impl ChatWidget { if had_stream_controller && self.stream_controllers_idle() { self.app_event_tx.send(AppEvent::StopCommitAnimation); } + if had_stream_controller { + self.request_completed_token_activity_output_insertion(); + } } pub(super) fn stream_controllers_idle(&self) -> bool { diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 2a5515648e..9c8be6ed36 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -1289,8 +1289,12 @@ async fn completed_token_activity_refresh_waits_for_active_stream() { Some("/tokens daily\n\n Token activity\n Token activity unavailable\n".to_string()), ); - chat.stream_controller = None; + chat.finalize_turn(); assert!(!chat.token_activity_history_insertion_blocked()); + assert!( + std::iter::from_fn(|| rx.try_recv().ok()) + .any(|event| matches!(event, AppEvent::CommitCompletedTokenActivityOutput)) + ); assert!(chat.take_completed_token_activity_output().is_some()); } diff --git a/codex-rs/tui/src/chatwidget/tokens.rs b/codex-rs/tui/src/chatwidget/tokens.rs index 50e426d528..5eb0e34ff0 100644 --- a/codex-rs/tui/src/chatwidget/tokens.rs +++ b/codex-rs/tui/src/chatwidget/tokens.rs @@ -694,6 +694,13 @@ impl ChatWidget { Some(output) } + pub(crate) fn request_completed_token_activity_output_insertion(&self) { + if self.completed_token_activity_output.is_some() { + self.app_event_tx + .send(AppEvent::CommitCompletedTokenActivityOutput); + } + } + pub(crate) fn clear_pending_token_activity_refreshes(&mut self) { let cleared_refresh = self.refreshing_token_activity_output.take().is_some(); let cleared_completed = self.completed_token_activity_output.take().is_some(); diff --git a/codex-rs/tui/src/chatwidget/turn_runtime.rs b/codex-rs/tui/src/chatwidget/turn_runtime.rs index 4868d6a25c..e07b19c4ee 100644 --- a/codex-rs/tui/src/chatwidget/turn_runtime.rs +++ b/codex-rs/tui/src/chatwidget/turn_runtime.rs @@ -51,7 +51,9 @@ impl ChatWidget { self.turn_lifecycle.start(Instant::now()); self.transcript.reset_turn_flags(); self.adaptive_chunking.reset(); - self.plan_stream_controller = None; + if self.plan_stream_controller.take().is_some() { + self.request_completed_token_activity_output_insertion(); + } self.turn_runtime_metrics = RuntimeMetricsSummary::default(); self.session_telemetry.reset_runtime_metrics(); self.bottom_pane.clear_quit_shortcut_hint(); @@ -127,6 +129,7 @@ impl ChatWidget { self.app_event_tx .send(AppEvent::ConsolidateProposedPlan(source)); } + self.request_completed_token_activity_output_insertion(); } self.flush_unified_exec_wait_streak(); if !from_replay { @@ -316,6 +319,7 @@ impl ChatWidget { self.adaptive_chunking.reset(); self.stream_controller = None; self.plan_stream_controller = None; + self.request_completed_token_activity_output_insertion(); self.status_state.pending_status_indicator_restore = false; self.request_status_line_branch_refresh(); self.request_status_line_git_summary_refresh();