Show a loading message when opening tasks from the agents overview (#45983)

## 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
This commit is contained in:
Eric Traut
2026-09-16 16:35:51 +00:00
committed by copyberry
parent 53401a2808
commit 0666c12e78
3 changed files with 59 additions and 0 deletions

View File

@@ -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();

View File

@@ -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::<Vec<_>>();
frame.render_widget_ref(&Paragraph::new(lines), frame.area());
})?;
tui.frame_requester().schedule_frame();
Ok(())
}

View File

@@ -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::<String>()
})
.map(|line| line.trim_end().to_string())
.collect::<Vec<_>>()
.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,