From 21ef6be571c8bd9bcea8ee992aca7fcc1c5b7cb2 Mon Sep 17 00:00:00 2001 From: easong-openai Date: Fri, 5 Sep 2025 16:33:47 -0700 Subject: [PATCH] apply patch spinner --- codex-rs/cloud-tasks/src/app.rs | 8 ++++ codex-rs/cloud-tasks/src/lib.rs | 67 +++++++++++++++++++++++++-------- codex-rs/cloud-tasks/src/ui.rs | 15 ++++++-- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/codex-rs/cloud-tasks/src/app.rs b/codex-rs/cloud-tasks/src/app.rs index 6fbc3c87df..12e79927ea 100644 --- a/codex-rs/cloud-tasks/src/app.rs +++ b/codex-rs/cloud-tasks/src/app.rs @@ -59,6 +59,8 @@ pub struct App { pub new_task: Option, // Apply preflight spinner state pub apply_preflight_inflight: bool, + // Apply action spinner state + pub apply_inflight: bool, // Background enrichment coordination pub list_generation: u64, pub in_flight: std::collections::HashSet, @@ -84,6 +86,7 @@ impl App { env_error: None, new_task: None, apply_preflight_inflight: false, + apply_inflight: false, list_generation: 0, in_flight: std::collections::HashSet::new(), } @@ -161,6 +164,11 @@ pub enum AppEvent { skipped: Vec, conflicts: Vec, }, + /// Background completion of apply action (actual patch application) + ApplyFinished { + id: TaskId, + result: std::result::Result, + }, } // Convenience aliases; currently unused. diff --git a/codex-rs/cloud-tasks/src/lib.rs b/codex-rs/cloud-tasks/src/lib.rs index 3f74b5e049..8fa474b238 100644 --- a/codex-rs/cloud-tasks/src/lib.rs +++ b/codex-rs/cloud-tasks/src/lib.rs @@ -373,7 +373,12 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option) -> a } } // Advance throbber only while loading. - if app.refresh_inflight || app.details_inflight || app.env_loading || app.apply_preflight_inflight { + if app.refresh_inflight + || app.details_inflight + || app.env_loading + || app.apply_preflight_inflight + || app.apply_inflight + { app.throbber.calc_next(); needs_redraw = true; let _ = frame_tx.send(Instant::now() + Duration::from_millis(100)); @@ -575,6 +580,37 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option) -> a app.details_inflight = false; needs_redraw = true; } + app::AppEvent::ApplyFinished { id, result } => { + // Only update if the modal still corresponds to this id. + if let Some(m) = &app.apply_modal { + if m.task_id != id { continue; } + } else { + continue; + } + app.apply_inflight = false; + match result { + Ok(outcome) => { + app.status = outcome.message.clone(); + if matches!(outcome.status, codex_cloud_tasks_client::ApplyStatus::Success) { + app.apply_modal = None; + app.diff_overlay = None; + // Refresh tasks after successful apply + let backend2 = backend.clone(); + let tx2 = tx.clone(); + let env_sel = app.env_filter.clone(); + tokio::spawn(async move { + let res = app::load_tasks(&*backend2, env_sel.as_deref()).await; + let _ = tx2.send(app::AppEvent::TasksLoaded { env: env_sel, result: res }); + }); + } + } + Err(e) => { + append_error_log(format!("apply_task failed for {}: {e}", id.0)); + app.status = format!("Apply failed: {e}"); + } + } + needs_redraw = true; + } } } // Render immediately after processing app events. @@ -724,21 +760,22 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option) -> a // Simple apply confirmation modal: y apply, p preflight, n/Esc cancel match key.code { KeyCode::Char('y') => { - if let Some(m) = app.apply_modal.take() { + if let Some(m) = app.apply_modal.as_ref() { + // Keep modal open and animate a spinner while applying + app.apply_inflight = true; app.status = format!("Applying '{}'...", m.title); - match codex_cloud_tasks_client::CloudBackend::apply_task(&*backend, m.task_id.clone()).await { - Ok(outcome) => { - app.status = outcome.message.clone(); - if matches!(outcome.status, codex_cloud_tasks_client::ApplyStatus::Success) { - app.diff_overlay = None; - if let Ok(tasks) = app::load_tasks(&*backend, app.env_filter.as_deref()).await { app.tasks = tasks; } - } - } - Err(e) => { - append_error_log(format!("apply_task failed for {}: {e}", m.task_id.0)); - app.status = format!("Apply failed: {e}"); - } - } + needs_redraw = true; + let backend2 = backend.clone(); + let tx2 = tx.clone(); + let id2 = m.task_id.clone(); + tokio::spawn(async move { + let res = codex_cloud_tasks_client::CloudBackend::apply_task(&*backend2, id2.clone()).await; + let evt = match res { + Ok(outcome) => app::AppEvent::ApplyFinished { id: id2, result: Ok(outcome) }, + Err(e) => app::AppEvent::ApplyFinished { id: id2, result: Err(format!("{e}")) }, + }; + let _ = tx2.send(evt); + }); } } KeyCode::Char('p') => { diff --git a/codex-rs/cloud-tasks/src/ui.rs b/codex-rs/cloud-tasks/src/ui.rs index 693583528a..959b96b61a 100644 --- a/codex-rs/cloud-tasks/src/ui.rs +++ b/codex-rs/cloud-tasks/src/ui.rs @@ -257,7 +257,12 @@ fn draw_footer(frame: &mut Frame, area: Rect, app: &mut App) { // Draw help text; avoid clearing the whole footer area every frame. frame.render_widget(para, top[0]); // Right side: spinner or clear the spinner area if idle to prevent stale glyphs. - if app.refresh_inflight || app.details_inflight || app.env_loading { + if app.refresh_inflight + || app.details_inflight + || app.env_loading + || app.apply_preflight_inflight + || app.apply_inflight + { draw_inline_spinner(frame, top[1], &mut app.throbber, "Loading…"); } else { frame.render_widget(Clear, top[1]); @@ -419,9 +424,13 @@ pub fn draw_apply_modal(frame: &mut Frame, area: Rect, app: &mut App) { .split(content); frame.render_widget(header, rows[0]); - // Body: spinner while preflight runs; otherwise show result message and path lists - if app.apply_preflight_inflight || m.result_message.is_none() { + // Body: spinner while preflight/apply runs; otherwise show result message and path lists + if app.apply_preflight_inflight { draw_centered_spinner(frame, rows[1], &mut app.throbber, "Checking…"); + } else if app.apply_inflight { + draw_centered_spinner(frame, rows[1], &mut app.throbber, "Applying…"); + } else if m.result_message.is_none() { + draw_centered_spinner(frame, rows[1], &mut app.throbber, "Loading…"); } else if let Some(msg) = &m.result_message { let mut body_lines: Vec = Vec::new(); let first = match m.result_level {