From 923f276f579c64251825ba767ff5bc36a28e36a0 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 13 Oct 2026 23:49:31 +0000 Subject: [PATCH] Keep the Agent command center visible during archive and delete (#53804) ## Why Archive and delete progress replaced the task list, hiding the selected task and surrounding context while the request was pending. ## What changed Render progress as a centered dialog over the Agent command center. Preserve the selected task and shared scroll/filter state, and re-render the backdrop on terminal resize. ## Testing Update narrow-terminal snapshots for both archive and delete to cover the progress overlay, task list, and selected row. Adapt the existing modal input-isolation test to supply a backdrop. GitOrigin-RevId: e155478aeaddef189fbc53b27b8ff05d54bc3711 --- codex-rs/tui/src/app/agents_overview.rs | 2 +- .../agents_overview_action_progress_tests.rs | 34 ++++++++++++++++--- .../tui/src/app/agents_overview_actions.rs | 28 ++++++++++++++- codex-rs/tui/src/app/agents_overview_tests.rs | 2 +- ...ons__progress_tests__archive_progress.snap | 31 ++++++++++++++--- ...ions__progress_tests__delete_progress.snap | 31 ++++++++++++++--- codex-rs/tui/src/bottom_pane/view_stack.rs | 2 +- 7 files changed, 111 insertions(+), 19 deletions(-) diff --git a/codex-rs/tui/src/app/agents_overview.rs b/codex-rs/tui/src/app/agents_overview.rs index ed8b8f0081..00f0633861 100644 --- a/codex-rs/tui/src/app/agents_overview.rs +++ b/codex-rs/tui/src/app/agents_overview.rs @@ -1221,7 +1221,7 @@ impl App { #[cfg(test)] #[path = "agents_overview_tests.rs"] -mod tests; +pub(super) mod tests; fn agents_overview_group( thread: &Thread, diff --git a/codex-rs/tui/src/app/agents_overview_action_progress_tests.rs b/codex-rs/tui/src/app/agents_overview_action_progress_tests.rs index 49ac829858..546b77fdab 100644 --- a/codex-rs/tui/src/app/agents_overview_action_progress_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_action_progress_tests.rs @@ -1,6 +1,9 @@ //! Progress rendering and input isolation while lifecycle requests are pending. use super::*; +use crate::app::agents_overview::tests::overview_thread; +use crate::chatwidget::tests::helpers::normalize_agent_center_snapshot; +use codex_app_server_protocol::ThreadStatus; use futures::stream; use pretty_assertions::assert_eq; use ratatui::Terminal; @@ -37,6 +40,7 @@ async fn modal_consumes_input_and_finishes_after_input_closes() -> color_eyre::R run_lifecycle_modal( &mut tui, AgentsOverviewAction::Archive, + &Paragraph::new("Agent command center"), async { wait_for_input.await.expect("all modal input was consumed"); tokio::task::yield_now().await; @@ -50,18 +54,38 @@ async fn modal_consumes_input_and_finishes_after_input_closes() -> color_eyre::R Ok(()) } -#[test] -fn lifecycle_progress_wraps_at_narrow_widths() { +#[tokio::test] +async fn lifecycle_progress_wraps_at_narrow_widths() { + let (app, _rx, _op_rx) = crate::app::tests::make_test_app_with_channels().await; + let threads = ["First task", "Selected task", "Last task"] + .into_iter() + .enumerate() + .map(|(index, name)| { + overview_thread( + ThreadId::from_u128(index as u128 + 1), + /*parent_thread_id*/ None, + name, + ThreadStatus::Idle, + ) + }) + .collect(); + let backdrop = app.agents_overview_view(threads, Some(ThreadId::from_u128(/*value*/ 2))); for (action, name) in [ (AgentsOverviewAction::Archive, "archive_progress"), (AgentsOverviewAction::Delete, "delete_progress"), ] { let progress = LifecycleProgress(action); - let mut terminal = - Terminal::new(TestBackend::new(36, progress.desired_height(/*width*/ 36))).unwrap(); + let progress = DialogOverlay { + backdrop: RenderableItem::Borrowed(&backdrop), + dialog: CenteredView(&progress), + }; + let mut terminal = Terminal::new(TestBackend::new(36, 24)).unwrap(); terminal .draw(|frame| progress.render(frame.area(), frame.buffer_mut())) .unwrap(); - insta::assert_snapshot!(name, terminal.backend()); + insta::assert_snapshot!( + name, + normalize_agent_center_snapshot(terminal.backend().to_string()) + ); } } diff --git a/codex-rs/tui/src/app/agents_overview_actions.rs b/codex-rs/tui/src/app/agents_overview_actions.rs index 9a03223034..9afbe1e269 100644 --- a/codex-rs/tui/src/app/agents_overview_actions.rs +++ b/codex-rs/tui/src/app/agents_overview_actions.rs @@ -6,10 +6,14 @@ use super::App; use crate::app_event::AgentsOverviewAction; use crate::app_event::AppEvent; use crate::app_server_session::AppServerSession; +use crate::bottom_pane::BottomPaneView; +use crate::bottom_pane::CenteredView; +use crate::bottom_pane::DialogOverlay; use crate::bottom_pane::SelectionItem; use crate::bottom_pane::SelectionViewParams; use crate::chatwidget::ChatWidget; use crate::render::renderable::Renderable; +use crate::render::renderable::RenderableItem; use crate::tui; use crate::wrapping::word_wrap_lines; use codex_app_server_protocol::ClientRequest; @@ -75,14 +79,21 @@ impl Renderable for LifecycleProgress { } } +impl BottomPaneView for LifecycleProgress {} + /// Keep the terminal responsive without dispatching navigation or queued task events. async fn run_lifecycle_modal( tui: &mut tui::Tui, action: AgentsOverviewAction, + backdrop: &dyn Renderable, operation: impl Future, events: impl Stream + Unpin, ) -> color_eyre::Result { let progress = LifecycleProgress(action); + let progress = DialogOverlay { + backdrop: RenderableItem::Borrowed(backdrop), + dialog: CenteredView(&progress), + }; tui.draw(u16::MAX, |frame| { progress.render(frame.area(), frame.buffer_mut()); })?; @@ -265,6 +276,20 @@ impl App { let mut removes_primary = self.primary_thread_id == Some(thread_id); let mut attempted = false; let primary_thread_id = self.primary_thread_id; + // Retain the displayed selection and shared scroll/filter state while the RPC + // borrows the live widget to stop voice. Re-render on resize rather than freezing pixels. + let selected_thread_id = self + .chat_widget + .selected_index_for_present_view(super::agents_overview::AGENTS_OVERVIEW_VIEW_ID) + .and_then(|index| self.agents_overview.visible_thread_ids.get(index).copied()); + let threads = self + .agents_overview + .threads + .values() + .flatten() + .cloned() + .collect(); + let backdrop = self.agents_overview_view(threads, selected_thread_id); let events = tui.event_stream(); let operation = async { let result = async { @@ -310,7 +335,8 @@ impl App { } (result, detach_primary) }; - let (result, detach_primary) = run_lifecycle_modal(tui, action, operation, events).await?; + let (result, detach_primary) = + run_lifecycle_modal(tui, action, &backdrop, operation, events).await?; if attempted { // A read started before the RPC can contain stale task and approval state. if let Some(refresh) = self.agents_overview.refresh_task.take() { diff --git a/codex-rs/tui/src/app/agents_overview_tests.rs b/codex-rs/tui/src/app/agents_overview_tests.rs index 78c018f14d..b28efb8d17 100644 --- a/codex-rs/tui/src/app/agents_overview_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_tests.rs @@ -242,7 +242,7 @@ async fn overview_right_preserves_editors_and_offline_state() { assert!(rx.try_recv().is_err()); } -fn overview_thread( +pub(in crate::app) fn overview_thread( thread_id: ThreadId, parent_thread_id: Option, name: &str, diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview_actions__progress_tests__archive_progress.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview_actions__progress_tests__archive_progress.snap index d6bd0e16e7..ba3b35a524 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview_actions__progress_tests__archive_progress.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview_actions__progress_tests__archive_progress.snap @@ -1,8 +1,29 @@ --- source: tui/src/app/agents_overview_action_progress_tests.rs -assertion_line: 62 -expression: terminal.backend() +assertion_line: 86 +expression: normalize_agent_center_snapshot(terminal.backend().to_string()) --- -"Archiving task… " -"Please wait. Task switching is " -"unavailable until this finishes. " +" Agent command center Group: Pr… " +" All 3 Needs you 0 › " +" ──────────────────────────────── " +" Tasks " +" /tmp/project 3 " +" ○ First task " +" › ○ Selected task " +" ○ Last task " +" " +" " +" Archiving task… " +" Please wait. Task switching is " +" unavailable until this finishes. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" ? help esc back ↑/↓ move ent… " diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview_actions__progress_tests__delete_progress.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview_actions__progress_tests__delete_progress.snap index 2a2f6f66c3..3ed43f3b66 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview_actions__progress_tests__delete_progress.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview_actions__progress_tests__delete_progress.snap @@ -1,8 +1,29 @@ --- source: tui/src/app/agents_overview_action_progress_tests.rs -assertion_line: 62 -expression: terminal.backend() +assertion_line: 86 +expression: normalize_agent_center_snapshot(terminal.backend().to_string()) --- -"Deleting task… " -"Please wait. Task switching is " -"unavailable until this finishes. " +" Agent command center Group: Pr… " +" All 3 Needs you 0 › " +" ──────────────────────────────── " +" Tasks " +" /tmp/project 3 " +" ○ First task " +" › ○ Selected task " +" ○ Last task " +" " +" " +" Deleting task… " +" Please wait. Task switching is " +" unavailable until this finishes. " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" ? help esc back ↑/↓ move ent… " diff --git a/codex-rs/tui/src/bottom_pane/view_stack.rs b/codex-rs/tui/src/bottom_pane/view_stack.rs index fb1d91a126..e300dfc83f 100644 --- a/codex-rs/tui/src/bottom_pane/view_stack.rs +++ b/codex-rs/tui/src/bottom_pane/view_stack.rs @@ -70,7 +70,7 @@ impl Renderable for ViewStack<'_> { } /// A dialog placed within the supplied viewport, without taking over its backdrop. -pub(crate) struct CenteredView<'a>(pub(super) &'a dyn BottomPaneView); +pub(crate) struct CenteredView<'a>(pub(crate) &'a dyn BottomPaneView); impl Renderable for CenteredView<'_> { fn render(&self, area: Rect, buf: &mut Buffer) {