agent_ui: Add more refinements for v2 flag (#50968)

Danilo Leal created

- Fixes message editor in empty state not consuming the whole height of
the panel
- Remove duped focused method in the `ListItem`
- Remove duped "new thread" buttons when group is empty
- Add some UI adjustments like removing labels and fading out truncated
items

Release Notes:

- N/A

Change summary

crates/agent_ui/src/agent_panel.rs                 |   2 
crates/agent_ui/src/connection_view/thread_view.rs |  26 +
crates/agent_ui/src/message_editor.rs              |   6 
crates/sidebar/src/sidebar.rs                      | 227 ++++++++-------
crates/ui/src/components/ai/thread_item.rs         |  66 +++-
crates/ui/src/components/list/list_item.rs         |  14 
6 files changed, 202 insertions(+), 139 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -3685,7 +3685,7 @@ impl AgentPanel {
                     h_flex()
                         .gap_1()
                         .child(agent_icon_element)
-                        .child(Label::new(selected_agent_label).color(label_color))
+                        .child(Label::new(selected_agent_label).color(label_color).ml_0p5())
                         .child(
                             Icon::new(chevron_icon)
                                 .color(icon_color)

crates/agent_ui/src/connection_view/thread_view.rs 🔗

@@ -2715,6 +2715,31 @@ impl ThreadView {
             (IconName::Maximize, "Expand Message Editor")
         };
 
+        if v2_empty_state {
+            self.message_editor.update(cx, |editor, cx| {
+                editor.set_mode(
+                    EditorMode::Full {
+                        scale_ui_elements_with_buffer_font_size: false,
+                        show_active_line_background: false,
+                        sizing_behavior: SizingBehavior::Default,
+                    },
+                    cx,
+                );
+            });
+        } else {
+            self.message_editor.update(cx, |editor, cx| {
+                editor.set_mode(
+                    EditorMode::AutoHeight {
+                        min_lines: AgentSettings::get_global(cx).message_editor_min_lines,
+                        max_lines: Some(
+                            AgentSettings::get_global(cx).set_message_editor_max_lines(),
+                        ),
+                    },
+                    cx,
+                );
+            });
+        }
+
         v_flex()
             .on_action(cx.listener(Self::expand_message_editor))
             .p_2()
@@ -2731,6 +2756,7 @@ impl ThreadView {
                 v_flex()
                     .relative()
                     .size_full()
+                    .when(v2_empty_state, |this| this.flex_1())
                     .pt_1()
                     .pr_2p5()
                     .child(self.message_editor.clone())

crates/agent_ui/src/message_editor.rs 🔗

@@ -1222,8 +1222,10 @@ impl MessageEditor {
 
     pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
         self.editor.update(cx, |editor, cx| {
-            editor.set_mode(mode);
-            cx.notify()
+            if *editor.mode() != mode {
+                editor.set_mode(mode);
+                cx.notify()
+            }
         });
     }
 

crates/sidebar/src/sidebar.rs 🔗

@@ -73,6 +73,7 @@ enum ListEntry {
         label: SharedString,
         workspace: Entity<Workspace>,
         highlight_positions: Vec<usize>,
+        has_threads: bool,
     },
     Thread {
         session_info: acp_thread::AgentSessionInfo,
@@ -322,10 +323,15 @@ impl Sidebar {
             window,
             |this, agent_panel, event: &AgentPanelEvent, _window, cx| match event {
                 AgentPanelEvent::ActiveViewChanged => {
-                    if let Some(thread) = agent_panel.read(cx).active_connection_view()
-                        && let Some(session_id) = thread.read(cx).parent_id(cx)
-                    {
-                        this.focused_thread = Some(session_id);
+                    match agent_panel.read(cx).active_connection_view() {
+                        Some(thread) => {
+                            if let Some(session_id) = thread.read(cx).parent_id(cx) {
+                                this.focused_thread = Some(session_id);
+                            }
+                        }
+                        None => {
+                            this.focused_thread = None;
+                        }
                     }
                     this.update_entries(cx);
                 }
@@ -334,7 +340,7 @@ impl Sidebar {
                         .read(cx)
                         .active_connection_view()
                         .and_then(|thread| thread.read(cx).parent_id(cx));
-                    if new_focused != this.focused_thread {
+                    if new_focused.is_some() && new_focused != this.focused_thread {
                         this.focused_thread = new_focused;
                         this.update_entries(cx);
                     }
@@ -522,6 +528,7 @@ impl Sidebar {
             }
 
             if !query.is_empty() {
+                let has_threads = !threads.is_empty();
                 let mut matched_threads = Vec::new();
                 for mut thread in threads {
                     if let ListEntry::Thread {
@@ -554,14 +561,17 @@ impl Sidebar {
                     label,
                     workspace: workspace.clone(),
                     highlight_positions: workspace_highlight_positions,
+                    has_threads,
                 });
                 entries.extend(matched_threads);
             } else {
+                let has_threads = !threads.is_empty();
                 entries.push(ListEntry::ProjectHeader {
                     path_list: path_list.clone(),
                     label,
                     workspace: workspace.clone(),
                     highlight_positions: Vec::new(),
+                    has_threads,
                 });
 
                 if is_collapsed {
@@ -677,12 +687,14 @@ impl Sidebar {
                 label,
                 workspace,
                 highlight_positions,
+                has_threads,
             } => self.render_project_header(
                 ix,
                 path_list,
                 label,
                 workspace,
                 highlight_positions,
+                *has_threads,
                 is_selected,
                 cx,
             ),
@@ -736,12 +748,12 @@ impl Sidebar {
         label: &SharedString,
         workspace: &Entity<Workspace>,
         highlight_positions: &[usize],
+        has_threads: bool,
         is_selected: bool,
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let id = SharedString::from(format!("project-header-{}", ix));
         let ib_id = SharedString::from(format!("project-header-new-thread-{}", ix));
-        let group = SharedString::from(format!("group-{}", ix));
 
         let is_collapsed = self.collapsed_groups.contains(path_list);
         let disclosure_icon = if is_collapsed {
@@ -774,20 +786,19 @@ impl Sidebar {
                 .into_any_element()
         };
 
-        // TODO: if is_selected, draw a blue border around the item.
-
         ListItem::new(id)
-            .selection_outlined(is_selected)
-            .group_name(&group)
             .toggle_state(is_active_workspace)
+            .focused(is_selected)
             .child(
-                h_flex().px_1().py_1p5().gap_0p5().child(label).child(
-                    div().visible_on_hover(group).child(
+                h_flex()
+                    .p_1()
+                    .gap_1p5()
+                    .child(
                         Icon::new(disclosure_icon)
                             .size(IconSize::Small)
-                            .color(Color::Muted),
-                    ),
-                ),
+                            .color(Color::Custom(cx.theme().colors().icon_muted.opacity(0.6))),
+                    )
+                    .child(label),
             )
             .end_hover_slot(
                 h_flex()
@@ -808,18 +819,21 @@ impl Sidebar {
                             )),
                         )
                     })
-                    .child(
-                        IconButton::new(ib_id, IconName::NewThread)
-                            .icon_size(IconSize::Small)
-                            .icon_color(Color::Muted)
-                            .tooltip(Tooltip::text("New Thread"))
-                            .on_click(cx.listener(move |this, _, window, cx| {
-                                this.selection = None;
-                                this.create_new_thread(&workspace_for_new_thread, window, cx);
-                            })),
-                    ),
+                    .when(has_threads, |this| {
+                        this.child(
+                            IconButton::new(ib_id, IconName::NewThread)
+                                .icon_size(IconSize::Small)
+                                .icon_color(Color::Muted)
+                                .tooltip(Tooltip::text("New Thread"))
+                                .on_click(cx.listener(move |this, _, window, cx| {
+                                    this.selection = None;
+                                    this.create_new_thread(&workspace_for_new_thread, window, cx);
+                                })),
+                        )
+                    }),
             )
             .on_click(cx.listener(move |this, _, window, cx| {
+                this.selection = None;
                 this.toggle_collapse(&path_list_for_toggle, window, cx);
             }))
             // TODO: Decide if we really want the header to be activating different workspaces
@@ -887,12 +901,7 @@ impl Sidebar {
         self.update_entries(cx);
     }
 
-    fn focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
-        if self.selection.is_none() && !self.contents.entries.is_empty() {
-            self.selection = Some(0);
-            cx.notify();
-        }
-    }
+    fn focus_in(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
 
     fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
         if self.reset_filter_editor_text(window, cx) {
@@ -1122,7 +1131,7 @@ impl Sidebar {
             .status(status)
             .notified(has_notification)
             .selected(self.focused_thread.as_ref() == Some(&session_info.session_id))
-            .outlined(is_selected)
+            .focused(is_selected)
             .on_click(cx.listener(move |this, _, window, cx| {
                 this.selection = None;
                 this.activate_thread(session_info.clone(), &workspace, window, cx);
@@ -1168,7 +1177,7 @@ impl Sidebar {
         let count = format!("({})", remaining_count);
 
         ListItem::new(id)
-            .selection_outlined(is_selected)
+            .focused(is_selected)
             .child(
                 h_flex()
                     .px_1()
@@ -1319,52 +1328,45 @@ impl Render for Sidebar {
                     .justify_between()
                     .border_b_1()
                     .border_color(cx.theme().colors().border)
-                    .child(
-                        h_flex()
-                            .gap_1()
-                            .child({
-                                let focus_handle_toggle = self.focus_handle.clone();
-                                let focus_handle_focus = self.focus_handle.clone();
-                                IconButton::new("close-sidebar", IconName::WorkspaceNavOpen)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::element(move |_, cx| {
-                                        v_flex()
-                                            .gap_1()
-                                            .child(
-                                                h_flex()
-                                                    .gap_2()
-                                                    .justify_between()
-                                                    .child(Label::new("Close Sidebar"))
-                                                    .child(KeyBinding::for_action_in(
-                                                        &ToggleWorkspaceSidebar,
-                                                        &focus_handle_toggle,
-                                                        cx,
-                                                    )),
-                                            )
-                                            .child(
-                                                h_flex()
-                                                    .pt_1()
-                                                    .gap_2()
-                                                    .border_t_1()
-                                                    .border_color(
-                                                        cx.theme().colors().border_variant,
-                                                    )
-                                                    .justify_between()
-                                                    .child(Label::new(focus_tooltip_label))
-                                                    .child(KeyBinding::for_action_in(
-                                                        &FocusWorkspaceSidebar,
-                                                        &focus_handle_focus,
-                                                        cx,
-                                                    )),
-                                            )
-                                            .into_any_element()
-                                    }))
-                                    .on_click(cx.listener(|_this, _, _window, cx| {
-                                        cx.emit(SidebarEvent::Close);
-                                    }))
-                            })
-                            .child(Label::new("Threads").size(LabelSize::Small)),
-                    )
+                    .child({
+                        let focus_handle_toggle = self.focus_handle.clone();
+                        let focus_handle_focus = self.focus_handle.clone();
+                        IconButton::new("close-sidebar", IconName::WorkspaceNavOpen)
+                            .icon_size(IconSize::Small)
+                            .tooltip(Tooltip::element(move |_, cx| {
+                                v_flex()
+                                    .gap_1()
+                                    .child(
+                                        h_flex()
+                                            .gap_2()
+                                            .justify_between()
+                                            .child(Label::new("Close Sidebar"))
+                                            .child(KeyBinding::for_action_in(
+                                                &ToggleWorkspaceSidebar,
+                                                &focus_handle_toggle,
+                                                cx,
+                                            )),
+                                    )
+                                    .child(
+                                        h_flex()
+                                            .pt_1()
+                                            .gap_2()
+                                            .border_t_1()
+                                            .border_color(cx.theme().colors().border_variant)
+                                            .justify_between()
+                                            .child(Label::new(focus_tooltip_label))
+                                            .child(KeyBinding::for_action_in(
+                                                &FocusWorkspaceSidebar,
+                                                &focus_handle_focus,
+                                                cx,
+                                            )),
+                                    )
+                                    .into_any_element()
+                            }))
+                            .on_click(cx.listener(|_this, _, _window, cx| {
+                                cx.emit(SidebarEvent::Close);
+                            }))
+                    })
                     .child(
                         IconButton::new("open-project", IconName::OpenFolder)
                             .icon_size(IconSize::Small)
@@ -1852,6 +1854,7 @@ mod tests {
                     label: "expanded-project".into(),
                     workspace: workspace.clone(),
                     highlight_positions: Vec::new(),
+                    has_threads: true,
                 },
                 // Thread with default (Completed) status, not active
                 ListEntry::Thread {
@@ -1954,6 +1957,7 @@ mod tests {
                     label: "collapsed-project".into(),
                     workspace: workspace.clone(),
                     highlight_positions: Vec::new(),
+                    has_threads: true,
                 },
             ];
             // Select the Running thread (index 2)
@@ -2014,11 +2018,16 @@ mod tests {
         cx.run_until_parked();
 
         // Entries: [header, thread3, thread2, thread1]
-        // Focusing the sidebar triggers focus_in, which selects the first entry
+        // Focusing the sidebar does not set a selection; select_next/select_previous
+        // handle None gracefully by starting from the first or last entry.
         open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
+
+        // First SelectNext from None starts at index 0
+        cx.dispatch_action(SelectNext);
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
 
-        // Move down through all entries
+        // Move down through remaining entries
         cx.dispatch_action(SelectNext);
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1));
 
@@ -2072,7 +2081,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_keyboard_focus_in_selects_first(cx: &mut TestAppContext) {
+    async fn test_keyboard_focus_in_does_not_set_selection(cx: &mut TestAppContext) {
         let project = init_test_project("/my-project", cx).await;
         let (multi_workspace, cx) =
             cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
@@ -2081,11 +2090,16 @@ mod tests {
         // Initially no selection
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
 
-        // Open the sidebar so it's rendered, then focus it to trigger focus_in
+        // Open the sidebar so it's rendered, then focus it to trigger focus_in.
+        // focus_in no longer sets a default selection.
         open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
-        assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
+        assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
+
+        // Manually set a selection, blur, then refocus — selection should be preserved
+        sidebar.update_in(cx, |sidebar, _window, _cx| {
+            sidebar.selection = Some(0);
+        });
 
-        // Blur the sidebar, then refocus — existing selection should be preserved
         cx.update(|window, _cx| {
             window.blur();
         });
@@ -2135,9 +2149,11 @@ mod tests {
             1
         );
 
-        // Focus the sidebar — focus_in selects the header (index 0)
+        // Focus the sidebar and manually select the header (index 0)
         open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
-        assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
+        sidebar.update_in(cx, |sidebar, _window, _cx| {
+            sidebar.selection = Some(0);
+        });
 
         // Press confirm on project header (workspace 0) to activate it.
         cx.dispatch_action(Confirm);
@@ -2176,9 +2192,9 @@ mod tests {
         assert_eq!(entries.len(), 7);
         assert!(entries.iter().any(|e| e.contains("View More (3)")));
 
-        // Focus sidebar (selects index 0), then navigate down to the "View More" entry (index 6)
+        // Focus sidebar (selection starts at None), then navigate down to the "View More" entry (index 6)
         open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
-        for _ in 0..6 {
+        for _ in 0..7 {
             cx.dispatch_action(SelectNext);
         }
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(6));
@@ -2210,9 +2226,11 @@ mod tests {
             vec!["v [my-project]", "  Thread 1"]
         );
 
-        // Focus sidebar — focus_in selects the header (index 0). Press left to collapse.
+        // Focus sidebar and manually select the header (index 0). Press left to collapse.
         open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
-        assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
+        sidebar.update_in(cx, |sidebar, _window, _cx| {
+            sidebar.selection = Some(0);
+        });
 
         cx.dispatch_action(CollapseSelectedEntry);
         cx.run_until_parked();
@@ -2248,9 +2266,10 @@ mod tests {
         multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
         cx.run_until_parked();
 
-        // Focus sidebar (selects header at index 0), then navigate down to the thread (child)
+        // Focus sidebar (selection starts at None), then navigate down to the thread (child)
         open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
         cx.dispatch_action(SelectNext);
+        cx.dispatch_action(SelectNext);
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1));
 
         assert_eq!(
@@ -2282,8 +2301,12 @@ mod tests {
             vec!["v [empty-project]", "  [+ New Thread]"]
         );
 
-        // Focus sidebar — focus_in selects the first entry (header at 0)
+        // Focus sidebar — focus_in does not set a selection
         open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
+
+        // First SelectNext from None starts at index 0 (header)
+        cx.dispatch_action(SelectNext);
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
 
         // SelectNext moves to the new thread button
@@ -2311,9 +2334,10 @@ mod tests {
         multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
         cx.run_until_parked();
 
-        // Focus sidebar (selects header at 0), navigate down to the thread (index 1)
+        // Focus sidebar (selection starts at None), navigate down to the thread (index 1)
         open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
         cx.dispatch_action(SelectNext);
+        cx.dispatch_action(SelectNext);
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1));
 
         // Collapse the group, which removes the thread from the list
@@ -2935,9 +2959,11 @@ mod tests {
         cx.run_until_parked();
 
         // User focuses the sidebar and collapses the group using keyboard:
-        // select the header, then press CollapseSelectedEntry to collapse.
+        // manually select the header, then press CollapseSelectedEntry to collapse.
         open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
-        assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
+        sidebar.update_in(cx, |sidebar, _window, _cx| {
+            sidebar.selection = Some(0);
+        });
         cx.dispatch_action(CollapseSelectedEntry);
         cx.run_until_parked();
 
@@ -3151,15 +3177,12 @@ mod tests {
         });
         assert_eq!(sidebar.read_with(cx, |sidebar, _| sidebar.selection), None);
 
-        // When the user tabs back into the sidebar, focus_in restores
-        // selection to the first entry for keyboard navigation.
+        // When the user tabs back into the sidebar, focus_in no longer
+        // restores selection — it stays None.
         sidebar.update_in(cx, |sidebar, window, cx| {
             sidebar.focus_in(window, cx);
         });
-        assert_eq!(
-            sidebar.read_with(cx, |sidebar, _| sidebar.selection),
-            Some(0)
-        );
+        assert_eq!(sidebar.read_with(cx, |sidebar, _| sidebar.selection), None);
     }
 
     #[gpui::test]

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

@@ -3,7 +3,7 @@ use crate::{
     prelude::*,
 };
 
-use gpui::{AnyView, ClickEvent, Hsla, SharedString};
+use gpui::{AnyView, ClickEvent, Hsla, SharedString, linear_color_stop, linear_gradient};
 
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
 pub enum AgentThreadStatus {
@@ -24,7 +24,7 @@ pub struct ThreadItem {
     notified: bool,
     status: AgentThreadStatus,
     selected: bool,
-    outlined: bool,
+    focused: bool,
     hovered: bool,
     added: Option<usize>,
     removed: Option<usize>,
@@ -48,7 +48,7 @@ impl ThreadItem {
             notified: false,
             status: AgentThreadStatus::default(),
             selected: false,
-            outlined: false,
+            focused: false,
             hovered: false,
             added: None,
             removed: None,
@@ -92,8 +92,8 @@ impl ThreadItem {
         self
     }
 
-    pub fn outlined(mut self, outlined: bool) -> Self {
-        self.outlined = outlined;
+    pub fn focused(mut self, focused: bool) -> Self {
+        self.focused = focused;
         self
     }
 
@@ -153,7 +153,7 @@ impl ThreadItem {
 
 impl RenderOnce for ThreadItem {
     fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
-        let clr = cx.theme().colors();
+        let color = cx.theme().colors();
         // let dot_separator = || {
         //     Label::new("•")
         //         .size(LabelSize::Small)
@@ -161,7 +161,7 @@ impl RenderOnce for ThreadItem {
         //         .alpha(0.5)
         // };
 
-        let icon_container = || h_flex().size_4().justify_center();
+        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 {
             Icon::from_external_svg(custom_svg)
                 .color(Color::Muted)
@@ -189,7 +189,7 @@ impl RenderOnce for ThreadItem {
         } else if self.status == AgentThreadStatus::Error {
             Some(decoration(IconDecorationKind::X, cx.theme().status().error))
         } else if self.notified {
-            Some(decoration(IconDecorationKind::Dot, clr.text_accent))
+            Some(decoration(IconDecorationKind::Dot, color.text_accent))
         } else {
             None
         };
@@ -209,15 +209,41 @@ impl RenderOnce for ThreadItem {
         let title = self.title;
         let highlight_positions = self.highlight_positions;
         let title_label = if highlight_positions.is_empty() {
-            Label::new(title).truncate().into_any_element()
+            Label::new(title).into_any_element()
         } else {
-            HighlightedLabel::new(title, highlight_positions)
-                .truncate()
-                .into_any_element()
+            HighlightedLabel::new(title, highlight_positions).into_any_element()
         };
 
+        let base_bg = if self.selected {
+            color.element_active
+        } else {
+            color.panel_background
+        };
+
+        let gradient_overlay = div()
+            .absolute()
+            .top_0()
+            .right(px(-10.0))
+            .w_12()
+            .h_full()
+            .bg(linear_gradient(
+                90.,
+                linear_color_stop(base_bg, 0.6),
+                linear_color_stop(base_bg.opacity(0.0), 0.),
+            ))
+            .group_hover("thread-item", |s| {
+                s.bg(linear_gradient(
+                    90.,
+                    linear_color_stop(color.element_hover, 0.6),
+                    linear_color_stop(color.element_hover.opacity(0.0), 0.),
+                ))
+            });
+
         v_flex()
             .id(self.id.clone())
+            .group("thread-item")
+            .relative()
+            .overflow_hidden()
             .cursor_pointer()
             .w_full()
             .map(|this| {
@@ -227,11 +253,11 @@ impl RenderOnce for ThreadItem {
                     this.px_2().py_1()
                 }
             })
-            .when(self.selected, |s| s.bg(clr.element_active))
+            .when(self.selected, |s| s.bg(color.element_active))
             .border_1()
             .border_color(gpui::transparent_black())
-            .when(self.outlined, |s| s.border_color(clr.panel_focused_border))
-            .hover(|s| s.bg(clr.element_hover))
+            .when(self.focused, |s| s.border_color(color.panel_focused_border))
+            .hover(|s| s.bg(color.element_hover))
             .on_hover(self.on_hover)
             .child(
                 h_flex()
@@ -249,6 +275,7 @@ impl RenderOnce for ThreadItem {
                             .child(title_label)
                             .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)),
                     )
+                    .child(gradient_overlay)
                     .when(running_or_action, |this| {
                         this.child(
                             h_flex()
@@ -271,7 +298,6 @@ impl RenderOnce for ThreadItem {
                     Label::new(worktree)
                         .size(LabelSize::Small)
                         .color(Color::Muted)
-                        .truncate_start()
                         .into_any_element()
                 } else {
                     HighlightedLabel::new(worktree, worktree_highlight_positions)
@@ -420,25 +446,25 @@ impl Component for ThreadItem {
                     .into_any_element(),
             ),
             single_example(
-                "Outlined Item (Keyboard Selection)",
+                "Focused Item (Keyboard Selection)",
                 container()
                     .child(
                         ThreadItem::new("ti-7", "Implement keyboard navigation")
                             .icon(IconName::AiClaude)
                             .timestamp("4:00 PM")
-                            .outlined(true),
+                            .focused(true),
                     )
                     .into_any_element(),
             ),
             single_example(
-                "Selected + Outlined",
+                "Selected + Focused",
                 container()
                     .child(
                         ThreadItem::new("ti-8", "Active and keyboard-focused thread")
                             .icon(IconName::AiGemini)
                             .timestamp("5:00 PM")
                             .selected(true)
-                            .outlined(true),
+                            .focused(true),
                     )
                     .into_any_element(),
             ),

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

@@ -42,7 +42,6 @@ pub struct ListItem {
     selectable: bool,
     always_show_disclosure_icon: bool,
     outlined: bool,
-    selection_outlined: Option<bool>,
     rounded: bool,
     overflow_x: bool,
     focused: Option<bool>,
@@ -72,7 +71,6 @@ impl ListItem {
             selectable: true,
             always_show_disclosure_icon: false,
             outlined: false,
-            selection_outlined: None,
             rounded: false,
             overflow_x: false,
             focused: None,
@@ -173,11 +171,6 @@ impl ListItem {
         self
     }
 
-    pub fn selection_outlined(mut self, outlined: bool) -> Self {
-        self.selection_outlined = Some(outlined);
-        self
-    }
-
     pub fn rounded(mut self) -> Self {
         self.rounded = true;
         self
@@ -248,13 +241,6 @@ impl RenderOnce for ListItem {
                     })
             })
             .when(self.rounded, |this| this.rounded_sm())
-            .when_some(self.selection_outlined, |this, outlined| {
-                this.border_1()
-                    .border_color(gpui::transparent_black())
-                    .when(outlined, |this| {
-                        this.border_color(cx.theme().colors().panel_focused_border)
-                    })
-            })
             .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover))
             .child(
                 h_flex()