Rearrange panels in agent mode

Max Brunsfeld created

Change summary

crates/agent_ui/src/agent_panel.rs                                    |  12 
crates/agent_ui/src/connection_view.rs                                |   4 
crates/collab/tests/integration/following_tests.rs                    |   7 
crates/collab/tests/integration/git_tests.rs                          |  10 
crates/collab/tests/integration/remote_editing_collaboration_tests.rs |   7 
crates/debugger_ui/src/tests.rs                                       |   7 
crates/outline_panel/src/outline_panel.rs                             |   3 
crates/project_panel/src/project_panel_tests.rs                       |  27 
crates/sidebar/src/sidebar.rs                                         |   4 
crates/workspace/src/dock.rs                                          | 110 
crates/workspace/src/multi_workspace.rs                               |   5 
crates/workspace/src/workspace.rs                                     | 107 
crates/zed/src/visual_test_runner.rs                                  |   3 
crates/zed/src/zed.rs                                                 | 119 
14 files changed, 314 insertions(+), 111 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -4908,7 +4908,8 @@ mod tests {
             let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
             let panel =
                 cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
-            workspace.add_panel(panel, window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel, position, window, cx);
         });
 
         cx.run_until_parked();
@@ -5132,7 +5133,8 @@ mod tests {
             let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
             let panel =
                 cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
-            workspace.add_panel(panel.clone(), window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel.clone(), position, window, cx);
             panel
         });
 
@@ -5242,7 +5244,8 @@ mod tests {
             let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
             let panel =
                 cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
-            workspace.add_panel(panel.clone(), window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel.clone(), position, window, cx);
             panel
         });
 
@@ -5327,7 +5330,8 @@ mod tests {
             let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
             let panel =
                 cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
-            workspace.add_panel(panel.clone(), window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel.clone(), position, window, cx);
             panel
         });
 

crates/agent_ui/src/connection_view.rs 🔗

@@ -2780,6 +2780,7 @@ pub(crate) mod tests {
     use std::path::{Path, PathBuf};
     use std::rc::Rc;
     use std::sync::Arc;
+    use workspace::dock::Panel;
     use workspace::{Item, MultiWorkspace};
 
     use crate::agent_panel;
@@ -3459,7 +3460,8 @@ pub(crate) mod tests {
                 cx.new(|cx| TextThreadStore::fake(workspace.project().clone(), cx));
             let panel =
                 cx.new(|cx| crate::AgentPanel::new(workspace, text_thread_store, None, window, cx));
-            workspace.add_panel(panel, window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel, position, window, cx);
 
             // Open the dock and activate the agent panel so it's visible
             workspace.focus_panel::<crate::AgentPanel>(window, cx);

crates/collab/tests/integration/following_tests.rs 🔗

@@ -18,8 +18,8 @@ use settings::SettingsStore;
 use text::{Point, ToPoint};
 use util::{path, rel_path::rel_path, test::sample_text};
 use workspace::{
-    CloseWindow, CollaboratorId, MultiWorkspace, ParticipantLocation, SplitDirection, Workspace,
-    item::ItemHandle as _,
+    CloseWindow, CollaboratorId, MultiWorkspace, Panel as _, ParticipantLocation, SplitDirection,
+    Workspace, item::ItemHandle as _,
 };
 
 use super::TestClient;
@@ -534,7 +534,8 @@ async fn test_basic_following(
         // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
         let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
         workspace_b.update_in(cx_b, |workspace, window, cx| {
-            workspace.add_panel(panel, window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel, position, window, cx);
             workspace.toggle_panel_focus::<TestPanel>(window, cx);
         });
         executor.run_until_parked();

crates/collab/tests/integration/git_tests.rs 🔗

@@ -12,6 +12,7 @@ use project::ProjectPath;
 use serde_json::json;
 
 use util::{path, rel_path::rel_path};
+use workspace::dock::Panel;
 use workspace::{MultiWorkspace, Workspace};
 
 use crate::TestServer;
@@ -371,12 +372,14 @@ async fn test_diff_stat_sync_between_host_and_downstream_client(
 
     let panel_a = workspace_a.update_in(cx_a, GitPanel::new_test);
     workspace_a.update_in(cx_a, |workspace, window, cx| {
-        workspace.add_panel(panel_a.clone(), window, cx);
+        let position = panel_a.read(cx).position(window, cx);
+        workspace.add_panel(panel_a.clone(), position, window, cx);
     });
 
     let panel_b = workspace_b.update_in(cx_b, GitPanel::new_test);
     workspace_b.update_in(cx_b, |workspace, window, cx| {
-        workspace.add_panel(panel_b.clone(), window, cx);
+        let position = panel_b.read(cx).position(window, cx);
+        workspace.add_panel(panel_b.clone(), position, window, cx);
     });
 
     cx_a.run_until_parked();
@@ -488,7 +491,8 @@ async fn test_diff_stat_sync_between_host_and_downstream_client(
     let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
     let panel_b = workspace_b.update_in(cx_b, GitPanel::new_test);
     workspace_b.update_in(cx_b, |workspace, window, cx| {
-        workspace.add_panel(panel_b.clone(), window, cx);
+        let position = panel_b.read(cx).position(window, cx);
+        workspace.add_panel(panel_b.clone(), position, window, cx);
     });
     cx_b.run_until_parked();
 

crates/collab/tests/integration/remote_editing_collaboration_tests.rs 🔗

@@ -42,6 +42,7 @@ use std::{
 };
 use task::TcpArgumentsTemplate;
 use util::{path, rel_path::rel_path};
+use workspace::dock::Panel;
 
 #[gpui::test(iterations = 10)]
 async fn test_sharing_an_ssh_remote_project(
@@ -783,7 +784,8 @@ async fn test_remote_server_debugger(
         .unwrap();
 
     workspace.update_in(cx_a, |workspace, window, cx| {
-        workspace.add_panel(debugger_panel, window, cx);
+        let position = debugger_panel.read(cx).position(window, cx);
+        workspace.add_panel(debugger_panel, position, window, cx);
     });
 
     cx_a.run_until_parked();
@@ -896,7 +898,8 @@ async fn test_slow_adapter_startup_retries(
         .unwrap();
 
     workspace.update_in(cx_a, |workspace, window, cx| {
-        workspace.add_panel(debugger_panel, window, cx);
+        let position = debugger_panel.read(cx).position(window, cx);
+        workspace.add_panel(debugger_panel, position, window, cx);
     });
 
     cx_a.run_until_parked();

crates/debugger_ui/src/tests.rs 🔗

@@ -9,6 +9,7 @@ use settings::SettingsStore;
 use task::SharedTaskContext;
 use terminal_view::terminal_panel::TerminalPanel;
 use workspace::MultiWorkspace;
+use workspace::dock::Panel;
 
 use crate::{debugger_panel::DebugPanel, session::DebugSession};
 
@@ -82,8 +83,10 @@ pub async fn init_test_workspace(
     workspace_handle
         .update(cx, |multi, window, cx| {
             multi.workspace().update(cx, |workspace, cx| {
-                workspace.add_panel(debugger_panel, window, cx);
-                workspace.add_panel(terminal_panel, window, cx);
+                let position = debugger_panel.read(cx).position(window, cx);
+                workspace.add_panel(debugger_panel, position, window, cx);
+                let position = terminal_panel.read(cx).position(window, cx);
+                workspace.add_panel(terminal_panel, position, window, cx);
             });
         })
         .unwrap();

crates/outline_panel/src/outline_panel.rs 🔗

@@ -6812,7 +6812,8 @@ outline: struct OutlineEntryExcerpt
         window
             .update(cx, |multi_workspace, window, cx| {
                 multi_workspace.workspace().update(cx, |workspace, cx| {
-                    workspace.add_panel(outline_panel, window, cx);
+                    let position = outline_panel.read(cx).position(window, cx);
+                    workspace.add_panel(outline_panel, position, window, cx);
                 });
             })
             .unwrap();

crates/project_panel/src/project_panel_tests.rs 🔗

@@ -11,6 +11,7 @@ use std::path::{Path, PathBuf};
 use util::{path, paths::PathStyle, rel_path::rel_path};
 use workspace::{
     AppState, ItemHandle, MultiWorkspace, Pane, Workspace,
+    dock::DockPosition,
     item::{Item, ProjectItem},
     register_project_item,
 };
@@ -527,7 +528,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
     let cx = &mut VisualTestContext::from_window(window.into(), cx);
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
     cx.run_until_parked();
@@ -960,7 +961,7 @@ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
     let cx = &mut VisualTestContext::from_window(window.into(), cx);
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
     cx.run_until_parked();
@@ -1073,7 +1074,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
     let cx = &mut VisualTestContext::from_window(window.into(), cx);
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
     cx.run_until_parked();
@@ -2335,7 +2336,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
     let cx = &mut VisualTestContext::from_window(window.into(), cx);
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
     cx.run_until_parked();
@@ -2541,7 +2542,7 @@ async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppCon
     let cx = &mut VisualTestContext::from_window(window.into(), cx);
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
     cx.run_until_parked();
@@ -2807,7 +2808,7 @@ async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) {
     let cx = &mut VisualTestContext::from_window(window.into(), cx);
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
     cx.run_until_parked();
@@ -5269,7 +5270,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
     let cx = &mut VisualTestContext::from_window(window.into(), cx);
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
     cx.run_until_parked();
@@ -5454,7 +5455,7 @@ async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppC
     let cx = &mut VisualTestContext::from_window(window.into(), cx);
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
     cx.run_until_parked();
@@ -7471,7 +7472,7 @@ async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
 
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
     cx.run_until_parked();
@@ -7551,7 +7552,7 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC
 
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
     cx.run_until_parked();
@@ -7696,7 +7697,7 @@ async fn test_create_entry_with_trailing_dot_windows(cx: &mut gpui::TestAppConte
 
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
     cx.run_until_parked();
@@ -9305,7 +9306,7 @@ async fn test_preserve_temporary_unfolded_active_index_on_blur_from_context_menu
 
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
 
@@ -9489,7 +9490,7 @@ async fn run_create_file_in_folded_path_case(
 
     let panel = workspace.update_in(cx, |workspace, window, cx| {
         let panel = ProjectPanel::new(workspace, window, cx);
-        workspace.add_panel(panel.clone(), window, cx);
+        workspace.add_panel(panel.clone(), DockPosition::Left, window, cx);
         panel
     });
 

crates/sidebar/src/sidebar.rs 🔗

@@ -1494,6 +1494,7 @@ mod tests {
     use settings::SettingsStore;
     use std::sync::Arc;
     use util::path_list::PathList;
+    use workspace::dock::Panel;
 
     fn init_test(cx: &mut TestAppContext) {
         cx.update(|cx| {
@@ -2500,7 +2501,8 @@ mod tests {
         workspace.update_in(cx, |workspace, window, cx| {
             let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
             let panel = cx.new(|cx| AgentPanel::test_new(workspace, text_thread_store, window, cx));
-            workspace.add_panel(panel.clone(), window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel.clone(), position, window, cx);
             panel
         })
     }

crates/workspace/src/dock.rs 🔗

@@ -83,6 +83,13 @@ pub trait PanelHandle: Send + Sync {
     fn to_any(&self) -> AnyView;
     fn activation_priority(&self, cx: &App) -> u32;
     fn enabled(&self, cx: &App) -> bool;
+    fn add_to_dock(
+        &self,
+        dock: &mut Dock,
+        workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut Context<Dock>,
+    ) -> usize;
     fn move_to_next_position(&self, window: &mut Window, cx: &mut App) {
         let current_position = self.position(window, cx);
         let next_position = [
@@ -187,6 +194,16 @@ where
     fn enabled(&self, cx: &App) -> bool {
         self.read(cx).enabled(cx)
     }
+
+    fn add_to_dock(
+        &self,
+        dock: &mut Dock,
+        workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut Context<Dock>,
+    ) -> usize {
+        dock.add_panel(self.clone(), workspace, window, cx)
+    }
 }
 
 impl From<&dyn PanelHandle> for AnyView {
@@ -262,7 +279,7 @@ impl DockPosition {
 
 struct PanelEntry {
     panel: Arc<dyn PanelHandle>,
-    _subscriptions: [Subscription; 3],
+    _subscriptions: [Subscription; 2],
 }
 
 pub struct PanelButtons {
@@ -467,57 +484,6 @@ impl Dock {
     ) -> usize {
         let subscriptions = [
             cx.observe(&panel, |_, _, cx| cx.notify()),
-            cx.observe_global_in::<SettingsStore>(window, {
-                let workspace = workspace.clone();
-                let panel = panel.clone();
-
-                move |this, window, cx| {
-                    let new_position = panel.read(cx).position(window, cx);
-                    if new_position == this.position {
-                        return;
-                    }
-
-                    let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
-                        if panel.is_zoomed(window, cx) {
-                            workspace.zoomed_position = Some(new_position);
-                        }
-                        match new_position {
-                            DockPosition::Left => &workspace.left_dock,
-                            DockPosition::Bottom => &workspace.bottom_dock,
-                            DockPosition::Right => &workspace.right_dock,
-                        }
-                        .clone()
-                    }) else {
-                        return;
-                    };
-
-                    let was_visible = this.is_open()
-                        && this.visible_panel().is_some_and(|active_panel| {
-                            active_panel.panel_id() == Entity::entity_id(&panel)
-                        });
-
-                    this.remove_panel(&panel, window, cx);
-
-                    new_dock.update(cx, |new_dock, cx| {
-                        new_dock.remove_panel(&panel, window, cx);
-                    });
-
-                    new_dock.update(cx, |new_dock, cx| {
-                        let index =
-                            new_dock.add_panel(panel.clone(), workspace.clone(), window, cx);
-                        if was_visible {
-                            new_dock.set_open(true, window, cx);
-                            new_dock.activate_panel(index, window, cx);
-                        }
-                    });
-
-                    workspace
-                        .update(cx, |workspace, cx| {
-                            workspace.serialize_workspace(window, cx);
-                        })
-                        .ok();
-                }
-            }),
             cx.subscribe_in(
                 &panel,
                 window,
@@ -666,6 +632,46 @@ impl Dock {
         }
     }
 
+    pub fn remove_panel_by_id(
+        &mut self,
+        panel_id: EntityId,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> bool {
+        if let Some(panel_ix) = self
+            .panel_entries
+            .iter()
+            .position(|entry| entry.panel.panel_id() == panel_id)
+        {
+            if let Some(active_panel_index) = self.active_panel_index.as_mut() {
+                match panel_ix.cmp(active_panel_index) {
+                    std::cmp::Ordering::Less => {
+                        *active_panel_index -= 1;
+                    }
+                    std::cmp::Ordering::Equal => {
+                        self.active_panel_index = None;
+                        self.set_open(false, window, cx);
+                    }
+                    std::cmp::Ordering::Greater => {}
+                }
+            }
+
+            self.panel_entries.remove(panel_ix);
+            cx.notify();
+
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn panel_ids(&self) -> Vec<EntityId> {
+        self.panel_entries
+            .iter()
+            .map(|entry| entry.panel.panel_id())
+            .collect()
+    }
+
     pub fn panels_len(&self) -> usize {
         self.panel_entries.len()
     }

crates/workspace/src/multi_workspace.rs 🔗

@@ -146,7 +146,7 @@ impl MultiWorkspace {
             active_workspace_index: 0,
             sidebar: None,
             sidebar_open: false,
-            is_singleton: false,
+            is_singleton: true,
             _sidebar_subscription: None,
             pending_removal_tasks: Vec::new(),
             _serialize_task: None,
@@ -488,11 +488,12 @@ impl MultiWorkspace {
     pub fn add_panel<T: Panel>(
         &mut self,
         panel: Entity<T>,
+        position: DockPosition,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         self.workspace().update(cx, |workspace, cx| {
-            workspace.add_panel(panel, window, cx);
+            workspace.add_panel(panel, position, window, cx);
         });
     }
 

crates/workspace/src/workspace.rs 🔗

@@ -2121,6 +2121,7 @@ impl Workspace {
     pub fn add_panel<T: Panel>(
         &mut self,
         panel: Entity<T>,
+        position: DockPosition,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -2128,8 +2129,7 @@ impl Workspace {
         cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
             .detach();
 
-        let dock_position = panel.position(window, cx);
-        let dock = self.dock_at_position(dock_position);
+        let dock = self.dock_at_position(position);
         let any_panel = panel.to_any();
 
         dock.update(cx, |dock, cx| {
@@ -2139,6 +2139,79 @@ impl Workspace {
         cx.emit(Event::PanelAdded(any_panel));
     }
 
+    pub fn move_panel_to_dock(
+        &mut self,
+        panel_id: EntityId,
+        new_position: DockPosition,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let current_dock_position = self
+            .all_docks()
+            .iter()
+            .find(|dock| dock.read(cx).panel_for_id(panel_id).is_some())
+            .map(|dock| dock.read(cx).position());
+
+        let Some(current_dock_position) = current_dock_position else {
+            return;
+        };
+
+        if current_dock_position == new_position {
+            return;
+        }
+
+        let current_dock = self.dock_at_position(current_dock_position).clone();
+
+        let was_visible = current_dock.read(cx).is_open()
+            && current_dock
+                .read(cx)
+                .visible_panel()
+                .is_some_and(|active_panel| active_panel.panel_id() == panel_id);
+
+        let panel_handle = current_dock.read(cx).panel_for_id(panel_id).cloned();
+
+        let Some(panel_handle) = panel_handle else {
+            return;
+        };
+
+        if panel_handle.is_zoomed(window, cx) {
+            self.zoomed_position = Some(new_position);
+        }
+
+        current_dock.update(cx, |dock, cx| {
+            dock.remove_panel_by_id(panel_id, window, cx);
+        });
+
+        let new_dock = self.dock_at_position(new_position).clone();
+
+        new_dock.update(cx, |dock, cx| {
+            dock.remove_panel_by_id(panel_id, window, cx);
+        });
+
+        let weak_self = self.weak_self.clone();
+        new_dock.update(cx, |dock, cx| {
+            let index = panel_handle.add_to_dock(dock, weak_self, window, cx);
+            if was_visible {
+                dock.set_open(true, window, cx);
+                dock.activate_panel(index, window, cx);
+            }
+        });
+
+        self.serialize_workspace(window, cx);
+    }
+
+    pub fn all_panel_ids_and_positions(&self, cx: &App) -> Vec<(EntityId, DockPosition)> {
+        let mut result = Vec::new();
+        for dock in self.all_docks() {
+            let dock = dock.read(cx);
+            let position = dock.position();
+            for panel_id in dock.panel_ids() {
+                result.push((panel_id, position));
+            }
+        }
+        result
+    }
+
     pub fn remove_panel<T: Panel>(
         &mut self,
         panel: &Entity<T>,
@@ -10907,7 +10980,8 @@ mod tests {
 
         let panel = workspace.update_in(cx, |workspace, window, cx| {
             let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
-            workspace.add_panel(panel.clone(), window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel.clone(), position, window, cx);
 
             workspace
                 .right_dock()
@@ -11057,7 +11131,8 @@ mod tests {
 
         let panel = workspace.update_in(cx, |workspace, window, cx| {
             let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
-            workspace.add_panel(panel.clone(), window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel.clone(), position, window, cx);
             panel
         });
 
@@ -11352,10 +11427,12 @@ mod tests {
         // Open two docks (left and right) with one panel each
         let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
             let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
-            workspace.add_panel(left_panel.clone(), window, cx);
+            let position = left_panel.read(cx).position(window, cx);
+            workspace.add_panel(left_panel.clone(), position, window, cx);
 
             let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
-            workspace.add_panel(right_panel.clone(), window, cx);
+            let position = right_panel.read(cx).position(window, cx);
+            workspace.add_panel(right_panel.clone(), position, window, cx);
 
             workspace.toggle_dock(DockPosition::Left, window, cx);
             workspace.toggle_dock(DockPosition::Right, window, cx);
@@ -11784,10 +11861,12 @@ mod tests {
 
         let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
             let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
-            workspace.add_panel(panel_1.clone(), window, cx);
+            let position = panel_1.read(cx).position(window, cx);
+            workspace.add_panel(panel_1.clone(), position, window, cx);
             workspace.toggle_dock(DockPosition::Left, window, cx);
             let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
-            workspace.add_panel(panel_2.clone(), window, cx);
+            let position = panel_2.read(cx).position(window, cx);
+            workspace.add_panel(panel_2.clone(), position, window, cx);
             workspace.toggle_dock(DockPosition::Right, window, cx);
 
             let left_dock = workspace.left_dock();
@@ -12695,7 +12774,8 @@ mod tests {
         // focus to the new panel.
         let panel = workspace.update_in(cx, |workspace, window, cx| {
             let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
-            workspace.add_panel(panel.clone(), window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel.clone(), position, window, cx);
 
             workspace
                 .right_dock()
@@ -13383,7 +13463,8 @@ mod tests {
         let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
         let panel = workspace.update_in(cx, |workspace, window, cx| {
             let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
-            workspace.add_panel(panel.clone(), window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel.clone(), position, window, cx);
 
             workspace
                 .right_dock()
@@ -13468,7 +13549,8 @@ mod tests {
         // Add a panel to workspace A's right dock and open the dock
         let panel = workspace_a.update_in(cx, |workspace, window, cx| {
             let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
-            workspace.add_panel(panel.clone(), window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel.clone(), position, window, cx);
             workspace
                 .right_dock()
                 .update(cx, |dock, cx| dock.set_open(true, window, cx));
@@ -13570,7 +13652,8 @@ mod tests {
 
         let panel = workspace.update_in(cx, |workspace, window, cx| {
             let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
-            workspace.add_panel(panel.clone(), window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel.clone(), position, window, cx);
             workspace
                 .right_dock()
                 .update(cx, |dock, cx| dock.set_open(true, window, cx));

crates/zed/src/visual_test_runner.rs 🔗

@@ -321,7 +321,8 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
 
     workspace_window
         .update(&mut cx, |workspace, window, cx| {
-            workspace.add_panel(panel, window, cx);
+            let position = panel.read(cx).position(window, cx);
+            workspace.add_panel(panel, position, window, cx);
         })
         .log_err();
 

crates/zed/src/zed.rs 🔗

@@ -84,10 +84,12 @@ use util::rel_path::RelPath;
 use util::{ResultExt, asset_str, maybe};
 use uuid::Uuid;
 use vim_mode_setting::VimModeSetting;
+use workspace::dock::PanelHandle;
 use workspace::notifications::{
     NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification,
 };
 
+use workspace::dock::DockPosition;
 use workspace::{
     AppState, MultiWorkspace, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace,
     WorkspaceSettings, create_and_open_local_file,
@@ -293,11 +295,13 @@ pub fn init(cx: &mut App) {
         let Some(handle) = window.downcast::<MultiWorkspace>() else {
             return;
         };
-        handle
-            .update(cx, |multi_workspace, window, cx| {
-                toggle_agent_mode(multi_workspace, window, cx);
-            })
-            .log_err();
+        cx.defer(move |cx| {
+            handle
+                .update(cx, |multi_workspace, window, cx| {
+                    toggle_agent_mode(multi_workspace, window, cx);
+                })
+                .log_err();
+        });
     });
 }
 
@@ -306,13 +310,19 @@ fn toggle_agent_mode(
     window: &mut Window,
     cx: &mut Context<'_, MultiWorkspace>,
 ) {
-    let is_singleton = multi_workspace.is_singleton();
+    let mut is_singleton = multi_workspace.is_singleton();
     if is_singleton {
         multi_workspace.set_singleton(false, window, cx);
         multi_workspace.open_sidebar(cx);
     } else {
         multi_workspace.set_singleton(true, window, cx);
     }
+    is_singleton = !is_singleton;
+    let agent_mode = !is_singleton;
+    let workspace = multi_workspace.workspace();
+    workspace.update(cx, |workspace, cx| {
+        update_panel_positions(workspace, window, agent_mode, cx);
+    });
 }
 
 fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {
@@ -648,6 +658,33 @@ fn show_software_emulation_warning_if_needed(
     }
 }
 
+fn is_agent_mode(cx: &mut AsyncWindowContext) -> bool {
+    cx.window_handle()
+        .downcast::<MultiWorkspace>()
+        .and_then(|handle| {
+            handle
+                .read_with(cx, |multi_workspace, _| !multi_workspace.is_singleton())
+                .ok()
+        })
+        .unwrap_or(false)
+}
+
+fn interpret_panel_dock_position(
+    panel: &dyn PanelHandle,
+    preferred: DockPosition,
+    agent_mode: bool,
+) -> DockPosition {
+    let is_agent_panel = panel.panel_key() == agent_ui::AgentPanel::panel_key();
+    if agent_mode {
+        if is_agent_panel {
+            return DockPosition::Left;
+        } else if preferred == DockPosition::Left {
+            return DockPosition::Right;
+        }
+    }
+    preferred
+}
+
 fn initialize_panels(
     prompt_builder: Arc<PromptBuilder>,
     window: &mut Window,
@@ -666,16 +703,18 @@ fn initialize_panels(
         );
         let debug_panel = DebugPanel::load(workspace_handle.clone(), cx);
 
-        async fn add_panel_when_ready(
-            panel_task: impl Future<Output = anyhow::Result<Entity<impl workspace::Panel>>> + 'static,
+        async fn add_panel_when_ready<T: workspace::Panel>(
+            panel_task: impl Future<Output = anyhow::Result<Entity<T>>> + 'static,
             workspace_handle: WeakEntity<Workspace>,
             mut cx: gpui::AsyncWindowContext,
         ) {
-            if let Some(panel) = panel_task.await.context("failed to load panel").log_err()
-            {
+            if let Some(panel) = panel_task.await.context("failed to load panel").log_err() {
+                let agent_mode = is_agent_mode(&mut cx);
                 workspace_handle
                     .update_in(&mut cx, |workspace, window, cx| {
-                        workspace.add_panel(panel, window, cx);
+                        let preferred = panel.position(window, cx);
+                        let position = interpret_panel_dock_position(&panel, preferred, agent_mode);
+                        workspace.add_panel(panel, position, window, cx);
                     })
                     .log_err();
             }
@@ -689,13 +728,62 @@ fn initialize_panels(
             add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()),
             add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()),
             add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()),
-            initialize_agent_panel(workspace_handle, prompt_builder, cx.clone()).map(|r| r.log_err()),
+            initialize_agent_panel(workspace_handle.clone(), prompt_builder, cx.clone())
+                .map(|r| r.log_err()),
         );
 
+        let mut cx = cx.clone();
+        workspace_handle.update_in(&mut cx, |workspace, window, cx| {
+            observe_settings_for_panel_positions(workspace, window, cx);
+        })?;
+
         anyhow::Ok(())
     })
 }
 
+fn observe_settings_for_panel_positions(
+    _workspace: &mut Workspace,
+    window: &mut Window,
+    cx: &mut Context<Workspace>,
+) {
+    cx.observe_global_in::<settings::SettingsStore>(window, move |workspace, window, cx| {
+        let agent_mode = window
+            .root::<MultiWorkspace>()
+            .flatten()
+            .is_some_and(|handle| !handle.read(cx).is_singleton());
+        update_panel_positions(workspace, window, agent_mode, cx);
+    })
+    .detach();
+}
+
+fn update_panel_positions(
+    workspace: &mut Workspace,
+    window: &mut Window,
+    agent_mode: bool,
+    cx: &mut Context<Workspace>,
+) {
+    let panels_and_positions = workspace.all_panel_ids_and_positions(cx);
+    for (panel_id, current_position) in panels_and_positions {
+        let panel_handle = workspace
+            .dock_at_position(current_position)
+            .read(cx)
+            .panel_for_id(panel_id)
+            .cloned();
+
+        let Some(panel_handle) = panel_handle else {
+            continue;
+        };
+
+        let preferred_position = panel_handle.position(window, cx);
+        let target_position =
+            interpret_panel_dock_position(panel_handle.as_ref(), preferred_position, agent_mode);
+
+        if target_position != current_position {
+            workspace.move_panel_to_dock(panel_id, target_position, window, cx);
+        }
+    }
+}
+
 fn setup_or_teardown_ai_panel<P: Panel>(
     workspace: &mut Workspace,
     window: &mut Window,
@@ -712,15 +800,18 @@ fn setup_or_teardown_ai_panel<P: Panel>(
         || cfg!(test);
     let existing_panel = workspace.panel::<P>(cx);
     match (disable_ai, existing_panel) {
-        (false, None) => cx.spawn_in(window, async move |workspace, cx| {
+        (false, None) => cx.spawn_in(window, async move |workspace, mut cx| {
             let panel = load_panel(workspace.clone(), cx.clone()).await?;
+            let agent_mode = is_agent_mode(&mut cx);
             workspace.update_in(cx, |workspace, window, cx| {
                 let disable_ai = SettingsStore::global(cx)
                     .get::<DisableAiSettings>(None)
                     .disable_ai;
                 let have_panel = workspace.panel::<P>(cx).is_some();
                 if !disable_ai && !have_panel {
-                    workspace.add_panel(panel, window, cx);
+                    let preferred = panel.position(window, cx);
+                    let position = interpret_panel_dock_position(&panel, preferred, agent_mode);
+                    workspace.add_panel(panel, position, window, cx);
                 }
             })
         }),