diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index cfae7a6310..6638824915 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -606,6 +606,8 @@ pub(crate) struct ChatWidget { mcp_startup_pending_next_round: HashMap, /// Tracks whether the buffered next round has seen any `Starting` update yet. mcp_startup_pending_next_round_saw_starting: bool, + /// Ignore startup notifications while a review that skipped MCP startup is active. + mcp_startup_updates_suppressed_for_review: bool, connectors: ConnectorsState, ide_context: IdeContextState, plugins_cache: PluginsCacheState, @@ -1257,6 +1259,7 @@ impl ChatWidget { self.flush_interrupt_queue(); self.flush_active_cell(); self.review.is_review_mode = false; + self.mcp_startup_updates_suppressed_for_review = false; self.restore_pre_review_token_info(); self.add_to_history(history_cell::new_review_status_line( "<< Code review finished >>".to_string(), diff --git a/codex-rs/tui/src/chatwidget/constructor.rs b/codex-rs/tui/src/chatwidget/constructor.rs index 7fb6fe54e9..05698e9cfb 100644 --- a/codex-rs/tui/src/chatwidget/constructor.rs +++ b/codex-rs/tui/src/chatwidget/constructor.rs @@ -163,6 +163,7 @@ impl ChatWidget { mcp_startup_allow_terminal_only_next_round: false, mcp_startup_pending_next_round: HashMap::new(), mcp_startup_pending_next_round_saw_starting: false, + mcp_startup_updates_suppressed_for_review: false, connectors: ConnectorsState::default(), ide_context: IdeContextState::default(), plugins_cache: PluginsCacheState::default(), diff --git a/codex-rs/tui/src/chatwidget/mcp_startup.rs b/codex-rs/tui/src/chatwidget/mcp_startup.rs index 8ab9b9b2e6..a0f0f97cf6 100644 --- a/codex-rs/tui/src/chatwidget/mcp_startup.rs +++ b/codex-rs/tui/src/chatwidget/mcp_startup.rs @@ -38,6 +38,10 @@ impl ChatWidget { status: McpStartupStatus, complete_when_settled: bool, ) { + if self.mcp_startup_updates_suppressed_for_review { + return; + } + let mut activated_pending_round = false; let startup_status = if self.mcp_startup_ignore_updates_until_next_start { // Ignore-mode buffers the next plausible round so stale post-finish @@ -221,6 +225,7 @@ impl ChatWidget { self.mcp_startup_allow_terminal_only_next_round = false; self.mcp_startup_pending_next_round.clear(); self.mcp_startup_pending_next_round_saw_starting = false; + self.mcp_startup_updates_suppressed_for_review = true; if mcp_startup_owned_status { self.set_status_header(String::from("Working")); } diff --git a/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs b/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs index eb9c463501..8eb27259ff 100644 --- a/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs +++ b/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs @@ -100,6 +100,34 @@ async fn review_with_args_during_mcp_startup_skips_local_startup_round() { assert!(chat.mcp_startup_status.is_none()); assert!(chat.bottom_pane.is_task_running()); assert_eq!(chat.status_state.current_status.header, "Working"); + + handle_entered_review_mode(&mut chat, "current changes"); + handle_exited_review_mode(&mut chat); + notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting); + notify_mcp_status(&mut chat, "beta", McpServerStartupState::Starting); + assert!(chat.mcp_startup_status.is_some()); +} + +#[tokio::test] +async fn skipped_mcp_startup_updates_do_not_reopen_or_finish_review_task() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.set_mcp_startup_expected_servers(["alpha".to_string(), "beta".to_string()]); + notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting); + + chat.dispatch_command_with_args( + SlashCommand::Review, + "check regressions".to_string(), + Vec::new(), + ); + assert_matches!(op_rx.try_recv(), Ok(Op::Review { .. })); + + notify_mcp_status(&mut chat, "beta", McpServerStartupState::Starting); + notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Ready); + notify_mcp_status(&mut chat, "beta", McpServerStartupState::Ready); + + assert!(chat.mcp_startup_status.is_none()); + assert!(chat.bottom_pane.is_task_running()); + assert_eq!(chat.status_state.current_status.header, "Working"); } #[tokio::test] diff --git a/codex-rs/tui/src/chatwidget/turn_runtime.rs b/codex-rs/tui/src/chatwidget/turn_runtime.rs index fdfad7f6d8..0b2e85182f 100644 --- a/codex-rs/tui/src/chatwidget/turn_runtime.rs +++ b/codex-rs/tui/src/chatwidget/turn_runtime.rs @@ -12,10 +12,12 @@ impl ChatWidget { /// Synchronize the bottom-pane "task running" indicator with the current lifecycles. /// /// The bottom pane only has one running flag, but this module treats it as a derived state of - /// both the agent turn lifecycle and MCP startup lifecycle. + /// the agent turn, review, and MCP startup lifecycles. pub(super) fn update_task_running_state(&mut self) { self.bottom_pane.set_task_running( - self.turn_lifecycle.agent_turn_running || self.mcp_startup_status.is_some(), + self.turn_lifecycle.agent_turn_running + || self.mcp_startup_status.is_some() + || self.review.is_review_mode, ); self.refresh_plan_mode_nudge(); self.refresh_status_surfaces();