From b5666319b4409ca882ace3ad8baaab513e5f3a8c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 Mar 2026 23:45:55 -0700 Subject: [PATCH] Move threads sidebar into agent panel (#51241) * [x] Put back persistence of sidebar open state * [x] when agent panel is docked right, put sidebar on the right side * [x] remove stale entries from `SidebarsByWindow` Release Notes: - N/A --------- Co-authored-by: Eric Holk Co-authored-by: Mikayla Maki Co-authored-by: Anthony Eid --- Cargo.lock | 29 - Cargo.toml | 3 - crates/agent_ui/Cargo.toml | 1 - crates/agent_ui/src/agent_panel.rs | 249 ++++++++- crates/agent_ui/src/agent_ui.rs | 1 + crates/agent_ui/src/connection_view.rs | 2 +- crates/{sidebar => agent_ui}/src/sidebar.rs | 497 ++++++++---------- .../debugger_ui/src/tests/stack_frame_list.rs | 4 +- .../src/platform_title_bar.rs | 40 +- crates/sidebar/Cargo.toml | 51 -- crates/sidebar/LICENSE-GPL | 1 - crates/title_bar/Cargo.toml | 1 - crates/title_bar/src/title_bar.rs | 125 +---- crates/workspace/src/multi_workspace.rs | 405 +------------- crates/workspace/src/persistence.rs | 10 +- crates/workspace/src/persistence/model.rs | 9 +- crates/workspace/src/status_bar.rs | 14 +- crates/workspace/src/workspace.rs | 29 +- crates/zed/Cargo.toml | 1 - crates/zed/src/visual_test_runner.rs | 42 +- crates/zed/src/zed.rs | 15 - 21 files changed, 517 insertions(+), 1012 deletions(-) rename crates/{sidebar => agent_ui}/src/sidebar.rs (91%) delete mode 100644 crates/sidebar/Cargo.toml delete mode 120000 crates/sidebar/LICENSE-GPL diff --git a/Cargo.lock b/Cargo.lock index dfd8a74acba5056e468a72d8cd105c0f2cfd156a..f11d2023b319501778768fdea39fb8dbb242a9e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15807,33 +15807,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "sidebar" -version = "0.1.0" -dependencies = [ - "acp_thread", - "agent", - "agent-client-protocol", - "agent_ui", - "assistant_text_thread", - "chrono", - "editor", - "feature_flags", - "fs", - "gpui", - "language_model", - "menu", - "project", - "recent_projects", - "serde_json", - "settings", - "theme", - "ui", - "util", - "workspace", - "zed_actions", -] - [[package]] name = "signal-hook" version = "0.3.18" @@ -17660,7 +17633,6 @@ dependencies = [ "client", "cloud_api_types", "db", - "feature_flags", "git_ui", "gpui", "notifications", @@ -21887,7 +21859,6 @@ dependencies = [ "settings_profile_selector", "settings_ui", "shellexpand 2.1.2", - "sidebar", "smol", "snippet_provider", "snippets_ui", diff --git a/Cargo.toml b/Cargo.toml index b6760fa917da7e051fd60a1375be49d516fcf113..c184837bfd6a67490169b7a6908b17b4d61e121f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -173,7 +173,6 @@ members = [ "crates/settings_profile_selector", "crates/settings_ui", "crates/shell_command_parser", - "crates/sidebar", "crates/snippet", "crates/snippet_provider", "crates/snippets_ui", @@ -412,7 +411,6 @@ rules_library = { path = "crates/rules_library" } scheduler = { path = "crates/scheduler" } search = { path = "crates/search" } session = { path = "crates/session" } -sidebar = { path = "crates/sidebar" } settings = { path = "crates/settings" } settings_content = { path = "crates/settings_content" } settings_json = { path = "crates/settings_json" } @@ -907,7 +905,6 @@ refineable = { codegen-units = 1 } release_channel = { codegen-units = 1 } reqwest_client = { codegen-units = 1 } session = { codegen-units = 1 } -sidebar = { codegen-units = 1 } snippet = { codegen-units = 1 } snippets_ui = { codegen-units = 1 } story = { codegen-units = 1 } diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 8b06417d2f5812ef2e0fb265e6afa4cfeb26eb3f..7a0910726e03221dc0a105d69c4852e7515e0c35 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -132,7 +132,6 @@ languages = { workspace = true, features = ["test-support"] } language_model = { workspace = true, "features" = ["test-support"] } pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } - semver.workspace = true reqwest_client.workspace = true diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 80f8925ad05414b9839ac53953156ef35c43e08f..630411c2400ee925f980b5d3a410cb3574e81cd6 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -65,9 +65,10 @@ use extension_host::ExtensionStore; use fs::Fs; use git::repository::validate_worktree_directory; use gpui::{ - Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner, - DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, - Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, + Action, Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardItem, + Corner, DismissEvent, DragMoveEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, + Focusable, KeyContext, MouseButton, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, + deferred, prelude::*, pulsating_between, }; use language::LanguageRegistry; use language_model::{ConfigurationError, LanguageModelRegistry}; @@ -79,15 +80,17 @@ use search::{BufferSearchBar, buffer_search}; use settings::{Settings, update_settings_file}; use theme::ThemeSettings; use ui::{ - Button, ButtonLike, Callout, ContextMenu, ContextMenuEntry, DocumentationSide, KeyBinding, - PopoverMenu, PopoverMenuHandle, SpinnerLabel, Tab, TintColor, Tooltip, prelude::*, + Button, ButtonLike, Callout, ContextMenu, ContextMenuEntry, DocumentationSide, Indicator, + KeyBinding, PopoverMenu, PopoverMenuHandle, SpinnerLabel, Tab, TintColor, Tooltip, prelude::*, utils::WithRemSize, }; use util::ResultExt as _; use workspace::{ - CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace, - WorkspaceId, + CollaboratorId, DraggedSelection, DraggedSidebar, DraggedTab, FocusWorkspaceSidebar, + MultiWorkspace, SIDEBAR_RESIZE_HANDLE_SIZE, ToggleWorkspaceSidebar, ToggleZoom, + ToolbarItemView, Workspace, WorkspaceId, dock::{DockPosition, Panel, PanelEvent}, + multi_workspace_enabled, }; use zed_actions::{ DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, @@ -99,6 +102,55 @@ const AGENT_PANEL_KEY: &str = "agent_panel"; const RECENTLY_UPDATED_MENU_LIMIT: usize = 6; const DEFAULT_THREAD_TITLE: &str = "New Thread"; +#[derive(Default)] +struct SidebarsByWindow( + collections::HashMap>, +); + +impl gpui::Global for SidebarsByWindow {} + +pub(crate) fn sidebar_is_open(window: &Window, cx: &App) -> bool { + if !multi_workspace_enabled(cx) { + return false; + } + let window_id = window.window_handle().window_id(); + cx.try_global::() + .and_then(|sidebars| sidebars.0.get(&window_id)?.upgrade()) + .is_some_and(|sidebar| sidebar.read(cx).is_open()) +} + +fn find_or_create_sidebar_for_window( + window: &mut Window, + cx: &mut App, +) -> Option> { + let window_id = window.window_handle().window_id(); + let multi_workspace = window.root::().flatten()?; + + if !cx.has_global::() { + cx.set_global(SidebarsByWindow::default()); + } + + cx.global_mut::() + .0 + .retain(|_, weak| weak.upgrade().is_some()); + + let existing = cx + .global::() + .0 + .get(&window_id) + .and_then(|weak| weak.upgrade()); + + if let Some(sidebar) = existing { + return Some(sidebar); + } + + let sidebar = cx.new(|cx| crate::sidebar::Sidebar::new(multi_workspace, window, cx)); + cx.global_mut::() + .0 + .insert(window_id, sidebar.downgrade()); + Some(sidebar) +} + fn read_serialized_panel(workspace_id: workspace::WorkspaceId) -> Option { let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY); let key = i64::from(workspace_id).to_string(); @@ -424,6 +476,30 @@ pub fn init(cx: &mut App) { panel.set_start_thread_in(action, cx); }); } + }) + .register_action(|workspace, _: &ToggleWorkspaceSidebar, window, cx| { + if !multi_workspace_enabled(cx) { + return; + } + if let Some(panel) = workspace.panel::(cx) { + if let Some(sidebar) = panel.read(cx).sidebar.clone() { + sidebar.update(cx, |sidebar, cx| { + sidebar.toggle(window, cx); + }); + } + } + }) + .register_action(|workspace, _: &FocusWorkspaceSidebar, window, cx| { + if !multi_workspace_enabled(cx) { + return; + } + if let Some(panel) = workspace.panel::(cx) { + if let Some(sidebar) = panel.read(cx).sidebar.clone() { + sidebar.update(cx, |sidebar, cx| { + sidebar.focus_or_unfocus(workspace, window, cx); + }); + } + } }); }, ) @@ -820,6 +896,7 @@ pub struct AgentPanel { last_configuration_error_telemetry: Option, on_boarding_upsell_dismissed: AtomicBool, _active_view_observation: Option, + pub(crate) sidebar: Option>, } impl AgentPanel { @@ -991,7 +1068,6 @@ impl AgentPanel { let client = workspace.client().clone(); let workspace_id = workspace.database_id(); let workspace = workspace.weak_handle(); - let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); @@ -1149,10 +1225,17 @@ impl AgentPanel { last_configuration_error_telemetry: None, on_boarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed()), _active_view_observation: None, + sidebar: None, }; // Initial sync of agent servers from extensions panel.sync_agent_servers_from_extensions(cx); + + cx.defer_in(window, move |this, window, cx| { + this.sidebar = find_or_create_sidebar_for_window(window, cx); + cx.notify(); + }); + panel } @@ -3526,9 +3609,109 @@ impl AgentPanel { }) } + fn sidebar_info(&self, cx: &App) -> Option<(AnyView, Pixels, bool)> { + if !multi_workspace_enabled(cx) { + return None; + } + let sidebar = self.sidebar.as_ref()?; + let is_open = sidebar.read(cx).is_open(); + let width = sidebar.read(cx).width(cx); + let view: AnyView = sidebar.clone().into(); + Some((view, width, is_open)) + } + + fn render_sidebar_toggle(&self, cx: &Context) -> Option { + if !multi_workspace_enabled(cx) { + return None; + } + let sidebar = self.sidebar.as_ref()?; + let sidebar_read = sidebar.read(cx); + if sidebar_read.is_open() { + return None; + } + let has_notifications = sidebar_read.has_notifications(cx); + + Some( + IconButton::new("toggle-workspace-sidebar", IconName::WorkspaceNavClosed) + .icon_size(IconSize::Small) + .when(has_notifications, |button| { + button + .indicator(Indicator::dot().color(Color::Accent)) + .indicator_border_color(Some(cx.theme().colors().tab_bar_background)) + }) + .tooltip(move |_, cx| { + Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx) + }) + .on_click(|_, window, cx| { + window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx); + }) + .into_any_element(), + ) + } + + fn render_sidebar(&self, cx: &Context) -> Option { + let (sidebar_view, sidebar_width, is_open) = self.sidebar_info(cx)?; + if !is_open { + return None; + } + + let docked_right = agent_panel_dock_position(cx) == DockPosition::Right; + let sidebar = self.sidebar.as_ref()?.downgrade(); + + let resize_handle = deferred( + div() + .id("sidebar-resize-handle") + .absolute() + .when(docked_right, |this| { + this.left(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.) + }) + .when(!docked_right, |this| { + this.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.) + }) + .top(px(0.)) + .h_full() + .w(SIDEBAR_RESIZE_HANDLE_SIZE) + .cursor_col_resize() + .on_drag(DraggedSidebar, |dragged, _, _, cx| { + cx.stop_propagation(); + cx.new(|_| dragged.clone()) + }) + .on_mouse_down(MouseButton::Left, |_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up(MouseButton::Left, move |event, _, cx| { + if event.click_count == 2 { + sidebar + .update(cx, |sidebar, cx| { + sidebar.set_width(None, cx); + }) + .ok(); + cx.stop_propagation(); + } + }) + .occlude(), + ); + + Some( + div() + .id("sidebar-container") + .relative() + .h_full() + .w(sidebar_width) + .flex_shrink_0() + .when(docked_right, |this| this.border_l_1()) + .when(!docked_right, |this| this.border_r_1()) + .border_color(cx.theme().colors().border) + .child(sidebar_view) + .child(resize_handle) + .into_any_element(), + ) + } + fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let agent_server_store = self.project.read(cx).agent_server_store().clone(); let focus_handle = self.focus_handle(cx); + 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 { @@ -3991,6 +4174,9 @@ impl AgentPanel { .size_full() .gap(DynamicSpacing::Base04.rems(cx)) .pl(DynamicSpacing::Base04.rems(cx)) + .when(!docked_right, |this| { + this.children(self.render_sidebar_toggle(cx)) + }) .child(agent_selector_menu) .child(self.render_start_thread_in_selector(cx)), ) @@ -4007,7 +4193,10 @@ impl AgentPanel { cx, )) }) - .child(self.render_panel_options_menu(window, cx)), + .child(self.render_panel_options_menu(window, cx)) + .when(docked_right, |this| { + this.children(self.render_sidebar_toggle(cx)) + }), ) .into_any_element() } else { @@ -4045,6 +4234,9 @@ impl AgentPanel { .size_full() .gap(DynamicSpacing::Base04.rems(cx)) .pl(DynamicSpacing::Base04.rems(cx)) + .when(!docked_right, |this| { + this.children(self.render_sidebar_toggle(cx)) + }) .child(match &self.active_view { ActiveView::History { .. } | ActiveView::Configuration => { self.render_toolbar_back_button(cx).into_any_element() @@ -4067,7 +4259,10 @@ impl AgentPanel { cx, )) }) - .child(self.render_panel_options_menu(window, cx)), + .child(self.render_panel_options_menu(window, cx)) + .when(docked_right, |this| { + this.children(self.render_sidebar_toggle(cx)) + }), ) .into_any_element() } @@ -4607,14 +4802,44 @@ impl Render for AgentPanel { }) .children(self.render_trial_end_upsell(window, cx)); + let sidebar = self.render_sidebar(cx); + let has_sidebar = sidebar.is_some(); + let docked_right = agent_panel_dock_position(cx) == DockPosition::Right; + + let panel = h_flex() + .size_full() + .when(has_sidebar, |this| { + this.on_drag_move(cx.listener( + move |this, e: &DragMoveEvent, _window, cx| { + if let Some(sidebar) = &this.sidebar { + let width = if docked_right { + e.bounds.right() - e.event.position.x + } else { + e.event.position.x + }; + sidebar.update(cx, |sidebar, cx| { + sidebar.set_width(Some(width), cx); + }); + } + }, + )) + }) + .map(|this| { + if docked_right { + this.child(content).children(sidebar) + } else { + this.children(sidebar).child(content) + } + }); + match self.active_view.which_font_size_used() { WhichFontSize::AgentFont => { WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx)) .size_full() - .child(content) + .child(panel) .into_any() } - _ => content.into_any(), + _ => panel.into_any(), } } } diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index d37dbdbbeb184cac31320b4bc9232354eb3dcc8d..292db8fc7c0398fdd8c8800b8acc2b3c6df22740 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -23,6 +23,7 @@ mod mode_selector; mod model_selector; mod model_selector_popover; mod profile_selector; +pub mod sidebar; mod slash_command; mod slash_command_picker; mod terminal_codegen; diff --git a/crates/agent_ui/src/connection_view.rs b/crates/agent_ui/src/connection_view.rs index 3b07929813e5583164700905a1fa327f3ac9d964..fd4ac66c05e380ddd3e1c3e2c196c5a397754c9d 100644 --- a/crates/agent_ui/src/connection_view.rs +++ b/crates/agent_ui/src/connection_view.rs @@ -2340,7 +2340,7 @@ impl ConnectionView { } if let Some(multi_workspace) = window.root::().flatten() { - multi_workspace.read(cx).is_sidebar_open() + crate::agent_panel::sidebar_is_open(window, cx) || self.agent_panel_visible(&multi_workspace, cx) } else { self.workspace diff --git a/crates/sidebar/src/sidebar.rs b/crates/agent_ui/src/sidebar.rs similarity index 91% rename from crates/sidebar/src/sidebar.rs rename to crates/agent_ui/src/sidebar.rs index dd1dcab9ee7b5c6de25630b9f0b8fcebcdad7cb2..2679807388eb6261f9bc32be10c10ed500078b22 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/agent_ui/src/sidebar.rs @@ -1,33 +1,32 @@ +use crate::{AgentPanel, AgentPanelEvent, NewThread}; use acp_thread::ThreadStatus; use agent::ThreadStore; use agent_client_protocol as acp; -use agent_ui::{AgentPanel, AgentPanelEvent, NewThread}; +use agent_settings::AgentSettings; use chrono::Utc; +use db::kvp::KEY_VALUE_STORE; use editor::{Editor, EditorElement, EditorStyle}; use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _}; use gpui::{ - AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, ListState, + Action as _, AnyElement, App, Context, Entity, FocusHandle, Focusable, FontStyle, ListState, Pixels, Render, SharedString, TextStyle, WeakEntity, Window, actions, list, prelude::*, px, relative, rems, }; use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; use project::Event as ProjectEvent; -use recent_projects::RecentProjects; use settings::Settings; use std::collections::{HashMap, HashSet}; use std::mem; use theme::{ActiveTheme, ThemeSettings}; -use ui::utils::TRAFFIC_LIGHT_PADDING; use ui::{ - AgentThreadStatus, ButtonStyle, HighlightedLabel, IconButtonShape, KeyBinding, ListItem, - PopoverMenu, PopoverMenuHandle, Tab, ThreadItem, TintColor, Tooltip, WithScrollbar, prelude::*, + AgentThreadStatus, ButtonStyle, HighlightedLabel, IconButtonShape, ListItem, Tab, ThreadItem, + Tooltip, WithScrollbar, prelude::*, }; +use util::ResultExt as _; use util::path_list::PathList; use workspace::{ - FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, Sidebar as WorkspaceSidebar, - SidebarEvent, ToggleWorkspaceSidebar, Workspace, + MultiWorkspace, MultiWorkspaceEvent, ToggleWorkspaceSidebar, Workspace, multi_workspace_enabled, }; -use zed_actions::OpenRecent; use zed_actions::editor::{MoveDown, MoveUp}; actions!( @@ -44,6 +43,27 @@ const DEFAULT_WIDTH: Pixels = px(320.0); const MIN_WIDTH: Pixels = px(200.0); const MAX_WIDTH: Pixels = px(800.0); const DEFAULT_THREADS_SHOWN: usize = 5; +const SIDEBAR_STATE_KEY: &str = "sidebar_state"; + +fn read_sidebar_open_state(multi_workspace_id: u64) -> bool { + KEY_VALUE_STORE + .scoped(SIDEBAR_STATE_KEY) + .read(&multi_workspace_id.to_string()) + .log_err() + .flatten() + .and_then(|json| serde_json::from_str::(&json).ok()) + .unwrap_or(false) +} + +async fn save_sidebar_open_state(multi_workspace_id: u64, is_open: bool) { + if let Ok(json) = serde_json::to_string(&is_open) { + KEY_VALUE_STORE + .scoped(SIDEBAR_STATE_KEY) + .write(multi_workspace_id.to_string(), json) + .await + .log_err(); + } +} #[derive(Clone, Debug)] struct ActiveThreadInfo { @@ -173,6 +193,8 @@ fn workspace_path_list_and_label( pub struct Sidebar { multi_workspace: WeakEntity, + persistence_key: Option, + is_open: bool, width: Pixels, focus_handle: FocusHandle, filter_editor: Entity, @@ -186,11 +208,8 @@ pub struct Sidebar { active_entry_index: Option, collapsed_groups: HashSet, expanded_groups: HashMap, - recent_projects_popover_handle: PopoverMenuHandle, } -impl EventEmitter for Sidebar {} - impl Sidebar { pub fn new( multi_workspace: Entity, @@ -212,7 +231,6 @@ impl Sidebar { window, |this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event { MultiWorkspaceEvent::ActiveWorkspaceChanged => { - this.focused_thread = None; this.update_entries(cx); } MultiWorkspaceEvent::WorkspaceAdded(workspace) => { @@ -270,8 +288,15 @@ impl Sidebar { this.update_entries(cx); }); + let persistence_key = multi_workspace.read(cx).database_id().map(|id| id.0); + let is_open = persistence_key + .map(read_sidebar_open_state) + .unwrap_or(false); + Self { multi_workspace: multi_workspace.downgrade(), + persistence_key, + is_open, width: DEFAULT_WIDTH, focus_handle, filter_editor, @@ -282,7 +307,6 @@ impl Sidebar { active_entry_index: None, collapsed_groups: HashSet::new(), expanded_groups: HashMap::new(), - recent_projects_popover_handle: PopoverMenuHandle::default(), } } @@ -334,31 +358,10 @@ impl Sidebar { cx.subscribe_in( agent_panel, window, - |this, agent_panel, event: &AgentPanelEvent, _window, cx| match event { - AgentPanelEvent::ActiveViewChanged => { - match agent_panel.read(cx).active_connection_view() { - Some(thread) => { - if let Some(session_id) = thread.read(cx).parent_id(cx) { - this.focused_thread = Some(session_id); - } - } - None => { - this.focused_thread = None; - } - } - this.update_entries(cx); - } - AgentPanelEvent::ThreadFocused => { - let new_focused = agent_panel - .read(cx) - .active_connection_view() - .and_then(|thread| thread.read(cx).parent_id(cx)); - if new_focused.is_some() && new_focused != this.focused_thread { - this.focused_thread = new_focused; - this.update_entries(cx); - } - } - AgentPanelEvent::BackgroundThreadChanged => { + |this, _agent_panel, event: &AgentPanelEvent, _window, cx| match event { + AgentPanelEvent::ActiveViewChanged + | AgentPanelEvent::ThreadFocused + | AgentPanelEvent::BackgroundThreadChanged => { this.update_entries(cx); } }, @@ -419,6 +422,12 @@ impl Sidebar { let workspaces = mw.workspaces().to_vec(); let active_workspace = mw.workspaces().get(mw.active_workspace_index()).cloned(); + self.focused_thread = active_workspace + .as_ref() + .and_then(|ws| ws.read(cx).panel::(cx)) + .and_then(|panel| panel.read(cx).active_connection_view().cloned()) + .and_then(|cv| cv.read(cx).parent_id(cx)); + let thread_store = ThreadStore::try_global(cx); let query = self.filter_editor.read(cx).text(cx); @@ -657,7 +666,7 @@ impl Sidebar { let Some(multi_workspace) = self.multi_workspace.upgrade() else { return; }; - if !multi_workspace.read(cx).multi_workspace_enabled(cx) { + if !multi_workspace_enabled(cx) { return; } @@ -885,8 +894,6 @@ impl Sidebar { return; }; - self.focused_thread = None; - multi_workspace.update(cx, |multi_workspace, cx| { multi_workspace.activate(workspace.clone(), cx); }); @@ -1173,48 +1180,6 @@ impl Sidebar { .into_any_element() } - fn render_recent_projects_button(&self, cx: &mut Context) -> impl IntoElement { - let workspace = self - .multi_workspace - .upgrade() - .map(|mw| mw.read(cx).workspace().downgrade()); - - let focus_handle = workspace - .as_ref() - .and_then(|ws| ws.upgrade()) - .map(|w| w.read(cx).focus_handle(cx)) - .unwrap_or_else(|| cx.focus_handle()); - - 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) - }) - }) - .trigger_with_tooltip( - IconButton::new("open-project", IconName::OpenFolder) - .icon_size(IconSize::Small) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)), - |_window, cx| { - Tooltip::for_action( - "Recent Projects", - &OpenRecent { - create_new_window: false, - }, - cx, - ) - }, - ) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - } - fn render_filter_input(&self, cx: &mut Context) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { @@ -1343,26 +1308,66 @@ impl Sidebar { } } -impl WorkspaceSidebar for Sidebar { - fn width(&self, _cx: &App) -> Pixels { - self.width +impl Sidebar { + pub fn is_open(&self) -> bool { + self.is_open } - fn set_width(&mut self, width: Option, cx: &mut Context) { - self.width = width.unwrap_or(DEFAULT_WIDTH).clamp(MIN_WIDTH, MAX_WIDTH); + pub fn set_open(&mut self, open: bool, cx: &mut Context) { + if self.is_open == open { + return; + } + self.is_open = open; cx.notify(); + if let Some(key) = self.persistence_key { + let is_open = self.is_open; + cx.background_spawn(async move { + save_sidebar_open_state(key, is_open).await; + }) + .detach(); + } } - fn has_notifications(&self, _cx: &App) -> bool { - !self.contents.notified_threads.is_empty() + pub fn toggle(&mut self, window: &mut Window, cx: &mut Context) { + let new_state = !self.is_open; + self.set_open(new_state, cx); + if new_state { + cx.focus_self(window); + } + } + + pub fn focus_or_unfocus( + &mut self, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) { + if self.is_open { + let sidebar_is_focused = self.focus_handle(cx).contains_focused(window, cx); + if sidebar_is_focused { + let active_pane = workspace.active_pane().clone(); + let pane_focus = active_pane.read(cx).focus_handle(cx); + window.focus(&pane_focus, cx); + } else { + cx.focus_self(window); + } + } else { + self.set_open(true, cx); + cx.focus_self(window); + } } - fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App) { - self.recent_projects_popover_handle.toggle(window, cx); + pub fn width(&self, _cx: &App) -> Pixels { + self.width + } + + pub fn set_width(&mut self, width: Option, cx: &mut Context) { + self.width = width.unwrap_or(DEFAULT_WIDTH).clamp(MIN_WIDTH, MAX_WIDTH); + cx.notify(); } - fn is_recent_projects_popover_deployed(&self) -> bool { - self.recent_projects_popover_handle.is_deployed() + pub fn has_notifications(&self, _cx: &App) -> bool { + !self.contents.notified_threads.is_empty() } } @@ -1374,18 +1379,9 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let titlebar_height = ui::utils::platform_title_bar_height(window); let ui_font = theme::setup_ui_font(window, cx); - let is_focused = self.focus_handle.is_focused(window) - || self.filter_editor.focus_handle(cx).is_focused(window); let has_query = self.has_filter_query(cx); - let focus_tooltip_label = if is_focused { - "Focus Workspace" - } else { - "Focus Sidebar" - }; - v_flex() .id("workspace-sidebar") .key_context("WorkspaceSidebar") @@ -1401,69 +1397,26 @@ impl Render for Sidebar { .on_action(cx.listener(Self::collapse_selected_entry)) .on_action(cx.listener(Self::cancel)) .font(ui_font) - .h_full() - .w(self.width) + .size_full() .bg(cx.theme().colors().surface_background) - .border_r_1() - .border_color(cx.theme().colors().border) - .child( - h_flex() - .flex_none() - .h(titlebar_height) - .w_full() - .mt_px() - .pb_px() - .pr_1() - .when_else( - cfg!(target_os = "macos") && !window.is_fullscreen(), - |this| this.pl(px(TRAFFIC_LIGHT_PADDING)), - |this| this.pl_2(), - ) - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border) - .child({ - let focus_handle_toggle = self.focus_handle.clone(); - let focus_handle_focus = self.focus_handle.clone(); - IconButton::new("close-sidebar", IconName::WorkspaceNavOpen) - .icon_size(IconSize::Small) - .tooltip(Tooltip::element(move |_, cx| { - v_flex() - .gap_1() - .child( - h_flex() - .gap_2() - .justify_between() - .child(Label::new("Close Sidebar")) - .child(KeyBinding::for_action_in( - &ToggleWorkspaceSidebar, - &focus_handle_toggle, - cx, - )), - ) - .child( - h_flex() - .pt_1() - .gap_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .justify_between() - .child(Label::new(focus_tooltip_label)) - .child(KeyBinding::for_action_in( - &FocusWorkspaceSidebar, - &focus_handle_focus, - cx, - )), - ) - .into_any_element() - })) - .on_click(cx.listener(|_this, _, _window, cx| { - cx.emit(SidebarEvent::Close); - })) - }) - .child(self.render_recent_projects_button(cx)), - ) - .child( + .child({ + let docked_right = + AgentSettings::get_global(cx).dock == settings::DockPosition::Right; + let render_close_button = || { + IconButton::new("sidebar-close-toggle", IconName::WorkspaceNavOpen) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action( + "Close Threads Sidebar", + &ToggleWorkspaceSidebar, + cx, + ) + }) + .on_click(|_, window, cx| { + window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx); + }) + }; + h_flex() .flex_none() .px_2p5() @@ -1471,6 +1424,7 @@ impl Render for Sidebar { .gap_2() .border_b_1() .border_color(cx.theme().colors().border) + .when(!docked_right, |this| this.child(render_close_button())) .child( Icon::new(IconName::MagnifyingGlass) .size(IconSize::Small) @@ -1487,8 +1441,9 @@ impl Render for Sidebar { this.update_entries(cx); })), ) - }), - ) + }) + .when(docked_right, |this| this.child(render_close_button())) + }) .child( v_flex() .flex_1() @@ -1509,26 +1464,24 @@ impl Render for Sidebar { #[cfg(test)] mod tests { use super::*; + use crate::test_support::{active_session_id, open_thread_with_connection, send_message}; use acp_thread::StubAgentConnection; use agent::ThreadStore; - use agent_ui::test_support::{active_session_id, open_thread_with_connection, send_message}; use assistant_text_thread::TextThreadStore; use chrono::DateTime; use feature_flags::FeatureFlagAppExt as _; use fs::FakeFs; use gpui::TestAppContext; - use settings::SettingsStore; use std::sync::Arc; use util::path_list::PathList; fn init_test(cx: &mut TestAppContext) { + crate::test_support::init_test(cx); cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - editor::init(cx); cx.update_flags(false, vec!["agent-v2".into()]); ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + prompt_store::init(cx); }); } @@ -1569,14 +1522,33 @@ mod tests { multi_workspace: &Entity, cx: &mut gpui::VisualTestContext, ) -> Entity { - let multi_workspace = multi_workspace.clone(); - let sidebar = - cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx))); - multi_workspace.update_in(cx, |mw, window, cx| { - mw.register_sidebar(sidebar.clone(), window, cx); + let (sidebar, _panel) = setup_sidebar_with_agent_panel(multi_workspace, cx); + sidebar + } + + fn setup_sidebar_with_agent_panel( + multi_workspace: &Entity, + cx: &mut gpui::VisualTestContext, + ) -> (Entity, Entity) { + let workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone()); + let project = workspace.read_with(cx, |ws, _cx| ws.project().clone()); + let panel = add_agent_panel(&workspace, &project, cx); + workspace.update_in(cx, |workspace, window, cx| { + workspace.right_dock().update(cx, |dock, cx| { + if let Some(panel_ix) = dock.panel_index_for_type::() { + dock.activate_panel(panel_ix, window, cx); + } + dock.set_open(true, window, cx); + }); }); cx.run_until_parked(); - sidebar + let sidebar = panel.read_with(cx, |panel, _cx| { + panel + .sidebar + .clone() + .expect("AgentPanel should have created a sidebar") + }); + (sidebar, panel) } async fn save_n_test_threads( @@ -1623,16 +1595,10 @@ mod tests { cx.run_until_parked(); } - fn open_and_focus_sidebar( - sidebar: &Entity, - multi_workspace: &Entity, - cx: &mut gpui::VisualTestContext, - ) { - multi_workspace.update_in(cx, |mw, window, cx| { - mw.toggle_sidebar(window, cx); - }); + fn open_and_focus_sidebar(sidebar: &Entity, cx: &mut gpui::VisualTestContext) { cx.run_until_parked(); - sidebar.update_in(cx, |_, window, cx| { + sidebar.update_in(cx, |sidebar, window, cx| { + sidebar.set_open(true, cx); cx.focus_self(window); }); cx.run_until_parked(); @@ -1886,7 +1852,7 @@ mod tests { assert!(entries.iter().any(|e| e.contains("View More (12)"))); // Focus and navigate to View More, then confirm to expand by one batch - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); for _ in 0..7 { cx.dispatch_action(SelectNext); } @@ -2169,7 +2135,7 @@ mod tests { // Entries: [header, thread3, thread2, thread1] // Focusing the sidebar does not set a selection; select_next/select_previous // handle None gracefully by starting from the first or last entry. - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None); // First SelectNext from None starts at index 0 @@ -2218,7 +2184,7 @@ mod tests { multi_workspace.update_in(cx, |_, _window, cx| cx.notify()); cx.run_until_parked(); - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); // SelectLast jumps to the end cx.dispatch_action(SelectLast); @@ -2241,7 +2207,7 @@ mod tests { // Open the sidebar so it's rendered, then focus it to trigger focus_in. // focus_in no longer sets a default selection. - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None); // Manually set a selection, blur, then refocus — selection should be preserved @@ -2273,6 +2239,9 @@ mod tests { }); cx.run_until_parked(); + // Add an agent panel to workspace 1 so the sidebar renders when it's active. + setup_sidebar_with_agent_panel(&multi_workspace, cx); + let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]); save_n_test_threads(1, &path_list, cx).await; multi_workspace.update_in(cx, |_, _window, cx| cx.notify()); @@ -2299,7 +2268,7 @@ mod tests { ); // Focus the sidebar and manually select the header (index 0) - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); sidebar.update_in(cx, |sidebar, _window, _cx| { sidebar.selection = Some(0); }); @@ -2342,7 +2311,7 @@ mod tests { assert!(entries.iter().any(|e| e.contains("View More (3)"))); // Focus sidebar (selection starts at None), then navigate down to the "View More" entry (index 6) - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); for _ in 0..7 { cx.dispatch_action(SelectNext); } @@ -2377,7 +2346,7 @@ mod tests { ); // Focus sidebar and manually select the header (index 0). Press left to collapse. - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); sidebar.update_in(cx, |sidebar, _window, _cx| { sidebar.selection = Some(0); }); @@ -2417,7 +2386,7 @@ mod tests { cx.run_until_parked(); // Focus sidebar (selection starts at None), then navigate down to the thread (child) - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); cx.dispatch_action(SelectNext); cx.dispatch_action(SelectNext); assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1)); @@ -2452,7 +2421,7 @@ mod tests { ); // Focus sidebar — focus_in does not set a selection - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None); // First SelectNext from None starts at index 0 (header) @@ -2485,7 +2454,7 @@ mod tests { cx.run_until_parked(); // Focus sidebar (selection starts at None), navigate down to the thread (index 1) - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); cx.dispatch_action(SelectNext); cx.dispatch_action(SelectNext); assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1)); @@ -2505,24 +2474,6 @@ mod tests { ); } - async fn init_test_project_with_agent_panel( - worktree_path: &str, - cx: &mut TestAppContext, - ) -> Entity { - agent_ui::test_support::init_test(cx); - cx.update(|cx| { - cx.update_flags(false, vec!["agent-v2".into()]); - ThreadStore::init_global(cx); - language_model::LanguageModelRegistry::test(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(worktree_path, serde_json::json!({ "src": {} })) - .await; - cx.update(|cx| ::set_global(fs.clone(), cx)); - project::Project::test(fs, [worktree_path.as_ref()], cx).await - } - fn add_agent_panel( workspace: &Entity, project: &Entity, @@ -2536,23 +2487,12 @@ mod tests { }) } - fn setup_sidebar_with_agent_panel( - multi_workspace: &Entity, - project: &Entity, - cx: &mut gpui::VisualTestContext, - ) -> (Entity, Entity) { - let sidebar = setup_sidebar(multi_workspace, cx); - let workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone()); - let panel = add_agent_panel(&workspace, project, cx); - (sidebar, panel) - } - #[gpui::test] async fn test_parallel_threads_shown_with_live_status(cx: &mut TestAppContext) { - let project = init_test_project_with_agent_panel("/my-project", cx).await; + let project = init_test_project("/my-project", cx).await; let (multi_workspace, cx) = cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); - let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx); + let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx); let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]); @@ -2595,10 +2535,10 @@ mod tests { #[gpui::test] async fn test_background_thread_completion_triggers_notification(cx: &mut TestAppContext) { - let project_a = init_test_project_with_agent_panel("/project-a", cx).await; + let project_a = init_test_project("/project-a", cx).await; let (multi_workspace, cx) = cx .add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx)); - let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, &project_a, cx); + let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, cx); let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]); @@ -2802,7 +2742,7 @@ mod tests { ); // User types a search query to filter down. - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); type_in_search(&sidebar, "alpha", cx); assert_eq!( visible_entries_as_strings(&sidebar, cx), @@ -3125,7 +3065,7 @@ mod tests { // User focuses the sidebar and collapses the group using keyboard: // manually select the header, then press CollapseSelectedEntry to collapse. - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); sidebar.update_in(cx, |sidebar, _window, _cx| { sidebar.selection = Some(0); }); @@ -3175,7 +3115,7 @@ mod tests { } cx.run_until_parked(); - open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + open_and_focus_sidebar(&sidebar, cx); // User types "fix" — two threads match. type_in_search(&sidebar, "fix", cx); @@ -3352,10 +3292,10 @@ mod tests { #[gpui::test] async fn test_thread_title_update_propagates_to_sidebar(cx: &mut TestAppContext) { - let project = init_test_project_with_agent_panel("/my-project", cx).await; + let project = init_test_project("/my-project", cx).await; let (multi_workspace, cx) = cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); - let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx); + let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx); let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]); @@ -3400,10 +3340,10 @@ mod tests { #[gpui::test] async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) { - let project_a = init_test_project_with_agent_panel("/project-a", cx).await; + let project_a = init_test_project("/project-a", cx).await; let (multi_workspace, cx) = cx .add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx)); - let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, &project_a, cx); + let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, cx); let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]); @@ -3432,7 +3372,8 @@ mod tests { let workspace_a = multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].clone()); // ── 1. Initial state: no focused thread ────────────────────────────── - // Workspace B is active (just added), so its header is the active entry. + // Workspace B is active (just added) and has no thread, so its header + // is the active entry. sidebar.read_with(cx, |sidebar, _cx| { assert_eq!( sidebar.focused_thread, None, @@ -3447,6 +3388,7 @@ mod tests { ); }); + // ── 2. Click thread in workspace A via sidebar ─────────────────────── sidebar.update_in(cx, |sidebar, window, cx| { sidebar.activate_thread( acp_thread::AgentSessionInfo { @@ -3490,6 +3432,7 @@ mod tests { ); }); + // ── 3. Open thread in workspace B, then click it via sidebar ───────── let connection_b = StubAgentConnection::new(); connection_b.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( acp::ContentChunk::new("Thread B".into()), @@ -3501,6 +3444,16 @@ mod tests { save_thread_to_store(&session_id_b, &path_list_b, cx).await; cx.run_until_parked(); + // Opening a thread in a non-active workspace should NOT change + // focused_thread — it's derived from the active workspace. + sidebar.read_with(cx, |sidebar, _cx| { + assert_eq!( + sidebar.focused_thread.as_ref(), + Some(&session_id_a), + "Opening a thread in a non-active workspace should not affect focused_thread" + ); + }); + // Workspace A is currently active. Click a thread in workspace B, // which also triggers a workspace switch. sidebar.update_in(cx, |sidebar, window, cx| { @@ -3535,25 +3488,30 @@ mod tests { ); }); + // ── 4. Switch workspace → focused_thread reflects new workspace ────── multi_workspace.update_in(cx, |mw, window, cx| { mw.activate_next_workspace(window, cx); }); cx.run_until_parked(); + // Workspace A is now active. Its agent panel still has session_id_a + // loaded, so focused_thread should reflect that. 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 workspaces should derive focused_thread from the new active workspace" ); let active_entry = sidebar .active_entry_index .and_then(|ix| sidebar.contents.entries.get(ix)); assert!( - matches!(active_entry, Some(ListEntry::ProjectHeader { .. })), - "Active entry should be the workspace header after external switch" + matches!(active_entry, Some(ListEntry::Thread(thread)) if thread.session_info.session_id == session_id_a), + "Active entry should be workspace_a's active thread" ); }); + // ── 5. Opening a thread in a non-active workspace is ignored ────────── let connection_b2 = StubAgentConnection::new(); connection_b2.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( acp::ContentChunk::new("New thread".into()), @@ -3564,69 +3522,48 @@ mod tests { save_thread_to_store(&session_id_b2, &path_list_b, cx).await; cx.run_until_parked(); + // Workspace A is still active, so focused_thread stays on session_id_a. sidebar.read_with(cx, |sidebar, _cx| { assert_eq!( sidebar.focused_thread.as_ref(), - Some(&session_id_b2), - "Opening a thread externally should set focused_thread" - ); - }); - - workspace_b.update_in(cx, |workspace, window, cx| { - workspace.focus_handle(cx).focus(window, cx); - }); - cx.run_until_parked(); - - sidebar.read_with(cx, |sidebar, _cx| { - assert_eq!( - sidebar.focused_thread.as_ref(), - Some(&session_id_b2), - "Defocusing the sidebar should not clear focused_thread" + Some(&session_id_a), + "Opening a thread in a non-active workspace should not affect focused_thread" ); }); + // ── 6. Activating workspace B shows its active thread ──────────────── sidebar.update_in(cx, |sidebar, window, cx| { sidebar.activate_workspace(&workspace_b, window, cx); }); cx.run_until_parked(); + // Workspace B is now active with session_id_b2 loaded. sidebar.read_with(cx, |sidebar, _cx| { assert_eq!( - sidebar.focused_thread, None, - "Clicking a workspace header should clear focused_thread" + sidebar.focused_thread.as_ref(), + Some(&session_id_b2), + "Activating workspace_b should show workspace_b's active thread" ); let active_entry = sidebar .active_entry_index .and_then(|ix| sidebar.contents.entries.get(ix)); assert!( - matches!(active_entry, Some(ListEntry::ProjectHeader { .. })), - "Active entry should be the workspace header" + matches!(active_entry, Some(ListEntry::Thread(thread)) if thread.session_info.session_id == session_id_b2), + "Active entry should be workspace_b's active thread" ); }); - // ── 8. Focusing the agent panel thread restores 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. - panel_b.update_in(cx, |panel, window, cx| { - if let Some(thread_view) = panel.active_connection_view() { - thread_view.read(cx).focus_handle(cx).focus(window, cx); - } + // ── 7. Switching back to workspace A reflects its thread ───────────── + multi_workspace.update_in(cx, |mw, window, cx| { + mw.activate_next_workspace(window, cx); }); cx.run_until_parked(); sidebar.read_with(cx, |sidebar, _cx| { assert_eq!( sidebar.focused_thread.as_ref(), - Some(&session_id_b2), - "Focusing the agent panel thread should set focused_thread" - ); - 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 focused thread" + Some(&session_id_a), + "Switching back to workspace_a should show its active thread" ); }); } diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 1f5ac5dea4a19af338feceaa2ee51fd9322fa9a5..9a9a9316fb09def438f78734831c5e560c838fba 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -1211,7 +1211,9 @@ async fn test_stack_frame_filter_persistence( cx.run_until_parked(); let workspace_id = workspace - .update(cx, |workspace, _window, cx| workspace.database_id(cx)) + .update(cx, |workspace, _window, cx| { + workspace.active_workspace_database_id(cx) + }) .ok() .flatten() .expect("workspace id has to be some for this test to work properly"); diff --git a/crates/platform_title_bar/src/platform_title_bar.rs b/crates/platform_title_bar/src/platform_title_bar.rs index 7053fe89e7fdc6ece9ad50fdd8facaf31dba3086..1db29b0f53d9e7b185e6c3cd3029ed2e6077753e 100644 --- a/crates/platform_title_bar/src/platform_title_bar.rs +++ b/crates/platform_title_bar/src/platform_title_bar.rs @@ -31,8 +31,6 @@ pub struct PlatformTitleBar { children: SmallVec<[AnyElement; 2]>, should_move: bool, system_window_tabs: Entity, - workspace_sidebar_open: bool, - sidebar_has_notifications: bool, } impl PlatformTitleBar { @@ -46,8 +44,6 @@ impl PlatformTitleBar { children: SmallVec::new(), should_move: false, system_window_tabs, - workspace_sidebar_open: false, - sidebar_has_notifications: false, } } @@ -74,28 +70,6 @@ impl PlatformTitleBar { SystemWindowTabs::init(cx); } - pub fn is_workspace_sidebar_open(&self) -> bool { - self.workspace_sidebar_open - } - - pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context) { - self.workspace_sidebar_open = open; - cx.notify(); - } - - pub fn sidebar_has_notifications(&self) -> bool { - self.sidebar_has_notifications - } - - pub fn set_sidebar_has_notifications( - &mut self, - has_notifications: bool, - cx: &mut Context, - ) { - self.sidebar_has_notifications = has_notifications; - cx.notify(); - } - pub fn is_multi_workspace_enabled(cx: &App) -> bool { cx.has_flag::() && !DisableAiSettings::get_global(cx).disable_ai } @@ -110,9 +84,6 @@ impl Render for PlatformTitleBar { let close_action = Box::new(workspace::CloseWindow); let children = mem::take(&mut self.children); - let is_multiworkspace_sidebar_open = - PlatformTitleBar::is_multi_workspace_enabled(cx) && self.is_workspace_sidebar_open(); - let title_bar = h_flex() .window_control_area(WindowControlArea::Drag) .w_full() @@ -161,9 +132,7 @@ impl Render for PlatformTitleBar { .map(|this| { if window.is_fullscreen() { this.pl_2() - } else if self.platform_style == PlatformStyle::Mac - && !is_multiworkspace_sidebar_open - { + } else if self.platform_style == PlatformStyle::Mac { this.pl(px(TRAFFIC_LIGHT_PADDING)) } else { this.pl_2() @@ -175,10 +144,9 @@ impl Render for PlatformTitleBar { .when(!(tiling.top || tiling.right), |el| { el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) }) - .when( - !(tiling.top || tiling.left) && !is_multiworkspace_sidebar_open, - |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING), - ) + .when(!(tiling.top || tiling.left), |el| { + el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) + }) // this border is to avoid a transparent gap in the rounded corners .mt(px(-1.)) .mb(px(-1.)) diff --git a/crates/sidebar/Cargo.toml b/crates/sidebar/Cargo.toml deleted file mode 100644 index e6b873704ffda9d241fec002eb0fdff0af979c48..0000000000000000000000000000000000000000 --- a/crates/sidebar/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "sidebar" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/sidebar.rs" - -[features] -default = [] - -[dependencies] -acp_thread.workspace = true -agent.workspace = true -agent-client-protocol.workspace = true -agent_ui.workspace = true -chrono.workspace = true -editor.workspace = true -feature_flags.workspace = true -fs.workspace = true -gpui.workspace = true -menu.workspace = true -project.workspace = true -recent_projects.workspace = true -settings.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -acp_thread = { workspace = true, features = ["test-support"] } -agent = { workspace = true, features = ["test-support"] } -agent_ui = { workspace = true, features = ["test-support"] } -assistant_text_thread = { workspace = true, features = ["test-support"] } -editor.workspace = true -language_model = { workspace = true, features = ["test-support"] } -serde_json.workspace = true -feature_flags.workspace = true -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } -recent_projects = { workspace = true, features = ["test-support"] } diff --git a/crates/sidebar/LICENSE-GPL b/crates/sidebar/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/sidebar/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index b5c10835c6bf85ea24db1ff9bad5abbbf3b517ee..f6483d1d70d4017edf8ab8b188d67ecf85e19aef 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -38,7 +38,6 @@ chrono.workspace = true client.workspace = true cloud_api_types.workspace = true db.workspace = true -feature_flags.workspace = true git_ui.workspace = true gpui = { workspace = true, features = ["screen-capture"] } notifications.workspace = true diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 96cc929c06039c14a9ce4eaa05fd067fbd95b7d0..916d58426b76f020bce8a9bf69971f34bc3803a4 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -24,16 +24,13 @@ use auto_update::AutoUpdateStatus; use call::ActiveCall; use client::{Client, UserStore, zed_urls}; use cloud_api_types::Plan; -use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt}; use gpui::{ Action, AnyElement, App, Context, Corner, Element, Empty, Entity, Focusable, InteractiveElement, IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div, }; use onboarding_banner::OnboardingBanner; -use project::{ - DisableAiSettings, Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees, -}; +use project::{Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees}; use remote::RemoteConnectionOptions; use settings::Settings; use settings::WorktreeId; @@ -47,8 +44,7 @@ use ui::{ use update_version::UpdateVersion; use util::ResultExt; use workspace::{ - MultiWorkspace, ToggleWorkspaceSidebar, ToggleWorktreeSecurity, Workspace, - notifications::NotifyResultExt, + MultiWorkspace, ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt, }; use zed_actions::OpenRemote; @@ -174,7 +170,6 @@ impl Render for TitleBar { let mut render_project_items = title_bar_settings.show_branch_name || title_bar_settings.show_project_items; title_bar - .children(self.render_workspace_sidebar_toggle(window, cx)) .when_some( self.application_menu.clone().filter(|_| !show_menus), |title_bar, menu| { @@ -357,7 +352,6 @@ impl TitleBar { // Set up observer to sync sidebar state from MultiWorkspace to PlatformTitleBar. { - let platform_titlebar = platform_titlebar.clone(); let window_handle = window.window_handle(); cx.spawn(async move |this: WeakEntity, cx| { let Some(multi_workspace_handle) = window_handle.downcast::() @@ -370,26 +364,8 @@ impl TitleBar { return; }; - let is_open = multi_workspace.read(cx).is_sidebar_open(); - let has_notifications = multi_workspace.read(cx).sidebar_has_notifications(cx); - platform_titlebar.update(cx, |titlebar, cx| { - titlebar.set_workspace_sidebar_open(is_open, cx); - titlebar.set_sidebar_has_notifications(has_notifications, cx); - }); - - let platform_titlebar = platform_titlebar.clone(); - let subscription = cx.observe(&multi_workspace, move |mw, cx| { - let is_open = mw.read(cx).is_sidebar_open(); - let has_notifications = mw.read(cx).sidebar_has_notifications(cx); - platform_titlebar.update(cx, |titlebar, cx| { - titlebar.set_workspace_sidebar_open(is_open, cx); - titlebar.set_sidebar_has_notifications(has_notifications, cx); - }); - }); - if let Some(this) = this.upgrade() { this.update(cx, |this, _| { - this._subscriptions.push(subscription); this.multi_workspace = Some(multi_workspace.downgrade()); }); } @@ -686,46 +662,7 @@ impl TitleBar { ) } - fn render_workspace_sidebar_toggle( - &self, - _window: &mut Window, - cx: &mut Context, - ) -> Option { - if !cx.has_flag::() || DisableAiSettings::get_global(cx).disable_ai { - return None; - } - - let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open(); - - if is_sidebar_open { - return None; - } - - let has_notifications = self.platform_titlebar.read(cx).sidebar_has_notifications(); - - Some( - IconButton::new("toggle-workspace-sidebar", IconName::WorkspaceNavClosed) - .icon_size(IconSize::Small) - .when(has_notifications, |button| { - button - .indicator(Indicator::dot().color(Color::Accent)) - .indicator_border_color(Some(cx.theme().colors().title_bar_background)) - }) - .tooltip(move |_, cx| { - Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx); - }) - .into_any_element(), - ) - } - - pub fn render_project_name( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { + pub fn render_project_name(&self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let workspace = self.workspace.clone(); let name = self.effective_active_worktree(cx).map(|worktree| { @@ -741,19 +678,6 @@ impl TitleBar { "Open Recent Project".to_string() }; - let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open(); - - if is_sidebar_open { - return self - .render_project_name_with_sidebar_popover( - window, - display_name, - is_project_selected, - cx, - ) - .into_any_element(); - } - let focus_handle = workspace .upgrade() .map(|w| w.read(cx).focus_handle(cx)) @@ -793,49 +717,6 @@ impl TitleBar { .into_any_element() } - fn render_project_name_with_sidebar_popover( - &self, - _window: &Window, - display_name: String, - is_project_selected: bool, - cx: &mut Context, - ) -> impl IntoElement { - let multi_workspace = self.multi_workspace.clone(); - - let is_popover_deployed = multi_workspace - .as_ref() - .and_then(|mw| mw.upgrade()) - .map(|mw| mw.read(cx).is_recent_projects_popover_deployed(cx)) - .unwrap_or(false); - - Button::new("project_name_trigger", display_name) - .label_size(LabelSize::Small) - .when(self.worktree_count(cx) > 1, |this| { - this.icon(IconName::ChevronDown) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - }) - .toggle_state(is_popover_deployed) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .when(!is_project_selected, |s| s.color(Color::Muted)) - .tooltip(move |_window, cx| { - Tooltip::for_action( - "Recent Projects", - &zed_actions::OpenRecent { - create_new_window: false, - }, - cx, - ) - }) - .on_click(move |_, window, cx| { - if let Some(mw) = multi_workspace.as_ref().and_then(|mw| mw.upgrade()) { - mw.update(cx, |mw, cx| { - mw.toggle_recent_projects_popover(window, cx); - }); - } - }) - } - pub fn render_project_branch(&self, cx: &mut Context) -> Option { let effective_worktree = self.effective_active_worktree(cx)?; let repository = self.get_repository_for_worktree(&effective_worktree, cx)?; diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 26af1ce27ecc28b7b541625a16731d0d721a7fc9..adfc62a2bd210b4da24202d734ba9f9eedd17aef 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -1,9 +1,8 @@ use anyhow::Result; use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt}; use gpui::{ - AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, - ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId, - actions, deferred, px, + App, Context, Entity, EntityId, EventEmitter, Focusable, ManagedView, Pixels, Render, + Subscription, Task, Tiling, Window, WindowId, actions, px, }; use project::{DisableAiSettings, Project}; use settings::Settings; @@ -12,11 +11,12 @@ use std::path::PathBuf; use ui::prelude::*; use util::ResultExt; -const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0); +pub const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0); use crate::{ CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, Panel, Toast, Workspace, WorkspaceId, client_side_decorations, notifications::NotificationId, + persistence::model::MultiWorkspaceId, }; actions!( @@ -41,31 +41,6 @@ pub enum MultiWorkspaceEvent { WorkspaceRemoved(EntityId), } -pub enum SidebarEvent { - Open, - Close, -} - -pub trait Sidebar: EventEmitter + Focusable + Render + Sized { - fn width(&self, cx: &App) -> Pixels; - fn set_width(&mut self, width: Option, cx: &mut Context); - fn has_notifications(&self, cx: &App) -> bool; - fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App); - fn is_recent_projects_popover_deployed(&self) -> bool; -} - -pub trait SidebarHandle: 'static + Send + Sync { - fn width(&self, cx: &App) -> Pixels; - fn set_width(&self, width: Option, cx: &mut App); - fn focus_handle(&self, cx: &App) -> FocusHandle; - fn focus(&self, window: &mut Window, cx: &mut App); - fn has_notifications(&self, cx: &App) -> bool; - fn to_any(&self) -> AnyView; - fn entity_id(&self) -> EntityId; - fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App); - fn is_recent_projects_popover_deployed(&self, cx: &App) -> bool; -} - #[derive(Clone)] pub struct DraggedSidebar; @@ -75,54 +50,11 @@ impl Render for DraggedSidebar { } } -impl SidebarHandle for Entity { - fn width(&self, cx: &App) -> Pixels { - self.read(cx).width(cx) - } - - fn set_width(&self, width: Option, cx: &mut App) { - self.update(cx, |this, cx| this.set_width(width, cx)) - } - - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.read(cx).focus_handle(cx) - } - - fn focus(&self, window: &mut Window, cx: &mut App) { - let handle = self.read(cx).focus_handle(cx); - window.focus(&handle, cx); - } - - fn has_notifications(&self, cx: &App) -> bool { - self.read(cx).has_notifications(cx) - } - - fn to_any(&self) -> AnyView { - self.clone().into() - } - - fn entity_id(&self) -> EntityId { - Entity::entity_id(self) - } - - fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| { - this.toggle_recent_projects_popover(window, cx); - }); - } - - fn is_recent_projects_popover_deployed(&self, cx: &App) -> bool { - self.read(cx).is_recent_projects_popover_deployed() - } -} - pub struct MultiWorkspace { window_id: WindowId, workspaces: Vec>, + database_id: Option, active_workspace_index: usize, - sidebar: Option>, - sidebar_open: bool, - _sidebar_subscription: Option, pending_removal_tasks: Vec>, _serialize_task: Option>, _create_task: Option>, @@ -131,6 +63,10 @@ pub struct MultiWorkspace { impl EventEmitter for MultiWorkspace {} +pub fn multi_workspace_enabled(cx: &App) -> bool { + cx.has_flag::() && !DisableAiSettings::get_global(cx).disable_ai +} + impl MultiWorkspace { pub fn new(workspace: Entity, window: &mut Window, cx: &mut Context) -> Self { let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| { @@ -145,142 +81,17 @@ impl MultiWorkspace { } }); let quit_subscription = cx.on_app_quit(Self::app_will_quit); - let settings_subscription = - cx.observe_global_in::(window, |this, window, cx| { - if DisableAiSettings::get_global(cx).disable_ai && this.sidebar_open { - this.close_sidebar(window, cx); - } - }); Self::subscribe_to_workspace(&workspace, cx); Self { window_id: window.window_handle().window_id(), + database_id: None, workspaces: vec![workspace], active_workspace_index: 0, - sidebar: None, - sidebar_open: false, - _sidebar_subscription: None, pending_removal_tasks: Vec::new(), _serialize_task: None, _create_task: None, - _subscriptions: vec![ - release_subscription, - quit_subscription, - settings_subscription, - ], - } - } - - pub fn register_sidebar( - &mut self, - sidebar: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let subscription = - cx.subscribe_in(&sidebar, window, |this, _, event, window, cx| match event { - SidebarEvent::Open => this.toggle_sidebar(window, cx), - SidebarEvent::Close => { - this.close_sidebar(window, cx); - } - }); - self.sidebar = Some(Box::new(sidebar)); - self._sidebar_subscription = Some(subscription); - } - - pub fn sidebar(&self) -> Option<&dyn SidebarHandle> { - self.sidebar.as_deref() - } - - pub fn sidebar_open(&self) -> bool { - self.sidebar_open && self.sidebar.is_some() - } - - pub fn sidebar_has_notifications(&self, cx: &App) -> bool { - self.sidebar - .as_ref() - .map_or(false, |s| s.has_notifications(cx)) - } - - pub fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App) { - if let Some(sidebar) = &self.sidebar { - sidebar.toggle_recent_projects_popover(window, cx); - } - } - - pub fn is_recent_projects_popover_deployed(&self, cx: &App) -> bool { - self.sidebar - .as_ref() - .map_or(false, |s| s.is_recent_projects_popover_deployed(cx)) - } - - pub fn multi_workspace_enabled(&self, cx: &App) -> bool { - cx.has_flag::() && !DisableAiSettings::get_global(cx).disable_ai - } - - pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context) { - if !self.multi_workspace_enabled(cx) { - return; - } - - if self.sidebar_open { - self.close_sidebar(window, cx); - } else { - self.open_sidebar(cx); - if let Some(sidebar) = &self.sidebar { - sidebar.focus(window, cx); - } - } - } - - pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context) { - if !self.multi_workspace_enabled(cx) { - return; - } - - if self.sidebar_open { - let sidebar_is_focused = self - .sidebar - .as_ref() - .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx)); - - if sidebar_is_focused { - let pane = self.workspace().read(cx).active_pane().clone(); - let pane_focus = pane.read(cx).focus_handle(cx); - window.focus(&pane_focus, cx); - } else if let Some(sidebar) = &self.sidebar { - sidebar.focus(window, cx); - } - } else { - self.open_sidebar(cx); - if let Some(sidebar) = &self.sidebar { - sidebar.focus(window, cx); - } - } - } - - pub fn open_sidebar(&mut self, cx: &mut Context) { - self.sidebar_open = true; - for workspace in &self.workspaces { - workspace.update(cx, |workspace, cx| { - workspace.set_workspace_sidebar_open(true, cx); - }); - } - self.serialize(cx); - cx.notify(); - } - - fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context) { - self.sidebar_open = false; - for workspace in &self.workspaces { - workspace.update(cx, |workspace, cx| { - workspace.set_workspace_sidebar_open(false, cx); - }); + _subscriptions: vec![release_subscription, quit_subscription], } - let pane = self.workspace().read(cx).active_pane().clone(); - let pane_focus = pane.read(cx).focus_handle(cx); - window.focus(&pane_focus, cx); - self.serialize(cx); - cx.notify(); } pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context) { @@ -318,10 +129,6 @@ impl MultiWorkspace { .detach(); } - pub fn is_sidebar_open(&self) -> bool { - self.sidebar_open - } - pub fn workspace(&self) -> &Entity { &self.workspaces[self.active_workspace_index] } @@ -335,7 +142,7 @@ impl MultiWorkspace { } pub fn activate(&mut self, workspace: Entity, cx: &mut Context) { - if !self.multi_workspace_enabled(cx) { + if !multi_workspace_enabled(cx) { self.workspaces[0] = workspace; self.active_workspace_index = 0; cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged); @@ -371,11 +178,6 @@ impl MultiWorkspace { if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) { index } else { - if self.sidebar_open { - workspace.update(cx, |workspace, cx| { - workspace.set_workspace_sidebar_open(true, cx); - }); - } Self::subscribe_to_workspace(&workspace, cx); self.workspaces.push(workspace.clone()); cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace)); @@ -384,6 +186,14 @@ impl MultiWorkspace { } } + pub fn database_id(&self) -> Option { + self.database_id + } + + pub fn set_database_id(&mut self, id: Option) { + self.database_id = id; + } + pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context) { debug_assert!( index < self.workspaces.len(), @@ -421,7 +231,6 @@ impl MultiWorkspace { let window_id = self.window_id; let state = crate::persistence::model::MultiWorkspaceState { active_workspace_id: self.workspace().read(cx).database_id(), - sidebar_open: self.sidebar_open, }; self._serialize_task = Some(cx.background_spawn(async move { crate::persistence::write_multi_workspace_state(window_id, state).await; @@ -540,7 +349,7 @@ impl MultiWorkspace { self.workspace().read(cx).items_of_type::(cx) } - pub fn database_id(&self, cx: &App) -> Option { + pub fn active_workspace_database_id(&self, cx: &App) -> Option { self.workspace().read(cx).database_id() } @@ -583,7 +392,7 @@ impl MultiWorkspace { } pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context) { - if !self.multi_workspace_enabled(cx) { + if !multi_workspace_enabled(cx) { return; } let app_state = self.workspace().read(cx).app_state().clone(); @@ -692,7 +501,7 @@ impl MultiWorkspace { ) -> Task> { let workspace = self.workspace().clone(); - if self.multi_workspace_enabled(cx) { + if multi_workspace_enabled(cx) { workspace.update(cx, |workspace, cx| { workspace.open_workspace_for_paths(true, paths, window, cx) }) @@ -719,57 +528,6 @@ impl MultiWorkspace { impl Render for MultiWorkspace { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let multi_workspace_enabled = self.multi_workspace_enabled(cx); - - let sidebar: Option = if multi_workspace_enabled && self.sidebar_open { - self.sidebar.as_ref().map(|sidebar_handle| { - let weak = cx.weak_entity(); - - let sidebar_width = sidebar_handle.width(cx); - let resize_handle = deferred( - div() - .id("sidebar-resize-handle") - .absolute() - .right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.) - .top(px(0.)) - .h_full() - .w(SIDEBAR_RESIZE_HANDLE_SIZE) - .cursor_col_resize() - .on_drag(DraggedSidebar, |dragged, _, _, cx| { - cx.stop_propagation(); - cx.new(|_| dragged.clone()) - }) - .on_mouse_down(MouseButton::Left, |_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up(MouseButton::Left, move |event, _, cx| { - if event.click_count == 2 { - weak.update(cx, |this, cx| { - if let Some(sidebar) = this.sidebar.as_mut() { - sidebar.set_width(None, cx); - } - }) - .ok(); - cx.stop_propagation(); - } - }) - .occlude(), - ); - - div() - .id("sidebar-container") - .relative() - .h_full() - .w(sidebar_width) - .flex_shrink_0() - .child(sidebar_handle.to_any()) - .child(resize_handle) - .into_any_element() - }) - } else { - None - }; - let ui_font = theme::setup_ui_font(window, cx); let text_color = cx.theme().colors().text; @@ -799,32 +557,6 @@ impl Render for MultiWorkspace { this.activate_previous_workspace(window, cx); }, )) - .when(self.multi_workspace_enabled(cx), |this| { - this.on_action(cx.listener( - |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| { - this.toggle_sidebar(window, cx); - }, - )) - .on_action(cx.listener( - |this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| { - this.focus_sidebar(window, cx); - }, - )) - }) - .when( - self.sidebar_open() && self.multi_workspace_enabled(cx), - |this| { - this.on_drag_move(cx.listener( - |this: &mut Self, e: &DragMoveEvent, _window, cx| { - if let Some(sidebar) = &this.sidebar { - let new_width = e.event.position.x; - sidebar.set_width(Some(new_width), cx); - } - }, - )) - .children(sidebar) - }, - ) .child( div() .flex() @@ -837,98 +569,9 @@ impl Render for MultiWorkspace { window, cx, Tiling { - left: multi_workspace_enabled && self.sidebar_open, + left: false, ..Tiling::default() }, ) } } - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui::TestAppContext; - use settings::SettingsStore; - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - DisableAiSettings::register(cx); - cx.update_flags(false, vec!["agent-v2".into()]); - }); - } - - #[gpui::test] - async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - - let (multi_workspace, cx) = - cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); - - multi_workspace.read_with(cx, |mw, cx| { - assert!(mw.multi_workspace_enabled(cx)); - }); - - multi_workspace.update_in(cx, |mw, _window, cx| { - mw.open_sidebar(cx); - assert!(mw.is_sidebar_open()); - }); - - cx.update(|_window, cx| { - DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx); - }); - cx.run_until_parked(); - - multi_workspace.read_with(cx, |mw, cx| { - assert!( - !mw.is_sidebar_open(), - "Sidebar should be closed when disable_ai is true" - ); - assert!( - !mw.multi_workspace_enabled(cx), - "Multi-workspace should be disabled when disable_ai is true" - ); - }); - - multi_workspace.update_in(cx, |mw, window, cx| { - mw.toggle_sidebar(window, cx); - }); - multi_workspace.read_with(cx, |mw, _cx| { - assert!( - !mw.is_sidebar_open(), - "Sidebar should remain closed when toggled with disable_ai true" - ); - }); - - cx.update(|_window, cx| { - DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx); - }); - cx.run_until_parked(); - - multi_workspace.read_with(cx, |mw, cx| { - assert!( - mw.multi_workspace_enabled(cx), - "Multi-workspace should be enabled after re-enabling AI" - ); - assert!( - !mw.is_sidebar_open(), - "Sidebar should still be closed after re-enabling AI (not auto-opened)" - ); - }); - - multi_workspace.update_in(cx, |mw, window, cx| { - mw.toggle_sidebar(window, cx); - }); - multi_workspace.read_with(cx, |mw, _cx| { - assert!( - mw.is_sidebar_open(), - "Sidebar should open when toggled after re-enabling AI" - ); - }); - } -} diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 492b7a8f385730feaa06dfe3b5e8b4cc0a20bb59..9f0b035049ebb5bfbeef7211acee9ced5288bb47 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -341,6 +341,7 @@ pub fn read_serialized_multi_workspaces( .map(read_multi_workspace_state) .unwrap_or_default(); model::SerializedMultiWorkspace { + id: window_id.map(|id| model::MultiWorkspaceId(id.as_u64())), workspaces: group, state, } @@ -3877,7 +3878,6 @@ mod tests { window_10, MultiWorkspaceState { active_workspace_id: Some(WorkspaceId(2)), - sidebar_open: true, }, ) .await; @@ -3886,7 +3886,6 @@ mod tests { window_20, MultiWorkspaceState { active_workspace_id: Some(WorkspaceId(3)), - sidebar_open: false, }, ) .await; @@ -3924,23 +3923,20 @@ mod tests { // Should produce 3 groups: window 10, window 20, and the orphan. assert_eq!(results.len(), 3); - // Window 10 group: 2 workspaces, active_workspace_id = 2, sidebar open. + // Window 10 group: 2 workspaces, active_workspace_id = 2. let group_10 = &results[0]; assert_eq!(group_10.workspaces.len(), 2); assert_eq!(group_10.state.active_workspace_id, Some(WorkspaceId(2))); - assert_eq!(group_10.state.sidebar_open, true); - // Window 20 group: 1 workspace, active_workspace_id = 3, sidebar closed. + // Window 20 group: 1 workspace, active_workspace_id = 3. let group_20 = &results[1]; assert_eq!(group_20.workspaces.len(), 1); assert_eq!(group_20.state.active_workspace_id, Some(WorkspaceId(3))); - assert_eq!(group_20.state.sidebar_open, false); // Orphan group: no window_id, so state is default. let group_none = &results[2]; assert_eq!(group_none.workspaces.len(), 1); assert_eq!(group_none.state.active_workspace_id, None); - assert_eq!(group_none.state.sidebar_open, false); } #[gpui::test] diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 0971ebd0ddc9265ccf9ea10da7745ba59914db30..c5251f20be9313a50f2256c54823d8839bdfe7fd 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -63,18 +63,19 @@ pub struct SessionWorkspace { #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] pub struct MultiWorkspaceState { pub active_workspace_id: Option, - pub sidebar_open: bool, } -/// The serialized state of a single MultiWorkspace window from a previous session: -/// all workspaces that shared the window, which one was active, and whether the -/// sidebar was open. +/// The serialized state of a single MultiWorkspace window from a previous session. #[derive(Debug, Clone)] pub struct SerializedMultiWorkspace { + pub id: Option, pub workspaces: Vec, pub state: MultiWorkspaceState, } +#[derive(Debug, Clone, Copy)] +pub struct MultiWorkspaceId(pub u64); + #[derive(Debug, PartialEq, Clone)] pub(crate) struct SerializedWorkspace { pub(crate) id: WorkspaceId, diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 5e0b8a7f6eabbd652f1f429342a837aa0b43e6d2..9087cbba42b054c1b247bdf3d9402688de4b7add 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -34,7 +34,6 @@ pub struct StatusBar { right_items: Vec>, active_pane: Entity, _observe_active_pane: Subscription, - workspace_sidebar_open: bool, } impl Render for StatusBar { @@ -52,10 +51,9 @@ impl Render for StatusBar { .when(!(tiling.bottom || tiling.right), |el| { el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) }) - .when( - !(tiling.bottom || tiling.left) && !self.workspace_sidebar_open, - |el| el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING), - ) + .when(!(tiling.bottom || tiling.left), |el| { + el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) + }) // This border is to avoid a transparent gap in the rounded corners .mb(px(-1.)) .border_b(px(1.0)) @@ -91,17 +89,11 @@ impl StatusBar { _observe_active_pane: cx.observe_in(active_pane, window, |this, _, window, cx| { this.update_active_pane_item(window, cx) }), - workspace_sidebar_open: false, }; this.update_active_pane_item(window, cx); this } - pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context) { - self.workspace_sidebar_open = open; - cx.notify(); - } - pub fn add_left_item(&mut self, item: Entity, window: &mut Window, cx: &mut Context) where T: 'static + StatusItemView, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 90f05d07a3a87a53ca25a1dc15da7663a95984a8..b57b5028a4e5558b1f90c715463165ba68d914e3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -28,8 +28,8 @@ pub use crate::notifications::NotificationFrame; pub use dock::Panel; pub use multi_workspace::{ DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, - NewWorkspaceInWindow, NextWorkspaceInWindow, PreviousWorkspaceInWindow, Sidebar, SidebarEvent, - SidebarHandle, ToggleWorkspaceSidebar, + NewWorkspaceInWindow, NextWorkspaceInWindow, PreviousWorkspaceInWindow, + SIDEBAR_RESIZE_HANDLE_SIZE, ToggleWorkspaceSidebar, multi_workspace_enabled, }; pub use path_list::{PathList, SerializedPathList}; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; @@ -80,8 +80,8 @@ use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; pub use persistence::{ DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, model::{ - DockStructure, ItemId, SerializedMultiWorkspace, SerializedWorkspaceLocation, - SessionWorkspace, + DockStructure, ItemId, MultiWorkspaceId, SerializedMultiWorkspace, + SerializedWorkspaceLocation, SessionWorkspace, }, read_serialized_multi_workspaces, }; @@ -2154,12 +2154,6 @@ impl Workspace { &self.status_bar } - pub fn set_workspace_sidebar_open(&self, open: bool, cx: &mut App) { - self.status_bar.update(cx, |status_bar, cx| { - status_bar.set_workspace_sidebar_open(open, cx); - }); - } - pub fn status_bar_visible(&self, cx: &App) -> bool { StatusBarSettings::get_global(cx).show } @@ -8184,7 +8178,11 @@ pub async fn restore_multiworkspace( app_state: Arc, cx: &mut AsyncApp, ) -> anyhow::Result { - let SerializedMultiWorkspace { workspaces, state } = multi_workspace; + let SerializedMultiWorkspace { + workspaces, + state, + id: window_id, + } = multi_workspace; let mut group_iter = workspaces.into_iter(); let first = group_iter .next() @@ -8248,6 +8246,7 @@ pub async fn restore_multiworkspace( if let Some(target_id) = state.active_workspace_id { window_handle .update(cx, |multi_workspace, window, cx| { + multi_workspace.set_database_id(window_id); let target_index = multi_workspace .workspaces() .iter() @@ -8269,14 +8268,6 @@ pub async fn restore_multiworkspace( .ok(); } - if state.sidebar_open { - window_handle - .update(cx, |multi_workspace, _, cx| { - multi_workspace.open_sidebar(cx); - }) - .ok(); - } - window_handle .update(cx, |_, window, _cx| { window.activate_window(); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 9c0c892ad7105cc5be9b3dd548659aa1f12a7966..2f61121d9c0aeb80a77d36bc4836b33c63936584 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -182,7 +182,6 @@ settings.workspace = true settings_profile_selector.workspace = true settings_ui.workspace = true shellexpand.workspace = true -sidebar.workspace = true smol.workspace = true snippet_provider.workspace = true snippets_ui.workspace = true diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index ead16b911e3ccf9ebd1b9f54113cb01dca849e9d..37642b012edcd133dfe770a4c57c5404658582b5 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -103,8 +103,8 @@ use { feature_flags::FeatureFlagAppExt as _, git_ui::project_diff::ProjectDiff, gpui::{ - App, AppContext as _, Bounds, KeyBinding, Modifiers, SharedString, VisualTestAppContext, - WindowBounds, WindowHandle, WindowOptions, point, px, size, + Action as _, App, AppContext as _, Bounds, KeyBinding, Modifiers, SharedString, + VisualTestAppContext, WindowBounds, WindowHandle, WindowOptions, point, px, size, }, image::RgbaImage, project_panel::ProjectPanel, @@ -2649,22 +2649,6 @@ fn run_multi_workspace_sidebar_visual_tests( cx.run_until_parked(); - // Create the sidebar and register it on the MultiWorkspace - let sidebar = multi_workspace_window - .update(cx, |_multi_workspace, window, cx| { - let multi_workspace_handle = cx.entity(); - cx.new(|cx| sidebar::Sidebar::new(multi_workspace_handle, window, cx)) - }) - .context("Failed to create sidebar")?; - - multi_workspace_window - .update(cx, |multi_workspace, window, cx| { - multi_workspace.register_sidebar(sidebar.clone(), window, cx); - }) - .context("Failed to register sidebar")?; - - cx.run_until_parked(); - // Save test threads to the ThreadStore for each workspace let save_tasks = multi_workspace_window .update(cx, |multi_workspace, _window, cx| { @@ -2742,8 +2726,8 @@ fn run_multi_workspace_sidebar_visual_tests( // Open the sidebar multi_workspace_window - .update(cx, |multi_workspace, window, cx| { - multi_workspace.toggle_sidebar(window, cx); + .update(cx, |_multi_workspace, window, cx| { + window.dispatch_action(workspace::ToggleWorkspaceSidebar.boxed_clone(), cx); }) .context("Failed to toggle sidebar")?; @@ -3181,24 +3165,10 @@ edition = "2021" cx.run_until_parked(); - // Create and register the workspace sidebar - let sidebar = workspace_window - .update(cx, |_multi_workspace, window, cx| { - let multi_workspace_handle = cx.entity(); - cx.new(|cx| sidebar::Sidebar::new(multi_workspace_handle, window, cx)) - }) - .context("Failed to create sidebar")?; - - workspace_window - .update(cx, |multi_workspace, window, cx| { - multi_workspace.register_sidebar(sidebar.clone(), window, cx); - }) - .context("Failed to register sidebar")?; - // Open the sidebar workspace_window - .update(cx, |multi_workspace, window, cx| { - multi_workspace.toggle_sidebar(window, cx); + .update(cx, |_multi_workspace, window, cx| { + window.dispatch_action(workspace::ToggleWorkspaceSidebar.boxed_clone(), cx); }) .context("Failed to toggle sidebar")?; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6eee25e6faddae5fdaae7ac2704a10a979b30ce7..b64bcbf3ab9ab5e29fdd473a200c2367e3f6f777 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -68,7 +68,6 @@ use settings::{ initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content, update_settings_file, }; -use sidebar::Sidebar; use std::time::Duration; use std::{ borrow::Cow, @@ -389,20 +388,6 @@ pub fn initialize_workspace( }) .unwrap_or(true) }); - - let window_handle = window.window_handle(); - let multi_workspace_handle = cx.entity(); - cx.defer(move |cx| { - window_handle - .update(cx, |_, window, cx| { - let sidebar = - cx.new(|cx| Sidebar::new(multi_workspace_handle.clone(), window, cx)); - multi_workspace_handle.update(cx, |multi_workspace, cx| { - multi_workspace.register_sidebar(sidebar, window, cx); - }); - }) - .ok(); - }); }) .detach();