Skip to content

Commit f75b188

Browse files
committed
tui: keep spinner/Esc interrupt during MCP startup
Fixes #7017 Signed-off-by: 2mawi2 <2mawi2@users.noreply.github.com>
1 parent 8878899 commit f75b188

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
@@ -334,6 +334,7 @@ pub(crate) struct ChatWidget {
334334
last_unified_wait: Option<UnifiedExecWaitState>,
335335
task_complete_pending: bool,
336336
unified_exec_sessions: Vec<UnifiedExecSessionSummary>,
337+
agent_turn_running: bool,
337338
mcp_startup_status: Option<HashMap<String, McpStartupStatus>>,
338339
// Queue of interruptive UI events deferred during an active write cycle
339340
interrupts: InterruptManager,
@@ -403,6 +404,11 @@ fn create_initial_user_message(text: String, image_paths: Vec<PathBuf>) -> Optio
403404
}
404405

405406
impl ChatWidget {
407+
fn update_task_running_state(&mut self) {
408+
self.bottom_pane
409+
.set_task_running(self.agent_turn_running || self.mcp_startup_status.is_some());
410+
}
411+
406412
fn flush_answer_stream_with_separator(&mut self) {
407413
if let Some(mut controller) = self.stream_controller.take()
408414
&& let Some(cell) = controller.finalize()
@@ -559,8 +565,9 @@ impl ChatWidget {
559565
// Raw reasoning uses the same flow as summarized reasoning
560566

561567
fn on_task_started(&mut self) {
568+
self.agent_turn_running = true;
562569
self.bottom_pane.clear_ctrl_c_quit_hint();
563-
self.bottom_pane.set_task_running(true);
570+
self.update_task_running_state();
564571
self.retry_status_header = None;
565572
self.bottom_pane.set_interrupt_hint_visible(true);
566573
self.set_status_header(String::from("Working"));
@@ -574,7 +581,8 @@ impl ChatWidget {
574581
self.flush_answer_stream_with_separator();
575582
self.flush_wait_cell();
576583
// Mark task stopped and request redraw now that all content is in history.
577-
self.bottom_pane.set_task_running(false);
584+
self.agent_turn_running = false;
585+
self.update_task_running_state();
578586
self.running_commands.clear();
579587
self.suppressed_exec_calls.clear();
580588
self.last_unified_wait = None;
@@ -708,7 +716,8 @@ impl ChatWidget {
708716
// Ensure any spinner is replaced by a red ✗ and flushed into history.
709717
self.finalize_active_cell_as_failed();
710718
// Reset running state and clear streaming buffers.
711-
self.bottom_pane.set_task_running(false);
719+
self.agent_turn_running = false;
720+
self.update_task_running_state();
712721
self.running_commands.clear();
713722
self.suppressed_exec_calls.clear();
714723
self.last_unified_wait = None;
@@ -740,7 +749,7 @@ impl ChatWidget {
740749
}
741750
status.insert(ev.server, ev.status);
742751
self.mcp_startup_status = Some(status);
743-
self.bottom_pane.set_task_running(true);
752+
self.update_task_running_state();
744753
if let Some(current) = &self.mcp_startup_status {
745754
let total = current.len();
746755
let mut starting: Vec<_> = current
@@ -796,7 +805,7 @@ impl ChatWidget {
796805
}
797806

798807
self.mcp_startup_status = None;
799-
self.bottom_pane.set_task_running(false);
808+
self.update_task_running_state();
800809
self.maybe_send_next_queued_input();
801810
self.request_redraw();
802811
}
@@ -1465,6 +1474,7 @@ impl ChatWidget {
14651474
last_unified_wait: None,
14661475
task_complete_pending: false,
14671476
unified_exec_sessions: Vec::new(),
1477+
agent_turn_running: false,
14681478
mcp_startup_status: None,
14691479
interrupts: InterruptManager::new(),
14701480
reasoning_buffer: String::new(),
@@ -1552,6 +1562,7 @@ impl ChatWidget {
15521562
last_unified_wait: None,
15531563
task_complete_pending: false,
15541564
unified_exec_sessions: Vec::new(),
1565+
agent_turn_running: false,
15551566
mcp_startup_status: None,
15561567
interrupts: InterruptManager::new(),
15571568
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
@@ -28,6 +28,7 @@ use codex_core::protocol::ExecCommandSource;
2828
use codex_core::protocol::ExecPolicyAmendment;
2929
use codex_core::protocol::ExitedReviewModeEvent;
3030
use codex_core::protocol::FileChange;
31+
use codex_core::protocol::McpStartupCompleteEvent;
3132
use codex_core::protocol::McpStartupStatus;
3233
use codex_core::protocol::McpStartupUpdateEvent;
3334
use codex_core::protocol::Op;
@@ -389,6 +390,7 @@ async fn make_chatwidget_manual(
389390
last_unified_wait: None,
390391
task_complete_pending: false,
391392
unified_exec_sessions: Vec::new(),
393+
agent_turn_running: false,
392394
mcp_startup_status: None,
393395
interrupts: InterruptManager::new(),
394396
reasoning_buffer: String::new(),
@@ -2734,6 +2736,32 @@ async fn mcp_startup_header_booting_snapshot() {
27342736
assert_snapshot!("mcp_startup_header_booting", terminal.backend());
27352737
}
27362738

2739+
#[tokio::test]
2740+
async fn mcp_startup_complete_does_not_clear_running_task() {
2741+
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
2742+
2743+
chat.handle_codex_event(Event {
2744+
id: "task-1".into(),
2745+
msg: EventMsg::TaskStarted(TaskStartedEvent {
2746+
model_context_window: None,
2747+
}),
2748+
});
2749+
2750+
assert!(chat.bottom_pane.is_task_running());
2751+
assert!(chat.bottom_pane.status_indicator_visible());
2752+
2753+
chat.handle_codex_event(Event {
2754+
id: "mcp-1".into(),
2755+
msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
2756+
ready: vec!["schaltwerk".into()],
2757+
..Default::default()
2758+
}),
2759+
});
2760+
2761+
assert!(chat.bottom_pane.is_task_running());
2762+
assert!(chat.bottom_pane.status_indicator_visible());
2763+
}
2764+
27372765
#[tokio::test]
27382766
async fn background_event_updates_status_header() {
27392767
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
@@ -301,6 +301,7 @@ pub(crate) struct ChatWidget {
301301
suppressed_exec_calls: HashSet<String>,
302302
last_unified_wait: Option<UnifiedExecWaitState>,
303303
task_complete_pending: bool,
304+
agent_turn_running: bool,
304305
mcp_startup_status: Option<HashMap<String, McpStartupStatus>>,
305306
// Queue of interruptive UI events deferred during an active write cycle
306307
interrupts: InterruptManager,
@@ -369,6 +370,11 @@ fn create_initial_user_message(text: String, image_paths: Vec<PathBuf>) -> Optio
369370
}
370371

371372
impl ChatWidget {
373+
fn update_task_running_state(&mut self) {
374+
self.bottom_pane
375+
.set_task_running(self.agent_turn_running || self.mcp_startup_status.is_some());
376+
}
377+
372378
fn flush_answer_stream_with_separator(&mut self) {
373379
if let Some(mut controller) = self.stream_controller.take()
374380
&& let Some(cell) = controller.finalize()
@@ -525,8 +531,9 @@ impl ChatWidget {
525531
// Raw reasoning uses the same flow as summarized reasoning
526532

527533
fn on_task_started(&mut self) {
534+
self.agent_turn_running = true;
528535
self.bottom_pane.clear_ctrl_c_quit_hint();
529-
self.bottom_pane.set_task_running(true);
536+
self.update_task_running_state();
530537
self.retry_status_header = None;
531538
self.bottom_pane.set_interrupt_hint_visible(true);
532539
self.set_status_header(String::from("Working"));
@@ -539,7 +546,8 @@ impl ChatWidget {
539546
// If a stream is currently active, finalize it.
540547
self.flush_answer_stream_with_separator();
541548
// Mark task stopped and request redraw now that all content is in history.
542-
self.bottom_pane.set_task_running(false);
549+
self.agent_turn_running = false;
550+
self.update_task_running_state();
543551
self.running_commands.clear();
544552
self.suppressed_exec_calls.clear();
545553
self.last_unified_wait = None;
@@ -673,7 +681,8 @@ impl ChatWidget {
673681
// Ensure any spinner is replaced by a red ✗ and flushed into history.
674682
self.finalize_active_cell_as_failed();
675683
// Reset running state and clear streaming buffers.
676-
self.bottom_pane.set_task_running(false);
684+
self.agent_turn_running = false;
685+
self.update_task_running_state();
677686
self.running_commands.clear();
678687
self.suppressed_exec_calls.clear();
679688
self.last_unified_wait = None;
@@ -705,7 +714,7 @@ impl ChatWidget {
705714
}
706715
status.insert(ev.server, ev.status);
707716
self.mcp_startup_status = Some(status);
708-
self.bottom_pane.set_task_running(true);
717+
self.update_task_running_state();
709718
if let Some(current) = &self.mcp_startup_status {
710719
let total = current.len();
711720
let mut starting: Vec<_> = current
@@ -761,7 +770,7 @@ impl ChatWidget {
761770
}
762771

763772
self.mcp_startup_status = None;
764-
self.bottom_pane.set_task_running(false);
773+
self.update_task_running_state();
765774
self.maybe_send_next_queued_input();
766775
self.request_redraw();
767776
}
@@ -1324,6 +1333,7 @@ impl ChatWidget {
13241333
suppressed_exec_calls: HashSet::new(),
13251334
last_unified_wait: None,
13261335
task_complete_pending: false,
1336+
agent_turn_running: false,
13271337
mcp_startup_status: None,
13281338
interrupts: InterruptManager::new(),
13291339
reasoning_buffer: String::new(),
@@ -1409,6 +1419,7 @@ impl ChatWidget {
14091419
suppressed_exec_calls: HashSet::new(),
14101420
last_unified_wait: None,
14111421
task_complete_pending: false,
1422+
agent_turn_running: false,
14121423
mcp_startup_status: None,
14131424
interrupts: InterruptManager::new(),
14141425
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
@@ -27,6 +27,7 @@ use codex_core::protocol::ExecCommandSource;
2727
use codex_core::protocol::ExecPolicyAmendment;
2828
use codex_core::protocol::ExitedReviewModeEvent;
2929
use codex_core::protocol::FileChange;
30+
use codex_core::protocol::McpStartupCompleteEvent;
3031
use codex_core::protocol::McpStartupStatus;
3132
use codex_core::protocol::McpStartupUpdateEvent;
3233
use codex_core::protocol::Op;
@@ -386,6 +387,7 @@ async fn make_chatwidget_manual(
386387
suppressed_exec_calls: HashSet::new(),
387388
last_unified_wait: None,
388389
task_complete_pending: false,
390+
agent_turn_running: false,
389391
mcp_startup_status: None,
390392
interrupts: InterruptManager::new(),
391393
reasoning_buffer: String::new(),
@@ -2390,6 +2392,32 @@ async fn mcp_startup_header_booting_snapshot() {
23902392
assert_snapshot!("mcp_startup_header_booting", terminal.backend());
23912393
}
23922394

2395+
#[tokio::test]
2396+
async fn mcp_startup_complete_does_not_clear_running_task() {
2397+
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
2398+
2399+
chat.handle_codex_event(Event {
2400+
id: "task-1".into(),
2401+
msg: EventMsg::TaskStarted(TaskStartedEvent {
2402+
model_context_window: None,
2403+
}),
2404+
});
2405+
2406+
assert!(chat.bottom_pane.is_task_running());
2407+
assert!(chat.bottom_pane.status_indicator_visible());
2408+
2409+
chat.handle_codex_event(Event {
2410+
id: "mcp-1".into(),
2411+
msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
2412+
ready: vec!["schaltwerk".into()],
2413+
..Default::default()
2414+
}),
2415+
});
2416+
2417+
assert!(chat.bottom_pane.is_task_running());
2418+
assert!(chat.bottom_pane.status_indicator_visible());
2419+
}
2420+
23932421
#[tokio::test]
23942422
async fn background_event_updates_status_header() {
23952423
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;

0 commit comments

Comments
 (0)