agent_ui: Add more UI refinements to sidebar (#51545)

Danilo Leal created

- Move archive button to the header for simplicity
- Hook up the delete button in the archive view
- Improve how titles are displayed before summary is generated
- Hook up keybinding for deleting threads in both the sidebar and
archive view

Release Notes:

- N/A

Change summary

assets/keymaps/default-linux.json           |   3 
assets/keymaps/default-macos.json           |   3 
assets/keymaps/default-windows.json         |   3 
crates/acp_thread/src/acp_thread.rs         |   4 
crates/agent/src/thread.rs                  |   8 +
crates/agent_ui/src/agent_panel.rs          |  59 ++++++-----
crates/agent_ui/src/sidebar.rs              | 115 +++++++++++++++-------
crates/agent_ui/src/threads_archive_view.rs |  89 ++++++++++++++++-
crates/ui/src/components/ai/thread_item.rs  |  25 ++++
9 files changed, 230 insertions(+), 79 deletions(-)

Detailed changes

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

@@ -671,13 +671,14 @@
     },
   },
   {
-    "context": "WorkspaceSidebar",
+    "context": "ThreadsSidebar",
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-n": "multi_workspace::NewWorkspaceInWindow",
       "left": "agents_sidebar::CollapseSelectedEntry",
       "right": "agents_sidebar::ExpandSelectedEntry",
       "enter": "menu::Confirm",
+      "shift-backspace": "agent::RemoveSelectedThread",
     },
   },
   {

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

@@ -739,13 +739,14 @@
     },
   },
   {
-    "context": "WorkspaceSidebar",
+    "context": "ThreadsSidebar",
     "use_key_equivalents": true,
     "bindings": {
       "cmd-n": "multi_workspace::NewWorkspaceInWindow",
       "left": "agents_sidebar::CollapseSelectedEntry",
       "right": "agents_sidebar::ExpandSelectedEntry",
       "enter": "menu::Confirm",
+      "shift-backspace": "agent::RemoveSelectedThread",
     },
   },
   {

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

@@ -675,13 +675,14 @@
     },
   },
   {
-    "context": "WorkspaceSidebar",
+    "context": "ThreadsSidebar",
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-n": "multi_workspace::NewWorkspaceInWindow",
       "left": "agents_sidebar::CollapseSelectedEntry",
       "right": "agents_sidebar::ExpandSelectedEntry",
       "enter": "menu::Confirm",
+      "shift-backspace": "agent::RemoveSelectedThread",
     },
   },
   {

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

@@ -1207,6 +1207,10 @@ impl AcpThread {
             .unwrap_or_else(|| self.title.clone())
     }
 
+    pub fn has_provisional_title(&self) -> bool {
+        self.provisional_title.is_some()
+    }
+
     pub fn entries(&self) -> &[AgentThreadEntry] {
         &self.entries
     }

crates/agent/src/thread.rs πŸ”—

@@ -2570,6 +2570,14 @@ impl Thread {
                 .is_some()
             {
                 _ = this.update(cx, |this, cx| this.set_title(title.into(), cx));
+            } else {
+                // Emit TitleUpdated even on failure so that the propagation
+                // chain (agent::Thread β†’ NativeAgent β†’ AcpThread) fires and
+                // clears any provisional title that was set before the turn.
+                _ = this.update(cx, |_, cx| {
+                    cx.emit(TitleUpdated);
+                    cx.notify();
+                });
             }
             _ = this.update(cx, |this, _| this.pending_title_generation = None);
         }));

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

@@ -3266,48 +3266,49 @@ impl AgentPanel {
 
         let content = match &self.active_view {
             ActiveView::AgentThread { server_view } => {
-                let is_generating_title = server_view
-                    .read(cx)
-                    .as_native_thread(cx)
-                    .map_or(false, |t| t.read(cx).is_generating_title());
+                let server_view_ref = server_view.read(cx);
+                let is_generating_title = server_view_ref.as_native_thread(cx).is_some()
+                    && server_view_ref.parent_thread(cx).map_or(false, |tv| {
+                        tv.read(cx).thread.read(cx).has_provisional_title()
+                    });
 
-                if let Some(title_editor) = server_view
-                    .read(cx)
+                if let Some(title_editor) = server_view_ref
                     .parent_thread(cx)
                     .map(|r| r.read(cx).title_editor.clone())
                 {
-                    let container = div()
-                        .w_full()
-                        .on_action({
-                            let thread_view = server_view.downgrade();
-                            move |_: &menu::Confirm, window, cx| {
-                                if let Some(thread_view) = thread_view.upgrade() {
-                                    thread_view.focus_handle(cx).focus(window, cx);
-                                }
-                            }
-                        })
-                        .on_action({
-                            let thread_view = server_view.downgrade();
-                            move |_: &editor::actions::Cancel, window, cx| {
-                                if let Some(thread_view) = thread_view.upgrade() {
-                                    thread_view.focus_handle(cx).focus(window, cx);
-                                }
-                            }
-                        })
-                        .child(title_editor);
-
                     if is_generating_title {
-                        container
+                        Label::new("New Thread…")
+                            .color(Color::Muted)
+                            .truncate()
                             .with_animation(
                                 "generating_title",
                                 Animation::new(Duration::from_secs(2))
                                     .repeat()
                                     .with_easing(pulsating_between(0.4, 0.8)),
-                                |div, delta| div.opacity(delta),
+                                |label, delta| label.alpha(delta),
                             )
                             .into_any_element()
                     } else {
-                        container.into_any_element()
+                        div()
+                            .w_full()
+                            .on_action({
+                                let thread_view = server_view.downgrade();
+                                move |_: &menu::Confirm, window, cx| {
+                                    if let Some(thread_view) = thread_view.upgrade() {
+                                        thread_view.focus_handle(cx).focus(window, cx);
+                                    }
+                                }
+                            })
+                            .on_action({
+                                let thread_view = server_view.downgrade();
+                                move |_: &editor::actions::Cancel, window, cx| {
+                                    if let Some(thread_view) = thread_view.upgrade() {
+                                        thread_view.focus_handle(cx).focus(window, cx);
+                                    }
+                                }
+                            })
+                            .child(title_editor)
+                            .into_any_element()
                     }
                 } else {
                     Label::new(server_view.read(cx).title(cx))

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

@@ -1,5 +1,5 @@
 use crate::threads_archive_view::{ThreadsArchiveView, ThreadsArchiveViewEvent};
-use crate::{Agent, AgentPanel, AgentPanelEvent, NewThread};
+use crate::{Agent, AgentPanel, AgentPanelEvent, NewThread, RemoveSelectedThread};
 use acp_thread::ThreadStatus;
 use action_log::DiffStats;
 use agent::ThreadStore;
@@ -83,6 +83,7 @@ struct ActiveThreadInfo {
     icon: IconName,
     icon_from_external_svg: Option<SharedString>,
     is_background: bool,
+    is_title_generating: bool,
     diff_stats: DiffStats,
 }
 
@@ -115,6 +116,7 @@ struct ThreadEntry {
     workspace: ThreadEntryWorkspace,
     is_live: bool,
     is_background: bool,
+    is_title_generating: bool,
     highlight_positions: Vec<usize>,
     worktree_name: Option<SharedString>,
     worktree_highlight_positions: Vec<usize>,
@@ -453,6 +455,8 @@ impl Sidebar {
                 let icon = thread_view_ref.agent_icon;
                 let icon_from_external_svg = thread_view_ref.agent_icon_from_external_svg.clone();
                 let title = thread.title();
+                let is_native = thread_view_ref.as_native_thread(cx).is_some();
+                let is_title_generating = is_native && thread.has_provisional_title();
                 let session_id = thread.session_id().clone();
                 let is_background = agent_panel_ref.is_background_thread(&session_id);
 
@@ -476,6 +480,7 @@ impl Sidebar {
                     icon,
                     icon_from_external_svg,
                     is_background,
+                    is_title_generating,
                     diff_stats,
                 }
             })
@@ -593,6 +598,7 @@ impl Sidebar {
                             workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                             is_live: false,
                             is_background: false,
+                            is_title_generating: false,
                             highlight_positions: Vec::new(),
                             worktree_name: None,
                             worktree_highlight_positions: Vec::new(),
@@ -646,6 +652,7 @@ impl Sidebar {
                                 workspace: target_workspace.clone(),
                                 is_live: false,
                                 is_background: false,
+                                is_title_generating: false,
                                 highlight_positions: Vec::new(),
                                 worktree_name: Some(worktree_name.clone()),
                                 worktree_highlight_positions: Vec::new(),
@@ -676,6 +683,7 @@ impl Sidebar {
                         thread.icon_from_external_svg = info.icon_from_external_svg.clone();
                         thread.is_live = true;
                         thread.is_background = info.is_background;
+                        thread.is_title_generating = info.is_title_generating;
                         thread.diff_stats = info.diff_stats;
                     }
                 }
@@ -1030,6 +1038,7 @@ impl Sidebar {
             .end_hover_gradient_overlay(true)
             .end_hover_slot(
                 h_flex()
+                    .gap_1()
                     .when(workspace_count > 1, |this| {
                         this.child(
                             IconButton::new(
@@ -1588,7 +1597,6 @@ impl Sidebar {
         let Some(thread_store) = ThreadStore::try_global(cx) else {
             return;
         };
-        self.hovered_thread_index = None;
         thread_store.update(cx, |store, cx| {
             store
                 .delete_thread(session_id.clone(), cx)
@@ -1596,6 +1604,25 @@ impl Sidebar {
         });
     }
 
+    fn remove_selected_thread(
+        &mut self,
+        _: &RemoveSelectedThread,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(ix) = self.selection else {
+            return;
+        };
+        let Some(ListEntry::Thread(thread)) = self.contents.entries.get(ix) else {
+            return;
+        };
+        if thread.agent != Agent::NativeAgent {
+            return;
+        }
+        let session_id = thread.session_info.session_id.clone();
+        self.delete_thread(&session_id, cx);
+    }
+
     fn render_thread(
         &self,
         ix: usize,
@@ -1620,6 +1647,7 @@ impl Sidebar {
         let is_selected = self.focused_thread.as_ref() == Some(&session_info.session_id);
         let can_delete = thread.agent == Agent::NativeAgent;
         let session_id_for_delete = thread.session_info.session_id.clone();
+        let focus_handle = self.focus_handle.clone();
 
         let id = SharedString::from(format!("thread-entry-{}", ix));
 
@@ -1660,6 +1688,7 @@ impl Sidebar {
             .when_some(timestamp, |this, ts| this.timestamp(ts))
             .highlight_positions(thread.highlight_positions.to_vec())
             .status(thread.status)
+            .generating_title(thread.is_title_generating)
             .notified(has_notification)
             .when(thread.diff_stats.lines_added > 0, |this| {
                 this.added(thread.diff_stats.lines_added as usize)
@@ -1679,16 +1708,27 @@ impl Sidebar {
                 }
                 cx.notify();
             }))
-            .when((is_hovered || is_selected) && can_delete, |this| {
+            .when(is_hovered && can_delete, |this| {
                 this.action_slot(
                     IconButton::new("delete-thread", IconName::Trash)
                         .icon_size(IconSize::Small)
                         .icon_color(Color::Muted)
-                        .tooltip(Tooltip::text("Delete Thread"))
+                        .tooltip({
+                            let focus_handle = focus_handle.clone();
+                            move |_window, cx| {
+                                Tooltip::for_action_in(
+                                    "Delete Thread",
+                                    &RemoveSelectedThread,
+                                    &focus_handle,
+                                    cx,
+                                )
+                            }
+                        })
                         .on_click({
                             let session_id = session_id_for_delete.clone();
                             cx.listener(move |this, _, _window, cx| {
                                 this.delete_thread(&session_id, cx);
+                                cx.stop_propagation();
                             })
                         }),
                 )
@@ -1848,17 +1888,30 @@ impl Sidebar {
                 this.child(self.render_sidebar_toggle_button(false, cx))
             })
             .child(self.render_filter_input())
-            .when(has_query, |this| {
-                this.when(!docked_right, |this| this.pr_1p5()).child(
-                    IconButton::new("clear_filter", IconName::Close)
-                        .shape(IconButtonShape::Square)
-                        .tooltip(Tooltip::text("Clear Search"))
-                        .on_click(cx.listener(|this, _, window, cx| {
-                            this.reset_filter_editor_text(window, cx);
-                            this.update_entries(cx);
-                        })),
-                )
-            })
+            .child(
+                h_flex()
+                    .gap_0p5()
+                    .when(!docked_right, |this| this.pr_1p5())
+                    .when(has_query, |this| {
+                        this.child(
+                            IconButton::new("clear_filter", IconName::Close)
+                                .shape(IconButtonShape::Square)
+                                .tooltip(Tooltip::text("Clear Search"))
+                                .on_click(cx.listener(|this, _, window, cx| {
+                                    this.reset_filter_editor_text(window, cx);
+                                    this.update_entries(cx);
+                                })),
+                        )
+                    })
+                    .child(
+                        IconButton::new("archive", IconName::Archive)
+                            .icon_size(IconSize::Small)
+                            .tooltip(Tooltip::text("Archive"))
+                            .on_click(cx.listener(|this, _, window, cx| {
+                                this.show_archive(window, cx);
+                            })),
+                    ),
+            )
             .when(docked_right, |this| {
                 this.pl_2()
                     .pr_0p5()
@@ -1866,27 +1919,6 @@ impl Sidebar {
             })
     }
 
-    fn render_thread_list_footer(&self, cx: &mut Context<Self>) -> impl IntoElement {
-        h_flex()
-            .p_1p5()
-            .border_t_1()
-            .border_color(cx.theme().colors().border_variant)
-            .child(
-                Button::new("view-archive", "Archive")
-                    .full_width()
-                    .label_size(LabelSize::Small)
-                    .style(ButtonStyle::Outlined)
-                    .start_icon(
-                        Icon::new(IconName::Archive)
-                            .size(IconSize::XSmall)
-                            .color(Color::Muted),
-                    )
-                    .on_click(cx.listener(|this, _, window, cx| {
-                        this.show_archive(window, cx);
-                    })),
-            )
-    }
-
     fn render_sidebar_toggle_button(
         &self,
         docked_right: bool,
@@ -2064,7 +2096,7 @@ impl Render for Sidebar {
 
         v_flex()
             .id("workspace-sidebar")
-            .key_context("WorkspaceSidebar")
+            .key_context("ThreadsSidebar")
             .track_focus(&self.focus_handle)
             .on_action(cx.listener(Self::select_next))
             .on_action(cx.listener(Self::select_previous))
@@ -2076,6 +2108,7 @@ impl Render for Sidebar {
             .on_action(cx.listener(Self::expand_selected_entry))
             .on_action(cx.listener(Self::collapse_selected_entry))
             .on_action(cx.listener(Self::cancel))
+            .on_action(cx.listener(Self::remove_selected_thread))
             .font(ui_font)
             .size_full()
             .bg(cx.theme().colors().surface_background)
@@ -2097,8 +2130,7 @@ impl Render for Sidebar {
                             )
                             .when_some(sticky_header, |this, header| this.child(header))
                             .vertical_scrollbar_for(&self.list_state, window, cx),
-                    )
-                    .child(self.render_thread_list_footer(cx)),
+                    ),
                 SidebarView::Archive => {
                     if let Some(archive_view) = &self.archive_view {
                         this.child(archive_view.clone())
@@ -2641,6 +2673,7 @@ mod tests {
                     workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: false,
                     is_background: false,
+                    is_title_generating: false,
                     highlight_positions: Vec::new(),
                     worktree_name: None,
                     worktree_highlight_positions: Vec::new(),
@@ -2663,6 +2696,7 @@ mod tests {
                     workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: true,
                     is_background: false,
+                    is_title_generating: false,
                     highlight_positions: Vec::new(),
                     worktree_name: None,
                     worktree_highlight_positions: Vec::new(),
@@ -2685,6 +2719,7 @@ mod tests {
                     workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: true,
                     is_background: false,
+                    is_title_generating: false,
                     highlight_positions: Vec::new(),
                     worktree_name: None,
                     worktree_highlight_positions: Vec::new(),
@@ -2707,6 +2742,7 @@ mod tests {
                     workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: false,
                     is_background: false,
+                    is_title_generating: false,
                     highlight_positions: Vec::new(),
                     worktree_name: None,
                     worktree_highlight_positions: Vec::new(),
@@ -2729,6 +2765,7 @@ mod tests {
                     workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: true,
                     is_background: true,
+                    is_title_generating: false,
                     highlight_positions: Vec::new(),
                     worktree_name: None,
                     worktree_highlight_positions: Vec::new(),

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

@@ -1,8 +1,12 @@
 use std::sync::Arc;
 
-use crate::{Agent, agent_connection_store::AgentConnectionStore, thread_history::ThreadHistory};
+use crate::{
+    Agent, RemoveSelectedThread, agent_connection_store::AgentConnectionStore,
+    thread_history::ThreadHistory,
+};
 use acp_thread::AgentSessionInfo;
 use agent::ThreadStore;
+use agent_client_protocol as acp;
 use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
 use editor::Editor;
 use fs::Fs;
@@ -109,6 +113,7 @@ pub struct ThreadsArchiveView {
     list_state: ListState,
     items: Vec<ArchiveListItem>,
     selection: Option<usize>,
+    hovered_index: Option<usize>,
     filter_editor: Entity<Editor>,
     _subscriptions: Vec<gpui::Subscription>,
     selected_agent_menu: PopoverMenuHandle<ContextMenu>,
@@ -152,6 +157,7 @@ impl ThreadsArchiveView {
             list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
             items: Vec::new(),
             selection: None,
+            hovered_index: None,
             filter_editor,
             _subscriptions: vec![filter_editor_subscription],
             selected_agent_menu: PopoverMenuHandle::default(),
@@ -272,6 +278,37 @@ impl ThreadsArchiveView {
         });
     }
 
+    fn delete_thread(&mut self, session_id: &acp::SessionId, cx: &mut Context<Self>) {
+        let Some(history) = &self.history else {
+            return;
+        };
+        if !history.read(cx).supports_delete() {
+            return;
+        }
+        let session_id = session_id.clone();
+        history.update(cx, |history, cx| {
+            history
+                .delete_session(&session_id, cx)
+                .detach_and_log_err(cx);
+        });
+    }
+
+    fn remove_selected_thread(
+        &mut self,
+        _: &RemoveSelectedThread,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(ix) = self.selection else {
+            return;
+        };
+        let Some(ArchiveListItem::Entry { session, .. }) = self.items.get(ix) else {
+            return;
+        };
+        let session_id = session.session_id.clone();
+        self.delete_thread(&session_id, cx);
+    }
+
     fn is_selectable_item(&self, ix: usize) -> bool {
         matches!(self.items.get(ix), Some(ArchiveListItem::Entry { .. }))
     }
@@ -377,9 +414,17 @@ impl ThreadsArchiveView {
                 highlight_positions,
             } => {
                 let is_selected = self.selection == Some(ix);
+                let hovered = self.hovered_index == Some(ix);
+                let supports_delete = self
+                    .history
+                    .as_ref()
+                    .map(|h| h.read(cx).supports_delete())
+                    .unwrap_or(false);
                 let title: SharedString =
                     session.title.clone().unwrap_or_else(|| "Untitled".into());
                 let session_info = session.clone();
+                let session_id_for_delete = session.session_id.clone();
+                let focus_handle = self.focus_handle.clone();
                 let highlight_positions = highlight_positions.clone();
 
                 let timestamp = session.created_at.or(session.updated_at).map(|entry_time| {
@@ -429,12 +474,45 @@ impl ThreadsArchiveView {
                             .gap_2()
                             .justify_between()
                             .child(title_label)
-                            .when_some(timestamp, |this, ts| {
-                                this.child(
-                                    Label::new(ts).size(LabelSize::Small).color(Color::Muted),
-                                )
+                            .when(!(hovered && supports_delete), |this| {
+                                this.when_some(timestamp, |this, ts| {
+                                    this.child(
+                                        Label::new(ts).size(LabelSize::Small).color(Color::Muted),
+                                    )
+                                })
                             }),
                     )
+                    .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
+                        if *is_hovered {
+                            this.hovered_index = Some(ix);
+                        } else if this.hovered_index == Some(ix) {
+                            this.hovered_index = None;
+                        }
+                        cx.notify();
+                    }))
+                    .end_slot::<IconButton>(if hovered && supports_delete {
+                        Some(
+                            IconButton::new("delete-thread", IconName::Trash)
+                                .icon_size(IconSize::Small)
+                                .icon_color(Color::Muted)
+                                .tooltip({
+                                    move |_window, cx| {
+                                        Tooltip::for_action_in(
+                                            "Delete Thread",
+                                            &RemoveSelectedThread,
+                                            &focus_handle,
+                                            cx,
+                                        )
+                                    }
+                                })
+                                .on_click(cx.listener(move |this, _, _, cx| {
+                                    this.delete_thread(&session_id_for_delete, cx);
+                                    cx.stop_propagation();
+                                })),
+                        )
+                    } else {
+                        None
+                    })
                     .on_click(cx.listener(move |this, _, window, cx| {
                         this.open_thread(session_info.clone(), window, cx);
                     }))
@@ -683,6 +761,7 @@ impl Render for ThreadsArchiveView {
             .on_action(cx.listener(Self::select_first))
             .on_action(cx.listener(Self::select_last))
             .on_action(cx.listener(Self::confirm))
+            .on_action(cx.listener(Self::remove_selected_thread))
             .size_full()
             .bg(cx.theme().colors().surface_background)
             .child(self.render_header(cx))

crates/ui/src/components/ai/thread_item.rs πŸ”—

@@ -3,7 +3,8 @@ use crate::{
     IconDecorationKind, prelude::*,
 };
 
-use gpui::{AnyView, ClickEvent, Hsla, SharedString};
+use gpui::{Animation, AnimationExt, AnyView, ClickEvent, Hsla, SharedString, pulsating_between};
+use std::time::Duration;
 
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
 pub enum AgentThreadStatus {
@@ -23,6 +24,7 @@ pub struct ThreadItem {
     timestamp: SharedString,
     notified: bool,
     status: AgentThreadStatus,
+    generating_title: bool,
     selected: bool,
     focused: bool,
     hovered: bool,
@@ -48,6 +50,7 @@ impl ThreadItem {
             timestamp: "".into(),
             notified: false,
             status: AgentThreadStatus::default(),
+            generating_title: false,
             selected: false,
             focused: false,
             hovered: false,
@@ -89,6 +92,11 @@ impl ThreadItem {
         self
     }
 
+    pub fn generating_title(mut self, generating: bool) -> Self {
+        self.generating_title = generating;
+        self
+    }
+
     pub fn selected(mut self, selected: bool) -> Self {
         self.selected = selected;
         self
@@ -221,7 +229,18 @@ impl RenderOnce for ThreadItem {
 
         let title = self.title;
         let highlight_positions = self.highlight_positions;
-        let title_label = if highlight_positions.is_empty() {
+        let title_label = if self.generating_title {
+            Label::new("New Thread…")
+                .color(Color::Muted)
+                .with_animation(
+                    "generating-title",
+                    Animation::new(Duration::from_secs(2))
+                        .repeat()
+                        .with_easing(pulsating_between(0.4, 0.8)),
+                    |label, delta| label.alpha(delta),
+                )
+                .into_any_element()
+        } else if highlight_positions.is_empty() {
             Label::new(title).into_any_element()
         } else {
             HighlightedLabel::new(title, highlight_positions).into_any_element()
@@ -283,7 +302,7 @@ impl RenderOnce for ThreadItem {
                             .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)),
                     )
                     .child(gradient_overlay)
-                    .when(self.hovered || self.selected, |this| {
+                    .when(self.hovered, |this| {
                         this.when_some(self.action_slot, |this, slot| {
                             let overlay = GradientFade::new(
                                 base_bg,