From e98765c212d04e7bced1d2d340e0cee61b6d7aa0 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 17 Jul 2026 22:36:03 +0000 Subject: [PATCH] Allow selecting path-backed agents in the TUI picker (#33922) ## Why Opening the agent picker stopped after adding status history whenever path-backed subagents were present, so those agents could not be selected from the picker. ## What changed - Continue into the picker after rendering status for path-backed subagents. - Skip duplicate liveness refreshes for path-backed threads and display non-primary agents by their canonical path when available. ## Testing - Add a snapshot test that opens the picker for a path-backed agent and verifies that selecting it emits `SelectAgentThread` for the expected thread. GitOrigin-RevId: 1668bb7817ce4e13c91d85c4d7ddab31c04c97b3 --- codex-rs/tui/src/app/session_lifecycle.rs | 25 ++++++++----- codex-rs/tui/src/app/tests.rs | 35 +++++++++++++++++++ ..._app__tests__path_backed_agent_picker.snap | 10 ++++++ 3 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 codex-rs/tui/src/snapshots/codex_tui__app__tests__path_backed_agent_picker.snap diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index 40d7a2384b..6f3323f882 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -26,7 +26,7 @@ impl App { .into_iter() .map(|(thread_id, _)| thread_id) .collect(); - for thread_id in path_backed_thread_ids { + for thread_id in path_backed_thread_ids.iter().copied() { if let Some(channel) = self.thread_event_channels.get(&thread_id) && channel.attachment() == ThreadEventAttachment::Live { @@ -80,7 +80,6 @@ impl App { .add_to_history(super::agent_status_feed::AgentStatusHistoryCell::new( entries, )); - return; } let mut thread_ids = self.agent_navigation.tracked_thread_ids(); @@ -90,7 +89,9 @@ impl App { } } for thread_id in thread_ids { - if self.side_threads.contains_key(&thread_id) { + if path_backed_thread_ids.contains(&thread_id) + || self.side_threads.contains_key(&thread_id) + { continue; } if !self @@ -127,11 +128,19 @@ impl App { } let id = thread_id; let is_primary = self.primary_thread_id == Some(thread_id); - let name = format_agent_picker_item_name( - entry.agent_nickname.as_deref(), - entry.agent_role.as_deref(), - is_primary, - ); + let name = entry + .agent_path + .as_deref() + .map(str::trim) + .filter(|agent_path| !is_primary && !agent_path.is_empty()) + .map(ToOwned::to_owned) + .unwrap_or_else(|| { + format_agent_picker_item_name( + entry.agent_nickname.as_deref(), + entry.agent_role.as_deref(), + is_primary, + ) + }); let uuid = thread_id.to_string(); SelectionItem { name: name.clone(), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 6a26b6b537..58acb6bfe8 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -1544,6 +1544,41 @@ async fn open_agent_picker_clears_running_hint_from_completed_snapshot() -> Resu Ok(()) } +#[tokio::test] +async fn open_agent_picker_selects_path_backed_agent() -> Result<()> { + let (mut app, mut app_event_rx, _op_rx) = Box::pin(make_test_app_with_channels()).await; + let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker( + app.chat_widget.config_ref(), + )) + .await + .expect("embedded app server"); + let thread_id = + ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread id"); + app.thread_event_channels + .insert(thread_id, ThreadEventChannel::new(/*capacity*/ 1)); + app.agent_navigation + .record_sub_agent_activity(SubAgentActivityDisplay { + thread_id, + agent_path: "/root/worker".to_string(), + is_running_hint: true, + }); + + Box::pin(app.open_agent_picker(&mut app_server)).await; + + assert_app_snapshot!( + "path_backed_agent_picker", + render_bottom_popup(&app.chat_widget, /*width*/ 80) + ); + while app_event_rx.try_recv().is_ok() {} + app.chat_widget + .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + assert_matches!( + app_event_rx.try_recv(), + Ok(AppEvent::SelectAgentThread(selected_thread_id)) if selected_thread_id == thread_id + ); + Ok(()) +} + #[tokio::test] async fn open_agent_picker_refreshes_replay_only_path_backed_liveness() -> Result<()> { let mut app = Box::pin(make_test_app()).await; diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__tests__path_backed_agent_picker.snap b/codex-rs/tui/src/snapshots/codex_tui__app__tests__path_backed_agent_picker.snap new file mode 100644 index 0000000000..c6fd5fdde5 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__app__tests__path_backed_agent_picker.snap @@ -0,0 +1,10 @@ +--- +source: tui/src/app/tests.rs +expression: "render_bottom_popup(&app.chat_widget, 80)" +--- + Subagents + Select an agent to watch. ⌥ + ← previous, ⌥ + → next. + +› 1. • /root/worker 00000000-0000-0000-0000-000000000123 + + Press enter to confirm or esc to go back