Add a setting for moving the sidebar to the right (#52457)

Mikayla Maki and Eric created

## Context

This adds a setting for controlling the sidebar side

## Self-Review Checklist

<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

---------

Co-authored-by: Eric <eric@zed.dev>

Change summary

Cargo.lock                                          |   2 
assets/icons/threads_sidebar_right_closed.svg       |   5 
assets/icons/threads_sidebar_right_open.svg         |   5 
assets/settings/default.json                        |   2 
crates/agent/src/tool_permissions.rs                |   1 
crates/agent_settings/src/agent_settings.rs         |  16 +
crates/agent_ui/src/agent_panel.rs                  |   6 
crates/agent_ui/src/agent_ui.rs                     |   2 
crates/agent_ui/src/threads_archive_view.rs         |  21 +
crates/collab_ui/src/notification_panel.rs          |   2 
crates/debugger_ui/src/debugger_panel.rs            |   2 
crates/git_ui/src/git_panel.rs                      |   2 
crates/icons/src/icons.rs                           |   2 
crates/platform_title_bar/src/platform_title_bar.rs |  47 ++-
crates/project_panel/src/project_panel.rs           |   2 
crates/settings_content/src/agent.rs                |  41 +++
crates/sidebar/Cargo.toml                           |   1 
crates/sidebar/src/sidebar.rs                       | 159 +++++++++-----
crates/terminal_view/src/terminal_panel.rs          |   2 
crates/title_bar/src/title_bar.rs                   | 114 ++-------
crates/workspace/Cargo.toml                         |   1 
crates/workspace/src/dock.rs                        |  14 +
crates/workspace/src/multi_workspace.rs             | 137 ++++++++++--
crates/workspace/src/status_bar.rs                  | 156 ++++++++++---
crates/workspace/src/workspace.rs                   |  47 ++-
25 files changed, 524 insertions(+), 265 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -15980,6 +15980,7 @@ dependencies = [
  "action_log",
  "agent",
  "agent-client-protocol",
+ "agent_settings",
  "agent_ui",
  "anyhow",
  "assistant_text_thread",
@@ -21516,6 +21517,7 @@ dependencies = [
 name = "workspace"
 version = "0.1.0"
 dependencies = [
+ "agent_settings",
  "any_vec",
  "anyhow",
  "async-recursion",

assets/icons/threads_sidebar_right_closed.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect opacity="0.1" width="5" height="12" rx="2" transform="matrix(-1 0 0 1 14 2)" fill="#C6CAD0"/>
+<path d="M9 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
+<rect x="2" y="2" width="12" height="12" rx="1.5" stroke="#C6CAD0" stroke-width="1.2"/>
+</svg>

assets/icons/threads_sidebar_right_open.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect opacity="0.8" width="5" height="12" rx="2" transform="matrix(-1 0 0 1 14 2)" fill="#C6CAD0"/>
+<path d="M9 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
+<rect x="2" y="2" width="12" height="12" rx="1.5" stroke="#C6CAD0" stroke-width="1.2"/>
+</svg>

assets/settings/default.json 🔗

@@ -943,6 +943,8 @@
     "button": true,
     // Where to dock the agent panel. Can be 'left', 'right' or 'bottom'.
     "dock": "right",
+    // Where to position the sidebar. Can be 'left', 'right', or 'follow_agent'.
+    "sidebar_side": "follow_agent",
     // Default width when the agent panel is docked to the left or right.
     "default_width": 640,
     // Default height when the agent panel is docked to the bottom.

crates/agent_settings/src/agent_settings.rs 🔗

@@ -12,7 +12,8 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{
     DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
-    NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, ToolPermissionMode,
+    NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, SidebarDockPosition,
+    SidebarSide, ToolPermissionMode,
 };
 
 pub use crate::agent_profile::*;
@@ -26,6 +27,7 @@ pub struct AgentSettings {
     pub enabled: bool,
     pub button: bool,
     pub dock: DockPosition,
+    pub sidebar_side: SidebarDockPosition,
     pub default_width: Pixels,
     pub default_height: Pixels,
     pub default_model: Option<LanguageModelSelection>,
@@ -77,6 +79,17 @@ impl AgentSettings {
         return None;
     }
 
+    pub fn sidebar_side(&self) -> SidebarSide {
+        match self.sidebar_side {
+            SidebarDockPosition::Left => SidebarSide::Left,
+            SidebarDockPosition::Right => SidebarSide::Right,
+            SidebarDockPosition::FollowAgent => match self.dock {
+                DockPosition::Right => SidebarSide::Right,
+                _ => SidebarSide::Left,
+            },
+        }
+    }
+
     pub fn set_message_editor_max_lines(&self) -> usize {
         self.message_editor_min_lines * 2
     }
@@ -407,6 +420,7 @@ impl Settings for AgentSettings {
             enabled: agent.enabled.unwrap(),
             button: agent.button.unwrap(),
             dock: agent.dock.unwrap(),
+            sidebar_side: agent.sidebar_side.unwrap(),
             default_width: px(agent.default_width.unwrap()),
             default_height: px(agent.default_height.unwrap()),
             default_model: Some(agent.default_model.unwrap()),

crates/agent_ui/src/agent_panel.rs 🔗

@@ -3186,13 +3186,17 @@ impl Panel for AgentPanel {
     }
 
     fn activation_priority(&self) -> u32 {
-        8
+        0
     }
 
     fn enabled(&self, cx: &App) -> bool {
         AgentSettings::get_global(cx).enabled(cx)
     }
 
+    fn is_agent_panel(&self) -> bool {
+        true
+    }
+
     fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
         self.zoomed
     }

crates/agent_ui/src/agent_ui.rs 🔗

@@ -648,7 +648,6 @@ mod tests {
             default_profile: AgentProfileId::default(),
             default_view: DefaultAgentView::Thread,
             profiles: Default::default(),
-
             notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
             play_sound_when_agent_done: false,
             single_file_review: false,
@@ -662,6 +661,7 @@ mod tests {
             tool_permissions: Default::default(),
             show_turn_stats: false,
             new_thread_location: Default::default(),
+            sidebar_side: Default::default(),
         };
 
         cx.update(|cx| {

crates/agent_ui/src/threads_archive_view.rs 🔗

@@ -7,6 +7,7 @@ use crate::{
 use acp_thread::AgentSessionInfo;
 use agent::ThreadStore;
 use agent_client_protocol as acp;
+use agent_settings::AgentSettings;
 use chrono::{DateTime, Datelike as _, Local, NaiveDate, TimeDelta, Utc};
 use editor::Editor;
 use fs::Fs;
@@ -17,6 +18,7 @@ use gpui::{
 use itertools::Itertools as _;
 use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
 use project::{AgentId, AgentServerStore};
+use settings::Settings as _;
 use theme::ActiveTheme;
 use ui::{
     ButtonLike, CommonAnimationExt, ContextMenu, ContextMenuEntry, Divider, HighlightedLabel,
@@ -795,7 +797,12 @@ impl ThreadsArchiveView {
 
     fn render_header(&self, window: &Window, cx: &mut Context<Self>) -> impl IntoElement {
         let has_query = !self.filter_editor.read(cx).text(cx).is_empty();
-        let traffic_lights = cfg!(target_os = "macos") && !window.is_fullscreen();
+        let sidebar_on_left = matches!(
+            AgentSettings::get_global(cx).sidebar_side(),
+            settings::SidebarSide::Left
+        );
+        let traffic_lights =
+            cfg!(target_os = "macos") && !window.is_fullscreen() && sidebar_on_left;
         let header_height = platform_title_bar_height(window);
         let show_focus_keybinding =
             self.selection.is_some() && !self.filter_editor.focus_handle(cx).is_focused(window);
@@ -804,15 +811,21 @@ impl ThreadsArchiveView {
             .h(header_height)
             .mt_px()
             .pb_px()
-            .when(traffic_lights, |this| {
-                this.pl(px(ui::utils::TRAFFIC_LIGHT_PADDING))
+            .map(|this| {
+                if traffic_lights {
+                    this.pl(px(ui::utils::TRAFFIC_LIGHT_PADDING))
+                } else {
+                    this.pl_1p5()
+                }
             })
             .pr_1p5()
             .gap_1()
             .justify_between()
             .border_b_1()
             .border_color(cx.theme().colors().border)
-            .child(Divider::vertical().color(ui::DividerColor::Border))
+            .when(traffic_lights, |this| {
+                this.child(Divider::vertical().color(ui::DividerColor::Border))
+            })
             .child(
                 h_flex()
                     .ml_1()

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -1601,7 +1601,7 @@ impl Panel for DebugPanel {
     }
 
     fn activation_priority(&self) -> u32 {
-        9
+        7
     }
 
     fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}

crates/icons/src/icons.rs 🔗

@@ -244,6 +244,8 @@ pub enum IconName {
     ThreadFromSummary,
     ThreadsSidebarLeftClosed,
     ThreadsSidebarLeftOpen,
+    ThreadsSidebarRightClosed,
+    ThreadsSidebarRightOpen,
     ThumbsDown,
     ThumbsUp,
     TodoComplete,

crates/platform_title_bar/src/platform_title_bar.rs 🔗

@@ -4,8 +4,8 @@ mod system_window_tabs;
 use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
 use gpui::{
     Action, AnyElement, App, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement,
-    MouseButton, ParentElement, StatefulInteractiveElement, Styled, Window, WindowButtonLayout,
-    WindowControlArea, div, px,
+    MouseButton, ParentElement, StatefulInteractiveElement, Styled, WeakEntity, Window,
+    WindowButtonLayout, WindowControlArea, div, px,
 };
 use project::DisableAiSettings;
 use settings::Settings;
@@ -15,6 +15,7 @@ use ui::{
     prelude::*,
     utils::{TRAFFIC_LIGHT_PADDING, platform_title_bar_height},
 };
+use workspace::{MultiWorkspace, SidebarRenderState, SidebarSide};
 
 use crate::{
     platforms::{platform_linux, platform_windows},
@@ -32,7 +33,7 @@ pub struct PlatformTitleBar {
     should_move: bool,
     system_window_tabs: Entity<SystemWindowTabs>,
     button_layout: Option<WindowButtonLayout>,
-    workspace_sidebar_open: bool,
+    multi_workspace: Option<WeakEntity<MultiWorkspace>>,
 }
 
 impl PlatformTitleBar {
@@ -47,10 +48,19 @@ impl PlatformTitleBar {
             should_move: false,
             system_window_tabs,
             button_layout: None,
-            workspace_sidebar_open: false,
+            multi_workspace: None,
         }
     }
 
+    pub fn with_multi_workspace(mut self, multi_workspace: WeakEntity<MultiWorkspace>) -> Self {
+        self.multi_workspace = Some(multi_workspace);
+        self
+    }
+
+    pub fn set_multi_workspace(&mut self, multi_workspace: WeakEntity<MultiWorkspace>) {
+        self.multi_workspace = Some(multi_workspace);
+    }
+
     pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
         if cfg!(any(target_os = "linux", target_os = "freebsd")) {
             if window.is_window_active() && !self.should_move {
@@ -92,13 +102,12 @@ 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();
+    fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState {
+        self.multi_workspace
+            .as_ref()
+            .and_then(|mw| mw.upgrade())
+            .map(|mw| mw.read(cx).sidebar_render_state(cx))
+            .unwrap_or_default()
     }
 
     pub fn is_multi_workspace_enabled(cx: &App) -> bool {
@@ -116,8 +125,7 @@ impl Render for PlatformTitleBar {
         let children = mem::take(&mut self.children);
 
         let button_layout = self.effective_button_layout(&decorations, cx);
-        let is_multiworkspace_sidebar_open =
-            PlatformTitleBar::is_multi_workspace_enabled(cx) && self.is_workspace_sidebar_open();
+        let sidebar = self.sidebar_render_state(cx);
 
         let title_bar = h_flex()
             .window_control_area(WindowControlArea::Drag)
@@ -168,7 +176,7 @@ impl Render for PlatformTitleBar {
                 if window.is_fullscreen() {
                     this.pl_2()
                 } else if self.platform_style == PlatformStyle::Mac
-                    && !is_multiworkspace_sidebar_open
+                    && !(sidebar.open && sidebar.side == SidebarSide::Left)
                 {
                     this.pl(px(TRAFFIC_LIGHT_PADDING))
                 } else if let Some(button_layout) =
@@ -186,11 +194,14 @@ impl Render for PlatformTitleBar {
             .map(|el| match decorations {
                 Decorations::Server => el,
                 Decorations::Client { tiling, .. } => el
-                    .when(!(tiling.top || tiling.right), |el| {
-                        el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
-                    })
                     .when(
-                        !(tiling.top || tiling.left) && !is_multiworkspace_sidebar_open,
+                        !(tiling.top || tiling.right)
+                            && !(sidebar.open && sidebar.side == SidebarSide::Right),
+                        |el| el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
+                    )
+                    .when(
+                        !(tiling.top || tiling.left)
+                            && !(sidebar.open && sidebar.side == SidebarSide::Left),
                         |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
                     )
                     // this border is to avoid a transparent gap in the rounded corners

crates/settings_content/src/agent.rs 🔗

@@ -33,6 +33,39 @@ pub enum NewThreadLocation {
     NewWorktree,
 }
 
+/// Where to position the sidebar.
+#[derive(
+    Clone,
+    Copy,
+    Debug,
+    Default,
+    PartialEq,
+    Eq,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    strum::VariantArray,
+    strum::VariantNames,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum SidebarDockPosition {
+    /// Always show the sidebar on the left side.
+    Left,
+    /// Always show the sidebar on the right side.
+    Right,
+    /// Show the sidebar on the same side as the agent panel.
+    #[default]
+    FollowAgent,
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub enum SidebarSide {
+    #[default]
+    Left,
+    Right,
+}
+
 #[with_fallible_options]
 #[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
 pub struct AgentSettingsContent {
@@ -48,6 +81,10 @@ pub struct AgentSettingsContent {
     ///
     /// Default: right
     pub dock: Option<DockPosition>,
+    /// Where to position the sidebar.
+    ///
+    /// Default: follow_agent
+    pub sidebar_side: Option<SidebarDockPosition>,
     /// Default width in pixels when the agent panel is docked to the left or right.
     ///
     /// Default: 640
@@ -157,6 +194,10 @@ impl AgentSettingsContent {
         self.dock = Some(dock);
     }
 
+    pub fn set_sidebar_side(&mut self, position: SidebarDockPosition) {
+        self.sidebar_side = Some(position);
+    }
+
     pub fn set_model(&mut self, language_model: LanguageModelSelection) {
         self.default_model = Some(language_model)
     }

crates/sidebar/Cargo.toml 🔗

@@ -19,6 +19,7 @@ acp_thread.workspace = true
 action_log.workspace = true
 agent.workspace = true
 agent-client-protocol.workspace = true
+agent_settings.workspace = true
 agent_ui.workspace = true
 anyhow.workspace = true
 chrono.workspace = true

crates/sidebar/src/sidebar.rs 🔗

@@ -1,6 +1,7 @@
 use acp_thread::ThreadStatus;
 use action_log::DiffStats;
 use agent_client_protocol::{self as acp};
+use agent_settings::AgentSettings;
 use agent_ui::thread_metadata_store::{SidebarThreadMetadataStore, ThreadMetadata};
 use agent_ui::threads_archive_view::{
     ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp,
@@ -37,7 +38,8 @@ use util::ResultExt as _;
 use util::path_list::PathList;
 use workspace::{
     AddFolderToProject, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, Open,
-    Sidebar as WorkspaceSidebar, ToggleWorkspaceSidebar, Workspace, WorkspaceId,
+    Sidebar as WorkspaceSidebar, SidebarSide, ToggleWorkspaceSidebar, Workspace, WorkspaceId,
+    sidebar_side_context_menu,
 };
 
 use zed_actions::OpenRecent;
@@ -2874,7 +2876,9 @@ impl Sidebar {
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
         let has_query = self.has_filter_query(cx);
-        let traffic_lights = cfg!(target_os = "macos") && !window.is_fullscreen();
+        let sidebar_on_left = self.side(cx) == SidebarSide::Left;
+        let traffic_lights =
+            cfg!(target_os = "macos") && !window.is_fullscreen() && sidebar_on_left;
         let header_height = platform_title_bar_height(window);
 
         h_flex()
@@ -2928,38 +2932,92 @@ impl Sidebar {
     }
 
     fn render_sidebar_toggle_button(&self, _cx: &mut Context<Self>) -> impl IntoElement {
-        IconButton::new("sidebar-close-toggle", IconName::ThreadsSidebarLeftOpen)
-            .icon_size(IconSize::Small)
-            .tooltip(Tooltip::element(move |_window, cx| {
-                v_flex()
-                    .gap_1()
-                    .child(
-                        h_flex()
-                            .gap_2()
-                            .justify_between()
-                            .child(Label::new("Toggle Sidebar"))
-                            .child(KeyBinding::for_action(&ToggleWorkspaceSidebar, cx)),
-                    )
-                    .child(
-                        h_flex()
-                            .pt_1()
-                            .gap_2()
-                            .border_t_1()
-                            .border_color(cx.theme().colors().border_variant)
-                            .justify_between()
-                            .child(Label::new("Focus Sidebar"))
-                            .child(KeyBinding::for_action(&FocusWorkspaceSidebar, cx)),
-                    )
-                    .into_any_element()
-            }))
-            .on_click(|_, window, cx| {
-                if let Some(multi_workspace) = window.root::<MultiWorkspace>().flatten() {
-                    multi_workspace.update(cx, |multi_workspace, cx| {
-                        multi_workspace.close_sidebar(window, cx);
-                    });
-                }
+        let on_right = AgentSettings::get_global(_cx).sidebar_side() == SidebarSide::Right;
+
+        sidebar_side_context_menu("sidebar-toggle-menu", _cx)
+            .anchor(if on_right {
+                gpui::Corner::BottomRight
+            } else {
+                gpui::Corner::BottomLeft
+            })
+            .attach(if on_right {
+                gpui::Corner::TopRight
+            } else {
+                gpui::Corner::TopLeft
+            })
+            .trigger(move |_is_active, _window, _cx| {
+                let icon = if on_right {
+                    IconName::ThreadsSidebarRightOpen
+                } else {
+                    IconName::ThreadsSidebarLeftOpen
+                };
+                IconButton::new("sidebar-close-toggle", icon)
+                    .icon_size(IconSize::Small)
+                    .tooltip(Tooltip::element(move |_window, cx| {
+                        v_flex()
+                            .gap_1()
+                            .child(
+                                h_flex()
+                                    .gap_2()
+                                    .justify_between()
+                                    .child(Label::new("Toggle Sidebar"))
+                                    .child(KeyBinding::for_action(&ToggleWorkspaceSidebar, cx)),
+                            )
+                            .child(
+                                h_flex()
+                                    .pt_1()
+                                    .gap_2()
+                                    .border_t_1()
+                                    .border_color(cx.theme().colors().border_variant)
+                                    .justify_between()
+                                    .child(Label::new("Focus Sidebar"))
+                                    .child(KeyBinding::for_action(&FocusWorkspaceSidebar, cx)),
+                            )
+                            .into_any_element()
+                    }))
+                    .on_click(|_, window, cx| {
+                        if let Some(multi_workspace) = window.root::<MultiWorkspace>().flatten() {
+                            multi_workspace.update(cx, |multi_workspace, cx| {
+                                multi_workspace.close_sidebar(window, cx);
+                            });
+                        }
+                    })
             })
     }
+
+    fn render_sidebar_bottom_bar(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
+        let on_right = self.side(cx) == SidebarSide::Right;
+        let is_archive = matches!(self.view, SidebarView::Archive(..));
+        let action_buttons = h_flex()
+            .gap_1()
+            .child(
+                IconButton::new("archive", IconName::Archive)
+                    .icon_size(IconSize::Small)
+                    .toggle_state(is_archive)
+                    .tooltip(move |_, cx| {
+                        Tooltip::for_action("Toggle Archived Threads", &ToggleArchive, cx)
+                    })
+                    .on_click(cx.listener(|this, _, window, cx| {
+                        this.toggle_archive(&ToggleArchive, window, cx);
+                    })),
+            )
+            .child(self.render_recent_projects_button(cx));
+        let border_color = cx.theme().colors().border;
+        let toggle_button = self.render_sidebar_toggle_button(cx);
+
+        let bar = h_flex()
+            .p_1()
+            .gap_1()
+            .justify_between()
+            .border_t_1()
+            .border_color(border_color);
+
+        if on_right {
+            bar.child(action_buttons).child(toggle_button)
+        } else {
+            bar.child(toggle_button).child(action_buttons)
+        }
+    }
 }
 
 impl Sidebar {
@@ -3054,6 +3112,10 @@ impl WorkspaceSidebar for Sidebar {
         matches!(self.view, SidebarView::ThreadList)
     }
 
+    fn side(&self, cx: &App) -> SidebarSide {
+        AgentSettings::get_global(cx).sidebar_side()
+    }
+
     fn prepare_for_focus(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
         self.selection = None;
         cx.notify();
@@ -3108,7 +3170,8 @@ impl Render for Sidebar {
             .h_full()
             .w(self.width)
             .bg(bg)
-            .border_r_1()
+            .when(self.side(cx) == SidebarSide::Left, |el| el.border_r_1())
+            .when(self.side(cx) == SidebarSide::Right, |el| el.border_l_1())
             .border_color(color.border)
             .map(|this| match &self.view {
                 SidebarView::ThreadList => this
@@ -3140,35 +3203,7 @@ impl Render for Sidebar {
                     }),
                 SidebarView::Archive(archive_view) => this.child(archive_view.clone()),
             })
-            .child(
-                h_flex()
-                    .p_1()
-                    .gap_1()
-                    .justify_between()
-                    .border_t_1()
-                    .border_color(cx.theme().colors().border)
-                    .child(self.render_sidebar_toggle_button(cx))
-                    .child(
-                        h_flex()
-                            .gap_1()
-                            .child(
-                                IconButton::new("archive", IconName::Archive)
-                                    .icon_size(IconSize::Small)
-                                    .toggle_state(matches!(self.view, SidebarView::Archive(..)))
-                                    .tooltip(move |_, cx| {
-                                        Tooltip::for_action(
-                                            "Toggle Archived Threads",
-                                            &ToggleArchive,
-                                            cx,
-                                        )
-                                    })
-                                    .on_click(cx.listener(|this, _, window, cx| {
-                                        this.toggle_archive(&ToggleArchive, window, cx);
-                                    })),
-                            )
-                            .child(self.render_recent_projects_button(cx)),
-                    ),
-            )
+            .child(self.render_sidebar_bottom_bar(cx))
     }
 }
 

crates/title_bar/src/title_bar.rs 🔗

@@ -81,7 +81,8 @@ pub fn init(cx: &mut App) {
         let Some(window) = window else {
             return;
         };
-        let item = cx.new(|cx| TitleBar::new("title-bar", workspace, window, cx));
+        let multi_workspace = workspace.multi_workspace().cloned();
+        let item = cx.new(|cx| TitleBar::new("title-bar", workspace, multi_workspace, window, cx));
         workspace.set_titlebar_item(item.into(), window, cx);
 
         workspace.register_action(|workspace, _: &SimulateUpdateAvailable, _window, cx| {
@@ -161,7 +162,18 @@ pub struct TitleBar {
 
 impl Render for TitleBar {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        self.sync_multi_workspace(window, cx);
+        if self.multi_workspace.is_none() {
+            if let Some(mw) = self
+                .workspace
+                .upgrade()
+                .and_then(|ws| ws.read(cx).multi_workspace().cloned())
+            {
+                self.multi_workspace = Some(mw.clone());
+                self.platform_titlebar.update(cx, |titlebar, _cx| {
+                    titlebar.set_multi_workspace(mw);
+                });
+            }
+        }
 
         let title_bar_settings = *TitleBarSettings::get_global(cx);
         let button_layout = title_bar_settings.button_layout;
@@ -308,6 +320,7 @@ impl TitleBar {
     pub fn new(
         id: impl Into<ElementId>,
         workspace: &Workspace,
+        multi_workspace: Option<WeakEntity<MultiWorkspace>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -385,52 +398,19 @@ impl TitleBar {
         });
 
         let update_version = cx.new(|cx| UpdateVersion::new(cx));
-        let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx));
-
-        // 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>()
-                else {
-                    return;
-                };
-
-                let _ = cx.update(|cx| {
-                    let Ok(multi_workspace) = multi_workspace_handle.entity(cx) else {
-                        return;
-                    };
-
-                    let is_open = multi_workspace.read(cx).sidebar_open();
-                    platform_titlebar.update(cx, |titlebar, cx| {
-                        titlebar.set_workspace_sidebar_open(is_open, cx);
-                    });
-
-                    let platform_titlebar = platform_titlebar.clone();
-                    let subscription = cx.observe(&multi_workspace, move |mw, cx| {
-                        let is_open = mw.read(cx).sidebar_open();
-                        platform_titlebar.update(cx, |titlebar, cx| {
-                            titlebar.set_workspace_sidebar_open(is_open, cx);
-                        });
-                    });
-
-                    if let Some(this) = this.upgrade() {
-                        this.update(cx, |this, _| {
-                            this._subscriptions.push(subscription);
-                            this.multi_workspace = Some(multi_workspace.downgrade());
-                        });
-                    }
-                });
-            })
-            .detach();
-        }
+        let platform_titlebar = cx.new(|cx| {
+            let mut titlebar = PlatformTitleBar::new(id, cx);
+            if let Some(mw) = multi_workspace.clone() {
+                titlebar = titlebar.with_multi_workspace(mw);
+            }
+            titlebar
+        });
 
         let mut this = Self {
             platform_titlebar,
             application_menu,
             workspace: workspace.weak_handle(),
-            multi_workspace: None,
+            multi_workspace,
             project,
             user_store,
             client,
@@ -446,46 +426,6 @@ impl TitleBar {
         this
     }
 
-    /// Used to update the title bar state in case the workspace has
-    /// been moved to a new window through the threads sidebar.
-    fn sync_multi_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let current = window
-            .root::<MultiWorkspace>()
-            .flatten()
-            .map(|mw| mw.entity_id());
-
-        let tracked = self
-            .multi_workspace
-            .as_ref()
-            .and_then(|weak| weak.upgrade())
-            .map(|mw| mw.entity_id());
-
-        if current == tracked {
-            return;
-        }
-
-        let Some(multi_workspace) = window.root::<MultiWorkspace>().flatten() else {
-            self.multi_workspace = None;
-            return;
-        };
-
-        let is_open = multi_workspace.read(cx).sidebar_open();
-        self.platform_titlebar.update(cx, |titlebar, cx| {
-            titlebar.set_workspace_sidebar_open(is_open, cx);
-        });
-
-        let platform_titlebar = self.platform_titlebar.clone();
-        let subscription = cx.observe(&multi_workspace, move |_this, mw, cx| {
-            let is_open = mw.read(cx).sidebar_open();
-            platform_titlebar.update(cx, |titlebar, cx| {
-                titlebar.set_workspace_sidebar_open(is_open, cx);
-            });
-        });
-
-        self.multi_workspace = Some(multi_workspace.downgrade());
-        self._subscriptions.push(subscription);
-    }
-
     fn worktree_count(&self, cx: &App) -> usize {
         self.project.read(cx).visible_worktrees(cx).count()
     }
@@ -777,7 +717,13 @@ impl TitleBar {
             "Open Recent Project".to_string()
         };
 
-        let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open();
+        let is_sidebar_open = self
+            .multi_workspace
+            .as_ref()
+            .and_then(|mw| mw.upgrade())
+            .map(|mw| mw.read(cx).sidebar_open())
+            .unwrap_or(false)
+            && PlatformTitleBar::is_multi_workspace_enabled(cx);
 
         let is_threads_list_view_active = self
             .multi_workspace

crates/workspace/Cargo.toml 🔗

@@ -27,6 +27,7 @@ test-support = [
 
 [dependencies]
 any_vec.workspace = true
+agent_settings.workspace = true
 anyhow.workspace = true
 async-recursion.workspace = true
 client.workspace = true

crates/workspace/src/dock.rs 🔗

@@ -69,6 +69,9 @@ pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
     fn enabled(&self, _cx: &App) -> bool {
         true
     }
+    fn is_agent_panel(&self) -> bool {
+        false
+    }
 }
 
 pub trait PanelHandle: Send + Sync {
@@ -95,6 +98,7 @@ pub trait PanelHandle: Send + Sync {
     fn to_any(&self) -> AnyView;
     fn activation_priority(&self, cx: &App) -> u32;
     fn enabled(&self, cx: &App) -> bool;
+    fn is_agent_panel(&self, cx: &App) -> bool;
     fn move_to_next_position(&self, window: &mut Window, cx: &mut App) {
         let current_position = self.position(window, cx);
         let next_position = [
@@ -207,6 +211,10 @@ where
     fn enabled(&self, cx: &App) -> bool {
         self.read(cx).enabled(cx)
     }
+
+    fn is_agent_panel(&self, cx: &App) -> bool {
+        self.read(cx).is_agent_panel()
+    }
 }
 
 impl From<&dyn PanelHandle> for AnyView {
@@ -720,6 +728,12 @@ impl Dock {
         self.panel_entries.len()
     }
 
+    pub fn has_agent_panel(&self, cx: &App) -> bool {
+        self.panel_entries
+            .iter()
+            .any(|entry| entry.panel.is_agent_panel(cx))
+    }
+
     pub fn activate_panel(&mut self, panel_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
         if Some(panel_ix) != self.active_panel_index {
             if let Some(active_panel) = self.active_panel_entry() {

crates/workspace/src/multi_workspace.rs 🔗

@@ -9,6 +9,7 @@ use project::DisableAiSettings;
 #[cfg(any(test, feature = "test-support"))]
 use project::Project;
 use settings::Settings;
+pub use settings::SidebarSide;
 use std::future::Future;
 use std::path::PathBuf;
 use std::sync::Arc;
@@ -16,6 +17,10 @@ use ui::prelude::*;
 use util::ResultExt;
 use zed_actions::agents_sidebar::MoveWorkspaceToNewWindow;
 
+use agent_settings::AgentSettings;
+use settings::SidebarDockPosition;
+use ui::{ContextMenu, right_click_menu};
+
 const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
 
 use crate::{
@@ -39,6 +44,47 @@ actions!(
     ]
 );
 
+#[derive(Default)]
+pub struct SidebarRenderState {
+    pub open: bool,
+    pub side: SidebarSide,
+}
+
+pub fn sidebar_side_context_menu(
+    id: impl Into<ElementId>,
+    cx: &App,
+) -> ui::RightClickMenu<ContextMenu> {
+    let current_position = AgentSettings::get_global(cx).sidebar_side;
+    right_click_menu(id).menu(move |window, cx| {
+        let fs = <dyn fs::Fs>::global(cx);
+        ContextMenu::build(window, cx, move |mut menu, _, _cx| {
+            let positions: [(SidebarDockPosition, &str); 3] = [
+                (SidebarDockPosition::Left, "Left"),
+                (SidebarDockPosition::Right, "Right"),
+                (SidebarDockPosition::FollowAgent, "Follow Agent Panel"),
+            ];
+            for (position, label) in positions {
+                let fs = fs.clone();
+                menu = menu.toggleable_entry(
+                    label,
+                    position == current_position,
+                    IconPosition::Start,
+                    None,
+                    move |_window, cx| {
+                        settings::update_settings_file(fs.clone(), cx, move |settings, _cx| {
+                            settings
+                                .agent
+                                .get_or_insert_default()
+                                .set_sidebar_side(position);
+                        });
+                    },
+                );
+            }
+            menu
+        })
+    })
+}
+
 pub enum MultiWorkspaceEvent {
     ActiveWorkspaceChanged,
     WorkspaceAdded(Entity<Workspace>),
@@ -49,6 +95,7 @@ pub trait Sidebar: 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 side(&self, _cx: &App) -> SidebarSide;
 
     fn is_threads_list_view_active(&self) -> bool {
         true
@@ -68,6 +115,8 @@ pub trait SidebarHandle: 'static + Send + Sync {
     fn entity_id(&self) -> EntityId;
 
     fn is_threads_list_view_active(&self, cx: &App) -> bool;
+
+    fn side(&self, cx: &App) -> SidebarSide;
 }
 
 #[derive(Clone)]
@@ -116,6 +165,10 @@ impl<T: Sidebar> SidebarHandle for Entity<T> {
     fn is_threads_list_view_active(&self, cx: &App) -> bool {
         self.read(cx).is_threads_list_view_active()
     }
+
+    fn side(&self, cx: &App) -> SidebarSide {
+        self.read(cx).side(cx)
+    }
 }
 
 pub struct MultiWorkspace {
@@ -132,6 +185,19 @@ pub struct MultiWorkspace {
 impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
 
 impl MultiWorkspace {
+    pub fn sidebar_side(&self, cx: &App) -> SidebarSide {
+        self.sidebar
+            .as_ref()
+            .map_or(SidebarSide::Left, |s| s.side(cx))
+    }
+
+    pub fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState {
+        SidebarRenderState {
+            open: self.sidebar_open() && self.multi_workspace_enabled(cx),
+            side: self.sidebar_side(cx),
+        }
+    }
+
     pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
             if let Some(task) = this._serialize_task.take() {
@@ -149,6 +215,10 @@ impl MultiWorkspace {
                 }
             });
         Self::subscribe_to_workspace(&workspace, cx);
+        let weak_self = cx.weak_entity();
+        workspace.update(cx, |workspace, cx| {
+            workspace.set_multi_workspace(weak_self, cx);
+        });
         Self {
             window_id: window.window_handle().window_id(),
             workspaces: vec![workspace],
@@ -167,20 +237,8 @@ impl MultiWorkspace {
 
     pub fn register_sidebar<T: Sidebar>(&mut self, sidebar: Entity<T>, cx: &mut Context<Self>) {
         self._subscriptions
-            .push(cx.observe(&sidebar, |this, _, cx| {
-                let has_notifications = this.sidebar_has_notifications(cx);
-                let is_open = this.sidebar_open;
-                let show_toggle = this.multi_workspace_enabled(cx);
-                for workspace in &this.workspaces {
-                    workspace.update(cx, |workspace, cx| {
-                        workspace.set_workspace_sidebar_open(
-                            is_open,
-                            has_notifications,
-                            show_toggle,
-                            cx,
-                        );
-                    });
-                }
+            .push(cx.observe(&sidebar, |_this, _, cx| {
+                cx.notify();
             }));
         self.sidebar = Some(Box::new(sidebar));
     }
@@ -266,11 +324,8 @@ impl MultiWorkspace {
     pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
         self.sidebar_open = true;
         let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
-        let has_notifications = self.sidebar_has_notifications(cx);
-        let show_toggle = self.multi_workspace_enabled(cx);
         for workspace in &self.workspaces {
-            workspace.update(cx, |workspace, cx| {
-                workspace.set_workspace_sidebar_open(true, has_notifications, show_toggle, cx);
+            workspace.update(cx, |workspace, _cx| {
                 workspace.set_sidebar_focus_handle(sidebar_focus_handle.clone());
             });
         }
@@ -280,11 +335,8 @@ impl MultiWorkspace {
 
     pub fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         self.sidebar_open = false;
-        let has_notifications = self.sidebar_has_notifications(cx);
-        let show_toggle = self.multi_workspace_enabled(cx);
         for workspace in &self.workspaces {
-            workspace.update(cx, |workspace, cx| {
-                workspace.set_workspace_sidebar_open(false, has_notifications, show_toggle, cx);
+            workspace.update(cx, |workspace, _cx| {
                 workspace.set_sidebar_focus_handle(None);
             });
         }
@@ -381,13 +433,14 @@ impl MultiWorkspace {
         } else {
             if self.sidebar_open {
                 let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
-                let has_notifications = self.sidebar_has_notifications(cx);
-                let show_toggle = self.multi_workspace_enabled(cx);
-                workspace.update(cx, |workspace, cx| {
-                    workspace.set_workspace_sidebar_open(true, has_notifications, show_toggle, cx);
+                workspace.update(cx, |workspace, _cx| {
                     workspace.set_sidebar_focus_handle(sidebar_focus_handle);
                 });
             }
+            let weak_self = cx.weak_entity();
+            workspace.update(cx, |workspace, cx| {
+                workspace.set_multi_workspace(weak_self, cx);
+            });
             Self::subscribe_to_workspace(&workspace, cx);
             self.workspaces.push(workspace.clone());
             cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
@@ -767,6 +820,8 @@ 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_side = self.sidebar_side(cx);
+        let sidebar_on_right = sidebar_side == SidebarSide::Right;
 
         let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open() {
             self.sidebar.as_ref().map(|sidebar_handle| {
@@ -777,7 +832,12 @@ impl Render for MultiWorkspace {
                     div()
                         .id("sidebar-resize-handle")
                         .absolute()
-                        .right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
+                        .when(!sidebar_on_right, |el| {
+                            el.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
+                        })
+                        .when(sidebar_on_right, |el| {
+                            el.left(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
+                        })
                         .top(px(0.))
                         .h_full()
                         .w(SIDEBAR_RESIZE_HANDLE_SIZE)
@@ -817,6 +877,12 @@ impl Render for MultiWorkspace {
             None
         };
 
+        let (left_sidebar, right_sidebar) = if sidebar_on_right {
+            (None, sidebar)
+        } else {
+            (sidebar, None)
+        };
+
         let ui_font = theme::setup_ui_font(window, cx);
         let text_color = cx.theme().colors().text;
 
@@ -855,16 +921,23 @@ impl Render for MultiWorkspace {
                     self.sidebar_open() && self.multi_workspace_enabled(cx),
                     |this| {
                         this.on_drag_move(cx.listener(
-                            |this: &mut Self, e: &DragMoveEvent<DraggedSidebar>, _window, cx| {
+                            move |this: &mut Self,
+                                  e: &DragMoveEvent<DraggedSidebar>,
+                                  window,
+                                  cx| {
                                 if let Some(sidebar) = &this.sidebar {
-                                    let new_width = e.event.position.x;
+                                    let new_width = if sidebar_on_right {
+                                        window.bounds().size.width - e.event.position.x
+                                    } else {
+                                        e.event.position.x
+                                    };
                                     sidebar.set_width(Some(new_width), cx);
                                 }
                             },
                         ))
-                        .children(sidebar)
                     },
                 )
+                .children(left_sidebar)
                 .child(
                     div()
                         .flex()
@@ -873,11 +946,13 @@ impl Render for MultiWorkspace {
                         .overflow_hidden()
                         .child(self.workspace().clone()),
                 )
+                .children(right_sidebar)
                 .child(self.workspace().read(cx).modal_layer.clone()),
             window,
             cx,
             Tiling {
-                left: multi_workspace_enabled && self.sidebar_open(),
+                left: !sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
+                right: sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
                 ..Tiling::default()
             },
         )

crates/workspace/src/status_bar.rs 🔗

@@ -1,7 +1,10 @@
-use crate::{ItemHandle, MultiWorkspace, Pane, ToggleWorkspaceSidebar};
+use crate::{
+    ItemHandle, MultiWorkspace, Pane, SidebarSide, ToggleWorkspaceSidebar,
+    sidebar_side_context_menu,
+};
 use gpui::{
-    AnyView, App, Context, Decorations, Entity, IntoElement, ParentElement, Render, Styled,
-    Subscription, Window,
+    AnyView, App, Context, Corner, Decorations, Entity, IntoElement, ParentElement, Render, Styled,
+    Subscription, WeakEntity, Window,
 };
 use std::any::TypeId;
 use theme::CLIENT_SIDE_DECORATION_ROUNDING;
@@ -29,18 +32,45 @@ trait StatusItemViewHandle: Send {
     fn item_type(&self) -> TypeId;
 }
 
+#[derive(Default)]
+struct SidebarStatus {
+    open: bool,
+    side: SidebarSide,
+    has_notifications: bool,
+    show_toggle: bool,
+}
+
+impl SidebarStatus {
+    fn query(multi_workspace: &Option<WeakEntity<MultiWorkspace>>, cx: &App) -> Self {
+        multi_workspace
+            .as_ref()
+            .and_then(|mw| mw.upgrade())
+            .map(|mw| {
+                let mw = mw.read(cx);
+                let enabled = mw.multi_workspace_enabled(cx);
+                Self {
+                    open: mw.sidebar_open() && enabled,
+                    side: mw.sidebar_side(cx),
+                    has_notifications: mw.sidebar_has_notifications(cx),
+                    show_toggle: enabled,
+                }
+            })
+            .unwrap_or_default()
+    }
+}
+
 pub struct StatusBar {
     left_items: Vec<Box<dyn StatusItemViewHandle>>,
     right_items: Vec<Box<dyn StatusItemViewHandle>>,
     active_pane: Entity<Pane>,
+    multi_workspace: Option<WeakEntity<MultiWorkspace>>,
     _observe_active_pane: Subscription,
-    workspace_sidebar_open: bool,
-    sidebar_has_notifications: bool,
-    show_sidebar_toggle: bool,
 }
 
 impl Render for StatusBar {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let sidebar = SidebarStatus::query(&self.multi_workspace, cx);
+
         h_flex()
             .w_full()
             .justify_between()
@@ -50,11 +80,14 @@ impl Render for StatusBar {
             .map(|el| match window.window_decorations() {
                 Decorations::Server => el,
                 Decorations::Client { tiling, .. } => el
-                    .when(!(tiling.bottom || tiling.right), |el| {
-                        el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
-                    })
                     .when(
-                        !(tiling.bottom || tiling.left) && !self.workspace_sidebar_open,
+                        !(tiling.bottom || tiling.right)
+                            && !(sidebar.open && sidebar.side == SidebarSide::Right),
+                        |el| el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING),
+                    )
+                    .when(
+                        !(tiling.bottom || tiling.left)
+                            && !(sidebar.open && sidebar.side == SidebarSide::Left),
                         |el| el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING),
                     )
                     // This border is to avoid a transparent gap in the rounded corners
@@ -62,44 +95,77 @@ impl Render for StatusBar {
                     .border_b(px(1.0))
                     .border_color(cx.theme().colors().status_bar_background),
             })
-            .child(self.render_left_tools(cx))
-            .child(self.render_right_tools())
+            .child(self.render_left_tools(&sidebar, cx))
+            .child(self.render_right_tools(&sidebar, cx))
     }
 }
 
 impl StatusBar {
-    fn render_left_tools(&self, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render_left_tools(
+        &self,
+        sidebar: &SidebarStatus,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
         h_flex()
             .gap_1()
             .min_w_0()
             .overflow_x_hidden()
             .when(
-                self.show_sidebar_toggle && !self.workspace_sidebar_open,
-                |this| this.child(self.render_sidebar_toggle(cx)),
+                sidebar.show_toggle && !sidebar.open && sidebar.side == SidebarSide::Left,
+                |this| this.child(self.render_sidebar_toggle(sidebar, cx)),
             )
             .children(self.left_items.iter().map(|item| item.to_any()))
     }
 
-    fn render_right_tools(&self) -> impl IntoElement {
+    fn render_right_tools(
+        &self,
+        sidebar: &SidebarStatus,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
         h_flex()
             .flex_shrink_0()
             .gap_1()
             .overflow_x_hidden()
             .children(self.right_items.iter().rev().map(|item| item.to_any()))
+            .when(
+                sidebar.show_toggle && !sidebar.open && sidebar.side == SidebarSide::Right,
+                |this| this.child(self.render_sidebar_toggle(sidebar, cx)),
+            )
     }
 
-    fn render_sidebar_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
-        h_flex()
-            .gap_0p5()
-            .child(
+    fn render_sidebar_toggle(
+        &self,
+        sidebar: &SidebarStatus,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
+        let on_right = sidebar.side == SidebarSide::Right;
+        let has_notifications = sidebar.has_notifications;
+        let indicator_border = cx.theme().colors().status_bar_background;
+
+        let toggle = sidebar_side_context_menu("sidebar-status-toggle-menu", cx)
+            .anchor(if on_right {
+                Corner::BottomRight
+            } else {
+                Corner::BottomLeft
+            })
+            .attach(if on_right {
+                Corner::TopRight
+            } else {
+                Corner::TopLeft
+            })
+            .trigger(move |_is_active, _window, _cx| {
                 IconButton::new(
                     "toggle-workspace-sidebar",
-                    IconName::ThreadsSidebarLeftClosed,
+                    if on_right {
+                        IconName::ThreadsSidebarRightClosed
+                    } else {
+                        IconName::ThreadsSidebarLeftClosed
+                    },
                 )
                 .icon_size(IconSize::Small)
-                .when(self.sidebar_has_notifications, |this| {
+                .when(has_notifications, |this| {
                     this.indicator(Indicator::dot().color(Color::Accent))
-                        .indicator_border_color(Some(cx.theme().colors().status_bar_background))
+                        .indicator_border_color(Some(indicator_border))
                 })
                 .tooltip(move |_, cx| {
                     Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
@@ -110,41 +176,47 @@ impl StatusBar {
                             multi_workspace.toggle_sidebar(window, cx);
                         });
                     }
-                }),
-            )
-            .child(Divider::vertical().color(ui::DividerColor::Border))
+                })
+            });
+
+        h_flex()
+            .gap_0p5()
+            .when(on_right, |this| {
+                this.child(Divider::vertical().color(ui::DividerColor::Border))
+            })
+            .child(toggle)
+            .when(!on_right, |this| {
+                this.child(Divider::vertical().color(ui::DividerColor::Border))
+            })
     }
 }
 
 impl StatusBar {
-    pub fn new(active_pane: &Entity<Pane>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+    pub fn new(
+        active_pane: &Entity<Pane>,
+        multi_workspace: Option<WeakEntity<MultiWorkspace>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
         let mut this = Self {
             left_items: Default::default(),
             right_items: Default::default(),
             active_pane: active_pane.clone(),
+            multi_workspace,
             _observe_active_pane: cx.observe_in(active_pane, window, |this, _, window, cx| {
                 this.update_active_pane_item(window, cx)
             }),
-            workspace_sidebar_open: false,
-            sidebar_has_notifications: false,
-            show_sidebar_toggle: 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 set_sidebar_has_notifications(&mut self, has: bool, cx: &mut Context<Self>) {
-        self.sidebar_has_notifications = has;
-        cx.notify();
-    }
-
-    pub fn set_show_sidebar_toggle(&mut self, show: bool, cx: &mut Context<Self>) {
-        self.show_sidebar_toggle = show;
+    pub fn set_multi_workspace(
+        &mut self,
+        multi_workspace: WeakEntity<MultiWorkspace>,
+        cx: &mut Context<Self>,
+    ) {
+        self.multi_workspace = Some(multi_workspace);
         cx.notify();
     }
 

crates/workspace/src/workspace.rs 🔗

@@ -29,7 +29,7 @@ pub use dock::Panel;
 pub use multi_workspace::{
     CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace,
     MultiWorkspaceEvent, NextWorkspace, PreviousWorkspace, Sidebar, SidebarHandle,
-    ToggleWorkspaceSidebar,
+    SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar, sidebar_side_context_menu,
 };
 pub use path_list::{PathList, SerializedPathList};
 pub use toast_layer::{ToastAction, ToastLayer, ToastView};
@@ -1342,6 +1342,7 @@ pub struct Workspace {
     removing: bool,
     _panels_task: Option<Task<Result<()>>>,
     sidebar_focus_handle: Option<FocusHandle>,
+    multi_workspace: Option<WeakEntity<MultiWorkspace>>,
 }
 
 impl EventEmitter<Event> for Workspace {}
@@ -1626,8 +1627,13 @@ impl Workspace {
         let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
         let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
         let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
+        let multi_workspace = window
+            .root::<MultiWorkspace>()
+            .flatten()
+            .map(|mw| mw.downgrade());
         let status_bar = cx.new(|cx| {
-            let mut status_bar = StatusBar::new(&center_pane.clone(), window, cx);
+            let mut status_bar =
+                StatusBar::new(&center_pane.clone(), multi_workspace.clone(), window, cx);
             status_bar.add_left_item(left_dock_buttons, window, cx);
             status_bar.add_right_item(right_dock_buttons, window, cx);
             status_bar.add_right_item(bottom_dock_buttons, window, cx);
@@ -1754,6 +1760,7 @@ impl Workspace {
             last_open_dock_positions: Vec::new(),
             removing: false,
             sidebar_focus_handle: None,
+            multi_workspace,
         }
     }
 
@@ -2127,6 +2134,13 @@ impl Workspace {
         }
     }
 
+    pub fn agent_panel_position(&self, cx: &App) -> Option<DockPosition> {
+        self.all_docks().into_iter().find_map(|dock| {
+            let dock = dock.read(cx);
+            dock.has_agent_panel(cx).then_some(dock.position())
+        })
+    }
+
     pub fn panel_size_state<T: Panel>(&self, cx: &App) -> Option<dock::PanelSizeState> {
         self.all_docks().into_iter().find_map(|dock| {
             let dock = dock.read(cx);
@@ -2327,20 +2341,6 @@ impl Workspace {
         &self.status_bar
     }
 
-    pub fn set_workspace_sidebar_open(
-        &self,
-        open: bool,
-        has_notifications: bool,
-        show_toggle: bool,
-        cx: &mut App,
-    ) {
-        self.status_bar.update(cx, |status_bar, cx| {
-            status_bar.set_workspace_sidebar_open(open, cx);
-            status_bar.set_sidebar_has_notifications(has_notifications, cx);
-            status_bar.set_show_sidebar_toggle(show_toggle, cx);
-        });
-    }
-
     pub fn set_sidebar_focus_handle(&mut self, handle: Option<FocusHandle>) {
         self.sidebar_focus_handle = handle;
     }
@@ -2349,6 +2349,21 @@ impl Workspace {
         StatusBarSettings::get_global(cx).show
     }
 
+    pub fn multi_workspace(&self) -> Option<&WeakEntity<MultiWorkspace>> {
+        self.multi_workspace.as_ref()
+    }
+
+    pub fn set_multi_workspace(
+        &mut self,
+        multi_workspace: WeakEntity<MultiWorkspace>,
+        cx: &mut App,
+    ) {
+        self.status_bar.update(cx, |status_bar, cx| {
+            status_bar.set_multi_workspace(multi_workspace.clone(), cx);
+        });
+        self.multi_workspace = Some(multi_workspace);
+    }
+
     pub fn app_state(&self) -> &Arc<AppState> {
         &self.app_state
     }