From 5a30cd3f92f28ba9a32b38c6d277b377493a253b Mon Sep 17 00:00:00 2001 From: jif-oai Date: Fri, 20 Feb 2026 15:40:34 +0000 Subject: [PATCH] feat: better agent picker in TUI (#12332) Screenshot 2026-02-20 at 15 04 52 --- codex-rs/tui/src/app.rs | 94 +++++++++++++++++-- ...i__app__tests__agent_picker_item_name.snap | 9 ++ 2 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 codex-rs/tui/src/snapshots/codex_tui__app__tests__agent_picker_item_name.snap diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index a46a927fb..589805964 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -808,38 +808,57 @@ impl App { async fn open_agent_picker(&mut self) { let thread_ids: Vec = 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 = 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 = thread_ids + let items: Vec = 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<()> { diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__tests__agent_picker_item_name.snap b/codex-rs/tui/src/snapshots/codex_tui__app__tests__agent_picker_item_name.snap new file mode 100644 index 000000000..0337f424a --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__app__tests__agent_picker_item_name.snap @@ -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