Merge remote-tracking branch 'origin/main' into oaken-spruce

Richard Feldman created

Change summary

Cargo.lock                                                            |  32 
Cargo.toml                                                            |   3 
crates/agent_ui/src/agent_panel.rs                                    | 673 
crates/agent_ui/src/agent_ui.rs                                       |  37 
crates/agent_ui/src/conversation_view/thread_view.rs                  |   5 
crates/agent_ui/src/thread_branch_picker.rs                           | 695 
crates/agent_ui/src/thread_worktree_picker.rs                         | 485 
crates/bedrock/src/models.rs                                          |  64 
crates/collab/tests/integration/git_tests.rs                          |  12 
crates/collab/tests/integration/remote_editing_collaboration_tests.rs |   6 
crates/collab_ui/src/collab_panel.rs                                  |   7 
crates/file_finder/Cargo.toml                                         |   1 
crates/file_finder/src/file_finder.rs                                 |  69 
crates/file_finder/src/file_finder_tests.rs                           | 230 
crates/fs/src/fake_git_repo.rs                                        | 113 
crates/fs/tests/integration/fake_git_repo.rs                          |  12 
crates/fuzzy_nucleo/Cargo.toml                                        |  21 
crates/fuzzy_nucleo/LICENSE-GPL                                       |   1 
crates/fuzzy_nucleo/src/fuzzy_nucleo.rs                               |   5 
crates/fuzzy_nucleo/src/matcher.rs                                    |  39 
crates/fuzzy_nucleo/src/paths.rs                                      | 352 
crates/git/src/repository.rs                                          | 120 
crates/git_ui/src/worktree_picker.rs                                  |   9 
crates/http_client/src/github_download.rs                             |  22 
crates/markdown/src/html/html_parser.rs                               | 117 
crates/markdown/src/html/html_rendering.rs                            |  18 
crates/markdown/src/markdown.rs                                       |  69 
crates/project/Cargo.toml                                             |   1 
crates/project/src/git_store.rs                                       | 102 
crates/project/src/project.rs                                         |  70 
crates/project/tests/integration/git_store.rs                         |  12 
crates/project_panel/src/project_panel_tests.rs                       | 146 
crates/proto/proto/git.proto                                          |   1 
crates/repl/src/kernels/ssh_kernel.rs                                 |   2 
crates/repl/src/kernels/wsl_kernel.rs                                 |   3 
crates/tasks_ui/src/modal.rs                                          |   4 
crates/workspace/src/pane.rs                                          |  66 
crates/zed/src/visual_test_runner.rs                                  |  18 
38 files changed, 3,171 insertions(+), 471 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -6183,6 +6183,7 @@ dependencies = [
  "file_icons",
  "futures 0.3.32",
  "fuzzy",
+ "fuzzy_nucleo",
  "gpui",
  "menu",
  "open_path_prompt",
@@ -6740,6 +6741,15 @@ dependencies = [
  "thread_local",
 ]
 
+[[package]]
+name = "fuzzy_nucleo"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+ "nucleo",
+ "util",
+]
+
 [[package]]
 name = "gaoya"
 version = "0.2.0"
@@ -11063,6 +11073,27 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
+[[package]]
+name = "nucleo"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4"
+dependencies = [
+ "nucleo-matcher",
+ "parking_lot",
+ "rayon",
+]
+
+[[package]]
+name = "nucleo-matcher"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
+dependencies = [
+ "memchr",
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "num"
 version = "0.4.3"
@@ -13203,6 +13234,7 @@ dependencies = [
  "fs",
  "futures 0.3.32",
  "fuzzy",
+ "fuzzy_nucleo",
  "git",
  "git2",
  "git_hosting_providers",

Cargo.toml πŸ”—

@@ -78,6 +78,7 @@ members = [
     "crates/fs",
     "crates/fs_benchmarks",
     "crates/fuzzy",
+    "crates/fuzzy_nucleo",
     "crates/git",
     "crates/git_graph",
     "crates/git_hosting_providers",
@@ -325,6 +326,7 @@ file_finder = { path = "crates/file_finder" }
 file_icons = { path = "crates/file_icons" }
 fs = { path = "crates/fs" }
 fuzzy = { path = "crates/fuzzy" }
+fuzzy_nucleo = { path = "crates/fuzzy_nucleo" }
 git = { path = "crates/git" }
 git_graph = { path = "crates/git_graph" }
 git_hosting_providers = { path = "crates/git_hosting_providers" }
@@ -609,6 +611,7 @@ naga = { version = "29.0", features = ["wgsl-in"] }
 nanoid = "0.4"
 nbformat = "1.2.0"
 nix = "0.29"
+nucleo = "0.5"
 num-format = "0.4.4"
 objc = "0.2"
 objc2-app-kit = { version = "0.3", default-features = false, features = [ "NSGraphics" ] }

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

@@ -28,21 +28,20 @@ use zed_actions::agent::{
 use crate::thread_metadata_store::ThreadMetadataStore;
 use crate::{
     AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, CycleStartThreadIn,
-    Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown,
-    OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, StartThreadIn,
-    ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
+    Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, NewWorktreeBranchTarget,
+    OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell,
+    StartThreadIn, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
     agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
     conversation_view::{AcpThreadViewEvent, ThreadView},
+    thread_branch_picker::ThreadBranchPicker,
+    thread_worktree_picker::ThreadWorktreePicker,
     ui::EndTrialUpsell,
 };
 use crate::{
     Agent, AgentInitialContent, ExternalSourcePrompt, NewExternalAgentThread,
     NewNativeAgentThreadFromSummary,
 };
-use crate::{
-    DEFAULT_THREAD_TITLE,
-    ui::{AcpOnboardingModal, HoldForDefault},
-};
+use crate::{DEFAULT_THREAD_TITLE, ui::AcpOnboardingModal};
 use crate::{ExpandMessageEditor, ThreadHistoryView};
 use crate::{ManageProfiles, ThreadHistoryViewEvent};
 use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore};
@@ -73,8 +72,8 @@ use terminal::terminal_settings::TerminalSettings;
 use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
 use theme_settings::ThemeSettings;
 use ui::{
-    Button, Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, DocumentationSide,
-    PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*, utils::WithRemSize,
+    Button, Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, PopoverMenu,
+    PopoverMenuHandle, Tab, Tooltip, prelude::*, utils::WithRemSize,
 };
 use util::{ResultExt as _, debug_panic};
 use workspace::{
@@ -620,7 +619,31 @@ impl StartThreadIn {
     fn label(&self) -> SharedString {
         match self {
             Self::LocalProject => "Current Worktree".into(),
-            Self::NewWorktree => "New Git Worktree".into(),
+            Self::NewWorktree {
+                worktree_name: Some(worktree_name),
+                ..
+            } => format!("New: {worktree_name}").into(),
+            Self::NewWorktree { .. } => "New Git Worktree".into(),
+            Self::LinkedWorktree { display_name, .. } => format!("From: {}", &display_name).into(),
+        }
+    }
+
+    fn worktree_branch_label(&self, default_branch_label: SharedString) -> Option<SharedString> {
+        match self {
+            Self::NewWorktree { branch_target, .. } => match branch_target {
+                NewWorktreeBranchTarget::CurrentBranch => Some(default_branch_label),
+                NewWorktreeBranchTarget::ExistingBranch { name } => {
+                    Some(format!("From: {name}").into())
+                }
+                NewWorktreeBranchTarget::CreateBranch { name, from_ref } => {
+                    if let Some(from_ref) = from_ref {
+                        Some(format!("From: {from_ref}").into())
+                    } else {
+                        Some(format!("From: {name}").into())
+                    }
+                }
+            },
+            _ => None,
         }
     }
 }
@@ -632,6 +655,17 @@ pub enum WorktreeCreationStatus {
     Error(SharedString),
 }
 
+#[derive(Clone, Debug)]
+enum WorktreeCreationArgs {
+    New {
+        worktree_name: Option<String>,
+        branch_target: NewWorktreeBranchTarget,
+    },
+    Linked {
+        worktree_path: PathBuf,
+    },
+}
+
 impl ActiveView {
     pub fn which_font_size_used(&self) -> WhichFontSize {
         match self {
@@ -662,7 +696,8 @@ pub struct AgentPanel {
     previous_view: Option<ActiveView>,
     background_threads: HashMap<acp::SessionId, Entity<ConversationView>>,
     new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
-    start_thread_in_menu_handle: PopoverMenuHandle<ContextMenu>,
+    start_thread_in_menu_handle: PopoverMenuHandle<ThreadWorktreePicker>,
+    thread_branch_menu_handle: PopoverMenuHandle<ThreadBranchPicker>,
     agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
     agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
     agent_navigation_menu: Option<Entity<ContextMenu>>,
@@ -689,7 +724,7 @@ impl AgentPanel {
         };
 
         let selected_agent = self.selected_agent.clone();
-        let start_thread_in = Some(self.start_thread_in);
+        let start_thread_in = Some(self.start_thread_in.clone());
 
         let last_active_thread = self.active_agent_thread(cx).map(|thread| {
             let thread = thread.read(cx);
@@ -794,18 +829,21 @@ impl AgentPanel {
                         } else if let Some(agent) = global_fallback {
                             panel.selected_agent = agent;
                         }
-                        if let Some(start_thread_in) = serialized_panel.start_thread_in {
+                        if let Some(ref start_thread_in) = serialized_panel.start_thread_in {
                             let is_worktree_flag_enabled =
                                 cx.has_flag::<AgentV2FeatureFlag>();
                             let is_valid = match &start_thread_in {
                                 StartThreadIn::LocalProject => true,
-                                StartThreadIn::NewWorktree => {
+                                StartThreadIn::NewWorktree { .. } => {
                                     let project = panel.project.read(cx);
                                     is_worktree_flag_enabled && !project.is_via_collab()
                                 }
+                                StartThreadIn::LinkedWorktree { path, .. } => {
+                                    is_worktree_flag_enabled && path.exists()
+                                }
                             };
                             if is_valid {
-                                panel.start_thread_in = start_thread_in;
+                                panel.start_thread_in = start_thread_in.clone();
                             } else {
                                 log::info!(
                                     "deserialized start_thread_in {:?} is no longer valid, falling back to LocalProject",
@@ -979,6 +1017,7 @@ impl AgentPanel {
             background_threads: HashMap::default(),
             new_thread_menu_handle: PopoverMenuHandle::default(),
             start_thread_in_menu_handle: PopoverMenuHandle::default(),
+            thread_branch_menu_handle: PopoverMenuHandle::default(),
             agent_panel_menu_handle: PopoverMenuHandle::default(),
             agent_navigation_menu_handle: PopoverMenuHandle::default(),
             agent_navigation_menu: None,
@@ -1948,24 +1987,43 @@ impl AgentPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if matches!(action, StartThreadIn::NewWorktree) && !cx.has_flag::<AgentV2FeatureFlag>() {
-            return;
-        }
-
-        let new_target = match *action {
+        let new_target = match action {
             StartThreadIn::LocalProject => StartThreadIn::LocalProject,
-            StartThreadIn::NewWorktree => {
+            StartThreadIn::NewWorktree { .. } => {
+                if !cx.has_flag::<AgentV2FeatureFlag>() {
+                    return;
+                }
+                if !self.project_has_git_repository(cx) {
+                    log::error!(
+                        "set_start_thread_in: cannot use worktree mode without a git repository"
+                    );
+                    return;
+                }
+                if self.project.read(cx).is_via_collab() {
+                    log::error!(
+                        "set_start_thread_in: cannot use worktree mode in a collab project"
+                    );
+                    return;
+                }
+                action.clone()
+            }
+            StartThreadIn::LinkedWorktree { .. } => {
+                if !cx.has_flag::<AgentV2FeatureFlag>() {
+                    return;
+                }
                 if !self.project_has_git_repository(cx) {
                     log::error!(
-                        "set_start_thread_in: cannot use NewWorktree without a git repository"
+                        "set_start_thread_in: cannot use LinkedWorktree without a git repository"
                     );
                     return;
                 }
                 if self.project.read(cx).is_via_collab() {
-                    log::error!("set_start_thread_in: cannot use NewWorktree in a collab project");
+                    log::error!(
+                        "set_start_thread_in: cannot use LinkedWorktree in a collab project"
+                    );
                     return;
                 }
-                StartThreadIn::NewWorktree
+                action.clone()
             }
         };
         self.start_thread_in = new_target;
@@ -1977,9 +2035,14 @@ impl AgentPanel {
     }
 
     fn cycle_start_thread_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let next = match self.start_thread_in {
-            StartThreadIn::LocalProject => StartThreadIn::NewWorktree,
-            StartThreadIn::NewWorktree => StartThreadIn::LocalProject,
+        let next = match &self.start_thread_in {
+            StartThreadIn::LocalProject => StartThreadIn::NewWorktree {
+                worktree_name: None,
+                branch_target: NewWorktreeBranchTarget::default(),
+            },
+            StartThreadIn::NewWorktree { .. } | StartThreadIn::LinkedWorktree { .. } => {
+                StartThreadIn::LocalProject
+            }
         };
         self.set_start_thread_in(&next, window, cx);
     }
@@ -1991,7 +2054,10 @@ impl AgentPanel {
             NewThreadLocation::LocalProject => StartThreadIn::LocalProject,
             NewThreadLocation::NewWorktree => {
                 if self.project_has_git_repository(cx) {
-                    StartThreadIn::NewWorktree
+                    StartThreadIn::NewWorktree {
+                        worktree_name: None,
+                        branch_target: NewWorktreeBranchTarget::default(),
+                    }
                 } else {
                     StartThreadIn::LocalProject
                 }
@@ -2219,15 +2285,39 @@ impl AgentPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if self.start_thread_in == StartThreadIn::NewWorktree {
-            self.handle_worktree_creation_requested(content, window, cx);
-        } else {
-            cx.defer_in(window, move |_this, window, cx| {
-                thread_view.update(cx, |thread_view, cx| {
-                    let editor = thread_view.message_editor.clone();
-                    thread_view.send_impl(editor, window, cx);
+        match &self.start_thread_in {
+            StartThreadIn::NewWorktree {
+                worktree_name,
+                branch_target,
+            } => {
+                self.handle_worktree_requested(
+                    content,
+                    WorktreeCreationArgs::New {
+                        worktree_name: worktree_name.clone(),
+                        branch_target: branch_target.clone(),
+                    },
+                    window,
+                    cx,
+                );
+            }
+            StartThreadIn::LinkedWorktree { path, .. } => {
+                self.handle_worktree_requested(
+                    content,
+                    WorktreeCreationArgs::Linked {
+                        worktree_path: path.clone(),
+                    },
+                    window,
+                    cx,
+                );
+            }
+            StartThreadIn::LocalProject => {
+                cx.defer_in(window, move |_this, window, cx| {
+                    thread_view.update(cx, |thread_view, cx| {
+                        let editor = thread_view.message_editor.clone();
+                        thread_view.send_impl(editor, window, cx);
+                    });
                 });
-            });
+            }
         }
     }
 
@@ -2289,6 +2379,33 @@ impl AgentPanel {
         (git_repos, non_git_paths)
     }
 
+    fn resolve_worktree_branch_target(
+        branch_target: &NewWorktreeBranchTarget,
+        existing_branches: &HashSet<String>,
+        occupied_branches: &HashSet<String>,
+    ) -> Result<(String, bool, Option<String>)> {
+        let generate_branch_name = || -> Result<String> {
+            let refs: Vec<&str> = existing_branches.iter().map(|s| s.as_str()).collect();
+            let mut rng = rand::rng();
+            crate::branch_names::generate_branch_name(&refs, &mut rng)
+                .ok_or_else(|| anyhow!("Failed to generate a unique branch name"))
+        };
+
+        match branch_target {
+            NewWorktreeBranchTarget::CreateBranch { name, from_ref } => {
+                Ok((name.clone(), false, from_ref.clone()))
+            }
+            NewWorktreeBranchTarget::ExistingBranch { name } => {
+                if occupied_branches.contains(name) {
+                    Ok((generate_branch_name()?, false, Some(name.clone())))
+                } else {
+                    Ok((name.clone(), true, None))
+                }
+            }
+            NewWorktreeBranchTarget::CurrentBranch => Ok((generate_branch_name()?, false, None)),
+        }
+    }
+
     /// Kicks off an async git-worktree creation for each repository. Returns:
     ///
     /// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuplesβ€”the
@@ -2297,7 +2414,10 @@ impl AgentPanel {
     ///   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>,
         branch_name: &str,
+        use_existing_branch: bool,
+        start_point: Option<String>,
         worktree_directory_setting: &str,
         cx: &mut Context<Self>,
     ) -> Result<(
@@ -2311,12 +2431,27 @@ impl AgentPanel {
         let mut creation_infos = Vec::new();
         let mut path_remapping = Vec::new();
 
+        let worktree_name = worktree_name.unwrap_or_else(|| branch_name.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(branch_name, worktree_directory_setting)?;
-                let receiver =
-                    repo.create_worktree(branch_name.to_string(), new_path.clone(), None);
+                    repo.path_for_new_linked_worktree(&worktree_name, worktree_directory_setting)?;
+                let target = if use_existing_branch {
+                    debug_assert!(
+                        git_repos.len() == 1,
+                        "use_existing_branch should only be true for a single repo"
+                    );
+                    git::repository::CreateWorktreeTarget::ExistingBranch {
+                        branch_name: branch_name.to_string(),
+                    }
+                } else {
+                    git::repository::CreateWorktreeTarget::NewBranch {
+                        branch_name: branch_name.to_string(),
+                        base_sha: start_point.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))
             })?;
@@ -2419,9 +2554,10 @@ impl AgentPanel {
         cx.notify();
     }
 
-    fn handle_worktree_creation_requested(
+    fn handle_worktree_requested(
         &mut self,
         content: Vec<acp::ContentBlock>,
+        args: WorktreeCreationArgs,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -2437,7 +2573,7 @@ impl AgentPanel {
 
         let (git_repos, non_git_paths) = self.classify_worktrees(cx);
 
-        if git_repos.is_empty() {
+        if matches!(args, WorktreeCreationArgs::New { .. }) && git_repos.is_empty() {
             self.set_worktree_creation_error(
                 "No git repositories found in the project".into(),
                 window,
@@ -2446,17 +2582,31 @@ impl AgentPanel {
             return;
         }
 
-        // Kick off branch listing as early as possible so it can run
-        // concurrently with the remaining synchronous setup work.
-        let branch_receivers: Vec<_> = git_repos
-            .iter()
-            .map(|repo| repo.update(cx, |repo, _cx| repo.branches()))
-            .collect();
-
-        let worktree_directory_setting = ProjectSettings::get_global(cx)
-            .git
-            .worktree_directory
-            .clone();
+        let (branch_receivers, worktree_receivers, worktree_directory_setting) =
+            if matches!(args, WorktreeCreationArgs::New { .. }) {
+                (
+                    Some(
+                        git_repos
+                            .iter()
+                            .map(|repo| repo.update(cx, |repo, _cx| repo.branches()))
+                            .collect::<Vec<_>>(),
+                    ),
+                    Some(
+                        git_repos
+                            .iter()
+                            .map(|repo| repo.update(cx, |repo, _cx| repo.worktrees()))
+                            .collect::<Vec<_>>(),
+                    ),
+                    Some(
+                        ProjectSettings::get_global(cx)
+                            .git
+                            .worktree_directory
+                            .clone(),
+                    ),
+                )
+            } else {
+                (None, None, None)
+            };
 
         let active_file_path = self.workspace.upgrade().and_then(|workspace| {
             let workspace = workspace.read(cx);
@@ -2476,77 +2626,124 @@ impl AgentPanel {
         let selected_agent = self.selected_agent();
 
         let task = cx.spawn_in(window, async move |this, cx| {
-            // Await the branch listings we kicked off earlier.
-            let mut existing_branches = Vec::new();
-            for result in futures::future::join_all(branch_receivers).await {
-                match result {
-                    Ok(Ok(branches)) => {
-                        for branch in branches {
-                            existing_branches.push(branch.name().to_string());
+            let (all_paths, path_remapping, has_non_git) = match args {
+                WorktreeCreationArgs::New {
+                    worktree_name,
+                    branch_target,
+                } => {
+                    let branch_receivers = branch_receivers
+                        .expect("branch receivers must be prepared for new worktree creation");
+                    let worktree_receivers = worktree_receivers
+                        .expect("worktree receivers must be prepared for new worktree creation");
+                    let worktree_directory_setting = worktree_directory_setting
+                        .expect("worktree directory must be prepared for new worktree creation");
+
+                    let mut existing_branches = HashSet::default();
+                    for result in futures::future::join_all(branch_receivers).await {
+                        match result {
+                            Ok(Ok(branches)) => {
+                                for branch in branches {
+                                    existing_branches.insert(branch.name().to_string());
+                                }
+                            }
+                            Ok(Err(err)) => {
+                                Err::<(), _>(err).log_err();
+                            }
+                            Err(_) => {}
                         }
                     }
-                    Ok(Err(err)) => {
-                        Err::<(), _>(err).log_err();
+
+                    let mut occupied_branches = HashSet::default();
+                    for result in futures::future::join_all(worktree_receivers).await {
+                        match result {
+                            Ok(Ok(worktrees)) => {
+                                for worktree in worktrees {
+                                    if let Some(branch_name) = worktree.branch_name() {
+                                        occupied_branches.insert(branch_name.to_string());
+                                    }
+                                }
+                            }
+                            Ok(Err(err)) => {
+                                Err::<(), _>(err).log_err();
+                            }
+                            Err(_) => {}
+                        }
                     }
-                    Err(_) => {}
-                }
-            }
 
-            let existing_branch_refs: Vec<&str> =
-                existing_branches.iter().map(|s| s.as_str()).collect();
-            let mut rng = rand::rng();
-            let branch_name =
-                match crate::branch_names::generate_branch_name(&existing_branch_refs, &mut rng) {
-                    Some(name) => name,
-                    None => {
-                        this.update_in(cx, |this, window, cx| {
-                            this.set_worktree_creation_error(
-                                "Failed to generate a unique branch name".into(),
-                                window,
+                    let (branch_name, use_existing_branch, start_point) =
+                        match Self::resolve_worktree_branch_target(
+                            &branch_target,
+                            &existing_branches,
+                            &occupied_branches,
+                        ) {
+                            Ok(target) => target,
+                            Err(err) => {
+                                this.update_in(cx, |this, window, cx| {
+                                    this.set_worktree_creation_error(
+                                        err.to_string().into(),
+                                        window,
+                                        cx,
+                                    );
+                                })?;
+                                return anyhow::Ok(());
+                            }
+                        };
+
+                    let (creation_infos, path_remapping) =
+                        match this.update_in(cx, |_this, _window, cx| {
+                            Self::start_worktree_creations(
+                                &git_repos,
+                                worktree_name,
+                                &branch_name,
+                                use_existing_branch,
+                                start_point,
+                                &worktree_directory_setting,
                                 cx,
-                            );
-                        })?;
-                        return anyhow::Ok(());
-                    }
-                };
+                            )
+                        }) {
+                            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 (creation_infos, path_remapping) = match this.update_in(cx, |_this, _window, cx| {
-                Self::start_worktree_creations(
-                    &git_repos,
-                    &branch_name,
-                    &worktree_directory_setting,
-                    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 created_paths =
+                        match Self::await_and_rollback_on_failure(creation_infos, 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(());
+                            }
+                        };
 
-            let created_paths = match Self::await_and_rollback_on_failure(creation_infos, 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(());
+                    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 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, Vec::new(), has_non_git)
                 }
             };
 
-            let mut all_paths = created_paths;
-            let has_non_git = !non_git_paths.is_empty();
-            all_paths.extend(non_git_paths.iter().cloned());
-
             let app_state = match workspace.upgrade() {
                 Some(workspace) => cx.update(|_, cx| workspace.read(cx).app_state().clone())?,
                 None => {
@@ -2562,7 +2759,7 @@ impl AgentPanel {
             };
 
             let this_for_error = this.clone();
-            if let Err(err) = Self::setup_new_workspace(
+            if let Err(err) = Self::open_worktree_workspace_and_start_thread(
                 this,
                 all_paths,
                 app_state,
@@ -2595,7 +2792,7 @@ impl AgentPanel {
         }));
     }
 
-    async fn setup_new_workspace(
+    async fn open_worktree_workspace_and_start_thread(
         this: WeakEntity<Self>,
         all_paths: Vec<PathBuf>,
         app_state: Arc<workspace::AppState>,
@@ -3149,25 +3346,15 @@ impl AgentPanel {
     }
 
     fn render_start_thread_in_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
-        use settings::{NewThreadLocation, Settings};
-
         let focus_handle = self.focus_handle(cx);
-        let has_git_repo = self.project_has_git_repository(cx);
-        let is_via_collab = self.project.read(cx).is_via_collab();
-        let fs = self.fs.clone();
 
         let is_creating = matches!(
             self.worktree_creation_status,
             Some(WorktreeCreationStatus::Creating)
         );
 
-        let current_target = self.start_thread_in;
         let trigger_label = self.start_thread_in.label();
 
-        let new_thread_location = AgentSettings::get_global(cx).new_thread_location;
-        let is_local_default = new_thread_location == NewThreadLocation::LocalProject;
-        let is_new_worktree_default = new_thread_location == NewThreadLocation::NewWorktree;
-
         let icon = if self.start_thread_in_menu_handle.is_deployed() {
             IconName::ChevronUp
         } else {
@@ -3178,13 +3365,9 @@ impl AgentPanel {
             .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
             .disabled(is_creating);
 
-        let dock_position = AgentSettings::get_global(cx).dock;
-        let documentation_side = match dock_position {
-            settings::DockPosition::Left => DocumentationSide::Right,
-            settings::DockPosition::Bottom | settings::DockPosition::Right => {
-                DocumentationSide::Left
-            }
-        };
+        let project = self.project.clone();
+        let current_target = self.start_thread_in.clone();
+        let fs = self.fs.clone();
 
         PopoverMenu::new("thread-target-selector")
             .trigger_with_tooltip(trigger_button, {
@@ -3198,89 +3381,66 @@ impl AgentPanel {
                 }
             })
             .menu(move |window, cx| {
-                let is_local_selected = current_target == StartThreadIn::LocalProject;
-                let is_new_worktree_selected = current_target == StartThreadIn::NewWorktree;
                 let fs = fs.clone();
+                Some(cx.new(|cx| {
+                    ThreadWorktreePicker::new(project.clone(), &current_target, fs, window, cx)
+                }))
+            })
+            .with_handle(self.start_thread_in_menu_handle.clone())
+            .anchor(Corner::TopLeft)
+            .offset(gpui::Point {
+                x: px(1.0),
+                y: px(1.0),
+            })
+    }
 
-                Some(ContextMenu::build(window, cx, move |menu, _window, _cx| {
-                    let new_worktree_disabled = !has_git_repo || is_via_collab;
+    fn render_new_worktree_branch_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
+        let is_creating = matches!(
+            self.worktree_creation_status,
+            Some(WorktreeCreationStatus::Creating)
+        );
+        let default_branch_label = if self.project.read(cx).repositories(cx).len() > 1 {
+            SharedString::from("From: current branches")
+        } else {
+            self.project
+                .read(cx)
+                .active_repository(cx)
+                .and_then(|repo| {
+                    repo.read(cx)
+                        .branch
+                        .as_ref()
+                        .map(|branch| SharedString::from(format!("From: {}", branch.name())))
+                })
+                .unwrap_or_else(|| SharedString::from("From: HEAD"))
+        };
+        let trigger_label = self
+            .start_thread_in
+            .worktree_branch_label(default_branch_label)
+            .unwrap_or_else(|| SharedString::from("From: HEAD"));
+        let icon = if self.thread_branch_menu_handle.is_deployed() {
+            IconName::ChevronUp
+        } else {
+            IconName::ChevronDown
+        };
+        let trigger_button = Button::new("thread-branch-trigger", trigger_label)
+            .start_icon(
+                Icon::new(IconName::GitBranch)
+                    .size(IconSize::Small)
+                    .color(Color::Muted),
+            )
+            .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
+            .disabled(is_creating);
+        let project = self.project.clone();
+        let current_target = self.start_thread_in.clone();
 
-                    menu.header("Start Thread In…")
-                        .item(
-                            ContextMenuEntry::new("Current Worktree")
-                                .toggleable(IconPosition::End, is_local_selected)
-                                .documentation_aside(documentation_side, move |_| {
-                                    HoldForDefault::new(is_local_default)
-                                        .more_content(false)
-                                        .into_any_element()
-                                })
-                                .handler({
-                                    let fs = fs.clone();
-                                    move |window, cx| {
-                                        if window.modifiers().secondary() {
-                                            update_settings_file(fs.clone(), cx, |settings, _| {
-                                                settings
-                                                    .agent
-                                                    .get_or_insert_default()
-                                                    .set_new_thread_location(
-                                                        NewThreadLocation::LocalProject,
-                                                    );
-                                            });
-                                        }
-                                        window.dispatch_action(
-                                            Box::new(StartThreadIn::LocalProject),
-                                            cx,
-                                        );
-                                    }
-                                }),
-                        )
-                        .item({
-                            let entry = ContextMenuEntry::new("New Git Worktree")
-                                .toggleable(IconPosition::End, is_new_worktree_selected)
-                                .disabled(new_worktree_disabled)
-                                .handler({
-                                    let fs = fs.clone();
-                                    move |window, cx| {
-                                        if window.modifiers().secondary() {
-                                            update_settings_file(fs.clone(), cx, |settings, _| {
-                                                settings
-                                                    .agent
-                                                    .get_or_insert_default()
-                                                    .set_new_thread_location(
-                                                        NewThreadLocation::NewWorktree,
-                                                    );
-                                            });
-                                        }
-                                        window.dispatch_action(
-                                            Box::new(StartThreadIn::NewWorktree),
-                                            cx,
-                                        );
-                                    }
-                                });
-
-                            if new_worktree_disabled {
-                                entry.documentation_aside(documentation_side, move |_| {
-                                    let reason = if !has_git_repo {
-                                        "No git repository found in this project."
-                                    } else {
-                                        "Not available for remote/collab projects yet."
-                                    };
-                                    Label::new(reason)
-                                        .color(Color::Muted)
-                                        .size(LabelSize::Small)
-                                        .into_any_element()
-                                })
-                            } else {
-                                entry.documentation_aside(documentation_side, move |_| {
-                                    HoldForDefault::new(is_new_worktree_default)
-                                        .more_content(false)
-                                        .into_any_element()
-                                })
-                            }
-                        })
+        PopoverMenu::new("thread-branch-selector")
+            .trigger_with_tooltip(trigger_button, Tooltip::text("Choose Worktree Branch…"))
+            .menu(move |window, cx| {
+                Some(cx.new(|cx| {
+                    ThreadBranchPicker::new(project.clone(), &current_target, window, cx)
                 }))
             })
-            .with_handle(self.start_thread_in_menu_handle.clone())
+            .with_handle(self.thread_branch_menu_handle.clone())
             .anchor(Corner::TopLeft)
             .offset(gpui::Point {
                 x: px(1.0),
@@ -3621,6 +3781,14 @@ impl AgentPanel {
                         .when(
                             has_visible_worktrees && self.project_has_git_repository(cx),
                             |this| this.child(self.render_start_thread_in_selector(cx)),
+                        )
+                        .when(
+                            has_v2_flag
+                                && matches!(
+                                    self.start_thread_in,
+                                    StartThreadIn::NewWorktree { .. }
+                                ),
+                            |this| this.child(self.render_new_worktree_branch_selector(cx)),
                         ),
                 )
                 .child(
@@ -5265,13 +5433,23 @@ mod tests {
 
         // Change thread target to NewWorktree.
         panel.update_in(cx, |panel, window, cx| {
-            panel.set_start_thread_in(&StartThreadIn::NewWorktree, window, cx);
+            panel.set_start_thread_in(
+                &StartThreadIn::NewWorktree {
+                    worktree_name: None,
+                    branch_target: NewWorktreeBranchTarget::default(),
+                },
+                window,
+                cx,
+            );
         });
 
         panel.read_with(cx, |panel, _cx| {
             assert_eq!(
                 *panel.start_thread_in(),
-                StartThreadIn::NewWorktree,
+                StartThreadIn::NewWorktree {
+                    worktree_name: None,
+                    branch_target: NewWorktreeBranchTarget::default(),
+                },
                 "thread target should be NewWorktree after set_thread_target"
             );
         });
@@ -5289,7 +5467,10 @@ mod tests {
         loaded_panel.read_with(cx, |panel, _cx| {
             assert_eq!(
                 *panel.start_thread_in(),
-                StartThreadIn::NewWorktree,
+                StartThreadIn::NewWorktree {
+                    worktree_name: None,
+                    branch_target: NewWorktreeBranchTarget::default(),
+                },
                 "thread target should survive serialization round-trip"
             );
         });
@@ -5420,6 +5601,53 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_resolve_worktree_branch_target() {
+        let existing_branches = HashSet::from_iter([
+            "main".to_string(),
+            "feature".to_string(),
+            "origin/main".to_string(),
+        ]);
+
+        let resolved = AgentPanel::resolve_worktree_branch_target(
+            &NewWorktreeBranchTarget::CreateBranch {
+                name: "new-branch".to_string(),
+                from_ref: Some("main".to_string()),
+            },
+            &existing_branches,
+            &HashSet::from_iter(["main".to_string()]),
+        )
+        .unwrap();
+        assert_eq!(
+            resolved,
+            ("new-branch".to_string(), false, Some("main".to_string()))
+        );
+
+        let resolved = AgentPanel::resolve_worktree_branch_target(
+            &NewWorktreeBranchTarget::ExistingBranch {
+                name: "feature".to_string(),
+            },
+            &existing_branches,
+            &HashSet::default(),
+        )
+        .unwrap();
+        assert_eq!(resolved, ("feature".to_string(), true, None));
+
+        let resolved = AgentPanel::resolve_worktree_branch_target(
+            &NewWorktreeBranchTarget::ExistingBranch {
+                name: "main".to_string(),
+            },
+            &existing_branches,
+            &HashSet::from_iter(["main".to_string()]),
+        )
+        .unwrap();
+        assert_eq!(resolved.1, false);
+        assert_eq!(resolved.2, Some("main".to_string()));
+        assert_ne!(resolved.0, "main");
+        assert!(existing_branches.contains("main"));
+        assert!(!existing_branches.contains(&resolved.0));
+    }
+
     #[gpui::test]
     async fn test_worktree_creation_preserves_selected_agent(cx: &mut TestAppContext) {
         init_test(cx);
@@ -5513,7 +5741,14 @@ mod tests {
             panel.selected_agent = Agent::Custom {
                 id: CODEX_ID.into(),
             };
-            panel.set_start_thread_in(&StartThreadIn::NewWorktree, window, cx);
+            panel.set_start_thread_in(
+                &StartThreadIn::NewWorktree {
+                    worktree_name: None,
+                    branch_target: NewWorktreeBranchTarget::default(),
+                },
+                window,
+                cx,
+            );
         });
 
         // Verify the panel has the Codex agent selected.
@@ -5532,7 +5767,15 @@ mod tests {
             "Hello from test",
         ))];
         panel.update_in(cx, |panel, window, cx| {
-            panel.handle_worktree_creation_requested(content, window, cx);
+            panel.handle_worktree_requested(
+                content,
+                WorktreeCreationArgs::New {
+                    worktree_name: None,
+                    branch_target: NewWorktreeBranchTarget::default(),
+                },
+                window,
+                cx,
+            );
         });
 
         // Let the async worktree creation + workspace setup complete.

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

@@ -28,13 +28,16 @@ mod terminal_codegen;
 mod terminal_inline_assistant;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test_support;
+mod thread_branch_picker;
 mod thread_history;
 mod thread_history_view;
 mod thread_import;
 pub mod thread_metadata_store;
+mod thread_worktree_picker;
 pub mod threads_archive_view;
 mod ui;
 
+use std::path::PathBuf;
 use std::rc::Rc;
 use std::sync::Arc;
 
@@ -314,16 +317,42 @@ 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>,
+    },
+}
+
 /// Sets where new threads will run.
-#[derive(
-    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Action,
-)]
+#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Action)]
 #[action(namespace = agent)]
 #[serde(rename_all = "snake_case", tag = "kind")]
 pub enum StartThreadIn {
     #[default]
     LocalProject,
-    NewWorktree,
+    NewWorktree {
+        /// When this is None, Zed will randomly generate a worktree name
+        /// otherwise, the provided name will be used.
+        #[serde(default)]
+        worktree_name: Option<String>,
+        #[serde(default)]
+        branch_target: NewWorktreeBranchTarget,
+    },
+    /// A linked worktree that already exists on disk.
+    LinkedWorktree { path: PathBuf, display_name: String },
 }
 
 /// Content to initialize new external agent with.

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

@@ -869,7 +869,10 @@ impl ThreadView {
                 .upgrade()
                 .and_then(|workspace| workspace.read(cx).panel::<AgentPanel>(cx))
                 .is_some_and(|panel| {
-                    panel.read(cx).start_thread_in() == &StartThreadIn::NewWorktree
+                    !matches!(
+                        panel.read(cx).start_thread_in(),
+                        StartThreadIn::LocalProject
+                    )
                 });
 
         if intercept_first_send {

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

@@ -0,0 +1,695 @@
+use std::collections::{HashMap, HashSet};
+
+use collections::HashSet as CollectionsHashSet;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+use fuzzy::StringMatchCandidate;
+use git::repository::Branch as GitBranch;
+use gpui::{
+    App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
+    ParentElement, Render, SharedString, Styled, Task, Window, rems,
+};
+use picker::{Picker, PickerDelegate, PickerEditorPosition};
+use project::Project;
+use ui::{
+    HighlightedLabel, Icon, IconName, Label, LabelCommon, ListItem, ListItemSpacing, Tooltip,
+    prelude::*,
+};
+use util::ResultExt as _;
+
+use crate::{NewWorktreeBranchTarget, StartThreadIn};
+
+pub(crate) struct ThreadBranchPicker {
+    picker: Entity<Picker<ThreadBranchPickerDelegate>>,
+    focus_handle: FocusHandle,
+    _subscription: gpui::Subscription,
+}
+
+impl ThreadBranchPicker {
+    pub fn new(
+        project: Entity<Project>,
+        current_target: &StartThreadIn,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let project_worktree_paths: HashSet<PathBuf> = project
+            .read(cx)
+            .visible_worktrees(cx)
+            .map(|worktree| worktree.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())
+            })
+            .unwrap_or_else(|| "HEAD".to_string());
+
+        let repository = if has_multiple_repositories {
+            None
+        } else {
+            project.read(cx).active_repository(cx)
+        };
+        let branches_request = repository
+            .clone()
+            .map(|repo| repo.update(cx, |repo, _| repo.branches()));
+        let default_branch_request = repository
+            .clone()
+            .map(|repo| repo.update(cx, |repo, _| repo.default_branch(false)));
+        let worktrees_request = repository.map(|repo| repo.update(cx, |repo, _| repo.worktrees()));
+
+        let (worktree_name, branch_target) = match current_target {
+            StartThreadIn::NewWorktree {
+                worktree_name,
+                branch_target,
+            } => (worktree_name.clone(), branch_target.clone()),
+            _ => (None, NewWorktreeBranchTarget::default()),
+        };
+
+        let delegate = ThreadBranchPickerDelegate {
+            matches: vec![ThreadBranchEntry::CurrentBranch],
+            all_branches: None,
+            occupied_branches: None,
+            selected_index: 0,
+            worktree_name,
+            branch_target,
+            project_worktree_paths,
+            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 focus_handle = picker.focus_handle(cx);
+
+        if let (Some(branches_request), Some(default_branch_request), Some(worktrees_request)) =
+            (branches_request, default_branch_request, worktrees_request)
+        {
+            let picker_handle = picker.downgrade();
+            cx.spawn_in(window, async move |_this, cx| {
+                let branches = branches_request.await??;
+                let default_branch = default_branch_request.await.ok().and_then(Result::ok).flatten();
+                let worktrees = worktrees_request.await??;
+
+                let remote_upstreams: CollectionsHashSet<_> = branches
+                    .iter()
+                    .filter_map(|branch| {
+                        branch
+                            .upstream
+                            .as_ref()
+                            .filter(|upstream| upstream.is_remote())
+                            .map(|upstream| upstream.ref_name.clone())
+                    })
+                    .collect();
+
+                let mut occupied_branches = HashMap::new();
+                for worktree in worktrees {
+                    let Some(branch_name) = worktree.branch_name().map(ToOwned::to_owned) else {
+                        continue;
+                    };
+
+                    let reason = if picker_handle
+                        .read_with(cx, |picker, _| {
+                            picker
+                                .delegate
+                                .project_worktree_paths
+                                .contains(&worktree.path)
+                        })
+                        .unwrap_or(false)
+                    {
+                        format!(
+                            "This branch is already checked out in the current project worktree at {}.",
+                            worktree.path.display()
+                        )
+                    } else {
+                        format!(
+                            "This branch is already checked out in a linked worktree at {}.",
+                            worktree.path.display()
+                        )
+                    };
+
+                    occupied_branches.insert(branch_name, reason);
+                }
+
+                let mut all_branches: Vec<_> = branches
+                    .into_iter()
+                    .filter(|branch| !remote_upstreams.contains(&branch.ref_name))
+                    .collect();
+                all_branches.sort_by_key(|branch| {
+                    (
+                        branch.is_remote(),
+                        !branch.is_head,
+                        branch
+                            .most_recent_commit
+                            .as_ref()
+                            .map(|commit| 0 - commit.commit_timestamp),
+                    )
+                });
+
+                picker_handle.update_in(cx, |picker, window, cx| {
+                    picker.delegate.all_branches = Some(all_branches);
+                    picker.delegate.occupied_branches = Some(occupied_branches);
+                    picker.delegate.default_branch_name = default_branch.map(|branch| branch.to_string());
+                    picker.refresh(window, cx);
+                })?;
+
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+
+        let subscription = cx.subscribe(&picker, |_, _, _, cx| {
+            cx.emit(DismissEvent);
+        });
+
+        Self {
+            picker,
+            focus_handle,
+            _subscription: subscription,
+        }
+    }
+}
+
+impl Focusable for ThreadBranchPicker {
+    fn focus_handle(&self, _cx: &App) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl EventEmitter<DismissEvent> for ThreadBranchPicker {}
+
+impl Render for ThreadBranchPicker {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        v_flex()
+            .w(rems(22.))
+            .elevation_3(cx)
+            .child(self.picker.clone())
+            .on_mouse_down_out(cx.listener(|_, _, _, cx| {
+                cx.emit(DismissEvent);
+            }))
+    }
+}
+
+#[derive(Clone)]
+enum ThreadBranchEntry {
+    CurrentBranch,
+    DefaultBranch,
+    ExistingBranch {
+        branch: GitBranch,
+        positions: Vec<usize>,
+        occupied_reason: Option<String>,
+    },
+    CreateNamed {
+        name: String,
+    },
+}
+
+pub(crate) struct ThreadBranchPickerDelegate {
+    matches: Vec<ThreadBranchEntry>,
+    all_branches: Option<Vec<GitBranch>>,
+    occupied_branches: Option<HashMap<String, String>>,
+    selected_index: usize,
+    worktree_name: Option<String>,
+    branch_target: NewWorktreeBranchTarget,
+    project_worktree_paths: HashSet<PathBuf>,
+    current_branch_name: String,
+    default_branch_name: Option<String>,
+    has_multiple_repositories: bool,
+}
+
+impl ThreadBranchPickerDelegate {
+    fn new_worktree_action(&self, branch_target: NewWorktreeBranchTarget) -> StartThreadIn {
+        StartThreadIn::NewWorktree {
+            worktree_name: self.worktree_name.clone(),
+            branch_target,
+        }
+    }
+
+    fn selected_entry_name(&self) -> Option<&str> {
+        match &self.branch_target {
+            NewWorktreeBranchTarget::CurrentBranch => None,
+            NewWorktreeBranchTarget::ExistingBranch { name } => Some(name),
+            NewWorktreeBranchTarget::CreateBranch {
+                from_ref: Some(from_ref),
+                ..
+            } => Some(from_ref),
+            NewWorktreeBranchTarget::CreateBranch { name, .. } => Some(name),
+        }
+    }
+
+    fn prefer_create_entry(&self) -> bool {
+        matches!(
+            &self.branch_target,
+            NewWorktreeBranchTarget::CreateBranch { from_ref: None, .. }
+        )
+    }
+
+    fn fixed_matches(&self) -> Vec<ThreadBranchEntry> {
+        let mut matches = vec![ThreadBranchEntry::CurrentBranch];
+        if !self.has_multiple_repositories
+            && self
+                .default_branch_name
+                .as_ref()
+                .is_some_and(|default_branch_name| default_branch_name != &self.current_branch_name)
+        {
+            matches.push(ThreadBranchEntry::DefaultBranch);
+        }
+        matches
+    }
+
+    fn current_branch_label(&self) -> SharedString {
+        if self.has_multiple_repositories {
+            SharedString::from("New branch from: current branches")
+        } else {
+            SharedString::from(format!("New branch from: {}", self.current_branch_name))
+        }
+    }
+
+    fn default_branch_label(&self) -> Option<SharedString> {
+        let default_branch_name = self
+            .default_branch_name
+            .as_ref()
+            .filter(|name| *name != &self.current_branch_name)?;
+        let is_occupied = self
+            .occupied_branches
+            .as_ref()
+            .is_some_and(|occupied| occupied.contains_key(default_branch_name));
+        let prefix = if is_occupied {
+            "New branch from"
+        } else {
+            "From"
+        };
+        Some(SharedString::from(format!(
+            "{prefix}: {default_branch_name}"
+        )))
+    }
+
+    fn branch_label_prefix(&self, branch_name: &str) -> &'static str {
+        let is_occupied = self
+            .occupied_branches
+            .as_ref()
+            .is_some_and(|occupied| occupied.contains_key(branch_name));
+        if is_occupied {
+            "New branch from: "
+        } else {
+            "From: "
+        }
+    }
+
+    fn sync_selected_index(&mut self) {
+        let selected_entry_name = self.selected_entry_name().map(ToOwned::to_owned);
+        let prefer_create = self.prefer_create_entry();
+
+        if prefer_create {
+            if let Some(ref selected_entry_name) = selected_entry_name {
+                if let Some(index) = self.matches.iter().position(|entry| {
+                    matches!(
+                        entry,
+                        ThreadBranchEntry::CreateNamed { name } if name == selected_entry_name
+                    )
+                }) {
+                    self.selected_index = index;
+                    return;
+                }
+            }
+        } else if let Some(ref selected_entry_name) = selected_entry_name {
+            if selected_entry_name == &self.current_branch_name {
+                if let Some(index) = self
+                    .matches
+                    .iter()
+                    .position(|entry| matches!(entry, ThreadBranchEntry::CurrentBranch))
+                {
+                    self.selected_index = index;
+                    return;
+                }
+            }
+
+            if self
+                .default_branch_name
+                .as_ref()
+                .is_some_and(|default_branch_name| default_branch_name == selected_entry_name)
+            {
+                if let Some(index) = self
+                    .matches
+                    .iter()
+                    .position(|entry| matches!(entry, ThreadBranchEntry::DefaultBranch))
+                {
+                    self.selected_index = index;
+                    return;
+                }
+            }
+
+            if let Some(index) = self.matches.iter().position(|entry| {
+                matches!(
+                    entry,
+                    ThreadBranchEntry::ExistingBranch { branch, .. }
+                        if branch.name() == selected_entry_name.as_str()
+                )
+            }) {
+                self.selected_index = index;
+                return;
+            }
+        }
+
+        if self.matches.len() > 1
+            && self
+                .matches
+                .iter()
+                .skip(1)
+                .all(|entry| matches!(entry, ThreadBranchEntry::CreateNamed { .. }))
+        {
+            self.selected_index = 1;
+            return;
+        }
+
+        self.selected_index = 0;
+    }
+}
+
+impl PickerDelegate for ThreadBranchPickerDelegate {
+    type ListItem = ListItem;
+
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+        "Search branches…".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 update_matches(
+        &mut self,
+        query: String,
+        window: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Task<()> {
+        if self.has_multiple_repositories {
+            let mut matches = self.fixed_matches();
+
+            if query.is_empty() {
+                if let Some(name) = self.selected_entry_name().map(ToOwned::to_owned) {
+                    if self.prefer_create_entry() {
+                        matches.push(ThreadBranchEntry::CreateNamed { name });
+                    }
+                }
+            } else {
+                matches.push(ThreadBranchEntry::CreateNamed {
+                    name: query.replace(' ', "-"),
+                });
+            }
+
+            self.matches = matches;
+            self.sync_selected_index();
+            return Task::ready(());
+        }
+
+        let Some(all_branches) = self.all_branches.clone() else {
+            self.matches = self.fixed_matches();
+            self.selected_index = 0;
+            return Task::ready(());
+        };
+        let occupied_branches = self.occupied_branches.clone().unwrap_or_default();
+
+        if query.is_empty() {
+            let mut matches = self.fixed_matches();
+            for branch in all_branches.into_iter().filter(|branch| {
+                branch.name() != self.current_branch_name
+                    && self
+                        .default_branch_name
+                        .as_ref()
+                        .is_none_or(|default_branch_name| branch.name() != default_branch_name)
+            }) {
+                matches.push(ThreadBranchEntry::ExistingBranch {
+                    occupied_reason: occupied_branches.get(branch.name()).cloned(),
+                    branch,
+                    positions: Vec::new(),
+                });
+            }
+
+            if let Some(selected_entry_name) = self.selected_entry_name().map(ToOwned::to_owned) {
+                let has_existing = matches.iter().any(|entry| {
+                    matches!(
+                        entry,
+                        ThreadBranchEntry::ExistingBranch { branch, .. }
+                            if branch.name() == selected_entry_name
+                    )
+                });
+                if self.prefer_create_entry() && !has_existing {
+                    matches.push(ThreadBranchEntry::CreateNamed {
+                        name: selected_entry_name,
+                    });
+                }
+            }
+
+            self.matches = matches;
+            self.sync_selected_index();
+            return Task::ready(());
+        }
+
+        let candidates: Vec<_> = all_branches
+            .iter()
+            .enumerate()
+            .map(|(ix, branch)| StringMatchCandidate::new(ix, branch.name()))
+            .collect();
+        let executor = cx.background_executor().clone();
+        let query_clone = query.clone();
+        let normalized_query = query.replace(' ', "-");
+
+        let task = cx.background_executor().spawn(async move {
+            fuzzy::match_strings(
+                &candidates,
+                &query_clone,
+                true,
+                true,
+                10000,
+                &Default::default(),
+                executor,
+            )
+            .await
+        });
+
+        let all_branches_clone = all_branches;
+        cx.spawn_in(window, async move |picker, cx| {
+            let fuzzy_matches = task.await;
+
+            picker
+                .update_in(cx, |picker, _window, cx| {
+                    let mut matches = picker.delegate.fixed_matches();
+
+                    for candidate in &fuzzy_matches {
+                        let branch = all_branches_clone[candidate.candidate_id].clone();
+                        if branch.name() == picker.delegate.current_branch_name
+                            || picker.delegate.default_branch_name.as_ref().is_some_and(
+                                |default_branch_name| branch.name() == default_branch_name,
+                            )
+                        {
+                            continue;
+                        }
+                        let occupied_reason = occupied_branches.get(branch.name()).cloned();
+                        matches.push(ThreadBranchEntry::ExistingBranch {
+                            branch,
+                            positions: candidate.positions.clone(),
+                            occupied_reason,
+                        });
+                    }
+
+                    if fuzzy_matches.is_empty() {
+                        matches.push(ThreadBranchEntry::CreateNamed {
+                            name: normalized_query.clone(),
+                        });
+                    }
+
+                    picker.delegate.matches = matches;
+                    if let Some(index) =
+                        picker.delegate.matches.iter().position(|entry| {
+                            matches!(entry, ThreadBranchEntry::ExistingBranch { .. })
+                        })
+                    {
+                        picker.delegate.selected_index = index;
+                    } else if !fuzzy_matches.is_empty() {
+                        picker.delegate.selected_index = 0;
+                    } else if let Some(index) =
+                        picker.delegate.matches.iter().position(|entry| {
+                            matches!(entry, ThreadBranchEntry::CreateNamed { .. })
+                        })
+                    {
+                        picker.delegate.selected_index = index;
+                    } else {
+                        picker.delegate.sync_selected_index();
+                    }
+                    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 {
+            ThreadBranchEntry::CurrentBranch => {
+                window.dispatch_action(
+                    Box::new(self.new_worktree_action(NewWorktreeBranchTarget::CurrentBranch)),
+                    cx,
+                );
+            }
+            ThreadBranchEntry::DefaultBranch => {
+                let Some(default_branch_name) = self.default_branch_name.clone() else {
+                    return;
+                };
+                window.dispatch_action(
+                    Box::new(
+                        self.new_worktree_action(NewWorktreeBranchTarget::ExistingBranch {
+                            name: default_branch_name,
+                        }),
+                    ),
+                    cx,
+                );
+            }
+            ThreadBranchEntry::ExistingBranch { branch, .. } => {
+                let branch_target = if branch.is_remote() {
+                    let branch_name = branch
+                        .ref_name
+                        .as_ref()
+                        .strip_prefix("refs/remotes/")
+                        .and_then(|stripped| stripped.split_once('/').map(|(_, name)| name))
+                        .unwrap_or(branch.name())
+                        .to_string();
+                    NewWorktreeBranchTarget::CreateBranch {
+                        name: branch_name,
+                        from_ref: Some(branch.name().to_string()),
+                    }
+                } else {
+                    NewWorktreeBranchTarget::ExistingBranch {
+                        name: branch.name().to_string(),
+                    }
+                };
+                window.dispatch_action(Box::new(self.new_worktree_action(branch_target)), cx);
+            }
+            ThreadBranchEntry::CreateNamed { name } => {
+                window.dispatch_action(
+                    Box::new(
+                        self.new_worktree_action(NewWorktreeBranchTarget::CreateBranch {
+                            name: name.clone(),
+                            from_ref: None,
+                        }),
+                    ),
+                    cx,
+                );
+            }
+        }
+
+        cx.emit(DismissEvent);
+    }
+
+    fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
+
+    fn separators_after_indices(&self) -> Vec<usize> {
+        let fixed_count = self.fixed_matches().len();
+        if self.matches.len() > fixed_count {
+            vec![fixed_count - 1]
+        } else {
+            Vec::new()
+        }
+    }
+
+    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)?;
+
+        match entry {
+            ThreadBranchEntry::CurrentBranch => Some(
+                ListItem::new("current-branch")
+                    .inset(true)
+                    .spacing(ListItemSpacing::Sparse)
+                    .toggle_state(selected)
+                    .start_slot(Icon::new(IconName::GitBranch).color(Color::Muted))
+                    .child(Label::new(self.current_branch_label())),
+            ),
+            ThreadBranchEntry::DefaultBranch => Some(
+                ListItem::new("default-branch")
+                    .inset(true)
+                    .spacing(ListItemSpacing::Sparse)
+                    .toggle_state(selected)
+                    .start_slot(Icon::new(IconName::GitBranch).color(Color::Muted))
+                    .child(Label::new(self.default_branch_label()?)),
+            ),
+            ThreadBranchEntry::ExistingBranch {
+                branch,
+                positions,
+                occupied_reason,
+            } => {
+                let prefix = self.branch_label_prefix(branch.name());
+                let branch_name = branch.name().to_string();
+                let full_label = format!("{prefix}{branch_name}");
+                let adjusted_positions: Vec<usize> =
+                    positions.iter().map(|&p| p + prefix.len()).collect();
+
+                let item = ListItem::new(SharedString::from(format!("branch-{ix}")))
+                    .inset(true)
+                    .spacing(ListItemSpacing::Sparse)
+                    .toggle_state(selected)
+                    .start_slot(Icon::new(IconName::GitBranch).color(Color::Muted))
+                    .child(HighlightedLabel::new(full_label, adjusted_positions).truncate());
+
+                Some(if let Some(reason) = occupied_reason.clone() {
+                    item.tooltip(Tooltip::text(reason))
+                } else if branch.is_remote() {
+                    item.tooltip(Tooltip::text(
+                        "Create a new local branch from this remote branch",
+                    ))
+                } else {
+                    item
+                })
+            }
+            ThreadBranchEntry::CreateNamed { name } => Some(
+                ListItem::new("create-named-branch")
+                    .inset(true)
+                    .spacing(ListItemSpacing::Sparse)
+                    .toggle_state(selected)
+                    .start_slot(Icon::new(IconName::Plus).color(Color::Accent))
+                    .child(Label::new(format!("Create Branch: \"{name}\"…"))),
+            ),
+        }
+    }
+
+    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
+        None
+    }
+}

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

@@ -0,0 +1,485 @@
+use std::path::PathBuf;
+use std::sync::Arc;
+
+use agent_settings::AgentSettings;
+use fs::Fs;
+use fuzzy::StringMatchCandidate;
+use git::repository::Worktree as GitWorktree;
+use gpui::{
+    App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
+    ParentElement, Render, SharedString, Styled, Task, Window, rems,
+};
+use picker::{Picker, PickerDelegate, PickerEditorPosition};
+use project::{Project, git_store::RepositoryId};
+use settings::{NewThreadLocation, Settings, update_settings_file};
+use ui::{
+    HighlightedLabel, Icon, IconName, Label, LabelCommon, ListItem, ListItemSpacing, Tooltip,
+    prelude::*,
+};
+use util::ResultExt as _;
+
+use crate::ui::HoldForDefault;
+use crate::{NewWorktreeBranchTarget, StartThreadIn};
+
+pub(crate) struct ThreadWorktreePicker {
+    picker: Entity<Picker<ThreadWorktreePickerDelegate>>,
+    focus_handle: FocusHandle,
+    _subscription: gpui::Subscription,
+}
+
+impl ThreadWorktreePicker {
+    pub fn new(
+        project: Entity<Project>,
+        current_target: &StartThreadIn,
+        fs: Arc<dyn Fs>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let project_worktree_paths: Vec<PathBuf> = project
+            .read(cx)
+            .visible_worktrees(cx)
+            .map(|wt| wt.read(cx).abs_path().to_path_buf())
+            .collect();
+
+        let preserved_branch_target = match current_target {
+            StartThreadIn::NewWorktree { branch_target, .. } => branch_target.clone(),
+            _ => NewWorktreeBranchTarget::default(),
+        };
+
+        let delegate = ThreadWorktreePickerDelegate {
+            matches: vec![
+                ThreadWorktreeEntry::CurrentWorktree,
+                ThreadWorktreeEntry::NewWorktree,
+            ],
+            all_worktrees: project
+                .read(cx)
+                .repositories(cx)
+                .iter()
+                .map(|(repo_id, repo)| (*repo_id, repo.read(cx).linked_worktrees.clone()))
+                .collect(),
+            project_worktree_paths,
+            selected_index: match current_target {
+                StartThreadIn::LocalProject => 0,
+                StartThreadIn::NewWorktree { .. } => 1,
+                _ => 0,
+            },
+            project: project.clone(),
+            preserved_branch_target,
+            fs,
+        };
+
+        let picker = cx.new(|cx| {
+            Picker::list(delegate, window, cx)
+                .list_measure_all()
+                .modal(false)
+                .max_height(Some(rems(20.).into()))
+        });
+
+        let subscription = cx.subscribe(&picker, |_, _, _, cx| {
+            cx.emit(DismissEvent);
+        });
+
+        Self {
+            focus_handle: picker.focus_handle(cx),
+            picker,
+            _subscription: subscription,
+        }
+    }
+}
+
+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(20.))
+            .elevation_3(cx)
+            .child(self.picker.clone())
+            .on_mouse_down_out(cx.listener(|_, _, _, cx| {
+                cx.emit(DismissEvent);
+            }))
+    }
+}
+
+#[derive(Clone)]
+enum ThreadWorktreeEntry {
+    CurrentWorktree,
+    NewWorktree,
+    LinkedWorktree {
+        worktree: GitWorktree,
+        positions: Vec<usize>,
+    },
+    CreateNamed {
+        name: String,
+        disabled_reason: Option<String>,
+    },
+}
+
+pub(crate) struct ThreadWorktreePickerDelegate {
+    matches: Vec<ThreadWorktreeEntry>,
+    all_worktrees: Vec<(RepositoryId, Arc<[GitWorktree]>)>,
+    project_worktree_paths: Vec<PathBuf>,
+    selected_index: usize,
+    preserved_branch_target: NewWorktreeBranchTarget,
+    project: Entity<Project>,
+    fs: Arc<dyn Fs>,
+}
+
+impl ThreadWorktreePickerDelegate {
+    fn new_worktree_action(&self, worktree_name: Option<String>) -> StartThreadIn {
+        StartThreadIn::NewWorktree {
+            worktree_name,
+            branch_target: self.preserved_branch_target.clone(),
+        }
+    }
+
+    fn sync_selected_index(&mut self) {
+        if let Some(index) = self
+            .matches
+            .iter()
+            .position(|entry| matches!(entry, ThreadWorktreeEntry::LinkedWorktree { .. }))
+        {
+            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 = ListItem;
+
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+        "Search or create worktrees…".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 separators_after_indices(&self) -> Vec<usize> {
+        if self.matches.len() > 2 {
+            vec![1]
+        } else {
+            Vec::new()
+        }
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        window: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Task<()> {
+        let has_multiple_repositories = self.all_worktrees.len() > 1;
+
+        let linked_worktrees: Vec<_> = if has_multiple_repositories {
+            Vec::new()
+        } else {
+            self.all_worktrees
+                .iter()
+                .flat_map(|(_, worktrees)| worktrees.iter())
+                .filter(|worktree| {
+                    !self
+                        .project_worktree_paths
+                        .iter()
+                        .any(|project_path| project_path == &worktree.path)
+                })
+                .cloned()
+                .collect()
+        };
+
+        let normalized_query = query.replace(' ', "-");
+        let has_named_worktree = self.all_worktrees.iter().any(|(_, worktrees)| {
+            worktrees
+                .iter()
+                .any(|worktree| worktree.display_name() == normalized_query)
+        });
+        let create_named_disabled_reason = if 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 mut matches = vec![
+            ThreadWorktreeEntry::CurrentWorktree,
+            ThreadWorktreeEntry::NewWorktree,
+        ];
+
+        if query.is_empty() {
+            for worktree in &linked_worktrees {
+                matches.push(ThreadWorktreeEntry::LinkedWorktree {
+                    worktree: worktree.clone(),
+                    positions: Vec::new(),
+                });
+            }
+        } else if linked_worktrees.is_empty() {
+            matches.push(ThreadWorktreeEntry::CreateNamed {
+                name: normalized_query,
+                disabled_reason: create_named_disabled_reason,
+            });
+        } else {
+            let candidates: Vec<_> = linked_worktrees
+                .iter()
+                .enumerate()
+                .map(|(ix, worktree)| StringMatchCandidate::new(ix, worktree.display_name()))
+                .collect();
+
+            let executor = cx.background_executor().clone();
+            let query_clone = query.clone();
+
+            let task = cx.background_executor().spawn(async move {
+                fuzzy::match_strings(
+                    &candidates,
+                    &query_clone,
+                    true,
+                    true,
+                    10000,
+                    &Default::default(),
+                    executor,
+                )
+                .await
+            });
+
+            let linked_worktrees_clone = linked_worktrees;
+            return 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::CurrentWorktree,
+                            ThreadWorktreeEntry::NewWorktree,
+                        ];
+
+                        for candidate in &fuzzy_matches {
+                            new_matches.push(ThreadWorktreeEntry::LinkedWorktree {
+                                worktree: linked_worktrees_clone[candidate.candidate_id].clone(),
+                                positions: candidate.positions.clone(),
+                            });
+                        }
+
+                        let has_exact_match = linked_worktrees_clone
+                            .iter()
+                            .any(|worktree| worktree.display_name() == query);
+
+                        if !has_exact_match {
+                            new_matches.push(ThreadWorktreeEntry::CreateNamed {
+                                name: normalized_query.clone(),
+                                disabled_reason: create_named_disabled_reason.clone(),
+                            });
+                        }
+
+                        picker.delegate.matches = new_matches;
+                        picker.delegate.sync_selected_index();
+
+                        cx.notify();
+                    })
+                    .log_err();
+            });
+        }
+
+        self.matches = matches;
+        self.sync_selected_index();
+
+        Task::ready(())
+    }
+
+    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::CurrentWorktree => {
+                if secondary {
+                    update_settings_file(self.fs.clone(), cx, |settings, _| {
+                        settings
+                            .agent
+                            .get_or_insert_default()
+                            .set_new_thread_location(NewThreadLocation::LocalProject);
+                    });
+                }
+                window.dispatch_action(Box::new(StartThreadIn::LocalProject), cx);
+            }
+            ThreadWorktreeEntry::NewWorktree => {
+                if secondary {
+                    update_settings_file(self.fs.clone(), cx, |settings, _| {
+                        settings
+                            .agent
+                            .get_or_insert_default()
+                            .set_new_thread_location(NewThreadLocation::NewWorktree);
+                    });
+                }
+                window.dispatch_action(Box::new(self.new_worktree_action(None)), cx);
+            }
+            ThreadWorktreeEntry::LinkedWorktree { worktree, .. } => {
+                window.dispatch_action(
+                    Box::new(StartThreadIn::LinkedWorktree {
+                        path: worktree.path.clone(),
+                        display_name: worktree.display_name().to_string(),
+                    }),
+                    cx,
+                );
+            }
+            ThreadWorktreeEntry::CreateNamed {
+                name,
+                disabled_reason: None,
+            } => {
+                window.dispatch_action(Box::new(self.new_worktree_action(Some(name.clone()))), 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_new_worktree_disabled =
+            project.repositories(cx).is_empty() || project.is_via_collab();
+        let new_thread_location = AgentSettings::get_global(cx).new_thread_location;
+        let is_local_default = new_thread_location == NewThreadLocation::LocalProject;
+        let is_new_worktree_default = new_thread_location == NewThreadLocation::NewWorktree;
+
+        match entry {
+            ThreadWorktreeEntry::CurrentWorktree => Some(
+                ListItem::new("current-worktree")
+                    .inset(true)
+                    .spacing(ListItemSpacing::Sparse)
+                    .toggle_state(selected)
+                    .start_slot(Icon::new(IconName::Folder).color(Color::Muted))
+                    .child(Label::new("Current Worktree"))
+                    .end_slot(HoldForDefault::new(is_local_default).more_content(false))
+                    .tooltip(Tooltip::text("Use the current project worktree")),
+            ),
+            ThreadWorktreeEntry::NewWorktree => {
+                let item = ListItem::new("new-worktree")
+                    .inset(true)
+                    .spacing(ListItemSpacing::Sparse)
+                    .toggle_state(selected)
+                    .disabled(is_new_worktree_disabled)
+                    .start_slot(
+                        Icon::new(IconName::Plus).color(if is_new_worktree_disabled {
+                            Color::Disabled
+                        } else {
+                            Color::Muted
+                        }),
+                    )
+                    .child(
+                        Label::new("New Git Worktree").color(if is_new_worktree_disabled {
+                            Color::Disabled
+                        } else {
+                            Color::Default
+                        }),
+                    );
+
+                Some(if is_new_worktree_disabled {
+                    item.tooltip(Tooltip::text("Requires a Git repository in the project"))
+                } else {
+                    item.end_slot(HoldForDefault::new(is_new_worktree_default).more_content(false))
+                        .tooltip(Tooltip::text("Start a thread in a new Git worktree"))
+                })
+            }
+            ThreadWorktreeEntry::LinkedWorktree {
+                worktree,
+                positions,
+            } => {
+                let display_name = worktree.display_name();
+                let first_line = display_name.lines().next().unwrap_or(display_name);
+                let positions: Vec<_> = positions
+                    .iter()
+                    .copied()
+                    .filter(|&pos| pos < first_line.len())
+                    .collect();
+
+                Some(
+                    ListItem::new(SharedString::from(format!("linked-worktree-{ix}")))
+                        .inset(true)
+                        .spacing(ListItemSpacing::Sparse)
+                        .toggle_state(selected)
+                        .start_slot(Icon::new(IconName::GitWorktree).color(Color::Muted))
+                        .child(HighlightedLabel::new(first_line.to_owned(), positions).truncate()),
+                )
+            }
+            ThreadWorktreeEntry::CreateNamed {
+                name,
+                disabled_reason,
+            } => {
+                let is_disabled = disabled_reason.is_some();
+                let item = ListItem::new("create-named-worktree")
+                    .inset(true)
+                    .spacing(ListItemSpacing::Sparse)
+                    .toggle_state(selected)
+                    .disabled(is_disabled)
+                    .start_slot(Icon::new(IconName::Plus).color(if is_disabled {
+                        Color::Disabled
+                    } else {
+                        Color::Accent
+                    }))
+                    .child(Label::new(format!("Create Worktree: \"{name}\"…")).color(
+                        if is_disabled {
+                            Color::Disabled
+                        } else {
+                            Color::Default
+                        },
+                    ));
+
+                Some(if let Some(reason) = disabled_reason.clone() {
+                    item.tooltip(Tooltip::text(reason))
+                } else {
+                    item
+                })
+            }
+        }
+    }
+
+    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
+        None
+    }
+}

crates/bedrock/src/models.rs πŸ”—

@@ -113,6 +113,10 @@ pub enum Model {
     MistralLarge3,
     #[serde(rename = "pixtral-large")]
     PixtralLarge,
+    #[serde(rename = "devstral-2-123b")]
+    Devstral2_123B,
+    #[serde(rename = "ministral-14b")]
+    Ministral14B,
 
     // Qwen models
     #[serde(rename = "qwen3-32b")]
@@ -146,9 +150,27 @@ pub enum Model {
     #[serde(rename = "gpt-oss-120b")]
     GptOss120B,
 
+    // NVIDIA Nemotron models
+    #[serde(rename = "nemotron-super-3-120b")]
+    NemotronSuper3_120B,
+    #[serde(rename = "nemotron-nano-3-30b")]
+    NemotronNano3_30B,
+
     // MiniMax models
     #[serde(rename = "minimax-m2")]
     MiniMaxM2,
+    #[serde(rename = "minimax-m2-1")]
+    MiniMaxM2_1,
+    #[serde(rename = "minimax-m2-5")]
+    MiniMaxM2_5,
+
+    // Z.AI GLM models
+    #[serde(rename = "glm-5")]
+    GLM5,
+    #[serde(rename = "glm-4-7")]
+    GLM4_7,
+    #[serde(rename = "glm-4-7-flash")]
+    GLM4_7Flash,
 
     // Moonshot models
     #[serde(rename = "kimi-k2-thinking")]
@@ -217,6 +239,8 @@ impl Model {
             Self::MagistralSmall => "magistral-small",
             Self::MistralLarge3 => "mistral-large-3",
             Self::PixtralLarge => "pixtral-large",
+            Self::Devstral2_123B => "devstral-2-123b",
+            Self::Ministral14B => "ministral-14b",
             Self::Qwen3_32B => "qwen3-32b",
             Self::Qwen3VL235B => "qwen3-vl-235b",
             Self::Qwen3_235B => "qwen3-235b",
@@ -230,7 +254,14 @@ impl Model {
             Self::Nova2Lite => "nova-2-lite",
             Self::GptOss20B => "gpt-oss-20b",
             Self::GptOss120B => "gpt-oss-120b",
+            Self::NemotronSuper3_120B => "nemotron-super-3-120b",
+            Self::NemotronNano3_30B => "nemotron-nano-3-30b",
             Self::MiniMaxM2 => "minimax-m2",
+            Self::MiniMaxM2_1 => "minimax-m2-1",
+            Self::MiniMaxM2_5 => "minimax-m2-5",
+            Self::GLM5 => "glm-5",
+            Self::GLM4_7 => "glm-4-7",
+            Self::GLM4_7Flash => "glm-4-7-flash",
             Self::KimiK2Thinking => "kimi-k2-thinking",
             Self::KimiK2_5 => "kimi-k2-5",
             Self::DeepSeekR1 => "deepseek-r1",
@@ -257,6 +288,8 @@ impl Model {
             Self::MagistralSmall => "mistral.magistral-small-2509",
             Self::MistralLarge3 => "mistral.mistral-large-3-675b-instruct",
             Self::PixtralLarge => "mistral.pixtral-large-2502-v1:0",
+            Self::Devstral2_123B => "mistral.devstral-2-123b",
+            Self::Ministral14B => "mistral.ministral-3-14b-instruct",
             Self::Qwen3VL235B => "qwen.qwen3-vl-235b-a22b",
             Self::Qwen3_32B => "qwen.qwen3-32b-v1:0",
             Self::Qwen3_235B => "qwen.qwen3-235b-a22b-2507-v1:0",
@@ -270,7 +303,14 @@ impl Model {
             Self::Nova2Lite => "amazon.nova-2-lite-v1:0",
             Self::GptOss20B => "openai.gpt-oss-20b-1:0",
             Self::GptOss120B => "openai.gpt-oss-120b-1:0",
+            Self::NemotronSuper3_120B => "nvidia.nemotron-super-3-120b",
+            Self::NemotronNano3_30B => "nvidia.nemotron-nano-3-30b",
             Self::MiniMaxM2 => "minimax.minimax-m2",
+            Self::MiniMaxM2_1 => "minimax.minimax-m2.1",
+            Self::MiniMaxM2_5 => "minimax.minimax-m2.5",
+            Self::GLM5 => "zai.glm-5",
+            Self::GLM4_7 => "zai.glm-4.7",
+            Self::GLM4_7Flash => "zai.glm-4.7-flash",
             Self::KimiK2Thinking => "moonshot.kimi-k2-thinking",
             Self::KimiK2_5 => "moonshotai.kimi-k2.5",
             Self::DeepSeekR1 => "deepseek.r1-v1:0",
@@ -297,6 +337,8 @@ impl Model {
             Self::MagistralSmall => "Magistral Small",
             Self::MistralLarge3 => "Mistral Large 3",
             Self::PixtralLarge => "Pixtral Large",
+            Self::Devstral2_123B => "Devstral 2 123B",
+            Self::Ministral14B => "Ministral 14B",
             Self::Qwen3VL235B => "Qwen3 VL 235B",
             Self::Qwen3_32B => "Qwen3 32B",
             Self::Qwen3_235B => "Qwen3 235B",
@@ -310,7 +352,14 @@ impl Model {
             Self::Nova2Lite => "Amazon Nova 2 Lite",
             Self::GptOss20B => "GPT OSS 20B",
             Self::GptOss120B => "GPT OSS 120B",
+            Self::NemotronSuper3_120B => "Nemotron Super 3 120B",
+            Self::NemotronNano3_30B => "Nemotron Nano 3 30B",
             Self::MiniMaxM2 => "MiniMax M2",
+            Self::MiniMaxM2_1 => "MiniMax M2.1",
+            Self::MiniMaxM2_5 => "MiniMax M2.5",
+            Self::GLM5 => "GLM 5",
+            Self::GLM4_7 => "GLM 4.7",
+            Self::GLM4_7Flash => "GLM 4.7 Flash",
             Self::KimiK2Thinking => "Kimi K2 Thinking",
             Self::KimiK2_5 => "Kimi K2.5",
             Self::DeepSeekR1 => "DeepSeek R1",
@@ -338,6 +387,7 @@ impl Model {
             Self::Llama4Scout17B | Self::Llama4Maverick17B => 128_000,
             Self::Gemma3_4B | Self::Gemma3_12B | Self::Gemma3_27B => 128_000,
             Self::MagistralSmall | Self::MistralLarge3 | Self::PixtralLarge => 128_000,
+            Self::Devstral2_123B | Self::Ministral14B => 256_000,
             Self::Qwen3_32B
             | Self::Qwen3VL235B
             | Self::Qwen3_235B
@@ -349,7 +399,9 @@ impl Model {
             Self::NovaPremier => 1_000_000,
             Self::Nova2Lite => 300_000,
             Self::GptOss20B | Self::GptOss120B => 128_000,
-            Self::MiniMaxM2 => 128_000,
+            Self::NemotronSuper3_120B | Self::NemotronNano3_30B => 262_000,
+            Self::MiniMaxM2 | Self::MiniMaxM2_1 | Self::MiniMaxM2_5 => 196_000,
+            Self::GLM5 | Self::GLM4_7 | Self::GLM4_7Flash => 203_000,
             Self::KimiK2Thinking | Self::KimiK2_5 => 128_000,
             Self::DeepSeekR1 | Self::DeepSeekV3_1 | Self::DeepSeekV3_2 => 128_000,
             Self::Custom { max_tokens, .. } => *max_tokens,
@@ -373,6 +425,7 @@ impl Model {
             | Self::MagistralSmall
             | Self::MistralLarge3
             | Self::PixtralLarge => 8_192,
+            Self::Devstral2_123B | Self::Ministral14B => 131_000,
             Self::Qwen3_32B
             | Self::Qwen3VL235B
             | Self::Qwen3_235B
@@ -382,7 +435,9 @@ impl Model {
             | Self::Qwen3Coder480B => 8_192,
             Self::NovaLite | Self::NovaPro | Self::NovaPremier | Self::Nova2Lite => 5_000,
             Self::GptOss20B | Self::GptOss120B => 16_000,
-            Self::MiniMaxM2 => 16_000,
+            Self::NemotronSuper3_120B | Self::NemotronNano3_30B => 131_000,
+            Self::MiniMaxM2 | Self::MiniMaxM2_1 | Self::MiniMaxM2_5 => 98_000,
+            Self::GLM5 | Self::GLM4_7 | Self::GLM4_7Flash => 101_000,
             Self::KimiK2Thinking | Self::KimiK2_5 => 16_000,
             Self::DeepSeekR1 | Self::DeepSeekV3_1 | Self::DeepSeekV3_2 => 16_000,
             Self::Custom {
@@ -419,6 +474,7 @@ impl Model {
             | Self::ClaudeSonnet4_6 => true,
             Self::NovaLite | Self::NovaPro | Self::NovaPremier | Self::Nova2Lite => true,
             Self::MistralLarge3 | Self::PixtralLarge | Self::MagistralSmall => true,
+            Self::Devstral2_123B | Self::Ministral14B => true,
             // Gemma accepts toolConfig without error but produces unreliable tool
             // calls -- malformed JSON args, hallucinated tool names, dropped calls.
             Self::Qwen3_32B
@@ -428,7 +484,9 @@ impl Model {
             | Self::Qwen3Coder30B
             | Self::Qwen3CoderNext
             | Self::Qwen3Coder480B => true,
-            Self::MiniMaxM2 => true,
+            Self::MiniMaxM2 | Self::MiniMaxM2_1 | Self::MiniMaxM2_5 => true,
+            Self::NemotronSuper3_120B | Self::NemotronNano3_30B => true,
+            Self::GLM5 | Self::GLM4_7 | Self::GLM4_7Flash => true,
             Self::KimiK2Thinking | Self::KimiK2_5 => true,
             Self::DeepSeekR1 | Self::DeepSeekV3_1 | Self::DeepSeekV3_2 => true,
             _ => false,

crates/collab/tests/integration/git_tests.rs πŸ”—

@@ -269,9 +269,11 @@ async fn test_remote_git_worktrees(
     cx_b.update(|cx| {
         repo_b.update(cx, |repository, _| {
             repository.create_worktree(
-                "feature-branch".to_string(),
+                git::repository::CreateWorktreeTarget::NewBranch {
+                    branch_name: "feature-branch".to_string(),
+                    base_sha: Some("abc123".to_string()),
+                },
                 worktree_directory.join("feature-branch"),
-                Some("abc123".to_string()),
             )
         })
     })
@@ -323,9 +325,11 @@ async fn test_remote_git_worktrees(
     cx_b.update(|cx| {
         repo_b.update(cx, |repository, _| {
             repository.create_worktree(
-                "bugfix-branch".to_string(),
+                git::repository::CreateWorktreeTarget::NewBranch {
+                    branch_name: "bugfix-branch".to_string(),
+                    base_sha: None,
+                },
                 worktree_directory.join("bugfix-branch"),
-                None,
             )
         })
     })

crates/collab/tests/integration/remote_editing_collaboration_tests.rs πŸ”—

@@ -473,9 +473,11 @@ async fn test_ssh_collaboration_git_worktrees(
     cx_b.update(|cx| {
         repo_b.update(cx, |repo, _| {
             repo.create_worktree(
-                "feature-branch".to_string(),
+                git::repository::CreateWorktreeTarget::NewBranch {
+                    branch_name: "feature-branch".to_string(),
+                    base_sha: Some("abc123".to_string()),
+                },
                 worktree_directory.join("feature-branch"),
-                Some("abc123".to_string()),
             )
         })
     })

crates/collab_ui/src/collab_panel.rs πŸ”—

@@ -1181,7 +1181,6 @@ impl CollabPanel {
         .into();
 
         ListItem::new(project_id as usize)
-            .height(px(24.))
             .toggle_state(is_selected)
             .on_click(cx.listener(move |this, _, window, cx| {
                 this.workspace
@@ -1222,7 +1221,6 @@ impl CollabPanel {
         let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
 
         ListItem::new(("screen", id))
-            .height(px(24.))
             .toggle_state(is_selected)
             .start_slot(
                 h_flex()
@@ -1269,7 +1267,6 @@ impl CollabPanel {
         let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);
 
         ListItem::new("channel-notes")
-            .height(px(24.))
             .toggle_state(is_selected)
             .on_click(cx.listener(move |this, _, window, cx| {
                 this.open_channel_notes(channel_id, window, cx);
@@ -3210,12 +3207,9 @@ impl CollabPanel {
             (IconName::Star, Color::Default, "Add to Favorites")
         };
 
-        let height = px(24.);
-
         h_flex()
             .id(ix)
             .group("")
-            .h(height)
             .w_full()
             .overflow_hidden()
             .when(!channel.is_root_channel(), |el| {
@@ -3245,7 +3239,6 @@ impl CollabPanel {
             )
             .child(
                 ListItem::new(ix)
-                    .height(height)
                     // Add one level of depth for the disclosure arrow.
                     .indent_level(depth + 1)
                     .indent_step_size(px(20.))

crates/file_finder/Cargo.toml πŸ”—

@@ -21,6 +21,7 @@ editor.workspace = true
 file_icons.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
+fuzzy_nucleo.workspace = true
 gpui.workspace = true
 menu.workspace = true
 open_path_prompt.workspace = true

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

@@ -9,7 +9,8 @@ use client::ChannelId;
 use collections::HashMap;
 use editor::Editor;
 use file_icons::FileIcons;
-use fuzzy::{CharBag, PathMatch, PathMatchCandidate, StringMatch, StringMatchCandidate};
+use fuzzy::{StringMatch, StringMatchCandidate};
+use fuzzy_nucleo::{PathMatch, PathMatchCandidate};
 use gpui::{
     Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
     KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity,
@@ -663,15 +664,6 @@ impl Matches {
 
         // For file-vs-file matches, use the existing detailed comparison.
         if let (Some(a_panel), Some(b_panel)) = (a.panel_match(), b.panel_match()) {
-            let a_in_filename = Self::is_filename_match(a_panel);
-            let b_in_filename = Self::is_filename_match(b_panel);
-
-            match (a_in_filename, b_in_filename) {
-                (true, false) => return cmp::Ordering::Greater,
-                (false, true) => return cmp::Ordering::Less,
-                _ => {}
-            }
-
             return a_panel.cmp(b_panel);
         }
 
@@ -691,32 +683,6 @@ impl Matches {
             Match::CreateNew(_) => 0.0,
         }
     }
-
-    /// Determines if the match occurred within the filename rather than in the path
-    fn is_filename_match(panel_match: &ProjectPanelOrdMatch) -> bool {
-        if panel_match.0.positions.is_empty() {
-            return false;
-        }
-
-        if let Some(filename) = panel_match.0.path.file_name() {
-            let path_str = panel_match.0.path.as_unix_str();
-
-            if let Some(filename_pos) = path_str.rfind(filename)
-                && panel_match.0.positions[0] >= filename_pos
-            {
-                let mut prev_position = panel_match.0.positions[0];
-                for p in &panel_match.0.positions[1..] {
-                    if *p != prev_position + 1 {
-                        return false;
-                    }
-                    prev_position = *p;
-                }
-                return true;
-            }
-        }
-
-        false
-    }
 }
 
 fn matching_history_items<'a>(
@@ -731,25 +697,16 @@ fn matching_history_items<'a>(
     let history_items_by_worktrees = history_items
         .into_iter()
         .chain(currently_opened)
-        .filter_map(|found_path| {
+        .map(|found_path| {
             let candidate = PathMatchCandidate {
                 is_dir: false, // You can't open directories as project items
                 path: &found_path.project.path,
                 // Only match history items names, otherwise their paths may match too many queries, producing false positives.
                 // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
                 // it would be shown first always, despite the latter being a better match.
-                char_bag: CharBag::from_iter(
-                    found_path
-                        .project
-                        .path
-                        .file_name()?
-                        .to_string()
-                        .to_lowercase()
-                        .chars(),
-                ),
             };
             candidates_paths.insert(&found_path.project, found_path);
-            Some((found_path.project.worktree_id, candidate))
+            (found_path.project.worktree_id, candidate)
         })
         .fold(
             HashMap::default(),
@@ -767,8 +724,9 @@ fn matching_history_items<'a>(
         let worktree_root_name = worktree_name_by_id
             .as_ref()
             .and_then(|w| w.get(&worktree).cloned());
+
         matching_history_paths.extend(
-            fuzzy::match_fixed_path_set(
+            fuzzy_nucleo::match_fixed_path_set(
                 candidates,
                 worktree.to_usize(),
                 worktree_root_name,
@@ -778,6 +736,18 @@ fn matching_history_items<'a>(
                 path_style,
             )
             .into_iter()
+            // filter matches where at least one matched position is in filename portion, to prevent directory matches, nucleo scores them higher as history items are matched against their full path
+            .filter(|path_match| {
+                if let Some(filename) = path_match.path.file_name() {
+                    let filename_start = path_match.path.as_unix_str().len() - filename.len();
+                    path_match
+                        .positions
+                        .iter()
+                        .any(|&pos| pos >= filename_start)
+                } else {
+                    true
+                }
+            })
             .filter_map(|path_match| {
                 candidates_paths
                     .remove_entry(&ProjectPath {
@@ -940,7 +910,7 @@ impl FileFinderDelegate {
         self.cancel_flag = Arc::new(AtomicBool::new(false));
         let cancel_flag = self.cancel_flag.clone();
         cx.spawn_in(window, async move |picker, cx| {
-            let matches = fuzzy::match_path_sets(
+            let matches = fuzzy_nucleo::match_path_sets(
                 candidate_sets.as_slice(),
                 query.path_query(),
                 &relative_to,
@@ -1452,7 +1422,6 @@ impl PickerDelegate for FileFinderDelegate {
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Task<()> {
-        let raw_query = raw_query.replace(' ', "");
         let raw_query = raw_query.trim();
 
         let raw_query = match &raw_query.get(0..2) {

crates/file_finder/src/file_finder_tests.rs πŸ”—

@@ -4161,3 +4161,233 @@ async fn test_clear_navigation_history(cx: &mut TestAppContext) {
         "Should have no history items after clearing"
     );
 }
+
+#[gpui::test]
+async fn test_order_independent_search(cx: &mut TestAppContext) {
+    let app_state = init_test(cx);
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            "/src",
+            json!({
+                "internal": {
+                    "auth": {
+                        "login.rs": "",
+                    }
+                }
+            }),
+        )
+        .await;
+    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+    let (picker, _, cx) = build_find_picker(project, cx);
+
+    // forward order
+    picker
+        .update_in(cx, |picker, window, cx| {
+            picker
+                .delegate
+                .spawn_search(test_path_position("auth internal"), window, cx)
+        })
+        .await;
+    picker.update(cx, |picker, _| {
+        let matches = collect_search_matches(picker).search_matches_only();
+        assert_eq!(matches.len(), 1);
+        assert_eq!(matches[0].path.as_unix_str(), "internal/auth/login.rs");
+    });
+
+    // reverse order should give same result
+    picker
+        .update_in(cx, |picker, window, cx| {
+            picker
+                .delegate
+                .spawn_search(test_path_position("internal auth"), window, cx)
+        })
+        .await;
+    picker.update(cx, |picker, _| {
+        let matches = collect_search_matches(picker).search_matches_only();
+        assert_eq!(matches.len(), 1);
+        assert_eq!(matches[0].path.as_unix_str(), "internal/auth/login.rs");
+    });
+}
+
+#[gpui::test]
+async fn test_filename_preferred_over_directory_match(cx: &mut TestAppContext) {
+    let app_state = init_test(cx);
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            "/src",
+            json!({
+                "crates": {
+                    "settings_ui": {
+                        "src": {
+                            "pages": {
+                                "audio_test_window.rs": "",
+                                "audio_input_output_setup.rs": "",
+                            }
+                        }
+                    },
+                    "audio": {
+                        "src": {
+                            "audio_settings.rs": "",
+                        }
+                    }
+                }
+            }),
+        )
+        .await;
+    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+    let (picker, _, cx) = build_find_picker(project, cx);
+
+    picker
+        .update_in(cx, |picker, window, cx| {
+            picker
+                .delegate
+                .spawn_search(test_path_position("settings audio"), window, cx)
+        })
+        .await;
+    picker.update(cx, |picker, _| {
+        let matches = collect_search_matches(picker).search_matches_only();
+        assert!(!matches.is_empty(),);
+        assert_eq!(
+            matches[0].path.as_unix_str(),
+            "crates/audio/src/audio_settings.rs"
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_start_of_word_preferred_over_scattered_match(cx: &mut TestAppContext) {
+    let app_state = init_test(cx);
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            "/src",
+            json!({
+                "crates": {
+                    "livekit_client": {
+                        "src": {
+                            "livekit_client": {
+                                "playback.rs": "",
+                            }
+                        }
+                    },
+                    "vim": {
+                        "test_data": {
+                            "test_record_replay_interleaved.json": "",
+                        }
+                    }
+                }
+            }),
+        )
+        .await;
+    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+    let (picker, _, cx) = build_find_picker(project, cx);
+
+    picker
+        .update_in(cx, |picker, window, cx| {
+            picker
+                .delegate
+                .spawn_search(test_path_position("live pla"), window, cx)
+        })
+        .await;
+    picker.update(cx, |picker, _| {
+        let matches = collect_search_matches(picker).search_matches_only();
+        assert!(!matches.is_empty(),);
+        assert_eq!(
+            matches[0].path.as_unix_str(),
+            "crates/livekit_client/src/livekit_client/playback.rs",
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_exact_filename_stem_preferred(cx: &mut TestAppContext) {
+    let app_state = init_test(cx);
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            "/src",
+            json!({
+                "assets": {
+                    "icons": {
+                        "file_icons": {
+                            "nix.svg": "",
+                        }
+                    }
+                },
+                "crates": {
+                    "zed": {
+                        "resources": {
+                            "app-icon-nightly@2x.png": "",
+                            "app-icon-preview@2x.png": "",
+                        }
+                    }
+                }
+            }),
+        )
+        .await;
+    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+    let (picker, _, cx) = build_find_picker(project, cx);
+
+    picker
+        .update_in(cx, |picker, window, cx| {
+            picker
+                .delegate
+                .spawn_search(test_path_position("nix icon"), window, cx)
+        })
+        .await;
+    picker.update(cx, |picker, _| {
+        let matches = collect_search_matches(picker).search_matches_only();
+        assert!(!matches.is_empty(),);
+        assert_eq!(
+            matches[0].path.as_unix_str(),
+            "assets/icons/file_icons/nix.svg",
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_exact_filename_with_directory_token(cx: &mut TestAppContext) {
+    let app_state = init_test(cx);
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            "/src",
+            json!({
+                "crates": {
+                    "agent_servers": {
+                        "src": {
+                            "acp.rs": "",
+                            "agent_server.rs": "",
+                            "custom.rs": "",
+                        }
+                    }
+                }
+            }),
+        )
+        .await;
+    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+    let (picker, _, cx) = build_find_picker(project, cx);
+
+    picker
+        .update_in(cx, |picker, window, cx| {
+            picker
+                .delegate
+                .spawn_search(test_path_position("acp server"), window, cx)
+        })
+        .await;
+    picker.update(cx, |picker, _| {
+        let matches = collect_search_matches(picker).search_matches_only();
+        assert!(!matches.is_empty(),);
+        assert_eq!(
+            matches[0].path.as_unix_str(),
+            "crates/agent_servers/src/acp.rs",
+        );
+    });
+}

crates/fs/src/fake_git_repo.rs πŸ”—

@@ -6,9 +6,10 @@ use git::{
     Oid, RunHook,
     blame::Blame,
     repository::{
-        AskPassDelegate, Branch, CommitDataReader, CommitDetails, CommitOptions, FetchOptions,
-        GRAPH_CHUNK_SIZE, GitRepository, GitRepositoryCheckpoint, InitialGraphCommitData, LogOrder,
-        LogSource, PushOptions, Remote, RepoPath, ResetMode, SearchCommitArgs, Worktree,
+        AskPassDelegate, Branch, CommitDataReader, CommitDetails, CommitOptions,
+        CreateWorktreeTarget, FetchOptions, GRAPH_CHUNK_SIZE, GitRepository,
+        GitRepositoryCheckpoint, InitialGraphCommitData, LogOrder, LogSource, PushOptions, Remote,
+        RepoPath, ResetMode, SearchCommitArgs, Worktree,
     },
     stash::GitStash,
     status::{
@@ -540,9 +541,8 @@ impl GitRepository for FakeGitRepository {
 
     fn create_worktree(
         &self,
-        branch_name: Option<String>,
+        target: CreateWorktreeTarget,
         path: PathBuf,
-        from_commit: Option<String>,
     ) -> BoxFuture<'_, Result<()>> {
         let fs = self.fs.clone();
         let executor = self.executor.clone();
@@ -550,30 +550,82 @@ impl GitRepository for FakeGitRepository {
         let common_dir_path = self.common_dir_path.clone();
         async move {
             executor.simulate_random_delay().await;
-            // Check for simulated error and duplicate branch before any side effects.
-            fs.with_git_state(&dot_git_path, false, |state| {
-                if let Some(message) = &state.simulated_create_worktree_error {
-                    anyhow::bail!("{message}");
-                }
-                if let Some(ref name) = branch_name {
-                    if state.branches.contains(name) {
-                        bail!("a branch named '{}' already exists", name);
+
+            let branch_name = target.branch_name().map(ToOwned::to_owned);
+            let create_branch_ref = matches!(target, CreateWorktreeTarget::NewBranch { .. });
+
+            // Check for simulated error and validate branch state before any side effects.
+            fs.with_git_state(&dot_git_path, false, {
+                let branch_name = branch_name.clone();
+                move |state| {
+                    if let Some(message) = &state.simulated_create_worktree_error {
+                        anyhow::bail!("{message}");
                     }
+
+                    match (create_branch_ref, branch_name.as_ref()) {
+                        (true, Some(branch_name)) => {
+                            if state.branches.contains(branch_name) {
+                                bail!("a branch named '{}' already exists", branch_name);
+                            }
+                        }
+                        (false, Some(branch_name)) => {
+                            if !state.branches.contains(branch_name) {
+                                bail!("no branch named '{}' exists", branch_name);
+                            }
+                        }
+                        (false, None) => {}
+                        (true, None) => bail!("branch name is required to create a branch"),
+                    }
+
+                    Ok(())
                 }
-                Ok(())
             })??;
 
+            let (branch_name, sha, create_branch_ref) = match target {
+                CreateWorktreeTarget::ExistingBranch { branch_name } => {
+                    let ref_name = format!("refs/heads/{branch_name}");
+                    let sha = fs.with_git_state(&dot_git_path, false, {
+                        move |state| {
+                            Ok::<_, anyhow::Error>(
+                                state
+                                    .refs
+                                    .get(&ref_name)
+                                    .cloned()
+                                    .unwrap_or_else(|| "fake-sha".to_string()),
+                            )
+                        }
+                    })??;
+                    (Some(branch_name), sha, false)
+                }
+                CreateWorktreeTarget::NewBranch {
+                    branch_name,
+                    base_sha: start_point,
+                } => (
+                    Some(branch_name),
+                    start_point.unwrap_or_else(|| "fake-sha".to_string()),
+                    true,
+                ),
+                CreateWorktreeTarget::Detached {
+                    base_sha: start_point,
+                } => (
+                    None,
+                    start_point.unwrap_or_else(|| "fake-sha".to_string()),
+                    false,
+                ),
+            };
+
             // Create the worktree checkout directory.
             fs.create_dir(&path).await?;
 
             // Create .git/worktrees/<name>/ directory with HEAD, commondir, gitdir.
-            let worktree_entry_name = branch_name
-                .as_deref()
-                .unwrap_or_else(|| path.file_name().unwrap().to_str().unwrap());
+            let worktree_entry_name = branch_name.as_deref().unwrap_or_else(|| {
+                path.file_name()
+                    .and_then(|name| name.to_str())
+                    .unwrap_or("detached")
+            });
             let worktrees_entry_dir = common_dir_path.join("worktrees").join(worktree_entry_name);
             fs.create_dir(&worktrees_entry_dir).await?;
 
-            let sha = from_commit.unwrap_or_else(|| "fake-sha".to_string());
             let head_content = if let Some(ref branch_name) = branch_name {
                 let ref_name = format!("refs/heads/{branch_name}");
                 format!("ref: {ref_name}")
@@ -604,15 +656,22 @@ impl GitRepository for FakeGitRepository {
                 false,
             )?;
 
-            // Update git state: add ref and branch.
-            fs.with_git_state(&dot_git_path, true, move |state| {
-                if let Some(branch_name) = branch_name {
-                    let ref_name = format!("refs/heads/{branch_name}");
-                    state.refs.insert(ref_name, sha);
-                    state.branches.insert(branch_name);
-                }
-                Ok::<(), anyhow::Error>(())
-            })??;
+            // Update git state for newly created branches.
+            if create_branch_ref {
+                fs.with_git_state(&dot_git_path, true, {
+                    let branch_name = branch_name.clone();
+                    let sha = sha.clone();
+                    move |state| {
+                        if let Some(branch_name) = branch_name {
+                            let ref_name = format!("refs/heads/{branch_name}");
+                            state.refs.insert(ref_name, sha);
+                            state.branches.insert(branch_name);
+                        }
+                        Ok::<(), anyhow::Error>(())
+                    }
+                })??;
+            }
+
             Ok(())
         }
         .boxed()

crates/fs/tests/integration/fake_git_repo.rs πŸ”—

@@ -24,9 +24,11 @@ async fn test_fake_worktree_lifecycle(cx: &mut TestAppContext) {
     // Create a worktree
     let worktree_1_dir = worktrees_dir.join("feature-branch");
     repo.create_worktree(
-        Some("feature-branch".to_string()),
+        git::repository::CreateWorktreeTarget::NewBranch {
+            branch_name: "feature-branch".to_string(),
+            base_sha: Some("abc123".to_string()),
+        },
         worktree_1_dir.clone(),
-        Some("abc123".to_string()),
     )
     .await
     .unwrap();
@@ -48,9 +50,11 @@ async fn test_fake_worktree_lifecycle(cx: &mut TestAppContext) {
     // Create a second worktree (without explicit commit)
     let worktree_2_dir = worktrees_dir.join("bugfix-branch");
     repo.create_worktree(
-        Some("bugfix-branch".to_string()),
+        git::repository::CreateWorktreeTarget::NewBranch {
+            branch_name: "bugfix-branch".to_string(),
+            base_sha: None,
+        },
         worktree_2_dir.clone(),
-        None,
     )
     .await
     .unwrap();

crates/fuzzy_nucleo/Cargo.toml πŸ”—

@@ -0,0 +1,21 @@
+[package]
+name = "fuzzy_nucleo"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/fuzzy_nucleo.rs"
+doctest = false
+
+[dependencies]
+nucleo.workspace = true
+gpui.workspace = true
+util.workspace = true
+
+[dev-dependencies]
+util = {workspace = true, features = ["test-support"]}

crates/fuzzy_nucleo/src/matcher.rs πŸ”—

@@ -0,0 +1,39 @@
+use std::sync::Mutex;
+
+static MATCHERS: Mutex<Vec<nucleo::Matcher>> = Mutex::new(Vec::new());
+
+pub const LENGTH_PENALTY: f64 = 0.01;
+
+pub fn get_matcher(config: nucleo::Config) -> nucleo::Matcher {
+    let mut matchers = MATCHERS.lock().unwrap();
+    match matchers.pop() {
+        Some(mut matcher) => {
+            matcher.config = config;
+            matcher
+        }
+        None => nucleo::Matcher::new(config),
+    }
+}
+
+pub fn return_matcher(matcher: nucleo::Matcher) {
+    MATCHERS.lock().unwrap().push(matcher);
+}
+
+pub fn get_matchers(n: usize, config: nucleo::Config) -> Vec<nucleo::Matcher> {
+    let mut matchers: Vec<_> = {
+        let mut pool = MATCHERS.lock().unwrap();
+        let available = pool.len().min(n);
+        pool.drain(..available)
+            .map(|mut matcher| {
+                matcher.config = config.clone();
+                matcher
+            })
+            .collect()
+    };
+    matchers.resize_with(n, || nucleo::Matcher::new(config.clone()));
+    matchers
+}
+
+pub fn return_matchers(mut matchers: Vec<nucleo::Matcher>) {
+    MATCHERS.lock().unwrap().append(&mut matchers);
+}

crates/fuzzy_nucleo/src/paths.rs πŸ”—

@@ -0,0 +1,352 @@
+use gpui::BackgroundExecutor;
+use std::{
+    cmp::Ordering,
+    sync::{
+        Arc,
+        atomic::{self, AtomicBool},
+    },
+};
+use util::{paths::PathStyle, rel_path::RelPath};
+
+use nucleo::Utf32Str;
+use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
+
+use crate::matcher::{self, LENGTH_PENALTY};
+
+#[derive(Clone, Debug)]
+pub struct PathMatchCandidate<'a> {
+    pub is_dir: bool,
+    pub path: &'a RelPath,
+}
+
+#[derive(Clone, Debug)]
+pub struct PathMatch {
+    pub score: f64,
+    pub positions: Vec<usize>,
+    pub worktree_id: usize,
+    pub path: Arc<RelPath>,
+    pub path_prefix: Arc<RelPath>,
+    pub is_dir: bool,
+    /// Number of steps removed from a shared parent with the relative path
+    /// Used to order closer paths first in the search list
+    pub distance_to_relative_ancestor: usize,
+}
+
+pub trait PathMatchCandidateSet<'a>: Send + Sync {
+    type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
+    fn id(&self) -> usize;
+    fn len(&self) -> usize;
+    fn is_empty(&self) -> bool {
+        self.len() == 0
+    }
+    fn root_is_file(&self) -> bool;
+    fn prefix(&self) -> Arc<RelPath>;
+    fn candidates(&'a self, start: usize) -> Self::Candidates;
+    fn path_style(&self) -> PathStyle;
+}
+
+impl PartialEq for PathMatch {
+    fn eq(&self, other: &Self) -> bool {
+        self.cmp(other).is_eq()
+    }
+}
+
+impl Eq for PathMatch {}
+
+impl PartialOrd for PathMatch {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for PathMatch {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.score
+            .partial_cmp(&other.score)
+            .unwrap_or(Ordering::Equal)
+            .then_with(|| self.worktree_id.cmp(&other.worktree_id))
+            .then_with(|| {
+                other
+                    .distance_to_relative_ancestor
+                    .cmp(&self.distance_to_relative_ancestor)
+            })
+            .then_with(|| self.path.cmp(&other.path))
+    }
+}
+
+fn make_atoms(query: &str, smart_case: bool) -> Vec<Atom> {
+    let case = if smart_case {
+        CaseMatching::Smart
+    } else {
+        CaseMatching::Ignore
+    };
+    query
+        .split_whitespace()
+        .map(|word| Atom::new(word, case, Normalization::Smart, AtomKind::Fuzzy, false))
+        .collect()
+}
+
+pub(crate) fn distance_between_paths(path: &RelPath, relative_to: &RelPath) -> usize {
+    let mut path_components = path.components();
+    let mut relative_components = relative_to.components();
+
+    while path_components
+        .next()
+        .zip(relative_components.next())
+        .map(|(path_component, relative_component)| path_component == relative_component)
+        .unwrap_or_default()
+    {}
+    path_components.count() + relative_components.count() + 1
+}
+
+fn get_filename_match_bonus(
+    candidate_buf: &str,
+    query_atoms: &[Atom],
+    matcher: &mut nucleo::Matcher,
+) -> f64 {
+    let filename = match std::path::Path::new(candidate_buf).file_name() {
+        Some(f) => f.to_str().unwrap_or(""),
+        None => return 0.0,
+    };
+    if filename.is_empty() || query_atoms.is_empty() {
+        return 0.0;
+    }
+    let mut buf = Vec::new();
+    let haystack = Utf32Str::new(filename, &mut buf);
+    let mut total_score = 0u32;
+    for atom in query_atoms {
+        if let Some(score) = atom.score(haystack, matcher) {
+            total_score = total_score.saturating_add(score as u32);
+        }
+    }
+    total_score as f64 / filename.len().max(1) as f64
+}
+struct Cancelled;
+
+fn path_match_helper<'a>(
+    matcher: &mut nucleo::Matcher,
+    atoms: &[Atom],
+    candidates: impl Iterator<Item = PathMatchCandidate<'a>>,
+    results: &mut Vec<PathMatch>,
+    worktree_id: usize,
+    path_prefix: &Arc<RelPath>,
+    root_is_file: bool,
+    relative_to: &Option<Arc<RelPath>>,
+    path_style: PathStyle,
+    cancel_flag: &AtomicBool,
+) -> Result<(), Cancelled> {
+    let mut candidate_buf = if !path_prefix.is_empty() && !root_is_file {
+        let mut s = path_prefix.display(path_style).to_string();
+        s.push_str(path_style.primary_separator());
+        s
+    } else {
+        String::new()
+    };
+    let path_prefix_len = candidate_buf.len();
+    let mut buf = Vec::new();
+    let mut matched_chars: Vec<u32> = Vec::new();
+    let mut atom_matched_chars = Vec::new();
+    for candidate in candidates {
+        buf.clear();
+        matched_chars.clear();
+        if cancel_flag.load(atomic::Ordering::Relaxed) {
+            return Err(Cancelled);
+        }
+
+        candidate_buf.truncate(path_prefix_len);
+        if root_is_file {
+            candidate_buf.push_str(path_prefix.as_unix_str());
+        } else {
+            candidate_buf.push_str(candidate.path.as_unix_str());
+        }
+
+        let haystack = Utf32Str::new(&candidate_buf, &mut buf);
+
+        let mut total_score: u32 = 0;
+        let mut all_matched = true;
+
+        for atom in atoms {
+            atom_matched_chars.clear();
+            if let Some(score) = atom.indices(haystack, matcher, &mut atom_matched_chars) {
+                total_score = total_score.saturating_add(score as u32);
+                matched_chars.extend_from_slice(&atom_matched_chars);
+            } else {
+                all_matched = false;
+                break;
+            }
+        }
+
+        if all_matched && !atoms.is_empty() {
+            matched_chars.sort_unstable();
+            matched_chars.dedup();
+
+            let length_penalty = candidate_buf.len() as f64 * LENGTH_PENALTY;
+            let filename_bonus = get_filename_match_bonus(&candidate_buf, atoms, matcher);
+            let adjusted_score = total_score as f64 + filename_bonus - length_penalty;
+            let mut positions: Vec<usize> = candidate_buf
+                .char_indices()
+                .enumerate()
+                .filter_map(|(char_offset, (byte_offset, _))| {
+                    matched_chars
+                        .contains(&(char_offset as u32))
+                        .then_some(byte_offset)
+                })
+                .collect();
+            positions.sort_unstable();
+
+            results.push(PathMatch {
+                score: adjusted_score,
+                positions,
+                worktree_id,
+                path: if root_is_file {
+                    Arc::clone(path_prefix)
+                } else {
+                    candidate.path.into()
+                },
+                path_prefix: if root_is_file {
+                    RelPath::empty().into()
+                } else {
+                    Arc::clone(path_prefix)
+                },
+                is_dir: candidate.is_dir,
+                distance_to_relative_ancestor: relative_to
+                    .as_ref()
+                    .map_or(usize::MAX, |relative_to| {
+                        distance_between_paths(candidate.path, relative_to.as_ref())
+                    }),
+            });
+        }
+    }
+    Ok(())
+}
+
+pub fn match_fixed_path_set(
+    candidates: Vec<PathMatchCandidate>,
+    worktree_id: usize,
+    worktree_root_name: Option<Arc<RelPath>>,
+    query: &str,
+    smart_case: bool,
+    max_results: usize,
+    path_style: PathStyle,
+) -> Vec<PathMatch> {
+    let mut config = nucleo::Config::DEFAULT;
+    config.set_match_paths();
+    let mut matcher = matcher::get_matcher(config);
+
+    let atoms = make_atoms(query, smart_case);
+
+    let root_is_file = worktree_root_name.is_some() && candidates.iter().all(|c| c.path.is_empty());
+
+    let path_prefix = worktree_root_name.unwrap_or_else(|| RelPath::empty().into());
+
+    let mut results = Vec::new();
+
+    path_match_helper(
+        &mut matcher,
+        &atoms,
+        candidates.into_iter(),
+        &mut results,
+        worktree_id,
+        &path_prefix,
+        root_is_file,
+        &None,
+        path_style,
+        &AtomicBool::new(false),
+    )
+    .ok();
+    util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
+    matcher::return_matcher(matcher);
+    results
+}
+
+pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
+    candidate_sets: &'a [Set],
+    query: &str,
+    relative_to: &Option<Arc<RelPath>>,
+    smart_case: bool,
+    max_results: usize,
+    cancel_flag: &AtomicBool,
+    executor: BackgroundExecutor,
+) -> Vec<PathMatch> {
+    let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
+    if path_count == 0 {
+        return Vec::new();
+    }
+
+    let path_style = candidate_sets[0].path_style();
+
+    let query = if path_style.is_windows() {
+        query.replace('\\', "/")
+    } else {
+        query.to_owned()
+    };
+
+    let atoms = make_atoms(&query, smart_case);
+
+    let num_cpus = executor.num_cpus().min(path_count);
+    let segment_size = path_count.div_ceil(num_cpus);
+    let mut segment_results = (0..num_cpus)
+        .map(|_| Vec::with_capacity(max_results))
+        .collect::<Vec<_>>();
+    let mut config = nucleo::Config::DEFAULT;
+    config.set_match_paths();
+    let mut matchers = matcher::get_matchers(num_cpus, config);
+    executor
+        .scoped(|scope| {
+            for (segment_idx, (results, matcher)) in segment_results
+                .iter_mut()
+                .zip(matchers.iter_mut())
+                .enumerate()
+            {
+                let atoms = atoms.clone();
+                let relative_to = relative_to.clone();
+                scope.spawn(async move {
+                    let segment_start = segment_idx * segment_size;
+                    let segment_end = segment_start + segment_size;
+
+                    let mut tree_start = 0;
+                    for candidate_set in candidate_sets {
+                        let tree_end = tree_start + candidate_set.len();
+
+                        if tree_start < segment_end && segment_start < tree_end {
+                            let start = tree_start.max(segment_start) - tree_start;
+                            let end = tree_end.min(segment_end) - tree_start;
+                            let candidates = candidate_set.candidates(start).take(end - start);
+
+                            if path_match_helper(
+                                matcher,
+                                &atoms,
+                                candidates,
+                                results,
+                                candidate_set.id(),
+                                &candidate_set.prefix(),
+                                candidate_set.root_is_file(),
+                                &relative_to,
+                                path_style,
+                                cancel_flag,
+                            )
+                            .is_err()
+                            {
+                                break;
+                            }
+                        }
+
+                        if tree_end >= segment_end {
+                            break;
+                        }
+                        tree_start = tree_end;
+                    }
+                });
+            }
+        })
+        .await;
+
+    matcher::return_matchers(matchers);
+    if cancel_flag.load(atomic::Ordering::Acquire) {
+        return Vec::new();
+    }
+
+    let mut results = segment_results.concat();
+    util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
+    results
+}

crates/git/src/repository.rs πŸ”—

@@ -241,20 +241,57 @@ pub struct Worktree {
     pub is_main: bool,
 }
 
+/// Describes how a new worktree should choose or create its checked-out HEAD.
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub enum CreateWorktreeTarget {
+    /// Check out an existing local branch in the new worktree.
+    ExistingBranch {
+        /// The existing local branch to check out.
+        branch_name: String,
+    },
+    /// Create a new local branch for the new worktree.
+    NewBranch {
+        /// The new local branch to create and check out.
+        branch_name: String,
+        /// The commit or ref to create the branch from. Uses `HEAD` when `None`.
+        base_sha: Option<String>,
+    },
+    /// Check out a commit or ref in detached HEAD state.
+    Detached {
+        /// The commit or ref to check out. Uses `HEAD` when `None`.
+        base_sha: Option<String>,
+    },
+}
+
+impl CreateWorktreeTarget {
+    pub fn branch_name(&self) -> Option<&str> {
+        match self {
+            Self::ExistingBranch { branch_name } | Self::NewBranch { branch_name, .. } => {
+                Some(branch_name)
+            }
+            Self::Detached { .. } => None,
+        }
+    }
+}
+
 impl Worktree {
+    /// Returns the branch name if the worktree is attached to a branch.
+    pub fn branch_name(&self) -> Option<&str> {
+        self.ref_name.as_ref().map(|ref_name| {
+            ref_name
+                .strip_prefix("refs/heads/")
+                .or_else(|| ref_name.strip_prefix("refs/remotes/"))
+                .unwrap_or(ref_name)
+        })
+    }
+
     /// Returns a display name for the worktree, suitable for use in the UI.
     ///
     /// If the worktree is attached to a branch, returns the branch name.
     /// Otherwise, returns the short SHA of the worktree's HEAD commit.
     pub fn display_name(&self) -> &str {
-        match self.ref_name {
-            Some(ref ref_name) => ref_name
-                .strip_prefix("refs/heads/")
-                .or_else(|| ref_name.strip_prefix("refs/remotes/"))
-                .unwrap_or(ref_name),
-            // Detached HEAD β€” show the short SHA as a fallback.
-            None => &self.sha[..self.sha.len().min(SHORT_SHA_LENGTH)],
-        }
+        self.branch_name()
+            .unwrap_or(&self.sha[..self.sha.len().min(SHORT_SHA_LENGTH)])
     }
 }
 
@@ -716,9 +753,8 @@ pub trait GitRepository: Send + Sync {
 
     fn create_worktree(
         &self,
-        branch_name: Option<String>,
+        target: CreateWorktreeTarget,
         path: PathBuf,
-        from_commit: Option<String>,
     ) -> BoxFuture<'_, Result<()>>;
 
     fn remove_worktree(&self, path: PathBuf, force: bool) -> BoxFuture<'_, Result<()>>;
@@ -1667,24 +1703,36 @@ impl GitRepository for RealGitRepository {
 
     fn create_worktree(
         &self,
-        branch_name: Option<String>,
+        target: CreateWorktreeTarget,
         path: PathBuf,
-        from_commit: Option<String>,
     ) -> BoxFuture<'_, Result<()>> {
         let git_binary = self.git_binary();
         let mut args = vec![OsString::from("worktree"), OsString::from("add")];
-        if let Some(branch_name) = &branch_name {
-            args.push(OsString::from("-b"));
-            args.push(OsString::from(branch_name.as_str()));
-        } else {
-            args.push(OsString::from("--detach"));
-        }
-        args.push(OsString::from("--"));
-        args.push(OsString::from(path.as_os_str()));
-        if let Some(from_commit) = from_commit {
-            args.push(OsString::from(from_commit));
-        } else {
-            args.push(OsString::from("HEAD"));
+
+        match &target {
+            CreateWorktreeTarget::ExistingBranch { branch_name } => {
+                args.push(OsString::from("--"));
+                args.push(OsString::from(path.as_os_str()));
+                args.push(OsString::from(branch_name));
+            }
+            CreateWorktreeTarget::NewBranch {
+                branch_name,
+                base_sha: start_point,
+            } => {
+                args.push(OsString::from("-b"));
+                args.push(OsString::from(branch_name));
+                args.push(OsString::from("--"));
+                args.push(OsString::from(path.as_os_str()));
+                args.push(OsString::from(start_point.as_deref().unwrap_or("HEAD")));
+            }
+            CreateWorktreeTarget::Detached {
+                base_sha: start_point,
+            } => {
+                args.push(OsString::from("--detach"));
+                args.push(OsString::from("--"));
+                args.push(OsString::from(path.as_os_str()));
+                args.push(OsString::from(start_point.as_deref().unwrap_or("HEAD")));
+            }
         }
 
         self.executor
@@ -4054,9 +4102,11 @@ mod tests {
 
         // Create a new worktree
         repo.create_worktree(
-            Some("test-branch".to_string()),
+            CreateWorktreeTarget::NewBranch {
+                branch_name: "test-branch".to_string(),
+                base_sha: Some("HEAD".to_string()),
+            },
             worktree_path.clone(),
-            Some("HEAD".to_string()),
         )
         .await
         .unwrap();
@@ -4113,9 +4163,11 @@ mod tests {
         // Create a worktree
         let worktree_path = worktrees_dir.join("worktree-to-remove");
         repo.create_worktree(
-            Some("to-remove".to_string()),
+            CreateWorktreeTarget::NewBranch {
+                branch_name: "to-remove".to_string(),
+                base_sha: Some("HEAD".to_string()),
+            },
             worktree_path.clone(),
-            Some("HEAD".to_string()),
         )
         .await
         .unwrap();
@@ -4137,9 +4189,11 @@ mod tests {
         // Create a worktree
         let worktree_path = worktrees_dir.join("dirty-wt");
         repo.create_worktree(
-            Some("dirty-wt".to_string()),
+            CreateWorktreeTarget::NewBranch {
+                branch_name: "dirty-wt".to_string(),
+                base_sha: Some("HEAD".to_string()),
+            },
             worktree_path.clone(),
-            Some("HEAD".to_string()),
         )
         .await
         .unwrap();
@@ -4207,9 +4261,11 @@ mod tests {
         // Create a worktree
         let old_path = worktrees_dir.join("old-worktree-name");
         repo.create_worktree(
-            Some("old-name".to_string()),
+            CreateWorktreeTarget::NewBranch {
+                branch_name: "old-name".to_string(),
+                base_sha: Some("HEAD".to_string()),
+            },
             old_path.clone(),
-            Some("HEAD".to_string()),
         )
         .await
         .unwrap();

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

@@ -318,8 +318,13 @@ impl WorktreeListDelegate {
                     .clone();
                 let new_worktree_path =
                     repo.path_for_new_linked_worktree(&branch, &worktree_directory_setting)?;
-                let receiver =
-                    repo.create_worktree(branch.clone(), new_worktree_path.clone(), commit);
+                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??;

crates/http_client/src/github_download.rs πŸ”—

@@ -207,11 +207,7 @@ async fn extract_tar_gz(
     from: impl AsyncRead + Unpin,
 ) -> Result<(), anyhow::Error> {
     let decompressed_bytes = GzipDecoder::new(BufReader::new(from));
-    let archive = async_tar::Archive::new(decompressed_bytes);
-    archive
-        .unpack(&destination_path)
-        .await
-        .with_context(|| format!("extracting {url} to {destination_path:?}"))?;
+    unpack_tar_archive(destination_path, url, decompressed_bytes).await?;
     Ok(())
 }
 
@@ -221,7 +217,21 @@ async fn extract_tar_bz2(
     from: impl AsyncRead + Unpin,
 ) -> Result<(), anyhow::Error> {
     let decompressed_bytes = BzDecoder::new(BufReader::new(from));
-    let archive = async_tar::Archive::new(decompressed_bytes);
+    unpack_tar_archive(destination_path, url, decompressed_bytes).await?;
+    Ok(())
+}
+
+async fn unpack_tar_archive(
+    destination_path: &Path,
+    url: &str,
+    archive_bytes: impl AsyncRead + Unpin,
+) -> Result<(), anyhow::Error> {
+    // We don't need to set the modified time. It's irrelevant to downloaded
+    // archive verification, and some filesystems return errors when asked to
+    // apply it after extraction.
+    let archive = async_tar::ArchiveBuilder::new(archive_bytes)
+        .set_preserve_mtime(false)
+        .build();
     archive
         .unpack(&destination_path)
         .await

crates/markdown/src/html/html_parser.rs πŸ”—

@@ -1,6 +1,6 @@
 use std::{cell::RefCell, collections::HashMap, mem, ops::Range};
 
-use gpui::{DefiniteLength, FontWeight, SharedString, px, relative};
+use gpui::{DefiniteLength, FontWeight, SharedString, TextAlign, px, relative};
 use html5ever::{
     Attribute, LocalName, ParseOpts, local_name, parse_document, tendril::TendrilSink,
 };
@@ -24,10 +24,17 @@ pub(crate) enum ParsedHtmlElement {
     List(ParsedHtmlList),
     Table(ParsedHtmlTable),
     BlockQuote(ParsedHtmlBlockQuote),
-    Paragraph(HtmlParagraph),
+    Paragraph(ParsedHtmlParagraph),
     Image(HtmlImage),
 }
 
+#[derive(Debug, Clone)]
+#[cfg_attr(test, derive(PartialEq))]
+pub(crate) struct ParsedHtmlParagraph {
+    pub text_align: Option<TextAlign>,
+    pub contents: HtmlParagraph,
+}
+
 impl ParsedHtmlElement {
     pub fn source_range(&self) -> Option<Range<usize>> {
         Some(match self {
@@ -35,7 +42,7 @@ impl ParsedHtmlElement {
             Self::List(list) => list.source_range.clone(),
             Self::Table(table) => table.source_range.clone(),
             Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
-            Self::Paragraph(text) => match text.first()? {
+            Self::Paragraph(paragraph) => match paragraph.contents.first()? {
                 HtmlParagraphChunk::Text(text) => text.source_range.clone(),
                 HtmlParagraphChunk::Image(image) => image.source_range.clone(),
             },
@@ -83,6 +90,7 @@ pub(crate) struct ParsedHtmlHeading {
     pub source_range: Range<usize>,
     pub level: HeadingLevel,
     pub contents: HtmlParagraph,
+    pub text_align: Option<TextAlign>,
 }
 
 #[derive(Debug, Clone)]
@@ -236,20 +244,21 @@ fn parse_html_node(
             consume_children(source_range, node, elements, context);
         }
         NodeData::Text { contents } => {
-            elements.push(ParsedHtmlElement::Paragraph(vec![
-                HtmlParagraphChunk::Text(ParsedHtmlText {
+            elements.push(ParsedHtmlElement::Paragraph(ParsedHtmlParagraph {
+                text_align: None,
+                contents: vec![HtmlParagraphChunk::Text(ParsedHtmlText {
                     source_range,
                     highlights: Vec::default(),
                     links: Vec::default(),
                     contents: contents.borrow().to_string().into(),
-                }),
-            ]));
+                })],
+            }));
         }
         NodeData::Comment { .. } => {}
         NodeData::Element { name, attrs, .. } => {
-            let mut styles = if let Some(styles) =
-                html_style_from_html_styles(extract_styles_from_attributes(attrs))
-            {
+            let styles_map = extract_styles_from_attributes(attrs);
+            let text_align = text_align_from_attributes(attrs, &styles_map);
+            let mut styles = if let Some(styles) = html_style_from_html_styles(styles_map) {
                 vec![styles]
             } else {
                 Vec::default()
@@ -270,7 +279,10 @@ fn parse_html_node(
                 );
 
                 if !paragraph.is_empty() {
-                    elements.push(ParsedHtmlElement::Paragraph(paragraph));
+                    elements.push(ParsedHtmlElement::Paragraph(ParsedHtmlParagraph {
+                        text_align,
+                        contents: paragraph,
+                    }));
                 }
             } else if matches!(
                 name.local,
@@ -303,6 +315,7 @@ fn parse_html_node(
                             _ => unreachable!(),
                         },
                         contents: paragraph,
+                        text_align,
                     }));
                 }
             } else if name.local == local_name!("ul") || name.local == local_name!("ol") {
@@ -589,6 +602,30 @@ fn html_style_from_html_styles(styles: HashMap<String, String>) -> Option<HtmlHi
     }
 }
 
+fn parse_text_align(value: &str) -> Option<TextAlign> {
+    match value.trim().to_ascii_lowercase().as_str() {
+        "left" => Some(TextAlign::Left),
+        "center" => Some(TextAlign::Center),
+        "right" => Some(TextAlign::Right),
+        _ => None,
+    }
+}
+
+fn text_align_from_styles(styles: &HashMap<String, String>) -> Option<TextAlign> {
+    styles
+        .get("text-align")
+        .and_then(|value| parse_text_align(value))
+}
+
+fn text_align_from_attributes(
+    attrs: &RefCell<Vec<Attribute>>,
+    styles: &HashMap<String, String>,
+) -> Option<TextAlign> {
+    text_align_from_styles(styles).or_else(|| {
+        attr_value(attrs, local_name!("align")).and_then(|value| parse_text_align(&value))
+    })
+}
+
 fn extract_styles_from_attributes(attrs: &RefCell<Vec<Attribute>>) -> HashMap<String, String> {
     let mut styles = HashMap::new();
 
@@ -770,6 +807,7 @@ fn extract_html_table(node: &Node, source_range: Range<usize>) -> Option<ParsedH
 #[cfg(test)]
 mod tests {
     use super::*;
+    use gpui::TextAlign;
 
     #[test]
     fn parses_html_styled_text() {
@@ -783,7 +821,7 @@ mod tests {
         let ParsedHtmlElement::Paragraph(paragraph) = &parsed.children[0] else {
             panic!("expected paragraph");
         };
-        let HtmlParagraphChunk::Text(text) = &paragraph[0] else {
+        let HtmlParagraphChunk::Text(text) = &paragraph.contents[0] else {
             panic!("expected text chunk");
         };
 
@@ -851,7 +889,7 @@ mod tests {
         let ParsedHtmlElement::Paragraph(paragraph) = &first_item.content[0] else {
             panic!("expected first item paragraph");
         };
-        let HtmlParagraphChunk::Text(text) = &paragraph[0] else {
+        let HtmlParagraphChunk::Text(text) = &paragraph.contents[0] else {
             panic!("expected first item text");
         };
         assert_eq!(text.contents.as_ref(), "parent");
@@ -866,7 +904,7 @@ mod tests {
         else {
             panic!("expected nested item paragraph");
         };
-        let HtmlParagraphChunk::Text(nested_text) = &nested_paragraph[0] else {
+        let HtmlParagraphChunk::Text(nested_text) = &nested_paragraph.contents[0] else {
             panic!("expected nested item text");
         };
         assert_eq!(nested_text.contents.as_ref(), "child");
@@ -875,9 +913,58 @@ mod tests {
         let ParsedHtmlElement::Paragraph(second_paragraph) = &second_item.content[0] else {
             panic!("expected second item paragraph");
         };
-        let HtmlParagraphChunk::Text(second_text) = &second_paragraph[0] else {
+        let HtmlParagraphChunk::Text(second_text) = &second_paragraph.contents[0] else {
             panic!("expected second item text");
         };
         assert_eq!(second_text.contents.as_ref(), "sibling");
     }
+
+    #[test]
+    fn parses_paragraph_text_align_from_style() {
+        let parsed = parse_html_block("<p style=\"text-align: center\">x</p>", 0..40).unwrap();
+        let ParsedHtmlElement::Paragraph(paragraph) = &parsed.children[0] else {
+            panic!("expected paragraph");
+        };
+        assert_eq!(paragraph.text_align, Some(TextAlign::Center));
+    }
+
+    #[test]
+    fn parses_heading_text_align_from_style() {
+        let parsed = parse_html_block("<h2 style=\"text-align: right\">Title</h2>", 0..45).unwrap();
+        let ParsedHtmlElement::Heading(heading) = &parsed.children[0] else {
+            panic!("expected heading");
+        };
+        assert_eq!(heading.text_align, Some(TextAlign::Right));
+    }
+
+    #[test]
+    fn parses_paragraph_text_align_from_align_attribute() {
+        let parsed = parse_html_block("<p align=\"center\">x</p>", 0..24).unwrap();
+        let ParsedHtmlElement::Paragraph(paragraph) = &parsed.children[0] else {
+            panic!("expected paragraph");
+        };
+        assert_eq!(paragraph.text_align, Some(TextAlign::Center));
+    }
+
+    #[test]
+    fn parses_heading_text_align_from_align_attribute() {
+        let parsed = parse_html_block("<h2 align=\"right\">Title</h2>", 0..30).unwrap();
+        let ParsedHtmlElement::Heading(heading) = &parsed.children[0] else {
+            panic!("expected heading");
+        };
+        assert_eq!(heading.text_align, Some(TextAlign::Right));
+    }
+
+    #[test]
+    fn prefers_style_text_align_over_align_attribute() {
+        let parsed = parse_html_block(
+            "<p align=\"left\" style=\"text-align: center\">x</p>",
+            0..50,
+        )
+        .unwrap();
+        let ParsedHtmlElement::Paragraph(paragraph) = &parsed.children[0] else {
+            panic!("expected paragraph");
+        };
+        assert_eq!(paragraph.text_align, Some(TextAlign::Center));
+    }
 }

crates/markdown/src/html/html_rendering.rs πŸ”—

@@ -79,9 +79,20 @@ impl MarkdownElement {
 
         match element {
             ParsedHtmlElement::Paragraph(paragraph) => {
-                self.push_markdown_paragraph(builder, &source_range, markdown_end);
-                self.render_html_paragraph(paragraph, source_allocator, builder, cx, markdown_end);
-                builder.pop_div();
+                self.push_markdown_paragraph(
+                    builder,
+                    &source_range,
+                    markdown_end,
+                    paragraph.text_align,
+                );
+                self.render_html_paragraph(
+                    &paragraph.contents,
+                    source_allocator,
+                    builder,
+                    cx,
+                    markdown_end,
+                );
+                self.pop_markdown_paragraph(builder);
             }
             ParsedHtmlElement::Heading(heading) => {
                 self.push_markdown_heading(
@@ -89,6 +100,7 @@ impl MarkdownElement {
                     heading.level,
                     &heading.source_range,
                     markdown_end,
+                    heading.text_align,
                 );
                 self.render_html_paragraph(
                     &heading.contents,

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

@@ -36,8 +36,8 @@ use gpui::{
     FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image,
     ImageFormat, ImageSource, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent,
     MouseMoveEvent, MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle,
-    StyleRefinement, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement,
-    actions, img, point, quad,
+    StyleRefinement, StyledText, Task, TextAlign, TextLayout, TextRun, TextStyle,
+    TextStyleRefinement, actions, img, point, quad,
 };
 use language::{CharClassifier, Language, LanguageRegistry, Rope};
 use parser::CodeBlockMetadata;
@@ -1025,8 +1025,17 @@ impl MarkdownElement {
         width: Option<DefiniteLength>,
         height: Option<DefiniteLength>,
     ) {
+        let align = builder.text_style().text_align;
         builder.modify_current_div(|el| {
-            el.items_center().flex().flex_row().child(
+            let mut image_container = el.flex().flex_row().items_center();
+
+            image_container = match align {
+                TextAlign::Left => image_container.justify_start(),
+                TextAlign::Center => image_container.justify_center(),
+                TextAlign::Right => image_container.justify_end(),
+            };
+
+            image_container.child(
                 img(source)
                     .max_w_full()
                     .when_some(height, |this, height| this.h(height))
@@ -1041,14 +1050,29 @@ impl MarkdownElement {
         builder: &mut MarkdownElementBuilder,
         range: &Range<usize>,
         markdown_end: usize,
+        text_align_override: Option<TextAlign>,
     ) {
-        builder.push_div(
-            div().when(!self.style.height_is_multiple_of_line_height, |el| {
-                el.mb_2().line_height(rems(1.3))
-            }),
-            range,
-            markdown_end,
-        );
+        let align = text_align_override.unwrap_or(self.style.base_text_style.text_align);
+        let mut paragraph = div().when(!self.style.height_is_multiple_of_line_height, |el| {
+            el.mb_2().line_height(rems(1.3))
+        });
+
+        paragraph = match align {
+            TextAlign::Center => paragraph.text_center(),
+            TextAlign::Left => paragraph.text_left(),
+            TextAlign::Right => paragraph.text_right(),
+        };
+
+        builder.push_text_style(TextStyleRefinement {
+            text_align: Some(align),
+            ..Default::default()
+        });
+        builder.push_div(paragraph, range, markdown_end);
+    }
+
+    fn pop_markdown_paragraph(&self, builder: &mut MarkdownElementBuilder) {
+        builder.pop_div();
+        builder.pop_text_style();
     }
 
     fn push_markdown_heading(
@@ -1057,15 +1081,26 @@ impl MarkdownElement {
         level: pulldown_cmark::HeadingLevel,
         range: &Range<usize>,
         markdown_end: usize,
+        text_align_override: Option<TextAlign>,
     ) {
+        let align = text_align_override.unwrap_or(self.style.base_text_style.text_align);
         let mut heading = div().mb_2();
         heading = apply_heading_style(heading, level, self.style.heading_level_styles.as_ref());
 
+        heading = match align {
+            TextAlign::Center => heading.text_center(),
+            TextAlign::Left => heading.text_left(),
+            TextAlign::Right => heading.text_right(),
+        };
+
         let mut heading_style = self.style.heading.clone();
         let heading_text_style = heading_style.text_style().clone();
         heading.style().refine(&heading_style);
 
-        builder.push_text_style(heading_text_style);
+        builder.push_text_style(TextStyleRefinement {
+            text_align: Some(align),
+            ..heading_text_style
+        });
         builder.push_div(heading, range, markdown_end);
     }
 
@@ -1571,10 +1606,16 @@ impl Element for MarkdownElement {
                             }
                         }
                         MarkdownTag::Paragraph => {
-                            self.push_markdown_paragraph(&mut builder, range, markdown_end);
+                            self.push_markdown_paragraph(&mut builder, range, markdown_end, None);
                         }
                         MarkdownTag::Heading { level, .. } => {
-                            self.push_markdown_heading(&mut builder, *level, range, markdown_end);
+                            self.push_markdown_heading(
+                                &mut builder,
+                                *level,
+                                range,
+                                markdown_end,
+                                None,
+                            );
                         }
                         MarkdownTag::BlockQuote => {
                             self.push_markdown_block_quote(&mut builder, range, markdown_end);
@@ -1826,7 +1867,7 @@ impl Element for MarkdownElement {
                         current_img_block_range.take();
                     }
                     MarkdownTagEnd::Paragraph => {
-                        builder.pop_div();
+                        self.pop_markdown_paragraph(&mut builder);
                     }
                     MarkdownTagEnd::Heading(_) => {
                         self.pop_markdown_heading(&mut builder);

crates/project/Cargo.toml πŸ”—

@@ -52,6 +52,7 @@ fancy-regex.workspace = true
 fs.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
+fuzzy_nucleo.workspace = true
 git.workspace = true
 git_hosting_providers.workspace = true
 globset.workspace = true

crates/project/src/git_store.rs πŸ”—

@@ -32,10 +32,10 @@ use git::{
     blame::Blame,
     parse_git_remote_url,
     repository::{
-        Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, DiffType, FetchOptions,
-        GitRepository, GitRepositoryCheckpoint, GraphCommitData, InitialGraphCommitData, LogOrder,
-        LogSource, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode, SearchCommitArgs,
-        UpstreamTrackingStatus, Worktree as GitWorktree,
+        Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, CreateWorktreeTarget,
+        DiffType, FetchOptions, GitRepository, GitRepositoryCheckpoint, GraphCommitData,
+        InitialGraphCommitData, LogOrder, LogSource, PushOptions, Remote, RemoteCommandOutput,
+        RepoPath, ResetMode, SearchCommitArgs, UpstreamTrackingStatus, Worktree as GitWorktree,
     },
     stash::{GitStash, StashEntry},
     status::{
@@ -329,12 +329,6 @@ pub struct GraphDataResponse<'a> {
     pub error: Option<SharedString>,
 }
 
-#[derive(Clone, Debug)]
-enum CreateWorktreeStartPoint {
-    Detached,
-    Branched { name: String },
-}
-
 pub struct Repository {
     this: WeakEntity<Self>,
     snapshot: RepositorySnapshot,
@@ -2414,18 +2408,23 @@ impl GitStore {
         let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
         let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
         let directory = PathBuf::from(envelope.payload.directory);
-        let start_point = if envelope.payload.name.is_empty() {
-            CreateWorktreeStartPoint::Detached
+        let name = envelope.payload.name;
+        let commit = envelope.payload.commit;
+        let use_existing_branch = envelope.payload.use_existing_branch;
+        let target = if name.is_empty() {
+            CreateWorktreeTarget::Detached { base_sha: commit }
+        } else if use_existing_branch {
+            CreateWorktreeTarget::ExistingBranch { branch_name: name }
         } else {
-            CreateWorktreeStartPoint::Branched {
-                name: envelope.payload.name,
+            CreateWorktreeTarget::NewBranch {
+                branch_name: name,
+                base_sha: commit,
             }
         };
-        let commit = envelope.payload.commit;
 
         repository_handle
             .update(&mut cx, |repository_handle, _| {
-                repository_handle.create_worktree_with_start_point(start_point, directory, commit)
+                repository_handle.create_worktree(target, directory)
             })
             .await??;
 
@@ -6004,50 +6003,43 @@ impl Repository {
         })
     }
 
-    fn create_worktree_with_start_point(
+    pub fn create_worktree(
         &mut self,
-        start_point: CreateWorktreeStartPoint,
+        target: CreateWorktreeTarget,
         path: PathBuf,
-        commit: Option<String>,
     ) -> oneshot::Receiver<Result<()>> {
-        if matches!(
-            &start_point,
-            CreateWorktreeStartPoint::Branched { name } if name.is_empty()
-        ) {
-            let (sender, receiver) = oneshot::channel();
-            sender
-                .send(Err(anyhow!("branch name cannot be empty")))
-                .ok();
-            return receiver;
-        }
-
         let id = self.id;
-        let message = match &start_point {
-            CreateWorktreeStartPoint::Detached => "git worktree add (detached)".into(),
-            CreateWorktreeStartPoint::Branched { name } => {
-                format!("git worktree add: {name}").into()
-            }
+        let job_description = match target.branch_name() {
+            Some(branch_name) => format!("git worktree add: {branch_name}"),
+            None => "git worktree add (detached)".to_string(),
         };
-
-        self.send_job(Some(message), move |repo, _cx| async move {
-            let branch_name = match start_point {
-                CreateWorktreeStartPoint::Detached => None,
-                CreateWorktreeStartPoint::Branched { name } => Some(name),
-            };
-            let remote_name = branch_name.clone().unwrap_or_default();
-
+        self.send_job(Some(job_description.into()), move |repo, _cx| async move {
             match repo {
                 RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
-                    backend.create_worktree(branch_name, path, commit).await
+                    backend.create_worktree(target, path).await
                 }
                 RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
+                    let (name, commit, use_existing_branch) = match target {
+                        CreateWorktreeTarget::ExistingBranch { branch_name } => {
+                            (branch_name, None, true)
+                        }
+                        CreateWorktreeTarget::NewBranch {
+                            branch_name,
+                            base_sha: start_point,
+                        } => (branch_name, start_point, false),
+                        CreateWorktreeTarget::Detached {
+                            base_sha: start_point,
+                        } => (String::new(), start_point, false),
+                    };
+
                     client
                         .request(proto::GitCreateWorktree {
                             project_id: project_id.0,
                             repository_id: id.to_proto(),
-                            name: remote_name,
+                            name,
                             directory: path.to_string_lossy().to_string(),
                             commit,
+                            use_existing_branch,
                         })
                         .await?;
 
@@ -6057,28 +6049,16 @@ impl Repository {
         })
     }
 
-    pub fn create_worktree(
-        &mut self,
-        branch_name: String,
-        path: PathBuf,
-        commit: Option<String>,
-    ) -> oneshot::Receiver<Result<()>> {
-        self.create_worktree_with_start_point(
-            CreateWorktreeStartPoint::Branched { name: branch_name },
-            path,
-            commit,
-        )
-    }
-
     pub fn create_worktree_detached(
         &mut self,
         path: PathBuf,
         commit: String,
     ) -> oneshot::Receiver<Result<()>> {
-        self.create_worktree_with_start_point(
-            CreateWorktreeStartPoint::Detached,
+        self.create_worktree(
+            CreateWorktreeTarget::Detached {
+                base_sha: Some(commit),
+            },
             path,
-            Some(commit),
         )
     }
 

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

@@ -6204,6 +6204,76 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
     }
 }
 
+impl<'a> fuzzy_nucleo::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
+    type Candidates = PathMatchCandidateSetNucleoIter<'a>;
+    fn id(&self) -> usize {
+        self.snapshot.id().to_usize()
+    }
+    fn len(&self) -> usize {
+        match self.candidates {
+            Candidates::Files => {
+                if self.include_ignored {
+                    self.snapshot.file_count()
+                } else {
+                    self.snapshot.visible_file_count()
+                }
+            }
+            Candidates::Directories => {
+                if self.include_ignored {
+                    self.snapshot.dir_count()
+                } else {
+                    self.snapshot.visible_dir_count()
+                }
+            }
+            Candidates::Entries => {
+                if self.include_ignored {
+                    self.snapshot.entry_count()
+                } else {
+                    self.snapshot.visible_entry_count()
+                }
+            }
+        }
+    }
+    fn prefix(&self) -> Arc<RelPath> {
+        if self.snapshot.root_entry().is_some_and(|e| e.is_file()) || self.include_root_name {
+            self.snapshot.root_name().into()
+        } else {
+            RelPath::empty().into()
+        }
+    }
+    fn root_is_file(&self) -> bool {
+        self.snapshot.root_entry().is_some_and(|f| f.is_file())
+    }
+    fn path_style(&self) -> PathStyle {
+        self.snapshot.path_style()
+    }
+    fn candidates(&'a self, start: usize) -> Self::Candidates {
+        PathMatchCandidateSetNucleoIter {
+            traversal: match self.candidates {
+                Candidates::Directories => self.snapshot.directories(self.include_ignored, start),
+                Candidates::Files => self.snapshot.files(self.include_ignored, start),
+                Candidates::Entries => self.snapshot.entries(self.include_ignored, start),
+            },
+        }
+    }
+}
+
+pub struct PathMatchCandidateSetNucleoIter<'a> {
+    traversal: Traversal<'a>,
+}
+
+impl<'a> Iterator for PathMatchCandidateSetNucleoIter<'a> {
+    type Item = fuzzy_nucleo::PathMatchCandidate<'a>;
+    fn next(&mut self) -> Option<Self::Item> {
+        self.traversal
+            .next()
+            .map(|entry| fuzzy_nucleo::PathMatchCandidate {
+                is_dir: entry.kind.is_dir(),
+                path: &entry.path,
+            })
+    }
+}
+
 impl EventEmitter<Event> for Project {}
 
 impl<'a> From<&'a ProjectPath> for SettingsLocation<'a> {

crates/project/tests/integration/git_store.rs πŸ”—

@@ -1267,9 +1267,11 @@ mod git_worktrees {
         cx.update(|cx| {
             repository.update(cx, |repository, _| {
                 repository.create_worktree(
-                    "feature-branch".to_string(),
+                    git::repository::CreateWorktreeTarget::NewBranch {
+                        branch_name: "feature-branch".to_string(),
+                        base_sha: Some("abc123".to_string()),
+                    },
                     worktree_1_directory.clone(),
-                    Some("abc123".to_string()),
                 )
             })
         })
@@ -1297,9 +1299,11 @@ mod git_worktrees {
         cx.update(|cx| {
             repository.update(cx, |repository, _| {
                 repository.create_worktree(
-                    "bugfix-branch".to_string(),
+                    git::repository::CreateWorktreeTarget::NewBranch {
+                        branch_name: "bugfix-branch".to_string(),
+                        base_sha: None,
+                    },
                     worktree_2_directory.clone(),
-                    None,
                 )
             })
         })

crates/project_panel/src/project_panel_tests.rs πŸ”—

@@ -11,7 +11,7 @@ use std::path::{Path, PathBuf};
 use util::{path, paths::PathStyle, rel_path::rel_path};
 use workspace::{
     AppState, ItemHandle, MultiWorkspace, Pane, Workspace,
-    item::{Item, ProjectItem},
+    item::{Item, ProjectItem, test::TestItem},
     register_project_item,
 };
 
@@ -6015,6 +6015,150 @@ async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_reveal_in_project_panel_notifications(cx: &mut gpui::TestAppContext) {
+    init_test_with_editor(cx);
+    let fs = FakeFs::new(cx.background_executor.clone());
+    fs.insert_tree(
+        "/workspace",
+        json!({
+            "README.md": ""
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/workspace".as_ref()], cx).await;
+    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+    let workspace = window
+        .read_with(cx, |mw, _| mw.workspace().clone())
+        .unwrap();
+    let cx = &mut VisualTestContext::from_window(window.into(), cx);
+    let panel = workspace.update_in(cx, ProjectPanel::new);
+    cx.run_until_parked();
+
+    // Ensure that, attempting to run `pane: reveal in project panel` without
+    // any active item does nothing, i.e., does not focus the project panel but
+    // it also does not show a notification.
+    cx.dispatch_action(workspace::RevealInProjectPanel::default());
+    cx.run_until_parked();
+
+    panel.update_in(cx, |panel, window, cx| {
+        assert!(
+            !panel.focus_handle(cx).is_focused(window),
+            "Project panel should not be focused after attempting to reveal an invisible worktree entry"
+        );
+
+        panel.workspace.update(cx, |workspace, cx| {
+            assert!(
+                workspace.active_item(cx).is_none(),
+                "Workspace should not have an active item"
+            );
+            assert_eq!(
+                workspace.notification_ids(),
+                vec![],
+                "No notification should be shown when there's no active item"
+            );
+        }).unwrap();
+    });
+
+    // Create a file in a different folder than the one in the project so we can
+    // later open it and ensure that, attempting to reveal it in the project
+    // panel shows a notification and does not focus the project panel.
+    fs.insert_tree(
+        "/external",
+        json!({
+            "file.txt": "External File",
+        }),
+    )
+    .await;
+
+    let (worktree, _) = project
+        .update(cx, |project, cx| {
+            project.find_or_create_worktree("/external/file.txt", false, cx)
+        })
+        .await
+        .unwrap();
+
+    workspace
+        .update_in(cx, |workspace, window, cx| {
+            let worktree_id = worktree.read(cx).id();
+            let path = rel_path("").into();
+            let project_path = ProjectPath { worktree_id, path };
+
+            workspace.open_path(project_path, None, true, window, cx)
+        })
+        .await
+        .unwrap();
+    cx.run_until_parked();
+
+    cx.dispatch_action(workspace::RevealInProjectPanel::default());
+    cx.run_until_parked();
+
+    panel.update_in(cx, |panel, window, cx| {
+        assert!(
+            !panel.focus_handle(cx).is_focused(window),
+            "Project panel should not be focused after attempting to reveal an invisible worktree entry"
+        );
+
+        panel.workspace.update(cx, |workspace, cx| {
+            assert!(
+                workspace.active_item(cx).is_some(),
+                "Workspace should have an active item"
+            );
+
+            let notification_ids = workspace.notification_ids();
+            assert_eq!(
+                notification_ids.len(),
+                1,
+                "A notification should be shown when trying to reveal an invisible worktree entry"
+            );
+
+            workspace.dismiss_notification(&notification_ids[0], cx);
+            assert_eq!(
+                workspace.notification_ids().len(),
+                0,
+                "No notifications should be left after dismissing"
+            );
+        }).unwrap();
+    });
+
+    // Create an empty buffer so we can ensure that, attempting to reveal it in
+    // the project panel shows a notification and does not focus the project
+    // panel.
+    let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
+    pane.update_in(cx, |pane, window, cx| {
+        let item = cx.new(|cx| TestItem::new(cx).with_label("Unsaved buffer"));
+        pane.add_item(Box::new(item), false, false, None, window, cx);
+    });
+
+    cx.dispatch_action(workspace::RevealInProjectPanel::default());
+    cx.run_until_parked();
+
+    panel.update_in(cx, |panel, window, cx| {
+        assert!(
+            !panel.focus_handle(cx).is_focused(window),
+            "Project panel should not be focused after attempting to reveal an unsaved buffer"
+        );
+
+        panel
+            .workspace
+            .update(cx, |workspace, cx| {
+                assert!(
+                    workspace.active_item(cx).is_some(),
+                    "Workspace should have an active item"
+                );
+
+                let notification_ids = workspace.notification_ids();
+                assert_eq!(
+                    notification_ids.len(),
+                    1,
+                    "A notification should be shown when trying to reveal an unsaved buffer"
+                );
+            })
+            .unwrap();
+    });
+}
+
 #[gpui::test]
 async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
     init_test(cx);

crates/proto/proto/git.proto πŸ”—

@@ -594,6 +594,7 @@ message GitCreateWorktree {
   string name = 3;
   string directory = 4;
   optional string commit = 5;
+  bool use_existing_branch = 6;
 }
 
 message GitCreateCheckpoint {

crates/repl/src/kernels/ssh_kernel.rs πŸ”—

@@ -215,7 +215,7 @@ impl SshRunningKernel {
                 &session_id,
             )
             .await
-            .context("failed to create iopub connection")?;
+            .context("Failed to create iopub connection. Is `ipykernel` installed in the remote environment? Try running `pip install ipykernel` on the remote host.")?;
 
             let peer_identity = runtimelib::peer_identity_for_session(&session_id)?;
             let shell_socket = runtimelib::create_client_shell_connection_with_identity(

crates/repl/src/kernels/wsl_kernel.rs πŸ”—

@@ -354,7 +354,8 @@ impl WslRunningKernel {
                 "",
                 &session_id,
             )
-            .await?;
+            .await
+            .context("Failed to create iopub connection. Is `ipykernel` installed in the WSL environment? Try running `pip install ipykernel` inside your WSL distribution.")?;
 
             let peer_identity = runtimelib::peer_identity_for_session(&session_id)?;
             let shell_socket = runtimelib::create_client_shell_connection_with_identity(

crates/tasks_ui/src/modal.rs πŸ”—

@@ -566,9 +566,7 @@ impl PickerDelegate for TasksModalDelegate {
                                         .checked_sub(1);
                                     picker.refresh(window, cx);
                                 }))
-                                .tooltip(|_, cx| {
-                                    Tooltip::simple("Delete Previously Scheduled Task", cx)
-                                }),
+                                .tooltip(|_, cx| Tooltip::simple("Delete from Recent Tasks", cx)),
                         );
                         item.end_slot_on_hover(delete_button)
                     } else {

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

@@ -10,7 +10,10 @@ use crate::{
         TabContentParams, TabTooltipContent, WeakItemHandle,
     },
     move_item,
-    notifications::NotifyResultExt,
+    notifications::{
+        NotificationId, NotifyResultExt, show_app_notification,
+        simple_message_notification::MessageNotification,
+    },
     toolbar::Toolbar,
     workspace_settings::{AutosaveSetting, FocusFollowsMouse, TabBarSettings, WorkspaceSettings},
 };
@@ -4400,17 +4403,64 @@ impl Render for Pane {
             ))
             .on_action(
                 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| {
+                    let Some(active_item) = pane.active_item() else {
+                        return;
+                    };
+
                     let entry_id = action
                         .entry_id
                         .map(ProjectEntryId::from_proto)
-                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
-                    if let Some(entry_id) = entry_id {
-                        pane.project
-                            .update(cx, |_, cx| {
-                                cx.emit(project::Event::RevealInProjectPanel(entry_id))
-                            })
-                            .ok();
+                        .or_else(|| active_item.project_entry_ids(cx).first().copied());
+
+                    let show_reveal_error_toast = |display_name: &str, cx: &mut App| {
+                        let notification_id = NotificationId::unique::<RevealInProjectPanel>();
+                        let message = SharedString::from(format!(
+                            "\"{display_name}\" is not part of any open projects."
+                        ));
+
+                        show_app_notification(notification_id, cx, move |cx| {
+                            let message = message.clone();
+                            cx.new(|cx| MessageNotification::new(message, cx))
+                        });
+                    };
+
+                    let Some(entry_id) = entry_id else {
+                        // When working with an unsaved buffer, display a toast
+                        // informing the user that the buffer is not present in
+                        // any of the open projects and stop execution, as we
+                        // don't want to open the project panel.
+                        let display_name = active_item
+                            .tab_tooltip_text(cx)
+                            .unwrap_or_else(|| active_item.tab_content_text(0, cx));
+
+                        return show_reveal_error_toast(&display_name, cx);
+                    };
+
+                    // We'll now check whether the entry belongs to a visible
+                    // worktree and, if that's not the case, it means the user
+                    // is interacting with a file that does not belong to any of
+                    // the open projects, so we'll show a toast informing them
+                    // of this and stop execution.
+                    let display_name = pane
+                        .project
+                        .read_with(cx, |project, cx| {
+                            project
+                                .worktree_for_entry(entry_id, cx)
+                                .filter(|worktree| !worktree.read(cx).is_visible())
+                                .map(|worktree| worktree.read(cx).root_name_str().to_string())
+                        })
+                        .ok()
+                        .flatten();
+
+                    if let Some(display_name) = display_name {
+                        return show_reveal_error_toast(&display_name, cx);
                     }
+
+                    pane.project
+                        .update(cx, |_, cx| {
+                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
+                        })
+                        .log_err();
                 }),
             )
             .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| {

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

@@ -3374,7 +3374,7 @@ fn run_start_thread_in_selector_visual_tests(
     cx: &mut VisualTestAppContext,
     update_baseline: bool,
 ) -> Result<TestResult> {
-    use agent_ui::{AgentPanel, StartThreadIn, WorktreeCreationStatus};
+    use agent_ui::{AgentPanel, NewWorktreeBranchTarget, StartThreadIn, WorktreeCreationStatus};
 
     // Enable feature flags so the thread target selector renders
     cx.update(|cx| {
@@ -3695,7 +3695,13 @@ edition = "2021"
 
     cx.update_window(workspace_window.into(), |_, _window, cx| {
         panel.update(cx, |panel, cx| {
-            panel.set_start_thread_in_for_tests(StartThreadIn::NewWorktree, cx);
+            panel.set_start_thread_in_for_tests(
+                StartThreadIn::NewWorktree {
+                    worktree_name: None,
+                    branch_target: NewWorktreeBranchTarget::default(),
+                },
+                cx,
+            );
         });
     })?;
     cx.run_until_parked();
@@ -3768,7 +3774,13 @@ edition = "2021"
     cx.run_until_parked();
 
     cx.update_window(workspace_window.into(), |_, window, cx| {
-        window.dispatch_action(Box::new(StartThreadIn::NewWorktree), cx);
+        window.dispatch_action(
+            Box::new(StartThreadIn::NewWorktree {
+                worktree_name: None,
+                branch_target: NewWorktreeBranchTarget::default(),
+            }),
+            cx,
+        );
     })?;
     cx.run_until_parked();