git_ui: Improve UI for the branch, stash, and worktree pickers (#52274)

Danilo Leal created

## Context

This PR refines the design for all of these Git pickers but making them
look more consistent and polished (using same fonts, dividers, and
alignment). It also adds a delete button to all items in each picker so
you can more intuitively delete each item if you're on a mouse-based
flow.

| Worktrees | Stashes | Branches |
|--------|--------|--------|
| <img width="1490" height="1178" alt="Screenshot 2026-03-23 at 9  15
2@2x"
src="https://github.com/user-attachments/assets/3143a626-1b97-43b5-b769-d6cab2c5df7c"
/> | <img width="1490" height="1178" alt="Screenshot 2026-03-23 at 9  15
3@2x"
src="https://github.com/user-attachments/assets/80cbd5ee-394d-42b7-84f8-2d9950d9889e"
/> | <img width="1490" height="1178" alt="Screenshot 2026-03-23 at 9 
15@2x"
src="https://github.com/user-attachments/assets/408fd12e-fb18-4233-a4bf-5fad53aa307f"
/> |

--- 

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

Release Notes:

- N/A

Change summary

assets/icons/box_open.svg            |   6 
crates/git_ui/src/branch_picker.rs   | 217 +++++++++++++++++------------
crates/git_ui/src/stash_picker.rs    |  36 ++++
crates/git_ui/src/worktree_picker.rs |  93 +++++++++---
crates/icons/src/icons.rs            |   1 
5 files changed, 232 insertions(+), 121 deletions(-)

Detailed changes

assets/icons/box_open.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 14.0043V8.59991" stroke="#C6CAD0" stroke-width="1.20097" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.9035 2.12072C10.0531 2.0371 10.2216 1.99319 10.3929 1.99319C10.5642 1.99319 10.7327 2.0371 10.8823 2.12072L13.4043 3.53786C13.5829 3.63887 13.7315 3.78548 13.8349 3.96272C13.9383 4.13997 13.9928 4.34148 13.9928 4.54668C13.9928 4.75187 13.9383 4.95339 13.8349 5.13063C13.7315 5.30787 13.5829 5.45449 13.4043 5.55549L6.09043 9.67481C5.94043 9.76037 5.77072 9.80537 5.59804 9.80537C5.42535 9.80537 5.25564 9.76037 5.10564 9.67481L2.59562 8.25767C2.417 8.15666 2.2684 8.01005 2.16501 7.83281C2.06162 7.65556 2.00714 7.45405 2.00714 7.24885C2.00714 7.04366 2.06162 6.84214 2.16501 6.6649C2.2684 6.48766 2.417 6.34104 2.59562 6.24004L9.9035 2.12072Z" stroke="#C6CAD0" stroke-width="1.20097" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8038 8.59991V10.9238C12.8041 11.1504 12.742 11.3728 12.6245 11.5666C12.507 11.7604 12.3384 11.9181 12.1373 12.0227L8.53441 13.8722C8.36933 13.958 8.18602 14.0027 7.99998 14.0027C7.81394 14.0027 7.63063 13.958 7.46555 13.8722L3.86264 12.0227C3.66153 11.9181 3.493 11.7604 3.37546 11.5666C3.25791 11.3728 3.19587 11.1504 3.19611 10.9238V8.59991" stroke="#C6CAD0" stroke-width="1.20097" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.4043 8.25766C13.5829 8.15666 13.7315 8.01004 13.8349 7.8328C13.9383 7.65556 13.9928 7.45404 13.9928 7.24885C13.9928 7.04365 13.9383 6.84213 13.8349 6.66489C13.7315 6.48765 13.5829 6.34103 13.4043 6.24003L6.09644 2.11471C5.94745 2.02938 5.77874 1.9845 5.60704 1.9845C5.43535 1.9845 5.26664 2.02938 5.11765 2.11471L2.59562 3.53785C2.417 3.63886 2.2684 3.78547 2.16501 3.96271C2.06162 4.13996 2.00714 4.34147 2.00714 4.54667C2.00714 4.75186 2.06162 4.95338 2.16501 5.13062C2.2684 5.30786 2.417 5.45448 2.59562 5.55548L9.90951 9.6748C10.0584 9.76036 10.2272 9.80538 10.3989 9.80538C10.5706 9.80538 10.7394 9.76036 10.8883 9.6748L13.4043 8.25766Z" stroke="#C6CAD0" stroke-width="1.20097" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

crates/git_ui/src/branch_picker.rs 🔗

@@ -559,8 +559,7 @@ impl PickerDelegate for BranchListDelegate {
         match self.state {
             PickerState::List | PickerState::NewRemote | PickerState::NewBranch => {
                 match self.branch_filter {
-                    BranchFilter::All => "Select branch or remote…",
-                    BranchFilter::Remote => "Select remote…",
+                    BranchFilter::All | BranchFilter::Remote => "Select branch…",
                 }
             }
             PickerState::CreateRemote(_) => "Enter a name for this remote…",
@@ -884,13 +883,13 @@ impl PickerDelegate for BranchListDelegate {
 
         let entry_icon = match entry {
             Entry::NewUrl { .. } | Entry::NewBranch { .. } | Entry::NewRemoteName { .. } => {
-                Icon::new(IconName::Plus).color(Color::Muted)
+                IconName::Plus
             }
             Entry::Branch { branch, .. } => {
                 if branch.is_remote() {
-                    Icon::new(IconName::Screen).color(Color::Muted)
+                    IconName::Screen
                 } else {
-                    Icon::new(IconName::GitBranchAlt).color(Color::Muted)
+                    IconName::GitBranchAlt
                 }
             }
         };
@@ -922,8 +921,11 @@ impl PickerDelegate for BranchListDelegate {
             Entry::NewUrl { .. } | Entry::NewBranch { .. } | Entry::NewRemoteName { .. }
         );
 
-        let deleted_branch_icon = |entry_ix: usize, is_head_branch: bool| {
+        let is_head_branch = entry.as_branch().is_some_and(|branch| branch.is_head);
+
+        let deleted_branch_icon = |entry_ix: usize| {
             IconButton::new(("delete", entry_ix), IconName::Trash)
+                .icon_size(IconSize::Small)
                 .tooltip(move |_, cx| {
                     Tooltip::for_action_in(
                         "Delete Branch",
@@ -932,7 +934,6 @@ impl PickerDelegate for BranchListDelegate {
                         cx,
                     )
                 })
-                .disabled(is_head_branch)
                 .on_click(cx.listener(move |this, _, window, cx| {
                     this.delegate.delete_at(entry_ix, window, cx);
                 }))
@@ -943,6 +944,7 @@ impl PickerDelegate for BranchListDelegate {
             let focus_handle = self.focus_handle.clone();
 
             IconButton::new("create_from_default", IconName::GitBranchPlus)
+                .icon_size(IconSize::Small)
                 .tooltip(move |_, cx| {
                     Tooltip::for_action_in(
                         tooltip_label.clone(),
@@ -965,105 +967,132 @@ impl PickerDelegate for BranchListDelegate {
                 .child(
                     h_flex()
                         .w_full()
-                        .gap_3()
+                        .gap_2p5()
                         .flex_grow()
-                        .child(entry_icon)
+                        .child(
+                            Icon::new(entry_icon)
+                                .color(Color::Muted)
+                                .size(IconSize::Small),
+                        )
                         .child(
                             v_flex()
                                 .id("info_container")
                                 .w_full()
                                 .child(entry_title)
-                                .child(
-                                    h_flex()
-                                        .w_full()
-                                        .justify_between()
-                                        .gap_1p5()
-                                        .when(self.style == BranchListStyle::Modal, |el| {
-                                            el.child(div().max_w_96().child({
-                                                let message = match entry {
-                                                    Entry::NewUrl { url } => {
-                                                        format!("Based off {url}")
-                                                    }
-                                                    Entry::NewRemoteName { url, .. } => {
-                                                        format!("Based off {url}")
-                                                    }
-                                                    Entry::NewBranch { .. } => {
-                                                        if let Some(current_branch) =
-                                                            self.repo.as_ref().and_then(|repo| {
-                                                                repo.read(cx)
-                                                                    .branch
-                                                                    .as_ref()
-                                                                    .map(|b| b.name())
-                                                            })
-                                                        {
-                                                            format!("Based off {}", current_branch)
-                                                        } else {
-                                                            "Based off the current branch"
-                                                                .to_string()
-                                                        }
-                                                    }
-                                                    Entry::Branch { .. } => {
-                                                        let show_author_name =
-                                                            ProjectSettings::get_global(cx)
-                                                                .git
-                                                                .branch_picker
-                                                                .show_author_name;
-
-                                                        subject.map_or(
-                                                            "No commits found".into(),
-                                                            |subject| {
-                                                                if show_author_name
-                                                                    && let Some(author) =
-                                                                        author_name
-                                                                {
-                                                                    format!(
-                                                                        "{}  •  {}",
-                                                                        author, subject
-                                                                    )
-                                                                } else {
-                                                                    subject.to_string()
-                                                                }
-                                                            },
-                                                        )
-                                                    }
-                                                };
-
-                                                Label::new(message)
-                                                    .size(LabelSize::Small)
-                                                    .color(Color::Muted)
-                                                    .truncate()
-                                            }))
-                                        })
-                                        .when_some(commit_time, |label, commit_time| {
-                                            label.child(
-                                                Label::new(commit_time)
-                                                    .size(LabelSize::Small)
-                                                    .color(Color::Muted),
-                                            )
-                                        }),
-                                )
+                                .child({
+                                    let message = match entry {
+                                        Entry::NewUrl { url } => format!("Based off {url}"),
+                                        Entry::NewRemoteName { url, .. } => {
+                                            format!("Based off {url}")
+                                        }
+                                        Entry::NewBranch { .. } => {
+                                            if let Some(current_branch) =
+                                                self.repo.as_ref().and_then(|repo| {
+                                                    repo.read(cx).branch.as_ref().map(|b| b.name())
+                                                })
+                                            {
+                                                format!("Based off {}", current_branch)
+                                            } else {
+                                                "Based off the current branch".to_string()
+                                            }
+                                        }
+                                        Entry::Branch { .. } => String::new(),
+                                    };
+
+                                    if matches!(entry, Entry::Branch { .. }) {
+                                        let show_author_name = ProjectSettings::get_global(cx)
+                                            .git
+                                            .branch_picker
+                                            .show_author_name;
+                                        let has_author = show_author_name && author_name.is_some();
+                                        let has_commit = commit_time.is_some();
+                                        let author_for_meta =
+                                            if show_author_name { author_name } else { None };
+
+                                        let dot = || {
+                                            Label::new("•")
+                                                .alpha(0.5)
+                                                .color(Color::Muted)
+                                                .size(LabelSize::Small)
+                                        };
+
+                                        h_flex()
+                                            .w_full()
+                                            .min_w_0()
+                                            .gap_1p5()
+                                            .when_some(author_for_meta, |this, author| {
+                                                this.child(
+                                                    Label::new(author)
+                                                        .color(Color::Muted)
+                                                        .size(LabelSize::Small),
+                                                )
+                                            })
+                                            .when_some(commit_time, |this, time| {
+                                                this.when(has_author, |this| this.child(dot()))
+                                                    .child(
+                                                        Label::new(time)
+                                                            .color(Color::Muted)
+                                                            .size(LabelSize::Small),
+                                                    )
+                                            })
+                                            .when_some(subject, |this, subj| {
+                                                this.when(has_commit, |this| this.child(dot()))
+                                                    .child(
+                                                        Label::new(subj.to_string())
+                                                            .color(Color::Muted)
+                                                            .size(LabelSize::Small)
+                                                            .truncate()
+                                                            .flex_1(),
+                                                    )
+                                            })
+                                            .when(!has_commit, |this| {
+                                                this.child(
+                                                    Label::new("No commits found")
+                                                        .color(Color::Muted)
+                                                        .size(LabelSize::Small),
+                                                )
+                                            })
+                                            .into_any_element()
+                                    } else {
+                                        Label::new(message)
+                                            .size(LabelSize::Small)
+                                            .color(Color::Muted)
+                                            .truncate()
+                                            .into_any_element()
+                                    }
+                                })
                                 .when_some(
                                     entry.as_branch().map(|b| b.name().to_string()),
-                                    |this, branch_name| this.tooltip(Tooltip::text(branch_name)),
+                                    |this, branch_name| {
+                                        this.map(|this| {
+                                            if is_head_branch {
+                                                this.tooltip(move |_, cx| {
+                                                    Tooltip::with_meta(
+                                                        branch_name.clone(),
+                                                        None,
+                                                        "Current Branch",
+                                                        cx,
+                                                    )
+                                                })
+                                            } else {
+                                                this.tooltip(Tooltip::text(branch_name))
+                                            }
+                                        })
+                                    },
                                 ),
                         ),
                 )
-                .when(
-                    self.editor_position() == PickerEditorPosition::End && !is_new_items,
-                    |this| {
-                        this.map(|this| {
-                            let is_head_branch =
-                                entry.as_branch().is_some_and(|branch| branch.is_head);
-                            if self.selected_index() == ix {
-                                this.end_slot(deleted_branch_icon(ix, is_head_branch))
-                            } else {
-                                this.end_hover_slot(deleted_branch_icon(ix, is_head_branch))
-                            }
-                        })
-                    },
-                )
+                .when(!is_new_items && !is_head_branch, |this| {
+                    this.map(|this| {
+                        if self.selected_index() == ix {
+                            this.end_slot(deleted_branch_icon(ix))
+                        } else {
+                            this.end_hover_slot(deleted_branch_icon(ix))
+                        }
+                    })
+                })
                 .when_some(
-                    if self.editor_position() == PickerEditorPosition::End && is_new_items {
+                    if is_new_items {
                         create_from_default_button
                     } else {
                         None

crates/git_ui/src/stash_picker.rs 🔗

@@ -468,7 +468,7 @@ impl PickerDelegate for StashListDelegate {
         ix: usize,
         selected: bool,
         _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
+        cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
         let entry_match = &self.matches[ix];
 
@@ -501,16 +501,46 @@ impl PickerDelegate for StashListDelegate {
                     .size(LabelSize::Small),
             );
 
+        let focus_handle = self.focus_handle.clone();
+
+        let drop_button = |entry_ix: usize| {
+            IconButton::new(("drop-stash", entry_ix), IconName::Trash)
+                .icon_size(IconSize::Small)
+                .tooltip(move |_, cx| {
+                    Tooltip::for_action_in("Drop Stash", &DropStashItem, &focus_handle, cx)
+                })
+                .on_click(cx.listener(move |this, _, window, cx| {
+                    this.delegate.drop_stash_at(entry_ix, window, cx);
+                }))
+        };
+
         Some(
             ListItem::new(format!("stash-{ix}"))
                 .inset(true)
                 .spacing(ListItemSpacing::Sparse)
                 .toggle_state(selected)
-                .child(v_flex().w_full().child(stash_label).child(branch_info))
+                .child(
+                    h_flex()
+                        .w_full()
+                        .gap_2p5()
+                        .child(
+                            Icon::new(IconName::BoxOpen)
+                                .size(IconSize::Small)
+                                .color(Color::Muted),
+                        )
+                        .child(div().w_full().child(stash_label).child(branch_info)),
+                )
                 .tooltip(Tooltip::text(format!(
                     "stash@{{{}}}",
                     entry_match.entry.index
-                ))),
+                )))
+                .map(|this| {
+                    if selected {
+                        this.end_slot(drop_button(ix))
+                    } else {
+                        this.end_hover_slot(drop_button(ix))
+                    }
+                }),
         )
     }
 

crates/git_ui/src/worktree_picker.rs 🔗

@@ -18,7 +18,7 @@ use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};
 use remote_connection::{RemoteConnectionModal, connect};
 use settings::Settings;
 use std::{path::PathBuf, sync::Arc};
-use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*};
+use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
 use util::ResultExt;
 use workspace::{ModalView, MultiWorkspace, Workspace, notifications::DetachAndPromptErr};
 
@@ -769,38 +769,83 @@ impl PickerDelegate for WorktreeListDelegate {
             )
         };
 
+        let focus_handle = self.focus_handle.clone();
+
+        let delete_button = |entry_ix: usize| {
+            IconButton::new(("delete-worktree", entry_ix), IconName::Trash)
+                .icon_size(IconSize::Small)
+                .tooltip(move |_, cx| {
+                    Tooltip::for_action_in("Delete Worktree", &DeleteWorktree, &focus_handle, cx)
+                })
+                .on_click(cx.listener(move |this, _, window, cx| {
+                    this.delegate.delete_at(entry_ix, window, cx);
+                }))
+        };
+
+        let entry_icon = if entry.is_new {
+            IconName::Plus
+        } else {
+            IconName::GitWorktree
+        };
+
         Some(
             ListItem::new(format!("worktree-menu-{ix}"))
                 .inset(true)
                 .spacing(ListItemSpacing::Sparse)
                 .toggle_state(selected)
                 .child(
-                    v_flex()
+                    h_flex()
                         .w_full()
+                        .gap_2p5()
                         .child(
-                            h_flex()
-                                .gap_2()
-                                .justify_between()
-                                .overflow_x_hidden()
-                                .child(branch_name)
-                                .when(!entry.is_new, |this| {
-                                    this.child(
-                                        Label::new(sha)
-                                            .size(LabelSize::Small)
-                                            .color(Color::Muted)
-                                            .buffer_font(cx)
-                                            .into_element(),
-                                    )
-                                }),
-                        )
-                        .child(
-                            Label::new(sublabel)
-                                .size(LabelSize::Small)
+                            Icon::new(entry_icon)
                                 .color(Color::Muted)
-                                .truncate()
-                                .into_any_element(),
-                        ),
-                ),
+                                .size(IconSize::Small),
+                        )
+                        .child(v_flex().w_full().child(branch_name).map(|this| {
+                            if entry.is_new {
+                                this.child(
+                                    Label::new(sublabel)
+                                        .size(LabelSize::Small)
+                                        .color(Color::Muted)
+                                        .truncate(),
+                                )
+                            } else {
+                                this.child(
+                                    h_flex()
+                                        .w_full()
+                                        .min_w_0()
+                                        .gap_1p5()
+                                        .child(
+                                            Label::new(sha)
+                                                .size(LabelSize::Small)
+                                                .color(Color::Muted),
+                                        )
+                                        .child(
+                                            Label::new("•")
+                                                .alpha(0.5)
+                                                .color(Color::Muted)
+                                                .size(LabelSize::Small),
+                                        )
+                                        .child(
+                                            Label::new(sublabel)
+                                                .truncate()
+                                                .color(Color::Muted)
+                                                .size(LabelSize::Small)
+                                                .flex_1(),
+                                        )
+                                        .into_any_element(),
+                                )
+                            }
+                        })),
+                )
+                .when(!entry.is_new, |this| {
+                    if selected {
+                        this.end_slot(delete_button(ix))
+                    } else {
+                        this.end_hover_slot(delete_button(ix))
+                    }
+                }),
         )
     }