From 1111483b4836b7c220ac2f08bbaa5b50e3d0052e Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Thu, 12 Nov 2026 23:53:07 +0000 Subject: [PATCH] Hide attached threads from the agents overview (#61218) ## Why Saved side chats with `attached_to_thread_id` set could appear as root sessions in the agents overview. ## What changed Exclude attached threads alongside ephemeral threads when building the overview. Reuse the same root eligibility check for history discovery, pinned threads, and refill accounting so attached threads do not count as overview roots. ## Testing Extend `shared_overview_shows_only_root_sessions` with a saved side chat attached to a root thread, verifying that it stays out of the overview snapshot. GitOrigin-RevId: 3417d444cfde7fc034c07ec124ba72cbf64b0ef6 --- codex-rs/tui/src/app/agents_overview.rs | 10 ++++------ codex-rs/tui/src/app/agents_overview_discovery.rs | 12 ++++++++---- codex-rs/tui/src/app/agents_overview_tests.rs | 8 ++++++++ codex-rs/tui/src/app/agents_overview_threads.rs | 10 ++-------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/codex-rs/tui/src/app/agents_overview.rs b/codex-rs/tui/src/app/agents_overview.rs index 14faa045b0..8ca06f0463 100644 --- a/codex-rs/tui/src/app/agents_overview.rs +++ b/codex-rs/tui/src/app/agents_overview.rs @@ -15,6 +15,8 @@ mod loading; #[path = "agents_overview_selection.rs"] mod selection; +use super::agents_overview_discovery::is_overview_root; +use super::agents_overview_discovery::is_visible_in_agents_overview; use super::agents_overview_view::AgentsOverviewGroup; use super::agents_overview_view::AgentsOverviewRow; use super::agents_overview_view::AgentsOverviewView; @@ -257,11 +259,7 @@ impl App { if displayed_refresh && !self.agents_overview.threads.contains_key(&thread_id) && !self.agents_overview.hidden_threads.contains(&thread_id) - && thread.parent_thread_id.is_none() - && !matches!( - thread.source, - SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) - ) + && is_overview_root(&thread) { self.agents_overview.refill_count = self.agents_overview.refill_count.saturating_sub(1); @@ -423,7 +421,7 @@ impl App { threads.push(target.clone()); } } - threads.retain(|thread| !thread.ephemeral); + threads.retain(is_visible_in_agents_overview); for thread in &mut threads { if thread.parent_thread_id.is_none() && let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { diff --git a/codex-rs/tui/src/app/agents_overview_discovery.rs b/codex-rs/tui/src/app/agents_overview_discovery.rs index 63e53ec2db..75e35dbcf3 100644 --- a/codex-rs/tui/src/app/agents_overview_discovery.rs +++ b/codex-rs/tui/src/app/agents_overview_discovery.rs @@ -74,7 +74,7 @@ pub(crate) async fn list_pinned_threads( Ok(page) => { let next_cursor = page.next_cursor.clone(); threads.extend(listed_threads(page).filter(|(thread, _)| { - is_overview_thread(thread) && supports_shared_pinning(&thread.source) + is_overview_root(thread) && supports_shared_pinning(&thread.source) })); let Some(next_cursor) = next_cursor else { break; @@ -93,8 +93,12 @@ pub(crate) async fn list_pinned_threads( Ok(Some(threads)) } -fn is_overview_thread(thread: &Thread) -> bool { - !thread.ephemeral +pub(super) fn is_visible_in_agents_overview(thread: &Thread) -> bool { + !thread.ephemeral && thread.attached_to_thread_id.is_none() +} + +pub(super) fn is_overview_root(thread: &Thread) -> bool { + is_visible_in_agents_overview(thread) && thread.parent_thread_id.is_none() && !matches!( thread.source, @@ -236,7 +240,7 @@ impl SourcePage { self.exhausted = page.next_cursor.is_none(); self.cursor = page.next_cursor.clone(); self.rows.extend( - listed_threads(page).filter(|(thread, _)| is_overview_thread(thread)), + listed_threads(page).filter(|(thread, _)| is_overview_root(thread)), ); return true; } diff --git a/codex-rs/tui/src/app/agents_overview_tests.rs b/codex-rs/tui/src/app/agents_overview_tests.rs index 0c521e5fb6..1ea48a6a60 100644 --- a/codex-rs/tui/src/app/agents_overview_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_tests.rs @@ -1631,6 +1631,14 @@ async fn shared_overview_shows_only_root_sessions() { ); side.ephemeral = true; threads.push(side); + let mut attached = overview_thread( + ThreadId::new(), + /*parent_thread_id*/ None, + "Saved side chat", + ThreadStatus::Idle, + ); + attached.attached_to_thread_id = Some(first_root.to_string()); + threads.push(attached); let default_model = app .model_catalog .models diff --git a/codex-rs/tui/src/app/agents_overview_threads.rs b/codex-rs/tui/src/app/agents_overview_threads.rs index e4be36118d..0156409db1 100644 --- a/codex-rs/tui/src/app/agents_overview_threads.rs +++ b/codex-rs/tui/src/app/agents_overview_threads.rs @@ -5,6 +5,7 @@ use super::App; use super::agents_overview::AGENTS_OVERVIEW_VIEW_ID; use super::agents_overview_details::preview_agent_message; +use super::agents_overview_discovery::is_overview_root; use super::agents_overview_discovery::list_pinned_threads; use super::app_server_event_targets::ServerNotificationThreadTarget; use super::app_server_event_targets::server_notification_thread_target; @@ -15,7 +16,6 @@ use crate::chatwidget::ChatWidget; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerNotification; -use codex_app_server_protocol::SessionSource; use codex_app_server_protocol::Thread; use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadLoadedListParams; @@ -26,7 +26,6 @@ use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::ThreadTurnsListParams; use codex_app_server_protocol::ThreadTurnsListResponse; use codex_protocol::ThreadId; -use codex_protocol::protocol::SubAgentSource; use futures::StreamExt; use std::collections::HashMap; use uuid::Uuid; @@ -61,12 +60,7 @@ impl App { self.agents_overview.removed_threads.insert(thread_id); self.agents_overview.unread.remove(&thread_id); if let Some(Some(thread)) = self.agents_overview.threads.remove(&thread_id) - && !thread.ephemeral - && thread.parent_thread_id.is_none() - && !matches!( - thread.source, - SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) - ) + && is_overview_root(&thread) && !self.agents_overview.hidden_threads.contains(&thread_id) { self.agents_overview.refill_count += 1;