sidebar: Remove `AgentSessionInfo` usage (#52732)

Bennet Bo Fenner , Ben Brandt , and Danilo Leal created

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

crates/agent_ui/src/thread_metadata_store.rs |  13 
crates/sidebar/src/sidebar.rs                | 353 +++++++--------------
crates/sidebar/src/sidebar_tests.rs          | 200 +++++------
crates/sidebar/src/thread_switcher.rs        |  21 
4 files changed, 227 insertions(+), 360 deletions(-)

Detailed changes

crates/agent_ui/src/thread_metadata_store.rs 🔗

@@ -105,6 +105,19 @@ pub struct ThreadMetadata {
     pub archived: bool,
 }
 
+impl From<&ThreadMetadata> for acp_thread::AgentSessionInfo {
+    fn from(meta: &ThreadMetadata) -> Self {
+        Self {
+            session_id: meta.session_id.clone(),
+            work_dirs: Some(meta.folder_paths.clone()),
+            title: Some(meta.title.clone()),
+            updated_at: Some(meta.updated_at),
+            created_at: meta.created_at,
+            meta: None,
+        }
+    }
+}
+
 impl ThreadMetadata {
     pub fn from_thread(
         is_archived: bool,

crates/sidebar/src/sidebar.rs 🔗

@@ -4,7 +4,7 @@ use acp_thread::ThreadStatus;
 use action_log::DiffStats;
 use agent_client_protocol::{self as acp};
 use agent_settings::AgentSettings;
-use agent_ui::thread_metadata_store::ThreadMetadataStore;
+use agent_ui::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore};
 use agent_ui::threads_archive_view::{
     ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp,
 };
@@ -92,19 +92,6 @@ struct ActiveThreadInfo {
     diff_stats: DiffStats,
 }
 
-impl From<&ActiveThreadInfo> for acp_thread::AgentSessionInfo {
-    fn from(info: &ActiveThreadInfo) -> Self {
-        Self {
-            session_id: info.session_id.clone(),
-            work_dirs: None,
-            title: Some(info.title.clone()),
-            updated_at: Some(Utc::now()),
-            created_at: Some(Utc::now()),
-            meta: None,
-        }
-    }
-}
-
 #[derive(Clone)]
 enum ThreadEntryWorkspace {
     Open(Entity<Workspace>),
@@ -120,8 +107,7 @@ struct WorktreeInfo {
 
 #[derive(Clone)]
 struct ThreadEntry {
-    agent: Agent,
-    session_info: acp_thread::AgentSessionInfo,
+    metadata: ThreadMetadata,
     icon: IconName,
     icon_from_external_svg: Option<SharedString>,
     status: AgentThreadStatus,
@@ -141,7 +127,7 @@ impl ThreadEntry {
     /// but if we have a correspond thread already loaded we want to apply the
     /// live information.
     fn apply_active_info(&mut self, info: &ActiveThreadInfo) {
-        self.session_info.title = Some(info.title.clone());
+        self.metadata.title = info.title.clone();
         self.status = info.status;
         self.icon = info.icon;
         self.icon_from_external_svg = info.icon_from_external_svg.clone();
@@ -191,7 +177,7 @@ impl ListEntry {
 
     fn session_id(&self) -> Option<&acp::SessionId> {
         match self {
-            ListEntry::Thread(thread_entry) => Some(&thread_entry.session_info.session_id),
+            ListEntry::Thread(thread_entry) => Some(&thread_entry.metadata.session_id),
             _ => None,
         }
     }
@@ -690,7 +676,7 @@ impl Sidebar {
             .iter()
             .filter_map(|entry| match entry {
                 ListEntry::Thread(thread) if thread.is_live => {
-                    Some((thread.session_info.session_id.clone(), thread.status))
+                    Some((thread.metadata.session_id.clone(), thread.status))
                 }
                 _ => None,
             })
@@ -710,7 +696,7 @@ impl Sidebar {
             .iter()
             .any(|ws| !workspace_path_list(ws, cx).paths().is_empty());
 
-        let resolve_agent = |agent_id: &AgentId| -> (Agent, IconName, Option<SharedString>) {
+        let resolve_agent_icon = |agent_id: &AgentId| -> (IconName, Option<SharedString>) {
             let agent = Agent::from(agent_id.clone());
             let icon = match agent {
                 Agent::NativeAgent => IconName::ZedAgent,
@@ -719,7 +705,7 @@ impl Sidebar {
             let icon_from_external_svg = agent_server_store
                 .as_ref()
                 .and_then(|store| store.read(cx).agent_icon(&agent_id));
-            (agent, icon, icon_from_external_svg)
+            (icon, icon_from_external_svg)
         };
 
         for (group_name, group) in project_groups.groups() {
@@ -768,19 +754,11 @@ impl Sidebar {
                         if !seen_session_ids.insert(row.session_id.clone()) {
                             continue;
                         }
-                        let (agent, icon, icon_from_external_svg) = resolve_agent(&row.agent_id);
+                        let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id);
                         let worktrees =
                             worktree_info_from_thread_paths(&row.folder_paths, &project_groups);
                         threads.push(ThreadEntry {
-                            agent,
-                            session_info: acp_thread::AgentSessionInfo {
-                                session_id: row.session_id.clone(),
-                                work_dirs: None,
-                                title: Some(row.title.clone()),
-                                updated_at: Some(row.updated_at),
-                                created_at: row.created_at,
-                                meta: None,
-                            },
+                            metadata: row,
                             icon,
                             icon_from_external_svg,
                             status: AgentThreadStatus::default(),
@@ -818,19 +796,11 @@ impl Sidebar {
                         if !seen_session_ids.insert(row.session_id.clone()) {
                             continue;
                         }
-                        let (agent, icon, icon_from_external_svg) = resolve_agent(&row.agent_id);
+                        let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id);
                         let worktrees =
                             worktree_info_from_thread_paths(&row.folder_paths, &project_groups);
                         threads.push(ThreadEntry {
-                            agent,
-                            session_info: acp_thread::AgentSessionInfo {
-                                session_id: row.session_id.clone(),
-                                work_dirs: None,
-                                title: Some(row.title.clone()),
-                                updated_at: Some(row.updated_at),
-                                created_at: row.created_at,
-                                meta: None,
-                            },
+                            metadata: row,
                             icon,
                             icon_from_external_svg,
                             status: AgentThreadStatus::default(),
@@ -862,11 +832,11 @@ impl Sidebar {
                 // Merge live info into threads and update notification state
                 // in a single pass.
                 for thread in &mut threads {
-                    if let Some(info) = live_info_by_session.get(&thread.session_info.session_id) {
+                    if let Some(info) = live_info_by_session.get(&thread.metadata.session_id) {
                         thread.apply_active_info(info);
                     }
 
-                    let session_id = &thread.session_info.session_id;
+                    let session_id = &thread.metadata.session_id;
 
                     let is_thread_workspace_active = match &thread.workspace {
                         ThreadEntryWorkspace::Open(thread_workspace) => active_workspace
@@ -890,16 +860,16 @@ impl Sidebar {
                 threads.sort_by(|a, b| {
                     let a_time = self
                         .thread_last_message_sent_or_queued
-                        .get(&a.session_info.session_id)
+                        .get(&a.metadata.session_id)
                         .copied()
-                        .or(a.session_info.created_at)
-                        .or(a.session_info.updated_at);
+                        .or(a.metadata.created_at)
+                        .or(Some(a.metadata.updated_at));
                     let b_time = self
                         .thread_last_message_sent_or_queued
-                        .get(&b.session_info.session_id)
+                        .get(&b.metadata.session_id)
                         .copied()
-                        .or(b.session_info.created_at)
-                        .or(b.session_info.updated_at);
+                        .or(b.metadata.created_at)
+                        .or(Some(b.metadata.updated_at));
                     b_time.cmp(&a_time)
                 });
             } else {
@@ -920,12 +890,7 @@ impl Sidebar {
 
                 let mut matched_threads: Vec<ThreadEntry> = Vec::new();
                 for mut thread in threads {
-                    let title = thread
-                        .session_info
-                        .title
-                        .as_ref()
-                        .map(|s| s.as_ref())
-                        .unwrap_or("");
+                    let title: &str = &thread.metadata.title;
                     if let Some(positions) = fuzzy_match_positions(&query, title) {
                         thread.highlight_positions = positions;
                     }
@@ -960,7 +925,7 @@ impl Sidebar {
                 });
 
                 for thread in matched_threads {
-                    current_session_ids.insert(thread.session_info.session_id.clone());
+                    current_session_ids.insert(thread.metadata.session_id.clone());
                     entries.push(thread.into());
                 }
             } else {
@@ -1011,7 +976,7 @@ impl Sidebar {
                 for (index, thread) in threads.into_iter().enumerate() {
                     let is_hidden = index >= count;
 
-                    let session_id = &thread.session_info.session_id;
+                    let session_id = &thread.metadata.session_id;
                     if is_hidden {
                         let is_promoted = thread.status == AgentThreadStatus::Running
                             || thread.status == AgentThreadStatus::WaitingForConfirmation
@@ -1861,22 +1826,15 @@ impl Sidebar {
                 self.toggle_collapse(&path_list, window, cx);
             }
             ListEntry::Thread(thread) => {
-                let session_info = thread.session_info.clone();
+                let metadata = thread.metadata.clone();
                 match &thread.workspace {
                     ThreadEntryWorkspace::Open(workspace) => {
                         let workspace = workspace.clone();
-                        self.activate_thread(
-                            thread.agent.clone(),
-                            session_info,
-                            &workspace,
-                            window,
-                            cx,
-                        );
+                        self.activate_thread(metadata, &workspace, window, cx);
                     }
                     ThreadEntryWorkspace::Closed(path_list) => {
                         self.open_workspace_and_activate_thread(
-                            thread.agent.clone(),
-                            session_info,
+                            metadata,
                             path_list.clone(),
                             window,
                             cx,
@@ -1942,8 +1900,7 @@ impl Sidebar {
 
     fn load_agent_thread_in_workspace(
         workspace: &Entity<Workspace>,
-        agent: Agent,
-        session_info: acp_thread::AgentSessionInfo,
+        metadata: &ThreadMetadata,
         focus: bool,
         window: &mut Window,
         cx: &mut App,
@@ -1955,10 +1912,10 @@ impl Sidebar {
         if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
             agent_panel.update(cx, |panel, cx| {
                 panel.load_agent_thread(
-                    agent,
-                    session_info.session_id,
-                    session_info.work_dirs,
-                    session_info.title,
+                    Agent::from(metadata.agent_id.clone()),
+                    metadata.session_id.clone(),
+                    Some(metadata.folder_paths.clone()),
+                    Some(metadata.title.clone()),
                     focus,
                     window,
                     cx,
@@ -1969,8 +1926,7 @@ impl Sidebar {
 
     fn activate_thread_locally(
         &mut self,
-        agent: Agent,
-        session_info: acp_thread::AgentSessionInfo,
+        metadata: &ThreadMetadata,
         workspace: &Entity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -1982,40 +1938,32 @@ impl Sidebar {
         // Set focused_thread eagerly so the sidebar highlight updates
         // immediately, rather than waiting for a deferred AgentPanel
         // event which can race with ActiveWorkspaceChanged clearing it.
-        self.focused_thread = Some(session_info.session_id.clone());
-        self.record_thread_access(&session_info.session_id);
+        self.focused_thread = Some(metadata.session_id.clone());
+        self.record_thread_access(&metadata.session_id);
 
         multi_workspace.update(cx, |multi_workspace, cx| {
             multi_workspace.activate(workspace.clone(), cx);
         });
 
-        Self::load_agent_thread_in_workspace(workspace, agent, session_info, true, window, cx);
+        Self::load_agent_thread_in_workspace(workspace, metadata, true, window, cx);
 
         self.update_entries(cx);
     }
 
     fn activate_thread_in_other_window(
         &self,
-        agent: Agent,
-        session_info: acp_thread::AgentSessionInfo,
+        metadata: ThreadMetadata,
         workspace: Entity<Workspace>,
         target_window: WindowHandle<MultiWorkspace>,
         cx: &mut Context<Self>,
     ) {
-        let target_session_id = session_info.session_id.clone();
+        let target_session_id = metadata.session_id.clone();
 
         let activated = target_window
             .update(cx, |multi_workspace, window, cx| {
                 window.activate_window();
                 multi_workspace.activate(workspace.clone(), cx);
-                Self::load_agent_thread_in_workspace(
-                    &workspace,
-                    agent,
-                    session_info,
-                    true,
-                    window,
-                    cx,
-                );
+                Self::load_agent_thread_in_workspace(&workspace, &metadata, true, window, cx);
             })
             .log_err()
             .is_some();
@@ -2040,8 +1988,7 @@ impl Sidebar {
 
     fn activate_thread(
         &mut self,
-        agent: Agent,
-        session_info: acp_thread::AgentSessionInfo,
+        metadata: ThreadMetadata,
         workspace: &Entity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -2050,7 +1997,7 @@ impl Sidebar {
             .find_workspace_in_current_window(cx, |candidate, _| candidate == workspace)
             .is_some()
         {
-            self.activate_thread_locally(agent, session_info, &workspace, window, cx);
+            self.activate_thread_locally(&metadata, &workspace, window, cx);
             return;
         }
 
@@ -2060,13 +2007,12 @@ impl Sidebar {
             return;
         };
 
-        self.activate_thread_in_other_window(agent, session_info, workspace, target_window, cx);
+        self.activate_thread_in_other_window(metadata, workspace, target_window, cx);
     }
 
     fn open_workspace_and_activate_thread(
         &mut self,
-        agent: Agent,
-        session_info: acp_thread::AgentSessionInfo,
+        metadata: ThreadMetadata,
         path_list: PathList,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -2084,7 +2030,7 @@ impl Sidebar {
             let workspace = open_task.await?;
 
             this.update_in(cx, |this, window, cx| {
-                this.activate_thread(agent, session_info, &workspace, window, cx);
+                this.activate_thread(metadata, &workspace, window, cx);
             })?;
             anyhow::Ok(())
         })
@@ -2113,31 +2059,23 @@ impl Sidebar {
 
     fn activate_archived_thread(
         &mut self,
-        agent: Agent,
-        session_info: acp_thread::AgentSessionInfo,
+        metadata: ThreadMetadata,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        ThreadMetadataStore::global(cx).update(cx, |store, cx| {
-            store.unarchive(&session_info.session_id, cx)
-        });
+        ThreadMetadataStore::global(cx)
+            .update(cx, |store, cx| store.unarchive(&metadata.session_id, cx));
 
-        if let Some(path_list) = &session_info.work_dirs {
-            if let Some(workspace) = self.find_current_workspace_for_path_list(path_list, cx) {
-                self.activate_thread_locally(agent, session_info, &workspace, window, cx);
+        if !metadata.folder_paths.paths().is_empty() {
+            let path_list = metadata.folder_paths.clone();
+            if let Some(workspace) = self.find_current_workspace_for_path_list(&path_list, cx) {
+                self.activate_thread_locally(&metadata, &workspace, window, cx);
             } else if let Some((target_window, workspace)) =
-                self.find_open_workspace_for_path_list(path_list, cx)
+                self.find_open_workspace_for_path_list(&path_list, cx)
             {
-                self.activate_thread_in_other_window(
-                    agent,
-                    session_info,
-                    workspace,
-                    target_window,
-                    cx,
-                );
+                self.activate_thread_in_other_window(metadata, workspace, target_window, cx);
             } else {
-                let path_list = path_list.clone();
-                self.open_workspace_and_activate_thread(agent, session_info, path_list, window, cx);
+                self.open_workspace_and_activate_thread(metadata, path_list, window, cx);
             }
             return;
         }
@@ -2150,7 +2088,7 @@ impl Sidebar {
         });
 
         if let Some(workspace) = active_workspace {
-            self.activate_thread_locally(agent, session_info, &workspace, window, cx);
+            self.activate_thread_locally(&metadata, &workspace, window, cx);
         }
     }
 
@@ -2306,7 +2244,7 @@ impl Sidebar {
         // a blank new thread in the panel instead.
         if self.focused_thread.as_ref() == Some(session_id) {
             let current_pos = self.contents.entries.iter().position(|entry| {
-                matches!(entry, ListEntry::Thread(t) if &t.session_info.session_id == session_id)
+                matches!(entry, ListEntry::Thread(t) if &t.metadata.session_id == session_id)
             });
 
             // Find the workspace that owns this thread's project group by
@@ -2358,10 +2296,7 @@ impl Sidebar {
             });
 
             if let Some(next) = next_thread {
-                let next_session_id = next.session_info.session_id.clone();
-                let next_agent = next.agent.clone();
-                let next_work_dirs = next.session_info.work_dirs.clone();
-                let next_title = next.session_info.title.clone();
+                let next_metadata = next.metadata.clone();
                 // Use the thread's own workspace when it has one open (e.g. an absorbed
                 // linked worktree thread that appears under the main workspace's header
                 // but belongs to its own workspace). Loading into the wrong panel binds
@@ -2371,17 +2306,17 @@ impl Sidebar {
                     ThreadEntryWorkspace::Open(ws) => Some(ws.clone()),
                     ThreadEntryWorkspace::Closed(_) => group_workspace,
                 };
-                self.focused_thread = Some(next_session_id.clone());
-                self.record_thread_access(&next_session_id);
+                self.focused_thread = Some(next_metadata.session_id.clone());
+                self.record_thread_access(&next_metadata.session_id);
 
                 if let Some(workspace) = target_workspace {
                     if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
                         agent_panel.update(cx, |panel, cx| {
                             panel.load_agent_thread(
-                                next_agent,
-                                next_session_id,
-                                next_work_dirs,
-                                next_title,
+                                Agent::from(next_metadata.agent_id.clone()),
+                                next_metadata.session_id.clone(),
+                                Some(next_metadata.folder_paths.clone()),
+                                Some(next_metadata.title.clone()),
                                 true,
                                 window,
                                 cx,
@@ -2419,7 +2354,7 @@ impl Sidebar {
             AgentThreadStatus::Completed | AgentThreadStatus::Error => {}
         }
 
-        let session_id = thread.session_info.session_id.clone();
+        let session_id = thread.metadata.session_id.clone();
         self.archive_thread(&session_id, window, cx)
     }
 
@@ -2453,28 +2388,22 @@ impl Sidebar {
                     };
                     let notified = self
                         .contents
-                        .is_thread_notified(&thread.session_info.session_id);
-                    let timestamp: SharedString = self
-                        .thread_last_message_sent_or_queued
-                        .get(&thread.session_info.session_id)
-                        .copied()
-                        .or(thread.session_info.created_at)
-                        .or(thread.session_info.updated_at)
-                        .map(format_history_entry_timestamp)
-                        .unwrap_or_default()
-                        .into();
+                        .is_thread_notified(&thread.metadata.session_id);
+                    let timestamp: SharedString = format_history_entry_timestamp(
+                        self.thread_last_message_sent_or_queued
+                            .get(&thread.metadata.session_id)
+                            .copied()
+                            .or(thread.metadata.created_at)
+                            .unwrap_or(thread.metadata.updated_at),
+                    )
+                    .into();
                     Some(ThreadSwitcherEntry {
-                        session_id: thread.session_info.session_id.clone(),
-                        title: thread
-                            .session_info
-                            .title
-                            .clone()
-                            .unwrap_or_else(|| "Untitled".into()),
+                        session_id: thread.metadata.session_id.clone(),
+                        title: thread.metadata.title.clone(),
                         icon: thread.icon,
                         icon_from_external_svg: thread.icon_from_external_svg.clone(),
                         status: thread.status,
-                        agent: thread.agent.clone(),
-                        session_info: thread.session_info.clone(),
+                        metadata: thread.metadata.clone(),
                         workspace,
                         worktree_name: thread.worktrees.first().map(|wt| wt.name.clone()),
 
@@ -2505,8 +2434,8 @@ impl Sidebar {
                         (Some(_), None) => std::cmp::Ordering::Less,
                         (None, Some(_)) => std::cmp::Ordering::Greater,
                         (None, None) => {
-                            let a_time = a.session_info.created_at.or(a.session_info.updated_at);
-                            let b_time = b.session_info.created_at.or(b.session_info.updated_at);
+                            let a_time = a.metadata.created_at.or(Some(a.metadata.updated_at));
+                            let b_time = b.metadata.created_at.or(Some(b.metadata.updated_at));
                             b_time.cmp(&a_time)
                         }
                     }
@@ -2560,16 +2489,11 @@ impl Sidebar {
 
         let weak_multi_workspace = self.multi_workspace.clone();
 
-        let original_agent = self
-            .focused_thread
-            .as_ref()
-            .and_then(|focused_id| entries.iter().find(|e| &e.session_id == focused_id))
-            .map(|e| e.agent.clone());
-        let original_session_info = self
+        let original_metadata = self
             .focused_thread
             .as_ref()
             .and_then(|focused_id| entries.iter().find(|e| &e.session_id == focused_id))
-            .map(|e| e.session_info.clone());
+            .map(|e| e.metadata.clone());
         let original_workspace = self
             .multi_workspace
             .upgrade()
@@ -2583,8 +2507,7 @@ impl Sidebar {
             let thread_switcher = thread_switcher.clone();
             move |this, _emitter, event: &ThreadSwitcherEvent, window, cx| match event {
                 ThreadSwitcherEvent::Preview {
-                    agent,
-                    session_info,
+                    metadata,
                     workspace,
                 } => {
                     if let Some(mw) = weak_multi_workspace.upgrade() {
@@ -2592,22 +2515,14 @@ impl Sidebar {
                             mw.activate(workspace.clone(), cx);
                         });
                     }
-                    this.focused_thread = Some(session_info.session_id.clone());
+                    this.focused_thread = Some(metadata.session_id.clone());
                     this.update_entries(cx);
-                    Self::load_agent_thread_in_workspace(
-                        workspace,
-                        agent.clone(),
-                        session_info.clone(),
-                        false,
-                        window,
-                        cx,
-                    );
+                    Self::load_agent_thread_in_workspace(workspace, metadata, false, window, cx);
                     let focus = thread_switcher.focus_handle(cx);
                     window.focus(&focus, cx);
                 }
                 ThreadSwitcherEvent::Confirmed {
-                    agent,
-                    session_info,
+                    metadata,
                     workspace,
                 } => {
                     if let Some(mw) = weak_multi_workspace.upgrade() {
@@ -2615,17 +2530,10 @@ impl Sidebar {
                             mw.activate(workspace.clone(), cx);
                         });
                     }
-                    this.record_thread_access(&session_info.session_id);
-                    this.focused_thread = Some(session_info.session_id.clone());
+                    this.record_thread_access(&metadata.session_id);
+                    this.focused_thread = Some(metadata.session_id.clone());
                     this.update_entries(cx);
-                    Self::load_agent_thread_in_workspace(
-                        workspace,
-                        agent.clone(),
-                        session_info.clone(),
-                        false,
-                        window,
-                        cx,
-                    );
+                    Self::load_agent_thread_in_workspace(workspace, metadata, false, window, cx);
                     this.dismiss_thread_switcher(cx);
                     workspace.update(cx, |workspace, cx| {
                         workspace.focus_panel::<AgentPanel>(window, cx);
@@ -2639,15 +2547,13 @@ impl Sidebar {
                             });
                         }
                     }
-                    if let Some(session_info) = &original_session_info {
-                        this.focused_thread = Some(session_info.session_id.clone());
+                    if let Some(metadata) = &original_metadata {
+                        this.focused_thread = Some(metadata.session_id.clone());
                         this.update_entries(cx);
-                        let agent = original_agent.clone().unwrap_or(Agent::NativeAgent);
                         if let Some(original_ws) = &original_workspace {
                             Self::load_agent_thread_in_workspace(
                                 original_ws,
-                                agent,
-                                session_info.clone(),
+                                metadata,
                                 false,
                                 window,
                                 cx,
@@ -2672,13 +2578,10 @@ impl Sidebar {
 
         // Replay the initial preview that was emitted during construction
         // before subscriptions were wired up.
-        let initial_preview = thread_switcher.read(cx).selected_entry().map(|entry| {
-            (
-                entry.agent.clone(),
-                entry.session_info.clone(),
-                entry.workspace.clone(),
-            )
-        });
+        let initial_preview = thread_switcher
+            .read(cx)
+            .selected_entry()
+            .map(|entry| (entry.metadata.clone(), entry.workspace.clone()));
 
         self.thread_switcher = Some(thread_switcher);
         self._thread_switcher_subscriptions = subscriptions;
@@ -2688,22 +2591,15 @@ impl Sidebar {
             });
         }
 
-        if let Some((agent, session_info, workspace)) = initial_preview {
+        if let Some((metadata, workspace)) = initial_preview {
             if let Some(mw) = self.multi_workspace.upgrade() {
                 mw.update(cx, |mw, cx| {
                     mw.activate(workspace.clone(), cx);
                 });
             }
-            self.focused_thread = Some(session_info.session_id.clone());
+            self.focused_thread = Some(metadata.session_id.clone());
             self.update_entries(cx);
-            Self::load_agent_thread_in_workspace(
-                &workspace,
-                agent,
-                session_info,
-                false,
-                window,
-                cx,
-            );
+            Self::load_agent_thread_in_workspace(&workspace, &metadata, false, window, cx);
         }
 
         window.focus(&focus, cx);
@@ -2718,36 +2614,32 @@ impl Sidebar {
     ) -> AnyElement {
         let has_notification = self
             .contents
-            .is_thread_notified(&thread.session_info.session_id);
-
-        let title: SharedString = thread
-            .session_info
-            .title
-            .clone()
-            .unwrap_or_else(|| "Untitled".into());
-        let session_info = thread.session_info.clone();
+            .is_thread_notified(&thread.metadata.session_id);
+
+        let title: SharedString = thread.metadata.title.clone();
+        let metadata = thread.metadata.clone();
         let thread_workspace = thread.workspace.clone();
 
         let is_hovered = self.hovered_thread_index == Some(ix);
-        let is_selected = self.agent_panel_visible
-            && self.focused_thread.as_ref() == Some(&session_info.session_id);
+        let is_selected =
+            self.agent_panel_visible && self.focused_thread.as_ref() == Some(&metadata.session_id);
         let is_running = matches!(
             thread.status,
             AgentThreadStatus::Running | AgentThreadStatus::WaitingForConfirmation
         );
 
-        let session_id_for_delete = thread.session_info.session_id.clone();
+        let session_id_for_delete = thread.metadata.session_id.clone();
         let focus_handle = self.focus_handle.clone();
 
         let id = SharedString::from(format!("thread-entry-{}", ix));
 
-        let timestamp = self
-            .thread_last_message_sent_or_queued
-            .get(&thread.session_info.session_id)
-            .copied()
-            .or(thread.session_info.created_at)
-            .or(thread.session_info.updated_at)
-            .map(format_history_entry_timestamp);
+        let timestamp = format_history_entry_timestamp(
+            self.thread_last_message_sent_or_queued
+                .get(&thread.metadata.session_id)
+                .copied()
+                .or(thread.metadata.created_at)
+                .unwrap_or(thread.metadata.updated_at),
+        );
 
         ThreadItem::new(id, title)
             .icon(thread.icon)
@@ -2766,7 +2658,7 @@ impl Sidebar {
                     })
                     .collect(),
             )
-            .when_some(timestamp, |this, ts| this.timestamp(ts))
+            .timestamp(timestamp)
             .highlight_positions(thread.highlight_positions.to_vec())
             .title_generating(thread.is_title_generating)
             .notified(has_notification)
@@ -2827,23 +2719,15 @@ impl Sidebar {
                 )
             })
             .on_click({
-                let agent = thread.agent.clone();
                 cx.listener(move |this, _, window, cx| {
                     this.selection = None;
                     match &thread_workspace {
                         ThreadEntryWorkspace::Open(workspace) => {
-                            this.activate_thread(
-                                agent.clone(),
-                                session_info.clone(),
-                                workspace,
-                                window,
-                                cx,
-                            );
+                            this.activate_thread(metadata.clone(), workspace, window, cx);
                         }
                         ThreadEntryWorkspace::Closed(path_list) => {
                             this.open_workspace_and_activate_thread(
-                                agent.clone(),
-                                session_info.clone(),
+                                metadata.clone(),
                                 path_list.clone(),
                                 window,
                                 cx,
@@ -3333,18 +3217,7 @@ impl Sidebar {
                 }
                 ThreadsArchiveViewEvent::Unarchive { thread } => {
                     this.show_thread_list(window, cx);
-
-                    let agent = Agent::from(thread.agent_id.clone());
-                    let session_info = acp_thread::AgentSessionInfo {
-                        session_id: thread.session_id.clone(),
-                        work_dirs: Some(thread.folder_paths.clone()),
-                        title: Some(thread.title.clone()),
-                        updated_at: Some(thread.updated_at),
-                        created_at: thread.created_at,
-                        meta: None,
-                    };
-
-                    this.activate_archived_thread(agent, session_info, window, cx);
+                    this.activate_archived_thread(thread.clone(), window, cx);
                 }
             },
         );

crates/sidebar/src/sidebar_tests.rs 🔗

@@ -11,6 +11,7 @@ use feature_flags::FeatureFlagAppExt as _;
 use fs::FakeFs;
 use gpui::TestAppContext;
 use pretty_assertions::assert_eq;
+use project::AgentId;
 use settings::SettingsStore;
 use std::{path::PathBuf, sync::Arc};
 use util::path_list::PathList;
@@ -30,9 +31,11 @@ fn init_test(cx: &mut TestAppContext) {
 }
 
 fn has_thread_entry(sidebar: &Sidebar, session_id: &acp::SessionId) -> bool {
-    sidebar.contents.entries.iter().any(
-        |entry| matches!(entry, ListEntry::Thread(t) if &t.session_info.session_id == session_id),
-    )
+    sidebar
+        .contents
+        .entries
+        .iter()
+        .any(|entry| matches!(entry, ListEntry::Thread(t) if &t.metadata.session_id == session_id))
 }
 
 async fn init_test_project(
@@ -176,12 +179,7 @@ fn visible_entries_as_strings(
                         format!("{} [{}]{}", icon, label, selected)
                     }
                     ListEntry::Thread(thread) => {
-                        let title = thread
-                            .session_info
-                            .title
-                            .as_ref()
-                            .map(|s| s.as_ref())
-                            .unwrap_or("Untitled");
+                        let title = thread.metadata.title.as_ref();
                         let active = if thread.is_live { " *" } else { "" };
                         let status_str = match thread.status {
                             AgentThreadStatus::Running => " (running)",
@@ -191,7 +189,7 @@ fn visible_entries_as_strings(
                         };
                         let notified = if sidebar
                             .contents
-                            .is_thread_notified(&thread.session_info.session_id)
+                            .is_thread_notified(&thread.metadata.session_id)
                         {
                             " (!)"
                         } else {
@@ -565,14 +563,14 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
                 is_active: true,
             },
             ListEntry::Thread(ThreadEntry {
-                agent: Agent::NativeAgent,
-                session_info: acp_thread::AgentSessionInfo {
+                metadata: ThreadMetadata {
                     session_id: acp::SessionId::new(Arc::from("t-1")),
-                    work_dirs: None,
-                    title: Some("Completed thread".into()),
-                    updated_at: Some(Utc::now()),
+                    agent_id: AgentId::new("zed-agent"),
+                    folder_paths: PathList::default(),
+                    title: "Completed thread".into(),
+                    updated_at: Utc::now(),
                     created_at: Some(Utc::now()),
-                    meta: None,
+                    archived: false,
                 },
                 icon: IconName::ZedAgent,
                 icon_from_external_svg: None,
@@ -587,14 +585,14 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
             }),
             // Active thread with Running status
             ListEntry::Thread(ThreadEntry {
-                agent: Agent::NativeAgent,
-                session_info: acp_thread::AgentSessionInfo {
+                metadata: ThreadMetadata {
                     session_id: acp::SessionId::new(Arc::from("t-2")),
-                    work_dirs: None,
-                    title: Some("Running thread".into()),
-                    updated_at: Some(Utc::now()),
+                    agent_id: AgentId::new("zed-agent"),
+                    folder_paths: PathList::default(),
+                    title: "Running thread".into(),
+                    updated_at: Utc::now(),
                     created_at: Some(Utc::now()),
-                    meta: None,
+                    archived: false,
                 },
                 icon: IconName::ZedAgent,
                 icon_from_external_svg: None,
@@ -609,14 +607,14 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
             }),
             // Active thread with Error status
             ListEntry::Thread(ThreadEntry {
-                agent: Agent::NativeAgent,
-                session_info: acp_thread::AgentSessionInfo {
+                metadata: ThreadMetadata {
                     session_id: acp::SessionId::new(Arc::from("t-3")),
-                    work_dirs: None,
-                    title: Some("Error thread".into()),
-                    updated_at: Some(Utc::now()),
+                    agent_id: AgentId::new("zed-agent"),
+                    folder_paths: PathList::default(),
+                    title: "Error thread".into(),
+                    updated_at: Utc::now(),
                     created_at: Some(Utc::now()),
-                    meta: None,
+                    archived: false,
                 },
                 icon: IconName::ZedAgent,
                 icon_from_external_svg: None,
@@ -631,14 +629,14 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
             }),
             // Thread with WaitingForConfirmation status, not active
             ListEntry::Thread(ThreadEntry {
-                agent: Agent::NativeAgent,
-                session_info: acp_thread::AgentSessionInfo {
+                metadata: ThreadMetadata {
                     session_id: acp::SessionId::new(Arc::from("t-4")),
-                    work_dirs: None,
-                    title: Some("Waiting thread".into()),
-                    updated_at: Some(Utc::now()),
+                    agent_id: AgentId::new("zed-agent"),
+                    folder_paths: PathList::default(),
+                    title: "Waiting thread".into(),
+                    updated_at: Utc::now(),
                     created_at: Some(Utc::now()),
-                    meta: None,
+                    archived: false,
                 },
                 icon: IconName::ZedAgent,
                 icon_from_external_svg: None,
@@ -653,14 +651,14 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
             }),
             // Background thread that completed (should show notification)
             ListEntry::Thread(ThreadEntry {
-                agent: Agent::NativeAgent,
-                session_info: acp_thread::AgentSessionInfo {
+                metadata: ThreadMetadata {
                     session_id: acp::SessionId::new(Arc::from("t-5")),
-                    work_dirs: None,
-                    title: Some("Notified thread".into()),
-                    updated_at: Some(Utc::now()),
+                    agent_id: AgentId::new("zed-agent"),
+                    folder_paths: PathList::default(),
+                    title: "Notified thread".into(),
+                    updated_at: Utc::now(),
                     created_at: Some(Utc::now()),
-                    meta: None,
+                    archived: false,
                 },
                 icon: IconName::ZedAgent,
                 icon_from_external_svg: None,
@@ -1919,14 +1917,14 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
 
     sidebar.update_in(cx, |sidebar, window, cx| {
         sidebar.activate_thread(
-            Agent::NativeAgent,
-            acp_thread::AgentSessionInfo {
+            ThreadMetadata {
                 session_id: session_id_a.clone(),
-                work_dirs: None,
-                title: Some("Test".into()),
-                updated_at: None,
+                agent_id: agent::ZED_AGENT_ID.clone(),
+                title: "Test".into(),
+                updated_at: Utc::now(),
                 created_at: None,
-                meta: None,
+                folder_paths: PathList::default(),
+                archived: false,
             },
             &workspace_a,
             window,
@@ -1974,14 +1972,14 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
     // which also triggers a workspace switch.
     sidebar.update_in(cx, |sidebar, window, cx| {
         sidebar.activate_thread(
-            Agent::NativeAgent,
-            acp_thread::AgentSessionInfo {
+            ThreadMetadata {
                 session_id: session_id_b.clone(),
-                work_dirs: None,
-                title: Some("Thread B".into()),
-                updated_at: None,
+                agent_id: agent::ZED_AGENT_ID.clone(),
+                title: "Thread B".into(),
+                updated_at: Utc::now(),
                 created_at: None,
-                meta: None,
+                folder_paths: PathList::default(),
+                archived: false,
             },
             &workspace_b,
             window,
@@ -3231,24 +3229,14 @@ async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_proje
                     );
                 }
                 ListEntry::Thread(thread)
-                    if thread
-                        .session_info
-                        .title
-                        .as_ref()
-                        .map(|title| title.as_ref())
-                        == Some("WT Thread")
+                    if thread.metadata.title.as_ref() == "WT Thread"
                         && thread.worktrees.first().map(|wt| wt.name.as_ref())
                             == Some("wt-feature-a") =>
                 {
                     saw_expected_thread = true;
                 }
                 ListEntry::Thread(thread) => {
-                    let title = thread
-                        .session_info
-                        .title
-                        .as_ref()
-                        .map(|title| title.as_ref())
-                        .unwrap_or("Untitled");
+                    let title = thread.metadata.title.as_ref();
                     let worktree_name = thread
                         .worktrees
                         .first()
@@ -3446,14 +3434,14 @@ async fn test_activate_archived_thread_with_saved_paths_activates_matching_works
     // switch to the workspace for project-b.
     sidebar.update_in(cx, |sidebar, window, cx| {
         sidebar.activate_archived_thread(
-            Agent::NativeAgent,
-            acp_thread::AgentSessionInfo {
+            ThreadMetadata {
                 session_id: session_id.clone(),
-                work_dirs: Some(PathList::new(&[PathBuf::from("/project-b")])),
-                title: Some("Archived Thread".into()),
-                updated_at: None,
+                agent_id: agent::ZED_AGENT_ID.clone(),
+                title: "Archived Thread".into(),
+                updated_at: Utc::now(),
                 created_at: None,
-                meta: None,
+                folder_paths: PathList::new(&[PathBuf::from("/project-b")]),
+                archived: false,
             },
             window,
             cx,
@@ -3507,14 +3495,14 @@ async fn test_activate_archived_thread_cwd_fallback_with_matching_workspace(
     // No thread saved to the store – cwd is the only path hint.
     sidebar.update_in(cx, |sidebar, window, cx| {
         sidebar.activate_archived_thread(
-            Agent::NativeAgent,
-            acp_thread::AgentSessionInfo {
+            ThreadMetadata {
                 session_id: acp::SessionId::new(Arc::from("unknown-session")),
-                work_dirs: Some(PathList::new(&[std::path::PathBuf::from("/project-b")])),
-                title: Some("CWD Thread".into()),
-                updated_at: None,
+                agent_id: agent::ZED_AGENT_ID.clone(),
+                title: "CWD Thread".into(),
+                updated_at: Utc::now(),
                 created_at: None,
-                meta: None,
+                folder_paths: PathList::new(&[std::path::PathBuf::from("/project-b")]),
+                archived: false,
             },
             window,
             cx,
@@ -3568,14 +3556,14 @@ async fn test_activate_archived_thread_no_paths_no_cwd_uses_active_workspace(
     // No saved thread, no cwd – should fall back to the active workspace.
     sidebar.update_in(cx, |sidebar, window, cx| {
         sidebar.activate_archived_thread(
-            Agent::NativeAgent,
-            acp_thread::AgentSessionInfo {
+            ThreadMetadata {
                 session_id: acp::SessionId::new(Arc::from("no-context-session")),
-                work_dirs: None,
-                title: Some("Contextless Thread".into()),
-                updated_at: None,
+                agent_id: agent::ZED_AGENT_ID.clone(),
+                title: "Contextless Thread".into(),
+                updated_at: Utc::now(),
                 created_at: None,
-                meta: None,
+                folder_paths: PathList::default(),
+                archived: false,
             },
             window,
             cx,
@@ -3622,14 +3610,14 @@ async fn test_activate_archived_thread_saved_paths_opens_new_workspace(cx: &mut
 
     sidebar.update_in(cx, |sidebar, window, cx| {
         sidebar.activate_archived_thread(
-            Agent::NativeAgent,
-            acp_thread::AgentSessionInfo {
+            ThreadMetadata {
                 session_id: session_id.clone(),
-                work_dirs: Some(path_list_b),
-                title: Some("New WS Thread".into()),
-                updated_at: None,
+                agent_id: agent::ZED_AGENT_ID.clone(),
+                title: "New WS Thread".into(),
+                updated_at: Utc::now(),
                 created_at: None,
-                meta: None,
+                folder_paths: path_list_b,
+                archived: false,
             },
             window,
             cx,
@@ -3671,14 +3659,14 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window(cx: &m
 
     sidebar.update_in(cx_a, |sidebar, window, cx| {
         sidebar.activate_archived_thread(
-            Agent::NativeAgent,
-            acp_thread::AgentSessionInfo {
+            ThreadMetadata {
                 session_id: session_id.clone(),
-                work_dirs: Some(PathList::new(&[PathBuf::from("/project-b")])),
-                title: Some("Cross Window Thread".into()),
-                updated_at: None,
+                agent_id: agent::ZED_AGENT_ID.clone(),
+                title: "Cross Window Thread".into(),
+                updated_at: Utc::now(),
                 created_at: None,
-                meta: None,
+                folder_paths: PathList::new(&[PathBuf::from("/project-b")]),
+                archived: false,
             },
             window,
             cx,
@@ -3747,14 +3735,14 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window_with_t
 
     sidebar_a.update_in(cx_a, |sidebar, window, cx| {
         sidebar.activate_archived_thread(
-            Agent::NativeAgent,
-            acp_thread::AgentSessionInfo {
+            ThreadMetadata {
                 session_id: session_id.clone(),
-                work_dirs: Some(PathList::new(&[PathBuf::from("/project-b")])),
-                title: Some("Cross Window Thread".into()),
-                updated_at: None,
+                agent_id: agent::ZED_AGENT_ID.clone(),
+                title: "Cross Window Thread".into(),
+                updated_at: Utc::now(),
                 created_at: None,
-                meta: None,
+                folder_paths: PathList::new(&[PathBuf::from("/project-b")]),
+                archived: false,
             },
             window,
             cx,
@@ -3822,14 +3810,14 @@ async fn test_activate_archived_thread_prefers_current_window_for_matching_paths
 
     sidebar_a.update_in(cx_a, |sidebar, window, cx| {
         sidebar.activate_archived_thread(
-            Agent::NativeAgent,
-            acp_thread::AgentSessionInfo {
+            ThreadMetadata {
                 session_id: session_id.clone(),
-                work_dirs: Some(PathList::new(&[PathBuf::from("/project-a")])),
-                title: Some("Current Window Thread".into()),
-                updated_at: None,
+                agent_id: agent::ZED_AGENT_ID.clone(),
+                title: "Current Window Thread".into(),
+                updated_at: Utc::now(),
                 created_at: None,
-                meta: None,
+                folder_paths: PathList::new(&[PathBuf::from("/project-a")]),
+                archived: false,
             },
             window,
             cx,

crates/sidebar/src/thread_switcher.rs 🔗

@@ -1,7 +1,6 @@
-use acp_thread;
 use action_log::DiffStats;
 use agent_client_protocol as acp;
-use agent_ui::Agent;
+use agent_ui::thread_metadata_store::ThreadMetadata;
 use gpui::{
     Action as _, Animation, AnimationExt, AnyElement, DismissEvent, Entity, EventEmitter,
     FocusHandle, Focusable, Hsla, Modifiers, ModifiersChangedEvent, Render, SharedString,
@@ -23,8 +22,7 @@ pub(crate) struct ThreadSwitcherEntry {
     pub icon: IconName,
     pub icon_from_external_svg: Option<SharedString>,
     pub status: AgentThreadStatus,
-    pub agent: Agent,
-    pub session_info: acp_thread::AgentSessionInfo,
+    pub metadata: ThreadMetadata,
     pub workspace: Entity<Workspace>,
     pub worktree_name: Option<SharedString>,
     pub diff_stats: DiffStats,
@@ -35,13 +33,11 @@ pub(crate) struct ThreadSwitcherEntry {
 
 pub(crate) enum ThreadSwitcherEvent {
     Preview {
-        agent: Agent,
-        session_info: acp_thread::AgentSessionInfo,
+        metadata: ThreadMetadata,
         workspace: Entity<Workspace>,
     },
     Confirmed {
-        agent: Agent,
-        session_info: acp_thread::AgentSessionInfo,
+        metadata: ThreadMetadata,
         workspace: Entity<Workspace>,
     },
     Dismissed,
@@ -72,8 +68,7 @@ impl ThreadSwitcher {
 
         if let Some(entry) = entries.get(selected_index) {
             cx.emit(ThreadSwitcherEvent::Preview {
-                agent: entry.agent.clone(),
-                session_info: entry.session_info.clone(),
+                metadata: entry.metadata.clone(),
                 workspace: entry.workspace.clone(),
             });
         }
@@ -130,8 +125,7 @@ impl ThreadSwitcher {
     fn emit_preview(&mut self, cx: &mut Context<Self>) {
         if let Some(entry) = self.entries.get(self.selected_index) {
             cx.emit(ThreadSwitcherEvent::Preview {
-                agent: entry.agent.clone(),
-                session_info: entry.session_info.clone(),
+                metadata: entry.metadata.clone(),
                 workspace: entry.workspace.clone(),
             });
         }
@@ -140,8 +134,7 @@ impl ThreadSwitcher {
     fn confirm(&mut self, _: &menu::Confirm, _window: &mut gpui::Window, cx: &mut Context<Self>) {
         if let Some(entry) = self.entries.get(self.selected_index) {
             cx.emit(ThreadSwitcherEvent::Confirmed {
-                agent: entry.agent.clone(),
-                session_info: entry.session_info.clone(),
+                metadata: entry.metadata.clone(),
                 workspace: entry.workspace.clone(),
             });
         }