sidebar: Load archived threads in the correct window (#51831)
Make sure we don't create a duplicate workspace if it is already open in another window. Release Notes: - N/A --------- Co-authored-by: cameron <cameron.studdstreet@gmail.com>
This commit is contained in:
1 parent
3af7b644a6
commit
eba7cea22a
1 file changed
+374
-30
+374
-30
@@ -10,7 +10,7 @@ use editor::Editor;
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _};
|
||||
use gpui::{
|
||||
Action as _, AnyElement, App, Context, Entity, FocusHandle, Focusable, ListState, Pixels,
|
||||
Render, SharedString, WeakEntity, Window, actions, list, prelude::*, px,
|
||||
Render, SharedString, WeakEntity, Window, WindowHandle, actions, list, prelude::*, px,
|
||||
};
|
||||
use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use project::{AgentId, Event as ProjectEvent};
|
||||
@@ -1656,7 +1656,68 @@ impl Sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_thread(
|
||||
fn find_workspace_across_windows(
|
||||
&self,
|
||||
cx: &App,
|
||||
predicate: impl Fn(&Entity<Workspace>, &App) -> bool,
|
||||
) -> Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> {
|
||||
cx.windows()
|
||||
.into_iter()
|
||||
.filter_map(|window| window.downcast::<MultiWorkspace>())
|
||||
.find_map(|window| {
|
||||
let workspace = window.read(cx).ok().and_then(|multi_workspace| {
|
||||
multi_workspace
|
||||
.workspaces()
|
||||
.iter()
|
||||
.find(|workspace| predicate(workspace, cx))
|
||||
.cloned()
|
||||
})?;
|
||||
Some((window, workspace))
|
||||
})
|
||||
}
|
||||
|
||||
fn find_workspace_in_current_window(
|
||||
&self,
|
||||
cx: &App,
|
||||
predicate: impl Fn(&Entity<Workspace>, &App) -> bool,
|
||||
) -> Option<Entity<Workspace>> {
|
||||
self.multi_workspace.upgrade().and_then(|multi_workspace| {
|
||||
multi_workspace
|
||||
.read(cx)
|
||||
.workspaces()
|
||||
.iter()
|
||||
.find(|workspace| predicate(workspace, cx))
|
||||
.cloned()
|
||||
})
|
||||
}
|
||||
|
||||
fn load_agent_thread_in_workspace(
|
||||
workspace: &Entity<Workspace>,
|
||||
agent: Agent,
|
||||
session_info: acp_thread::AgentSessionInfo,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_panel::<AgentPanel>(window, cx);
|
||||
});
|
||||
|
||||
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,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_thread_locally(
|
||||
&mut self,
|
||||
agent: Agent,
|
||||
session_info: acp_thread::AgentSessionInfo,
|
||||
@@ -1677,27 +1738,72 @@ impl Sidebar {
|
||||
multi_workspace.activate(workspace.clone(), cx);
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_panel::<AgentPanel>(window, cx);
|
||||
});
|
||||
|
||||
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,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
Self::load_agent_thread_in_workspace(workspace, agent, session_info, window, cx);
|
||||
|
||||
self.update_entries(false, cx);
|
||||
}
|
||||
|
||||
fn activate_thread_in_other_window(
|
||||
&self,
|
||||
agent: Agent,
|
||||
session_info: acp_thread::AgentSessionInfo,
|
||||
workspace: Entity<Workspace>,
|
||||
target_window: WindowHandle<MultiWorkspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let target_session_id = session_info.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, window, cx);
|
||||
})
|
||||
.log_err()
|
||||
.is_some();
|
||||
|
||||
if activated {
|
||||
if let Some(target_sidebar) = target_window
|
||||
.read(cx)
|
||||
.ok()
|
||||
.and_then(|multi_workspace| {
|
||||
multi_workspace.sidebar().map(|sidebar| sidebar.to_any())
|
||||
})
|
||||
.and_then(|sidebar| sidebar.downcast::<Self>().ok())
|
||||
{
|
||||
target_sidebar.update(cx, |sidebar, cx| {
|
||||
sidebar.focused_thread = Some(target_session_id);
|
||||
sidebar.update_entries(false, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_thread(
|
||||
&mut self,
|
||||
agent: Agent,
|
||||
session_info: acp_thread::AgentSessionInfo,
|
||||
workspace: &Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self
|
||||
.find_workspace_in_current_window(cx, |candidate, _| candidate == workspace)
|
||||
.is_some()
|
||||
{
|
||||
self.activate_thread_locally(agent, session_info, &workspace, window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some((target_window, workspace)) =
|
||||
self.find_workspace_across_windows(cx, |candidate, _| candidate == workspace)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.activate_thread_in_other_window(agent, session_info, workspace, target_window, cx);
|
||||
}
|
||||
|
||||
fn open_workspace_and_activate_thread(
|
||||
&mut self,
|
||||
agent: Agent,
|
||||
@@ -1725,18 +1831,24 @@ impl Sidebar {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn find_open_workspace_for_path_list(
|
||||
fn find_current_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()
|
||||
self.find_workspace_in_current_window(cx, |workspace, cx| {
|
||||
workspace_path_list(workspace, cx).paths() == path_list.paths()
|
||||
})
|
||||
}
|
||||
|
||||
fn find_open_workspace_for_path_list(
|
||||
&self,
|
||||
path_list: &PathList,
|
||||
cx: &App,
|
||||
) -> Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> {
|
||||
self.find_workspace_across_windows(cx, |workspace, cx| {
|
||||
workspace_path_list(workspace, cx).paths() == path_list.paths()
|
||||
})
|
||||
}
|
||||
|
||||
fn activate_archived_thread(
|
||||
@@ -1747,8 +1859,18 @@ impl Sidebar {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(path_list) = &session_info.work_dirs {
|
||||
if let Some(workspace) = self.find_open_workspace_for_path_list(&path_list, cx) {
|
||||
self.activate_thread(agent, session_info, &workspace, window, cx);
|
||||
if let Some(workspace) = self.find_current_workspace_for_path_list(path_list, cx) {
|
||||
self.activate_thread_locally(agent, session_info, &workspace, window, cx);
|
||||
} else if let Some((target_window, workspace)) =
|
||||
self.find_open_workspace_for_path_list(path_list, cx)
|
||||
{
|
||||
self.activate_thread_in_other_window(
|
||||
agent,
|
||||
session_info,
|
||||
workspace,
|
||||
target_window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
let path_list = path_list.clone();
|
||||
self.open_workspace_and_activate_thread(agent, session_info, path_list, window, cx);
|
||||
@@ -1764,7 +1886,7 @@ impl Sidebar {
|
||||
});
|
||||
|
||||
if let Some(workspace) = active_workspace {
|
||||
self.activate_thread(agent, session_info, &workspace, window, cx);
|
||||
self.activate_thread_locally(agent, session_info, &workspace, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5500,4 +5622,226 @@ mod tests {
|
||||
"should have opened a second workspace for the archived thread's saved paths"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_activate_archived_thread_reuses_workspace_in_another_window(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
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_a =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
|
||||
let multi_workspace_b =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project_b, window, cx));
|
||||
|
||||
let multi_workspace_a_entity = multi_workspace_a.root(cx).unwrap();
|
||||
|
||||
let cx_a = &mut gpui::VisualTestContext::from_window(multi_workspace_a.into(), cx);
|
||||
let sidebar = setup_sidebar(&multi_workspace_a_entity, cx_a);
|
||||
|
||||
let session_id = acp::SessionId::new(Arc::from("archived-cross-window"));
|
||||
|
||||
sidebar.update_in(cx_a, |sidebar, window, cx| {
|
||||
sidebar.activate_archived_thread(
|
||||
Agent::NativeAgent,
|
||||
acp_thread::AgentSessionInfo {
|
||||
session_id: session_id.clone(),
|
||||
work_dirs: Some(PathList::new(&[PathBuf::from("/project-b")])),
|
||||
title: Some("Cross Window Thread".into()),
|
||||
updated_at: None,
|
||||
created_at: None,
|
||||
meta: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
multi_workspace_a
|
||||
.read_with(cx_a, |mw, _| mw.workspaces().len())
|
||||
.unwrap(),
|
||||
1,
|
||||
"should not add the other window's workspace into the current window"
|
||||
);
|
||||
assert_eq!(
|
||||
multi_workspace_b
|
||||
.read_with(cx_a, |mw, _| mw.workspaces().len())
|
||||
.unwrap(),
|
||||
1,
|
||||
"should reuse the existing workspace in the other window"
|
||||
);
|
||||
assert!(
|
||||
cx_a.read(|cx| cx.active_window().unwrap()) == *multi_workspace_b,
|
||||
"should activate the window that already owns the matching workspace"
|
||||
);
|
||||
sidebar.read_with(cx_a, |sidebar, _| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread, None,
|
||||
"source window's sidebar should not eagerly claim focus for a thread opened in another window"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_activate_archived_thread_reuses_workspace_in_another_window_with_target_sidebar(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
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_a =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
|
||||
let multi_workspace_b =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project_b.clone(), window, cx));
|
||||
|
||||
let multi_workspace_a_entity = multi_workspace_a.root(cx).unwrap();
|
||||
let multi_workspace_b_entity = multi_workspace_b.root(cx).unwrap();
|
||||
|
||||
let cx_a = &mut gpui::VisualTestContext::from_window(multi_workspace_a.into(), cx);
|
||||
let sidebar_a = setup_sidebar(&multi_workspace_a_entity, cx_a);
|
||||
|
||||
let cx_b = &mut gpui::VisualTestContext::from_window(multi_workspace_b.into(), cx);
|
||||
let sidebar_b = setup_sidebar(&multi_workspace_b_entity, cx_b);
|
||||
let workspace_b = multi_workspace_b_entity.read_with(cx_b, |mw, _| mw.workspace().clone());
|
||||
let _panel_b = add_agent_panel(&workspace_b, &project_b, cx_b);
|
||||
|
||||
let session_id = acp::SessionId::new(Arc::from("archived-cross-window-with-sidebar"));
|
||||
|
||||
sidebar_a.update_in(cx_a, |sidebar, window, cx| {
|
||||
sidebar.activate_archived_thread(
|
||||
Agent::NativeAgent,
|
||||
acp_thread::AgentSessionInfo {
|
||||
session_id: session_id.clone(),
|
||||
work_dirs: Some(PathList::new(&[PathBuf::from("/project-b")])),
|
||||
title: Some("Cross Window Thread".into()),
|
||||
updated_at: None,
|
||||
created_at: None,
|
||||
meta: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
multi_workspace_a
|
||||
.read_with(cx_a, |mw, _| mw.workspaces().len())
|
||||
.unwrap(),
|
||||
1,
|
||||
"should not add the other window's workspace into the current window"
|
||||
);
|
||||
assert_eq!(
|
||||
multi_workspace_b
|
||||
.read_with(cx_a, |mw, _| mw.workspaces().len())
|
||||
.unwrap(),
|
||||
1,
|
||||
"should reuse the existing workspace in the other window"
|
||||
);
|
||||
assert!(
|
||||
cx_a.read(|cx| cx.active_window().unwrap()) == *multi_workspace_b,
|
||||
"should activate the window that already owns the matching workspace"
|
||||
);
|
||||
sidebar_a.read_with(cx_a, |sidebar, _| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread, None,
|
||||
"source window's sidebar should not eagerly claim focus for a thread opened in another window"
|
||||
);
|
||||
});
|
||||
sidebar_b.read_with(cx_b, |sidebar, _| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread.as_ref(),
|
||||
Some(&session_id),
|
||||
"target window's sidebar should eagerly focus the activated archived thread"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_activate_archived_thread_prefers_current_window_for_matching_paths(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/project-a", serde_json::json!({ "src": {} }))
|
||||
.await;
|
||||
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
|
||||
|
||||
let project_b = project::Project::test(fs.clone(), ["/project-a".as_ref()], cx).await;
|
||||
let project_a = project::Project::test(fs.clone(), ["/project-a".as_ref()], cx).await;
|
||||
|
||||
let multi_workspace_b =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project_b, window, cx));
|
||||
let multi_workspace_a =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
|
||||
|
||||
let multi_workspace_a_entity = multi_workspace_a.root(cx).unwrap();
|
||||
|
||||
let cx_a = &mut gpui::VisualTestContext::from_window(multi_workspace_a.into(), cx);
|
||||
let sidebar_a = setup_sidebar(&multi_workspace_a_entity, cx_a);
|
||||
|
||||
let session_id = acp::SessionId::new(Arc::from("archived-current-window"));
|
||||
|
||||
sidebar_a.update_in(cx_a, |sidebar, window, cx| {
|
||||
sidebar.activate_archived_thread(
|
||||
Agent::NativeAgent,
|
||||
acp_thread::AgentSessionInfo {
|
||||
session_id: session_id.clone(),
|
||||
work_dirs: Some(PathList::new(&[PathBuf::from("/project-a")])),
|
||||
title: Some("Current Window Thread".into()),
|
||||
updated_at: None,
|
||||
created_at: None,
|
||||
meta: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
|
||||
assert!(
|
||||
cx_a.read(|cx| cx.active_window().unwrap()) == *multi_workspace_a,
|
||||
"should keep activation in the current window when it already has a matching workspace"
|
||||
);
|
||||
sidebar_a.read_with(cx_a, |sidebar, _| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread.as_ref(),
|
||||
Some(&session_id),
|
||||
"current window's sidebar should eagerly focus the activated archived thread"
|
||||
);
|
||||
});
|
||||
assert_eq!(
|
||||
multi_workspace_a
|
||||
.read_with(cx_a, |mw, _| mw.workspaces().len())
|
||||
.unwrap(),
|
||||
1,
|
||||
"current window should continue reusing its existing workspace"
|
||||
);
|
||||
assert_eq!(
|
||||
multi_workspace_b
|
||||
.read_with(cx_a, |mw, _| mw.workspaces().len())
|
||||
.unwrap(),
|
||||
1,
|
||||
"other windows should not be activated just because they also match the saved paths"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user