agent_ui: Add UI refinements to the sidebar (#51307)

Danilo Leal created

Release Notes:

- N/A

Change summary

assets/icons/threads_sidebar_left_closed.svg  |   5 
assets/icons/threads_sidebar_left_open.svg    |   5 
assets/icons/threads_sidebar_right_closed.svg |   5 
assets/icons/threads_sidebar_right_open.svg   |   5 
assets/icons/workspace_nav_closed.svg         |   5 
assets/icons/workspace_nav_open.svg           |   5 
crates/agent_ui/src/agent_panel.rs            | 133 +++++++++++++-------
crates/agent_ui/src/sidebar.rs                |  87 +++++++++----
crates/icons/src/icons.rs                     |   6 
crates/ui/src/components/ai/thread_item.rs    |  67 +++++----
crates/ui/src/components/list/list_item.rs    |   8 +
11 files changed, 211 insertions(+), 120 deletions(-)

Detailed changes

assets/icons/threads_sidebar_left_closed.svg 🔗

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

assets/icons/threads_sidebar_left_open.svg 🔗

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

assets/icons/threads_sidebar_right_closed.svg 🔗

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

assets/icons/threads_sidebar_right_open.svg 🔗

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

assets/icons/workspace_nav_closed.svg 🔗

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

assets/icons/workspace_nav_open.svg 🔗

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

crates/agent_ui/src/agent_panel.rs 🔗

@@ -483,9 +483,17 @@ pub fn init(cx: &mut App) {
                     }
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         if let Some(sidebar) = panel.read(cx).sidebar.clone() {
+                            let was_open = sidebar.read(cx).is_open();
                             sidebar.update(cx, |sidebar, cx| {
                                 sidebar.toggle(window, cx);
                             });
+                            // When closing the sidebar, restore focus to the active pane
+                            // to avoid "zombie focus" on the now-hidden sidebar elements
+                            if was_open {
+                                let active_pane = workspace.active_pane().clone();
+                                let pane_focus = active_pane.read(cx).focus_handle(cx);
+                                window.focus(&pane_focus, cx);
+                            }
                         }
                     }
                 })
@@ -3623,7 +3631,7 @@ impl AgentPanel {
         Some((view, width, is_open))
     }
 
-    fn render_sidebar_toggle(&self, cx: &Context<Self>) -> Option<AnyElement> {
+    fn render_sidebar_toggle(&self, docked_right: bool, cx: &Context<Self>) -> Option<AnyElement> {
         if !multi_workspace_enabled(cx) {
             return None;
         }
@@ -3634,20 +3642,41 @@ impl AgentPanel {
         }
         let has_notifications = sidebar_read.has_notifications(cx);
 
+        let icon = if docked_right {
+            IconName::ThreadsSidebarRightClosed
+        } else {
+            IconName::ThreadsSidebarLeftClosed
+        };
+
         Some(
-            IconButton::new("toggle-workspace-sidebar", IconName::WorkspaceNavClosed)
-                .icon_size(IconSize::Small)
-                .when(has_notifications, |button| {
-                    button
-                        .indicator(Indicator::dot().color(Color::Accent))
-                        .indicator_border_color(Some(cx.theme().colors().tab_bar_background))
-                })
-                .tooltip(move |_, cx| {
-                    Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
-                })
-                .on_click(|_, window, cx| {
-                    window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
+            h_flex()
+                .h_full()
+                .px_1()
+                .map(|this| {
+                    if docked_right {
+                        this.border_l_1()
+                    } else {
+                        this.border_r_1()
+                    }
                 })
+                .border_color(cx.theme().colors().border_variant)
+                .child(
+                    IconButton::new("toggle-workspace-sidebar", icon)
+                        .icon_size(IconSize::Small)
+                        .when(has_notifications, |button| {
+                            button
+                                .indicator(Indicator::dot().color(Color::Accent))
+                                .indicator_border_color(Some(
+                                    cx.theme().colors().tab_bar_background,
+                                ))
+                        })
+                        .tooltip(move |_, cx| {
+                            Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
+                        })
+                        .on_click(|_, window, cx| {
+                            window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
+                        }),
+                )
                 .into_any_element(),
         )
     }
@@ -4104,6 +4133,23 @@ impl AgentPanel {
 
         let use_v2_empty_toolbar = has_v2_flag && is_empty_state && !is_in_history_or_config;
 
+        let is_sidebar_open = self
+            .sidebar
+            .as_ref()
+            .map(|s| s.read(cx).is_open())
+            .unwrap_or(false);
+
+        let base_container = h_flex()
+            .id("agent-panel-toolbar")
+            .h(Tab::container_height(cx))
+            .max_w_full()
+            .flex_none()
+            .justify_between()
+            .gap_2()
+            .bg(cx.theme().colors().tab_bar_background)
+            .border_b_1()
+            .border_color(cx.theme().colors().border);
+
         if use_v2_empty_toolbar {
             let (chevron_icon, icon_color, label_color) =
                 if self.new_thread_menu_handle.is_deployed() {
@@ -4162,34 +4208,26 @@ impl AgentPanel {
                     y: px(1.0),
                 });
 
-            h_flex()
-                .id("agent-panel-toolbar")
-                .h(Tab::container_height(cx))
-                .max_w_full()
-                .flex_none()
-                .justify_between()
-                .gap_2()
-                .bg(cx.theme().colors().tab_bar_background)
-                .border_b_1()
-                .border_color(cx.theme().colors().border)
+            base_container
                 .child(
                     h_flex()
                         .size_full()
-                        .gap(DynamicSpacing::Base04.rems(cx))
-                        .pl(DynamicSpacing::Base04.rems(cx))
+                        .gap_1()
+                        .when(is_sidebar_open || docked_right, |this| this.pl_1())
                         .when(!docked_right, |this| {
-                            this.children(self.render_sidebar_toggle(cx))
+                            this.children(self.render_sidebar_toggle(false, cx))
                         })
                         .child(agent_selector_menu)
                         .child(self.render_start_thread_in_selector(cx)),
                 )
                 .child(
                     h_flex()
+                        .h_full()
                         .flex_none()
-                        .gap(DynamicSpacing::Base02.rems(cx))
-                        .pl(DynamicSpacing::Base04.rems(cx))
-                        .pr(DynamicSpacing::Base06.rems(cx))
-                        .when(show_history_menu, |this| {
+                        .gap_1()
+                        .pl_1()
+                        .pr_1()
+                        .when(show_history_menu && !has_v2_flag, |this| {
                             this.child(self.render_recent_entries_menu(
                                 IconName::MenuAltTemp,
                                 Corner::TopRight,
@@ -4198,7 +4236,7 @@ impl AgentPanel {
                         })
                         .child(self.render_panel_options_menu(window, cx))
                         .when(docked_right, |this| {
-                            this.children(self.render_sidebar_toggle(cx))
+                            this.children(self.render_sidebar_toggle(true, cx))
                         }),
                 )
                 .into_any_element()
@@ -4222,23 +4260,19 @@ impl AgentPanel {
                 .with_handle(self.new_thread_menu_handle.clone())
                 .menu(move |window, cx| new_thread_menu_builder(window, cx));
 
-            h_flex()
-                .id("agent-panel-toolbar")
-                .h(Tab::container_height(cx))
-                .max_w_full()
-                .flex_none()
-                .justify_between()
-                .gap_2()
-                .bg(cx.theme().colors().tab_bar_background)
-                .border_b_1()
-                .border_color(cx.theme().colors().border)
+            base_container
                 .child(
                     h_flex()
                         .size_full()
-                        .gap(DynamicSpacing::Base04.rems(cx))
-                        .pl(DynamicSpacing::Base04.rems(cx))
+                        .map(|this| {
+                            if is_sidebar_open || docked_right {
+                                this.pl_1().gap_1()
+                            } else {
+                                this.pl_0().gap_0p5()
+                            }
+                        })
                         .when(!docked_right, |this| {
-                            this.children(self.render_sidebar_toggle(cx))
+                            this.children(self.render_sidebar_toggle(false, cx))
                         })
                         .child(match &self.active_view {
                             ActiveView::History { .. } | ActiveView::Configuration => {
@@ -4250,12 +4284,13 @@ impl AgentPanel {
                 )
                 .child(
                     h_flex()
+                        .h_full()
                         .flex_none()
-                        .gap(DynamicSpacing::Base02.rems(cx))
-                        .pl(DynamicSpacing::Base04.rems(cx))
-                        .pr(DynamicSpacing::Base06.rems(cx))
+                        .gap_1()
+                        .pl_1()
+                        .pr_1()
                         .child(new_thread_menu)
-                        .when(show_history_menu, |this| {
+                        .when(show_history_menu && !has_v2_flag, |this| {
                             this.child(self.render_recent_entries_menu(
                                 IconName::MenuAltTemp,
                                 Corner::TopRight,
@@ -4264,7 +4299,7 @@ impl AgentPanel {
                         })
                         .child(self.render_panel_options_menu(window, cx))
                         .when(docked_right, |this| {
-                            this.children(self.render_sidebar_toggle(cx))
+                            this.children(self.render_sidebar_toggle(true, cx))
                         }),
                 )
                 .into_any_element()

crates/agent_ui/src/sidebar.rs 🔗

@@ -713,6 +713,8 @@ impl Sidebar {
         let is_group_header_after_first =
             ix > 0 && matches!(entry, ListEntry::ProjectHeader { .. });
 
+        let docked_right = AgentSettings::get_global(cx).dock == settings::DockPosition::Right;
+
         let rendered = match entry {
             ListEntry::ProjectHeader {
                 path_list,
@@ -728,9 +730,12 @@ impl Sidebar {
                 highlight_positions,
                 *has_threads,
                 is_selected,
+                docked_right,
                 cx,
             ),
-            ListEntry::Thread(thread) => self.render_thread(ix, thread, is_selected, cx),
+            ListEntry::Thread(thread) => {
+                self.render_thread(ix, thread, is_selected, docked_right, cx)
+            }
             ListEntry::ViewMore {
                 path_list,
                 remaining_count,
@@ -770,6 +775,7 @@ impl Sidebar {
         highlight_positions: &[usize],
         has_threads: bool,
         is_selected: bool,
+        docked_right: bool,
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let id = SharedString::from(format!("project-header-{}", ix));
@@ -815,12 +821,13 @@ impl Sidebar {
             .group_name(group_name)
             .toggle_state(is_active_workspace)
             .focused(is_selected)
+            .docked_right(docked_right)
             .child(
                 h_flex()
                     .relative()
                     .min_w_0()
                     .w_full()
-                    .p_1()
+                    .py_1()
                     .gap_1p5()
                     .child(
                         Icon::new(disclosure_icon)
@@ -969,7 +976,7 @@ impl Sidebar {
     }
 
     fn has_filter_query(&self, cx: &App) -> bool {
-        self.filter_editor.read(cx).buffer().read(cx).is_empty()
+        !self.filter_editor.read(cx).text(cx).is_empty()
     }
 
     fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
@@ -1156,6 +1163,7 @@ impl Sidebar {
         ix: usize,
         thread: &ThreadEntry,
         is_selected: bool,
+        docked_right: bool,
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let has_notification = self
@@ -1171,6 +1179,7 @@ impl Sidebar {
         let workspace = thread.workspace.clone();
 
         let id = SharedString::from(format!("thread-entry-{}", ix));
+
         ThreadItem::new(id, title)
             .icon(thread.icon)
             .when_some(thread.icon_from_external_svg.clone(), |this, svg| {
@@ -1187,6 +1196,7 @@ impl Sidebar {
             })
             .selected(self.focused_thread.as_ref() == Some(&session_info.session_id))
             .focused(is_selected)
+            .docked_right(docked_right)
             .on_click(cx.listener(move |this, _, window, cx| {
                 this.selection = None;
                 this.activate_thread(session_info.clone(), &workspace, window, cx);
@@ -1301,6 +1311,7 @@ impl Sidebar {
         div()
             .w_full()
             .p_2()
+            .pt_1p5()
             .child(
                 Button::new(
                     SharedString::from(format!("new-thread-btn-{}", ix)),
@@ -1320,6 +1331,40 @@ impl Sidebar {
             )
             .into_any_element()
     }
+
+    fn render_sidebar_toggle_button(
+        &self,
+        docked_right: bool,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
+        let icon = if docked_right {
+            IconName::ThreadsSidebarRightOpen
+        } else {
+            IconName::ThreadsSidebarLeftOpen
+        };
+
+        h_flex()
+            .h_full()
+            .px_1()
+            .map(|this| {
+                if docked_right {
+                    this.pr_1p5().border_l_1()
+                } else {
+                    this.border_r_1()
+                }
+            })
+            .border_color(cx.theme().colors().border_variant)
+            .child(
+                IconButton::new("sidebar-close-toggle", icon)
+                    .icon_size(IconSize::Small)
+                    .tooltip(move |_, cx| {
+                        Tooltip::for_action("Close Threads Sidebar", &ToggleWorkspaceSidebar, cx)
+                    })
+                    .on_click(|_, window, cx| {
+                        window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
+                    }),
+            )
+    }
 }
 
 impl Sidebar {
@@ -1416,37 +1461,19 @@ impl Render for Sidebar {
             .child({
                 let docked_right =
                     AgentSettings::get_global(cx).dock == settings::DockPosition::Right;
-                let render_close_button = || {
-                    IconButton::new("sidebar-close-toggle", IconName::WorkspaceNavOpen)
-                        .icon_size(IconSize::Small)
-                        .tooltip(move |_, cx| {
-                            Tooltip::for_action(
-                                "Close Threads Sidebar",
-                                &ToggleWorkspaceSidebar,
-                                cx,
-                            )
-                        })
-                        .on_click(|_, window, cx| {
-                            window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
-                        })
-                };
 
                 h_flex()
-                    .flex_none()
-                    .px_2p5()
                     .h(Tab::container_height(cx))
-                    .gap_2()
+                    .flex_none()
+                    .gap_1p5()
                     .border_b_1()
                     .border_color(cx.theme().colors().border)
-                    .when(!docked_right, |this| this.child(render_close_button()))
-                    .child(
-                        Icon::new(IconName::MagnifyingGlass)
-                            .size(IconSize::Small)
-                            .color(Color::Muted),
-                    )
+                    .when(!docked_right, |this| {
+                        this.child(self.render_sidebar_toggle_button(false, cx))
+                    })
                     .child(self.render_filter_input(cx))
                     .when(has_query, |this| {
-                        this.pr_1().child(
+                        this.when(!docked_right, |this| this.pr_1p5()).child(
                             IconButton::new("clear_filter", IconName::Close)
                                 .shape(IconButtonShape::Square)
                                 .tooltip(Tooltip::text("Clear Search"))
@@ -1456,7 +1483,11 @@ impl Render for Sidebar {
                                 })),
                         )
                     })
-                    .when(docked_right, |this| this.child(render_close_button()))
+                    .when(docked_right, |this| {
+                        this.pl_2()
+                            .pr_0p5()
+                            .child(self.render_sidebar_toggle_button(true, cx))
+                    })
             })
             .child(
                 v_flex()

crates/icons/src/icons.rs 🔗

@@ -244,6 +244,10 @@ pub enum IconName {
     ThinkingModeOff,
     Thread,
     ThreadFromSummary,
+    ThreadsSidebarLeftClosed,
+    ThreadsSidebarLeftOpen,
+    ThreadsSidebarRightClosed,
+    ThreadsSidebarRightOpen,
     ThumbsDown,
     ThumbsUp,
     TodoComplete,
@@ -272,8 +276,6 @@ pub enum IconName {
     UserRoundPen,
     Warning,
     WholeWord,
-    WorkspaceNavClosed,
-    WorkspaceNavOpen,
     XCircle,
     XCircleFilled,
     ZedAgent,

crates/ui/src/components/ai/thread_item.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    DecoratedIcon, DiffStat, GradientFade, HighlightedLabel, IconDecoration, IconDecorationKind,
-    SpinnerLabel, prelude::*,
+    CommonAnimationExt, DecoratedIcon, DiffStat, GradientFade, HighlightedLabel, IconDecoration,
+    IconDecorationKind, prelude::*,
 };
 
 use gpui::{AnyView, ClickEvent, Hsla, SharedString};
@@ -26,6 +26,7 @@ pub struct ThreadItem {
     selected: bool,
     focused: bool,
     hovered: bool,
+    docked_right: bool,
     added: Option<usize>,
     removed: Option<usize>,
     worktree: Option<SharedString>,
@@ -50,6 +51,7 @@ impl ThreadItem {
             selected: false,
             focused: false,
             hovered: false,
+            docked_right: false,
             added: None,
             removed: None,
             worktree: None,
@@ -107,6 +109,11 @@ impl ThreadItem {
         self
     }
 
+    pub fn docked_right(mut self, docked_right: bool) -> Self {
+        self.docked_right = docked_right;
+        self
+    }
+
     pub fn worktree(mut self, worktree: impl Into<SharedString>) -> Self {
         self.worktree = Some(worktree.into());
         self
@@ -154,12 +161,12 @@ impl ThreadItem {
 impl RenderOnce for ThreadItem {
     fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
         let color = cx.theme().colors();
-        // let dot_separator = || {
-        //     Label::new("•")
-        //         .size(LabelSize::Small)
-        //         .color(Color::Muted)
-        //         .alpha(0.5)
-        // };
+        let dot_separator = || {
+            Label::new("•")
+                .size(LabelSize::Small)
+                .color(Color::Muted)
+                .alpha(0.5)
+        };
 
         let icon_container = || h_flex().size_4().flex_none().justify_center();
         let agent_icon = if let Some(custom_svg) = self.custom_icon_from_external_svg {
@@ -194,17 +201,23 @@ impl RenderOnce for ThreadItem {
             None
         };
 
-        let icon = if let Some(decoration) = decoration {
-            icon_container().child(DecoratedIcon::new(agent_icon, Some(decoration)))
-        } else {
-            icon_container().child(agent_icon)
-        };
-
         let is_running = matches!(
             self.status,
             AgentThreadStatus::Running | AgentThreadStatus::WaitingForConfirmation
         );
-        let running_or_action = is_running || (self.hovered && self.action_slot.is_some());
+
+        let icon = if is_running {
+            icon_container().child(
+                Icon::new(IconName::LoadCircle)
+                    .size(IconSize::Small)
+                    .color(Color::Muted)
+                    .with_rotate_animation(2),
+            )
+        } else if let Some(decoration) = decoration {
+            icon_container().child(DecoratedIcon::new(agent_icon, Some(decoration)))
+        } else {
+            icon_container().child(agent_icon)
+        };
 
         let title = self.title;
         let highlight_positions = self.highlight_positions;
@@ -244,13 +257,16 @@ impl RenderOnce for ThreadItem {
                 if has_worktree || has_diff_stats {
                     this.p_2()
                 } else {
-                    this.px_2().py_1()
+                    this.p_1()
                 }
             })
             .when(self.selected, |s| s.bg(color.element_active))
             .border_1()
             .border_color(gpui::transparent_black())
-            .when(self.focused, |s| s.border_color(color.panel_focused_border))
+            .when(self.focused, |s| {
+                s.when(self.docked_right, |s| s.border_r_2())
+                    .border_color(color.border_focused)
+            })
             .hover(|s| s.bg(color.element_hover))
             .on_hover(self.on_hover)
             .child(
@@ -270,20 +286,8 @@ impl RenderOnce for ThreadItem {
                             .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)),
                     )
                     .child(gradient_overlay)
-                    .when(running_or_action, |this| {
-                        this.child(
-                            h_flex()
-                                .gap_1()
-                                .when(is_running, |this| {
-                                    this.child(
-                                        icon_container()
-                                            .child(SpinnerLabel::new().color(Color::Accent)),
-                                    )
-                                })
-                                .when(self.hovered, |this| {
-                                    this.when_some(self.action_slot, |this, slot| this.child(slot))
-                                }),
-                        )
+                    .when(self.hovered, |this| {
+                        this.when_some(self.action_slot, |this, slot| this.child(slot))
                     }),
             )
             .when_some(self.worktree, |this, worktree| {
@@ -306,6 +310,7 @@ impl RenderOnce for ThreadItem {
                         .gap_1p5()
                         .child(icon_container()) // Icon Spacing
                         .child(worktree_label)
+                        .child(dot_separator())
                         .when(has_diff_stats, |this| {
                             this.child(DiffStat::new(
                                 diff_stat_id.clone(),

crates/ui/src/components/list/list_item.rs 🔗

@@ -48,6 +48,7 @@ pub struct ListItem {
     rounded: bool,
     overflow_x: bool,
     focused: Option<bool>,
+    docked_right: bool,
 }
 
 impl ListItem {
@@ -78,6 +79,7 @@ impl ListItem {
             rounded: false,
             overflow_x: false,
             focused: None,
+            docked_right: false,
         }
     }
 
@@ -194,6 +196,11 @@ impl ListItem {
         self.focused = Some(focused);
         self
     }
+
+    pub fn docked_right(mut self, docked_right: bool) -> Self {
+        self.docked_right = docked_right;
+        self
+    }
 }
 
 impl Disableable for ListItem {
@@ -247,6 +254,7 @@ impl RenderOnce for ListItem {
                 this.when_some(self.focused, |this, focused| {
                     if focused {
                         this.border_1()
+                            .when(self.docked_right, |this| this.border_r_2())
                             .border_color(cx.theme().colors().border_focused)
                     } else {
                         this.border_1()