Detailed changes
@@ -82,7 +82,7 @@ use ui::{
use util::{ResultExt as _, debug_panic};
use workspace::{
CollaboratorId, DraggedSelection, DraggedTab, OpenResult, PathList, SerializedPathList,
- ToggleZoom, ToolbarItemView, Workspace, WorkspaceId,
+ ToggleWorkspaceSidebar, ToggleZoom, ToolbarItemView, Workspace, WorkspaceId,
dock::{DockPosition, Panel, PanelEvent},
};
use zed_actions::{
@@ -3435,6 +3435,7 @@ impl AgentPanel {
.action("Profiles", Box::new(ManageProfiles::default()))
.action("Settings", Box::new(OpenSettings))
.separator()
+ .action("Toggle Threads Sidebar", Box::new(ToggleWorkspaceSidebar))
.action(full_screen_label, Box::new(ToggleZoom));
if has_auth_methods {
@@ -999,7 +999,10 @@ impl ThreadView {
let text: String = contents
.iter()
.filter_map(|block| match block {
- acp::ContentBlock::Text(text_content) => Some(text_content.text.as_str()),
+ acp::ContentBlock::Text(text_content) => Some(text_content.text.clone()),
+ acp::ContentBlock::ResourceLink(resource_link) => {
+ Some(format!("@{}", resource_link.name))
+ }
_ => None,
})
.collect::<Vec<_>>()
@@ -5,6 +5,7 @@ mod remote_servers;
mod ssh_config;
use std::{
+ collections::HashSet,
path::{Path, PathBuf},
sync::Arc,
};
@@ -547,6 +548,7 @@ impl RecentProjects {
create_new_window,
focus_handle,
open_folders,
+ HashSet::new(),
project_connection_options,
ProjectPickerStyle::Modal,
);
@@ -557,6 +559,7 @@ impl RecentProjects {
pub fn popover(
workspace: WeakEntity<Workspace>,
+ excluded_workspace_ids: HashSet<WorkspaceId>,
create_new_window: bool,
focus_handle: FocusHandle,
window: &mut Window,
@@ -580,6 +583,7 @@ impl RecentProjects {
create_new_window,
focus_handle,
open_folders,
+ excluded_workspace_ids,
project_connection_options,
ProjectPickerStyle::Popover,
);
@@ -627,6 +631,7 @@ impl Render for RecentProjects {
pub struct RecentProjectsDelegate {
workspace: WeakEntity<Workspace>,
open_folders: Vec<OpenFolderEntry>,
+ excluded_workspace_ids: HashSet<WorkspaceId>,
workspaces: Vec<(
WorkspaceId,
SerializedWorkspaceLocation,
@@ -652,6 +657,7 @@ impl RecentProjectsDelegate {
create_new_window: bool,
focus_handle: FocusHandle,
open_folders: Vec<OpenFolderEntry>,
+ excluded_workspace_ids: HashSet<WorkspaceId>,
project_connection_options: Option<RemoteConnectionOptions>,
style: ProjectPickerStyle,
) -> Self {
@@ -659,6 +665,7 @@ impl RecentProjectsDelegate {
Self {
workspace,
open_folders,
+ excluded_workspace_ids,
workspaces: Vec::new(),
filtered_entries: Vec::new(),
selected_index: 0,
@@ -1546,6 +1553,10 @@ impl RecentProjectsDelegate {
workspace_id: WorkspaceId,
cx: &mut Context<Picker<Self>>,
) -> bool {
+ if self.excluded_workspace_ids.contains(&workspace_id) {
+ return true;
+ }
+
if let Some(workspace) = self.workspace.upgrade() {
let workspace = workspace.read(cx);
if Some(workspace_id) == workspace.database_id() {
@@ -30,7 +30,7 @@ use util::ResultExt as _;
use util::path_list::PathList;
use workspace::{
FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, Sidebar as WorkspaceSidebar,
- ToggleWorkspaceSidebar, Workspace,
+ ToggleWorkspaceSidebar, Workspace, WorkspaceId,
};
use zed_actions::OpenRecent;
@@ -268,10 +268,6 @@ impl Sidebar {
window,
|this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event {
MultiWorkspaceEvent::ActiveWorkspaceChanged => {
- // Don't clear focused_thread when the active workspace
- // changed because a workspace was removed — the focused
- // thread may still be valid in the new active workspace.
- // Only clear it for explicit user-initiated switches.
if mem::take(&mut this.pending_workspace_removal) {
// If the removed workspace had no focused thread, seed
// from the new active panel so its current thread gets
@@ -288,7 +284,21 @@ impl Sidebar {
}
}
} else {
- this.focused_thread = None;
+ // Seed focused_thread from the new active panel so
+ // the sidebar highlights the correct thread.
+ this.focused_thread = this
+ .multi_workspace
+ .upgrade()
+ .and_then(|mw| {
+ let ws = mw.read(cx).workspace();
+ ws.read(cx).panel::<AgentPanel>(cx)
+ })
+ .and_then(|panel| {
+ panel
+ .read(cx)
+ .active_conversation()
+ .and_then(|cv| cv.read(cx).parent_id(cx))
+ });
}
this.observe_draft_editor(cx);
this.update_entries(false, cx);
@@ -1423,6 +1433,10 @@ impl Sidebar {
}
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ if matches!(self.view, SidebarView::Archive) {
+ return;
+ }
+
if self.selection.is_none() {
self.filter_editor.focus_handle(cx).focus(window, cx);
}
@@ -1605,6 +1619,11 @@ impl Sidebar {
return;
};
+ // 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());
+
multi_workspace.update(cx, |multi_workspace, cx| {
multi_workspace.activate(workspace.clone(), cx);
});
@@ -2070,9 +2089,10 @@ impl Sidebar {
}
fn render_recent_projects_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
- let workspace = self
- .multi_workspace
- .upgrade()
+ let multi_workspace = self.multi_workspace.upgrade();
+
+ let workspace = multi_workspace
+ .as_ref()
.map(|mw| mw.read(cx).workspace().downgrade());
let focus_handle = workspace
@@ -2081,13 +2101,31 @@ impl Sidebar {
.map(|w| w.read(cx).focus_handle(cx))
.unwrap_or_else(|| cx.focus_handle());
+ let excluded_workspace_ids: HashSet<WorkspaceId> = multi_workspace
+ .as_ref()
+ .map(|mw| {
+ mw.read(cx)
+ .workspaces()
+ .iter()
+ .filter_map(|ws| ws.read(cx).database_id())
+ .collect()
+ })
+ .unwrap_or_default();
+
let popover_handle = self.recent_projects_popover_handle.clone();
PopoverMenu::new("sidebar-recent-projects-menu")
.with_handle(popover_handle)
.menu(move |window, cx| {
workspace.as_ref().map(|ws| {
- RecentProjects::popover(ws.clone(), false, focus_handle.clone(), window, cx)
+ RecentProjects::popover(
+ ws.clone(),
+ excluded_workspace_ids.clone(),
+ false,
+ focus_handle.clone(),
+ window,
+ cx,
+ )
})
})
.trigger_with_tooltip(
@@ -2300,9 +2338,11 @@ impl Sidebar {
.child(
h_flex()
.gap_1()
- .when(self.selection.is_some(), |this| {
- this.child(KeyBinding::for_action(&FocusSidebarFilter, cx))
- })
+ .when(
+ self.selection.is_some()
+ && !self.filter_editor.focus_handle(cx).is_focused(window),
+ |this| this.child(KeyBinding::for_action(&FocusSidebarFilter, cx)),
+ )
.when(has_query, |this| {
this.child(
IconButton::new("clear_filter", IconName::Close)
@@ -4594,12 +4634,16 @@ mod tests {
sidebar.read_with(cx, |sidebar, _cx| {
assert_eq!(
- sidebar.focused_thread, None,
- "External workspace switch should clear focused_thread"
+ sidebar.focused_thread.as_ref(),
+ Some(&session_id_a),
+ "Switching workspace should seed focused_thread from the new active panel"
);
- assert_eq!(
- sidebar.active_entry_index, None,
- "No active entry when no thread is focused"
+ let active_entry = sidebar
+ .active_entry_index
+ .and_then(|ix| sidebar.contents.entries.get(ix));
+ assert!(
+ matches!(active_entry, Some(ListEntry::Thread(thread)) if thread.session_info.session_id == session_id_a),
+ "Active entry should be the seeded thread"
);
});
@@ -4619,8 +4663,9 @@ mod tests {
// the selection highlight to jump around.
sidebar.read_with(cx, |sidebar, _cx| {
assert_eq!(
- sidebar.focused_thread, None,
- "Opening a thread in a non-active panel should not set focused_thread"
+ sidebar.focused_thread.as_ref(),
+ Some(&session_id_a),
+ "Opening a thread in a non-active panel should not change focused_thread"
);
});
@@ -4631,8 +4676,9 @@ mod tests {
sidebar.read_with(cx, |sidebar, _cx| {
assert_eq!(
- sidebar.focused_thread, None,
- "Defocusing the sidebar should not set focused_thread"
+ sidebar.focused_thread.as_ref(),
+ Some(&session_id_a),
+ "Defocusing the sidebar should not change focused_thread"
);
});
@@ -4647,19 +4693,23 @@ mod tests {
sidebar.read_with(cx, |sidebar, _cx| {
assert_eq!(
- sidebar.focused_thread, None,
- "Switching workspace should clear focused_thread"
+ sidebar.focused_thread.as_ref(),
+ Some(&session_id_b2),
+ "Switching workspace should seed focused_thread from the new active panel"
);
- assert_eq!(
- sidebar.active_entry_index, None,
- "No active entry when no thread is focused"
+ let active_entry = sidebar
+ .active_entry_index
+ .and_then(|ix| sidebar.contents.entries.get(ix));
+ assert!(
+ matches!(active_entry, Some(ListEntry::Thread(thread)) if thread.session_info.session_id == session_id_b2),
+ "Active entry should be the seeded thread"
);
});
- // ── 8. Focusing the agent panel thread restores focused_thread ────
+ // ── 8. Focusing the agent panel thread keeps focused_thread ────
// Workspace B still has session_id_b2 loaded in the agent panel.
// Clicking into the thread (simulated by focusing its view) should
- // set focused_thread via the ThreadFocused event.
+ // keep focused_thread since it was already seeded on workspace switch.
panel_b.update_in(cx, |panel, window, cx| {
if let Some(thread_view) = panel.active_conversation_view() {
thread_view.read(cx).focus_handle(cx).focus(window, cx);
@@ -37,6 +37,7 @@ use project::{
use remote::RemoteConnectionOptions;
use settings::Settings;
use settings::WorktreeId;
+use std::collections::HashSet;
use std::sync::Arc;
use theme::ActiveTheme;
use title_bar_settings::TitleBarSettings;
@@ -47,7 +48,7 @@ use ui::{
use update_version::UpdateVersion;
use util::ResultExt;
use workspace::{
- MultiWorkspace, ToggleWorkspaceSidebar, ToggleWorktreeSecurity, Workspace,
+ MultiWorkspace, ToggleWorkspaceSidebar, ToggleWorktreeSecurity, Workspace, WorkspaceId,
notifications::NotifyResultExt,
};
use zed_actions::OpenRemote;
@@ -761,10 +762,24 @@ impl TitleBar {
.map(|w| w.read(cx).focus_handle(cx))
.unwrap_or_else(|| cx.focus_handle());
+ let excluded_workspace_ids: HashSet<WorkspaceId> = self
+ .multi_workspace
+ .as_ref()
+ .and_then(|mw| mw.upgrade())
+ .map(|mw| {
+ mw.read(cx)
+ .workspaces()
+ .iter()
+ .filter_map(|ws| ws.read(cx).database_id())
+ .collect()
+ })
+ .unwrap_or_default();
+
PopoverMenu::new("recent-projects-menu")
.menu(move |window, cx| {
Some(recent_projects::RecentProjects::popover(
workspace.clone(),
+ excluded_workspace_ids.clone(),
false,
focus_handle.clone(),
window,
@@ -654,8 +654,10 @@ impl MultiWorkspace {
self.pending_removal_tasks.retain(|task| !task.is_ready());
self.pending_removal_tasks
.push(cx.background_spawn(async move {
+ // Clear the session binding instead of deleting the row so
+ // the workspace still appears in the recent-projects list.
crate::persistence::DB
- .delete_workspace_by_id(workspace_id)
+ .set_session_binding(workspace_id, None, None)
.await
.log_err();
}));
@@ -4106,7 +4106,7 @@ mod tests {
}
#[gpui::test]
- async fn test_remove_workspace_deletes_db_row(cx: &mut gpui::TestAppContext) {
+ async fn test_remove_workspace_clears_session_binding(cx: &mut gpui::TestAppContext) {
use crate::multi_workspace::MultiWorkspace;
use feature_flags::FeatureFlagAppExt;
use gpui::AppContext as _;
@@ -4170,10 +4170,25 @@ mod tests {
cx.run_until_parked();
- // The row should be deleted, not just have session_id cleared.
+ // The row should still exist so it continues to appear in recent
+ // projects, but the session binding should be cleared so it is not
+ // restored as part of any future session.
assert!(
- DB.workspace_for_id(workspace2_db_id).is_none(),
- "Removed workspace's DB row should be deleted entirely"
+ DB.workspace_for_id(workspace2_db_id).is_some(),
+ "Removed workspace's DB row should be preserved for recent projects"
+ );
+
+ let session_workspaces = DB
+ .last_session_workspace_locations("remove-test-session", None, fs.as_ref())
+ .await
+ .unwrap();
+ let restored_ids: Vec<WorkspaceId> = session_workspaces
+ .iter()
+ .map(|sw| sw.workspace_id)
+ .collect();
+ assert!(
+ !restored_ids.contains(&workspace2_db_id),
+ "Removed workspace should not appear in session restoration"
);
}
@@ -4361,10 +4376,24 @@ mod tests {
});
futures::future::join_all(all_tasks).await;
- // After awaiting, the DB row should be deleted.
+ // The row should still exist (for recent projects), but the session
+ // binding should have been cleared by the pending removal task.
+ assert!(
+ DB.workspace_for_id(workspace2_db_id).is_some(),
+ "Workspace row should be preserved for recent projects"
+ );
+
+ let session_workspaces = DB
+ .last_session_workspace_locations("pending-removal-session", None, fs.as_ref())
+ .await
+ .unwrap();
+ let restored_ids: Vec<WorkspaceId> = session_workspaces
+ .iter()
+ .map(|sw| sw.workspace_id)
+ .collect();
assert!(
- DB.workspace_for_id(workspace2_db_id).is_none(),
- "Pending removal task should have deleted the workspace row when awaited"
+ !restored_ids.contains(&workspace2_db_id),
+ "Pending removal task should have cleared the session binding"
);
}