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
This commit is contained in:
Eric Traut
2026-07-17 23:11:07 +00:00
committed by copyberry
parent d4982d0a33
commit e98765c212
3 changed files with 62 additions and 8 deletions
+17 -8
View File
@@ -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(),
+35
View File
@@ -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;
@@ -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