Detailed changes
@@ -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",
@@ -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 }
@@ -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
@@ -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<gpui::WindowId, gpui::WeakEntity<crate::sidebar::Sidebar>>,
+);
+
+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::<SidebarsByWindow>()
+ .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<Entity<crate::sidebar::Sidebar>> {
+ let window_id = window.window_handle().window_id();
+ let multi_workspace = window.root::<MultiWorkspace>().flatten()?;
+
+ if !cx.has_global::<SidebarsByWindow>() {
+ cx.set_global(SidebarsByWindow::default());
+ }
+
+ cx.global_mut::<SidebarsByWindow>()
+ .0
+ .retain(|_, weak| weak.upgrade().is_some());
+
+ let existing = cx
+ .global::<SidebarsByWindow>()
+ .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::<SidebarsByWindow>()
+ .0
+ .insert(window_id, sidebar.downgrade());
+ Some(sidebar)
+}
+
fn read_serialized_panel(workspace_id: workspace::WorkspaceId) -> Option<SerializedAgentPanel> {
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::<AgentPanel>(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::<AgentPanel>(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<String>,
on_boarding_upsell_dismissed: AtomicBool,
_active_view_observation: Option<Subscription>,
+ pub(crate) sidebar: Option<Entity<crate::sidebar::Sidebar>>,
}
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<Self>) -> Option<AnyElement> {
+ 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<Self>) -> Option<AnyElement> {
+ 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<Self>) -> 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<DraggedSidebar>, _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(),
}
}
}
@@ -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;
@@ -2340,7 +2340,7 @@ impl ConnectionView {
}
if let Some(multi_workspace) = window.root::<MultiWorkspace>().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
@@ -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::<bool>(&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<MultiWorkspace>,
+ persistence_key: Option<u64>,
+ is_open: bool,
width: Pixels,
focus_handle: FocusHandle,
filter_editor: Entity<Editor>,
@@ -186,11 +208,8 @@ pub struct Sidebar {
active_entry_index: Option<usize>,
collapsed_groups: HashSet<PathList>,
expanded_groups: HashMap<PathList, usize>,
- recent_projects_popover_handle: PopoverMenuHandle<RecentProjects>,
}
-impl EventEmitter<SidebarEvent> for Sidebar {}
-
impl Sidebar {
pub fn new(
multi_workspace: Entity<MultiWorkspace>,
@@ -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::<AgentPanel>(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<Self>) -> 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<Self>) -> 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<Pixels>, cx: &mut Context<Self>) {
- self.width = width.unwrap_or(DEFAULT_WIDTH).clamp(MIN_WIDTH, MAX_WIDTH);
+ pub fn set_open(&mut self, open: bool, cx: &mut Context<Self>) {
+ 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<Self>) {
+ 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<Self>,
+ ) {
+ 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<Pixels>, cx: &mut Context<Self>) {
+ 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<Self>) -> 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<MultiWorkspace>,
cx: &mut gpui::VisualTestContext,
) -> Entity<Sidebar> {
- 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<MultiWorkspace>,
+ cx: &mut gpui::VisualTestContext,
+ ) -> (Entity<Sidebar>, Entity<AgentPanel>) {
+ 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::<AgentPanel>() {
+ 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<Sidebar>,
- multi_workspace: &Entity<MultiWorkspace>,
- cx: &mut gpui::VisualTestContext,
- ) {
- multi_workspace.update_in(cx, |mw, window, cx| {
- mw.toggle_sidebar(window, cx);
- });
+ fn open_and_focus_sidebar(sidebar: &Entity<Sidebar>, 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<project::Project> {
- 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| <dyn fs::Fs>::set_global(fs.clone(), cx));
- project::Project::test(fs, [worktree_path.as_ref()], cx).await
- }
-
fn add_agent_panel(
workspace: &Entity<Workspace>,
project: &Entity<project::Project>,
@@ -2536,23 +2487,12 @@ mod tests {
})
}
- fn setup_sidebar_with_agent_panel(
- multi_workspace: &Entity<MultiWorkspace>,
- project: &Entity<project::Project>,
- cx: &mut gpui::VisualTestContext,
- ) -> (Entity<Sidebar>, Entity<AgentPanel>) {
- 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"
);
});
}
@@ -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");
@@ -31,8 +31,6 @@ pub struct PlatformTitleBar {
children: SmallVec<[AnyElement; 2]>,
should_move: bool,
system_window_tabs: Entity<SystemWindowTabs>,
- 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>) {
- 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>,
- ) {
- self.sidebar_has_notifications = has_notifications;
- cx.notify();
- }
-
pub fn is_multi_workspace_enabled(cx: &App) -> bool {
cx.has_flag::<AgentV2FeatureFlag>() && !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.))
@@ -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"] }
@@ -1 +0,0 @@
-../../LICENSE-GPL
@@ -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
@@ -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<TitleBar>, cx| {
let Some(multi_workspace_handle) = window_handle.downcast::<MultiWorkspace>()
@@ -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<Self>,
- ) -> Option<AnyElement> {
- if !cx.has_flag::<AgentV2FeatureFlag>() || 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<Self>,
- ) -> impl IntoElement {
+ pub fn render_project_name(&self, _: &mut Window, cx: &mut Context<Self>) -> 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<Self>,
- ) -> 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<Self>) -> Option<impl IntoElement> {
let effective_worktree = self.effective_active_worktree(cx)?;
let repository = self.get_repository_for_worktree(&effective_worktree, cx)?;
@@ -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<SidebarEvent> + Focusable + Render + Sized {
- fn width(&self, cx: &App) -> Pixels;
- fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
- 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<Pixels>, 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<T: Sidebar> SidebarHandle for Entity<T> {
- fn width(&self, cx: &App) -> Pixels {
- self.read(cx).width(cx)
- }
-
- fn set_width(&self, width: Option<Pixels>, 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<Entity<Workspace>>,
+ database_id: Option<MultiWorkspaceId>,
active_workspace_index: usize,
- sidebar: Option<Box<dyn SidebarHandle>>,
- sidebar_open: bool,
- _sidebar_subscription: Option<Subscription>,
pending_removal_tasks: Vec<Task<()>>,
_serialize_task: Option<Task<()>>,
_create_task: Option<Task<()>>,
@@ -131,6 +63,10 @@ pub struct MultiWorkspace {
impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
+pub fn multi_workspace_enabled(cx: &App) -> bool {
+ cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
+}
+
impl MultiWorkspace {
pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> 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::<settings::SettingsStore>(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<T: Sidebar>(
- &mut self,
- sidebar: Entity<T>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- 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::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
- }
-
- pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- 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<Self>) {
- 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>) {
- 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>) {
- 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<Self>) {
@@ -318,10 +129,6 @@ impl MultiWorkspace {
.detach();
}
- pub fn is_sidebar_open(&self) -> bool {
- self.sidebar_open
- }
-
pub fn workspace(&self) -> &Entity<Workspace> {
&self.workspaces[self.active_workspace_index]
}
@@ -335,7 +142,7 @@ impl MultiWorkspace {
}
pub fn activate(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) {
- 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<MultiWorkspaceId> {
+ self.database_id
+ }
+
+ pub fn set_database_id(&mut self, id: Option<MultiWorkspaceId>) {
+ self.database_id = id;
+ }
+
pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
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::<T>(cx)
}
- pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
+ pub fn active_workspace_database_id(&self, cx: &App) -> Option<WorkspaceId> {
self.workspace().read(cx).database_id()
}
@@ -583,7 +392,7 @@ impl MultiWorkspace {
}
pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- 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<Result<()>> {
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<Self>) -> impl IntoElement {
- let multi_workspace_enabled = self.multi_workspace_enabled(cx);
-
- let sidebar: Option<AnyElement> = 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<DraggedSidebar>, _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"
- );
- });
- }
-}
@@ -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]
@@ -63,18 +63,19 @@ pub struct SessionWorkspace {
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct MultiWorkspaceState {
pub active_workspace_id: Option<WorkspaceId>,
- 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<MultiWorkspaceId>,
pub workspaces: Vec<SessionWorkspace>,
pub state: MultiWorkspaceState,
}
+#[derive(Debug, Clone, Copy)]
+pub struct MultiWorkspaceId(pub u64);
+
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct SerializedWorkspace {
pub(crate) id: WorkspaceId,
@@ -34,7 +34,6 @@ pub struct StatusBar {
right_items: Vec<Box<dyn StatusItemViewHandle>>,
active_pane: Entity<Pane>,
_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>) {
- self.workspace_sidebar_open = open;
- cx.notify();
- }
-
pub fn add_left_item<T>(&mut self, item: Entity<T>, window: &mut Window, cx: &mut Context<Self>)
where
T: 'static + StatusItemView,
@@ -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<AppState>,
cx: &mut AsyncApp,
) -> anyhow::Result<MultiWorkspaceRestoreResult> {
- 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();
@@ -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
@@ -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")?;
@@ -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();