agent_ui: Wire up archive entry loading (#51475)

Ben Brandt , cameron , and Bennet Bo Fenner created

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>

Change summary

crates/agent_ui/src/agent_panel.rs          | 162 ++++----
crates/agent_ui/src/sidebar.rs              | 420 ++++++++++++++++++++--
crates/agent_ui/src/thread_history_view.rs  |  18 
crates/agent_ui/src/threads_archive_view.rs |  11 
crates/agent_ui/src/ui/mention_crease.rs    |  13 
5 files changed, 492 insertions(+), 132 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -888,7 +888,7 @@ pub struct AgentPanel {
     zoomed: bool,
     pending_serialization: Option<Task<Result<()>>>,
     onboarding: Entity<AgentPanelOnboarding>,
-    selected_agent: AgentType,
+    selected_agent_type: AgentType,
     start_thread_in: StartThreadIn,
     worktree_creation_status: Option<WorktreeCreationStatus>,
     _thread_view_subscription: Option<Subscription>,
@@ -908,7 +908,7 @@ impl AgentPanel {
         };
 
         let width = self.width;
-        let selected_agent = self.selected_agent.clone();
+        let selected_agent_type = self.selected_agent_type.clone();
         let start_thread_in = Some(self.start_thread_in);
 
         let last_active_thread = self.active_agent_thread(cx).map(|thread| {
@@ -916,7 +916,7 @@ impl AgentPanel {
             let title = thread.title();
             SerializedActiveThread {
                 session_id: thread.session_id().0.to_string(),
-                agent_type: self.selected_agent.clone(),
+                agent_type: self.selected_agent_type.clone(),
                 title: if title.as_ref() != DEFAULT_THREAD_TITLE {
                     Some(title.to_string())
                 } else {
@@ -931,7 +931,7 @@ impl AgentPanel {
                 workspace_id,
                 SerializedAgentPanel {
                     width,
-                    selected_agent: Some(selected_agent),
+                    selected_agent: Some(selected_agent_type),
                     last_active_thread,
                     start_thread_in,
                 },
@@ -1017,7 +1017,7 @@ impl AgentPanel {
                     panel.update(cx, |panel, cx| {
                         panel.width = serialized_panel.width.map(|w| w.round());
                         if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
-                            panel.selected_agent = selected_agent;
+                            panel.selected_agent_type = selected_agent;
                         }
                         if let Some(start_thread_in) = serialized_panel.start_thread_in {
                             let is_worktree_flag_enabled =
@@ -1045,8 +1045,18 @@ impl AgentPanel {
                 if let Some(thread_info) = last_active_thread {
                     let agent_type = thread_info.agent_type.clone();
                     panel.update(cx, |panel, cx| {
-                        panel.selected_agent = agent_type;
-                        panel.load_agent_thread_inner(thread_info.session_id.into(), thread_info.cwd, thread_info.title.map(SharedString::from), false, window, cx);
+                        panel.selected_agent_type = agent_type;
+                        if let Some(agent) = panel.selected_agent() {
+                            panel.load_agent_thread(
+                                agent,
+                                thread_info.session_id.into(),
+                                thread_info.cwd,
+                                thread_info.title.map(SharedString::from),
+                                false,
+                                window,
+                                cx,
+                            );
+                        }
                     });
                 }
                 panel
@@ -1214,7 +1224,7 @@ impl AgentPanel {
             onboarding,
             text_thread_history,
             thread_store,
-            selected_agent: AgentType::default(),
+            selected_agent_type: AgentType::default(),
             start_thread_in: StartThreadIn::default(),
             worktree_creation_status: None,
             _thread_view_subscription: None,
@@ -1403,8 +1413,8 @@ impl AgentPanel {
             editor
         });
 
-        if self.selected_agent != AgentType::TextThread {
-            self.selected_agent = AgentType::TextThread;
+        if self.selected_agent_type != AgentType::TextThread {
+            self.selected_agent_type = AgentType::TextThread;
             self.serialize(cx);
         }
 
@@ -1464,7 +1474,7 @@ impl AgentPanel {
             .detach();
 
             let server = agent.server(fs, thread_store);
-            self.create_external_thread(
+            self.create_agent_thread(
                 server,
                 resume_session_id,
                 cwd,
@@ -1497,7 +1507,7 @@ impl AgentPanel {
 
                 let server = ext_agent.server(fs, thread_store);
                 this.update_in(cx, |agent_panel, window, cx| {
-                    agent_panel.create_external_thread(
+                    agent_panel.create_agent_thread(
                         server,
                         resume_session_id,
                         cwd,
@@ -1558,7 +1568,7 @@ impl AgentPanel {
     }
 
     fn has_history_for_selected_agent(&self, cx: &App) -> bool {
-        match &self.selected_agent {
+        match &self.selected_agent_type {
             AgentType::TextThread | AgentType::NativeAgent => true,
             AgentType::Custom { name } => {
                 let agent = Agent::Custom { name: name.clone() };
@@ -1575,7 +1585,7 @@ impl AgentPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<History> {
-        match &self.selected_agent {
+        match &self.selected_agent_type {
             AgentType::TextThread => Some(History::TextThreads),
             AgentType::NativeAgent => {
                 let history = self
@@ -1587,7 +1597,7 @@ impl AgentPanel {
                     .clone();
 
                 Some(History::AgentThreads {
-                    view: self.create_thread_history_view(history, window, cx),
+                    view: self.create_thread_history_view(Agent::NativeAgent, history, window, cx),
                 })
             }
             AgentType::Custom { name } => {
@@ -1601,7 +1611,7 @@ impl AgentPanel {
                     .clone();
                 if history.read(cx).has_session_list() {
                     Some(History::AgentThreads {
-                        view: self.create_thread_history_view(history, window, cx),
+                        view: self.create_thread_history_view(agent, history, window, cx),
                     })
                 } else {
                     None
@@ -1612,22 +1622,29 @@ impl AgentPanel {
 
     fn create_thread_history_view(
         &self,
+        agent: Agent,
         history: Entity<ThreadHistory>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Entity<ThreadHistoryView> {
         let view = cx.new(|cx| ThreadHistoryView::new(history.clone(), window, cx));
-        cx.subscribe_in(&view, window, |this, _, event, window, cx| match event {
-            ThreadHistoryViewEvent::Open(thread) => {
-                this.load_agent_thread(
-                    thread.session_id.clone(),
-                    thread.cwd.clone(),
-                    thread.title.clone(),
-                    window,
-                    cx,
-                );
-            }
-        })
+        cx.subscribe_in(
+            &view,
+            window,
+            move |this, _, event, window, cx| match event {
+                ThreadHistoryViewEvent::Open(thread) => {
+                    this.load_agent_thread(
+                        agent.clone(),
+                        thread.session_id.clone(),
+                        thread.cwd.clone(),
+                        thread.title.clone(),
+                        true,
+                        window,
+                        cx,
+                    );
+                }
+            },
+        )
         .detach();
         view
     }
@@ -1691,8 +1708,8 @@ impl AgentPanel {
             )
         });
 
-        if self.selected_agent != AgentType::TextThread {
-            self.selected_agent = AgentType::TextThread;
+        if self.selected_agent_type != AgentType::TextThread {
+            self.selected_agent_type = AgentType::TextThread;
             self.serialize(cx);
         }
 
@@ -2266,13 +2283,17 @@ impl AgentPanel {
                             let entry = entry.clone();
                             panel
                                 .update(cx, move |this, cx| {
-                                    this.load_agent_thread(
-                                        entry.session_id.clone(),
-                                        entry.cwd.clone(),
-                                        entry.title.clone(),
-                                        window,
-                                        cx,
-                                    );
+                                    if let Some(agent) = this.selected_agent() {
+                                        this.load_agent_thread(
+                                            agent,
+                                            entry.session_id.clone(),
+                                            entry.cwd.clone(),
+                                            entry.title.clone(),
+                                            true,
+                                            window,
+                                            cx,
+                                        );
+                                    }
                                 })
                                 .ok();
                         }
@@ -2322,10 +2343,6 @@ impl AgentPanel {
         menu.separator()
     }
 
-    pub fn selected_agent(&self) -> AgentType {
-        self.selected_agent.clone()
-    }
-
     fn subscribe_to_active_thread_view(
         server_view: &Entity<ConnectionView>,
         window: &mut Window,
@@ -2396,8 +2413,8 @@ impl AgentPanel {
         }
     }
 
-    fn selected_external_agent(&self) -> Option<Agent> {
-        match &self.selected_agent {
+    pub(crate) fn selected_agent(&self) -> Option<Agent> {
+        match &self.selected_agent_type {
             AgentType::NativeAgent => Some(Agent::NativeAgent),
             AgentType::Custom { name } => Some(Agent::Custom { name: name.clone() }),
             AgentType::TextThread => None,
@@ -2493,17 +2510,7 @@ impl AgentPanel {
 
     pub fn load_agent_thread(
         &mut self,
-        session_id: acp::SessionId,
-        cwd: Option<PathBuf>,
-        title: Option<SharedString>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.load_agent_thread_inner(session_id, cwd, title, true, window, cx);
-    }
-
-    fn load_agent_thread_inner(
-        &mut self,
+        agent: Agent,
         session_id: acp::SessionId,
         cwd: Option<PathBuf>,
         title: Option<SharedString>,
@@ -2541,9 +2548,6 @@ impl AgentPanel {
             }
         }
 
-        let Some(agent) = self.selected_external_agent() else {
-            return;
-        };
         self.external_thread(
             Some(agent),
             Some(session_id),
@@ -2556,7 +2560,7 @@ impl AgentPanel {
         );
     }
 
-    pub(crate) fn create_external_thread(
+    pub(crate) fn create_agent_thread(
         &mut self,
         server: Rc<dyn AgentServer>,
         resume_session_id: Option<acp::SessionId>,
@@ -2571,8 +2575,8 @@ impl AgentPanel {
         cx: &mut Context<Self>,
     ) {
         let selected_agent = AgentType::from(ext_agent.clone());
-        if self.selected_agent != selected_agent {
-            self.selected_agent = selected_agent;
+        if self.selected_agent_type != selected_agent {
+            self.selected_agent_type = selected_agent;
             self.serialize(cx);
         }
         let thread_store = server
@@ -2825,8 +2829,8 @@ impl AgentPanel {
     ) {
         self.worktree_creation_status = Some(WorktreeCreationStatus::Error(message));
         if matches!(self.active_view, ActiveView::Uninitialized) {
-            let selected_agent = self.selected_agent.clone();
-            self.new_agent_thread(selected_agent, window, cx);
+            let selected_agent_type = self.selected_agent_type.clone();
+            self.new_agent_thread(selected_agent_type, window, cx);
         }
         cx.notify();
     }
@@ -3218,8 +3222,8 @@ impl Panel for AgentPanel {
                 Some(WorktreeCreationStatus::Creating)
             )
         {
-            let selected_agent = self.selected_agent.clone();
-            self.new_agent_thread_inner(selected_agent, false, window, cx);
+            let selected_agent_type = self.selected_agent_type.clone();
+            self.new_agent_thread_inner(selected_agent_type, false, window, cx);
         }
     }
 
@@ -3871,16 +3875,16 @@ impl AgentPanel {
         let docked_right = agent_panel_dock_position(cx) == DockPosition::Right;
 
         let (selected_agent_custom_icon, selected_agent_label) =
-            if let AgentType::Custom { name, .. } = &self.selected_agent {
+            if let AgentType::Custom { name, .. } = &self.selected_agent_type {
                 let store = agent_server_store.read(cx);
                 let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
 
                 let label = store
                     .agent_display_name(&ExternalAgentServerName(name.clone()))
-                    .unwrap_or_else(|| self.selected_agent.label());
+                    .unwrap_or_else(|| self.selected_agent_type.label());
                 (icon, label)
             } else {
-                (None, self.selected_agent.label())
+                (None, self.selected_agent_type.label())
             };
 
         let active_thread = match &self.active_view {
@@ -3894,7 +3898,7 @@ impl AgentPanel {
         let new_thread_menu_builder: Rc<
             dyn Fn(&mut Window, &mut App) -> Option<Entity<ContextMenu>>,
         > = {
-            let selected_agent = self.selected_agent.clone();
+            let selected_agent = self.selected_agent_type.clone();
             let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
 
             let workspace = self.workspace.clone();
@@ -4210,7 +4214,7 @@ impl AgentPanel {
 
         let has_custom_icon = selected_agent_custom_icon.is_some();
         let selected_agent_custom_icon_for_button = selected_agent_custom_icon.clone();
-        let selected_agent_builtin_icon = self.selected_agent.icon();
+        let selected_agent_builtin_icon = self.selected_agent_type.icon();
         let selected_agent_label_for_tooltip = selected_agent_label.clone();
 
         let selected_agent = div()
@@ -4220,7 +4224,7 @@ impl AgentPanel {
                     .child(Icon::from_external_svg(icon_path).color(Color::Muted))
             })
             .when(!has_custom_icon, |this| {
-                this.when_some(self.selected_agent.icon(), |this, icon| {
+                this.when_some(self.selected_agent_type.icon(), |this, icon| {
                     this.px_1().child(Icon::new(icon).color(Color::Muted))
                 })
             })
@@ -5230,7 +5234,7 @@ impl AgentPanel {
             name: server.name(),
         };
 
-        self.create_external_thread(
+        self.create_agent_thread(
             server, None, None, None, None, workspace, project, ext_agent, true, window, cx,
         );
     }
@@ -5378,7 +5382,7 @@ mod tests {
             );
         });
 
-        let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
+        let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent_type.clone());
 
         // --- Set up workspace B: ClaudeCode, width=400, no active thread ---
         let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
@@ -5388,7 +5392,7 @@ mod tests {
 
         panel_b.update(cx, |panel, _cx| {
             panel.width = Some(px(400.0));
-            panel.selected_agent = AgentType::Custom {
+            panel.selected_agent_type = AgentType::Custom {
                 name: "claude-acp".into(),
             };
         });
@@ -5421,7 +5425,7 @@ mod tests {
                 "workspace A width should be restored"
             );
             assert_eq!(
-                panel.selected_agent, agent_type_a,
+                panel.selected_agent_type, agent_type_a,
                 "workspace A agent type should be restored"
             );
             assert!(
@@ -5438,7 +5442,7 @@ mod tests {
                 "workspace B width should be restored"
             );
             assert_eq!(
-                panel.selected_agent,
+                panel.selected_agent_type,
                 AgentType::Custom {
                     name: "claude-acp".into()
                 },
@@ -5922,7 +5926,15 @@ mod tests {
 
         // Load thread A back via load_agent_thread — should promote from background.
         panel.update_in(&mut cx, |panel, window, cx| {
-            panel.load_agent_thread(session_id_a.clone(), None, None, window, cx);
+            panel.load_agent_thread(
+                panel.selected_agent().expect("selected agent must be set"),
+                session_id_a.clone(),
+                None,
+                None,
+                true,
+                window,
+                cx,
+            );
         });
 
         // Thread A should now be the active view, promoted from background.

crates/agent_ui/src/sidebar.rs 🔗

@@ -1,5 +1,5 @@
 use crate::threads_archive_view::{ThreadsArchiveView, ThreadsArchiveViewEvent};
-use crate::{AgentPanel, AgentPanelEvent, NewThread};
+use crate::{Agent, AgentPanel, AgentPanelEvent, NewThread};
 use acp_thread::ThreadStatus;
 use action_log::DiffStats;
 use agent::ThreadStore;
@@ -107,6 +107,7 @@ enum ThreadEntryWorkspace {
 
 #[derive(Clone)]
 struct ThreadEntry {
+    agent: Agent,
     session_info: acp_thread::AgentSessionInfo,
     icon: IconName,
     icon_from_external_svg: Option<SharedString>,
@@ -192,7 +193,7 @@ fn root_repository_snapshots(
     workspace: &Entity<Workspace>,
     cx: &App,
 ) -> Vec<project::git_store::RepositorySnapshot> {
-    let (path_list, _) = workspace_path_list_and_label(workspace, cx);
+    let path_list = workspace_path_list(workspace, cx);
     let project = workspace.read(cx).project().read(cx);
     project
         .repositories(cx)
@@ -208,34 +209,23 @@ fn root_repository_snapshots(
         .collect()
 }
 
-fn workspace_path_list_and_label(
-    workspace: &Entity<Workspace>,
-    cx: &App,
-) -> (PathList, SharedString) {
-    let workspace_ref = workspace.read(cx);
-    let mut paths = Vec::new();
-    let mut names = Vec::new();
-
-    for worktree in workspace_ref.worktrees(cx) {
-        let worktree_ref = worktree.read(cx);
-        if !worktree_ref.is_visible() {
-            continue;
-        }
-        let abs_path = worktree_ref.abs_path();
-        paths.push(abs_path.to_path_buf());
+fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList {
+    PathList::new(&workspace.read(cx).root_paths(cx))
+}
+
+fn workspace_label_from_path_list(path_list: &PathList) -> SharedString {
+    let mut names = Vec::with_capacity(path_list.paths().len());
+    for abs_path in path_list.paths() {
         if let Some(name) = abs_path.file_name() {
             names.push(name.to_string_lossy().to_string());
         }
     }
-
-    let label: SharedString = if names.is_empty() {
+    if names.is_empty() {
         // TODO: Can we do something better in this case?
         "Empty Workspace".into()
     } else {
         names.join(", ").into()
-    };
-
-    (PathList::new(&paths), label)
+    }
 }
 
 pub struct Sidebar {
@@ -578,7 +568,8 @@ impl Sidebar {
                 continue;
             }
 
-            let (path_list, label) = workspace_path_list_and_label(workspace, cx);
+            let path_list = workspace_path_list(workspace, cx);
+            let label = workspace_label_from_path_list(&path_list);
 
             let is_collapsed = self.collapsed_groups.contains(&path_list);
             let should_load_threads = !is_collapsed || !query.is_empty();
@@ -592,6 +583,7 @@ impl Sidebar {
                     for meta in thread_store.read(cx).threads_for_paths(&path_list) {
                         seen_session_ids.insert(meta.id.clone());
                         threads.push(ThreadEntry {
+                            agent: Agent::NativeAgent,
                             session_info: meta.into(),
                             icon: IconName::ZedAgent,
                             icon_from_external_svg: None,
@@ -644,6 +636,7 @@ impl Sidebar {
                                 continue;
                             }
                             threads.push(ThreadEntry {
+                                agent: Agent::NativeAgent,
                                 session_info: meta.into(),
                                 icon: IconName::ZedAgent,
                                 icon_from_external_svg: None,
@@ -1222,7 +1215,7 @@ impl Sidebar {
         // contains other folders.
         let mut to_remove: Vec<Entity<Workspace>> = Vec::new();
         for workspace in &workspaces {
-            let (path_list, _) = workspace_path_list_and_label(workspace, cx);
+            let path_list = workspace_path_list(workspace, cx);
             if path_list.paths().len() != 1 {
                 continue;
             }
@@ -1370,10 +1363,17 @@ impl Sidebar {
                 match &thread.workspace {
                     ThreadEntryWorkspace::Open(workspace) => {
                         let workspace = workspace.clone();
-                        self.activate_thread(session_info, &workspace, window, cx);
+                        self.activate_thread(
+                            thread.agent.clone(),
+                            session_info,
+                            &workspace,
+                            window,
+                            cx,
+                        );
                     }
                     ThreadEntryWorkspace::Closed(path_list) => {
                         self.open_workspace_and_activate_thread(
+                            thread.agent.clone(),
                             session_info,
                             path_list.clone(),
                             window,
@@ -1405,6 +1405,7 @@ impl Sidebar {
 
     fn activate_thread(
         &mut self,
+        agent: Agent,
         session_info: acp_thread::AgentSessionInfo,
         workspace: &Entity<Workspace>,
         window: &mut Window,
@@ -1425,18 +1426,23 @@ 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.cwd,
                     session_info.title,
+                    true,
                     window,
                     cx,
                 );
             });
         }
+
+        self.update_entries(cx);
     }
 
     fn open_workspace_and_activate_thread(
         &mut self,
+        agent: Agent,
         session_info: acp_thread::AgentSessionInfo,
         path_list: PathList,
         window: &mut Window,
@@ -1454,13 +1460,69 @@ impl Sidebar {
         cx.spawn_in(window, async move |this, cx| {
             let workspace = open_task.await?;
             this.update_in(cx, |this, window, cx| {
-                this.activate_thread(session_info, &workspace, window, cx);
+                this.activate_thread(agent, session_info, &workspace, window, cx);
             })?;
             anyhow::Ok(())
         })
         .detach_and_log_err(cx);
     }
 
+    fn find_open_workspace_for_path_list(
+        &self,
+        path_list: &PathList,
+        cx: &App,
+    ) -> Option<Entity<Workspace>> {
+        let multi_workspace = self.multi_workspace.upgrade()?;
+        multi_workspace
+            .read(cx)
+            .workspaces()
+            .iter()
+            .find(|workspace| workspace_path_list(workspace, cx).paths() == path_list.paths())
+            .cloned()
+    }
+
+    fn activate_archived_thread(
+        &mut self,
+        agent: Agent,
+        session_info: acp_thread::AgentSessionInfo,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let saved_path_list = ThreadStore::try_global(cx).and_then(|thread_store| {
+            thread_store
+                .read(cx)
+                .thread_from_session_id(&session_info.session_id)
+                .map(|thread| thread.folder_paths.clone())
+        });
+        let path_list = saved_path_list.or_else(|| {
+            // we don't have saved metadata, so create path list based on the cwd
+            session_info
+                .cwd
+                .as_ref()
+                .map(|cwd| PathList::new(&[cwd.to_path_buf()]))
+        });
+
+        if let Some(path_list) = path_list {
+            if let Some(workspace) = self.find_open_workspace_for_path_list(&path_list, cx) {
+                self.activate_thread(agent, session_info, &workspace, window, cx);
+            } else {
+                self.open_workspace_and_activate_thread(agent, session_info, path_list, window, cx);
+            }
+            return;
+        }
+
+        let active_workspace = self.multi_workspace.upgrade().and_then(|w| {
+            w.read(cx)
+                .workspaces()
+                .get(w.read(cx).active_workspace_index())
+                .cloned()
+        });
+
+        if let Some(workspace) = active_workspace {
+            self.activate_thread(agent, session_info, &workspace, window, cx);
+        }
+    }
+
     fn expand_selected_entry(
         &mut self,
         _: &ExpandSelectedEntry,
@@ -1589,22 +1651,32 @@ impl Sidebar {
             .selected(self.focused_thread.as_ref() == Some(&session_info.session_id))
             .focused(is_selected)
             .docked_right(docked_right)
-            .on_click(cx.listener(move |this, _, window, cx| {
-                this.selection = None;
-                match &thread_workspace {
-                    ThreadEntryWorkspace::Open(workspace) => {
-                        this.activate_thread(session_info.clone(), workspace, window, cx);
-                    }
-                    ThreadEntryWorkspace::Closed(path_list) => {
-                        this.open_workspace_and_activate_thread(
-                            session_info.clone(),
-                            path_list.clone(),
-                            window,
-                            cx,
-                        );
+            .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,
+                            );
+                        }
+                        ThreadEntryWorkspace::Closed(path_list) => {
+                            this.open_workspace_and_activate_thread(
+                                agent.clone(),
+                                session_info.clone(),
+                                path_list.clone(),
+                                window,
+                                cx,
+                            );
+                        }
                     }
-                }
-            }))
+                })
+            })
             .into_any_element()
     }
 
@@ -1852,8 +1924,12 @@ impl Sidebar {
                 ThreadsArchiveViewEvent::Close => {
                     this.show_thread_list(window, cx);
                 }
-                ThreadsArchiveViewEvent::OpenThread(_session_info) => {
-                    //TODO: Actually open thread once we support it
+                ThreadsArchiveViewEvent::OpenThread {
+                    agent,
+                    session_info,
+                } => {
+                    this.show_thread_list(window, cx);
+                    this.activate_archived_thread(agent.clone(), session_info.clone(), window, cx);
                 }
             },
         );
@@ -2506,6 +2582,7 @@ mod tests {
                 },
                 // Thread with default (Completed) status, not active
                 ListEntry::Thread(ThreadEntry {
+                    agent: Agent::NativeAgent,
                     session_info: acp_thread::AgentSessionInfo {
                         session_id: acp::SessionId::new(Arc::from("t-1")),
                         cwd: None,
@@ -2527,6 +2604,7 @@ mod tests {
                 }),
                 // Active thread with Running status
                 ListEntry::Thread(ThreadEntry {
+                    agent: Agent::NativeAgent,
                     session_info: acp_thread::AgentSessionInfo {
                         session_id: acp::SessionId::new(Arc::from("t-2")),
                         cwd: None,
@@ -2548,6 +2626,7 @@ mod tests {
                 }),
                 // Active thread with Error status
                 ListEntry::Thread(ThreadEntry {
+                    agent: Agent::NativeAgent,
                     session_info: acp_thread::AgentSessionInfo {
                         session_id: acp::SessionId::new(Arc::from("t-3")),
                         cwd: None,
@@ -2569,6 +2648,7 @@ mod tests {
                 }),
                 // Thread with WaitingForConfirmation status, not active
                 ListEntry::Thread(ThreadEntry {
+                    agent: Agent::NativeAgent,
                     session_info: acp_thread::AgentSessionInfo {
                         session_id: acp::SessionId::new(Arc::from("t-4")),
                         cwd: None,
@@ -2590,6 +2670,7 @@ mod tests {
                 }),
                 // Background thread that completed (should show notification)
                 ListEntry::Thread(ThreadEntry {
+                    agent: Agent::NativeAgent,
                     session_info: acp_thread::AgentSessionInfo {
                         session_id: acp::SessionId::new(Arc::from("t-5")),
                         cwd: None,
@@ -3940,6 +4021,7 @@ mod tests {
         // ── 2. Click thread in workspace A via sidebar ───────────────────────
         sidebar.update_in(cx, |sidebar, window, cx| {
             sidebar.activate_thread(
+                Agent::NativeAgent,
                 acp_thread::AgentSessionInfo {
                     session_id: session_id_a.clone(),
                     cwd: None,
@@ -4007,6 +4089,7 @@ mod tests {
         // which also triggers a workspace switch.
         sidebar.update_in(cx, |sidebar, window, cx| {
             sidebar.activate_thread(
+                Agent::NativeAgent,
                 acp_thread::AgentSessionInfo {
                     session_id: session_id_b.clone(),
                     cwd: None,
@@ -4469,9 +4552,8 @@ mod tests {
             mw.workspaces()[1].clone()
         });
 
-        let (new_path_list, _) = new_workspace.read_with(cx, |_, cx| {
-            workspace_path_list_and_label(&new_workspace, cx)
-        });
+        let new_path_list =
+            new_workspace.read_with(cx, |_, cx| workspace_path_list(&new_workspace, cx));
         assert_eq!(
             new_path_list,
             PathList::new(&[std::path::PathBuf::from("/wt-feature-a")]),
@@ -4593,4 +4675,250 @@ mod tests {
             "clicking an absorbed worktree thread should activate the worktree workspace"
         );
     }
+
+    #[gpui::test]
+    async fn test_activate_archived_thread_with_saved_paths_activates_matching_workspace(
+        cx: &mut TestAppContext,
+    ) {
+        // Thread has saved metadata in ThreadStore. A matching workspace is
+        // already open. Expected: activates the matching workspace.
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/project-a", serde_json::json!({ "src": {} }))
+            .await;
+        fs.insert_tree("/project-b", serde_json::json!({ "src": {} }))
+            .await;
+        cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+
+        let project_a = project::Project::test(fs.clone(), ["/project-a".as_ref()], cx).await;
+        let project_b = project::Project::test(fs.clone(), ["/project-b".as_ref()], cx).await;
+
+        let (multi_workspace, cx) =
+            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
+
+        multi_workspace.update_in(cx, |mw, window, cx| {
+            mw.test_add_workspace(project_b, window, cx);
+        });
+
+        let sidebar = setup_sidebar(&multi_workspace, cx);
+
+        // Save a thread with path_list pointing to project-b.
+        let path_list_b = PathList::new(&[std::path::PathBuf::from("/project-b")]);
+        let session_id = acp::SessionId::new(Arc::from("archived-1"));
+        save_thread_to_store(&session_id, &path_list_b, cx).await;
+
+        // Ensure workspace A is active.
+        multi_workspace.update_in(cx, |mw, window, cx| {
+            mw.activate_index(0, window, cx);
+        });
+        cx.run_until_parked();
+        assert_eq!(
+            multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
+            0
+        );
+
+        // Call activate_archived_thread – should resolve saved paths and
+        // switch to the workspace for project-b.
+        sidebar.update_in(cx, |sidebar, window, cx| {
+            sidebar.activate_archived_thread(
+                Agent::NativeAgent,
+                acp_thread::AgentSessionInfo {
+                    session_id: session_id.clone(),
+                    cwd: Some("/project-b".into()),
+                    title: Some("Archived Thread".into()),
+                    updated_at: None,
+                    created_at: None,
+                    meta: None,
+                },
+                window,
+                cx,
+            );
+        });
+        cx.run_until_parked();
+
+        assert_eq!(
+            multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
+            1,
+            "should have activated the workspace matching the saved path_list"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_activate_archived_thread_cwd_fallback_with_matching_workspace(
+        cx: &mut TestAppContext,
+    ) {
+        // Thread has no saved metadata but session_info has cwd. A matching
+        // workspace is open. Expected: uses cwd to find and activate it.
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/project-a", serde_json::json!({ "src": {} }))
+            .await;
+        fs.insert_tree("/project-b", serde_json::json!({ "src": {} }))
+            .await;
+        cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+
+        let project_a = project::Project::test(fs.clone(), ["/project-a".as_ref()], cx).await;
+        let project_b = project::Project::test(fs.clone(), ["/project-b".as_ref()], cx).await;
+
+        let (multi_workspace, cx) =
+            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
+
+        multi_workspace.update_in(cx, |mw, window, cx| {
+            mw.test_add_workspace(project_b, window, cx);
+        });
+
+        let sidebar = setup_sidebar(&multi_workspace, cx);
+
+        // Start with workspace A active.
+        multi_workspace.update_in(cx, |mw, window, cx| {
+            mw.activate_index(0, window, cx);
+        });
+        cx.run_until_parked();
+        assert_eq!(
+            multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
+            0
+        );
+
+        // 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 {
+                    session_id: acp::SessionId::new(Arc::from("unknown-session")),
+                    cwd: Some(std::path::PathBuf::from("/project-b")),
+                    title: Some("CWD Thread".into()),
+                    updated_at: None,
+                    created_at: None,
+                    meta: None,
+                },
+                window,
+                cx,
+            );
+        });
+        cx.run_until_parked();
+
+        assert_eq!(
+            multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
+            1,
+            "should have activated the workspace matching the cwd"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_activate_archived_thread_no_paths_no_cwd_uses_active_workspace(
+        cx: &mut TestAppContext,
+    ) {
+        // Thread has no saved metadata and no cwd. Expected: falls back to
+        // the currently active workspace.
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/project-a", serde_json::json!({ "src": {} }))
+            .await;
+        fs.insert_tree("/project-b", serde_json::json!({ "src": {} }))
+            .await;
+        cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+
+        let project_a = project::Project::test(fs.clone(), ["/project-a".as_ref()], cx).await;
+        let project_b = project::Project::test(fs.clone(), ["/project-b".as_ref()], cx).await;
+
+        let (multi_workspace, cx) =
+            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
+
+        multi_workspace.update_in(cx, |mw, window, cx| {
+            mw.test_add_workspace(project_b, window, cx);
+        });
+
+        let sidebar = setup_sidebar(&multi_workspace, cx);
+
+        // Activate workspace B (index 1) to make it the active one.
+        multi_workspace.update_in(cx, |mw, window, cx| {
+            mw.activate_index(1, window, cx);
+        });
+        cx.run_until_parked();
+        assert_eq!(
+            multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
+            1
+        );
+
+        // 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 {
+                    session_id: acp::SessionId::new(Arc::from("no-context-session")),
+                    cwd: None,
+                    title: Some("Contextless Thread".into()),
+                    updated_at: None,
+                    created_at: None,
+                    meta: None,
+                },
+                window,
+                cx,
+            );
+        });
+        cx.run_until_parked();
+
+        assert_eq!(
+            multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
+            1,
+            "should have stayed on the active workspace when no path info is available"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_activate_archived_thread_saved_paths_opens_new_workspace(
+        cx: &mut TestAppContext,
+    ) {
+        // Thread has saved metadata pointing to a path with no open workspace.
+        // Expected: opens a new workspace for that path.
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/project-a", serde_json::json!({ "src": {} }))
+            .await;
+        fs.insert_tree("/project-b", serde_json::json!({ "src": {} }))
+            .await;
+        cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+
+        let project_a = project::Project::test(fs.clone(), ["/project-a".as_ref()], cx).await;
+
+        let (multi_workspace, cx) =
+            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
+
+        let sidebar = setup_sidebar(&multi_workspace, cx);
+
+        // Save a thread with path_list pointing to project-b – which has no
+        // open workspace.
+        let path_list_b = PathList::new(&[std::path::PathBuf::from("/project-b")]);
+        let session_id = acp::SessionId::new(Arc::from("archived-new-ws"));
+        save_thread_to_store(&session_id, &path_list_b, cx).await;
+
+        assert_eq!(
+            multi_workspace.read_with(cx, |mw, _| mw.workspaces().len()),
+            1,
+            "should start with one workspace"
+        );
+
+        sidebar.update_in(cx, |sidebar, window, cx| {
+            sidebar.activate_archived_thread(
+                Agent::NativeAgent,
+                acp_thread::AgentSessionInfo {
+                    session_id: session_id.clone(),
+                    cwd: None,
+                    title: Some("New WS Thread".into()),
+                    updated_at: None,
+                    created_at: None,
+                    meta: None,
+                },
+                window,
+                cx,
+            );
+        });
+        cx.run_until_parked();
+
+        assert_eq!(
+            multi_workspace.read_with(cx, |mw, _| mw.workspaces().len()),
+            2,
+            "should have opened a second workspace for the archived thread's saved paths"
+        );
+    }
 }

crates/agent_ui/src/thread_history_view.rs 🔗

@@ -751,13 +751,17 @@ impl RenderOnce for HistoryEntryElement {
                     {
                         if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
                             panel.update(cx, |panel, cx| {
-                                panel.load_agent_thread(
-                                    entry.session_id.clone(),
-                                    entry.cwd.clone(),
-                                    entry.title.clone(),
-                                    window,
-                                    cx,
-                                );
+                                if let Some(agent) = panel.selected_agent() {
+                                    panel.load_agent_thread(
+                                        agent,
+                                        entry.session_id.clone(),
+                                        entry.cwd.clone(),
+                                        entry.title.clone(),
+                                        true,
+                                        window,
+                                        cx,
+                                    );
+                                }
                             });
                         }
                     }

crates/agent_ui/src/threads_archive_view.rs 🔗

@@ -89,7 +89,10 @@ fn fuzzy_match_positions(query: &str, text: &str) -> Option<Vec<usize>> {
 
 pub enum ThreadsArchiveViewEvent {
     Close,
-    OpenThread(AgentSessionInfo),
+    OpenThread {
+        agent: Agent,
+        session_info: AgentSessionInfo,
+    },
 }
 
 impl EventEmitter<ThreadsArchiveViewEvent> for ThreadsArchiveView {}
@@ -263,7 +266,10 @@ impl ThreadsArchiveView {
     ) {
         self.selection = None;
         self.reset_filter_editor_text(window, cx);
-        cx.emit(ThreadsArchiveViewEvent::OpenThread(session_info));
+        cx.emit(ThreadsArchiveViewEvent::OpenThread {
+            agent: self.selected_agent.clone(),
+            session_info,
+        });
     }
 
     fn is_selectable_item(&self, ix: usize) -> bool {
@@ -413,7 +419,6 @@ impl ThreadsArchiveView {
 
                 ListItem::new(id)
                     .toggle_state(is_selected)
-                    .disabled(true)
                     .child(
                         h_flex()
                             .min_w_0()

crates/agent_ui/src/ui/mention_crease.rs 🔗

@@ -13,6 +13,8 @@ use theme::ThemeSettings;
 use ui::{ButtonLike, TintColor, Tooltip, prelude::*};
 use workspace::{OpenOptions, Workspace};
 
+use crate::Agent;
+
 #[derive(IntoElement)]
 pub struct MentionCrease {
     id: ElementId,
@@ -275,8 +277,17 @@ fn open_thread(
         return;
     };
 
+    // Right now we only support loading threads in the native agent
     panel.update(cx, |panel, cx| {
-        panel.load_agent_thread(id, None, Some(name.into()), window, cx)
+        panel.load_agent_thread(
+            Agent::NativeAgent,
+            id,
+            None,
+            Some(name.into()),
+            true,
+            window,
+            cx,
+        )
     });
 }