feat: better agent picker in TUI (#12332)

<img width="486" height="112" alt="Screenshot 2026-02-20 at 15 04 52"
src="https://github.com/user-attachments/assets/0d744f58-d902-4638-aeaf-27e7389ccd73"
/>
This commit is contained in:
jif-oai
2026-02-20 15:40:34 +00:00
committed by GitHub
Unverified
parent 4d60c803ba
commit 5a30cd3f92
2 changed files with 94 additions and 9 deletions
+85 -9
View File
@@ -808,38 +808,57 @@ impl App {
async fn open_agent_picker(&mut self) {
let thread_ids: Vec<ThreadId> = self.thread_event_channels.keys().cloned().collect();
let mut agent_threads = Vec::new();
for thread_id in thread_ids {
if self.server.get_thread(thread_id).await.is_err() {
self.thread_event_channels.remove(&thread_id);
match self.server.get_thread(thread_id).await {
Ok(thread) => {
let session_source = thread.config_snapshot().await.session_source;
agent_threads.push((
thread_id,
session_source.get_nickname(),
session_source.get_agent_role(),
));
}
Err(_) => {
self.thread_event_channels.remove(&thread_id);
}
}
}
if self.thread_event_channels.is_empty() {
if agent_threads.is_empty() {
self.chat_widget
.add_info_message("No agents available yet.".to_string(), None);
return;
}
let mut thread_ids: Vec<ThreadId> = self.thread_event_channels.keys().cloned().collect();
thread_ids.sort_by_key(ToString::to_string);
agent_threads.sort_by(|(left, ..), (right, ..)| left.to_string().cmp(&right.to_string()));
let mut initial_selected_idx = None;
let items: Vec<SelectionItem> = thread_ids
let items: Vec<SelectionItem> = agent_threads
.iter()
.enumerate()
.map(|(idx, thread_id)| {
.map(|(idx, (thread_id, agent_nickname, agent_role))| {
if self.active_thread_id == Some(*thread_id) {
initial_selected_idx = Some(idx);
}
let id = *thread_id;
let is_primary = self.primary_thread_id == Some(*thread_id);
let name = format_agent_picker_item_name(
*thread_id,
agent_nickname.as_deref(),
agent_role.as_deref(),
is_primary,
);
let uuid = thread_id.to_string();
SelectionItem {
name: thread_id.to_string(),
name: name.clone(),
description: Some(uuid.clone()),
is_current: self.active_thread_id == Some(*thread_id),
actions: vec![Box::new(move |tx| {
tx.send(AppEvent::SelectAgentThread(id));
})],
dismiss_on_select: true,
search_value: Some(thread_id.to_string()),
search_value: Some(format!("{name} {uuid}")),
..Default::default()
}
})
@@ -2892,6 +2911,28 @@ impl App {
}
}
fn format_agent_picker_item_name(
_thread_id: ThreadId,
agent_nickname: Option<&str>,
agent_role: Option<&str>,
is_primary: bool,
) -> String {
if is_primary {
return "Main [default]".to_string();
}
let agent_nickname = agent_nickname
.map(str::trim)
.filter(|nickname| !nickname.is_empty());
let agent_role = agent_role.map(str::trim).filter(|role| !role.is_empty());
match (agent_nickname, agent_role) {
(Some(agent_nickname), Some(agent_role)) => format!("{agent_nickname} [{agent_role}]"),
(Some(agent_nickname), None) => agent_nickname.to_string(),
(None, Some(agent_role)) => format!("[{agent_role}]"),
(None, None) => "Agent".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -3072,6 +3113,41 @@ mod tests {
Ok(())
}
#[test]
fn agent_picker_item_name_snapshot() {
let thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread id");
let snapshot = [
format!(
"{} | {}",
format_agent_picker_item_name(thread_id, Some("Robie"), Some("explorer"), true),
thread_id
),
format!(
"{} | {}",
format_agent_picker_item_name(thread_id, Some("Robie"), Some("explorer"), false),
thread_id
),
format!(
"{} | {}",
format_agent_picker_item_name(thread_id, Some("Robie"), None, false),
thread_id
),
format!(
"{} | {}",
format_agent_picker_item_name(thread_id, None, Some("explorer"), false),
thread_id
),
format!(
"{} | {}",
format_agent_picker_item_name(thread_id, None, None, false),
thread_id
),
]
.join("\n");
assert_snapshot!("agent_picker_item_name", snapshot);
}
#[tokio::test]
async fn active_non_primary_shutdown_target_returns_none_for_non_shutdown_event() -> Result<()>
{
@@ -0,0 +1,9 @@
---
source: tui/src/app.rs
expression: snapshot
---
Main [default] | 00000000-0000-0000-0000-000000000123
Robie [explorer] | 00000000-0000-0000-0000-000000000123
Robie | 00000000-0000-0000-0000-000000000123
[explorer] | 00000000-0000-0000-0000-000000000123
Agent | 00000000-0000-0000-0000-000000000123