@@ -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.
@@ -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"
+ );
+ }
}