From 0666c12e785930e361d09289acf46f1d20cabbb2 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 16 Sep 2026 16:35:51 +0000 Subject: [PATCH] Show a loading message when opening tasks from the agents overview (#45983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Task attachment awaits server requests while the event handler cannot process scheduled frames. Prompts and chat widget replacement can also clear the terminal during loading. ## What changed Draw a bold `Loading task…` message immediately when attaching a task, and redraw it after folder selection, trust prompts, and chat widget replacement. Schedule a frame to restore the normal view afterward. ## Testing Extend the cold-resume test to verify that a loading message appears before server requests complete, survives widget replacement, and is replaced on the next draw. GitOrigin-RevId: cb2e73efe38758025964aa53f52127a2b2609fca --- codex-rs/tui/src/app/agents_overview.rs | 8 +++++ .../tui/src/app/agents_overview_loading.rs | 19 +++++++++++ codex-rs/tui/src/app/agents_overview_tests.rs | 32 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 codex-rs/tui/src/app/agents_overview_loading.rs diff --git a/codex-rs/tui/src/app/agents_overview.rs b/codex-rs/tui/src/app/agents_overview.rs index eae0db9175..609fd36dfd 100644 --- a/codex-rs/tui/src/app/agents_overview.rs +++ b/codex-rs/tui/src/app/agents_overview.rs @@ -8,6 +8,9 @@ pub(crate) use new::PendingWorktree; #[path = "agents_overview_errors.rs"] mod errors; +#[path = "agents_overview_loading.rs"] +mod loading; + use super::agents_overview_view::AgentsOverviewGroup; use super::agents_overview_view::AgentsOverviewRow; use super::agents_overview_view::AgentsOverviewView; @@ -332,6 +335,7 @@ impl App { self.chat_widget.pre_draw_tick(); return Ok(AppRunControl::Continue); } + loading::draw(tui)?; if self.primary_thread_id != Some(root_thread_id) { let previous_displayed_thread_id = self.current_displayed_thread_id(); if let Some(id) = previous_displayed_thread_id @@ -456,6 +460,8 @@ impl App { } local_settings = crate::local_settings::LocalSettings::from(&resume_config); } + // Folder selection and trust prompts can replace or clear the loading frame. + loading::draw(tui)?; let baseline_approval = resume_config.permissions.approval_policy.value(); let baseline_permissions = RuntimePermissionProfileOverride::from_config(&resume_config); @@ -608,6 +614,8 @@ impl App { self.add_agents_overview_error(format!("Failed to attach to task: {error}")); return Ok(AppRunControl::Continue); } + // Replacing the widget clears the terminal before the remaining server requests. + loading::draw(tui)?; if read_only { self.ensure_thread_channel(root_thread_id) .mark_external_writer(); diff --git a/codex-rs/tui/src/app/agents_overview_loading.rs b/codex-rs/tui/src/app/agents_overview_loading.rs new file mode 100644 index 0000000000..a7798ef517 --- /dev/null +++ b/codex-rs/tui/src/app/agents_overview_loading.rs @@ -0,0 +1,19 @@ +//! Transient feedback while command-center selection waits for a session to load. +//! Draw directly because the app event handler cannot process scheduled frames while awaiting. + +use crate::tui::Tui; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; + +pub(super) fn draw(tui: &mut Tui) -> std::io::Result<()> { + tui.draw(u16::MAX, |frame| { + let lines = textwrap::wrap("Loading task…", usize::from(frame.area().width.max(1))) + .into_iter() + .map(|line| Line::from(line.into_owned().bold())) + .collect::>(); + frame.render_widget_ref(&Paragraph::new(lines), frame.area()); + })?; + tui.frame_requester().schedule_frame(); + Ok(()) +} diff --git a/codex-rs/tui/src/app/agents_overview_tests.rs b/codex-rs/tui/src/app/agents_overview_tests.rs index bc49d91c31..a045d8d613 100644 --- a/codex-rs/tui/src/app/agents_overview_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_tests.rs @@ -1830,8 +1830,40 @@ async fn overview_cold_resume_honors_working_directory_selection() -> Result<()> test_path_buf("/").abs() }; let mut tui = crate::tui::test_support::make_test_tui()?; + // On this current-thread runtime, the server cannot answer until we yield. Cancel the + // pending selection to release its terminal borrow and inspect what the user sees now. + let mut selection = + Box::pin(app.select_agents_overview_thread(&mut tui, &mut app_server, thread_id)); + assert!(futures::poll!(&mut selection).is_pending()); + drop(selection); + let pending_loading = + crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal).clone(); app.select_agents_overview_thread(&mut tui, &mut app_server, thread_id) .await?; + // The widget replacement clears the terminal; feedback must remain until the next draw. + let loading = + crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal).clone(); + assert_eq!(pending_loading, loading); + let loading_text = loading + .content + .chunks(usize::from(loading.area.width)) + .map(|row| { + row.iter() + .map(ratatui::buffer::Cell::symbol) + .collect::() + }) + .map(|line| line.trim_end().to_string()) + .collect::>() + .join("\n"); + insta::allow_duplicates! { + insta::assert_snapshot!(loading_text.trim_end(), @"Loading task…"); + } + app.handle_tui_event(&mut tui, &mut app_server, TuiEvent::Draw) + .await?; + assert_ne!( + crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal), + &loading, + ); let observed = app_server .resume_thread( &app.local_settings,