diff --git a/crates/agent/src/db.rs b/crates/agent/src/db.rs index 6b6312e48176c93fbfb12f97e26c7943c6cbf89a..d5166c5df931b6f7fad63769449aaa9784b5263f 100644 --- a/crates/agent/src/db.rs +++ b/crates/agent/src/db.rs @@ -424,4 +424,20 @@ impl ThreadsDatabase { Ok(()) }) } + + pub fn delete_threads(&self) -> Task> { + 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(()) + }) + } } diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs index 3bfbd99677feed5db53d96d2fa96316ac49abce4..efc0e3966d30fbc8bc7857c9da0404ce7dd4201f 100644 --- a/crates/agent/src/history_store.rs +++ b/crates/agent/src/history_store.rs @@ -188,6 +188,15 @@ impl HistoryStore { }) } + pub fn delete_threads(&mut self, cx: &mut Context) -> Task> { + 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, diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index 11718c63475212fbe8b996b2f6edae8b4295c91a..29759093303a684fdfd9ad255d269516ed7a29b9 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/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, local_timezone: UtcOffset, + confirming_delete_history: bool, _update_task: Task<()>, _subscriptions: Vec, } @@ -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.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.confirming_delete_history = true; + cx.notify(); + } + + fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context) { + self.confirming_delete_history = false; + cx.notify(); + } + fn render_list_items( &mut self, range: Range, @@ -447,6 +467,8 @@ impl Focusable for AcpThreadHistory { impl Render for AcpThreadHistory { fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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, + ); + })), + ), + ) + }), + ) + }) } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 22eb11e24a8fd706c80aa65c3dcf5d8ae3876ddc..aa152018b180047815cc461d80e48dba0996b3cd 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/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| { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index ae4cb70d4af419184519afb53ab62849b8a0eab8..5f5682b7dcc90d2b779744ba353380987a5907a1 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/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.