Skip to content

Commit adf3982

Browse files
2mawi2joshka-oai
authored andcommitted
tui: keep spinner/Esc interrupt during MCP startup
Fixes #7017 Signed-off-by: 2mawi2 <2mawi2@users.noreply.github.com>
1 parent bde734f commit adf3982

File tree

4 files changed

+88
-10
lines changed

4 files changed

+88
-10
lines changed

codex-rs/tui/src/chatwidget.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ pub(crate) struct ChatWidget {
364364
last_unified_wait: Option<UnifiedExecWaitState>,
365365
task_complete_pending: bool,
366366
unified_exec_processes: Vec<UnifiedExecProcessSummary>,
367+
agent_turn_running: bool,
367368
mcp_startup_status: Option<HashMap<String, McpStartupStatus>>,
368369
// Queue of interruptive UI events deferred during an active write cycle
369370
interrupts: InterruptManager,
@@ -457,6 +458,11 @@ fn create_initial_user_message(text: String, image_paths: Vec<PathBuf>) -> Optio
457458
}
458459

459460
impl ChatWidget {
461+
fn update_task_running_state(&mut self) {
462+
self.bottom_pane
463+
.set_task_running(self.agent_turn_running || self.mcp_startup_status.is_some());
464+
}
465+
460466
fn flush_answer_stream_with_separator(&mut self) {
461467
if let Some(mut controller) = self.stream_controller.take()
462468
&& let Some(cell) = controller.finalize()
@@ -613,8 +619,9 @@ impl ChatWidget {
613619
// Raw reasoning uses the same flow as summarized reasoning
614620

615621
fn on_task_started(&mut self) {
622+
self.agent_turn_running = true;
616623
self.bottom_pane.clear_ctrl_c_quit_hint();
617-
self.bottom_pane.set_task_running(true);
624+
self.update_task_running_state();
618625
self.retry_status_header = None;
619626
self.bottom_pane.set_interrupt_hint_visible(true);
620627
self.set_status_header(String::from("Working"));
@@ -628,7 +635,8 @@ impl ChatWidget {
628635
self.flush_answer_stream_with_separator();
629636
self.flush_wait_cell();
630637
// Mark task stopped and request redraw now that all content is in history.
631-
self.bottom_pane.set_task_running(false);
638+
self.agent_turn_running = false;
639+
self.update_task_running_state();
632640
self.running_commands.clear();
633641
self.suppressed_exec_calls.clear();
634642
self.last_unified_wait = None;
@@ -760,7 +768,8 @@ impl ChatWidget {
760768
// Ensure any spinner is replaced by a red ✗ and flushed into history.
761769
self.finalize_active_cell_as_failed();
762770
// Reset running state and clear streaming buffers.
763-
self.bottom_pane.set_task_running(false);
771+
self.agent_turn_running = false;
772+
self.update_task_running_state();
764773
self.running_commands.clear();
765774
self.suppressed_exec_calls.clear();
766775
self.last_unified_wait = None;
@@ -789,7 +798,7 @@ impl ChatWidget {
789798
}
790799
status.insert(ev.server, ev.status);
791800
self.mcp_startup_status = Some(status);
792-
self.bottom_pane.set_task_running(true);
801+
self.update_task_running_state();
793802
if let Some(current) = &self.mcp_startup_status {
794803
let total = current.len();
795804
let mut starting: Vec<_> = current
@@ -845,7 +854,7 @@ impl ChatWidget {
845854
}
846855

847856
self.mcp_startup_status = None;
848-
self.bottom_pane.set_task_running(false);
857+
self.update_task_running_state();
849858
self.maybe_send_next_queued_input();
850859
self.request_redraw();
851860
}
@@ -1522,6 +1531,7 @@ impl ChatWidget {
15221531
last_unified_wait: None,
15231532
task_complete_pending: false,
15241533
unified_exec_processes: Vec::new(),
1534+
agent_turn_running: false,
15251535
mcp_startup_status: None,
15261536
interrupts: InterruptManager::new(),
15271537
reasoning_buffer: String::new(),
@@ -1612,6 +1622,7 @@ impl ChatWidget {
16121622
last_unified_wait: None,
16131623
task_complete_pending: false,
16141624
unified_exec_processes: Vec::new(),
1625+
agent_turn_running: false,
16151626
mcp_startup_status: None,
16161627
interrupts: InterruptManager::new(),
16171628
reasoning_buffer: String::new(),

codex-rs/tui/src/chatwidget/tests.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use codex_core::protocol::ExecCommandSource;
3030
use codex_core::protocol::ExecPolicyAmendment;
3131
use codex_core::protocol::ExitedReviewModeEvent;
3232
use codex_core::protocol::FileChange;
33+
use codex_core::protocol::McpStartupCompleteEvent;
3334
use codex_core::protocol::McpStartupStatus;
3435
use codex_core::protocol::McpStartupUpdateEvent;
3536
use codex_core::protocol::Op;
@@ -409,6 +410,7 @@ async fn make_chatwidget_manual(
409410
last_unified_wait: None,
410411
task_complete_pending: false,
411412
unified_exec_processes: Vec::new(),
413+
agent_turn_running: false,
412414
mcp_startup_status: None,
413415
interrupts: InterruptManager::new(),
414416
reasoning_buffer: String::new(),
@@ -2953,6 +2955,32 @@ async fn mcp_startup_header_booting_snapshot() {
29532955
assert_snapshot!("mcp_startup_header_booting", terminal.backend());
29542956
}
29552957

2958+
#[tokio::test]
2959+
async fn mcp_startup_complete_does_not_clear_running_task() {
2960+
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
2961+
2962+
chat.handle_codex_event(Event {
2963+
id: "task-1".into(),
2964+
msg: EventMsg::TurnStarted(TurnStartedEvent {
2965+
model_context_window: None,
2966+
}),
2967+
});
2968+
2969+
assert!(chat.bottom_pane.is_task_running());
2970+
assert!(chat.bottom_pane.status_indicator_visible());
2971+
2972+
chat.handle_codex_event(Event {
2973+
id: "mcp-1".into(),
2974+
msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
2975+
ready: vec!["schaltwerk".into()],
2976+
..Default::default()
2977+
}),
2978+
});
2979+
2980+
assert!(chat.bottom_pane.is_task_running());
2981+
assert!(chat.bottom_pane.status_indicator_visible());
2982+
}
2983+
29562984
#[tokio::test]
29572985
async fn background_event_updates_status_header() {
29582986
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;

codex-rs/tui2/src/chatwidget.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ pub(crate) struct ChatWidget {
331331
suppressed_exec_calls: HashSet<String>,
332332
last_unified_wait: Option<UnifiedExecWaitState>,
333333
task_complete_pending: bool,
334+
agent_turn_running: bool,
334335
mcp_startup_status: Option<HashMap<String, McpStartupStatus>>,
335336
// Queue of interruptive UI events deferred during an active write cycle
336337
interrupts: InterruptManager,
@@ -423,6 +424,11 @@ fn create_initial_user_message(text: String, image_paths: Vec<PathBuf>) -> Optio
423424
}
424425

425426
impl ChatWidget {
427+
fn update_task_running_state(&mut self) {
428+
self.bottom_pane
429+
.set_task_running(self.agent_turn_running || self.mcp_startup_status.is_some());
430+
}
431+
426432
fn flush_answer_stream_with_separator(&mut self) {
427433
if let Some(mut controller) = self.stream_controller.take()
428434
&& let Some(cell) = controller.finalize()
@@ -579,8 +585,9 @@ impl ChatWidget {
579585
// Raw reasoning uses the same flow as summarized reasoning
580586

581587
fn on_task_started(&mut self) {
588+
self.agent_turn_running = true;
582589
self.bottom_pane.clear_ctrl_c_quit_hint();
583-
self.bottom_pane.set_task_running(true);
590+
self.update_task_running_state();
584591
self.retry_status_header = None;
585592
self.bottom_pane.set_interrupt_hint_visible(true);
586593
self.set_status_header(String::from("Working"));
@@ -593,7 +600,8 @@ impl ChatWidget {
593600
// If a stream is currently active, finalize it.
594601
self.flush_answer_stream_with_separator();
595602
// Mark task stopped and request redraw now that all content is in history.
596-
self.bottom_pane.set_task_running(false);
603+
self.agent_turn_running = false;
604+
self.update_task_running_state();
597605
self.running_commands.clear();
598606
self.suppressed_exec_calls.clear();
599607
self.last_unified_wait = None;
@@ -725,7 +733,8 @@ impl ChatWidget {
725733
// Ensure any spinner is replaced by a red ✗ and flushed into history.
726734
self.finalize_active_cell_as_failed();
727735
// Reset running state and clear streaming buffers.
728-
self.bottom_pane.set_task_running(false);
736+
self.agent_turn_running = false;
737+
self.update_task_running_state();
729738
self.running_commands.clear();
730739
self.suppressed_exec_calls.clear();
731740
self.last_unified_wait = None;
@@ -754,7 +763,7 @@ impl ChatWidget {
754763
}
755764
status.insert(ev.server, ev.status);
756765
self.mcp_startup_status = Some(status);
757-
self.bottom_pane.set_task_running(true);
766+
self.update_task_running_state();
758767
if let Some(current) = &self.mcp_startup_status {
759768
let total = current.len();
760769
let mut starting: Vec<_> = current
@@ -810,7 +819,7 @@ impl ChatWidget {
810819
}
811820

812821
self.mcp_startup_status = None;
813-
self.bottom_pane.set_task_running(false);
822+
self.update_task_running_state();
814823
self.maybe_send_next_queued_input();
815824
self.request_redraw();
816825
}
@@ -1381,6 +1390,7 @@ impl ChatWidget {
13811390
suppressed_exec_calls: HashSet::new(),
13821391
last_unified_wait: None,
13831392
task_complete_pending: false,
1393+
agent_turn_running: false,
13841394
mcp_startup_status: None,
13851395
interrupts: InterruptManager::new(),
13861396
reasoning_buffer: String::new(),
@@ -1469,6 +1479,7 @@ impl ChatWidget {
14691479
suppressed_exec_calls: HashSet::new(),
14701480
last_unified_wait: None,
14711481
task_complete_pending: false,
1482+
agent_turn_running: false,
14721483
mcp_startup_status: None,
14731484
interrupts: InterruptManager::new(),
14741485
reasoning_buffer: String::new(),

codex-rs/tui2/src/chatwidget/tests.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use codex_core::protocol::ExecCommandSource;
2929
use codex_core::protocol::ExecPolicyAmendment;
3030
use codex_core::protocol::ExitedReviewModeEvent;
3131
use codex_core::protocol::FileChange;
32+
use codex_core::protocol::McpStartupCompleteEvent;
3233
use codex_core::protocol::McpStartupStatus;
3334
use codex_core::protocol::McpStartupUpdateEvent;
3435
use codex_core::protocol::Op;
@@ -397,6 +398,7 @@ async fn make_chatwidget_manual(
397398
suppressed_exec_calls: HashSet::new(),
398399
last_unified_wait: None,
399400
task_complete_pending: false,
401+
agent_turn_running: false,
400402
mcp_startup_status: None,
401403
interrupts: InterruptManager::new(),
402404
reasoning_buffer: String::new(),
@@ -2520,6 +2522,32 @@ async fn mcp_startup_header_booting_snapshot() {
25202522
assert_snapshot!("mcp_startup_header_booting", terminal.backend());
25212523
}
25222524

2525+
#[tokio::test]
2526+
async fn mcp_startup_complete_does_not_clear_running_task() {
2527+
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
2528+
2529+
chat.handle_codex_event(Event {
2530+
id: "task-1".into(),
2531+
msg: EventMsg::TurnStarted(TurnStartedEvent {
2532+
model_context_window: None,
2533+
}),
2534+
});
2535+
2536+
assert!(chat.bottom_pane.is_task_running());
2537+
assert!(chat.bottom_pane.status_indicator_visible());
2538+
2539+
chat.handle_codex_event(Event {
2540+
id: "mcp-1".into(),
2541+
msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
2542+
ready: vec!["schaltwerk".into()],
2543+
..Default::default()
2544+
}),
2545+
});
2546+
2547+
assert!(chat.bottom_pane.is_task_running());
2548+
assert!(chat.bottom_pane.status_indicator_visible());
2549+
}
2550+
25232551
#[tokio::test]
25242552
async fn background_event_updates_status_header() {
25252553
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;

0 commit comments

Comments
 (0)