Keep the agents overview open when there is no chat to return to (#60271)

## What changed

Only allow the cancel action to dismiss the agents overview when a chat is displayed or a startup thread is pending. Hide the `esc back` footer hint when there is no chat to return to, while retaining back hints for help and metadata editing.

## Testing

Add coverage for returning to a chat that is still starting and for pressing Escape in the overview after cancelling folder trust. Update snapshots and test setup to reflect whether a chat is available.

GitOrigin-RevId: af259330eb2a743fde449bbbd47c7f0eb6fa937e
This commit is contained in:
Eric Traut
2026-11-08 17:26:39 +00:00
committed by copyberry
parent a73fa8a869
commit 9ba7a3df52
12 changed files with 45 additions and 33 deletions
+4 -16
View File
@@ -32,12 +32,6 @@ async fn agents_defers_launch_directory_trust_until_starting_a_task() -> Result<
daemon_startup("agents-untrusted").await
}
#[tokio::test]
#[cfg(unix)]
async fn agents_checks_trust_when_starting_after_dismissing_overview() -> Result<()> {
daemon_startup("agents-untrusted-new").await
}
#[tokio::test]
#[cfg(unix)]
async fn discovered_managed_daemon_can_restart_with_auto_start_disabled() -> Result<()> {
@@ -202,7 +196,7 @@ async fn daemon_startup(command: &str) -> Result<()> {
""
},
serde_json::to_string(&workspace_path)?,
if command.starts_with("agents-untrusted") {
if command == "agents-untrusted" {
"untrusted"
} else {
"trusted"
@@ -246,7 +240,6 @@ async fn daemon_startup(command: &str) -> Result<()> {
| "agents"
| "agents-no-daemon"
| "agents-untrusted"
| "agents-untrusted-new"
| "discovered"
| "bedrock"
| "bedrock-running"
@@ -316,18 +309,13 @@ async fn daemon_startup(command: &str) -> Result<()> {
"Notconnectedtothelocalbackgroundserver"
} else if command == "agents" {
args.push("agents".into());
steps.push_back(("Notasksyet", b"\x1b"));
steps.push_back(("Notasksyet", b"n"));
steps.push_back(("GPT-5.6-Terra", b"/daemon\r"));
steps.push_back(("Restartbackgroundserver", b"\x1b[F\r"));
"Restartbackgroundserver?"
} else if command.starts_with("agents-untrusted") {
} else if command == "agents-untrusted" {
args.push("agents".into());
if command == "agents-untrusted-new" {
steps.push_back(("Notasksyet", b"\x1b"));
steps.push_back(("GPT-5.6-Terra", b"/new\r"));
} else {
steps.push_back(("Notasksyet", b"n"));
}
steps.push_back(("Notasksyet", b"n"));
"entercontinue·escback"
} else if command == "discovered" {
args.extend(["--disable".into(), "daemon_auto_start".into()]);
@@ -41,7 +41,7 @@ expression: transition
│
│
│
? help esc back ↑/↓ move enter open n new
? help ↑/↓ move enter open n new
--- Start a task ---
+3 -1
View File
@@ -146,8 +146,10 @@ impl AgentsOverviewView {
}
let cancel = if state.help {
self.keymap.primary_hint(ListAction::Cancel)
} else {
} else if state.editing_metadata() || self.can_return_to_chat {
self.center_list_hint(ListAction::Cancel)
} else {
None
};
if let Some(hint) = cancel {
hints.push((hint.display_label(), "back".into()));
+13 -4
View File
@@ -25,7 +25,7 @@ fn screen(view: &AgentsOverviewView, width: u16, height: u16) -> String {
#[tokio::test]
async fn live_center_keeps_status_order_across_progress_refreshes() {
let mut app = make_test_app().await;
let mut app = make_test_app_with_chat().await;
app.agents_overview.view_state.lock().unwrap().grouping = AgentsOverviewGrouping::Status;
let ids = [1, 2, 3, 4, 5].map(ThreadId::from_u128);
let mut threads = [
@@ -562,9 +562,18 @@ async fn live_center_rename_retains_target_when_status_leaves_filter() -> Result
Ok(())
}
#[tokio::test]
async fn escape_returns_to_a_chat_that_is_still_starting() {
let mut app = make_test_app().await;
app.pending_startup_thread_start = true;
let mut view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
view.handle_key_event(KeyCode::Esc.into());
assert!(view.is_complete());
}
#[tokio::test]
async fn live_center_navigation_and_complete_hints() {
let app = make_test_app().await;
let app = make_test_app_with_chat().await;
let ready = ThreadId::from_u128(/*value*/ 42);
let needs_you = ThreadId::from_u128(/*value*/ 43);
let unloaded = ThreadId::from_u128(/*value*/ 44);
@@ -714,7 +723,7 @@ async fn live_center_fixed_shortcuts_yield_to_configured_actions() {
#[tokio::test]
async fn live_center_hints_follow_configured_bindings() {
let mut app = make_test_app().await;
let mut app = make_test_app_with_chat().await;
let config: TuiKeymap = toml::from_str(
r#"
[list]
@@ -858,7 +867,7 @@ async fn overview_clears_voice_badge_after_async_close() -> Result<()> {
#[tokio::test]
async fn live_center_show_more_navigation_search_and_loading() {
let mut app = make_test_app().await;
let mut app = make_test_app_with_chat().await;
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
app.app_event_tx = AppEventSender::new(tx);
app.agents_overview.view_state.lock().unwrap().has_more = true;
+2
View File
@@ -383,6 +383,8 @@ impl App {
.map(|(rank, id)| (id, rank))
.collect()
});
view.can_return_to_chat =
self.pending_startup_thread_start || self.current_displayed_thread_id().is_some();
view.pin_action_pending = self.agents_overview.pending_pin_change.is_some();
view
}
+13 -5
View File
@@ -8,10 +8,17 @@ mod command_center;
use super::super::agents_overview_view::AgentsOverviewGroup;
use super::super::agents_overview_view::AgentsOverviewGrouping;
use super::*;
use crate::app::owned_transcript::tests::attach_thread;
use crate::app::tests::make_test_app_with_channels;
use crate::chatwidget::tests::helpers::normalize_agent_center_snapshot;
use codex_config::types::AgentsOverviewGrouping as PersistedAgentsOverviewGrouping;
async fn make_test_app_with_chat() -> Box<App> {
let mut app = make_test_app().await;
attach_thread(&mut app, ThreadId::new());
app
}
#[tokio::test]
async fn overview_worktree_creation_busy_state() {
let mut app = make_test_app().await;
@@ -787,7 +794,7 @@ async fn check_hidden_thread_does_not_refresh_shared_overview(source: &str) {
#[tokio::test]
async fn agents_overview_details_show_available_attention_without_expanding_rows() -> Result<()> {
let mut app = make_test_app().await;
let mut app = make_test_app_with_chat().await;
let app_server = crate::start_embedded_app_server_for_picker(&app.config).await?;
let [root, failed_agent, child, unloaded] = std::array::from_fn(|_| ThreadId::new());
let waiting = ThreadStatus::Active {
@@ -911,7 +918,7 @@ fn reasoning_delta(thread_id: ThreadId, item_id: &str, delta: &str) -> ServerNot
#[tokio::test]
async fn agents_overview_details_render_markdown() {
let mut app = make_test_app().await;
let mut app = make_test_app_with_chat().await;
let thread_id = ThreadId::new();
let mut thread = overview_thread(
thread_id,
@@ -1053,7 +1060,7 @@ fn agents_overview_markdown_preview_preserves_layout_and_bounds() {
#[tokio::test]
async fn agents_overview_reasoning_uses_existing_events_and_expires_with_attachment() {
let mut app = make_test_app().await;
let mut app = make_test_app_with_chat().await;
let thread_id = ThreadId::new();
let mut thread = overview_thread(
thread_id,
@@ -1153,7 +1160,7 @@ async fn agents_overview_reasoning_uses_existing_events_and_expires_with_attachm
#[tokio::test]
async fn worktrees_overview_groups_by_project_or_directory() {
let mut app = make_test_app().await;
let mut app = make_test_app_with_chat().await;
let root = tempfile::tempdir().unwrap();
let primary = root.path().join("primary");
let linked = root.path().join("linked");
@@ -1304,7 +1311,7 @@ fn overview_directory_group_keys_normalize_aliases() {
#[tokio::test]
async fn overview_model_grouping_shows_details_and_preserves_selection() {
let mut app = make_test_app().await;
let mut app = make_test_app_with_chat().await;
let threads = [
("Older task", Some("model-a"), 1),
("Other model", Some("model-b"), 2),
@@ -1365,6 +1372,7 @@ async fn shared_overview_shows_only_root_sessions() {
let first_root = ThreadId::from_string("00000000-0000-0000-0000-000000000101").unwrap();
let unloaded_root = ThreadId::from_string("00000000-0000-0000-0000-000000000102").unwrap();
let [child, second_root, side_thread] = std::array::from_fn(|_| ThreadId::new());
attach_thread(&mut app, first_root);
app.primary_thread_id = Some(first_root);
let mut threads = vec![
@@ -12,7 +12,7 @@ use pretty_assertions::assert_eq;
#[tokio::test]
async fn selected_usage_is_cached_and_account_changes_discard_old_results() -> Result<()> {
let mut app = make_test_app().await;
let mut app = make_test_app_with_chat().await;
let app_server = crate::start_embedded_app_server_for_picker(&app.config).await?;
app.chat_widget.update_account_state(
/*status_account_display*/ None,
+5 -1
View File
@@ -204,6 +204,7 @@ pub(super) struct AgentsOverviewView {
directory_group_keys: Vec<PathBuf>,
pub(super) pinned_thread_ranks: Option<HashMap<ThreadId, usize>>,
pub(super) pin_action_pending: bool,
pub(super) can_return_to_chat: bool,
selected: usize,
state: Arc<Mutex<AgentsOverviewViewState>>,
app_event_tx: AppEventSender,
@@ -281,6 +282,7 @@ impl AgentsOverviewView {
directory_group_keys,
pinned_thread_ranks: None,
pin_action_pending: false,
can_return_to_chat: false,
selected,
state,
app_event_tx,
@@ -796,7 +798,9 @@ impl BottomPaneView for AgentsOverviewView {
}
ListAction::Accept => self.activate(),
ListAction::Cancel => {
if matches!(self.on_ctrl_c(), CancellationEvent::NotHandled) {
if self.can_return_to_chat
&& matches!(self.on_ctrl_c(), CancellationEvent::NotHandled)
{
self.state().completion = Some(ViewCompletion::Cancelled);
}
}
@@ -1165,7 +1165,6 @@ impl App {
}
return;
}
// Dismissing the initial agents overview can expose /new before any folder consent.
// Resolve trust before loading defaults and applying explicit permission choices.
if self.current_displayed_thread_id().is_none() {
let mut config = self.chat_widget.config_ref().clone();
@@ -1,6 +1,5 @@
---
source: tui/src/app/agents_overview_tests.rs
assertion_line: 633
expression: "age.replace_all(&rendered, \" [age]\")"
---
Agent command center Group: Project g
@@ -26,4 +25,4 @@ expression: "age.replace_all(&rendered, \" [age]\")"
? help esc back ↑/↓ move enter open n new
? help ↑/↓ move enter open n new
@@ -25,4 +25,4 @@ expression: rendered
? help esc back ↑/↓ move enter open n n…
? help ↑/↓ move enter open n new
@@ -168,6 +168,7 @@ async fn connected_trust_cancellation_and_acceptance_control_task_creation() ->
};
for (expected, input) in [
(prompt, b"\x1b".as_slice()),
("Launch-folder task", b"\x1b"),
("Launch-folder task", b"\x1b[Bn"),
(prompt, b"\x1b"),
("n new", b"n"),