mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T>(
|
||||
tui: &mut tui::Tui,
|
||||
action: AgentsOverviewAction,
|
||||
backdrop: &dyn Renderable,
|
||||
operation: impl Future<Output = T>,
|
||||
events: impl Stream<Item = tui::TuiEvent> + Unpin,
|
||||
) -> color_eyre::Result<T> {
|
||||
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() {
|
||||
|
||||
@@ -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<ThreadId>,
|
||||
name: &str,
|
||||
|
||||
+26
-5
@@ -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… "
|
||||
|
||||
+26
-5
@@ -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… "
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user