agent_ui: Enable deleting a thread from the sidebar (#51532)

Danilo Leal created

Currently only available for native threads.

Release Notes:

- N/A

Change summary

crates/agent_ui/src/sidebar.rs             | 48 ++++++++++++++++++++++-
crates/ui/src/components/ai/thread_item.rs | 18 +++++++-
2 files changed, 60 insertions(+), 6 deletions(-)

Detailed changes

crates/agent_ui/src/sidebar.rs 🔗

@@ -243,6 +243,7 @@ pub struct Sidebar {
     selection: Option<usize>,
     focused_thread: Option<acp::SessionId>,
     active_entry_index: Option<usize>,
+    hovered_thread_index: Option<usize>,
     collapsed_groups: HashSet<PathList>,
     expanded_groups: HashMap<PathList, usize>,
     view: SidebarView,
@@ -345,6 +346,7 @@ impl Sidebar {
             selection: None,
             focused_thread: None,
             active_entry_index: None,
+            hovered_thread_index: None,
             collapsed_groups: HashSet::new(),
             expanded_groups: HashMap::new(),
             view: SidebarView::default(),
@@ -1582,11 +1584,23 @@ impl Sidebar {
         }
     }
 
+    fn delete_thread(&mut self, session_id: &acp::SessionId, cx: &mut Context<Self>) {
+        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)
+                .detach_and_log_err(cx);
+        });
+    }
+
     fn render_thread(
         &self,
         ix: usize,
         thread: &ThreadEntry,
-        is_selected: bool,
+        is_focused: bool,
         docked_right: bool,
         cx: &mut Context<Self>,
     ) -> AnyElement {
@@ -1602,6 +1616,11 @@ impl Sidebar {
         let session_info = thread.session_info.clone();
         let thread_workspace = thread.workspace.clone();
 
+        let is_hovered = self.hovered_thread_index == Some(ix);
+        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 id = SharedString::from(format!("thread-entry-{}", ix));
 
         let timestamp = thread
@@ -1648,9 +1667,32 @@ impl Sidebar {
             .when(thread.diff_stats.lines_removed > 0, |this| {
                 this.removed(thread.diff_stats.lines_removed as usize)
             })
-            .selected(self.focused_thread.as_ref() == Some(&session_info.session_id))
-            .focused(is_selected)
+            .selected(is_selected)
+            .focused(is_focused)
             .docked_right(docked_right)
+            .hovered(is_hovered)
+            .on_hover(cx.listener(move |this, is_hovered: &bool, _window, cx| {
+                if *is_hovered {
+                    this.hovered_thread_index = Some(ix);
+                } else if this.hovered_thread_index == Some(ix) {
+                    this.hovered_thread_index = None;
+                }
+                cx.notify();
+            }))
+            .when((is_hovered || is_selected) && 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"))
+                        .on_click({
+                            let session_id = session_id_for_delete.clone();
+                            cx.listener(move |this, _, _window, cx| {
+                                this.delete_thread(&session_id, cx);
+                            })
+                        }),
+                )
+            })
             .on_click({
                 let agent = thread.agent.clone();
                 cx.listener(move |this, _, window, cx| {

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

@@ -283,8 +283,20 @@ impl RenderOnce for ThreadItem {
                             .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)),
                     )
                     .child(gradient_overlay)
-                    .when(self.hovered, |this| {
-                        this.when_some(self.action_slot, |this, slot| this.child(slot))
+                    .when(self.hovered || self.selected, |this| {
+                        this.when_some(self.action_slot, |this, slot| {
+                            let overlay = GradientFade::new(
+                                base_bg,
+                                color.element_hover,
+                                color.element_active,
+                            )
+                            .width(px(64.0))
+                            .right(px(6.))
+                            .gradient_stop(0.75)
+                            .group_name("thread-item");
+
+                            this.child(h_flex().relative().child(overlay).child(slot))
+                        })
                     }),
             )
             .when_some(self.worktree, |this, worktree| {
@@ -337,7 +349,7 @@ impl RenderOnce for ThreadItem {
                         .when(has_diff_stats, |this| {
                             this.child(
                                 DiffStat::new(diff_stat_id, added_count, removed_count)
-                                    .tooltip("Unreviewed changes"),
+                                    .tooltip("Unreviewed Changes"),
                             )
                         })
                         .when(has_diff_stats && has_timestamp, |this| {