Move the worktree picker to the title bar + make it always visible (#54183) (cherry-pick to preview) (#54378)

Eric Holk , Danilo Leal , Nathan Sobo , Zed Zippy , and Ben Brandt created

Cherry-pick of #54183 to `v0.233.x`.

This PR makes Zed only have one worktree picker, as opposed to a flavor
of it in the title bar and another in the agent panel. It then moves it
to the title bar, making it always present, so that its trigger is
separate from the branch picker (which now contains only two views:
branches and stashes). For the worktree picker, I'm mostly favoring the
behavior we've introduced in the agent-panel-flavored version.

It also updates the title bar settings migration to use the JSON
`migrate_settings` helper instead of a shallow Tree-sitter rewrite, so
old `show_branch_icon = true` values are promoted to
`show_branch_status_icon = true` across root, platform, release-channel,
and profile settings scopes.

### Conflicts resolved

- `crates/migrator/src/migrations.rs` and
`crates/migrator/src/migrator.rs` β€” kept both the existing
`m_2026_04_15` (HTTP context servers) migration already on `v0.233.x`
and the new `m_2026_04_17` (branch icon) migration from this
cherry-pick, in order.
- `crates/agent_ui/src/agent_panel.rs` β€” dropped the now-removed
`CreateWorktree`, `SwitchWorktree`, `NewWorktreeBranchTarget`, and
`ToggleWorktreeSelector` imports while preserving `ShowThreadMetadata` /
`ShowAllSidebarThreadMetadata` that landed via the omnibus cherry-pick
(#54368). Removed the top-level `anyhow!` import (only used inside
`#[cfg(test)]`, which has its own import); kept `chrono` since it's
still used by the metadata UI.

Release Notes:

- Improved migration of the title bar branch status icon setting.

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>

Change summary

Cargo.lock                                              |    2 
assets/keymaps/default-linux.json                       |   23 
assets/keymaps/default-macos.json                       |   23 
assets/keymaps/default-windows.json                     |   23 
assets/settings/default.json                            |    4 
crates/agent_ui/Cargo.toml                              |    1 
crates/agent_ui/src/agent_panel.rs                      | 1395 +---------
crates/agent_ui/src/agent_ui.rs                         |   51 
crates/agent_ui/src/conversation_view.rs                |    7 
crates/agent_ui/src/thread_worktree_picker.rs           | 1036 --------
crates/call/src/call_impl/mod.rs                        |    2 
crates/git_ui/Cargo.toml                                |    2 
crates/git_ui/src/branch_picker.rs                      |   12 
crates/git_ui/src/git_picker.rs                         |  160 -
crates/git_ui/src/git_ui.rs                             |   62 
crates/git_ui/src/stash_picker.rs                       |   12 
crates/git_ui/src/worktree_names.rs                     |    0 
crates/git_ui/src/worktree_picker.rs                    | 1107 +++----
crates/git_ui/src/worktree_service.rs                   |  809 ++++++
crates/migrator/src/migrations.rs                       |    6 
crates/migrator/src/migrations/m_2026_04_17/settings.rs |   47 
crates/migrator/src/migrator.rs                         |  251 +
crates/recent_projects/src/remote_connections.rs        |    2 
crates/recent_projects/src/remote_servers.rs            |    2 
crates/settings_content/src/title_bar.rs                |    6 
crates/settings_ui/src/page_data.rs                     |   10 
crates/sidebar/src/sidebar.rs                           |   21 
crates/sidebar/src/sidebar_tests.rs                     |   50 
crates/title_bar/src/title_bar.rs                       |  169 
crates/title_bar/src/title_bar_settings.rs              |    4 
crates/workspace/src/multi_workspace.rs                 |   89 
crates/workspace/src/multi_workspace_tests.rs           |    2 
crates/workspace/src/persistence.rs                     |    8 
crates/workspace/src/workspace.rs                       |  107 
crates/zed/src/visual_test_runner.rs                    |    6 
crates/zed/src/zed.rs                                   |   82 
crates/zed_actions/src/lib.rs                           |   44 
docs/src/ai/llm-providers.md                            |    2 
docs/src/reference/all-settings.md                      |    4 
docs/src/visual-customization.md                        |    2 
40 files changed, 2,418 insertions(+), 3,227 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -353,6 +353,7 @@ dependencies = [
  "futures 0.3.32",
  "fuzzy",
  "git",
+ "git_ui",
  "gpui",
  "gpui_tokio",
  "heapless",
@@ -7262,6 +7263,7 @@ dependencies = [
  "db",
  "editor",
  "file_icons",
+ "fs",
  "futures 0.3.32",
  "fuzzy",
  "git",

assets/keymaps/default-linux.json πŸ”—

@@ -237,7 +237,6 @@
       "shift-alt-j": "agent::ToggleNavigationMenu",
       "shift-alt-i": "agent::ToggleOptionsMenu",
       "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
-      "ctrl-shift-t": "agent::ToggleWorktreeSelector",
       "shift-alt-escape": "agent::ExpandMessageEditor",
       "ctrl->": "agent::AddSelectionToThread",
       "ctrl-shift-e": "project_panel::ToggleFocus",
@@ -630,6 +629,7 @@
       // "alt-ctrl-shift-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
       "alt-ctrl-shift-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
       "alt-ctrl-shift-b": "branches::OpenRecent",
+      "alt-ctrl-shift-w": "git::Worktree",
       "alt-shift-enter": "toast::RunAction",
       "ctrl-~": "workspace::NewTerminal",
       "save": "workspace::Save",
@@ -1387,15 +1387,6 @@
       "ctrl-shift-enter": "workspace::OpenWithSystem",
     },
   },
-  {
-    "context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
-    "use_key_equivalents": true,
-    "bindings": {
-      "ctrl-shift-space": "git::WorktreeFromDefaultOnWindow",
-      "ctrl-space": "git::WorktreeFromDefault",
-      "ctrl-shift-backspace": "git::DeleteWorktree",
-    },
-  },
   {
     // Handled under a more specific context to avoid conflicts with the
     // `OpenCurrentFile` keybind from the settings UI
@@ -1531,9 +1522,15 @@
   {
     "context": "GitPicker",
     "bindings": {
-      "alt-1": "git_picker::ActivateWorktreesTab",
-      "alt-2": "git_picker::ActivateBranchesTab",
-      "alt-3": "git_picker::ActivateStashTab",
+      "alt-1": "git_picker::ActivateBranchesTab",
+      "alt-2": "git_picker::ActivateStashTab",
+    },
+  },
+  {
+    "context": "WorktreePicker || (WorktreePicker > Picker > Editor)",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-shift-backspace": "worktree_picker::DeleteWorktree",
     },
   },
 ]

assets/keymaps/default-macos.json πŸ”—

@@ -275,7 +275,6 @@
       "cmd-shift-j": "agent::ToggleNavigationMenu",
       "cmd-alt-m": "agent::ToggleOptionsMenu",
       "cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
-      "cmd-shift-t": "agent::ToggleWorktreeSelector",
       "shift-alt-escape": "agent::ExpandMessageEditor",
       "cmd->": "agent::AddSelectionToThread",
       "cmd-shift-e": "project_panel::ToggleFocus",
@@ -698,6 +697,7 @@
       "ctrl-cmd-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
       "ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true, "create_new_window": false }],
       "cmd-ctrl-b": "branches::OpenRecent",
+      "cmd-ctrl-w": "git::Worktree",
       "ctrl-~": "workspace::NewTerminal",
       "cmd-s": "workspace::Save",
       "cmd-k s": "workspace::SaveWithoutFormat",
@@ -1484,15 +1484,6 @@
       "ctrl-shift-enter": "workspace::OpenWithSystem",
     },
   },
-  {
-    "context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
-    "use_key_equivalents": true,
-    "bindings": {
-      "ctrl-shift-space": "git::WorktreeFromDefaultOnWindow",
-      "ctrl-space": "git::WorktreeFromDefault",
-      "cmd-shift-backspace": "git::DeleteWorktree",
-    },
-  },
   {
     // Handled under a more specific context to avoid conflicts with the
     // `OpenCurrentFile` keybind from the settings UI
@@ -1592,9 +1583,15 @@
   {
     "context": "GitPicker",
     "bindings": {
-      "cmd-1": "git_picker::ActivateWorktreesTab",
-      "cmd-2": "git_picker::ActivateBranchesTab",
-      "cmd-3": "git_picker::ActivateStashTab",
+      "cmd-1": "git_picker::ActivateBranchesTab",
+      "cmd-2": "git_picker::ActivateStashTab",
+    },
+  },
+  {
+    "context": "WorktreePicker || (WorktreePicker > Picker > Editor)",
+    "use_key_equivalents": true,
+    "bindings": {
+      "cmd-shift-backspace": "worktree_picker::DeleteWorktree",
     },
   },
   {

assets/keymaps/default-windows.json πŸ”—

@@ -238,7 +238,6 @@
       "shift-alt-j": "agent::ToggleNavigationMenu",
       "shift-alt-i": "agent::ToggleOptionsMenu",
       "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
-      "ctrl-shift-t": "agent::ToggleWorktreeSelector",
       "shift-alt-escape": "agent::ExpandMessageEditor",
       "ctrl-shift-.": "agent::AddSelectionToThread",
       "ctrl-shift-e": "project_panel::ToggleFocus",
@@ -626,6 +625,7 @@
       // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
       "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
       "shift-alt-b": "branches::OpenRecent",
+      "shift-alt-w": "git::Worktree",
       "shift-alt-enter": "toast::RunAction",
       "ctrl-shift-`": "workspace::NewTerminal",
       "ctrl-s": "workspace::Save",
@@ -1402,15 +1402,6 @@
       "ctrl-5": ["welcome::OpenRecentProject", 4],
     },
   },
-  {
-    "context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
-    "use_key_equivalents": true,
-    "bindings": {
-      "ctrl-shift-space": "git::WorktreeFromDefaultOnWindow",
-      "ctrl-space": "git::WorktreeFromDefault",
-      "ctrl-shift-backspace": "git::DeleteWorktree",
-    },
-  },
   {
     // Handled under a more specific context to avoid conflicts with the
     // `OpenCurrentFile` keybind from the settings UI
@@ -1509,9 +1500,15 @@
   {
     "context": "GitPicker",
     "bindings": {
-      "alt-1": "git_picker::ActivateWorktreesTab",
-      "alt-2": "git_picker::ActivateBranchesTab",
-      "alt-3": "git_picker::ActivateStashTab",
+      "alt-1": "git_picker::ActivateBranchesTab",
+      "alt-2": "git_picker::ActivateStashTab",
+    },
+  },
+  {
+    "context": "WorktreePicker || (WorktreePicker > Picker > Editor)",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-shift-backspace": "worktree_picker::DeleteWorktree",
     },
   },
   {

assets/settings/default.json πŸ”—

@@ -473,8 +473,8 @@
   "use_system_window_tabs": false,
   // Titlebar related settings
   "title_bar": {
-    // Whether to show the branch icon beside branch switcher in the titlebar.
-    "show_branch_icon": false,
+    // Whether to show git status indicators on the branch icon in the titlebar.
+    "show_branch_status_icon": false,
     // Whether to show the branch name button in the titlebar.
     "show_branch_name": true,
     // Whether to show the project host and name in the titlebar.

crates/agent_ui/Cargo.toml πŸ”—

@@ -55,6 +55,7 @@ file_icons.workspace = true
 fs.workspace = true
 futures.workspace = true
 git.workspace = true
+git_ui.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
 gpui_tokio.workspace = true

crates/agent_ui/src/agent_panel.rs πŸ”—

@@ -1,5 +1,5 @@
 use std::{
-    path::{Path, PathBuf},
+    path::PathBuf,
     rc::Rc,
     sync::{
         Arc,
@@ -32,14 +32,12 @@ use zed_actions::{
 use crate::DEFAULT_THREAD_TITLE;
 use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore};
 use crate::{
-    AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, CreateWorktree,
-    Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, NewWorktreeBranchTarget,
-    OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell,
-    ShowAllSidebarThreadMetadata, ShowThreadMetadata, SwitchWorktree, ToggleNavigationMenu,
-    ToggleNewThreadMenu, ToggleOptionsMenu, ToggleWorktreeSelector,
+    AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow,
+    InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
+    OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, ShowAllSidebarThreadMetadata,
+    ShowThreadMetadata, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
     agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
     conversation_view::{AcpThreadViewEvent, ThreadView},
-    thread_worktree_picker::ThreadWorktreePicker,
     ui::EndTrialUpsell,
 };
 use crate::{
@@ -51,7 +49,7 @@ use crate::{ManageProfiles, ThreadHistoryViewEvent};
 use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore};
 use agent_settings::AgentSettings;
 use ai_onboarding::AgentPanelOnboarding;
-use anyhow::{Context as _, Result, anyhow};
+use anyhow::{Context as _, Result};
 use chrono::{DateTime, Utc};
 use client::UserStore;
 use cloud_api_types::Plan;
@@ -62,18 +60,14 @@ use extension_host::ExtensionStore;
 use fs::Fs;
 use gpui::{
     Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner,
-    DismissEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, Focusable,
-    KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*,
-    pulsating_between,
+    DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels,
+    Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
 };
 use language::LanguageRegistry;
 use language_model::LanguageModelRegistry;
-use project::project_settings::ProjectSettings;
-use project::trusted_worktrees::{PathTrust, TrustedWorktrees};
-use project::{Project, ProjectPath, Worktree, linked_worktree_short_name};
+use project::{Project, ProjectPath, Worktree};
 use prompt_store::{PromptStore, UserPromptId};
 use release_channel::ReleaseChannel;
-use remote::RemoteConnectionOptions;
 use rules_library::{RulesLibrary, open_rules_library};
 use settings::TerminalDockPosition;
 use settings::{Settings, update_settings_file};
@@ -86,8 +80,8 @@ use ui::{
 };
 use util::{ResultExt as _, debug_panic};
 use workspace::{
-    CollaboratorId, DockStructure, DraggedSelection, DraggedTab, OpenMode, PathList,
-    SerializedPathList, ToggleWorkspaceSidebar, ToggleZoom, Workspace, WorkspaceId,
+    CollaboratorId, DraggedSelection, DraggedTab, PathList, SerializedPathList,
+    ToggleWorkspaceSidebar, ToggleZoom, Workspace, WorkspaceId,
     dock::{DockPosition, Panel, PanelEvent},
 };
 
@@ -279,14 +273,6 @@ pub fn init(cx: &mut App) {
                         });
                     }
                 })
-                .register_action(|workspace, _: &ToggleWorktreeSelector, window, cx| {
-                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                        workspace.focus_panel::<AgentPanel>(window, cx);
-                        panel.update(cx, |panel, cx| {
-                            panel.toggle_worktree_selector(&ToggleWorktreeSelector, window, cx);
-                        });
-                    }
-                })
                 .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
                     window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
                     window.refresh();
@@ -508,28 +494,6 @@ pub fn init(cx: &mut App) {
                             });
                         });
                     },
-                )
-                .register_action(
-                    |workspace: &mut Workspace, action: &CreateWorktree, window, cx| {
-                        let previous_state =
-                            AgentPanel::capture_workspace_state(workspace, window, cx);
-                        if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                            panel.update(cx, |panel, cx| {
-                                panel.create_worktree(action, previous_state, window, cx);
-                            });
-                        }
-                    },
-                )
-                .register_action(
-                    |workspace: &mut Workspace, action: &SwitchWorktree, window, cx| {
-                        let previous_state =
-                            AgentPanel::capture_workspace_state(workspace, window, cx);
-                        if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                            panel.update(cx, |panel, cx| {
-                                panel.switch_to_worktree(action, previous_state, window, cx);
-                            });
-                        }
-                    },
                 );
         },
     )
@@ -719,61 +683,6 @@ enum WhichFontSize {
     None,
 }
 
-#[derive(Clone, Debug)]
-pub enum WorktreeCreationStatus {
-    Creating(SharedString),
-    Loading(SharedString),
-    Error(SharedString),
-}
-
-#[derive(Clone, Debug)]
-enum WorktreeCreationArgs {
-    New {
-        worktree_name: Option<String>,
-        branch_target: NewWorktreeBranchTarget,
-    },
-    Linked {
-        worktree_path: PathBuf,
-        display_name: String,
-    },
-}
-
-struct PreviousWorkspaceState {
-    dock_structure: DockStructure,
-    open_file_paths: Vec<PathBuf>,
-    active_file_path: Option<PathBuf>,
-}
-
-#[cfg(test)]
-impl PreviousWorkspaceState {
-    /// An empty state with all docks hidden and no open files.
-    fn empty() -> Self {
-        use workspace::DockData;
-
-        Self {
-            dock_structure: DockStructure {
-                left: DockData {
-                    visible: false,
-                    active_panel: None,
-                    zoom: false,
-                },
-                right: DockData {
-                    visible: false,
-                    active_panel: None,
-                    zoom: false,
-                },
-                bottom: DockData {
-                    visible: false,
-                    active_panel: None,
-                    zoom: false,
-                },
-            },
-            open_file_paths: Vec::new(),
-            active_file_path: None,
-        }
-    }
-}
-
 impl BaseView {
     pub fn which_font_size_used(&self) -> WhichFontSize {
         WhichFontSize::AgentFont
@@ -809,7 +718,6 @@ pub struct AgentPanel {
     draft_thread: Option<Entity<ConversationView>>,
     retained_threads: HashMap<ThreadId, Entity<ConversationView>>,
     new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
-    start_thread_in_menu_handle: PopoverMenuHandle<ThreadWorktreePicker>,
     agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
     agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
     agent_navigation_menu: Option<Entity<ContextMenu>>,
@@ -820,10 +728,8 @@ pub struct AgentPanel {
     new_user_onboarding: Entity<AgentPanelOnboarding>,
     new_user_onboarding_upsell_dismissed: AtomicBool,
     selected_agent: Agent,
-    worktree_creation_status: Option<(EntityId, WorktreeCreationStatus)>,
     _thread_view_subscription: Option<Subscription>,
     _active_thread_focus_subscription: Option<Subscription>,
-    _worktree_creation_task: Option<Task<()>>,
     show_trust_workspace_message: bool,
     _base_view_observation: Option<Subscription>,
     _draft_editor_observation: Option<Subscription>,
@@ -1176,7 +1082,6 @@ impl AgentPanel {
             draft_thread: None,
             retained_threads: HashMap::default(),
             new_thread_menu_handle: PopoverMenuHandle::default(),
-            start_thread_in_menu_handle: PopoverMenuHandle::default(),
             agent_panel_menu_handle: PopoverMenuHandle::default(),
             agent_navigation_menu_handle: PopoverMenuHandle::default(),
             agent_navigation_menu: None,
@@ -1187,10 +1092,8 @@ impl AgentPanel {
             new_user_onboarding: onboarding,
             thread_store,
             selected_agent: Agent::default(),
-            worktree_creation_status: None,
             _thread_view_subscription: None,
             _active_thread_focus_subscription: None,
-            _worktree_creation_task: None,
             show_trust_workspace_message: false,
             new_user_onboarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)),
             _base_view_observation: None,
@@ -1760,15 +1663,6 @@ impl AgentPanel {
         self.new_thread_menu_handle.toggle(window, cx);
     }
 
-    pub fn toggle_worktree_selector(
-        &mut self,
-        _: &ToggleWorktreeSelector,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.start_thread_in_menu_handle.toggle(window, cx);
-    }
-
     pub fn increase_font_size(
         &mut self,
         action: &IncreaseBufferFontSize,
@@ -2841,1133 +2735,272 @@ impl AgentPanel {
                 .is_some_and(|active| active.entity_id() == draft.entity_id())
         })
     }
+}
 
-    // TODO: The mapping from workspace root paths to git repositories needs a
-    // unified approach across the codebase: this method, `sidebar::is_root_repo`,
-    // thread persistence (which PathList is saved to the database), and thread
-    // querying (which PathList is used to read threads back). All of these need
-    // to agree on how repos are resolved for a given workspace, especially in
-    // multi-root and nested-repo configurations.
-    /// Partitions the project's visible worktrees into git-backed repositories
-    /// and plain (non-git) paths. Git repos will have worktrees created for
-    /// them; non-git paths are carried over to the new workspace as-is.
-    ///
-    /// When multiple worktrees map to the same repository, the most specific
-    /// match wins (deepest work directory path), with a deterministic
-    /// tie-break on entity id. Each repository appears at most once.
-    fn classify_worktrees(
-        &self,
-        cx: &App,
-    ) -> (Vec<Entity<project::git_store::Repository>>, Vec<PathBuf>) {
-        let project = &self.project;
-        let repositories = project.read(cx).repositories(cx).clone();
-        let mut git_repos: Vec<Entity<project::git_store::Repository>> = Vec::new();
-        let mut non_git_paths: Vec<PathBuf> = Vec::new();
-        let mut seen_repo_ids = std::collections::HashSet::new();
-
-        for worktree in project.read(cx).visible_worktrees(cx) {
-            let wt_path = worktree.read(cx).abs_path();
-
-            let matching_repo = repositories
-                .iter()
-                .filter_map(|(id, repo)| {
-                    let work_dir = repo.read(cx).work_directory_abs_path.clone();
-                    if wt_path.starts_with(work_dir.as_ref()) {
-                        Some((*id, repo.clone(), work_dir.as_ref().components().count()))
-                    } else {
-                        None
-                    }
-                })
-                .max_by(
-                    |(left_id, _left_repo, left_depth), (right_id, _right_repo, right_depth)| {
-                        left_depth
-                            .cmp(right_depth)
-                            .then_with(|| left_id.cmp(right_id))
-                    },
-                );
-
-            if let Some((id, repo, _)) = matching_repo {
-                if seen_repo_ids.insert(id) {
-                    git_repos.push(repo);
+impl Focusable for AgentPanel {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        match self.visible_surface() {
+            VisibleSurface::Uninitialized => self.focus_handle.clone(),
+            VisibleSurface::AgentThread(conversation_view) => conversation_view.focus_handle(cx),
+            VisibleSurface::History(view) => view.read(cx).focus_handle(cx),
+            VisibleSurface::Configuration(configuration) => {
+                if let Some(configuration) = configuration {
+                    configuration.focus_handle(cx)
+                } else {
+                    self.focus_handle.clone()
                 }
-            } else {
-                non_git_paths.push(wt_path.to_path_buf());
-            }
-        }
-
-        (git_repos, non_git_paths)
-    }
-
-    fn resolve_worktree_branch_target(
-        branch_target: &NewWorktreeBranchTarget,
-    ) -> (Option<String>, Option<String>) {
-        match branch_target {
-            NewWorktreeBranchTarget::CurrentBranch => (None, None),
-            NewWorktreeBranchTarget::ExistingBranch { name } => {
-                (Some(name.clone()), Some(name.clone()))
-            }
-            NewWorktreeBranchTarget::CreateBranch { name, from_ref } => {
-                (Some(name.clone()), from_ref.clone())
             }
         }
     }
+}
 
-    fn maybe_propagate_worktree_trust(
-        this: &WeakEntity<Self>,
-        new_workspace: &Entity<workspace::Workspace>,
-        paths: &[PathBuf],
-        cx: &mut AsyncWindowContext,
-    ) {
-        cx.update(|_, cx| {
-            if ProjectSettings::get_global(cx).session.trust_all_worktrees {
-                return;
-            }
-            let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) else {
-                return;
-            };
+fn agent_panel_dock_position(cx: &App) -> DockPosition {
+    AgentSettings::get_global(cx).dock.into()
+}
 
-            let source_is_trusted = this
-                .upgrade()
-                .map(|panel| {
-                    let source_worktree_store = panel.read(cx).project.read(cx).worktree_store();
-                    !trusted_store
-                        .read(cx)
-                        .has_restricted_worktrees(&source_worktree_store, cx)
-                })
-                .unwrap_or(false);
+pub enum AgentPanelEvent {
+    ActiveViewChanged,
+    ThreadFocused,
+    RetainedThreadChanged,
+    ThreadInteracted { thread_id: ThreadId },
+}
 
-            if !source_is_trusted {
-                return;
-            }
+impl EventEmitter<PanelEvent> for AgentPanel {}
+impl EventEmitter<AgentPanelEvent> for AgentPanel {}
 
-            let worktree_store = new_workspace.read(cx).project().read(cx).worktree_store();
-            let paths_to_trust: HashSet<_> = paths
-                .iter()
-                .filter_map(|path| {
-                    let (worktree, _) = worktree_store.read(cx).find_worktree(path, cx)?;
-                    Some(PathTrust::Worktree(worktree.read(cx).id()))
-                })
-                .collect();
+impl Panel for AgentPanel {
+    fn persistent_name() -> &'static str {
+        "AgentPanel"
+    }
 
-            if !paths_to_trust.is_empty() {
-                trusted_store.update(cx, |store, cx| {
-                    store.trust(&worktree_store, paths_to_trust, cx);
-                });
-            }
-        })
-        .ok();
+    fn panel_key() -> &'static str {
+        AGENT_PANEL_KEY
     }
 
-    /// Kicks off an async git-worktree creation for each repository. Returns:
-    ///
-    /// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuplesβ€”the
-    ///   receiver resolves once the git worktree command finishes.
-    /// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs used
-    ///   later to remap open editor tabs into the new workspace.
-    fn start_worktree_creations(
-        git_repos: &[Entity<project::git_store::Repository>],
-        worktree_name: Option<String>,
-        existing_worktree_names: &[String],
-        existing_worktree_paths: &HashSet<PathBuf>,
-        base_ref: Option<String>,
-        worktree_directory_setting: &str,
-        rng: &mut impl rand::Rng,
-        cx: &mut Context<Self>,
-    ) -> Result<(
-        Vec<(
-            Entity<project::git_store::Repository>,
-            PathBuf,
-            futures::channel::oneshot::Receiver<Result<()>>,
-        )>,
-        Vec<(PathBuf, PathBuf)>,
-    )> {
-        let mut creation_infos = Vec::new();
-        let mut path_remapping = Vec::new();
-
-        let worktree_name = worktree_name.unwrap_or_else(|| {
-            let existing_refs: Vec<&str> =
-                existing_worktree_names.iter().map(|s| s.as_str()).collect();
-            crate::worktree_names::generate_worktree_name(&existing_refs, rng)
-                .unwrap_or_else(|| "worktree".to_string())
-        });
-
-        for repo in git_repos {
-            let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| {
-                let new_path =
-                    repo.path_for_new_linked_worktree(&worktree_name, worktree_directory_setting)?;
-                if existing_worktree_paths.contains(&new_path) {
-                    anyhow::bail!("A worktree already exists at {}", new_path.display());
-                }
-                let target = git::repository::CreateWorktreeTarget::Detached {
-                    base_sha: base_ref.clone(),
-                };
-                let receiver = repo.create_worktree(target, new_path.clone());
-                let work_dir = repo.work_directory_abs_path.clone();
-                anyhow::Ok((work_dir, new_path, receiver))
-            })?;
-            path_remapping.push((work_dir.to_path_buf(), new_path.clone()));
-            creation_infos.push((repo.clone(), new_path, receiver));
-        }
+    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
+        agent_panel_dock_position(cx)
+    }
 
-        Ok((creation_infos, path_remapping))
-    }
-
-    /// Waits for every in-flight worktree creation to complete. If any
-    /// creation fails, all successfully-created worktrees are rolled back
-    /// (removed) so the project isn't left in a half-migrated state.
-    async fn await_and_rollback_on_failure(
-        creation_infos: Vec<(
-            Entity<project::git_store::Repository>,
-            PathBuf,
-            futures::channel::oneshot::Receiver<Result<()>>,
-        )>,
-        fs: Arc<dyn Fs>,
-        cx: &mut AsyncWindowContext,
-    ) -> Result<Vec<PathBuf>> {
-        let mut created_paths: Vec<PathBuf> = Vec::new();
-        let mut repos_and_paths: Vec<(Entity<project::git_store::Repository>, PathBuf)> =
-            Vec::new();
-        let mut first_error: Option<anyhow::Error> = None;
-
-        for (repo, new_path, receiver) in creation_infos {
-            repos_and_paths.push((repo.clone(), new_path.clone()));
-            match receiver.await {
-                Ok(Ok(())) => {
-                    created_paths.push(new_path);
-                }
-                Ok(Err(err)) => {
-                    if first_error.is_none() {
-                        first_error = Some(err);
-                    }
-                }
-                Err(_canceled) => {
-                    if first_error.is_none() {
-                        first_error = Some(anyhow!("Worktree creation was canceled"));
-                    }
-                }
-            }
-        }
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        position != DockPosition::Bottom
+    }
 
-        let Some(err) = first_error else {
-            return Ok(created_paths);
+    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
+        let side = match position {
+            DockPosition::Left => "left",
+            DockPosition::Right | DockPosition::Bottom => "right",
         };
+        telemetry::event!("Agent Panel Side Changed", side = side);
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            settings
+                .agent
+                .get_or_insert_default()
+                .set_dock(position.into());
+        });
+    }
 
-        // Rollback all attempted worktrees (both successful and failed,
-        // since a failed creation may have left an orphan directory).
-        let mut rollback_futures = Vec::new();
-        for (rollback_repo, rollback_path) in &repos_and_paths {
-            let receiver = cx
-                .update(|_, cx| {
-                    rollback_repo.update(cx, |repo, _cx| {
-                        repo.remove_worktree(rollback_path.clone(), true)
-                    })
-                })
-                .ok();
-
-            rollback_futures.push((rollback_path.clone(), receiver));
+    fn default_size(&self, window: &Window, cx: &App) -> Pixels {
+        let settings = AgentSettings::get_global(cx);
+        match self.position(window, cx) {
+            DockPosition::Left | DockPosition::Right => settings.default_width,
+            DockPosition::Bottom => settings.default_height,
         }
+    }
 
-        let mut rollback_failures: Vec<String> = Vec::new();
-        for (path, receiver_opt) in rollback_futures {
-            let mut git_remove_failed = false;
-
-            if let Some(receiver) = receiver_opt {
-                match receiver.await {
-                    Ok(Ok(())) => {}
-                    Ok(Err(rollback_err)) => {
-                        log::error!(
-                            "git worktree remove failed for {}: {rollback_err}",
-                            path.display()
-                        );
-                        git_remove_failed = true;
-                    }
-                    Err(canceled) => {
-                        log::error!(
-                            "git worktree remove failed for {}: {canceled}",
-                            path.display()
-                        );
-                        git_remove_failed = true;
-                    }
-                }
-            } else {
-                log::error!(
-                    "failed to dispatch git worktree remove for {}",
-                    path.display()
-                );
-                git_remove_failed = true;
-            }
-
-            // `git worktree remove` normally removes this directory, but since
-            // `git worktree remove` failed (or wasn't dispatched), manually rm the directory.
-            if git_remove_failed {
-                if let Err(fs_err) = fs
-                    .remove_dir(
-                        &path,
-                        fs::RemoveOptions {
-                            recursive: true,
-                            ignore_if_not_exists: true,
-                        },
-                    )
-                    .await
-                {
-                    let msg = format!("{}: failed to remove directory: {fs_err}", path.display());
-                    log::error!("{}", msg);
-                    rollback_failures.push(msg);
-                }
-            }
-        }
-        let mut error_message = format!("Failed to create worktree: {err}");
-        if !rollback_failures.is_empty() {
-            error_message.push_str("\n\nFailed to clean up: ");
-            error_message.push_str(&rollback_failures.join(", "));
+    fn min_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
+        match self.position(window, cx) {
+            DockPosition::Left | DockPosition::Right => Some(MIN_PANEL_WIDTH),
+            DockPosition::Bottom => None,
         }
-        Err(anyhow!(error_message))
     }
 
-    /// Attempts to check out a branch in a newly created worktree.
-    /// First tries checking out an existing branch, then tries creating a new
-    /// branch. If both fail, the worktree stays in detached HEAD state.
-    async fn try_checkout_branch_in_worktree(
-        repo: &Entity<project::git_store::Repository>,
-        branch_name: &str,
-        worktree_path: &Path,
-        cx: &mut AsyncWindowContext,
-    ) {
-        // First, try checking out the branch (it may already exist).
-        let Ok(receiver) = cx.update(|_, cx| {
-            repo.update(cx, |repo, _cx| {
-                repo.checkout_branch_in_worktree(
-                    branch_name.to_string(),
-                    worktree_path.to_path_buf(),
-                    false,
-                )
-            })
-        }) else {
-            log::warn!(
-                "Failed to check out branch {branch_name} for worktree at {}. \
-                 Staying in detached HEAD state.",
-                worktree_path.display(),
-            );
+    fn supports_flexible_size(&self) -> bool {
+        true
+    }
 
-            return;
-        };
+    fn has_flexible_size(&self, _window: &Window, cx: &App) -> bool {
+        AgentSettings::get_global(cx).flexible
+    }
 
-        let Ok(result) = receiver.await else {
-            log::warn!(
-                "Branch checkout was canceled for worktree at {}. \
-                 Staying in detached HEAD state.",
-                worktree_path.display()
-            );
+    fn set_flexible_size(&mut self, flexible: bool, _window: &mut Window, cx: &mut Context<Self>) {
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            settings
+                .agent
+                .get_or_insert_default()
+                .set_flexible_size(flexible);
+        });
+    }
 
-            return;
-        };
+    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
+        if active {
+            self.ensure_thread_initialized(window, cx);
+        }
+    }
 
-        if let Err(err) = result {
-            log::info!(
-                "Failed to check out branch '{branch_name}' in worktree at {}, \
-                         will try creating it: {err}",
-                worktree_path.display()
-            );
-        } else {
-            log::info!(
-                "Checked out branch '{branch_name}' in worktree at {}",
-                worktree_path.display()
-            );
+    fn remote_id() -> Option<proto::PanelId> {
+        Some(proto::PanelId::AssistantPanel)
+    }
 
-            return;
-        }
+    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
+        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
+    }
 
-        // Checkout failed, so try creating the branch.
-        let create_result = cx.update(|_, cx| {
-            repo.update(cx, |repo, _cx| {
-                repo.checkout_branch_in_worktree(
-                    branch_name.to_string(),
-                    worktree_path.to_path_buf(),
-                    true,
-                )
-            })
-        });
+    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
+        Some("Agent Panel")
+    }
 
-        match create_result {
-            Ok(receiver) => match receiver.await {
-                Ok(Ok(())) => {
-                    log::info!(
-                        "Created and checked out branch '{branch_name}' in worktree at {}",
-                        worktree_path.display()
-                    );
-                }
-                Ok(Err(err)) => {
-                    log::warn!(
-                        "Failed to create branch '{branch_name}' in worktree at {}: {err}. \
-                         Staying in detached HEAD state.",
-                        worktree_path.display()
-                    );
-                }
-                Err(_) => {
-                    log::warn!(
-                        "Branch creation was canceled for worktree at {}. \
-                         Staying in detached HEAD state.",
-                        worktree_path.display()
-                    );
-                }
-            },
-            Err(err) => {
-                log::warn!(
-                    "Failed to dispatch branch creation for worktree at {}: {err}. \
-                     Staying in detached HEAD state.",
-                    worktree_path.display(),
-                );
-            }
-        }
+    fn toggle_action(&self) -> Box<dyn Action> {
+        Box::new(ToggleFocus)
     }
 
-    fn capture_workspace_state(
-        workspace: &Workspace,
-        window: &Window,
-        cx: &App,
-    ) -> PreviousWorkspaceState {
-        let dock_structure = workspace.capture_dock_state(window, cx);
-        let open_file_paths = workspace.open_item_abs_paths(cx);
-        let active_file_path = workspace
-            .active_item(cx)
-            .and_then(|item| item.project_path(cx))
-            .and_then(|pp| workspace.project().read(cx).absolute_path(&pp, cx));
-
-        PreviousWorkspaceState {
-            dock_structure,
-            open_file_paths,
-            active_file_path,
-        }
+    fn activation_priority(&self) -> u32 {
+        0
     }
 
-    fn create_worktree(
-        &mut self,
-        action: &CreateWorktree,
-        previous_workspace_state: PreviousWorkspaceState,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if !self.project_has_git_repository(cx) {
-            log::error!("create_worktree: no git repository in the project");
-            return;
-        }
-        if self.project.read(cx).is_via_collab() {
-            log::error!("create_worktree: not supported in collab projects");
-            return;
-        }
-        if matches!(
-            self.worktree_creation_status,
-            Some((
-                _,
-                WorktreeCreationStatus::Creating(_) | WorktreeCreationStatus::Loading(_)
-            ))
-        ) {
-            return;
-        }
+    fn enabled(&self, cx: &App) -> bool {
+        AgentSettings::get_global(cx).enabled(cx)
+    }
 
-        let content = self.take_active_initial_content(cx);
-        let content_blocks = match content {
-            Some(AgentInitialContent::ContentBlock { blocks, .. }) => blocks,
-            _ => Vec::new(),
-        };
+    fn is_agent_panel(&self) -> bool {
+        true
+    }
 
-        self.handle_worktree_requested(
-            content_blocks,
-            WorktreeCreationArgs::New {
-                worktree_name: action.worktree_name.clone(),
-                branch_target: action.branch_target.clone(),
-            },
-            previous_workspace_state,
-            window,
-            cx,
-        );
+    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
+        self.zoomed
     }
 
-    fn switch_to_worktree(
-        &mut self,
-        action: &SwitchWorktree,
-        previous_workspace_state: PreviousWorkspaceState,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if !self.project_has_git_repository(cx) {
-            log::error!("switch_to_worktree: no git repository in the project");
-            return;
-        }
-        if self.project.read(cx).is_via_collab() {
-            log::error!("switch_to_worktree: not supported in collab projects");
-            return;
-        }
-        if matches!(
-            self.worktree_creation_status,
-            Some((
-                _,
-                WorktreeCreationStatus::Creating(_) | WorktreeCreationStatus::Loading(_)
-            ))
-        ) {
-            return;
-        }
-
-        let content = self.take_active_initial_content(cx);
-        let content_blocks = match content {
-            Some(AgentInitialContent::ContentBlock { blocks, .. }) => blocks,
-            _ => Vec::new(),
-        };
-
-        self.handle_worktree_requested(
-            content_blocks,
-            WorktreeCreationArgs::Linked {
-                worktree_path: action.path.clone(),
-                display_name: action.display_name.clone(),
-            },
-            previous_workspace_state,
-            window,
-            cx,
-        );
-    }
-
-    fn set_worktree_creation_error(
-        &mut self,
-        message: SharedString,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some((_, status)) = &mut self.worktree_creation_status {
-            *status = WorktreeCreationStatus::Error(message);
-        }
-        if matches!(self.base_view, BaseView::Uninitialized) {
-            let selected_agent = self.selected_agent.clone();
-            self.new_agent_thread(selected_agent, window, cx);
-        }
+    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
+        self.zoomed = zoomed;
         cx.notify();
     }
+}
 
-    fn handle_worktree_requested(
-        &mut self,
-        content: Vec<acp::ContentBlock>,
-        args: WorktreeCreationArgs,
-        previous_workspace_state: PreviousWorkspaceState,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if matches!(
-            self.worktree_creation_status,
-            Some((
-                _,
-                WorktreeCreationStatus::Creating(_) | WorktreeCreationStatus::Loading(_)
-            ))
-        ) {
-            return;
-        }
-
-        let conversation_view_id = self
-            .active_conversation_view()
-            .map(|v| v.entity_id())
-            .unwrap_or_else(|| EntityId::from(0u64));
-        let display_name: SharedString = match &args {
-            WorktreeCreationArgs::New {
-                worktree_name: Some(name),
-                ..
-            } => name.clone().into(),
-            WorktreeCreationArgs::New { .. } => "worktree".into(),
-            WorktreeCreationArgs::Linked { display_name, .. } => display_name.clone().into(),
-        };
-        let status = if matches!(args, WorktreeCreationArgs::Linked { .. }) {
-            WorktreeCreationStatus::Loading(display_name)
-        } else {
-            WorktreeCreationStatus::Creating(display_name)
-        };
-        self.worktree_creation_status = Some((conversation_view_id, status));
-        cx.notify();
-
-        let (git_repos, non_git_paths) = self.classify_worktrees(cx);
-
-        if matches!(args, WorktreeCreationArgs::New { .. }) && git_repos.is_empty() {
-            self.set_worktree_creation_error(
-                "No git repositories found in the project".into(),
-                window,
-                cx,
-            );
-            return;
-        }
-
-        let remote_connection_options = self.project.read(cx).remote_connection_options(cx);
-
-        if remote_connection_options.is_some() {
-            let is_disconnected = self
-                .project
-                .read(cx)
-                .remote_client()
-                .is_some_and(|client| client.read(cx).is_disconnected());
-            if is_disconnected {
-                self.set_worktree_creation_error(
-                    "Cannot create worktree: remote connection is not active".into(),
-                    window,
-                    cx,
-                );
-                return;
-            }
+impl AgentPanel {
+    fn ensure_thread_initialized(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        if matches!(self.base_view, BaseView::Uninitialized) {
+            self.activate_draft(false, window, cx);
         }
-
-        let workspace = self.workspace.clone();
-        let window_handle = window
-            .window_handle()
-            .downcast::<workspace::MultiWorkspace>();
-
-        let selected_agent = self.selected_agent();
-
-        let git_repo_work_dirs: Vec<PathBuf> = git_repos
-            .iter()
-            .map(|repo| repo.read(cx).work_directory_abs_path.to_path_buf())
-            .collect();
-
-        let task = cx.spawn_in(window, async move |this, cx| {
-            let (all_paths, path_remapping, has_non_git) = match args {
-                WorktreeCreationArgs::New {
-                    worktree_name,
-                    branch_target,
-                } => {
-                    let worktree_receivers: Vec<_> = this.update_in(cx, |_this, _window, cx| {
-                        git_repos
-                            .iter()
-                            .map(|repo| repo.update(cx, |repo, _cx| repo.worktrees()))
-                            .collect()
-                    })?;
-                    let worktree_directory_setting = this.update_in(cx, |_this, _window, cx| {
-                        ProjectSettings::get_global(cx)
-                            .git
-                            .worktree_directory
-                            .clone()
-                    })?;
-
-                    let mut existing_worktree_names = Vec::new();
-                    let mut existing_worktree_paths = HashSet::default();
-                    for result in futures::future::join_all(worktree_receivers).await {
-                        match result {
-                            Ok(Ok(worktrees)) => {
-                                for worktree in worktrees {
-                                    if let Some(name) = worktree
-                                        .path
-                                        .parent()
-                                        .and_then(|p| p.file_name())
-                                        .and_then(|n| n.to_str())
-                                    {
-                                        existing_worktree_names.push(name.to_string());
-                                    }
-                                    existing_worktree_paths.insert(worktree.path.clone());
-                                }
-                            }
-                            Ok(Err(err)) => {
-                                Err::<(), _>(err).log_err();
-                            }
-                            Err(_) => {}
-                        }
-                    }
-
-                    let mut rng = rand::rng();
-
-                    let (branch_to_checkout, base_ref) =
-                        Self::resolve_worktree_branch_target(&branch_target);
-
-                    let (creation_infos, path_remapping) =
-                        match this.update_in(cx, |_this, _window, cx| {
-                            Self::start_worktree_creations(
-                                &git_repos,
-                                worktree_name,
-                                &existing_worktree_names,
-                                &existing_worktree_paths,
-                                base_ref,
-                                &worktree_directory_setting,
-                                &mut rng,
-                                cx,
-                            )
-                        }) {
-                            Ok(Ok(result)) => result,
-                            Ok(Err(err)) | Err(err) => {
-                                this.update_in(cx, |this, window, cx| {
-                                    this.set_worktree_creation_error(
-                                        format!("Failed to validate worktree directory: {err}")
-                                            .into(),
-                                        window,
-                                        cx,
-                                    );
-                                })
-                                .log_err();
-                                return anyhow::Ok(());
-                            }
-                        };
-
-                    let repo_paths: Vec<(Entity<project::git_store::Repository>, PathBuf)> =
-                        creation_infos
-                            .iter()
-                            .map(|(repo, path, _)| (repo.clone(), path.clone()))
-                            .collect();
-
-                    let fs = cx.update(|_, cx| <dyn Fs>::global(cx))?;
-
-                    let created_paths =
-                        match Self::await_and_rollback_on_failure(creation_infos, fs, cx).await {
-                            Ok(paths) => paths,
-                            Err(err) => {
-                                this.update_in(cx, |this, window, cx| {
-                                    this.set_worktree_creation_error(
-                                        format!("{err}").into(),
-                                        window,
-                                        cx,
-                                    );
-                                })?;
-                                return anyhow::Ok(());
-                            }
-                        };
-
-                    if let Some(ref branch_name) = branch_to_checkout {
-                        for (repo, worktree_path) in &repo_paths {
-                            Self::try_checkout_branch_in_worktree(
-                                repo,
-                                branch_name,
-                                worktree_path,
-                                cx,
-                            )
-                            .await;
-                        }
-                    }
-
-                    let mut all_paths = created_paths;
-                    let has_non_git = !non_git_paths.is_empty();
-                    all_paths.extend(non_git_paths.iter().cloned());
-                    (all_paths, path_remapping, has_non_git)
-                }
-                WorktreeCreationArgs::Linked { worktree_path, .. } => {
-                    let path_remapping: Vec<(PathBuf, PathBuf)> = git_repo_work_dirs
-                        .iter()
-                        .map(|work_dir| (work_dir.clone(), worktree_path.clone()))
-                        .collect();
-                    let mut all_paths = vec![worktree_path];
-                    let has_non_git = !non_git_paths.is_empty();
-                    all_paths.extend(non_git_paths.iter().cloned());
-                    (all_paths, path_remapping, has_non_git)
-                }
-            };
-
-            if workspace.upgrade().is_none() {
-                this.update_in(cx, |this, window, cx| {
-                    this.set_worktree_creation_error(
-                        "Workspace no longer available".into(),
-                        window,
-                        cx,
-                    );
-                })?;
-                return anyhow::Ok(());
-            }
-
-            let this_for_error = this.clone();
-            if let Err(err) = Self::open_worktree_workspace_and_start_thread(
-                this,
-                all_paths,
-                window_handle,
-                previous_workspace_state,
-                path_remapping,
-                non_git_paths,
-                has_non_git,
-                content,
-                selected_agent,
-                remote_connection_options,
-                cx,
-            )
-            .await
-            {
-                this_for_error
-                    .update_in(cx, |this, window, cx| {
-                        this.set_worktree_creation_error(
-                            format!("Failed to set up workspace: {err}").into(),
-                            window,
-                            cx,
-                        );
-                    })
-                    .log_err();
-            }
-            anyhow::Ok(())
-        });
-
-        self._worktree_creation_task = Some(cx.background_spawn(async move {
-            task.await.log_err();
-        }));
     }
 
-    async fn open_worktree_workspace_and_start_thread(
-        this: WeakEntity<Self>,
-        all_paths: Vec<PathBuf>,
-        window_handle: Option<gpui::WindowHandle<workspace::MultiWorkspace>>,
-        previous_workspace_state: PreviousWorkspaceState,
-        path_remapping: Vec<(PathBuf, PathBuf)>,
-        non_git_paths: Vec<PathBuf>,
-        has_non_git: bool,
-        content: Vec<acp::ContentBlock>,
-        selected_agent: Option<Agent>,
-        remote_connection_options: Option<RemoteConnectionOptions>,
-        cx: &mut AsyncWindowContext,
-    ) -> Result<()> {
-        let window_handle = window_handle
-            .ok_or_else(|| anyhow!("No window handle available for workspace creation"))?;
-
-        let (workspace_task, modal_workspace) =
-            window_handle.update(cx, |multi_workspace, window, cx| {
-                let path_list = PathList::new(&all_paths);
-                let active_workspace = multi_workspace.workspace().clone();
-                let modal_workspace = active_workspace.clone();
-
-                let dock_structure = previous_workspace_state.dock_structure;
-                let init = Box::new(
-                    move |workspace: &mut Workspace,
-                          window: &mut Window,
-                          cx: &mut Context<Workspace>| {
-                        workspace.set_dock_structure(dock_structure, window, cx);
-                    },
-                );
-
-                let task = multi_workspace.find_or_create_workspace(
-                    path_list,
-                    remote_connection_options,
-                    None,
-                    move |connection_options, window, cx| {
-                        remote_connection::connect_with_modal(
-                            &active_workspace,
-                            connection_options,
-                            window,
-                            cx,
-                        )
-                    },
-                    &[],
-                    Some(init),
-                    OpenMode::Add,
-                    window,
-                    cx,
-                );
-                (task, modal_workspace)
-            })?;
-
-        let result = workspace_task.await;
-        remote_connection::dismiss_connection_modal(&modal_workspace, cx);
-        let new_workspace = result?;
-
-        let panels_task = new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task());
-
-        if let Some(task) = panels_task {
-            task.await.log_err();
+    fn destination_has_meaningful_state(&self, cx: &App) -> bool {
+        if self.overlay_view.is_some() || !self.retained_threads.is_empty() {
+            return true;
         }
 
-        new_workspace
-            .update(cx, |workspace, cx| {
-                workspace.project().read(cx).wait_for_initial_scan(cx)
-            })
-            .await;
-
-        new_workspace
-            .update(cx, |workspace, cx| {
-                let repos = workspace
-                    .project()
+        match &self.base_view {
+            BaseView::Uninitialized => false,
+            BaseView::AgentThread { conversation_view } => {
+                let has_entries = conversation_view
                     .read(cx)
-                    .repositories(cx)
-                    .values()
-                    .cloned()
-                    .collect::<Vec<_>>();
-
-                let tasks = repos
-                    .into_iter()
-                    .map(|repo| repo.update(cx, |repo, _| repo.barrier()));
-                futures::future::join_all(tasks)
-            })
-            .await;
-
-        Self::maybe_propagate_worktree_trust(&this, &new_workspace, &all_paths, cx);
-
-        let initial_content = AgentInitialContent::ContentBlock {
-            blocks: content,
-            auto_submit: false,
-        };
-
-        window_handle.update(cx, |_multi_workspace, window, cx| {
-            new_workspace.update(cx, |workspace, cx| {
-                if has_non_git {
-                    let toast_id = workspace::notifications::NotificationId::unique::<AgentPanel>();
-                    workspace.show_toast(
-                        workspace::Toast::new(
-                            toast_id,
-                            "Some project folders are not git repositories. \
-                             They were included as-is without creating a worktree.",
-                        ),
-                        cx,
-                    );
-                }
-
-                // Remap every previously-open file path into the new worktree.
-                // Paths that can't be remapped (e.g. files that don't exist on
-                // the target branch) are silently skipped β€” best-effort.
-                let remap_path = |original_path: PathBuf| -> Option<PathBuf> {
-                    let best_match = path_remapping
-                        .iter()
-                        .filter_map(|(old_root, new_root)| {
-                            original_path.strip_prefix(old_root).ok().map(|relative| {
-                                (old_root.components().count(), new_root.join(relative))
-                            })
-                        })
-                        .max_by_key(|(depth, _)| *depth);
-
-                    if let Some((_, remapped_path)) = best_match {
-                        return Some(remapped_path);
-                    }
-
-                    for non_git in &non_git_paths {
-                        if original_path.starts_with(non_git) {
-                            return Some(original_path);
-                        }
-                    }
-                    None
-                };
-
-                let remapped_active_path = previous_workspace_state
-                    .active_file_path
-                    .and_then(|p| remap_path(p));
-
-                // Collect all remapped paths, deduplicating and preserving order.
-                // The active file is placed last so it ends up as the focused tab.
-                let mut paths_to_open: Vec<PathBuf> = Vec::new();
-                let mut seen = HashSet::default();
-                for path in previous_workspace_state.open_file_paths {
-                    if let Some(remapped) = remap_path(path) {
-                        if remapped_active_path.as_ref() != Some(&remapped)
-                            && seen.insert(remapped.clone())
-                        {
-                            paths_to_open.push(remapped);
-                        }
-                    }
-                }
-
-                if let Some(active) = &remapped_active_path {
-                    if seen.insert(active.clone()) {
-                        paths_to_open.push(active.clone());
-                    }
+                    .root_thread_view()
+                    .is_some_and(|tv| !tv.read(cx).thread.read(cx).entries().is_empty());
+                if has_entries {
+                    return true;
                 }
 
-                if !paths_to_open.is_empty() {
-                    let open_task = workspace.open_paths(
-                        paths_to_open,
-                        workspace::OpenOptions {
-                            focus: Some(false),
-                            ..Default::default()
-                        },
-                        None,
-                        window,
-                        cx,
-                    );
-                    cx.spawn(async move |_, _| -> anyhow::Result<()> {
-                        for item in open_task.await.into_iter().flatten() {
-                            // Best-effort: files that don't exist on the target
-                            // branch will fail to open and that's fine.
-                            item.log_err();
-                        }
-                        Ok(())
+                conversation_view
+                    .read(cx)
+                    .root_thread_view()
+                    .is_some_and(|thread_view| {
+                        let thread_view = thread_view.read(cx);
+                        thread_view
+                            .thread
+                            .read(cx)
+                            .draft_prompt()
+                            .is_some_and(|draft| !draft.is_empty())
+                            || !thread_view
+                                .message_editor
+                                .read(cx)
+                                .text(cx)
+                                .trim()
+                                .is_empty()
                     })
-                    .detach_and_log_err(cx);
-                }
-            });
-        })?;
-
-        window_handle.update(cx, |multi_workspace, window, cx| {
-            multi_workspace.activate(new_workspace.clone(), window, cx);
-
-            new_workspace.update(cx, |workspace, cx| {
-                workspace.run_create_worktree_tasks(window, cx);
-
-                workspace.focus_panel::<AgentPanel>(window, cx);
-
-                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                    panel.update(cx, |panel, cx| {
-                        panel.external_thread(
-                            selected_agent,
-                            None,
-                            None,
-                            None,
-                            Some(initial_content),
-                            true,
-                            "agent_panel",
-                            window,
-                            cx,
-                        );
-                    });
-                }
-            })
-        })?;
-
-        this.update_in(cx, |this, window, cx| {
-            this.worktree_creation_status = None;
-
-            if let Some(thread_view) = this.active_thread_view(cx) {
-                thread_view.update(cx, |thread_view, cx| {
-                    thread_view
-                        .message_editor
-                        .update(cx, |editor, cx| editor.clear(window, cx));
-                });
-            }
-
-            this.serialize(cx);
-            cx.notify();
-        })?;
-
-        anyhow::Ok(())
-    }
-}
-
-impl Focusable for AgentPanel {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        match self.visible_surface() {
-            VisibleSurface::Uninitialized => self.focus_handle.clone(),
-            VisibleSurface::AgentThread(conversation_view) => conversation_view.focus_handle(cx),
-            VisibleSurface::History(view) => view.read(cx).focus_handle(cx),
-            VisibleSurface::Configuration(configuration) => {
-                if let Some(configuration) = configuration {
-                    configuration.focus_handle(cx)
-                } else {
-                    self.focus_handle.clone()
-                }
             }
         }
     }
-}
 
-fn agent_panel_dock_position(cx: &App) -> DockPosition {
-    AgentSettings::get_global(cx).dock.into()
-}
-
-pub enum AgentPanelEvent {
-    ActiveViewChanged,
-    ThreadFocused,
-    RetainedThreadChanged,
-    ThreadInteracted { thread_id: ThreadId },
-}
-
-impl EventEmitter<PanelEvent> for AgentPanel {}
-impl EventEmitter<AgentPanelEvent> for AgentPanel {}
-
-impl Panel for AgentPanel {
-    fn persistent_name() -> &'static str {
-        "AgentPanel"
-    }
-
-    fn panel_key() -> &'static str {
-        AGENT_PANEL_KEY
-    }
-
-    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
-        agent_panel_dock_position(cx)
-    }
-
-    fn position_is_valid(&self, position: DockPosition) -> bool {
-        position != DockPosition::Bottom
+    fn active_initial_content(&self, cx: &App) -> Option<AgentInitialContent> {
+        self.active_thread_view(cx).and_then(|thread_view| {
+            thread_view
+                .read(cx)
+                .thread
+                .read(cx)
+                .draft_prompt()
+                .map(|draft| AgentInitialContent::ContentBlock {
+                    blocks: draft.to_vec(),
+                    auto_submit: false,
+                })
+                .filter(|initial_content| match initial_content {
+                    AgentInitialContent::ContentBlock { blocks, .. } => !blocks.is_empty(),
+                    _ => true,
+                })
+                .or_else(|| {
+                    let text = thread_view.read(cx).message_editor.read(cx).text(cx);
+                    if text.trim().is_empty() {
+                        None
+                    } else {
+                        Some(AgentInitialContent::ContentBlock {
+                            blocks: vec![acp::ContentBlock::Text(acp::TextContent::new(text))],
+                            auto_submit: false,
+                        })
+                    }
+                })
+        })
     }
 
-    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
-        let side = match position {
-            DockPosition::Left => "left",
-            DockPosition::Right | DockPosition::Bottom => "right",
+    fn source_panel_initialization(
+        source_workspace: &WeakEntity<Workspace>,
+        cx: &App,
+    ) -> Option<(Agent, AgentInitialContent)> {
+        let source_workspace = source_workspace.upgrade()?;
+        let source_panel = source_workspace.read(cx).panel::<AgentPanel>(cx)?;
+        let source_panel = source_panel.read(cx);
+        let initial_content = source_panel.active_initial_content(cx)?;
+        let agent = if source_panel.project.read(cx).is_via_collab() {
+            Agent::NativeAgent
+        } else {
+            source_panel.selected_agent.clone()
         };
-        telemetry::event!("Agent Panel Side Changed", side = side);
-        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
-            settings
-                .agent
-                .get_or_insert_default()
-                .set_dock(position.into());
-        });
+        Some((agent, initial_content))
     }
 
-    fn default_size(&self, window: &Window, cx: &App) -> Pixels {
-        let settings = AgentSettings::get_global(cx);
-        match self.position(window, cx) {
-            DockPosition::Left | DockPosition::Right => settings.default_width,
-            DockPosition::Bottom => settings.default_height,
-        }
-    }
-
-    fn min_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
-        match self.position(window, cx) {
-            DockPosition::Left | DockPosition::Right => Some(MIN_PANEL_WIDTH),
-            DockPosition::Bottom => None,
-        }
-    }
-
-    fn supports_flexible_size(&self) -> bool {
-        true
-    }
-
-    fn has_flexible_size(&self, _window: &Window, cx: &App) -> bool {
-        AgentSettings::get_global(cx).flexible
-    }
-
-    fn set_flexible_size(&mut self, flexible: bool, _window: &mut Window, cx: &mut Context<Self>) {
-        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
-            settings
-                .agent
-                .get_or_insert_default()
-                .set_flexible_size(flexible);
-        });
-    }
-
-    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
-        if active {
-            self.ensure_thread_initialized(window, cx);
+    pub fn initialize_from_source_workspace_if_needed(
+        &mut self,
+        source_workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> bool {
+        if self.destination_has_meaningful_state(cx) {
+            return false;
         }
-    }
-
-    fn remote_id() -> Option<proto::PanelId> {
-        Some(proto::PanelId::AssistantPanel)
-    }
-
-    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
-        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
-    }
 
-    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
-        Some("Agent Panel")
-    }
-
-    fn toggle_action(&self) -> Box<dyn Action> {
-        Box::new(ToggleFocus)
-    }
-
-    fn activation_priority(&self) -> u32 {
-        0
-    }
-
-    fn enabled(&self, cx: &App) -> bool {
-        AgentSettings::get_global(cx).enabled(cx)
-    }
+        let Some((agent, initial_content)) =
+            Self::source_panel_initialization(&source_workspace, cx)
+        else {
+            return false;
+        };
 
-    fn is_agent_panel(&self) -> bool {
+        let agent = if self.project.read(cx).is_via_collab() {
+            Agent::NativeAgent
+        } else {
+            agent
+        };
+        let thread = self.create_agent_thread(
+            agent,
+            None,
+            None,
+            None,
+            Some(initial_content),
+            "agent_panel",
+            window,
+            cx,
+        );
+        self.draft_thread = Some(thread.conversation_view.clone());
+        self.observe_draft_editor(&thread.conversation_view, cx);
+        self.set_base_view(thread.into(), false, window, cx);
         true
     }
 
-    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
-        self.zoomed
-    }
-
-    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
-        self.zoomed = zoomed;
-        cx.notify();
-    }
-}
-
-impl AgentPanel {
-    fn ensure_thread_initialized(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if matches!(self.base_view, BaseView::Uninitialized)
-            && !matches!(
-                self.worktree_creation_status,
-                Some((_, WorktreeCreationStatus::Creating(_)))
-            )
-        {
-            self.activate_draft(false, window, cx);
-        }
-    }
-
     fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
         let content = match self.visible_surface() {
             VisibleSurface::AgentThread(conversation_view) => {

crates/agent_ui/src/agent_ui.rs πŸ”—

@@ -32,12 +32,10 @@ mod thread_history_view;
 mod thread_import;
 pub mod thread_metadata_store;
 pub mod thread_worktree_archive;
-mod thread_worktree_picker;
+
 pub mod threads_archive_view;
 mod ui;
-mod worktree_names;
 
-use std::path::PathBuf;
 use std::rc::Rc;
 use std::sync::Arc;
 
@@ -65,9 +63,7 @@ use std::any::TypeId;
 use workspace::Workspace;
 
 use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
-pub use crate::agent_panel::{
-    AgentPanel, AgentPanelEvent, MaxIdleRetainedThreads, WorktreeCreationStatus,
-};
+pub use crate::agent_panel::{AgentPanel, AgentPanelEvent, MaxIdleRetainedThreads};
 use crate::agent_registry_ui::AgentRegistryPage;
 pub use crate::inline_assistant::InlineAssistant;
 pub use crate::thread_metadata_store::ThreadId;
@@ -84,6 +80,7 @@ pub use thread_import::{
     channels_with_threads, import_threads_from_other_channels,
 };
 use zed_actions;
+pub use zed_actions::{CreateWorktree, NewWorktreeBranchTarget, SwitchWorktree};
 
 pub const DEFAULT_THREAD_TITLE: &str = "New Agent Thread";
 const PARALLEL_AGENT_LAYOUT_BACKFILL_KEY: &str = "parallel_agent_layout_backfilled";
@@ -92,8 +89,6 @@ actions!(
     [
         /// Toggles the menu to create new agent threads.
         ToggleNewThreadMenu,
-        /// Toggles the worktree selector popover for choosing which worktree to use.
-        ToggleWorktreeSelector,
         /// Toggles the navigation menu for switching between threads and views.
         ToggleNavigationMenu,
         /// Toggles the options menu for agent settings and preferences.
@@ -348,46 +343,6 @@ impl Agent {
     }
 }
 
-/// Describes which branch to use when creating a new git worktree.
-#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case", tag = "kind")]
-pub enum NewWorktreeBranchTarget {
-    /// Create a new randomly named branch from the current HEAD.
-    /// Will match worktree name if the newly created worktree was also randomly named.
-    #[default]
-    CurrentBranch,
-    /// Check out an existing branch, or create a new branch from it if it's
-    /// already occupied by another worktree.
-    ExistingBranch { name: String },
-    /// Create a new branch with an explicit name, optionally from a specific ref.
-    CreateBranch {
-        name: String,
-        #[serde(default)]
-        from_ref: Option<String>,
-    },
-}
-
-/// Creates a new git worktree and switches the workspace to it.
-/// Dispatched by the unified worktree picker when the user selects a "Create new worktree" entry.
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Action)]
-#[action(namespace = agent)]
-#[serde(deny_unknown_fields)]
-pub struct CreateWorktree {
-    /// When this is None, Zed will randomly generate a worktree name.
-    pub worktree_name: Option<String>,
-    pub branch_target: NewWorktreeBranchTarget,
-}
-
-/// Switches the workspace to an existing linked worktree.
-/// Dispatched by the unified worktree picker when the user selects an existing worktree.
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Action)]
-#[action(namespace = agent)]
-#[serde(deny_unknown_fields)]
-pub struct SwitchWorktree {
-    pub path: PathBuf,
-    pub display_name: String,
-}
-
 /// Content to initialize new external agent with.
 pub enum AgentInitialContent {
     ThreadSummary {

crates/agent_ui/src/conversation_view.rs πŸ”—

@@ -2560,7 +2560,12 @@ impl ConversationView {
                                     .update(cx, |multi_workspace, window, cx| {
                                         window.activate_window();
                                         if let Some(workspace) = workspace_handle.upgrade() {
-                                            multi_workspace.activate(workspace.clone(), window, cx);
+                                            multi_workspace.activate(
+                                                workspace.clone(),
+                                                None,
+                                                window,
+                                                cx,
+                                            );
                                             workspace.update(cx, |workspace, cx| {
                                                 workspace.reveal_panel::<AgentPanel>(window, cx);
                                                 if let Some(panel) =

crates/agent_ui/src/thread_worktree_picker.rs πŸ”—

@@ -1,1036 +0,0 @@
-use std::path::PathBuf;
-use std::sync::Arc;
-
-use collections::HashSet;
-use fuzzy::StringMatchCandidate;
-use git::repository::Worktree as GitWorktree;
-use gpui::{
-    AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
-    IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, rems,
-};
-use picker::{Picker, PickerDelegate, PickerEditorPosition};
-use project::Project;
-use project::git_store::RepositoryEvent;
-use ui::{Divider, HighlightedLabel, ListItem, ListItemSpacing, Tooltip, prelude::*};
-use util::ResultExt as _;
-use util::paths::PathExt;
-
-use crate::{CreateWorktree, NewWorktreeBranchTarget, SwitchWorktree};
-
-pub(crate) struct ThreadWorktreePicker {
-    picker: Entity<Picker<ThreadWorktreePickerDelegate>>,
-    focus_handle: FocusHandle,
-    _subscriptions: Vec<Subscription>,
-}
-
-impl ThreadWorktreePicker {
-    pub fn new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
-        let project_worktree_paths: HashSet<PathBuf> = project
-            .read(cx)
-            .visible_worktrees(cx)
-            .map(|wt| wt.read(cx).abs_path().to_path_buf())
-            .collect();
-
-        let has_multiple_repositories = project.read(cx).repositories(cx).len() > 1;
-
-        let current_branch_name = project.read(cx).active_repository(cx).and_then(|repo| {
-            repo.read(cx)
-                .branch
-                .as_ref()
-                .map(|branch| branch.name().to_string())
-        });
-
-        let repository = if has_multiple_repositories {
-            None
-        } else {
-            project.read(cx).active_repository(cx)
-        };
-
-        // Fetch worktrees from the git backend (includes main + all linked)
-        let all_worktrees_request = repository
-            .clone()
-            .map(|repo| repo.update(cx, |repo, _| repo.worktrees()));
-
-        let default_branch_request = repository
-            .clone()
-            .map(|repo| repo.update(cx, |repo, _| repo.default_branch(false)));
-
-        let initial_matches = vec![ThreadWorktreeEntry::CreateFromCurrentBranch];
-
-        let delegate = ThreadWorktreePickerDelegate {
-            matches: initial_matches,
-            all_worktrees: Vec::new(),
-            project_worktree_paths,
-            selected_index: 0,
-            project,
-            current_branch_name,
-            default_branch_name: None,
-            has_multiple_repositories,
-        };
-
-        let picker = cx.new(|cx| {
-            Picker::list(delegate, window, cx)
-                .list_measure_all()
-                .modal(false)
-                .max_height(Some(rems(20.).into()))
-        });
-
-        let mut subscriptions = Vec::new();
-
-        // Fetch worktrees and default branch asynchronously
-        {
-            let picker_handle = picker.downgrade();
-            cx.spawn_in(window, async move |_this, cx| {
-                let all_worktrees: Vec<_> = match all_worktrees_request {
-                    Some(req) => match req.await {
-                        Ok(Ok(worktrees)) => {
-                            worktrees.into_iter().filter(|wt| !wt.is_bare).collect()
-                        }
-                        Ok(Err(err)) => {
-                            log::warn!("ThreadWorktreePicker: git worktree list failed: {err}");
-                            return anyhow::Ok(());
-                        }
-                        Err(_) => {
-                            log::warn!("ThreadWorktreePicker: worktree request was cancelled");
-                            return anyhow::Ok(());
-                        }
-                    },
-                    None => Vec::new(),
-                };
-
-                let default_branch = match default_branch_request {
-                    Some(req) => req.await.ok().and_then(Result::ok).flatten(),
-                    None => None,
-                };
-
-                picker_handle.update_in(cx, |picker, window, cx| {
-                    picker.delegate.all_worktrees = all_worktrees;
-                    picker.delegate.default_branch_name =
-                        default_branch.map(|branch| branch.to_string());
-                    picker.refresh(window, cx);
-                })?;
-
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
-        }
-
-        // Subscribe to repository events to live-update the worktree list
-        if let Some(repo) = &repository {
-            let picker_entity = picker.downgrade();
-            subscriptions.push(cx.subscribe_in(
-                repo,
-                window,
-                move |_this, repo, event: &RepositoryEvent, window, cx| {
-                    if matches!(event, RepositoryEvent::GitWorktreeListChanged) {
-                        let worktrees_request = repo.update(cx, |repo, _| repo.worktrees());
-                        let picker = picker_entity.clone();
-                        cx.spawn_in(window, async move |_, cx| {
-                            let all_worktrees: Vec<_> = worktrees_request
-                                .await??
-                                .into_iter()
-                                .filter(|wt| !wt.is_bare)
-                                .collect();
-                            picker.update_in(cx, |picker, window, cx| {
-                                picker.delegate.all_worktrees = all_worktrees;
-                                picker.refresh(window, cx);
-                            })?;
-                            anyhow::Ok(())
-                        })
-                        .detach_and_log_err(cx);
-                    }
-                },
-            ));
-        }
-
-        subscriptions.push(cx.subscribe(&picker, |_, _, _, cx| {
-            cx.emit(DismissEvent);
-        }));
-
-        Self {
-            focus_handle: picker.focus_handle(cx),
-            picker,
-            _subscriptions: subscriptions,
-        }
-    }
-}
-
-impl Focusable for ThreadWorktreePicker {
-    fn focus_handle(&self, _cx: &App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl EventEmitter<DismissEvent> for ThreadWorktreePicker {}
-
-impl Render for ThreadWorktreePicker {
-    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        v_flex()
-            .w(rems(34.))
-            .elevation_3(cx)
-            .child(self.picker.clone())
-            .on_mouse_down_out(cx.listener(|_, _, _, cx| {
-                cx.emit(DismissEvent);
-            }))
-    }
-}
-
-#[derive(Clone)]
-enum ThreadWorktreeEntry {
-    CreateFromCurrentBranch,
-    CreateFromDefaultBranch {
-        default_branch_name: String,
-    },
-    Separator,
-    Worktree {
-        worktree: GitWorktree,
-        positions: Vec<usize>,
-    },
-    CreateNamed {
-        name: String,
-        /// When Some, create from this branch name (e.g. "main"). When None, create from current branch.
-        from_branch: Option<String>,
-        disabled_reason: Option<String>,
-    },
-}
-
-pub(crate) struct ThreadWorktreePickerDelegate {
-    matches: Vec<ThreadWorktreeEntry>,
-    all_worktrees: Vec<GitWorktree>,
-    project_worktree_paths: HashSet<PathBuf>,
-    selected_index: usize,
-    project: Entity<Project>,
-    current_branch_name: Option<String>,
-    default_branch_name: Option<String>,
-    has_multiple_repositories: bool,
-}
-
-impl ThreadWorktreePickerDelegate {
-    fn build_fixed_entries(&self) -> Vec<ThreadWorktreeEntry> {
-        let mut entries = Vec::new();
-
-        entries.push(ThreadWorktreeEntry::CreateFromCurrentBranch);
-
-        if !self.has_multiple_repositories {
-            if let Some(ref default_branch) = self.default_branch_name {
-                let is_different = self
-                    .current_branch_name
-                    .as_ref()
-                    .is_none_or(|current| current != default_branch);
-                if is_different {
-                    entries.push(ThreadWorktreeEntry::CreateFromDefaultBranch {
-                        default_branch_name: default_branch.clone(),
-                    });
-                }
-            }
-        }
-
-        entries
-    }
-
-    fn all_repo_worktrees(&self) -> &[GitWorktree] {
-        if self.has_multiple_repositories {
-            &[]
-        } else {
-            &self.all_worktrees
-        }
-    }
-
-    fn sync_selected_index(&mut self, has_query: bool) {
-        if !has_query {
-            return;
-        }
-
-        // When filtering, prefer selecting the first worktree match
-        if let Some(index) = self
-            .matches
-            .iter()
-            .position(|entry| matches!(entry, ThreadWorktreeEntry::Worktree { .. }))
-        {
-            self.selected_index = index;
-        } else if let Some(index) = self
-            .matches
-            .iter()
-            .position(|entry| matches!(entry, ThreadWorktreeEntry::CreateNamed { .. }))
-        {
-            self.selected_index = index;
-        } else {
-            self.selected_index = 0;
-        }
-    }
-}
-
-impl PickerDelegate for ThreadWorktreePickerDelegate {
-    type ListItem = AnyElement;
-
-    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        "Select a worktree for this thread…".into()
-    }
-
-    fn editor_position(&self) -> PickerEditorPosition {
-        PickerEditorPosition::Start
-    }
-
-    fn match_count(&self) -> usize {
-        self.matches.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(
-        &mut self,
-        ix: usize,
-        _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
-    ) {
-        self.selected_index = ix;
-    }
-
-    fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
-        !matches!(self.matches.get(ix), Some(ThreadWorktreeEntry::Separator))
-    }
-
-    fn update_matches(
-        &mut self,
-        query: String,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Task<()> {
-        let repo_worktrees = self.all_repo_worktrees().to_vec();
-
-        let normalized_query = query.replace(' ', "-");
-        let main_worktree_path = self
-            .all_worktrees
-            .iter()
-            .find(|wt| wt.is_main)
-            .map(|wt| wt.path.clone());
-        let has_named_worktree = self.all_worktrees.iter().any(|worktree| {
-            worktree.directory_name(main_worktree_path.as_deref()) == normalized_query
-        });
-        let create_named_disabled_reason: Option<String> = if self.has_multiple_repositories {
-            Some("Cannot create a named worktree in a project with multiple repositories".into())
-        } else if has_named_worktree {
-            Some("A worktree with this name already exists".into())
-        } else {
-            None
-        };
-
-        let show_default_branch_create = !self.has_multiple_repositories
-            && self.default_branch_name.as_ref().is_some_and(|default| {
-                self.current_branch_name
-                    .as_ref()
-                    .is_none_or(|current| current != default)
-            });
-        let default_branch_name = self.default_branch_name.clone();
-
-        if query.is_empty() {
-            let mut matches = self.build_fixed_entries();
-
-            if !repo_worktrees.is_empty() {
-                let main_worktree_path = repo_worktrees
-                    .iter()
-                    .find(|wt| wt.is_main)
-                    .map(|wt| wt.path.clone());
-
-                let mut sorted = repo_worktrees;
-                let project_paths = &self.project_worktree_paths;
-
-                sorted.sort_by(|a, b| {
-                    let a_is_current = project_paths.contains(&a.path);
-                    let b_is_current = project_paths.contains(&b.path);
-                    b_is_current.cmp(&a_is_current).then_with(|| {
-                        a.directory_name(main_worktree_path.as_deref())
-                            .cmp(&b.directory_name(main_worktree_path.as_deref()))
-                    })
-                });
-
-                matches.push(ThreadWorktreeEntry::Separator);
-                for worktree in sorted {
-                    matches.push(ThreadWorktreeEntry::Worktree {
-                        worktree,
-                        positions: Vec::new(),
-                    });
-                }
-            }
-
-            self.matches = matches;
-            self.sync_selected_index(false);
-            return Task::ready(());
-        }
-
-        // When the user is typing, fuzzy-match worktree names using display_name
-        let main_worktree_path = repo_worktrees
-            .iter()
-            .find(|wt| wt.is_main)
-            .map(|wt| wt.path.clone());
-        let candidates: Vec<_> = repo_worktrees
-            .iter()
-            .enumerate()
-            .map(|(ix, worktree)| {
-                StringMatchCandidate::new(
-                    ix,
-                    &worktree.directory_name(main_worktree_path.as_deref()),
-                )
-            })
-            .collect();
-
-        let executor = cx.background_executor().clone();
-
-        let task = cx.background_executor().spawn(async move {
-            fuzzy::match_strings(
-                &candidates,
-                &query,
-                true,
-                true,
-                10000,
-                &Default::default(),
-                executor,
-            )
-            .await
-        });
-
-        let repo_worktrees_clone = repo_worktrees;
-        cx.spawn_in(window, async move |picker, cx| {
-            let fuzzy_matches = task.await;
-
-            picker
-                .update_in(cx, |picker, _window, cx| {
-                    let mut new_matches: Vec<ThreadWorktreeEntry> = Vec::new();
-
-                    for candidate in &fuzzy_matches {
-                        new_matches.push(ThreadWorktreeEntry::Worktree {
-                            worktree: repo_worktrees_clone[candidate.candidate_id].clone(),
-                            positions: candidate.positions.clone(),
-                        });
-                    }
-
-                    if !new_matches.is_empty() {
-                        new_matches.push(ThreadWorktreeEntry::Separator);
-                    }
-                    new_matches.push(ThreadWorktreeEntry::CreateNamed {
-                        name: normalized_query.clone(),
-                        from_branch: None,
-                        disabled_reason: create_named_disabled_reason.clone(),
-                    });
-                    if show_default_branch_create {
-                        if let Some(ref default_branch) = default_branch_name {
-                            new_matches.push(ThreadWorktreeEntry::CreateNamed {
-                                name: normalized_query.clone(),
-                                from_branch: Some(default_branch.clone()),
-                                disabled_reason: create_named_disabled_reason.clone(),
-                            });
-                        }
-                    }
-
-                    picker.delegate.matches = new_matches;
-                    picker.delegate.sync_selected_index(true);
-
-                    cx.notify();
-                })
-                .log_err();
-        })
-    }
-
-    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        let Some(entry) = self.matches.get(self.selected_index) else {
-            return;
-        };
-
-        match entry {
-            ThreadWorktreeEntry::Separator => return,
-
-            ThreadWorktreeEntry::CreateFromCurrentBranch => {
-                window.dispatch_action(
-                    Box::new(CreateWorktree {
-                        worktree_name: None,
-                        branch_target: NewWorktreeBranchTarget::CurrentBranch,
-                    }),
-                    cx,
-                );
-            }
-
-            ThreadWorktreeEntry::CreateFromDefaultBranch {
-                default_branch_name,
-            } => {
-                window.dispatch_action(
-                    Box::new(CreateWorktree {
-                        worktree_name: None,
-                        branch_target: NewWorktreeBranchTarget::ExistingBranch {
-                            name: default_branch_name.clone(),
-                        },
-                    }),
-                    cx,
-                );
-            }
-
-            ThreadWorktreeEntry::Worktree { worktree, .. } => {
-                let is_current = self.project_worktree_paths.contains(&worktree.path);
-
-                if is_current {
-                    // Already in this worktree β€” just dismiss
-                } else {
-                    let main_worktree_path = self
-                        .all_worktrees
-                        .iter()
-                        .find(|wt| wt.is_main)
-                        .map(|wt| wt.path.as_path());
-                    window.dispatch_action(
-                        Box::new(SwitchWorktree {
-                            path: worktree.path.clone(),
-                            display_name: worktree.directory_name(main_worktree_path),
-                        }),
-                        cx,
-                    );
-                }
-            }
-
-            ThreadWorktreeEntry::CreateNamed {
-                name,
-                from_branch,
-                disabled_reason: None,
-            } => {
-                let branch_target = match from_branch {
-                    Some(branch) => NewWorktreeBranchTarget::ExistingBranch {
-                        name: branch.clone(),
-                    },
-                    None => NewWorktreeBranchTarget::CurrentBranch,
-                };
-                window.dispatch_action(
-                    Box::new(CreateWorktree {
-                        worktree_name: Some(name.clone()),
-                        branch_target,
-                    }),
-                    cx,
-                );
-            }
-
-            ThreadWorktreeEntry::CreateNamed {
-                disabled_reason: Some(_),
-                ..
-            } => {
-                return;
-            }
-        }
-
-        cx.emit(DismissEvent);
-    }
-
-    fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        _window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let entry = self.matches.get(ix)?;
-        let project = self.project.read(cx);
-        let is_create_disabled = project.repositories(cx).is_empty() || project.is_via_collab();
-
-        let no_git_reason: SharedString = "Requires a Git repository in the project".into();
-
-        let create_new_list_item = |id: SharedString,
-                                    label: SharedString,
-                                    disabled_tooltip: Option<SharedString>,
-                                    selected: bool| {
-            let is_disabled = disabled_tooltip.is_some();
-            ListItem::new(id)
-                .inset(true)
-                .spacing(ListItemSpacing::Sparse)
-                .toggle_state(selected)
-                .child(
-                    h_flex()
-                        .w_full()
-                        .gap_2p5()
-                        .child(
-                            Icon::new(IconName::Plus)
-                                .map(|this| {
-                                    if is_disabled {
-                                        this.color(Color::Disabled)
-                                    } else {
-                                        this.color(Color::Muted)
-                                    }
-                                })
-                                .size(IconSize::Small),
-                        )
-                        .child(
-                            Label::new(label).when(is_disabled, |this| this.color(Color::Disabled)),
-                        ),
-                )
-                .when_some(disabled_tooltip, |this, reason| {
-                    this.tooltip(Tooltip::text(reason))
-                })
-                .into_any_element()
-        };
-
-        match entry {
-            ThreadWorktreeEntry::Separator => Some(
-                div()
-                    .py(DynamicSpacing::Base04.rems(cx))
-                    .child(Divider::horizontal())
-                    .into_any_element(),
-            ),
-
-            ThreadWorktreeEntry::CreateFromCurrentBranch => {
-                let branch_label = if self.has_multiple_repositories {
-                    "current branches".to_string()
-                } else {
-                    self.current_branch_name
-                        .clone()
-                        .unwrap_or_else(|| "HEAD".to_string())
-                };
-
-                let label = format!("Create new worktree based on {branch_label}");
-
-                let disabled_tooltip = is_create_disabled.then(|| no_git_reason.clone());
-
-                let item = create_new_list_item(
-                    "create-from-current".to_string().into(),
-                    label.into(),
-                    disabled_tooltip,
-                    selected,
-                );
-
-                Some(item.into_any_element())
-            }
-
-            ThreadWorktreeEntry::CreateFromDefaultBranch {
-                default_branch_name,
-            } => {
-                let label = format!("Create new worktree based on {default_branch_name}");
-
-                let disabled_tooltip = is_create_disabled.then(|| no_git_reason.clone());
-
-                let item = create_new_list_item(
-                    "create-from-main".to_string().into(),
-                    label.into(),
-                    disabled_tooltip,
-                    selected,
-                );
-
-                Some(item.into_any_element())
-            }
-
-            ThreadWorktreeEntry::Worktree {
-                worktree,
-                positions,
-            } => {
-                let main_worktree_path = self
-                    .all_worktrees
-                    .iter()
-                    .find(|wt| wt.is_main)
-                    .map(|wt| wt.path.as_path());
-                let display_name = worktree.directory_name(main_worktree_path);
-                let first_line = display_name.lines().next().unwrap_or(&display_name);
-                let positions: Vec<_> = positions
-                    .iter()
-                    .copied()
-                    .filter(|&pos| pos < first_line.len())
-                    .collect();
-                let path = worktree.path.compact().to_string_lossy().to_string();
-                let sha = worktree.sha.chars().take(7).collect::<String>();
-
-                let is_current = self.project_worktree_paths.contains(&worktree.path);
-
-                let entry_icon = if is_current {
-                    IconName::Check
-                } else {
-                    IconName::GitWorktree
-                };
-
-                Some(
-                    ListItem::new(SharedString::from(format!("worktree-{ix}")))
-                        .inset(true)
-                        .spacing(ListItemSpacing::Sparse)
-                        .toggle_state(selected)
-                        .child(
-                            h_flex()
-                                .w_full()
-                                .gap_2p5()
-                                .child(
-                                    Icon::new(entry_icon)
-                                        .color(if is_current {
-                                            Color::Accent
-                                        } else {
-                                            Color::Muted
-                                        })
-                                        .size(IconSize::Small),
-                                )
-                                .child(
-                                    v_flex()
-                                        .w_full()
-                                        .min_w_0()
-                                        .child(
-                                            HighlightedLabel::new(first_line.to_owned(), positions)
-                                                .truncate(),
-                                        )
-                                        .child(
-                                            h_flex()
-                                                .w_full()
-                                                .min_w_0()
-                                                .gap_1p5()
-                                                .when_some(
-                                                    worktree.branch_name().map(|b| b.to_string()),
-                                                    |this, branch| {
-                                                        this.child(
-                                                            Label::new(branch)
-                                                                .size(LabelSize::Small)
-                                                                .color(Color::Muted),
-                                                        )
-                                                        .child(
-                                                            Label::new("\u{2022}")
-                                                                .alpha(0.5)
-                                                                .color(Color::Muted)
-                                                                .size(LabelSize::Small),
-                                                        )
-                                                    },
-                                                )
-                                                .when(!sha.is_empty(), |this| {
-                                                    this.child(
-                                                        Label::new(sha)
-                                                            .size(LabelSize::Small)
-                                                            .color(Color::Muted),
-                                                    )
-                                                    .child(
-                                                        Label::new("\u{2022}")
-                                                            .alpha(0.5)
-                                                            .color(Color::Muted)
-                                                            .size(LabelSize::Small),
-                                                    )
-                                                })
-                                                .child(
-                                                    Label::new(path)
-                                                        .truncate_start()
-                                                        .color(Color::Muted)
-                                                        .size(LabelSize::Small)
-                                                        .flex_1(),
-                                                ),
-                                        ),
-                                ),
-                        )
-                        .into_any_element(),
-                )
-            }
-
-            ThreadWorktreeEntry::CreateNamed {
-                name,
-                from_branch,
-                disabled_reason,
-            } => {
-                let branch_label = from_branch
-                    .as_deref()
-                    .unwrap_or(self.current_branch_name.as_deref().unwrap_or("HEAD"));
-                let label = format!("Create \"{name}\" based on {branch_label}");
-                let element_id = match from_branch {
-                    Some(branch) => format!("create-named-from-{branch}"),
-                    None => "create-named-from-current".to_string(),
-                };
-
-                let item = create_new_list_item(
-                    element_id.into(),
-                    label.into(),
-                    disabled_reason.clone().map(SharedString::from),
-                    selected,
-                );
-
-                Some(item.into_any_element())
-            }
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use fs::FakeFs;
-    use gpui::TestAppContext;
-    use project::Project;
-    use settings::SettingsStore;
-
-    fn init_test(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            theme_settings::init(theme::LoadThemes::JustBase, cx);
-            editor::init(cx);
-            release_channel::init("0.0.0".parse().unwrap(), cx);
-            crate::agent_panel::init(cx);
-        });
-    }
-
-    fn make_worktree(path: &str, branch: &str, is_main: bool) -> GitWorktree {
-        GitWorktree {
-            path: PathBuf::from(path),
-            ref_name: Some(format!("refs/heads/{branch}").into()),
-            sha: "abc1234".into(),
-            is_main,
-            is_bare: false,
-        }
-    }
-
-    fn build_delegate(
-        project: Entity<Project>,
-        all_worktrees: Vec<GitWorktree>,
-        project_worktree_paths: HashSet<PathBuf>,
-        current_branch_name: Option<String>,
-        default_branch_name: Option<String>,
-        has_multiple_repositories: bool,
-    ) -> ThreadWorktreePickerDelegate {
-        ThreadWorktreePickerDelegate {
-            matches: vec![ThreadWorktreeEntry::CreateFromCurrentBranch],
-            all_worktrees,
-            project_worktree_paths,
-            selected_index: 0,
-            project,
-            current_branch_name,
-            default_branch_name,
-            has_multiple_repositories,
-        }
-    }
-
-    fn entry_names(delegate: &ThreadWorktreePickerDelegate) -> Vec<String> {
-        delegate
-            .matches
-            .iter()
-            .map(|entry| match entry {
-                ThreadWorktreeEntry::CreateFromCurrentBranch => {
-                    "CreateFromCurrentBranch".to_string()
-                }
-                ThreadWorktreeEntry::CreateFromDefaultBranch {
-                    default_branch_name,
-                } => format!("CreateFromDefaultBranch({default_branch_name})"),
-                ThreadWorktreeEntry::Separator => "---".to_string(),
-                ThreadWorktreeEntry::Worktree { worktree, .. } => {
-                    format!("Worktree({})", worktree.path.display())
-                }
-                ThreadWorktreeEntry::CreateNamed {
-                    name,
-                    from_branch,
-                    disabled_reason,
-                } => {
-                    let branch = from_branch
-                        .as_deref()
-                        .map(|b| format!("from {b}"))
-                        .unwrap_or_else(|| "from current".to_string());
-                    if disabled_reason.is_some() {
-                        format!("CreateNamed({name}, {branch}, disabled)")
-                    } else {
-                        format!("CreateNamed({name}, {branch})")
-                    }
-                }
-            })
-            .collect()
-    }
-
-    type PickerWindow = gpui::WindowHandle<Picker<ThreadWorktreePickerDelegate>>;
-
-    async fn make_picker(
-        cx: &mut TestAppContext,
-        all_worktrees: Vec<GitWorktree>,
-        project_worktree_paths: HashSet<PathBuf>,
-        current_branch_name: Option<String>,
-        default_branch_name: Option<String>,
-        has_multiple_repositories: bool,
-    ) -> PickerWindow {
-        let fs = FakeFs::new(cx.executor());
-        let project = Project::test(fs, [], cx).await;
-
-        cx.add_window(|window, cx| {
-            let delegate = build_delegate(
-                project,
-                all_worktrees,
-                project_worktree_paths,
-                current_branch_name,
-                default_branch_name,
-                has_multiple_repositories,
-            );
-            Picker::list(delegate, window, cx)
-                .list_measure_all()
-                .modal(false)
-        })
-    }
-
-    #[gpui::test]
-    async fn test_empty_query_entries(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        // When on `main` with default branch also `main`, only CreateFromCurrentBranch
-        // is shown as a fixed entry. Worktrees are listed with the current one first.
-        let worktrees = vec![
-            make_worktree("/repo", "main", true),
-            make_worktree("/repo-feature", "feature", false),
-            make_worktree("/repo-bugfix", "bugfix", false),
-        ];
-        let project_paths: HashSet<PathBuf> = [PathBuf::from("/repo")].into_iter().collect();
-
-        let picker = make_picker(
-            cx,
-            worktrees,
-            project_paths,
-            Some("main".into()),
-            Some("main".into()),
-            false,
-        )
-        .await;
-
-        picker
-            .update(cx, |picker, window, cx| picker.refresh(window, cx))
-            .unwrap();
-        cx.run_until_parked();
-
-        let names = picker
-            .read_with(cx, |picker, _| entry_names(&picker.delegate))
-            .unwrap();
-
-        assert_eq!(
-            names,
-            vec![
-                "CreateFromCurrentBranch",
-                "---",
-                "Worktree(/repo)",
-                "Worktree(/repo-bugfix)",
-                "Worktree(/repo-feature)",
-            ]
-        );
-
-        // When current branch differs from default, CreateFromDefaultBranch appears.
-        picker
-            .update(cx, |picker, _window, cx| {
-                picker.delegate.current_branch_name = Some("feature".into());
-                picker.delegate.default_branch_name = Some("main".into());
-                cx.notify();
-            })
-            .unwrap();
-        picker
-            .update(cx, |picker, window, cx| picker.refresh(window, cx))
-            .unwrap();
-        cx.run_until_parked();
-
-        let names = picker
-            .read_with(cx, |picker, _| entry_names(&picker.delegate))
-            .unwrap();
-
-        assert!(names.contains(&"CreateFromDefaultBranch(main)".to_string()));
-    }
-
-    #[gpui::test]
-    async fn test_query_filtering_and_create_entries(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let picker = make_picker(
-            cx,
-            vec![
-                make_worktree("/repo", "main", true),
-                make_worktree("/repo-feature", "feature", false),
-                make_worktree("/repo-bugfix", "bugfix", false),
-                make_worktree("/my-worktree", "experiment", false),
-            ],
-            HashSet::default(),
-            Some("dev".into()),
-            Some("main".into()),
-            false,
-        )
-        .await;
-
-        // Partial match filters to matching worktrees and offers to create
-        // from both current branch and default branch.
-        picker
-            .update(cx, |picker, window, cx| {
-                picker.set_query("feat", window, cx)
-            })
-            .unwrap();
-        cx.run_until_parked();
-
-        let names = picker
-            .read_with(cx, |picker, _| entry_names(&picker.delegate))
-            .unwrap();
-        assert!(names.contains(&"Worktree(/repo-feature)".to_string()));
-        assert!(
-            names.contains(&"CreateNamed(feat, from current)".to_string()),
-            "should offer to create from current branch, got: {names:?}"
-        );
-        assert!(
-            names.contains(&"CreateNamed(feat, from main)".to_string()),
-            "should offer to create from default branch, got: {names:?}"
-        );
-        assert!(!names.contains(&"Worktree(/repo-bugfix)".to_string()));
-
-        // Exact match: both create entries appear but are disabled.
-        picker
-            .update(cx, |picker, window, cx| {
-                picker.set_query("repo-feature", window, cx)
-            })
-            .unwrap();
-        cx.run_until_parked();
-
-        let names = picker
-            .read_with(cx, |picker, _| entry_names(&picker.delegate))
-            .unwrap();
-        assert!(
-            names.contains(&"CreateNamed(repo-feature, from current, disabled)".to_string()),
-            "exact name match should show disabled create entries, got: {names:?}"
-        );
-
-        // Spaces are normalized to hyphens: "my worktree" matches "my-worktree".
-        picker
-            .update(cx, |picker, window, cx| {
-                picker.set_query("my worktree", window, cx)
-            })
-            .unwrap();
-        cx.run_until_parked();
-
-        let names = picker
-            .read_with(cx, |picker, _| entry_names(&picker.delegate))
-            .unwrap();
-        assert!(
-            names.contains(&"CreateNamed(my-worktree, from current, disabled)".to_string()),
-            "spaces should normalize to hyphens and detect existing worktree, got: {names:?}"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_multi_repo_hides_worktrees_and_disables_create_named(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let picker = make_picker(
-            cx,
-            vec![
-                make_worktree("/repo", "main", true),
-                make_worktree("/repo-feature", "feature", false),
-            ],
-            HashSet::default(),
-            Some("main".into()),
-            Some("main".into()),
-            true,
-        )
-        .await;
-
-        picker
-            .update(cx, |picker, window, cx| picker.refresh(window, cx))
-            .unwrap();
-        cx.run_until_parked();
-
-        let names = picker
-            .read_with(cx, |picker, _| entry_names(&picker.delegate))
-            .unwrap();
-        assert_eq!(names, vec!["CreateFromCurrentBranch"]);
-
-        picker
-            .update(cx, |picker, window, cx| {
-                picker.set_query("new-thing", window, cx)
-            })
-            .unwrap();
-        cx.run_until_parked();
-
-        let names = picker
-            .read_with(cx, |picker, _| entry_names(&picker.delegate))
-            .unwrap();
-        assert!(
-            names.contains(&"CreateNamed(new-thing, from current, disabled)".to_string()),
-            "multi-repo should disable create named, got: {names:?}"
-        );
-    }
-}

crates/call/src/call_impl/mod.rs πŸ”—

@@ -40,7 +40,7 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
             &cx.entity(),
             window,
             move |multi_workspace, _, event: &MultiWorkspaceEvent, window, cx| {
-                if !matches!(event, MultiWorkspaceEvent::ActiveWorkspaceChanged)
+                if !matches!(event, MultiWorkspaceEvent::ActiveWorkspaceChanged { .. })
                     && window.is_window_active()
                 {
                     return;

crates/git_ui/Cargo.toml πŸ”—

@@ -27,6 +27,7 @@ component.workspace = true
 db.workspace = true
 editor.workspace = true
 file_icons.workspace = true
+fs.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
 git.workspace = true
@@ -45,6 +46,7 @@ picker.workspace = true
 project.workspace = true
 prompt_store.workspace = true
 proto.workspace = true
+rand.workspace = true
 remote_connection.workspace = true
 remote.workspace = true
 schemars.workspace = true

crates/git_ui/src/branch_picker.rs πŸ”—

@@ -97,10 +97,11 @@ pub fn create_embedded(
     workspace: WeakEntity<Workspace>,
     repository: Option<Entity<Repository>>,
     width: Rems,
+    show_footer: bool,
     window: &mut Window,
     cx: &mut Context<BranchList>,
 ) -> BranchList {
-    BranchList::new_embedded(workspace, repository, width, window, cx)
+    BranchList::new_embedded(workspace, repository, width, show_footer, window, cx)
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -164,6 +165,7 @@ impl BranchList {
 
         picker.update(cx, |picker, _| {
             picker.delegate.focus_handle = picker_focus_handle.clone();
+            picker.delegate.show_footer = !embedded;
         });
 
         let mut subscriptions = Vec::new();
@@ -223,6 +225,7 @@ impl BranchList {
         workspace: WeakEntity<Workspace>,
         repository: Option<Entity<Repository>>,
         width: Rems,
+        show_footer: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -235,6 +238,9 @@ impl BranchList {
             window,
             cx,
         );
+        this.picker.update(cx, |picker, _| {
+            picker.delegate.show_footer = show_footer;
+        });
         this._subscriptions
             .push(cx.subscribe(&this.picker, |_, _, _, cx| {
                 cx.emit(DismissEvent);
@@ -386,6 +392,7 @@ pub struct BranchListDelegate {
     state: PickerState,
     focus_handle: FocusHandle,
     restore_selected_branch: Option<SharedString>,
+    show_footer: bool,
 }
 
 #[derive(Debug)]
@@ -452,6 +459,7 @@ impl BranchListDelegate {
             state: PickerState::List,
             focus_handle: cx.focus_handle(),
             restore_selected_branch: None,
+            show_footer: false,
         }
     }
 
@@ -1172,7 +1180,7 @@ impl PickerDelegate for BranchListDelegate {
     }
 
     fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
-        if self.editor_position() == PickerEditorPosition::End {
+        if !self.show_footer || self.editor_position() == PickerEditorPosition::End {
             return None;
         }
         let focus_handle = self.focus_handle.clone();

crates/git_ui/src/git_picker.rs πŸ”—

@@ -14,18 +14,11 @@ use workspace::{ModalView, Workspace, pane};
 
 use crate::branch_picker::{self, BranchList, DeleteBranch, FilterRemotes};
 use crate::stash_picker::{self, DropStashItem, ShowStashItem, StashList};
-use crate::worktree_picker::{
-    self, DeleteWorktree, WorktreeFromDefault, WorktreeFromDefaultOnWindow, WorktreeList,
-};
 
-actions!(
-    git_picker,
-    [ActivateBranchesTab, ActivateWorktreesTab, ActivateStashTab,]
-);
+actions!(git_picker, [ActivateBranchesTab, ActivateStashTab,]);
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 pub enum GitPickerTab {
-    Worktrees,
     Branches,
     Stash,
 }
@@ -34,7 +27,6 @@ impl Display for GitPickerTab {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let label = match self {
             GitPickerTab::Branches => "Branches",
-            GitPickerTab::Worktrees => "Worktrees",
             GitPickerTab::Stash => "Stash",
         };
         write!(f, "{}", label)
@@ -47,7 +39,6 @@ pub struct GitPicker {
     repository: Option<Entity<Repository>>,
     width: Rems,
     branch_list: Option<Entity<BranchList>>,
-    worktree_list: Option<Entity<WorktreeList>>,
     stash_list: Option<Entity<StashList>>,
     _subscriptions: Vec<Subscription>,
     popover_style: bool,
@@ -80,7 +71,6 @@ impl GitPicker {
             repository,
             width,
             branch_list: None,
-            worktree_list: None,
             stash_list: None,
             _subscriptions: Vec::new(),
             popover_style,
@@ -95,9 +85,6 @@ impl GitPicker {
             GitPickerTab::Branches => {
                 self.ensure_branch_list(window, cx);
             }
-            GitPickerTab::Worktrees => {
-                self.ensure_worktree_list(window, cx);
-            }
             GitPickerTab::Stash => {
                 self.ensure_stash_list(window, cx);
             }
@@ -110,11 +97,13 @@ impl GitPicker {
         cx: &mut Context<Self>,
     ) -> Entity<BranchList> {
         if self.branch_list.is_none() {
+            let show_footer = !self.popover_style;
             let branch_list = cx.new(|cx| {
                 branch_picker::create_embedded(
                     self.workspace.clone(),
                     self.repository.clone(),
                     self.width,
+                    show_footer,
                     window,
                     cx,
                 )
@@ -132,45 +121,19 @@ impl GitPicker {
         self.branch_list.clone().unwrap()
     }
 
-    fn ensure_worktree_list(
-        &mut self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Entity<WorktreeList> {
-        if self.worktree_list.is_none() {
-            let worktree_list = cx.new(|cx| {
-                worktree_picker::create_embedded(
-                    self.repository.clone(),
-                    self.workspace.clone(),
-                    self.width,
-                    window,
-                    cx,
-                )
-            });
-
-            let subscription = cx.subscribe(&worktree_list, |this, _, _: &DismissEvent, cx| {
-                if this.tab == GitPickerTab::Worktrees {
-                    cx.emit(DismissEvent);
-                }
-            });
-
-            self._subscriptions.push(subscription);
-            self.worktree_list = Some(worktree_list);
-        }
-        self.worktree_list.clone().unwrap()
-    }
-
     fn ensure_stash_list(
         &mut self,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Entity<StashList> {
         if self.stash_list.is_none() {
+            let show_footer = !self.popover_style;
             let stash_list = cx.new(|cx| {
                 stash_picker::create_embedded(
                     self.repository.clone(),
                     self.workspace.clone(),
                     self.width,
+                    show_footer,
                     window,
                     cx,
                 )
@@ -190,9 +153,8 @@ impl GitPicker {
 
     fn activate_next_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         self.tab = match self.tab {
-            GitPickerTab::Worktrees => GitPickerTab::Branches,
             GitPickerTab::Branches => GitPickerTab::Stash,
-            GitPickerTab::Stash => GitPickerTab::Worktrees,
+            GitPickerTab::Stash => GitPickerTab::Branches,
         };
         self.ensure_active_picker(window, cx);
         self.focus_active_picker(window, cx);
@@ -201,8 +163,7 @@ impl GitPicker {
 
     fn activate_previous_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         self.tab = match self.tab {
-            GitPickerTab::Worktrees => GitPickerTab::Stash,
-            GitPickerTab::Branches => GitPickerTab::Worktrees,
+            GitPickerTab::Branches => GitPickerTab::Stash,
             GitPickerTab::Stash => GitPickerTab::Branches,
         };
         self.ensure_active_picker(window, cx);
@@ -217,11 +178,6 @@ impl GitPicker {
                     branch_list.focus_handle(cx).focus(window, cx);
                 }
             }
-            GitPickerTab::Worktrees => {
-                if let Some(worktree_list) = &self.worktree_list {
-                    worktree_list.focus_handle(cx).focus(window, cx);
-                }
-            }
             GitPickerTab::Stash => {
                 if let Some(stash_list) = &self.stash_list {
                     stash_list.focus_handle(cx).focus(window, cx);
@@ -233,30 +189,12 @@ impl GitPicker {
     fn render_tab_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
         let focus_handle = self.focus_handle(cx);
         let branches_focus_handle = focus_handle.clone();
-        let worktrees_focus_handle = focus_handle.clone();
         let stash_focus_handle = focus_handle;
 
         h_flex().p_2().pb_0p5().w_full().child(
             ToggleButtonGroup::single_row(
                 "git-picker-tabs",
                 [
-                    ToggleButtonSimple::new(
-                        GitPickerTab::Worktrees.to_string(),
-                        cx.listener(|this, _, window, cx| {
-                            this.tab = GitPickerTab::Worktrees;
-                            this.ensure_active_picker(window, cx);
-                            this.focus_active_picker(window, cx);
-                            cx.notify();
-                        }),
-                    )
-                    .tooltip(move |_, cx| {
-                        Tooltip::for_action_in(
-                            "Toggle Worktree Picker",
-                            &ActivateWorktreesTab,
-                            &worktrees_focus_handle,
-                            cx,
-                        )
-                    }),
                     ToggleButtonSimple::new(
                         GitPickerTab::Branches.to_string(),
                         cx.listener(|this, _, window, cx| {
@@ -297,9 +235,8 @@ impl GitPicker {
             .style(ToggleButtonGroupStyle::Outlined)
             .auto_width()
             .selected_index(match self.tab {
-                GitPickerTab::Worktrees => 0,
-                GitPickerTab::Branches => 1,
-                GitPickerTab::Stash => 2,
+                GitPickerTab::Branches => 0,
+                GitPickerTab::Stash => 1,
             }),
         )
     }
@@ -314,10 +251,6 @@ impl GitPicker {
                 let branch_list = self.ensure_branch_list(window, cx);
                 branch_list.into_any_element()
             }
-            GitPickerTab::Worktrees => {
-                let worktree_list = self.ensure_worktree_list(window, cx);
-                worktree_list.into_any_element()
-            }
             GitPickerTab::Stash => {
                 let stash_list = self.ensure_stash_list(window, cx);
                 stash_list.into_any_element()
@@ -339,13 +272,6 @@ impl GitPicker {
                     });
                 }
             }
-            GitPickerTab::Worktrees => {
-                if let Some(worktree_list) = &self.worktree_list {
-                    worktree_list.update(cx, |list, cx| {
-                        list.handle_modifiers_changed(ev, window, cx);
-                    });
-                }
-            }
             GitPickerTab::Stash => {
                 if let Some(stash_list) = &self.stash_list {
                     stash_list.update(cx, |list, cx| {
@@ -382,45 +308,6 @@ impl GitPicker {
         }
     }
 
-    fn handle_worktree_from_default(
-        &mut self,
-        _: &WorktreeFromDefault,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(worktree_list) = &self.worktree_list {
-            worktree_list.update(cx, |list, cx| {
-                list.handle_new_worktree(false, window, cx);
-            });
-        }
-    }
-
-    fn handle_worktree_from_default_on_window(
-        &mut self,
-        _: &WorktreeFromDefaultOnWindow,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(worktree_list) = &self.worktree_list {
-            worktree_list.update(cx, |list, cx| {
-                list.handle_new_worktree(true, window, cx);
-            });
-        }
-    }
-
-    fn handle_worktree_delete(
-        &mut self,
-        _: &DeleteWorktree,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(worktree_list) = &self.worktree_list {
-            worktree_list.update(cx, |list, cx| {
-                list.handle_delete(&DeleteWorktree, window, cx);
-            });
-        }
-    }
-
     fn handle_drop_stash(
         &mut self,
         _: &DropStashItem,
@@ -459,11 +346,6 @@ impl Focusable for GitPicker {
                     return branch_list.focus_handle(cx);
                 }
             }
-            GitPickerTab::Worktrees => {
-                if let Some(worktree_list) = &self.worktree_list {
-                    return worktree_list.focus_handle(cx);
-                }
-            }
             GitPickerTab::Stash => {
                 if let Some(stash_list) = &self.stash_list {
                     return stash_list.focus_handle(cx);
@@ -492,7 +374,6 @@ impl Render for GitPicker {
                 key_context.add("GitPicker");
                 match self.tab {
                     GitPickerTab::Branches => key_context.add("GitBranchSelector"),
-                    GitPickerTab::Worktrees => key_context.add("GitWorktreeSelector"),
                     GitPickerTab::Stash => key_context.add("StashList"),
                 }
                 key_context
@@ -517,12 +398,6 @@ impl Render for GitPicker {
                 this.focus_active_picker(window, cx);
                 cx.notify();
             }))
-            .on_action(cx.listener(|this, _: &ActivateWorktreesTab, window, cx| {
-                this.tab = GitPickerTab::Worktrees;
-                this.ensure_active_picker(window, cx);
-                this.focus_active_picker(window, cx);
-                cx.notify();
-            }))
             .on_action(cx.listener(|this, _: &ActivateStashTab, window, cx| {
                 this.tab = GitPickerTab::Stash;
                 this.ensure_active_picker(window, cx);
@@ -534,11 +409,6 @@ impl Render for GitPicker {
                 el.on_action(cx.listener(Self::handle_delete_branch))
                     .on_action(cx.listener(Self::handle_filter_remotes))
             })
-            .when(self.tab == GitPickerTab::Worktrees, |el| {
-                el.on_action(cx.listener(Self::handle_worktree_from_default))
-                    .on_action(cx.listener(Self::handle_worktree_from_default_on_window))
-                    .on_action(cx.listener(Self::handle_worktree_delete))
-            })
             .when(self.tab == GitPickerTab::Stash, |el| {
                 el.on_action(cx.listener(Self::handle_drop_stash))
                     .on_action(cx.listener(Self::handle_show_stash))
@@ -557,15 +427,6 @@ pub fn open_branches(
     open_with_tab(workspace, GitPickerTab::Branches, window, cx);
 }
 
-pub fn open_worktrees(
-    workspace: &mut Workspace,
-    _: &zed_actions::git::Worktree,
-    window: &mut Window,
-    cx: &mut Context<Workspace>,
-) {
-    open_with_tab(workspace, GitPickerTab::Worktrees, window, cx);
-}
-
 pub fn open_stash(
     workspace: &mut Workspace,
     _: &zed_actions::git::ViewStash,
@@ -617,9 +478,6 @@ pub fn register(workspace: &mut Workspace) {
             open_with_tab(workspace, GitPickerTab::Branches, window, cx);
         },
     );
-    workspace.register_action(|workspace, _: &zed_actions::git::Worktree, window, cx| {
-        open_with_tab(workspace, GitPickerTab::Worktrees, window, cx);
-    });
     workspace.register_action(|workspace, _: &zed_actions::git::ViewStash, window, cx| {
         open_with_tab(workspace, GitPickerTab::Stash, window, cx);
     });

crates/git_ui/src/git_ui.rs πŸ”—

@@ -22,7 +22,7 @@ use menu::{Cancel, Confirm};
 use project::git_store::Repository;
 use project_diff::ProjectDiff;
 use ui::prelude::*;
-use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr};
+use workspace::{ModalView, OpenMode, Workspace, notifications::DetachAndPromptErr};
 use zed_actions;
 
 use crate::{git_panel::GitPanel, text_diff_view::TextDiffView};
@@ -45,7 +45,9 @@ pub(crate) mod remote_output;
 pub mod repository_selector;
 pub mod stash_picker;
 pub mod text_diff_view;
+pub mod worktree_names;
 pub mod worktree_picker;
+pub mod worktree_service;
 
 pub use conflict_view::MergeConflictIndicator;
 
@@ -65,6 +67,64 @@ pub fn init(cx: &mut App) {
         repository_selector::register(workspace);
         git_picker::register(workspace);
 
+        workspace.register_action(
+            |workspace, action: &zed_actions::CreateWorktree, window, cx| {
+                worktree_service::handle_create_worktree(workspace, action, window, None, cx);
+            },
+        );
+        workspace.register_action(
+            |workspace, action: &zed_actions::SwitchWorktree, window, cx| {
+                worktree_service::handle_switch_worktree(workspace, action, window, None, cx);
+            },
+        );
+
+        workspace.register_action(|workspace, _: &zed_actions::git::Worktree, window, cx| {
+            let focused_dock = workspace.focused_dock_position(window, cx);
+            let project = workspace.project().clone();
+            let workspace_handle = workspace.weak_handle();
+            workspace.toggle_modal(window, cx, |window, cx| {
+                worktree_picker::WorktreePicker::new_modal(
+                    project,
+                    workspace_handle,
+                    focused_dock,
+                    window,
+                    cx,
+                )
+            });
+        });
+
+        workspace.register_action(
+            |workspace, action: &zed_actions::OpenWorktreeInNewWindow, window, cx| {
+                let path = action.path.clone();
+                let is_remote = !workspace.project().read(cx).is_local();
+
+                if is_remote {
+                    let connection_options =
+                        workspace.project().read(cx).remote_connection_options(cx);
+                    let app_state = workspace.app_state().clone();
+                    let workspace_handle = workspace.weak_handle();
+                    cx.spawn_in(window, async move |_, cx| {
+                        if let Some(connection_options) = connection_options {
+                            crate::worktree_picker::open_remote_worktree(
+                                connection_options,
+                                vec![path],
+                                app_state,
+                                workspace_handle,
+                                cx,
+                            )
+                            .await?;
+                        }
+                        anyhow::Ok(())
+                    })
+                    .detach_and_log_err(cx);
+                } else {
+                    workspace
+                        .open_workspace_for_paths(OpenMode::NewWindow, vec![path], window, cx)
+                        .detach_and_log_err(cx);
+                }
+            },
+        );
+
         let project = workspace.project().read(cx);
         if project.is_read_only(cx) {
             return;

crates/git_ui/src/stash_picker.rs πŸ”—

@@ -46,10 +46,11 @@ pub fn create_embedded(
     repository: Option<Entity<Repository>>,
     workspace: WeakEntity<Workspace>,
     width: Rems,
+    show_footer: bool,
     window: &mut Window,
     cx: &mut Context<StashList>,
 ) -> StashList {
-    StashList::new_embedded(repository, workspace, width, window, cx)
+    StashList::new_embedded(repository, workspace, width, show_footer, window, cx)
 }
 
 pub struct StashList {
@@ -133,6 +134,7 @@ impl StashList {
         let picker_focus_handle = picker.focus_handle(cx);
         picker.update(cx, |picker, _| {
             picker.delegate.focus_handle = picker_focus_handle.clone();
+            picker.delegate.show_footer = !embedded;
         });
 
         Self {
@@ -147,10 +149,14 @@ impl StashList {
         repository: Option<Entity<Repository>>,
         workspace: WeakEntity<Workspace>,
         width: Rems,
+        show_footer: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
         let mut this = Self::new_inner(repository, workspace, width, true, window, cx);
+        this.picker.update(cx, |picker, _| {
+            picker.delegate.show_footer = show_footer;
+        });
         this._subscriptions
             .push(cx.subscribe(&this.picker, |_, _, _, cx| {
                 cx.emit(DismissEvent);
@@ -236,6 +242,7 @@ pub struct StashListDelegate {
     modifiers: Modifiers,
     focus_handle: FocusHandle,
     timezone: UtcOffset,
+    show_footer: bool,
 }
 
 impl StashListDelegate {
@@ -257,6 +264,7 @@ impl StashListDelegate {
             modifiers: Default::default(),
             focus_handle: cx.focus_handle(),
             timezone,
+            show_footer: false,
         }
     }
 
@@ -614,7 +622,7 @@ impl PickerDelegate for StashListDelegate {
     }
 
     fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
-        if self.matches.is_empty() {
+        if !self.show_footer || self.matches.is_empty() {
             return None;
         }
 

crates/git_ui/src/worktree_picker.rs πŸ”—

@@ -1,95 +1,88 @@
+use std::path::PathBuf;
+use std::sync::Arc;
+
 use anyhow::Context as _;
 use collections::HashSet;
 use fuzzy::StringMatchCandidate;
-
 use git::repository::Worktree as GitWorktree;
 use gpui::{
-    Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
-    Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement,
-    Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems,
+    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+    IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity,
+    Window, actions, rems,
 };
 use picker::{Picker, PickerDelegate, PickerEditorPosition};
-use project::project_settings::ProjectSettings;
-use project::{
-    git_store::{Repository, RepositoryEvent},
-    trusted_worktrees::{PathTrust, TrustedWorktrees},
+use project::Project;
+use project::git_store::RepositoryEvent;
+use ui::{
+    Button, Divider, HighlightedLabel, IconButton, KeyBinding, ListItem, ListItemSpacing, Tooltip,
+    prelude::*,
 };
-use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};
-use remote_connection::{RemoteConnectionModal, connect};
-use settings::Settings;
-use std::{path::PathBuf, sync::Arc};
-use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
-use util::{ResultExt, debug_panic, paths::PathExt};
+use util::ResultExt as _;
+use util::paths::PathExt;
 use workspace::{
-    ModalView, MultiWorkspace, OpenMode, Workspace, notifications::DetachAndPromptErr,
+    ModalView, MultiWorkspace, Workspace, dock::DockPosition, notifications::DetachAndPromptErr,
 };
 
 use crate::git_panel::show_error_toast;
+use zed_actions::{
+    CreateWorktree, NewWorktreeBranchTarget, OpenWorktreeInNewWindow, SwitchWorktree,
+};
 
-actions!(
-    git,
-    [
-        WorktreeFromDefault,
-        WorktreeFromDefaultOnWindow,
-        DeleteWorktree
-    ]
-);
-
-pub fn open(
-    workspace: &mut Workspace,
-    _: &zed_actions::git::Worktree,
-    window: &mut Window,
-    cx: &mut Context<Workspace>,
-) {
-    let repository = workspace.project().read(cx).active_repository(cx);
-    let workspace_handle = workspace.weak_handle();
-    workspace.toggle_modal(window, cx, |window, cx| {
-        WorktreeList::new(repository, workspace_handle, rems(34.), window, cx)
-    })
-}
-
-pub fn create_embedded(
-    repository: Option<Entity<Repository>>,
-    workspace: WeakEntity<Workspace>,
-    width: Rems,
-    window: &mut Window,
-    cx: &mut Context<WorktreeList>,
-) -> WorktreeList {
-    WorktreeList::new_embedded(repository, workspace, width, window, cx)
-}
+actions!(worktree_picker, [DeleteWorktree]);
 
-pub struct WorktreeList {
-    width: Rems,
-    pub picker: Entity<Picker<WorktreeListDelegate>>,
-    picker_focus_handle: FocusHandle,
+pub struct WorktreePicker {
+    picker: Entity<Picker<WorktreePickerDelegate>>,
+    focus_handle: FocusHandle,
     _subscriptions: Vec<Subscription>,
-    embedded: bool,
 }
 
-impl WorktreeList {
-    fn new(
-        repository: Option<Entity<Repository>>,
+impl WorktreePicker {
+    pub fn new(
+        project: Entity<Project>,
         workspace: WeakEntity<Workspace>,
-        width: Rems,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
-        let mut this = Self::new_inner(repository, workspace, width, false, window, cx);
-        this._subscriptions
-            .push(cx.subscribe(&this.picker, |_, _, _, cx| {
-                cx.emit(DismissEvent);
-            }));
-        this
+        let focused_dock = workspace
+            .upgrade()
+            .and_then(|workspace| workspace.read(cx).focused_dock_position(window, cx));
+        Self::new_inner(project, workspace, focused_dock, false, window, cx)
+    }
+
+    pub fn new_modal(
+        project: Entity<Project>,
+        workspace: WeakEntity<Workspace>,
+        focused_dock: Option<DockPosition>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        Self::new_inner(project, workspace, focused_dock, true, window, cx)
     }
 
     fn new_inner(
-        repository: Option<Entity<Repository>>,
+        project: Entity<Project>,
         workspace: WeakEntity<Workspace>,
-        width: Rems,
-        embedded: bool,
+        focused_dock: Option<DockPosition>,
+        show_footer: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
+        let project_ref = project.read(cx);
+        let project_worktree_paths: HashSet<PathBuf> = project_ref
+            .visible_worktrees(cx)
+            .map(|wt| wt.read(cx).abs_path().to_path_buf())
+            .collect();
+
+        let has_multiple_repositories = project_ref.repositories(cx).len() > 1;
+        let repository = project_ref.active_repository(cx);
+
+        let current_branch_name = repository.as_ref().and_then(|repo| {
+            repo.read(cx)
+                .branch
+                .as_ref()
+                .map(|branch| branch.name().to_string())
+        });
+
         let all_worktrees_request = repository
             .clone()
             .map(|repository| repository.update(cx, |repository, _| repository.worktrees()));
@@ -98,65 +91,94 @@ impl WorktreeList {
             repository.update(cx, |repository, _| repository.default_branch(false))
         });
 
-        cx.spawn_in(window, async move |this, cx| {
-            let all_worktrees: Vec<_> = all_worktrees_request
-                .context("No active repository")?
-                .await??
-                .into_iter()
-                .filter(|worktree| !worktree.is_bare) // hide bare repositories
-                .collect();
-
-            let default_branch = default_branch_request
-                .context("No active repository")?
-                .await
-                .map(Result::ok)
-                .ok()
-                .flatten()
-                .flatten();
-
-            this.update_in(cx, |this, window, cx| {
-                this.picker.update(cx, |picker, cx| {
-                    picker.delegate.all_worktrees = Some(all_worktrees);
-                    picker.delegate.default_branch = default_branch;
-                    picker.delegate.refresh_forbidden_deletion_path(cx);
-                    picker.refresh(window, cx);
-                })
-            })?;
+        let initial_matches = vec![WorktreeEntry::CreateFromCurrentBranch];
 
-            anyhow::Ok(())
-        })
-        .detach_and_log_err(cx);
+        let delegate = WorktreePickerDelegate {
+            matches: initial_matches,
+            all_worktrees: Vec::new(),
+            project_worktree_paths,
+            selected_index: 0,
+            project,
+            workspace,
+            focused_dock,
+            current_branch_name,
+            default_branch_name: None,
+            has_multiple_repositories,
+            focus_handle: cx.focus_handle(),
+            show_footer,
+        };
 
-        let delegate = WorktreeListDelegate::new(workspace, repository.clone(), window, cx);
         let picker = cx.new(|cx| {
-            Picker::uniform_list(delegate, window, cx)
+            Picker::list(delegate, window, cx)
+                .list_measure_all()
                 .show_scrollbar(true)
-                .modal(!embedded)
+                .modal(false)
+                .max_height(Some(rems(20.).into()))
         });
+
         let picker_focus_handle = picker.focus_handle(cx);
         picker.update(cx, |picker, _| {
-            picker.delegate.focus_handle = picker_focus_handle.clone();
+            picker.delegate.focus_handle = picker_focus_handle;
         });
 
         let mut subscriptions = Vec::new();
+
+        {
+            let picker_handle = picker.downgrade();
+            cx.spawn_in(window, async move |_this, cx| {
+                let all_worktrees: Vec<_> = match all_worktrees_request {
+                    Some(req) => match req.await {
+                        Ok(Ok(worktrees)) => {
+                            worktrees.into_iter().filter(|wt| !wt.is_bare).collect()
+                        }
+                        Ok(Err(err)) => {
+                            log::warn!("WorktreePicker: git worktree list failed: {err}");
+                            return anyhow::Ok(());
+                        }
+                        Err(_) => {
+                            log::warn!("WorktreePicker: worktree request was cancelled");
+                            return anyhow::Ok(());
+                        }
+                    },
+                    None => Vec::new(),
+                };
+
+                let default_branch = match default_branch_request {
+                    Some(req) => req.await.ok().and_then(Result::ok).flatten(),
+                    None => None,
+                };
+
+                picker_handle.update_in(cx, |picker, window, cx| {
+                    picker.delegate.all_worktrees = all_worktrees;
+                    picker.delegate.default_branch_name =
+                        default_branch.map(|branch| branch.to_string());
+                    picker.refresh(window, cx);
+                })?;
+
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+
         if let Some(repo) = &repository {
-            let picker_entity = picker.clone();
-            subscriptions.push(cx.subscribe(
+            let picker_entity = picker.downgrade();
+            subscriptions.push(cx.subscribe_in(
                 repo,
-                move |_this, repo, event: &RepositoryEvent, cx| {
+                window,
+                move |_this, repo, event: &RepositoryEvent, window, cx| {
                     if matches!(event, RepositoryEvent::GitWorktreeListChanged) {
                         let worktrees_request = repo.update(cx, |repo, _| repo.worktrees());
                         let picker = picker_entity.clone();
-                        cx.spawn(async move |_, cx| {
+                        cx.spawn_in(window, async move |_, cx| {
                             let all_worktrees: Vec<_> = worktrees_request
                                 .await??
                                 .into_iter()
-                                .filter(|worktree| !worktree.is_bare)
+                                .filter(|wt| !wt.is_bare)
                                 .collect();
-                            picker.update(cx, |picker, cx| {
-                                picker.delegate.all_worktrees = Some(all_worktrees);
-                                picker.delegate.refresh_forbidden_deletion_path(cx);
-                            });
+                            picker.update_in(cx, |picker, window, cx| {
+                                picker.delegate.all_worktrees = all_worktrees;
+                                picker.refresh(window, cx);
+                            })?;
                             anyhow::Ok(())
                         })
                         .detach_and_log_err(cx);
@@ -165,386 +187,166 @@ impl WorktreeList {
             ));
         }
 
+        subscriptions.push(cx.subscribe(&picker, |_, _, _, cx| {
+            cx.emit(DismissEvent);
+        }));
+
         Self {
+            focus_handle: picker.focus_handle(cx),
             picker,
-            picker_focus_handle,
-            width,
             _subscriptions: subscriptions,
-            embedded,
         }
     }
-
-    fn new_embedded(
-        repository: Option<Entity<Repository>>,
-        workspace: WeakEntity<Workspace>,
-        width: Rems,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let mut this = Self::new_inner(repository, workspace, width, true, window, cx);
-        this._subscriptions
-            .push(cx.subscribe(&this.picker, |_, _, _, cx| {
-                cx.emit(DismissEvent);
-            }));
-        this
-    }
-
-    pub fn handle_modifiers_changed(
-        &mut self,
-        ev: &ModifiersChangedEvent,
-        _: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.picker
-            .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)
-    }
-
-    pub fn handle_new_worktree(
-        &mut self,
-        replace_current_window: bool,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.picker.update(cx, |picker, cx| {
-            let ix = picker.delegate.selected_index();
-            let Some(entry) = picker.delegate.matches.get(ix) else {
-                return;
-            };
-            let Some(default_branch) = picker.delegate.default_branch.clone() else {
-                return;
-            };
-            if !entry.is_new {
-                return;
-            }
-            picker.delegate.create_worktree(
-                entry.worktree.display_name(),
-                replace_current_window,
-                Some(default_branch.into()),
-                window,
-                cx,
-            );
-        })
-    }
-
-    pub fn handle_delete(
-        &mut self,
-        _: &DeleteWorktree,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.picker.update(cx, |picker, cx| {
-            picker
-                .delegate
-                .delete_at(picker.delegate.selected_index, window, cx)
-        })
-    }
 }
-impl ModalView for WorktreeList {}
-impl EventEmitter<DismissEvent> for WorktreeList {}
 
-impl Focusable for WorktreeList {
-    fn focus_handle(&self, _: &App) -> FocusHandle {
-        self.picker_focus_handle.clone()
+impl Focusable for WorktreePicker {
+    fn focus_handle(&self, _cx: &App) -> FocusHandle {
+        self.focus_handle.clone()
     }
 }
 
-impl Render for WorktreeList {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+impl ModalView for WorktreePicker {}
+impl EventEmitter<DismissEvent> for WorktreePicker {}
+
+impl Render for WorktreePicker {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         v_flex()
-            .key_context("GitWorktreeSelector")
-            .w(self.width)
-            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
-            .on_action(cx.listener(|this, _: &WorktreeFromDefault, w, cx| {
-                this.handle_new_worktree(false, w, cx)
-            }))
-            .on_action(cx.listener(|this, _: &WorktreeFromDefaultOnWindow, w, cx| {
-                this.handle_new_worktree(true, w, cx)
+            .key_context("WorktreePicker")
+            .w(rems(34.))
+            .elevation_3(cx)
+            .child(self.picker.clone())
+            .on_mouse_down_out(cx.listener(|_, _, _, cx| {
+                cx.emit(DismissEvent);
             }))
             .on_action(cx.listener(|this, _: &DeleteWorktree, window, cx| {
-                this.handle_delete(&DeleteWorktree, window, cx)
+                this.picker.update(cx, |picker, cx| {
+                    let ix = picker.delegate.selected_index;
+                    picker.delegate.delete_worktree(ix, window, cx);
+                });
             }))
-            .child(self.picker.clone())
-            .when(!self.embedded, |el| {
-                el.on_mouse_down_out({
-                    cx.listener(move |this, _, window, cx| {
-                        this.picker.update(cx, |this, cx| {
-                            this.cancel(&Default::default(), window, cx);
-                        })
-                    })
-                })
-            })
     }
 }
 
-#[derive(Debug, Clone)]
-struct WorktreeEntry {
-    worktree: GitWorktree,
-    positions: Vec<usize>,
-    is_new: bool,
+#[derive(Clone)]
+enum WorktreeEntry {
+    CreateFromCurrentBranch,
+    CreateFromDefaultBranch {
+        default_branch_name: String,
+    },
+    Separator,
+    Worktree {
+        worktree: GitWorktree,
+        positions: Vec<usize>,
+    },
+    CreateNamed {
+        name: String,
+        from_branch: Option<String>,
+        disabled_reason: Option<String>,
+    },
 }
 
-impl WorktreeEntry {
-    fn can_delete(&self, forbidden_deletion_path: Option<&PathBuf>) -> bool {
-        !self.is_new
-            && !self.worktree.is_main
-            && forbidden_deletion_path != Some(&self.worktree.path)
-    }
-}
-
-pub struct WorktreeListDelegate {
+struct WorktreePickerDelegate {
     matches: Vec<WorktreeEntry>,
-    all_worktrees: Option<Vec<GitWorktree>>,
-    workspace: WeakEntity<Workspace>,
-    repo: Option<Entity<Repository>>,
+    all_worktrees: Vec<GitWorktree>,
+    project_worktree_paths: HashSet<PathBuf>,
     selected_index: usize,
-    last_query: String,
-    modifiers: Modifiers,
+    project: Entity<Project>,
+    workspace: WeakEntity<Workspace>,
+    focused_dock: Option<DockPosition>,
+    current_branch_name: Option<String>,
+    default_branch_name: Option<String>,
+    has_multiple_repositories: bool,
     focus_handle: FocusHandle,
-    default_branch: Option<SharedString>,
-    forbidden_deletion_path: Option<PathBuf>,
-    current_worktree_path: Option<PathBuf>,
+    show_footer: bool,
 }
 
-impl WorktreeListDelegate {
-    fn new(
-        workspace: WeakEntity<Workspace>,
-        repo: Option<Entity<Repository>>,
-        _window: &mut Window,
-        cx: &mut Context<WorktreeList>,
-    ) -> Self {
-        let current_worktree_path = repo
-            .as_ref()
-            .map(|r| r.read(cx).work_directory_abs_path.to_path_buf());
+impl WorktreePickerDelegate {
+    fn build_fixed_entries(&self) -> Vec<WorktreeEntry> {
+        let mut entries = Vec::new();
 
-        Self {
-            matches: vec![],
-            all_worktrees: None,
-            workspace,
-            selected_index: 0,
-            repo,
-            last_query: Default::default(),
-            modifiers: Default::default(),
-            focus_handle: cx.focus_handle(),
-            default_branch: None,
-            forbidden_deletion_path: None,
-            current_worktree_path,
-        }
-    }
+        entries.push(WorktreeEntry::CreateFromCurrentBranch);
 
-    fn create_worktree(
-        &self,
-        worktree_branch: &str,
-        replace_current_window: bool,
-        commit: Option<String>,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) {
-        let Some(repo) = self.repo.clone() else {
-            return;
-        };
-
-        let branch = worktree_branch.to_string();
-        let workspace = self.workspace.clone();
-        cx.spawn_in(window, async move |_, cx| {
-            let (receiver, new_worktree_path) = repo.update(cx, |repo, cx| {
-                let worktree_directory_setting = ProjectSettings::get_global(cx)
-                    .git
-                    .worktree_directory
-                    .clone();
-                let new_worktree_path =
-                    repo.path_for_new_linked_worktree(&branch, &worktree_directory_setting)?;
-                let receiver = repo.create_worktree(
-                    git::repository::CreateWorktreeTarget::NewBranch {
-                        branch_name: branch.clone(),
-                        base_sha: commit,
-                    },
-                    new_worktree_path.clone(),
-                );
-                anyhow::Ok((receiver, new_worktree_path))
-            })?;
-            receiver.await??;
-
-            workspace.update(cx, |workspace, cx| {
-                if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
-                    let repo_path = &repo.read(cx).snapshot().work_directory_abs_path;
-                    let project = workspace.project();
-                    if let Some((parent_worktree, _)) =
-                        project.read(cx).find_worktree(repo_path, cx)
-                    {
-                        let worktree_store = project.read(cx).worktree_store();
-                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
-                            if trusted_worktrees.can_trust(
-                                &worktree_store,
-                                parent_worktree.read(cx).id(),
-                                cx,
-                            ) {
-                                trusted_worktrees.trust(
-                                    &worktree_store,
-                                    HashSet::from_iter([PathTrust::AbsPath(
-                                        new_worktree_path.clone(),
-                                    )]),
-                                    cx,
-                                );
-                            }
-                        });
-                    }
+        if !self.has_multiple_repositories {
+            if let Some(ref default_branch) = self.default_branch_name {
+                let is_different = self
+                    .current_branch_name
+                    .as_ref()
+                    .is_none_or(|current| current != default_branch);
+                if is_different {
+                    entries.push(WorktreeEntry::CreateFromDefaultBranch {
+                        default_branch_name: default_branch.clone(),
+                    });
                 }
-            })?;
-
-            let (connection_options, app_state, is_local) =
-                workspace.update(cx, |workspace, cx| {
-                    let project = workspace.project().clone();
-                    let connection_options = project.read(cx).remote_connection_options(cx);
-                    let app_state = workspace.app_state().clone();
-                    let is_local = project.read(cx).is_local();
-                    (connection_options, app_state, is_local)
-                })?;
-
-            if is_local {
-                workspace
-                    .update_in(cx, |workspace, window, cx| {
-                        workspace.open_workspace_for_paths(
-                            OpenMode::Activate,
-                            vec![new_worktree_path],
-                            window,
-                            cx,
-                        )
-                    })?
-                    .await?;
-            } else if let Some(connection_options) = connection_options {
-                open_remote_worktree(
-                    connection_options,
-                    vec![new_worktree_path],
-                    app_state,
-                    workspace.clone(),
-                    replace_current_window,
-                    cx,
-                )
-                .await?;
             }
+        }
 
-            anyhow::Ok(())
-        })
-        .detach_and_prompt_err("Failed to create worktree", window, cx, |e, _, _| {
-            let msg = e.to_string();
-            if msg.contains("git.worktree_directory") {
-                Some(format!("Invalid git.worktree_directory setting: {}", e))
-            } else {
-                Some(msg)
-            }
-        });
+        entries
     }
 
-    fn open_worktree(
-        &self,
-        worktree_path: &PathBuf,
-        replace_current_window: bool,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) {
-        let workspace = self.workspace.clone();
-        let path = worktree_path.clone();
-
-        let Some((connection_options, app_state, is_local)) = workspace
-            .update(cx, |workspace, cx| {
-                let project = workspace.project().clone();
-                let connection_options = project.read(cx).remote_connection_options(cx);
-                let app_state = workspace.app_state().clone();
-                let is_local = project.read(cx).is_local();
-                (connection_options, app_state, is_local)
-            })
-            .log_err()
-        else {
-            return;
-        };
-        let open_mode = if replace_current_window {
-            OpenMode::Activate
-        } else {
-            OpenMode::NewWindow
-        };
+    fn all_repo_worktrees(&self) -> &[GitWorktree] {
+        &self.all_worktrees
+    }
 
-        if is_local {
-            let open_task = workspace.update(cx, |workspace, cx| {
-                workspace.open_workspace_for_paths(open_mode, vec![path], window, cx)
-            });
-            cx.spawn(async move |_, _| {
-                open_task?.await?;
-                anyhow::Ok(())
-            })
-            .detach_and_prompt_err(
-                "Failed to open worktree",
-                window,
-                cx,
-                |e, _, _| Some(e.to_string()),
-            );
-        } else if let Some(connection_options) = connection_options {
-            cx.spawn_in(window, async move |_, cx| {
-                open_remote_worktree(
-                    connection_options,
-                    vec![path],
-                    app_state,
-                    workspace,
-                    replace_current_window,
-                    cx,
-                )
-                .await
-            })
-            .detach_and_prompt_err(
-                "Failed to open worktree",
-                window,
-                cx,
-                |e, _, _| Some(e.to_string()),
-            );
+    fn creation_blocked_reason(&self, cx: &App) -> Option<SharedString> {
+        let project = self.project.read(cx);
+        if project.is_via_collab() {
+            Some("Worktree creation is not supported in collaborative projects".into())
+        } else if project.repositories(cx).is_empty() {
+            Some("Requires a Git repository in the project".into())
+        } else {
+            None
         }
-
-        cx.emit(DismissEvent);
     }
 
-    fn base_branch<'a>(&'a self, cx: &'a mut Context<Picker<Self>>) -> Option<&'a str> {
-        self.repo
-            .as_ref()
-            .and_then(|repo| repo.read(cx).branch.as_ref().map(|b| b.name()))
+    fn can_delete_worktree(&self, worktree: &GitWorktree) -> bool {
+        !worktree.is_main && !self.project_worktree_paths.contains(&worktree.path)
     }
 
-    fn delete_at(&self, idx: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        let Some(entry) = self.matches.get(idx).cloned() else {
+    fn delete_worktree(&self, ix: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        let Some(entry) = self.matches.get(ix) else {
+            return;
+        };
+        let WorktreeEntry::Worktree { worktree, .. } = entry else {
             return;
         };
-        if !entry.can_delete(self.forbidden_deletion_path.as_ref()) {
+        if !self.can_delete_worktree(worktree) {
             return;
         }
-        let Some(repo) = self.repo.clone() else {
+
+        let repo = self.project.read(cx).active_repository(cx);
+        let Some(repo) = repo else {
             return;
         };
+        let path = worktree.path.clone();
         let workspace = self.workspace.clone();
-        let path = entry.worktree.path;
 
         cx.spawn_in(window, async move |picker, cx| {
             let result = repo
                 .update(cx, |repo, _| repo.remove_worktree(path.clone(), false))
                 .await?;
 
-            if let Err(e) = result {
-                log::error!("Failed to remove worktree: {}", e);
+            if let Err(error) = result {
+                log::error!("Failed to remove worktree: {}", error);
+
                 if let Some(workspace) = workspace.upgrade() {
                     cx.update(|_window, cx| {
                         show_error_toast(
                             workspace,
                             format!("worktree remove {}", path.display()),
-                            e,
+                            error,
                             cx,
                         )
                     })?;
                 }
+
                 return Ok(());
             }
 
-            picker.update_in(cx, |picker, _, cx| {
-                picker.delegate.matches.retain(|e| e.worktree.path != path);
-                if let Some(all_worktrees) = &mut picker.delegate.all_worktrees {
-                    all_worktrees.retain(|w| w.path != path);
-                }
-                picker.delegate.refresh_forbidden_deletion_path(cx);
+            picker.update_in(cx, |picker, _window, cx| {
+                picker.delegate.matches.retain(|e| {
+                    !matches!(e, WorktreeEntry::Worktree { worktree, .. } if worktree.path == path)
+                });
+                picker.delegate.all_worktrees.retain(|w| w.path != path);
                 if picker.delegate.matches.is_empty() {
                     picker.delegate.selected_index = 0;
                 } else if picker.delegate.selected_index >= picker.delegate.matches.len() {
@@ -555,139 +357,37 @@ impl WorktreeListDelegate {
 
             anyhow::Ok(())
         })
-        .detach();
+        .detach_and_log_err(cx);
     }
 
-    fn refresh_forbidden_deletion_path(&mut self, cx: &App) {
-        let Some(workspace) = self.workspace.upgrade() else {
-            debug_panic!("Workspace should always be available or else the picker would be closed");
-            self.forbidden_deletion_path = None;
+    fn sync_selected_index(&mut self, has_query: bool) {
+        if !has_query {
             return;
-        };
-
-        let visible_worktree_paths = workspace.read_with(cx, |workspace, cx| {
-            workspace
-                .project()
-                .read(cx)
-                .visible_worktrees(cx)
-                .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
-                .collect::<Vec<_>>()
-        });
+        }
 
-        self.forbidden_deletion_path = if visible_worktree_paths.len() == 1 {
-            visible_worktree_paths.into_iter().next()
+        if let Some(index) = self
+            .matches
+            .iter()
+            .position(|entry| matches!(entry, WorktreeEntry::Worktree { .. }))
+        {
+            self.selected_index = index;
+        } else if let Some(index) = self
+            .matches
+            .iter()
+            .position(|entry| matches!(entry, WorktreeEntry::CreateNamed { .. }))
+        {
+            self.selected_index = index;
         } else {
-            None
-        };
+            self.selected_index = 0;
+        }
     }
 }
 
-async fn open_remote_worktree(
-    connection_options: RemoteConnectionOptions,
-    paths: Vec<PathBuf>,
-    app_state: Arc<workspace::AppState>,
-    workspace: WeakEntity<Workspace>,
-    replace_current_window: bool,
-    cx: &mut AsyncWindowContext,
-) -> anyhow::Result<()> {
-    let workspace_window = cx
-        .window_handle()
-        .downcast::<MultiWorkspace>()
-        .ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?;
-
-    let connect_task = workspace.update_in(cx, |workspace, window, cx| {
-        workspace.toggle_modal(window, cx, |window, cx| {
-            RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
-        });
-
-        let prompt = workspace
-            .active_modal::<RemoteConnectionModal>(cx)
-            .expect("Modal just created")
-            .read(cx)
-            .prompt
-            .clone();
-
-        connect(
-            ConnectionIdentifier::setup(),
-            connection_options.clone(),
-            prompt,
-            window,
-            cx,
-        )
-        .prompt_err("Failed to connect", window, cx, |_, _, _| None)
-    })?;
-
-    let session = connect_task.await;
-
-    workspace
-        .update_in(cx, |workspace, _window, cx| {
-            if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
-                prompt.update(cx, |prompt, cx| prompt.finished(cx))
-            }
-        })
-        .ok();
-
-    let Some(Some(session)) = session else {
-        return Ok(());
-    };
-
-    let new_project: Entity<project::Project> = cx.update(|_, cx| {
-        project::Project::remote(
-            session,
-            app_state.client.clone(),
-            app_state.node_runtime.clone(),
-            app_state.user_store.clone(),
-            app_state.languages.clone(),
-            app_state.fs.clone(),
-            true,
-            cx,
-        )
-    })?;
-
-    let window_to_use = if replace_current_window {
-        workspace_window
-    } else {
-        let workspace_position = cx
-            .update(|_, cx| {
-                workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
-            })?
-            .await
-            .context("fetching workspace position from db")?;
-
-        let mut options =
-            cx.update(|_, cx| (app_state.build_window_options)(workspace_position.display, cx))?;
-        options.window_bounds = workspace_position.window_bounds;
-
-        cx.open_window(options, |window, cx| {
-            let workspace = cx.new(|cx| {
-                let mut workspace =
-                    Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
-                workspace.centered_layout = workspace_position.centered_layout;
-                workspace
-            });
-            cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
-        })?
-    };
-
-    workspace::open_remote_project_with_existing_connection(
-        connection_options,
-        new_project,
-        paths,
-        app_state,
-        window_to_use,
-        None,
-        cx,
-    )
-    .await?;
-
-    Ok(())
-}
-
-impl PickerDelegate for WorktreeListDelegate {
-    type ListItem = ListItem;
+impl PickerDelegate for WorktreePickerDelegate {
+    type ListItem = AnyElement;
 
     fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        "Select worktree…".into()
+        "Select a worktree…".into()
     }
 
     fn editor_position(&self) -> PickerEditorPosition {
@@ -706,115 +406,276 @@ impl PickerDelegate for WorktreeListDelegate {
         &mut self,
         ix: usize,
         _window: &mut Window,
-        _: &mut Context<Picker<Self>>,
+        _cx: &mut Context<Picker<Self>>,
     ) {
         self.selected_index = ix;
     }
 
+    fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
+        !matches!(self.matches.get(ix), Some(WorktreeEntry::Separator))
+    }
+
     fn update_matches(
         &mut self,
         query: String,
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Task<()> {
-        let Some(all_worktrees) = self.all_worktrees.clone() else {
-            return Task::ready(());
+        let repo_worktrees = self.all_repo_worktrees().to_vec();
+
+        let normalized_query = query.replace(' ', "-");
+        let main_worktree_path = self
+            .all_worktrees
+            .iter()
+            .find(|wt| wt.is_main)
+            .map(|wt| wt.path.clone());
+        let has_named_worktree = self.all_worktrees.iter().any(|worktree| {
+            worktree.directory_name(main_worktree_path.as_deref()) == normalized_query
+        });
+        let create_named_disabled_reason: Option<String> = if self.has_multiple_repositories {
+            Some("Cannot create a named worktree in a project with multiple repositories".into())
+        } else if has_named_worktree {
+            Some("A worktree with this name already exists".into())
+        } else {
+            None
         };
 
-        cx.spawn_in(window, async move |picker, cx| {
-            let main_worktree_path = all_worktrees
-                .iter()
-                .find(|wt| wt.is_main)
-                .map(|wt| wt.path.clone());
-
-            let mut matches: Vec<WorktreeEntry> = if query.is_empty() {
-                all_worktrees
-                    .into_iter()
-                    .map(|worktree| WorktreeEntry {
-                        worktree,
-                        positions: Vec::new(),
-                        is_new: false,
-                    })
-                    .collect()
-            } else {
-                let candidates = all_worktrees
+        let show_default_branch_create = !self.has_multiple_repositories
+            && self.default_branch_name.as_ref().is_some_and(|default| {
+                self.current_branch_name
+                    .as_ref()
+                    .is_none_or(|current| current != default)
+            });
+        let default_branch_name = self.default_branch_name.clone();
+
+        if query.is_empty() {
+            let mut matches = self.build_fixed_entries();
+
+            if !repo_worktrees.is_empty() {
+                let main_worktree_path = repo_worktrees
                     .iter()
-                    .enumerate()
-                    .map(|(ix, worktree)| {
-                        StringMatchCandidate::new(
-                            ix,
-                            &worktree.directory_name(main_worktree_path.as_deref()),
-                        )
+                    .find(|wt| wt.is_main)
+                    .map(|wt| wt.path.clone());
+
+                let mut sorted = repo_worktrees;
+                let project_paths = &self.project_worktree_paths;
+
+                sorted.sort_by(|a, b| {
+                    let a_is_current = project_paths.contains(&a.path);
+                    let b_is_current = project_paths.contains(&b.path);
+                    b_is_current.cmp(&a_is_current).then_with(|| {
+                        a.directory_name(main_worktree_path.as_deref())
+                            .cmp(&b.directory_name(main_worktree_path.as_deref()))
                     })
-                    .collect::<Vec<StringMatchCandidate>>();
-                fuzzy::match_strings(
-                    &candidates,
-                    &query,
-                    true,
-                    true,
-                    10000,
-                    &Default::default(),
-                    cx.background_executor().clone(),
+                });
+
+                matches.push(WorktreeEntry::Separator);
+                for worktree in sorted {
+                    matches.push(WorktreeEntry::Worktree {
+                        worktree,
+                        positions: Vec::new(),
+                    });
+                }
+            }
+
+            self.matches = matches;
+            self.sync_selected_index(false);
+            return Task::ready(());
+        }
+
+        let main_worktree_path = repo_worktrees
+            .iter()
+            .find(|wt| wt.is_main)
+            .map(|wt| wt.path.clone());
+        let candidates: Vec<_> = repo_worktrees
+            .iter()
+            .enumerate()
+            .map(|(ix, worktree)| {
+                StringMatchCandidate::new(
+                    ix,
+                    &worktree.directory_name(main_worktree_path.as_deref()),
                 )
-                .await
-                .into_iter()
-                .map(|candidate| WorktreeEntry {
-                    worktree: all_worktrees[candidate.candidate_id].clone(),
-                    positions: candidate.positions,
-                    is_new: false,
-                })
-                .collect()
-            };
+            })
+            .collect();
+
+        let executor = cx.background_executor().clone();
+
+        let task = cx.background_executor().spawn(async move {
+            fuzzy::match_strings(
+                &candidates,
+                &query,
+                true,
+                true,
+                10000,
+                &Default::default(),
+                executor,
+            )
+            .await
+        });
+
+        let repo_worktrees_clone = repo_worktrees;
+        cx.spawn_in(window, async move |picker, cx| {
+            let fuzzy_matches = task.await;
+
             picker
-                .update(cx, |picker, _| {
-                    if !query.is_empty()
-                        && !matches.first().is_some_and(|entry| {
-                            entry.worktree.directory_name(main_worktree_path.as_deref()) == query
-                        })
-                    {
-                        let query = query.replace(' ', "-");
-                        matches.push(WorktreeEntry {
-                            worktree: GitWorktree {
-                                path: Default::default(),
-                                ref_name: Some(format!("refs/heads/{query}").into()),
-                                sha: Default::default(),
-                                is_main: false,
-                                is_bare: false,
-                            },
-                            positions: Vec::new(),
-                            is_new: true,
-                        })
+                .update_in(cx, |picker, _window, cx| {
+                    let mut new_matches: Vec<WorktreeEntry> = Vec::new();
+
+                    for candidate in &fuzzy_matches {
+                        new_matches.push(WorktreeEntry::Worktree {
+                            worktree: repo_worktrees_clone[candidate.candidate_id].clone(),
+                            positions: candidate.positions.clone(),
+                        });
                     }
-                    let delegate = &mut picker.delegate;
-                    delegate.matches = matches;
-                    if delegate.matches.is_empty() {
-                        delegate.selected_index = 0;
-                    } else {
-                        delegate.selected_index =
-                            core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
+
+                    if !new_matches.is_empty() {
+                        new_matches.push(WorktreeEntry::Separator);
                     }
-                    delegate.last_query = query;
+                    new_matches.push(WorktreeEntry::CreateNamed {
+                        name: normalized_query.clone(),
+                        from_branch: None,
+                        disabled_reason: create_named_disabled_reason.clone(),
+                    });
+                    if show_default_branch_create {
+                        if let Some(ref default_branch) = default_branch_name {
+                            new_matches.push(WorktreeEntry::CreateNamed {
+                                name: normalized_query.clone(),
+                                from_branch: Some(default_branch.clone()),
+                                disabled_reason: create_named_disabled_reason.clone(),
+                            });
+                        }
+                    }
+
+                    picker.delegate.matches = new_matches;
+                    picker.delegate.sync_selected_index(true);
+
+                    cx.notify();
                 })
                 .log_err();
         })
     }
 
     fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        let Some(entry) = self.matches.get(self.selected_index()) else {
+        let Some(entry) = self.matches.get(self.selected_index) else {
             return;
         };
-        if entry.is_new {
-            self.create_worktree(&entry.worktree.display_name(), secondary, None, window, cx);
-        } else {
-            self.open_worktree(&entry.worktree.path, !secondary, window, cx);
+
+        match entry {
+            WorktreeEntry::Separator => return,
+            WorktreeEntry::CreateFromCurrentBranch => {
+                if self.creation_blocked_reason(cx).is_some() {
+                    return;
+                }
+                if let Some(workspace) = self.workspace.upgrade() {
+                    workspace.update(cx, |workspace, cx| {
+                        crate::worktree_service::handle_create_worktree(
+                            workspace,
+                            &CreateWorktree {
+                                worktree_name: None,
+                                branch_target: NewWorktreeBranchTarget::CurrentBranch,
+                            },
+                            window,
+                            self.focused_dock,
+                            cx,
+                        );
+                    });
+                }
+            }
+            WorktreeEntry::CreateFromDefaultBranch {
+                default_branch_name,
+            } => {
+                if self.creation_blocked_reason(cx).is_some() {
+                    return;
+                }
+                if let Some(workspace) = self.workspace.upgrade() {
+                    workspace.update(cx, |workspace, cx| {
+                        crate::worktree_service::handle_create_worktree(
+                            workspace,
+                            &CreateWorktree {
+                                worktree_name: None,
+                                branch_target: NewWorktreeBranchTarget::ExistingBranch {
+                                    name: default_branch_name.clone(),
+                                },
+                            },
+                            window,
+                            self.focused_dock,
+                            cx,
+                        );
+                    });
+                }
+            }
+            WorktreeEntry::Worktree { worktree, .. } => {
+                let is_current = self.project_worktree_paths.contains(&worktree.path);
+
+                if !is_current {
+                    if secondary {
+                        window.dispatch_action(
+                            Box::new(OpenWorktreeInNewWindow {
+                                path: worktree.path.clone(),
+                            }),
+                            cx,
+                        );
+                    } else {
+                        let main_worktree_path = self
+                            .all_worktrees
+                            .iter()
+                            .find(|wt| wt.is_main)
+                            .map(|wt| wt.path.as_path());
+                        if let Some(workspace) = self.workspace.upgrade() {
+                            workspace.update(cx, |workspace, cx| {
+                                crate::worktree_service::handle_switch_worktree(
+                                    workspace,
+                                    &SwitchWorktree {
+                                        path: worktree.path.clone(),
+                                        display_name: worktree.directory_name(main_worktree_path),
+                                    },
+                                    window,
+                                    self.focused_dock,
+                                    cx,
+                                );
+                            });
+                        }
+                    }
+                }
+            }
+            WorktreeEntry::CreateNamed {
+                name,
+                from_branch,
+                disabled_reason: None,
+            } => {
+                let branch_target = match from_branch {
+                    Some(branch) => NewWorktreeBranchTarget::ExistingBranch {
+                        name: branch.clone(),
+                    },
+                    None => NewWorktreeBranchTarget::CurrentBranch,
+                };
+                if let Some(workspace) = self.workspace.upgrade() {
+                    workspace.update(cx, |workspace, cx| {
+                        crate::worktree_service::handle_create_worktree(
+                            workspace,
+                            &CreateWorktree {
+                                worktree_name: Some(name.clone()),
+                                branch_target,
+                            },
+                            window,
+                            self.focused_dock,
+                            cx,
+                        );
+                    });
+                }
+            }
+            WorktreeEntry::CreateNamed {
+                disabled_reason: Some(_),
+                ..
+            } => {
+                return;
+            }
         }
 
         cx.emit(DismissEvent);
     }
 
-    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
-        cx.emit(DismissEvent);
-    }
+    fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
 
     fn render_match(
         &self,

crates/git_ui/src/worktree_service.rs πŸ”—

@@ -0,0 +1,809 @@
+use std::path::PathBuf;
+use std::sync::Arc;
+
+use anyhow::anyhow;
+use collections::HashSet;
+use fs::Fs;
+use gpui::{AsyncWindowContext, Entity, SharedString, WeakEntity};
+use project::Project;
+use project::git_store::Repository;
+use project::project_settings::ProjectSettings;
+use project::trusted_worktrees::{PathTrust, TrustedWorktrees};
+use remote::RemoteConnectionOptions;
+use settings::Settings;
+use workspace::{MultiWorkspace, OpenMode, PreviousWorkspaceState, Workspace, dock::DockPosition};
+use zed_actions::NewWorktreeBranchTarget;
+
+use util::ResultExt as _;
+
+use crate::git_panel::show_error_toast;
+use crate::worktree_names;
+
+/// Whether a worktree operation is creating a new one or switching to an
+/// existing one. Controls whether the source workspace's state (dock layout,
+/// open files, agent panel draft) is inherited by the destination.
+enum WorktreeOperation {
+    Create,
+    Switch,
+}
+
+/// Classifies the project's visible worktrees into git-managed repositories
+/// and non-git paths. Each unique repository is returned only once.
+pub fn classify_worktrees(
+    project: &Project,
+    cx: &gpui::App,
+) -> (Vec<Entity<Repository>>, Vec<PathBuf>) {
+    let repositories = project.repositories(cx).clone();
+    let mut git_repos: Vec<Entity<Repository>> = Vec::new();
+    let mut non_git_paths: Vec<PathBuf> = Vec::new();
+    let mut seen_repo_ids = HashSet::default();
+
+    for worktree in project.visible_worktrees(cx) {
+        let wt_path = worktree.read(cx).abs_path();
+
+        let matching_repo = repositories
+            .iter()
+            .filter_map(|(id, repo)| {
+                let work_dir = repo.read(cx).work_directory_abs_path.clone();
+                if wt_path.starts_with(work_dir.as_ref()) {
+                    Some((*id, repo.clone(), work_dir.as_ref().components().count()))
+                } else {
+                    None
+                }
+            })
+            .max_by(
+                |(left_id, _left_repo, left_depth), (right_id, _right_repo, right_depth)| {
+                    left_depth
+                        .cmp(right_depth)
+                        .then_with(|| left_id.cmp(right_id))
+                },
+            );
+
+        if let Some((id, repo, _)) = matching_repo {
+            if seen_repo_ids.insert(id) {
+                git_repos.push(repo);
+            }
+        } else {
+            non_git_paths.push(wt_path.to_path_buf());
+        }
+    }
+
+    (git_repos, non_git_paths)
+}
+
+/// Resolves a branch target into the ref the new worktree should be based on.
+/// Returns `None` for `CurrentBranch`, meaning "use the current HEAD".
+pub fn resolve_worktree_branch_target(branch_target: &NewWorktreeBranchTarget) -> Option<String> {
+    match branch_target {
+        NewWorktreeBranchTarget::CurrentBranch => None,
+        NewWorktreeBranchTarget::ExistingBranch { name } => Some(name.clone()),
+    }
+}
+
+/// Kicks off an async git-worktree creation for each repository. Returns:
+///
+/// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuples.
+/// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs for remapping editor tabs.
+fn start_worktree_creations(
+    git_repos: &[Entity<Repository>],
+    worktree_name: Option<String>,
+    existing_worktree_names: &[String],
+    existing_worktree_paths: &HashSet<PathBuf>,
+    base_ref: Option<String>,
+    worktree_directory_setting: &str,
+    rng: &mut impl rand::Rng,
+    cx: &mut gpui::App,
+) -> anyhow::Result<(
+    Vec<(
+        Entity<Repository>,
+        PathBuf,
+        futures::channel::oneshot::Receiver<anyhow::Result<()>>,
+    )>,
+    Vec<(PathBuf, PathBuf)>,
+)> {
+    let mut creation_infos = Vec::new();
+    let mut path_remapping = Vec::new();
+
+    let worktree_name = worktree_name.unwrap_or_else(|| {
+        let existing_refs: Vec<&str> = existing_worktree_names.iter().map(|s| s.as_str()).collect();
+        worktree_names::generate_worktree_name(&existing_refs, rng)
+            .unwrap_or_else(|| "worktree".to_string())
+    });
+
+    for repo in git_repos {
+        let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| {
+            let new_path =
+                repo.path_for_new_linked_worktree(&worktree_name, worktree_directory_setting)?;
+            if existing_worktree_paths.contains(&new_path) {
+                anyhow::bail!("A worktree already exists at {}", new_path.display());
+            }
+            let target = git::repository::CreateWorktreeTarget::Detached {
+                base_sha: base_ref.clone(),
+            };
+            let receiver = repo.create_worktree(target, new_path.clone());
+            let work_dir = repo.work_directory_abs_path.clone();
+            anyhow::Ok((work_dir, new_path, receiver))
+        })?;
+        path_remapping.push((work_dir.to_path_buf(), new_path.clone()));
+        creation_infos.push((repo.clone(), new_path, receiver));
+    }
+
+    Ok((creation_infos, path_remapping))
+}
+
+/// Waits for every in-flight worktree creation to complete. If any
+/// creation fails, all successfully-created worktrees are rolled back
+/// (removed) so the project isn't left in a half-migrated state.
+pub async fn await_and_rollback_on_failure(
+    creation_infos: Vec<(
+        Entity<Repository>,
+        PathBuf,
+        futures::channel::oneshot::Receiver<anyhow::Result<()>>,
+    )>,
+    fs: Arc<dyn Fs>,
+    cx: &mut AsyncWindowContext,
+) -> anyhow::Result<Vec<PathBuf>> {
+    let mut created_paths: Vec<PathBuf> = Vec::new();
+    let mut repos_and_paths: Vec<(Entity<Repository>, PathBuf)> = Vec::new();
+    let mut first_error: Option<anyhow::Error> = None;
+
+    for (repo, new_path, receiver) in creation_infos {
+        repos_and_paths.push((repo.clone(), new_path.clone()));
+        match receiver.await {
+            Ok(Ok(())) => {
+                created_paths.push(new_path);
+            }
+            Ok(Err(err)) => {
+                if first_error.is_none() {
+                    first_error = Some(err);
+                }
+            }
+            Err(_canceled) => {
+                if first_error.is_none() {
+                    first_error = Some(anyhow!("Worktree creation was canceled"));
+                }
+            }
+        }
+    }
+
+    let Some(err) = first_error else {
+        return Ok(created_paths);
+    };
+
+    // Rollback all attempted worktrees
+    let mut rollback_futures = Vec::new();
+    for (rollback_repo, rollback_path) in &repos_and_paths {
+        let receiver = cx
+            .update(|_, cx| {
+                rollback_repo.update(cx, |repo, _cx| {
+                    repo.remove_worktree(rollback_path.clone(), true)
+                })
+            })
+            .ok();
+
+        rollback_futures.push((rollback_path.clone(), receiver));
+    }
+
+    let mut rollback_failures: Vec<String> = Vec::new();
+    for (path, receiver_opt) in rollback_futures {
+        let mut git_remove_failed = false;
+
+        if let Some(receiver) = receiver_opt {
+            match receiver.await {
+                Ok(Ok(())) => {}
+                Ok(Err(rollback_err)) => {
+                    log::error!(
+                        "git worktree remove failed for {}: {rollback_err}",
+                        path.display()
+                    );
+                    git_remove_failed = true;
+                }
+                Err(canceled) => {
+                    log::error!(
+                        "git worktree remove failed for {}: {canceled}",
+                        path.display()
+                    );
+                    git_remove_failed = true;
+                }
+            }
+        } else {
+            log::error!(
+                "failed to dispatch git worktree remove for {}",
+                path.display()
+            );
+            git_remove_failed = true;
+        }
+
+        if git_remove_failed {
+            if let Err(fs_err) = fs
+                .remove_dir(
+                    &path,
+                    fs::RemoveOptions {
+                        recursive: true,
+                        ignore_if_not_exists: true,
+                    },
+                )
+                .await
+            {
+                let msg = format!("{}: failed to remove directory: {fs_err}", path.display());
+                log::error!("{}", msg);
+                rollback_failures.push(msg);
+            }
+        }
+    }
+    let mut error_message = format!("Failed to create worktree: {err}");
+    if !rollback_failures.is_empty() {
+        error_message.push_str("\n\nFailed to clean up: ");
+        error_message.push_str(&rollback_failures.join(", "));
+    }
+    Err(anyhow!(error_message))
+}
+
+/// Propagates worktree trust from the source workspace to the new workspace.
+/// If the source project's worktrees are all trusted, the new worktree paths
+/// will also be trusted automatically.
+fn maybe_propagate_worktree_trust(
+    source_workspace: &WeakEntity<Workspace>,
+    new_workspace: &Entity<Workspace>,
+    paths: &[PathBuf],
+    cx: &mut AsyncWindowContext,
+) {
+    cx.update(|_, cx| {
+        if ProjectSettings::get_global(cx).session.trust_all_worktrees {
+            return;
+        }
+        let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) else {
+            return;
+        };
+
+        let source_is_trusted = source_workspace
+            .upgrade()
+            .map(|workspace| {
+                let source_worktree_store = workspace.read(cx).project().read(cx).worktree_store();
+                !trusted_store
+                    .read(cx)
+                    .has_restricted_worktrees(&source_worktree_store, cx)
+            })
+            .unwrap_or(false);
+
+        if !source_is_trusted {
+            return;
+        }
+
+        let worktree_store = new_workspace.read(cx).project().read(cx).worktree_store();
+        let paths_to_trust: HashSet<_> = paths
+            .iter()
+            .filter_map(|path| {
+                let (worktree, _) = worktree_store.read(cx).find_worktree(path, cx)?;
+                Some(PathTrust::Worktree(worktree.read(cx).id()))
+            })
+            .collect();
+
+        if !paths_to_trust.is_empty() {
+            trusted_store.update(cx, |store, cx| {
+                store.trust(&worktree_store, paths_to_trust, cx);
+            });
+        }
+    })
+    .ok();
+}
+
+/// Handles the `CreateWorktree` action generically, without any agent panel involvement.
+/// Creates a new git worktree, opens the workspace, restores layout and files.
+pub fn handle_create_worktree(
+    workspace: &mut Workspace,
+    action: &zed_actions::CreateWorktree,
+    window: &mut gpui::Window,
+    fallback_focused_dock: Option<DockPosition>,
+    cx: &mut gpui::Context<Workspace>,
+) {
+    let project = workspace.project().clone();
+
+    if project.read(cx).repositories(cx).is_empty() {
+        log::error!("create_worktree: no git repository in the project");
+        return;
+    }
+    if project.read(cx).is_via_collab() {
+        log::error!("create_worktree: not supported in collab projects");
+        return;
+    }
+
+    // Guard against concurrent creation
+    if workspace.active_worktree_creation().label.is_some() {
+        return;
+    }
+
+    let previous_state =
+        workspace.capture_state_for_worktree_switch(window, fallback_focused_dock, cx);
+    let workspace_handle = workspace.weak_handle();
+    let window_handle = window.window_handle().downcast::<MultiWorkspace>();
+    let remote_connection_options = project.read(cx).remote_connection_options(cx);
+
+    let (git_repos, non_git_paths) = classify_worktrees(project.read(cx), cx);
+
+    if git_repos.is_empty() {
+        show_error_toast(
+            cx.entity(),
+            "worktree create",
+            anyhow!("No git repositories found in the project"),
+            cx,
+        );
+        return;
+    }
+
+    if remote_connection_options.is_some() {
+        let is_disconnected = project
+            .read(cx)
+            .remote_client()
+            .is_some_and(|client| client.read(cx).is_disconnected());
+        if is_disconnected {
+            show_error_toast(
+                cx.entity(),
+                "worktree create",
+                anyhow!("Cannot create worktree: remote connection is not active"),
+                cx,
+            );
+            return;
+        }
+    }
+
+    let worktree_name = action.worktree_name.clone();
+    let branch_target = action.branch_target.clone();
+    let display_name: SharedString = worktree_name
+        .as_deref()
+        .unwrap_or("worktree")
+        .to_string()
+        .into();
+
+    workspace.set_active_worktree_creation(Some(display_name), false, cx);
+
+    cx.spawn_in(window, async move |_workspace_entity, mut cx| {
+        let result = do_create_worktree(
+            git_repos,
+            non_git_paths,
+            worktree_name,
+            branch_target,
+            previous_state,
+            workspace_handle.clone(),
+            window_handle,
+            remote_connection_options,
+            &mut cx,
+        )
+        .await;
+
+        if let Err(err) = &result {
+            log::error!("Failed to create worktree: {err}");
+            workspace_handle
+                .update(cx, |workspace, cx| {
+                    workspace.set_active_worktree_creation(None, false, cx);
+                    show_error_toast(cx.entity(), "worktree create", anyhow!("{err:#}"), cx);
+                })
+                .ok();
+        }
+
+        result
+    })
+    .detach_and_log_err(cx);
+}
+
+pub fn handle_switch_worktree(
+    workspace: &mut Workspace,
+    action: &zed_actions::SwitchWorktree,
+    window: &mut gpui::Window,
+    fallback_focused_dock: Option<DockPosition>,
+    cx: &mut gpui::Context<Workspace>,
+) {
+    let project = workspace.project().clone();
+
+    if project.read(cx).repositories(cx).is_empty() {
+        log::error!("switch_to_worktree: no git repository in the project");
+        return;
+    }
+    if project.read(cx).is_via_collab() {
+        log::error!("switch_to_worktree: not supported in collab projects");
+        return;
+    }
+
+    // Guard against concurrent creation
+    if workspace.active_worktree_creation().label.is_some() {
+        return;
+    }
+
+    let previous_state =
+        workspace.capture_state_for_worktree_switch(window, fallback_focused_dock, cx);
+    let workspace_handle = workspace.weak_handle();
+    let window_handle = window.window_handle().downcast::<MultiWorkspace>();
+    let remote_connection_options = project.read(cx).remote_connection_options(cx);
+
+    let (git_repos, non_git_paths) = classify_worktrees(project.read(cx), cx);
+
+    let git_repo_work_dirs: Vec<PathBuf> = git_repos
+        .iter()
+        .map(|repo| repo.read(cx).work_directory_abs_path.to_path_buf())
+        .collect();
+
+    let display_name: SharedString = action.display_name.clone().into();
+
+    workspace.set_active_worktree_creation(Some(display_name), true, cx);
+
+    let worktree_path = action.path.clone();
+
+    cx.spawn_in(window, async move |_workspace_entity, mut cx| {
+        let result = do_switch_worktree(
+            worktree_path,
+            git_repo_work_dirs,
+            non_git_paths,
+            previous_state,
+            workspace_handle.clone(),
+            window_handle,
+            remote_connection_options,
+            &mut cx,
+        )
+        .await;
+
+        if let Err(err) = &result {
+            log::error!("Failed to switch worktree: {err}");
+            workspace_handle
+                .update(cx, |workspace, cx| {
+                    workspace.set_active_worktree_creation(None, false, cx);
+                    show_error_toast(cx.entity(), "worktree switch", anyhow!("{err:#}"), cx);
+                })
+                .ok();
+        }
+
+        result
+    })
+    .detach_and_log_err(cx);
+}
+
+async fn do_create_worktree(
+    git_repos: Vec<Entity<Repository>>,
+    non_git_paths: Vec<PathBuf>,
+    worktree_name: Option<String>,
+    branch_target: NewWorktreeBranchTarget,
+    previous_state: PreviousWorkspaceState,
+    workspace: WeakEntity<Workspace>,
+    window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
+    remote_connection_options: Option<RemoteConnectionOptions>,
+    cx: &mut AsyncWindowContext,
+) -> anyhow::Result<()> {
+    // List existing worktrees from all repos to detect name collisions
+    let worktree_receivers: Vec<_> = cx.update(|_, cx| {
+        git_repos
+            .iter()
+            .map(|repo| repo.update(cx, |repo, _cx| repo.worktrees()))
+            .collect()
+    })?;
+    let worktree_directory_setting = cx.update(|_, cx| {
+        ProjectSettings::get_global(cx)
+            .git
+            .worktree_directory
+            .clone()
+    })?;
+
+    let mut existing_worktree_names = Vec::new();
+    let mut existing_worktree_paths = HashSet::default();
+    for result in futures::future::join_all(worktree_receivers).await {
+        match result {
+            Ok(Ok(worktrees)) => {
+                for worktree in worktrees {
+                    if let Some(name) = worktree
+                        .path
+                        .parent()
+                        .and_then(|p| p.file_name())
+                        .and_then(|n| n.to_str())
+                    {
+                        existing_worktree_names.push(name.to_string());
+                    }
+                    existing_worktree_paths.insert(worktree.path.clone());
+                }
+            }
+            Ok(Err(err)) => {
+                Err::<(), _>(err).log_err();
+            }
+            Err(_) => {}
+        }
+    }
+
+    let mut rng = rand::rng();
+
+    let base_ref = resolve_worktree_branch_target(&branch_target);
+
+    let (creation_infos, path_remapping) = cx.update(|_, cx| {
+        start_worktree_creations(
+            &git_repos,
+            worktree_name,
+            &existing_worktree_names,
+            &existing_worktree_paths,
+            base_ref,
+            &worktree_directory_setting,
+            &mut rng,
+            cx,
+        )
+    })??;
+
+    let fs = cx.update(|_, cx| <dyn Fs>::global(cx))?;
+
+    let created_paths = await_and_rollback_on_failure(creation_infos, fs, cx).await?;
+
+    let mut all_paths = created_paths;
+    let has_non_git = !non_git_paths.is_empty();
+    all_paths.extend(non_git_paths.iter().cloned());
+
+    open_worktree_workspace(
+        all_paths,
+        path_remapping,
+        non_git_paths,
+        has_non_git,
+        previous_state,
+        workspace,
+        window_handle,
+        remote_connection_options,
+        WorktreeOperation::Create,
+        cx,
+    )
+    .await
+}
+
+async fn do_switch_worktree(
+    worktree_path: PathBuf,
+    git_repo_work_dirs: Vec<PathBuf>,
+    non_git_paths: Vec<PathBuf>,
+    previous_state: PreviousWorkspaceState,
+    workspace: WeakEntity<Workspace>,
+    window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
+    remote_connection_options: Option<RemoteConnectionOptions>,
+    cx: &mut AsyncWindowContext,
+) -> anyhow::Result<()> {
+    let path_remapping: Vec<(PathBuf, PathBuf)> = git_repo_work_dirs
+        .iter()
+        .map(|work_dir| (work_dir.clone(), worktree_path.clone()))
+        .collect();
+
+    let mut all_paths = vec![worktree_path];
+    let has_non_git = !non_git_paths.is_empty();
+    all_paths.extend(non_git_paths.iter().cloned());
+
+    open_worktree_workspace(
+        all_paths,
+        path_remapping,
+        non_git_paths,
+        has_non_git,
+        previous_state,
+        workspace,
+        window_handle,
+        remote_connection_options,
+        WorktreeOperation::Switch,
+        cx,
+    )
+    .await
+}
+
+/// Core workspace opening logic shared by both create and switch flows.
+async fn open_worktree_workspace(
+    all_paths: Vec<PathBuf>,
+    path_remapping: Vec<(PathBuf, PathBuf)>,
+    non_git_paths: Vec<PathBuf>,
+    has_non_git: bool,
+    previous_state: PreviousWorkspaceState,
+    workspace: WeakEntity<Workspace>,
+    window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
+    remote_connection_options: Option<RemoteConnectionOptions>,
+    operation: WorktreeOperation,
+    cx: &mut AsyncWindowContext,
+) -> anyhow::Result<()> {
+    let window_handle = window_handle
+        .ok_or_else(|| anyhow!("No window handle available for workspace creation"))?;
+
+    let focused_dock = previous_state.focused_dock;
+
+    let is_creating_new_worktree = matches!(operation, WorktreeOperation::Create);
+
+    let source_for_transfer = if is_creating_new_worktree {
+        Some(workspace.clone())
+    } else {
+        None
+    };
+
+    let (workspace_task, modal_workspace) =
+        window_handle.update(cx, |multi_workspace, window, cx| {
+            let path_list = util::path_list::PathList::new(&all_paths);
+            let active_workspace = multi_workspace.workspace().clone();
+            let modal_workspace = active_workspace.clone();
+
+            let init: Option<
+                Box<
+                    dyn FnOnce(&mut Workspace, &mut gpui::Window, &mut gpui::Context<Workspace>)
+                        + Send,
+                >,
+            > = if is_creating_new_worktree {
+                let dock_structure = previous_state.dock_structure;
+                Some(Box::new(
+                    move |workspace: &mut Workspace,
+                          window: &mut gpui::Window,
+                          cx: &mut gpui::Context<Workspace>| {
+                        workspace.set_dock_structure(dock_structure, window, cx);
+                    },
+                ))
+            } else {
+                None
+            };
+
+            let task = multi_workspace.find_or_create_workspace_with_source_workspace(
+                path_list,
+                remote_connection_options,
+                None,
+                move |connection_options, window, cx| {
+                    remote_connection::connect_with_modal(
+                        &active_workspace,
+                        connection_options,
+                        window,
+                        cx,
+                    )
+                },
+                &[],
+                init,
+                OpenMode::Add,
+                source_for_transfer.clone(),
+                window,
+                cx,
+            );
+            (task, modal_workspace)
+        })?;
+
+    let result = workspace_task.await;
+    remote_connection::dismiss_connection_modal(&modal_workspace, cx);
+    let new_workspace = result?;
+
+    let panels_task = new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task());
+
+    if let Some(task) = panels_task {
+        task.await.log_err();
+    }
+
+    new_workspace
+        .update(cx, |workspace, cx| {
+            workspace.project().read(cx).wait_for_initial_scan(cx)
+        })
+        .await;
+
+    new_workspace
+        .update(cx, |workspace, cx| {
+            let repos = workspace
+                .project()
+                .read(cx)
+                .repositories(cx)
+                .values()
+                .cloned()
+                .collect::<Vec<_>>();
+
+            let tasks = repos
+                .into_iter()
+                .map(|repo| repo.update(cx, |repo, _| repo.barrier()));
+            futures::future::join_all(tasks)
+        })
+        .await;
+
+    maybe_propagate_worktree_trust(&workspace, &new_workspace, &all_paths, cx);
+
+    if is_creating_new_worktree {
+        window_handle.update(cx, |_multi_workspace, window, cx| {
+            new_workspace.update(cx, |workspace, cx| {
+                if has_non_git {
+                    struct WorktreeCreationToast;
+                    let toast_id =
+                        workspace::notifications::NotificationId::unique::<WorktreeCreationToast>();
+                    workspace.show_toast(
+                        workspace::Toast::new(
+                            toast_id,
+                            "Some project folders are not git repositories. \
+                             They were included as-is without creating a worktree.",
+                        ),
+                        cx,
+                    );
+                }
+
+                // Remap every previously-open file path into the new worktree.
+                let remap_path = |original_path: PathBuf| -> Option<PathBuf> {
+                    let best_match = path_remapping
+                        .iter()
+                        .filter_map(|(old_root, new_root)| {
+                            original_path.strip_prefix(old_root).ok().map(|relative| {
+                                (old_root.components().count(), new_root.join(relative))
+                            })
+                        })
+                        .max_by_key(|(depth, _)| *depth);
+
+                    if let Some((_, remapped_path)) = best_match {
+                        return Some(remapped_path);
+                    }
+
+                    for non_git in &non_git_paths {
+                        if original_path.starts_with(non_git) {
+                            return Some(original_path);
+                        }
+                    }
+                    None
+                };
+
+                let remapped_active_path =
+                    previous_state.active_file_path.and_then(|p| remap_path(p));
+
+                let mut paths_to_open: Vec<PathBuf> = Vec::new();
+                let mut seen = HashSet::default();
+                for path in previous_state.open_file_paths {
+                    if let Some(remapped) = remap_path(path) {
+                        if remapped_active_path.as_ref() != Some(&remapped)
+                            && seen.insert(remapped.clone())
+                        {
+                            paths_to_open.push(remapped);
+                        }
+                    }
+                }
+
+                if let Some(active) = &remapped_active_path {
+                    if seen.insert(active.clone()) {
+                        paths_to_open.push(active.clone());
+                    }
+                }
+
+                if !paths_to_open.is_empty() {
+                    let should_focus_center = focused_dock.is_none();
+                    let open_task = workspace.open_paths(
+                        paths_to_open,
+                        workspace::OpenOptions {
+                            focus: Some(false),
+                            ..Default::default()
+                        },
+                        None,
+                        window,
+                        cx,
+                    );
+                    cx.spawn_in(window, async move |workspace, cx| {
+                        for item in open_task.await.into_iter().flatten() {
+                            item.log_err();
+                        }
+                        if should_focus_center {
+                            workspace.update_in(cx, |workspace, window, cx| {
+                                workspace.focus_center_pane(window, cx);
+                            })?;
+                        }
+                        anyhow::Ok(())
+                    })
+                    .detach_and_log_err(cx);
+                }
+            });
+        })?;
+    }
+
+    // Clear the creation status on the SOURCE workspace so its title bar
+    // stops showing the loading indicator immediately.
+    workspace
+        .update(cx, |ws, cx| {
+            ws.set_active_worktree_creation(None, false, cx);
+        })
+        .ok();
+
+    window_handle.update(cx, |multi_workspace, window, cx| {
+        multi_workspace.activate(new_workspace.clone(), source_for_transfer, window, cx);
+
+        new_workspace.update(cx, |workspace, cx| {
+            workspace.run_create_worktree_tasks(window, cx);
+        });
+    })?;
+
+    if is_creating_new_worktree {
+        if let Some(dock_position) = focused_dock {
+            window_handle.update(cx, |_multi_workspace, window, cx| {
+                new_workspace.update(cx, |workspace, cx| {
+                    let dock = workspace.dock_at_position(dock_position);
+                    if let Some(panel) = dock.read(cx).active_panel() {
+                        panel.panel_focus_handle(cx).focus(window, cx);
+                    }
+                });
+            })?;
+        }
+    }
+
+    anyhow::Ok(())
+}

crates/migrator/src/migrations.rs πŸ”—

@@ -340,3 +340,9 @@ pub(crate) mod m_2026_04_15 {
 
     pub(crate) use settings::remove_settings_from_http_context_servers;
 }
+
+pub(crate) mod m_2026_04_17 {
+    mod settings;
+
+    pub(crate) use settings::promote_show_branch_icon_true_to_show_branch_status_icon;
+}

crates/migrator/src/migrations/m_2026_04_17/settings.rs πŸ”—

@@ -0,0 +1,47 @@
+use anyhow::Result;
+use serde_json::Value;
+
+use crate::migrations::migrate_settings;
+
+const SETTINGS_KEY: &str = "settings";
+const TITLE_BAR_KEY: &str = "title_bar";
+const OLD_KEY: &str = "show_branch_icon";
+const NEW_KEY: &str = "show_branch_status_icon";
+
+pub fn promote_show_branch_icon_true_to_show_branch_status_icon(value: &mut Value) -> Result<()> {
+    migrate_settings(value, &mut migrate_one)
+}
+
+fn migrate_one(object: &mut serde_json::Map<String, Value>) -> Result<()> {
+    migrate_title_bar_value(object);
+
+    if let Some(settings) = object
+        .get_mut(SETTINGS_KEY)
+        .and_then(|value| value.as_object_mut())
+    {
+        migrate_title_bar_value(settings);
+    }
+
+    Ok(())
+}
+
+fn migrate_title_bar_value(object: &mut serde_json::Map<String, Value>) {
+    let Some(title_bar) = object
+        .get_mut(TITLE_BAR_KEY)
+        .and_then(|value| value.as_object_mut())
+    else {
+        return;
+    };
+
+    let Some(old_value) = title_bar.remove(OLD_KEY) else {
+        return;
+    };
+
+    if title_bar.contains_key(NEW_KEY) {
+        return;
+    }
+
+    if old_value == Value::Bool(true) {
+        title_bar.insert(NEW_KEY.to_string(), Value::Bool(true));
+    }
+}

crates/migrator/src/migrator.rs πŸ”—

@@ -251,6 +251,9 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
         MigrationType::Json(migrations::m_2026_04_01::restructure_profiles_with_settings_key),
         MigrationType::Json(migrations::m_2026_04_10::rename_web_search_to_search_web),
         MigrationType::Json(migrations::m_2026_04_15::remove_settings_from_http_context_servers),
+        MigrationType::Json(
+            migrations::m_2026_04_17::promote_show_branch_icon_true_to_show_branch_status_icon,
+        ),
     ];
     run_migrations(text, migrations)
 }
@@ -5041,4 +5044,252 @@ mod tests {
             ),
         );
     }
+
+    #[test]
+    fn test_promote_show_branch_icon_true_to_show_branch_status_icon_at_root() {
+        assert_migrate_settings(
+            &r#"
+            {
+                "title_bar": {
+                    "show_branch_icon": true,
+                    "show_branch_name": true
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "title_bar": {
+                        "show_branch_status_icon": true,
+                        "show_branch_name": true
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_drop_show_branch_icon_false_without_setting_status_icon() {
+        assert_migrate_settings(
+            &r#"
+            {
+                "title_bar": {
+                    "show_branch_icon": false,
+                    "show_branch_name": true
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "title_bar": {
+                        "show_branch_name": true
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_promote_show_branch_icon_true_to_show_branch_status_icon_in_platform_override() {
+        assert_migrate_settings(
+            &r#"
+            {
+                "macos": {
+                    "title_bar": {
+                        "show_branch_icon": true,
+                        "show_branch_name": true
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "macos": {
+                        "title_bar": {
+                            "show_branch_status_icon": true,
+                            "show_branch_name": true
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_promote_show_branch_icon_true_to_show_branch_status_icon_in_release_override() {
+        assert_migrate_settings(
+            &r#"
+            {
+                "preview": {
+                    "title_bar": {
+                        "show_branch_icon": true,
+                        "show_branch_name": true
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "preview": {
+                        "title_bar": {
+                            "show_branch_status_icon": true,
+                            "show_branch_name": true
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_promote_show_branch_icon_true_to_show_branch_status_icon_in_profiles() {
+        assert_migrate_settings(
+            &r#"
+            {
+                "profiles": {
+                    "work": {
+                        "title_bar": {
+                            "show_branch_icon": true,
+                            "show_branch_name": true
+                        }
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "profiles": {
+                        "work": {
+                            "settings": {
+                                "title_bar": {
+                                    "show_branch_status_icon": true,
+                                    "show_branch_name": true
+                                }
+                            }
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_promote_show_branch_icon_true_to_show_branch_status_icon_across_all_scopes() {
+        assert_migrate_settings(
+            &r#"
+            {
+                "title_bar": {
+                    "show_branch_icon": true,
+                    "show_branch_name": true
+                },
+                "macos": {
+                    "title_bar": {
+                        "show_branch_icon": true,
+                        "show_branch_name": true
+                    }
+                },
+                "preview": {
+                    "title_bar": {
+                        "show_branch_icon": true,
+                        "show_branch_name": true
+                    }
+                },
+                "profiles": {
+                    "work": {
+                        "title_bar": {
+                            "show_branch_icon": true,
+                            "show_branch_name": true
+                        }
+                    }
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "title_bar": {
+                        "show_branch_status_icon": true,
+                        "show_branch_name": true
+                    },
+                    "macos": {
+                        "title_bar": {
+                            "show_branch_status_icon": true,
+                            "show_branch_name": true
+                        }
+                    },
+                    "preview": {
+                        "title_bar": {
+                            "show_branch_status_icon": true,
+                            "show_branch_name": true
+                        }
+                    },
+                    "profiles": {
+                        "work": {
+                            "settings": {
+                                "title_bar": {
+                                    "show_branch_status_icon": true,
+                                    "show_branch_name": true
+                                }
+                            }
+                        }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_promote_show_branch_icon_true_to_show_branch_status_icon_no_change_when_already_migrated()
+     {
+        assert_migrate_settings(
+            &r#"
+            {
+                "title_bar": {
+                    "show_branch_status_icon": true,
+                    "show_branch_name": true
+                }
+            }
+            "#
+            .unindent(),
+            None,
+        );
+
+        // No title_bar key β€” should be unchanged
+        assert_migrate_settings(&r#"{ "theme": "One Dark" }"#.unindent(), None);
+
+        // title_bar without show_branch_icon β€” should be unchanged
+        assert_migrate_settings(
+            &r#"
+            {
+                "title_bar": {
+                    "show_branch_name": true
+                }
+            }
+            "#
+            .unindent(),
+            None,
+        );
+    }
 }

crates/recent_projects/src/remote_connections.rs πŸ”—

@@ -160,7 +160,7 @@ pub async fn open_remote_project(
             let open_results = existing_window
                 .update(cx, |multi_workspace, window, cx| {
                     window.activate_window();
-                    multi_workspace.activate(existing_workspace.clone(), window, cx);
+                    multi_workspace.activate(existing_workspace.clone(), None, window, cx);
                     existing_workspace.update(cx, |workspace, cx| {
                         workspace.open_paths(
                             resolved_paths,

crates/recent_projects/src/remote_servers.rs πŸ”—

@@ -505,7 +505,7 @@ impl ProjectPicker {
                     }?;
 
                     let items = open_remote_project_with_existing_connection(
-                        connection, project, paths, app_state, window, None, cx,
+                        connection, project, paths, app_state, window, None, None, cx,
                     )
                     .await
                     .log_err();

crates/settings_content/src/title_bar.rs πŸ”—

@@ -81,10 +81,12 @@ impl From<String> for WindowButtonLayoutContent {
 #[with_fallible_options]
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct TitleBarSettingsContent {
-    /// Whether to show the branch icon beside branch switcher in the title bar.
+    /// Whether to show git status indicators on the branch icon in the title bar.
+    /// When enabled, the branch icon changes to reflect the current repository
+    /// status (e.g. modified, added, deleted, or conflict).
     ///
     /// Default: false
-    pub show_branch_icon: Option<bool>,
+    pub show_branch_status_icon: Option<bool>,
     /// Whether to show onboarding banners in the title bar.
     ///
     /// Default: true

crates/settings_ui/src/page_data.rs πŸ”—

@@ -3631,22 +3631,22 @@ fn window_and_layout_page() -> SettingsPage {
         [
             SettingsPageItem::SectionHeader("Title Bar"),
             SettingsPageItem::SettingItem(SettingItem {
-                title: "Show Branch Icon",
-                description: "Show the branch icon beside branch switcher in the titlebar.",
+                title: "Show Branch Status Icon",
+                description: "Show git status indicators on the branch icon in the titlebar.",
                 field: Box::new(SettingField {
-                    json_path: Some("title_bar.show_branch_icon"),
+                    json_path: Some("title_bar.show_branch_status_icon"),
                     pick: |settings_content| {
                         settings_content
                             .title_bar
                             .as_ref()?
-                            .show_branch_icon
+                            .show_branch_status_icon
                             .as_ref()
                     },
                     write: |settings_content, value| {
                         settings_content
                             .title_bar
                             .get_or_insert_default()
-                            .show_branch_icon = value;
+                            .show_branch_status_icon = value;
                     },
                 }),
                 metadata: None,

crates/sidebar/src/sidebar.rs πŸ”—

@@ -494,7 +494,7 @@ impl Sidebar {
             &multi_workspace,
             window,
             |this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event {
-                MultiWorkspaceEvent::ActiveWorkspaceChanged => {
+                MultiWorkspaceEvent::ActiveWorkspaceChanged { .. } => {
                     this.sync_active_entry_from_active_workspace(cx);
                     this.replace_archived_panel_thread(window, cx);
                     this.update_entries(cx);
@@ -1989,6 +1989,7 @@ impl Sidebar {
                                             .update(cx, |multi_workspace, cx| {
                                                 multi_workspace.activate(
                                                     activate_workspace.clone(),
+                                                    None,
                                                     window,
                                                     cx,
                                                 );
@@ -2475,7 +2476,7 @@ impl Sidebar {
         }
 
         multi_workspace.update(cx, |multi_workspace, cx| {
-            multi_workspace.activate(workspace.clone(), window, cx);
+            multi_workspace.activate(workspace.clone(), None, window, cx);
             if retain {
                 multi_workspace.retain_active_workspace(cx);
             }
@@ -2515,7 +2516,7 @@ impl Sidebar {
         let activated = target_window
             .update(cx, |multi_workspace, window, cx| {
                 window.activate_window();
-                multi_workspace.activate(workspace.clone(), window, cx);
+                multi_workspace.activate(workspace.clone(), None, window, cx);
                 Self::load_agent_thread_in_workspace(&workspace, &metadata, true, window, cx);
             })
             .log_err()
@@ -3537,7 +3538,7 @@ impl Sidebar {
     ) {
         if let Some(multi_workspace) = self.multi_workspace.upgrade() {
             multi_workspace.update(cx, |mw, cx| {
-                mw.activate(workspace.clone(), window, cx);
+                mw.activate(workspace.clone(), None, window, cx);
             });
         }
     }
@@ -3729,7 +3730,7 @@ impl Sidebar {
                 } => {
                     if let Some(mw) = weak_multi_workspace.upgrade() {
                         mw.update(cx, |mw, cx| {
-                            mw.activate(workspace.clone(), window, cx);
+                            mw.activate(workspace.clone(), None, window, cx);
                         });
                     }
                     this.active_entry = Some(ActiveEntry {
@@ -3748,7 +3749,7 @@ impl Sidebar {
                 } => {
                     if let Some(mw) = weak_multi_workspace.upgrade() {
                         mw.update(cx, |mw, cx| {
-                            mw.activate(workspace.clone(), window, cx);
+                            mw.activate(workspace.clone(), None, window, cx);
                             mw.retain_active_workspace(cx);
                         });
                     }
@@ -3766,7 +3767,7 @@ impl Sidebar {
                     if let Some(mw) = weak_multi_workspace.upgrade() {
                         if let Some(original_ws) = &original_workspace {
                             mw.update(cx, |mw, cx| {
-                                mw.activate(original_ws.clone(), window, cx);
+                                mw.activate(original_ws.clone(), None, window, cx);
                             });
                         }
                     }
@@ -3823,7 +3824,7 @@ impl Sidebar {
         if let Some((metadata, workspace)) = initial_preview {
             if let Some(mw) = self.multi_workspace.upgrade() {
                 mw.update(cx, |mw, cx| {
-                    mw.activate(workspace.clone(), window, cx);
+                    mw.activate(workspace.clone(), None, window, cx);
                 });
             }
             self.active_entry = Some(ActiveEntry {
@@ -4070,7 +4071,7 @@ impl Sidebar {
         };
 
         multi_workspace.update(cx, |multi_workspace, cx| {
-            multi_workspace.activate(workspace.clone(), window, cx);
+            multi_workspace.activate(workspace.clone(), None, window, cx);
         });
 
         let draft_id = workspace.update(cx, |workspace, cx| {
@@ -4197,7 +4198,7 @@ impl Sidebar {
                 .workspace_for_paths(key.path_list(), key.host().as_ref(), cx)
         }) {
             multi_workspace.update(cx, |multi_workspace, cx| {
-                multi_workspace.activate(workspace, window, cx);
+                multi_workspace.activate(workspace, None, window, cx);
                 multi_workspace.retain_active_workspace(cx);
             });
         } else {

crates/sidebar/src/sidebar_tests.rs πŸ”—

@@ -2098,7 +2098,7 @@ async fn test_confirm_on_historical_thread_activates_workspace(cx: &mut TestAppC
     // Switch to workspace 1 so we can verify the confirm switches back.
     multi_workspace.update_in(cx, |mw, window, cx| {
         let workspace = mw.workspaces().nth(1).unwrap().clone();
-        mw.activate(workspace, window, cx);
+        mw.activate(workspace, None, window, cx);
     });
     cx.run_until_parked();
     assert_eq!(
@@ -2597,7 +2597,7 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
 
     multi_workspace.update_in(cx, |mw, window, cx| {
         let workspace = mw.workspaces().next().unwrap().clone();
-        mw.activate(workspace, window, cx);
+        mw.activate(workspace, None, window, cx);
     });
     cx.run_until_parked();
 
@@ -2653,7 +2653,7 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
     multi_workspace.update_in(cx, |mw, window, cx| {
         let workspace = mw.workspaces().find(|w| *w == &workspace_b).cloned();
         if let Some(workspace) = workspace {
-            mw.activate(workspace, window, cx);
+            mw.activate(workspace, None, window, cx);
         }
     });
     cx.run_until_parked();
@@ -2917,7 +2917,7 @@ async fn test_cmd_n_shows_new_thread_entry_in_absorbed_worktree(cx: &mut TestApp
     // Switch to the worktree workspace.
     multi_workspace.update_in(cx, |mw, window, cx| {
         let workspace = mw.workspaces().nth(1).unwrap().clone();
-        mw.activate(workspace, window, cx);
+        mw.activate(workspace, None, window, cx);
     });
 
     // Create a non-empty thread in the worktree workspace.
@@ -3521,7 +3521,7 @@ async fn test_absorbed_worktree_running_thread_shows_live_status(cx: &mut TestAp
     // Switch back to the main workspace before setting up the sidebar.
     multi_workspace.update_in(cx, |mw, window, cx| {
         let workspace = mw.workspaces().next().unwrap().clone();
-        mw.activate(workspace, window, cx);
+        mw.activate(workspace, None, window, cx);
     });
 
     // Start a thread in the worktree workspace's panel and keep it
@@ -3614,7 +3614,7 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp
 
     multi_workspace.update_in(cx, |mw, window, cx| {
         let workspace = mw.workspaces().next().unwrap().clone();
-        mw.activate(workspace, window, cx);
+        mw.activate(workspace, None, window, cx);
     });
 
     let connection = StubAgentConnection::new();
@@ -3937,7 +3937,7 @@ async fn test_clicking_absorbed_worktree_thread_activates_worktree_workspace(
     // Activate the main workspace before setting up the sidebar.
     let main_workspace = multi_workspace.update_in(cx, |mw, window, cx| {
         let workspace = mw.workspaces().next().unwrap().clone();
-        mw.activate(workspace.clone(), window, cx);
+        mw.activate(workspace.clone(), None, window, cx);
         workspace
     });
 
@@ -4018,7 +4018,7 @@ async fn test_activate_archived_thread_with_saved_paths_activates_matching_works
     // Ensure workspace A is active.
     multi_workspace.update_in(cx, |mw, window, cx| {
         let workspace = mw.workspaces().next().unwrap().clone();
-        mw.activate(workspace, window, cx);
+        mw.activate(workspace, None, window, cx);
     });
     cx.run_until_parked();
     assert_eq!(
@@ -4088,7 +4088,7 @@ async fn test_activate_archived_thread_cwd_fallback_with_matching_workspace(
     // Start with workspace A active.
     multi_workspace.update_in(cx, |mw, window, cx| {
         let workspace = mw.workspaces().next().unwrap().clone();
-        mw.activate(workspace, window, cx);
+        mw.activate(workspace, None, window, cx);
     });
     cx.run_until_parked();
     assert_eq!(
@@ -4155,7 +4155,7 @@ async fn test_activate_archived_thread_no_paths_no_cwd_uses_active_workspace(
     // Activate workspace B (index 1) to make it the active one.
     multi_workspace.update_in(cx, |mw, window, cx| {
         let workspace = mw.workspaces().nth(1).unwrap().clone();
-        mw.activate(workspace, window, cx);
+        mw.activate(workspace, None, window, cx);
     });
     cx.run_until_parked();
     assert_eq!(
@@ -4557,7 +4557,7 @@ async fn test_archive_thread_uses_next_threads_own_workspace(cx: &mut TestAppCon
     // Activate main workspace so the sidebar tracks the main panel.
     multi_workspace.update_in(cx, |mw, window, cx| {
         let workspace = mw.workspaces().next().unwrap().clone();
-        mw.activate(workspace, window, cx);
+        mw.activate(workspace, None, window, cx);
     });
 
     let main_workspace =
@@ -6461,7 +6461,7 @@ async fn test_unarchive_into_inactive_existing_workspace_does_not_leave_active_d
     cx.run_until_parked();
 
     multi_workspace.update_in(cx, |mw, window, cx| {
-        mw.activate(workspace_a.clone(), window, cx);
+        mw.activate(workspace_a.clone(), None, window, cx);
     });
     cx.run_until_parked();
 
@@ -6853,7 +6853,7 @@ async fn test_switch_to_workspace_with_archived_thread_shows_no_active_entry(
     let workspace_a =
         multi_workspace.read_with(cx, |mw, _| mw.workspaces().next().unwrap().clone());
     multi_workspace.update_in(cx, |mw, window, cx| {
-        mw.activate(workspace_a.clone(), window, cx);
+        mw.activate(workspace_a.clone(), None, window, cx);
     });
     cx.run_until_parked();
 
@@ -7014,7 +7014,7 @@ async fn test_archive_last_thread_on_linked_worktree_does_not_create_new_thread_
 
     // Activate the linked worktree workspace so the sidebar tracks it.
     multi_workspace.update_in(cx, |mw, window, cx| {
-        mw.activate(worktree_workspace.clone(), window, cx);
+        mw.activate(worktree_workspace.clone(), None, window, cx);
     });
 
     // Open a thread in the linked worktree panel and send a message
@@ -7185,7 +7185,7 @@ async fn test_archive_last_thread_on_linked_worktree_with_no_siblings_leaves_gro
 
     // Activate the linked worktree workspace.
     multi_workspace.update_in(cx, |mw, window, cx| {
-        mw.activate(worktree_workspace.clone(), window, cx);
+        mw.activate(worktree_workspace.clone(), None, window, cx);
     });
 
     // Open a thread on the linked worktree β€” this is the ONLY thread.
@@ -7486,7 +7486,7 @@ async fn test_archive_thread_on_linked_worktree_selects_sibling_thread(cx: &mut
 
     // Activate the linked worktree workspace.
     multi_workspace.update_in(cx, |mw, window, cx| {
-        mw.activate(worktree_workspace.clone(), window, cx);
+        mw.activate(worktree_workspace.clone(), None, window, cx);
     });
 
     // Open a thread on the linked worktree.
@@ -7641,7 +7641,7 @@ async fn test_linked_worktree_workspace_reachable_and_dismissable(cx: &mut TestA
     // Switch back to the main workspace.
     multi_workspace.update_in(cx, |mw, window, cx| {
         let main_ws = mw.workspaces().next().unwrap().clone();
-        mw.activate(main_ws, window, cx);
+        mw.activate(main_ws, None, window, cx);
     });
     cx.run_until_parked();
 
@@ -7850,7 +7850,9 @@ async fn test_transient_workspace_retained(cx: &mut TestAppContext) {
     );
 
     // Switch to A β€” B survives. (Switching from one internal workspace, to another)
-    multi_workspace.update_in(cx, |mw, window, cx| mw.activate(workspace_a, window, cx));
+    multi_workspace.update_in(cx, |mw, window, cx| {
+        mw.activate(workspace_a, None, window, cx)
+    });
     cx.run_until_parked();
     assert_eq!(
         multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
@@ -8318,7 +8320,7 @@ async fn test_project_header_click_restores_last_viewed(cx: &mut TestAppContext)
             .clone()
     });
     multi_workspace.update_in(cx, |mw, window, cx| {
-        mw.activate(workspace_a.clone(), window, cx);
+        mw.activate(workspace_a.clone(), None, window, cx);
     });
     cx.run_until_parked();
 
@@ -8406,11 +8408,11 @@ async fn test_activating_workspace_with_draft_does_not_create_extras(cx: &mut Te
 
     // Switch away from project-b, then back.
     multi_workspace.update_in(cx, |mw, window, cx| {
-        mw.activate(workspace_a.clone(), window, cx);
+        mw.activate(workspace_a.clone(), None, window, cx);
     });
     cx.run_until_parked();
     multi_workspace.update_in(cx, |mw, window, cx| {
-        mw.activate(workspace_b.clone(), window, cx);
+        mw.activate(workspace_b.clone(), None, window, cx);
     });
     cx.run_until_parked();
 
@@ -9146,7 +9148,7 @@ mod property_test {
                         .unwrap_or_else(|| mw.workspace().clone())
                 });
                 multi_workspace.update_in(cx, |mw, window, cx| {
-                    mw.activate(workspace, window, cx);
+                    mw.activate(workspace, None, window, cx);
                 });
             }
             Operation::AddLinkedWorktree {
@@ -10921,7 +10923,7 @@ async fn test_cmd_click_project_header_returns_to_last_active_linked_worktree_wo
     // workspaces β€” what matters for this test is the explicit sequence of
     // activations below.)
     multi_workspace.update_in(cx, |mw, window, cx| {
-        mw.activate(worktree_workspace_a.clone(), window, cx);
+        mw.activate(worktree_workspace_a.clone(), None, window, cx);
     });
     cx.run_until_parked();
     assert_eq!(
@@ -10934,7 +10936,7 @@ async fn test_cmd_click_project_header_returns_to_last_active_linked_worktree_wo
     // workspace remains the linked-worktree one (group B getting activated
     // records *its own* last-active workspace, not group A's).
     multi_workspace.update_in(cx, |mw, window, cx| {
-        mw.activate(workspace_b.clone(), window, cx);
+        mw.activate(workspace_b.clone(), None, window, cx);
     });
     cx.run_until_parked();
     assert_eq!(

crates/title_bar/src/title_bar.rs πŸ”—

@@ -7,6 +7,7 @@ mod update_version;
 
 use crate::application_menu::{ApplicationMenu, show_menus};
 use crate::plan_chip::PlanChip;
+use git_ui::worktree_picker::WorktreePicker;
 pub use platform_title_bar::{
     self, DraggedWindowTab, MergeAllWindows, MoveTabToNewWindow, PlatformTitleBar,
     ShowNextWindowTab, ShowPreviousWindowTab,
@@ -389,6 +390,16 @@ impl TitleBar {
             }),
         );
         subscriptions.push(cx.observe(&user_store, |_a, _, cx| cx.notify()));
+        if let Some(workspace_entity) = workspace.weak_handle().upgrade() {
+            subscriptions.push(cx.subscribe(
+                &workspace_entity,
+                |_, _, event: &workspace::Event, cx| {
+                    if matches!(event, workspace::Event::WorktreeCreationChanged) {
+                        cx.notify();
+                    }
+                },
+            ));
+        }
         subscriptions.push(cx.observe_button_layout_changed(window, |_, _, cx| cx.notify()));
         if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
             subscriptions.push(cx.subscribe(&trusted_worktrees, |_, _, _, cx| {
@@ -816,12 +827,14 @@ impl TitleBar {
         repository: Entity<project::git_store::Repository>,
         linked_worktree_name: Option<SharedString>,
         cx: &mut Context<Self>,
-    ) -> Option<impl IntoElement> {
+    ) -> Option<AnyElement> {
         let workspace = self.workspace.upgrade()?;
 
-        let (branch_name, icon_info) = {
+        let (branch_name, icon_info, is_detached_head) = {
             let repo = repository.read(cx);
 
+            let is_detached_head = repo.branch.is_none();
+
             let branch_name = repo
                 .branch
                 .as_ref()
@@ -851,67 +864,131 @@ impl TitleBar {
                 (IconName::GitBranch, Color::Muted)
             };
 
-            (branch_name, icon_info)
+            (branch_name, icon_info, is_detached_head)
         };
 
         let branch_name = branch_name?;
         let settings = TitleBarSettings::get_global(cx);
         let effective_repository = Some(repository);
 
-        Some(
-            PopoverMenu::new("branch-menu")
+        let worktree_label: SharedString = linked_worktree_name.unwrap_or_else(|| "main".into());
+
+        let (creation_in_progress, is_switch) = self
+            .workspace
+            .upgrade()
+            .map(|ws| {
+                let creation = ws.read(cx).active_worktree_creation();
+                (creation.label.clone(), creation.is_switch)
+            })
+            .unwrap_or((None, false));
+        let is_creating = creation_in_progress.is_some();
+
+        let display_label: SharedString = if let Some(ref name) = creation_in_progress {
+            if is_switch {
+                format!("Loading {}…", name).into()
+            } else {
+                format!("Creating {}…", name).into()
+            }
+        } else {
+            worktree_label.clone()
+        };
+
+        let worktree_button = {
+            let project = self.project.clone();
+            let workspace_handle = workspace.downgrade();
+            PopoverMenu::new("worktree-picker-menu")
                 .menu(move |window, cx| {
-                    Some(git_ui::git_picker::popover(
-                        workspace.downgrade(),
-                        effective_repository.clone(),
-                        git_ui::git_picker::GitPickerTab::Branches,
-                        gpui::rems(34.),
-                        window,
-                        cx,
-                    ))
+                    // When opened from the title bar, focus is on the trigger
+                    // button (not a dock), so `focused_dock` is `None`. That's
+                    // fine β€” there's no prior dock focus to restore.
+                    Some(cx.new(|cx| {
+                        WorktreePicker::new(project.clone(), workspace_handle.clone(), window, cx)
+                    }))
                 })
                 .trigger_with_tooltip(
-                    ButtonLike::new("project_branch_trigger")
+                    Button::new("worktree_picker_trigger", display_label)
                         .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                        .child(
-                            h_flex()
-                                .gap_0p5()
-                                .when(settings.show_branch_icon, |this| {
-                                    let (icon, icon_color) = icon_info;
-                                    this.child(
-                                        Icon::new(icon).size(IconSize::XSmall).color(icon_color),
-                                    )
-                                })
-                                .when_some(linked_worktree_name.as_ref(), |this, worktree_name| {
-                                    this.child(
-                                        Label::new(worktree_name)
-                                            .size(LabelSize::Small)
-                                            .color(Color::Muted),
-                                    )
-                                    .child(
-                                        Label::new("/").size(LabelSize::Small).color(
-                                            Color::Custom(
-                                                cx.theme().colors().text_muted.opacity(0.4),
-                                            ),
-                                        ),
-                                    )
-                                })
-                                .child(
-                                    Label::new(branch_name)
-                                        .size(LabelSize::Small)
-                                        .color(Color::Muted),
-                                ),
+                        .label_size(LabelSize::Small)
+                        .color(Color::Muted)
+                        .loading(is_creating)
+                        .start_icon(
+                            Icon::new(IconName::GitWorktree)
+                                .size(IconSize::XSmall)
+                                .color(Color::Muted),
                         ),
                     move |_window, cx| {
                         Tooltip::with_meta(
-                            "Git Switcher",
-                            Some(&zed_actions::git::Branch),
-                            "Worktrees, Branches, and Stashes",
+                            "Worktree",
+                            Some(&zed_actions::git::Worktree),
+                            format!("Currently In Use: {}", worktree_label),
                             cx,
                         )
                     },
                 )
-                .anchor(gpui::Corner::TopLeft),
+                .anchor(gpui::Corner::TopLeft)
+        };
+
+        let branch_tooltip_label = branch_name.clone();
+        let (branch_icon, branch_icon_color) = if settings.show_branch_status_icon {
+            icon_info
+        } else {
+            (IconName::GitBranch, Color::Muted)
+        };
+
+        let trigger = if is_detached_head {
+            Button::new("project_branch_trigger", "Create Branch")
+                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                .label_size(LabelSize::Small)
+                .start_icon(
+                    Icon::new(IconName::GitBranchPlus)
+                        .size(IconSize::XSmall)
+                        .color(Color::Muted),
+                )
+        } else {
+            Button::new("project_branch_trigger", branch_name)
+                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                .label_size(LabelSize::Small)
+                .color(Color::Muted)
+                .start_icon(
+                    Icon::new(branch_icon)
+                        .size(IconSize::XSmall)
+                        .color(branch_icon_color),
+                )
+        };
+
+        let git_picker_button = PopoverMenu::new("branch-menu")
+            .menu(move |window, cx| {
+                Some(git_ui::git_picker::popover(
+                    workspace.downgrade(),
+                    effective_repository.clone(),
+                    git_ui::git_picker::GitPickerTab::Branches,
+                    gpui::rems(34.),
+                    window,
+                    cx,
+                ))
+            })
+            .trigger_with_tooltip(trigger, move |_window, cx| {
+                let meta = if is_detached_head {
+                    format!("Detached HEAD: {}", branch_tooltip_label)
+                } else {
+                    format!("Currently Checked Out: {}", branch_tooltip_label)
+                };
+                Tooltip::with_meta("Branch & Stash", Some(&zed_actions::git::Branch), meta, cx)
+            })
+            .anchor(gpui::Corner::TopLeft);
+
+        Some(
+            h_flex()
+                .gap_px()
+                .child(worktree_button)
+                .child(
+                    Label::new("/")
+                        .size(LabelSize::Small)
+                        .color(Color::Muted)
+                        .alpha(0.25),
+                )
+                .child(git_picker_button)
+                .into_any_element(),
         )
     }
 

crates/title_bar/src/title_bar_settings.rs πŸ”—

@@ -3,7 +3,7 @@ use settings::{RegisterSetting, Settings, SettingsContent};
 
 #[derive(Copy, Clone, Debug, RegisterSetting)]
 pub struct TitleBarSettings {
-    pub show_branch_icon: bool,
+    pub show_branch_status_icon: bool,
     pub show_onboarding_banner: bool,
     pub show_user_picture: bool,
     pub show_branch_name: bool,
@@ -18,7 +18,7 @@ impl Settings for TitleBarSettings {
     fn from_settings(s: &SettingsContent) -> Self {
         let content = s.title_bar.clone().unwrap();
         TitleBarSettings {
-            show_branch_icon: content.show_branch_icon.unwrap(),
+            show_branch_status_icon: content.show_branch_status_icon.unwrap(),
             show_onboarding_banner: content.show_onboarding_banner.unwrap(),
             show_user_picture: content.show_user_picture.unwrap(),
             show_branch_name: content.show_branch_name.unwrap(),

crates/workspace/src/multi_workspace.rs πŸ”—

@@ -103,7 +103,9 @@ pub fn sidebar_side_context_menu(
 }
 
 pub enum MultiWorkspaceEvent {
-    ActiveWorkspaceChanged,
+    ActiveWorkspaceChanged {
+        source_workspace: Option<WeakEntity<Workspace>>,
+    },
     WorkspaceAdded(Entity<Workspace>),
     WorkspaceRemoved(EntityId),
     ProjectGroupsChanged,
@@ -578,7 +580,7 @@ impl MultiWorkspace {
 
         cx.subscribe_in(workspace, window, |this, workspace, event, window, cx| {
             if let WorkspaceEvent::Activate = event {
-                this.activate(workspace.clone(), window, cx);
+                this.activate(workspace.clone(), None, window, cx);
             }
         })
         .detach();
@@ -730,7 +732,7 @@ impl MultiWorkspace {
             self.retained_workspaces.push(workspace.clone());
         }
 
-        self.activate(workspace.clone(), window, cx);
+        self.activate(workspace.clone(), None, window, cx);
         cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
     }
 
@@ -1137,19 +1139,52 @@ impl MultiWorkspace {
         open_mode: OpenMode,
         window: &mut Window,
         cx: &mut Context<Self>,
+    ) -> Task<Result<Entity<Workspace>>> {
+        self.find_or_create_workspace_with_source_workspace(
+            paths,
+            host,
+            provisional_project_group_key,
+            connect_remote,
+            excluding,
+            init,
+            open_mode,
+            None,
+            window,
+            cx,
+        )
+    }
+
+    pub fn find_or_create_workspace_with_source_workspace(
+        &mut self,
+        paths: PathList,
+        host: Option<RemoteConnectionOptions>,
+        provisional_project_group_key: Option<ProjectGroupKey>,
+        connect_remote: impl FnOnce(
+            RemoteConnectionOptions,
+            &mut Window,
+            &mut Context<Self>,
+        ) -> Task<Result<Option<Entity<remote::RemoteClient>>>>
+        + 'static,
+        excluding: &[Entity<Workspace>],
+        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
+        open_mode: OpenMode,
+        source_workspace: Option<WeakEntity<Workspace>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
     ) -> Task<Result<Entity<Workspace>>> {
         if let Some(workspace) = self.workspace_for_paths(&paths, host.as_ref(), cx) {
-            self.activate(workspace.clone(), window, cx);
+            self.activate(workspace.clone(), source_workspace, window, cx);
             return Task::ready(Ok(workspace));
         }
 
         let Some(connection_options) = host else {
-            return self.find_or_create_local_workspace(
+            return self.find_or_create_local_workspace_with_source_workspace(
                 paths,
                 provisional_project_group_key,
                 excluding,
                 init,
                 open_mode,
+                source_workspace,
                 window,
                 cx,
             );
@@ -1215,6 +1250,7 @@ impl MultiWorkspace {
                 app_state,
                 window_handle,
                 provisional_project_group_key,
+                source_workspace,
                 cx,
             )
             .await?;
@@ -1243,10 +1279,33 @@ impl MultiWorkspace {
         open_mode: OpenMode,
         window: &mut Window,
         cx: &mut Context<Self>,
+    ) -> Task<Result<Entity<Workspace>>> {
+        self.find_or_create_local_workspace_with_source_workspace(
+            path_list,
+            project_group,
+            excluding,
+            init,
+            open_mode,
+            None,
+            window,
+            cx,
+        )
+    }
+
+    pub fn find_or_create_local_workspace_with_source_workspace(
+        &mut self,
+        path_list: PathList,
+        project_group: Option<ProjectGroupKey>,
+        excluding: &[Entity<Workspace>],
+        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
+        open_mode: OpenMode,
+        source_workspace: Option<WeakEntity<Workspace>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
     ) -> Task<Result<Entity<Workspace>>> {
         if let Some(workspace) = self.workspace_for_paths_excluding(&path_list, None, excluding, cx)
         {
-            self.activate(workspace.clone(), window, cx);
+            self.activate(workspace.clone(), source_workspace, window, cx);
             return Task::ready(Ok(workspace));
         }
 
@@ -1291,7 +1350,12 @@ impl MultiWorkspace {
                                 cx,
                             )
                             .inspect(|workspace| {
-                                multi_workspace.activate(workspace.clone(), window, cx);
+                                multi_workspace.activate(
+                                    workspace.clone(),
+                                    source_workspace.clone(),
+                                    window,
+                                    cx,
+                                );
                             })
                     })
                     .ok()
@@ -1354,6 +1418,7 @@ impl MultiWorkspace {
     pub fn activate(
         &mut self,
         workspace: Entity<Workspace>,
+        source_workspace: Option<WeakEntity<Workspace>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -1386,7 +1451,7 @@ impl MultiWorkspace {
             self.detach_workspace(&old_active_workspace, cx);
         }
 
-        cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
+        cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged { source_workspace });
         self.serialize(cx);
         self.focus_active_workspace(window, cx);
         cx.notify();
@@ -1671,7 +1736,7 @@ impl MultiWorkspace {
         cx: &mut Context<Self>,
     ) -> Entity<Workspace> {
         let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
-        self.activate(workspace.clone(), window, cx);
+        self.activate(workspace.clone(), None, window, cx);
         workspace
     }
 
@@ -1702,7 +1767,7 @@ impl MultiWorkspace {
             cx,
         );
         let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
-        self.activate(new_workspace.clone(), window, cx);
+        self.activate(new_workspace.clone(), None, window, cx);
 
         let weak_workspace = new_workspace.downgrade();
         let db = crate::persistence::WorkspaceDb::global(cx);
@@ -1827,12 +1892,12 @@ impl MultiWorkspace {
                         !workspaces.contains(&new_active),
                         "fallback workspace must not be one of the workspaces being removed"
                     );
-                    this.activate(new_active, window, cx);
+                    this.activate(new_active, None, window, cx);
                 })?;
             } else {
                 this.update_in(cx, |this, window, cx| {
                     if *this.workspace() != original_active {
-                        this.activate(original_active, window, cx);
+                        this.activate(original_active, None, window, cx);
                     }
                 })?;
             }

crates/workspace/src/multi_workspace_tests.rs πŸ”—

@@ -555,7 +555,7 @@ async fn test_close_workspace_prefers_already_loaded_neighboring_workspace(
     });
 
     multi_workspace.update_in(cx, |multi_workspace, window, cx| {
-        multi_workspace.activate(workspace_a.clone(), window, cx);
+        multi_workspace.activate(workspace_a.clone(), None, window, cx);
         multi_workspace.test_add_project_group(ProjectGroup {
             key: project_c_key.clone(),
             workspaces: Vec::new(),

crates/workspace/src/persistence.rs πŸ”—

@@ -2568,7 +2568,7 @@ mod tests {
         let workspace2 = multi_workspace.update_in(cx, |mw, window, cx| {
             let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
             workspace.update(cx, |ws, _cx| ws.set_random_database_id());
-            mw.activate(workspace.clone(), window, cx);
+            mw.activate(workspace.clone(), None, window, cx);
             workspace
         });
 
@@ -4947,7 +4947,7 @@ mod tests {
 
         // Activate workspace B so removing its group exercises the fallback.
         multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.activate(workspace_b.clone(), window, cx);
+            mw.activate(workspace_b.clone(), None, window, cx);
         });
         cx.run_until_parked();
 
@@ -4976,7 +4976,7 @@ mod tests {
         let workspace_a =
             multi_workspace.read_with(cx, |mw, _cx| mw.workspaces().next().unwrap().clone());
         multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.activate(workspace_a.clone(), window, cx);
+            mw.activate(workspace_a.clone(), None, window, cx);
         });
         cx.run_until_parked();
 
@@ -5052,7 +5052,7 @@ mod tests {
 
         // Activate workspace_a so removing it triggers the fallback path.
         multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.activate(workspace_a.clone(), window, cx);
+            mw.activate(workspace_a.clone(), None, window, cx);
         });
         cx.run_until_parked();
 

crates/workspace/src/workspace.rs πŸ”—

@@ -1109,6 +1109,23 @@ struct GlobalAppState(Arc<AppState>);
 
 impl Global for GlobalAppState {}
 
+/// Tracks worktree creation progress for the workspace.
+/// Read by the title bar to show a loading indicator on the worktree button.
+#[derive(Default)]
+pub struct ActiveWorktreeCreation {
+    pub label: Option<SharedString>,
+    pub is_switch: bool,
+}
+
+/// Captured workspace state used when switching between worktrees.
+/// Stores the layout and open files so they can be restored in the new workspace.
+pub struct PreviousWorkspaceState {
+    pub dock_structure: DockStructure,
+    pub open_file_paths: Vec<PathBuf>,
+    pub active_file_path: Option<PathBuf>,
+    pub focused_dock: Option<DockPosition>,
+}
+
 pub struct WorkspaceStore {
     workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity<Workspace>)>,
     client: Arc<Client>,
@@ -1269,6 +1286,7 @@ pub enum Event {
     ModalOpened,
     Activate,
     PanelAdded(AnyView),
+    WorktreeCreationChanged,
 }
 
 #[derive(Debug, Clone)]
@@ -1377,6 +1395,7 @@ pub struct Workspace {
     _panels_task: Option<Task<Result<()>>>,
     sidebar_focus_handle: Option<FocusHandle>,
     multi_workspace: Option<WeakEntity<MultiWorkspace>>,
+    active_worktree_creation: ActiveWorktreeCreation,
 }
 
 impl EventEmitter<Event> for Workspace {}
@@ -1805,6 +1824,7 @@ impl Workspace {
             removing: false,
             sidebar_focus_handle: None,
             multi_workspace,
+            active_worktree_creation: ActiveWorktreeCreation::default(),
             open_in_dev_container: false,
             _dev_container_task: None,
         }
@@ -1947,7 +1967,7 @@ impl Workspace {
                         });
                         match open_mode {
                             OpenMode::Activate => {
-                                multi_workspace.activate(workspace.clone(), window, cx);
+                                multi_workspace.activate(workspace.clone(), None, window, cx);
                             }
                             OpenMode::Add => {
                                 multi_workspace.add(workspace.clone(), &*window, cx);
@@ -2178,6 +2198,64 @@ impl Workspace {
         }
     }
 
+    /// Returns which dock currently has focus, or `None` if focus is in the
+    /// center pane or elsewhere. Does NOT fall back to any global state.
+    pub fn focused_dock_position(&self, window: &Window, cx: &App) -> Option<DockPosition> {
+        [
+            (DockPosition::Left, &self.left_dock),
+            (DockPosition::Right, &self.right_dock),
+            (DockPosition::Bottom, &self.bottom_dock),
+        ]
+        .into_iter()
+        .find(|(_, dock)| {
+            dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
+        })
+        .map(|(position, _)| position)
+    }
+
+    pub fn active_worktree_creation(&self) -> &ActiveWorktreeCreation {
+        &self.active_worktree_creation
+    }
+
+    pub fn set_active_worktree_creation(
+        &mut self,
+        label: Option<SharedString>,
+        is_switch: bool,
+        cx: &mut Context<Self>,
+    ) {
+        self.active_worktree_creation.label = label;
+        self.active_worktree_creation.is_switch = is_switch;
+        cx.emit(Event::WorktreeCreationChanged);
+        cx.notify();
+    }
+
+    /// Captures the current workspace state for restoring after a worktree switch.
+    /// This includes dock layout, open file paths, and the active file path.
+    pub fn capture_state_for_worktree_switch(
+        &self,
+        window: &Window,
+        fallback_focused_dock: Option<DockPosition>,
+        cx: &App,
+    ) -> PreviousWorkspaceState {
+        let dock_structure = self.capture_dock_state(window, cx);
+        let open_file_paths = self.open_item_abs_paths(cx);
+        let active_file_path = self
+            .active_item(cx)
+            .and_then(|item| item.project_path(cx))
+            .and_then(|pp| self.project().read(cx).absolute_path(&pp, cx));
+
+        let focused_dock = self
+            .focused_dock_position(window, cx)
+            .or(fallback_focused_dock);
+
+        PreviousWorkspaceState {
+            dock_structure,
+            open_file_paths,
+            active_file_path,
+            focused_dock,
+        }
+    }
+
     pub fn open_item_abs_paths(&self, cx: &App) -> Vec<PathBuf> {
         self.items(cx)
             .filter_map(|item| {
@@ -3449,6 +3527,11 @@ impl Workspace {
                         OpenOptions {
                             requesting_window,
                             open_mode,
+                            workspace_matching: if open_mode == OpenMode::NewWindow {
+                                WorkspaceMatching::None
+                            } else {
+                                WorkspaceMatching::default()
+                            },
                             ..Default::default()
                         },
                         cx,
@@ -9608,7 +9691,7 @@ pub fn open_paths(
             let open_task = existing
                 .update(cx, |multi_workspace, window, cx| {
                     window.activate_window();
-                    multi_workspace.activate(target_workspace.clone(), window, cx);
+                    multi_workspace.activate(target_workspace.clone(), None, window, cx);
                     target_workspace.update(cx, |workspace, cx| {
                         if open_in_dev_container {
                             workspace.set_open_in_dev_container(true);
@@ -9834,6 +9917,7 @@ pub fn open_remote_project_with_new_connection(
             app_state,
             window,
             None,
+            None,
             cx,
         )
         .await
@@ -9847,6 +9931,7 @@ pub fn open_remote_project_with_existing_connection(
     app_state: Arc<AppState>,
     window: WindowHandle<MultiWorkspace>,
     provisional_project_group_key: Option<ProjectGroupKey>,
+    source_workspace: Option<WeakEntity<Workspace>>,
     cx: &mut AsyncApp,
 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
     cx.spawn(async move |cx| {
@@ -9861,6 +9946,7 @@ pub fn open_remote_project_with_existing_connection(
             app_state,
             window,
             provisional_project_group_key,
+            source_workspace,
             cx,
         )
         .await
@@ -9875,6 +9961,7 @@ async fn open_remote_project_inner(
     app_state: Arc<AppState>,
     window: WindowHandle<MultiWorkspace>,
     provisional_project_group_key: Option<ProjectGroupKey>,
+    source_workspace: Option<WeakEntity<Workspace>>,
     cx: &mut AsyncApp,
 ) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
     let db = cx.update(|cx| WorkspaceDb::global(cx));
@@ -9945,7 +10032,7 @@ async fn open_remote_project_inner(
                 cx,
             );
         } else {
-            multi_workspace.activate(new_workspace.clone(), window, cx);
+            multi_workspace.activate(new_workspace.clone(), source_workspace, window, cx);
         }
         new_workspace
     })?;
@@ -10033,7 +10120,7 @@ pub fn join_in_room_project(
         {
             existing_window
                 .update(cx, |multi_workspace, window, cx| {
-                    multi_workspace.activate(target_workspace, window, cx);
+                    multi_workspace.activate(target_workspace, None, window, cx);
                 })
                 .ok();
             existing_window
@@ -10973,7 +11060,7 @@ mod tests {
         // Activate workspace A
         multi_workspace_handle
             .update(cx, |mw, window, cx| {
-                mw.activate(workspace_a.clone(), window, cx);
+                mw.activate(workspace_a.clone(), None, window, cx);
             })
             .unwrap();
 
@@ -11058,7 +11145,7 @@ mod tests {
         // Activate workspace A.
         multi_workspace_handle
             .update(cx, |mw, window, cx| {
-                mw.activate(workspace_a.clone(), window, cx);
+                mw.activate(workspace_a.clone(), None, window, cx);
             })
             .unwrap();
 
@@ -11110,7 +11197,7 @@ mod tests {
         let remove_task = multi_workspace_handle
             .update(cx, |mw, window, cx| {
                 // First switch back to A.
-                mw.activate(workspace_a.clone(), window, cx);
+                mw.activate(workspace_a.clone(), None, window, cx);
                 mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
             })
             .unwrap();
@@ -14836,7 +14923,7 @@ mod tests {
         multi_workspace_handle
             .update(cx, |mw, window, cx| {
                 let workspace = mw.workspaces().next().unwrap().clone();
-                mw.activate(workspace, window, cx);
+                mw.activate(workspace, None, window, cx);
             })
             .unwrap();
 
@@ -14882,7 +14969,7 @@ mod tests {
         multi_workspace_handle
             .update(cx, |mw, window, cx| {
                 let workspace = mw.workspaces().nth(1).unwrap().clone();
-                mw.activate(workspace, window, cx);
+                mw.activate(workspace, None, window, cx);
             })
             .unwrap();
         cx.run_until_parked();
@@ -14891,7 +14978,7 @@ mod tests {
         multi_workspace_handle
             .update(cx, |mw, window, cx| {
                 let workspace = mw.workspaces().next().unwrap().clone();
-                mw.activate(workspace, window, cx);
+                mw.activate(workspace, None, window, cx);
             })
             .unwrap();
         cx.run_until_parked();

crates/zed/src/visual_test_runner.rs πŸ”—

@@ -2605,7 +2605,7 @@ fn run_multi_workspace_sidebar_visual_tests(
                     });
                     cx.new(|cx| {
                         let mut multi_workspace = MultiWorkspace::new(workspace1, window, cx);
-                        multi_workspace.activate(workspace2, window, cx);
+                        multi_workspace.activate(workspace2, None, window, cx);
                         multi_workspace
                     })
                 },
@@ -2657,7 +2657,7 @@ fn run_multi_workspace_sidebar_visual_tests(
     multi_workspace_window
         .update(cx, |multi_workspace, window, cx| {
             let workspace = multi_workspace.workspaces().next().unwrap().clone();
-            multi_workspace.activate(workspace, window, cx);
+            multi_workspace.activate(workspace, None, window, cx);
         })
         .context("Failed to activate workspace 1")?;
 
@@ -3393,7 +3393,7 @@ fn open_sidebar_test_window(
                             let ws = cx.new(|cx| {
                                 Workspace::new(None, project, app_state.clone(), window, cx)
                             });
-                            mw.activate(ws, window, cx);
+                            mw.activate(ws, None, window, cx);
                         }
                         mw
                     })

crates/zed/src/zed.rs πŸ”—

@@ -423,6 +423,38 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut App) {
 
         let window_handle = window.window_handle();
         let multi_workspace_handle = cx.entity();
+        cx.subscribe_in(
+            &multi_workspace_handle,
+            window,
+            |this, _multi_workspace, event: &workspace::MultiWorkspaceEvent, window, cx| {
+                let workspace::MultiWorkspaceEvent::ActiveWorkspaceChanged { source_workspace } =
+                    event
+                else {
+                    return;
+                };
+
+                let active_workspace = this.workspace().clone();
+                let source_workspace = source_workspace.clone();
+                active_workspace.update(cx, |workspace, cx| {
+                    if let Some(ref source) = source_workspace {
+                        if let Some(panel) = workspace.panel::<agent_ui::AgentPanel>(cx) {
+                            panel.update(cx, |panel, cx| {
+                                panel.initialize_from_source_workspace_if_needed(
+                                    source.clone(),
+                                    window,
+                                    cx,
+                                );
+                            });
+                        }
+                    }
+
+                    ensure_agent_panel_for_workspace(workspace, source_workspace, window, cx)
+                        .detach_and_log_err(cx);
+                });
+            },
+        )
+        .detach();
+
         cx.defer(move |cx| {
             window_handle
                 .update(cx, |_, window, cx| {
@@ -735,24 +767,43 @@ fn setup_or_teardown_ai_panel<P: Panel>(
     }
 }
 
+fn ensure_agent_panel_for_workspace(
+    workspace: &mut Workspace,
+    source_workspace: Option<WeakEntity<Workspace>>,
+    window: &mut Window,
+    cx: &mut Context<Workspace>,
+) -> Task<anyhow::Result<()>> {
+    let task = setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
+        agent_ui::AgentPanel::load(workspace, cx)
+    });
+
+    cx.spawn_in(window, async move |workspace, cx| {
+        task.await?;
+        workspace.update_in(cx, |workspace, window, cx| {
+            if let Some(source_workspace) = source_workspace.clone()
+                && let Some(panel) = workspace.panel::<agent_ui::AgentPanel>(cx)
+            {
+                panel.update(cx, |panel, cx| {
+                    panel.initialize_from_source_workspace_if_needed(source_workspace, window, cx);
+                });
+            }
+        })
+    })
+}
+
 async fn initialize_agent_panel(
     workspace_handle: WeakEntity<Workspace>,
     mut cx: AsyncWindowContext,
 ) -> anyhow::Result<()> {
     workspace_handle
         .update_in(&mut cx, |workspace, window, cx| {
-            setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
-                agent_ui::AgentPanel::load(workspace, cx)
-            })
+            ensure_agent_panel_for_workspace(workspace, None, window, cx)
         })?
         .await?;
 
     workspace_handle.update_in(&mut cx, |workspace, window, cx| {
         cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
-            setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
-                agent_ui::AgentPanel::load(workspace, cx)
-            })
-            .detach_and_log_err(cx);
+            ensure_agent_panel_for_workspace(workspace, None, window, cx).detach_and_log_err(cx);
         })
         .detach();
 
@@ -1542,7 +1593,7 @@ fn quit(_: &Quit, cx: &mut App) {
             for workspace in workspaces {
                 if let Some(should_close) = window
                     .update(cx, |multi_workspace, window, cx| {
-                        multi_workspace.activate(workspace.clone(), window, cx);
+                        multi_workspace.activate(workspace.clone(), None, window, cx);
                         window.activate_window();
                         workspace.update(cx, |workspace, cx| {
                             workspace.prepare_to_close(CloseIntent::Quit, window, cx)
@@ -5113,6 +5164,7 @@ mod tests {
                 "vim",
                 "window",
                 "workspace",
+                "worktree_picker",
                 "zed",
                 "zed_actions",
                 "zed_predict_onboarding",
@@ -5657,10 +5709,10 @@ mod tests {
 
         window
             .update(cx, |multi_workspace, window, cx| {
-                multi_workspace.activate(workspace2.clone(), window, cx);
-                multi_workspace.activate(workspace3.clone(), window, cx);
+                multi_workspace.activate(workspace2.clone(), None, window, cx);
+                multi_workspace.activate(workspace3.clone(), None, window, cx);
                 // Switch back to workspace1 for test setup
-                multi_workspace.activate(workspace1.clone(), window, cx);
+                multi_workspace.activate(workspace1.clone(), None, window, cx);
                 assert_eq!(multi_workspace.workspace(), &workspace1);
             })
             .unwrap();
@@ -5844,8 +5896,8 @@ mod tests {
 
         window1
             .update(cx, |multi_workspace, window, cx| {
-                multi_workspace.activate(workspace1_2.clone(), window, cx);
-                multi_workspace.activate(workspace1_1.clone(), window, cx);
+                multi_workspace.activate(workspace1_2.clone(), None, window, cx);
+                multi_workspace.activate(workspace1_1.clone(), None, window, cx);
             })
             .unwrap();
 
@@ -6164,7 +6216,7 @@ mod tests {
         window_a
             .update(cx, |multi_workspace, window, cx| {
                 let workspace = multi_workspace.workspaces().next().unwrap().clone();
-                multi_workspace.activate(workspace, window, cx);
+                multi_workspace.activate(workspace, None, window, cx);
             })
             .unwrap();
 
@@ -6372,7 +6424,7 @@ mod tests {
                     })
                     .expect("workspace_a should exist")
                     .clone();
-                mw.activate(workspace_a, window, cx);
+                mw.activate(workspace_a, None, window, cx);
             })
             .unwrap();
         cx.run_until_parked();

crates/zed_actions/src/lib.rs πŸ”—

@@ -1,6 +1,7 @@
 use gpui::{Action, actions};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
 
 // If the zed binary doesn't use anything in this crate, it will be optimized away
 // and the actions won't initialize. So we just provide an empty initialization function
@@ -251,6 +252,49 @@ pub mod workspace {
     );
 }
 
+/// Describes which ref to base a new git worktree on. The worktree is
+/// always created in a detached HEAD state; users can opt into creating
+/// a branch afterwards from the worktree itself.
+#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case", tag = "kind")]
+pub enum NewWorktreeBranchTarget {
+    /// Create a detached worktree from the current HEAD.
+    #[default]
+    CurrentBranch,
+    /// Create a detached worktree at the tip of an existing branch.
+    ExistingBranch { name: String },
+}
+
+/// Creates a new git worktree and switches the workspace to it.
+/// Dispatched by the unified worktree picker when the user selects a "Create new worktree" entry.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Action)]
+#[action(namespace = git)]
+#[serde(deny_unknown_fields)]
+pub struct CreateWorktree {
+    /// When this is None, Zed will randomly generate a worktree name.
+    pub worktree_name: Option<String>,
+    pub branch_target: NewWorktreeBranchTarget,
+}
+
+/// Switches the workspace to an existing linked worktree.
+/// Dispatched by the unified worktree picker when the user selects an existing worktree.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Action)]
+#[action(namespace = git)]
+#[serde(deny_unknown_fields)]
+pub struct SwitchWorktree {
+    pub path: PathBuf,
+    pub display_name: String,
+}
+
+/// Opens an existing worktree in a new window.
+/// Dispatched by the worktree picker's "Open in New Window" button.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Action)]
+#[action(namespace = git)]
+#[serde(deny_unknown_fields)]
+pub struct OpenWorktreeInNewWindow {
+    pub path: PathBuf,
+}
+
 pub mod git {
     use gpui::actions;
 

docs/src/ai/llm-providers.md πŸ”—

@@ -762,7 +762,7 @@ You can also set a custom endpoint for Vercel AI Gateway in your settings file:
 [Vercel v0](https://v0.app/docs/api/model) is a model for generating full-stack apps, with framework-aware completions for stacks like Next.js and Vercel.
 It supports text and image inputs and provides fast streaming responses.
 
-The v0 models are [OpenAI-compatible models](/#openai-api-compatible), and Vercel appears as a dedicated provider in the panel's settings view.
+The v0 models are [OpenAI-compatible models](#openai-api-compatible), and Vercel appears as a dedicated provider in the panel's settings view.
 
 To start using it with Zed, ensure you have first created a [v0 API key](https://v0.dev/chat/settings/keys).
 Once you have it, paste it directly into the Vercel provider section in the panel's settings view.

docs/src/reference/all-settings.md πŸ”—

@@ -4659,7 +4659,7 @@ Run the {#action theme_selector::Toggle} action in the command palette to see a
 ```json [settings]
 {
   "title_bar": {
-    "show_branch_icon": false,
+    "show_branch_status_icon": false,
     "show_branch_name": true,
     "show_project_items": true,
     "show_onboarding_banner": true,
@@ -4674,7 +4674,7 @@ Run the {#action theme_selector::Toggle} action in the command palette to see a
 
 **Options**
 
-- `show_branch_icon`: Whether to show the branch icon beside branch switcher in the titlebar
+- `show_branch_status_icon`: Whether to show git status indicators on the branch icon in the titlebar
 - `show_branch_name`: Whether to show the branch name button in the titlebar
 - `show_project_items`: Whether to show the project host and name in the titlebar
 - `show_onboarding_banner`: Whether to show onboarding banners in the titlebar

docs/src/visual-customization.md πŸ”—

@@ -118,7 +118,7 @@ To disable this behavior use:
 ```json [settings]
   // Control which items are shown/hidden in the title bar
   "title_bar": {
-    "show_branch_icon": false,      // Show/hide branch icon beside branch switcher
+    "show_branch_status_icon": false, // Show git status on branch icon
     "show_branch_name": true,       // Show/hide branch name
     "show_project_items": true,     // Show/hide project host and name
     "show_onboarding_banner": true, // Show/hide onboarding banners