agent_ui: Add support for deleting thread history (#43370)

Remco Smits and Danilo Leal created

This PR adds support for deleting your entire thread history. This is
inspired by a Zed user from the meetup in Amsterdam, he was missing this
feature.

**Demo**


https://github.com/user-attachments/assets/5a195007-1094-4ec6-902a-1b83db5ec508

Release Notes:

- AI: Add support for deleting your entire thread history

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

crates/agent/src/db.rs                    | 16 ++++
crates/agent/src/history_store.rs         |  9 ++
crates/agent_ui/src/acp/thread_history.rs | 95 +++++++++++++++++++++++-
crates/agent_ui/src/agent_panel.rs        | 16 ++-
crates/agent_ui/src/agent_ui.rs           |  2 
5 files changed, 127 insertions(+), 11 deletions(-)

Detailed changes

crates/agent/src/db.rs 🔗

@@ -424,4 +424,20 @@ impl ThreadsDatabase {
             Ok(())
         })
     }
+
+    pub fn delete_threads(&self) -> Task<Result<()>> {
+        let connection = self.connection.clone();
+
+        self.executor.spawn(async move {
+            let connection = connection.lock();
+
+            let mut delete = connection.exec_bound::<()>(indoc! {"
+                DELETE FROM threads
+            "})?;
+
+            delete(())?;
+
+            Ok(())
+        })
+    }
 }

crates/agent/src/history_store.rs 🔗

@@ -188,6 +188,15 @@ impl HistoryStore {
         })
     }
 
+    pub fn delete_threads(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
+        let database_future = ThreadsDatabase::connect(cx);
+        cx.spawn(async move |this, cx| {
+            let database = database_future.await.map_err(|err| anyhow!(err))?;
+            database.delete_threads().await?;
+            this.update(cx, |this, cx| this.reload(cx))
+        })
+    }
+
     pub fn delete_text_thread(
         &mut self,
         path: Arc<Path>,

crates/agent_ui/src/acp/thread_history.rs 🔗

@@ -1,5 +1,5 @@
 use crate::acp::AcpThreadView;
-use crate::{AgentPanel, RemoveSelectedThread};
+use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
 use agent::{HistoryEntry, HistoryStore};
 use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
 use editor::{Editor, EditorEvent};
@@ -12,7 +12,7 @@ use std::{fmt::Display, ops::Range};
 use text::Bias;
 use time::{OffsetDateTime, UtcOffset};
 use ui::{
-    HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, WithScrollbar,
+    HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar,
     prelude::*,
 };
 
@@ -25,6 +25,7 @@ pub struct AcpThreadHistory {
     search_query: SharedString,
     visible_items: Vec<ListItemType>,
     local_timezone: UtcOffset,
+    confirming_delete_history: bool,
     _update_task: Task<()>,
     _subscriptions: Vec<gpui::Subscription>,
 }
@@ -98,6 +99,7 @@ impl AcpThreadHistory {
             )
             .unwrap(),
             search_query: SharedString::default(),
+            confirming_delete_history: false,
             _subscriptions: vec![search_editor_subscription, history_store_subscription],
             _update_task: Task::ready(()),
         };
@@ -331,6 +333,24 @@ impl AcpThreadHistory {
         task.detach_and_log_err(cx);
     }
 
+    fn remove_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
+        self.history_store.update(cx, |store, cx| {
+            store.delete_threads(cx).detach_and_log_err(cx)
+        });
+        self.confirming_delete_history = false;
+        cx.notify();
+    }
+
+    fn prompt_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
+        self.confirming_delete_history = true;
+        cx.notify();
+    }
+
+    fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
+        self.confirming_delete_history = false;
+        cx.notify();
+    }
+
     fn render_list_items(
         &mut self,
         range: Range<usize>,
@@ -447,6 +467,8 @@ impl Focusable for AcpThreadHistory {
 
 impl Render for AcpThreadHistory {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let has_no_history = self.history_store.read(cx).is_empty(cx);
+
         v_flex()
             .key_context("ThreadHistory")
             .size_full()
@@ -457,9 +479,12 @@ impl Render for AcpThreadHistory {
             .on_action(cx.listener(Self::select_last))
             .on_action(cx.listener(Self::confirm))
             .on_action(cx.listener(Self::remove_selected_thread))
+            .on_action(cx.listener(|this, _: &RemoveHistory, window, cx| {
+                this.remove_history(window, cx);
+            }))
             .child(
                 h_flex()
-                    .h(px(41.)) // Match the toolbar perfectly
+                    .h(Tab::container_height(cx))
                     .w_full()
                     .py_1()
                     .px_2()
@@ -481,7 +506,7 @@ impl Render for AcpThreadHistory {
                     .overflow_hidden()
                     .flex_grow();
 
-                if self.history_store.read(cx).is_empty(cx) {
+                if has_no_history {
                     view.justify_center().items_center().child(
                         Label::new("You don't have any past threads yet.")
                             .size(LabelSize::Small)
@@ -512,6 +537,68 @@ impl Render for AcpThreadHistory {
                     )
                 }
             })
+            .when(!has_no_history, |this| {
+                this.child(
+                    h_flex()
+                        .p_2()
+                        .border_t_1()
+                        .border_color(cx.theme().colors().border_variant)
+                        .when(!self.confirming_delete_history, |this| {
+                            this.child(
+                                Button::new("delete_history", "Delete All History")
+                                    .full_width()
+                                    .style(ButtonStyle::Outlined)
+                                    .label_size(LabelSize::Small)
+                                    .on_click(cx.listener(|this, _, window, cx| {
+                                        this.prompt_delete_history(window, cx);
+                                    })),
+                            )
+                        })
+                        .when(self.confirming_delete_history, |this| {
+                            this.w_full()
+                                .gap_2()
+                                .flex_wrap()
+                                .justify_between()
+                                .child(
+                                    h_flex()
+                                        .flex_wrap()
+                                        .gap_1()
+                                        .child(
+                                            Label::new("Delete all threads?")
+                                                .size(LabelSize::Small),
+                                        )
+                                        .child(
+                                            Label::new("You won't be able to recover them later.")
+                                                .size(LabelSize::Small)
+                                                .color(Color::Muted),
+                                        ),
+                                )
+                                .child(
+                                    h_flex()
+                                        .gap_1()
+                                        .child(
+                                            Button::new("cancel_delete", "Cancel")
+                                                .label_size(LabelSize::Small)
+                                                .on_click(cx.listener(|this, _, window, cx| {
+                                                    this.cancel_delete_history(window, cx);
+                                                })),
+                                        )
+                                        .child(
+                                            Button::new("confirm_delete", "Delete")
+                                                .style(ButtonStyle::Tinted(ui::TintColor::Error))
+                                                .color(Color::Error)
+                                                .label_size(LabelSize::Small)
+                                                .on_click(cx.listener(|_, _, window, cx| {
+                                                    window.dispatch_action(
+                                                        Box::new(RemoveHistory),
+                                                        cx,
+                                                    );
+                                                })),
+                                        ),
+                                )
+                        }),
+                )
+            })
     }
 }
 

crates/agent_ui/src/agent_panel.rs 🔗

@@ -20,10 +20,9 @@ use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
 use crate::ManageProfiles;
 use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
 use crate::{
-    AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
-    NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory,
-    ResetTrialEndUpsell, ResetTrialUpsell, ToggleNavigationMenu, ToggleNewThreadMenu,
-    ToggleOptionsMenu,
+    AddContextServer, AgentDiffPane, Follow, InlineAssistant, NewTextThread, NewThread,
+    OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell,
+    ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
     acp::AcpThreadView,
     agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
     slash_command::SlashCommandCompletionProvider,
@@ -614,11 +613,14 @@ impl AgentPanel {
                     if let Some(panel) = panel.upgrade() {
                         menu = Self::populate_recently_opened_menu_section(menu, panel, cx);
                     }
-                    menu.action("View All", Box::new(OpenHistory))
-                        .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
+
+                    menu = menu
+                        .action("View All", Box::new(OpenHistory))
                         .fixed_width(px(320.).into())
                         .keep_open_on_confirm(false)
-                        .key_context("NavigationMenu")
+                        .key_context("NavigationMenu");
+
+                    menu
                 });
             weak_panel
                 .update(cx, |panel, cx| {

crates/agent_ui/src/agent_ui.rs 🔗

@@ -69,6 +69,8 @@ actions!(
         CycleModeSelector,
         /// Expands the message editor to full size.
         ExpandMessageEditor,
+        /// Removes all thread history.
+        RemoveHistory,
         /// Opens the conversation history view.
         OpenHistory,
         /// Adds a context server to the configuration.