From a5b116c40c127b4639009254aa5cd4dfb1f84a5d Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Thu, 9 Apr 2026 12:25:08 +0100 Subject: [PATCH] agent_ui: Remove history view --- assets/keymaps/default-linux.json | 2 - assets/keymaps/default-macos.json | 2 - assets/keymaps/default-windows.json | 2 - crates/agent_ui/src/agent_panel.rs | 351 +------- crates/agent_ui/src/agent_ui.rs | 8 - crates/agent_ui/src/conversation_view.rs | 178 +---- .../src/conversation_view/thread_view.rs | 29 - crates/agent_ui/src/thread_history.rs | 42 +- crates/agent_ui/src/thread_history_view.rs | 751 ------------------ 9 files changed, 51 insertions(+), 1314 deletions(-) delete mode 100644 crates/agent_ui/src/thread_history_view.rs diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index fb305fe768931dd6f52f1b5d890ad6771b7b5cac..ab5bdf67c389333a11fc92df19c87067589a30bb 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -224,7 +224,6 @@ "context": "AgentPanel", "bindings": { "ctrl-n": "agent::NewThread", - "ctrl-shift-h": "agent::OpenHistory", "ctrl-alt-c": "agent::OpenSettings", "ctrl-alt-p": "agent::ManageProfiles", "ctrl-alt-l": "agent::OpenRulesLibrary", @@ -234,7 +233,6 @@ "alt-tab": "agent::CycleFavoriteModels", // `alt-l` is provided as an alternative to `alt-tab` as the latter breaks on Linux under the `AgentPanel` context "alt-l": "agent::CycleFavoriteModels", - "shift-alt-j": "agent::ToggleNavigationMenu", "shift-alt-i": "agent::ToggleOptionsMenu", "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "ctrl-shift-t": "agent::CycleStartThreadIn", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 5fb408640b2c5083f4d3379bf927178c96bed4b6..cbd9c5c014ca18c033f4746244bdc57ebafd03b4 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -264,7 +264,6 @@ "use_key_equivalents": true, "bindings": { "cmd-n": "agent::NewThread", - "cmd-shift-h": "agent::OpenHistory", "cmd-alt-c": "agent::OpenSettings", "cmd-alt-l": "agent::OpenRulesLibrary", "cmd-alt-p": "agent::ManageProfiles", @@ -272,7 +271,6 @@ "shift-tab": "agent::CycleModeSelector", "cmd-alt-/": "agent::ToggleModelSelector", "alt-tab": "agent::CycleFavoriteModels", - "cmd-shift-j": "agent::ToggleNavigationMenu", "cmd-alt-m": "agent::ToggleOptionsMenu", "cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "cmd-shift-t": "agent::CycleStartThreadIn", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 34d161577ee315857becf7c9e3c9353402e56876..6c4ce5010e1e4d7edc9e3ed58bacdef0a4cc5a33 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -225,7 +225,6 @@ "use_key_equivalents": true, "bindings": { "ctrl-n": "agent::NewThread", - "ctrl-shift-h": "agent::OpenHistory", "shift-alt-c": "agent::OpenSettings", "shift-alt-l": "agent::OpenRulesLibrary", "shift-alt-p": "agent::ManageProfiles", @@ -235,7 +234,6 @@ // `alt-l` is provided as an alternative to `alt-tab` as the latter breaks on Windows under the `AgentPanel` context "alt-l": "agent::CycleFavoriteModels", "shift-alt-/": "agent::ToggleModelSelector", - "shift-alt-j": "agent::ToggleNavigationMenu", "shift-alt-i": "agent::ToggleOptionsMenu", "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu", "ctrl-shift-t": "agent::CycleStartThreadIn", diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index f5bc572f853d770981d36853222cf10f7108a26b..2533a253aeb467b7b989700cf330cd5fed459a7e 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -24,12 +24,13 @@ use zed_actions::agent::{ ResolveConflictsWithAgent, ReviewBranchDiff, }; +use crate::agent_connection_store::AgentConnectionStore; use crate::thread_metadata_store::ThreadMetadataStore; use crate::{ AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, CycleStartThreadIn, Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, NewWorktreeBranchTarget, - OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, - StartThreadIn, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu, + OpenActiveThreadAsMarkdown, OpenAgentDiff, ResetTrialEndUpsell, ResetTrialUpsell, + StartThreadIn, ToggleNewThreadMenu, ToggleOptionsMenu, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, conversation_view::{AcpThreadViewEvent, ThreadView}, thread_branch_picker::ThreadBranchPicker, @@ -41,12 +42,10 @@ use crate::{ NewNativeAgentThreadFromSummary, }; use crate::{DEFAULT_THREAD_TITLE, ui::AcpOnboardingModal}; -use crate::{ExpandMessageEditor, ThreadHistoryView}; -use crate::{ManageProfiles, ThreadHistoryViewEvent}; -use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore}; +use crate::{ExpandMessageEditor, ManageProfiles}; use agent_settings::{AgentSettings, WindowLayout}; use ai_onboarding::AgentPanelOnboarding; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Result, anyhow}; use client::UserStore; use cloud_api_types::Plan; use collections::HashMap; @@ -56,8 +55,8 @@ use extension_host::ExtensionStore; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner, - DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, - Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, + Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription, + Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, }; use language::LanguageRegistry; use language_model::LanguageModelRegistry; @@ -76,7 +75,7 @@ use ui::{ Button, ButtonLike, Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*, utils::WithRemSize, }; -use util::{ResultExt as _, debug_panic}; +use util::ResultExt as _; use workspace::{ CollaboratorId, DraggedSelection, DraggedTab, PathList, SerializedPathList, ToggleWorkspaceSidebar, ToggleZoom, Workspace, WorkspaceId, @@ -89,7 +88,6 @@ use zed_actions::{ }; const AGENT_PANEL_KEY: &str = "agent_panel"; -const RECENTLY_UPDATED_MENU_LIMIT: usize = 6; const LAST_USED_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; #[derive(Serialize, Deserialize)] @@ -191,12 +189,6 @@ pub fn init(cx: &mut App) { panel.update(cx, |panel, cx| panel.expand_message_editor(window, cx)); } }) - .register_action(|workspace, _: &OpenHistory, window, cx| { - if let Some(panel) = workspace.panel::(cx) { - workspace.focus_panel::(window, cx); - panel.update(cx, |panel, cx| panel.open_history(window, cx)); - } - }) .register_action(|workspace, _: &OpenSettings, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); @@ -247,14 +239,6 @@ pub fn init(cx: &mut App) { AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx); } }) - .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| { - if let Some(panel) = workspace.panel::(cx) { - workspace.focus_panel::(window, cx); - panel.update(cx, |panel, cx| { - panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx); - }); - } - }) .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); @@ -606,9 +590,6 @@ enum ActiveView { AgentThread { conversation_view: Entity, }, - History { - view: Entity, - }, Configuration, } @@ -776,9 +757,7 @@ enum WorktreeCreationArgs { impl ActiveView { pub fn which_font_size_used(&self) -> WhichFontSize { match self { - ActiveView::Uninitialized - | ActiveView::AgentThread { .. } - | ActiveView::History { .. } => WhichFontSize::AgentFont, + ActiveView::Uninitialized | ActiveView::AgentThread { .. } => WhichFontSize::AgentFont, ActiveView::Configuration => WhichFontSize::None, } } @@ -806,8 +785,6 @@ pub struct AgentPanel { start_thread_in_menu_handle: PopoverMenuHandle, thread_branch_menu_handle: PopoverMenuHandle, agent_panel_menu_handle: PopoverMenuHandle, - agent_navigation_menu_handle: PopoverMenuHandle, - agent_navigation_menu: Option>, _extension_subscription: Option, _project_subscription: Subscription, _git_store_subscription: Subscription, @@ -990,7 +967,7 @@ impl AgentPanel { pub(crate) fn new( workspace: &Workspace, prompt_store: Option>, - window: &mut Window, + _window: &mut Window, cx: &mut Context, ) -> Self { let fs = workspace.app_state().fs.clone(); @@ -1008,48 +985,6 @@ impl AgentPanel { let active_view = ActiveView::Uninitialized; - let weak_panel = cx.entity().downgrade(); - - window.defer(cx, move |window, cx| { - let panel = weak_panel.clone(); - let agent_navigation_menu = - ContextMenu::build_persistent(window, cx, move |mut menu, window, cx| { - if let Some(panel) = panel.upgrade() { - if let Some(history) = panel - .update(cx, |panel, cx| panel.history_for_selected_agent(window, cx)) - { - menu = Self::populate_recently_updated_menu_section( - menu, panel, history, cx, - ); - menu = menu.action("View All", Box::new(OpenHistory)); - } - } - - menu = menu - .fixed_width(px(320.).into()) - .keep_open_on_confirm(false) - .key_context("NavigationMenu"); - - menu - }); - weak_panel - .update(cx, |panel, cx| { - cx.subscribe_in( - &agent_navigation_menu, - window, - |_, menu, _: &DismissEvent, window, cx| { - menu.update(cx, |menu, _| { - menu.clear_selected(); - }); - cx.focus_self(window); - }, - ) - .detach(); - panel.agent_navigation_menu = Some(agent_navigation_menu); - }) - .ok(); - }); - let weak_panel = cx.entity().downgrade(); let onboarding = cx.new(|cx| { AgentPanelOnboarding::new( @@ -1184,8 +1119,6 @@ impl AgentPanel { start_thread_in_menu_handle: PopoverMenuHandle::default(), thread_branch_menu_handle: PopoverMenuHandle::default(), agent_panel_menu_handle: PopoverMenuHandle::default(), - agent_navigation_menu_handle: PopoverMenuHandle::default(), - agent_navigation_menu: None, _extension_subscription: extension_subscription, _project_subscription, _git_store_subscription, @@ -1348,40 +1281,28 @@ impl AgentPanel { ) { let session_id = action.from_session_id.clone(); - let Some(history) = self - .connection_store + let Some(thread) = ThreadStore::global(cx) .read(cx) - .entry(&Agent::NativeAgent) - .and_then(|e| e.read(cx).history().cloned()) + .entries() + .find(|t| t.id == session_id) else { - debug_panic!("Native agent is not registered"); + log::error!("No session found for summarization with id {}", session_id); return; }; - cx.spawn_in(window, async move |this, cx| { - this.update_in(cx, |this, window, cx| { - let thread = history - .read(cx) - .session_for_id(&session_id) - .context("Session not found")?; - - this.external_thread( - Some(Agent::NativeAgent), - None, - None, - None, - Some(AgentInitialContent::ThreadSummary { - session_id: thread.session_id, - title: thread.title, - }), - true, - window, - cx, - ); - anyhow::Ok(()) - }) - }) - .detach_and_log_err(cx); + self.external_thread( + Some(Agent::NativeAgent), + None, + None, + None, + Some(AgentInitialContent::ThreadSummary { + session_id: thread.id, + title: Some(thread.title), + }), + true, + window, + cx, + ); } fn external_thread( @@ -1456,102 +1377,13 @@ impl AgentPanel { }) } - fn has_history_for_selected_agent(&self, cx: &App) -> bool { - match &self.selected_agent { - Agent::NativeAgent => true, - Agent::Custom { .. } => self - .connection_store - .read(cx) - .entry(&self.selected_agent) - .map_or(false, |entry| entry.read(cx).history().is_some()), - } - } - - fn history_for_selected_agent( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let agent = self.selected_agent.clone(); - let history = self - .connection_store - .read(cx) - .entry(&agent)? - .read(cx) - .history()? - .clone(); - Some(self.create_thread_history_view(agent, history, window, cx)) - } - - fn create_thread_history_view( - &self, - agent: Agent, - history: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let view = cx.new(|cx| ThreadHistoryView::new(history.clone(), window, cx)); - cx.subscribe_in( - &view, - window, - move |this, _, event, window, cx| match event { - ThreadHistoryViewEvent::Open(thread) => { - this.load_agent_thread( - agent.clone(), - thread.session_id.clone(), - thread.work_dirs.clone(), - thread.title.clone(), - true, - window, - cx, - ); - } - }, - ) - .detach(); - view - } - - fn open_history(&mut self, window: &mut Window, cx: &mut Context) { - let Some(view) = self.history_for_selected_agent(window, cx) else { - return; - }; - - if let ActiveView::History { view: active_view } = &self.active_view { - if active_view == &view { - if let Some(previous_view) = self.previous_view.take() { - self.set_active_view(previous_view, true, window, cx); - } - return; - } - } - - self.set_active_view(ActiveView::History { view }, true, window, cx); - cx.notify(); - } - pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context) { - match self.active_view { - ActiveView::Configuration | ActiveView::History { .. } => { - if let Some(previous_view) = self.previous_view.take() { - self.set_active_view(previous_view, true, window, cx); - } - cx.notify(); + if let ActiveView::Configuration = self.active_view { + if let Some(previous_view) = self.previous_view.take() { + self.set_active_view(previous_view, true, window, cx); } - _ => {} - } - } - - pub fn toggle_navigation_menu( - &mut self, - _: &ToggleNavigationMenu, - window: &mut Window, - cx: &mut Context, - ) { - if !self.has_history_for_selected_agent(cx) { - return; + cx.notify(); } - self.agent_navigation_menu_handle.toggle(window, cx); } pub fn toggle_options_menu( @@ -2043,16 +1875,12 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - let was_in_agent_history = matches!(self.active_view, ActiveView::History { .. }); let current_is_uninitialized = matches!(self.active_view, ActiveView::Uninitialized); - let current_is_history = matches!(self.active_view, ActiveView::History { .. }); - let new_is_history = matches!(new_view, ActiveView::History { .. }); - let current_is_config = matches!(self.active_view, ActiveView::Configuration); let new_is_config = matches!(new_view, ActiveView::Configuration); - let current_is_overlay = current_is_history || current_is_config; - let new_is_overlay = new_is_history || new_is_config; + let current_is_overlay = current_is_config; + let new_is_overlay = new_is_config; if current_is_uninitialized || (current_is_overlay && !new_is_overlay) { self.active_view = new_view; @@ -2101,78 +1929,12 @@ impl AgentPanel { } }; - if let ActiveView::History { view } = &self.active_view { - if !was_in_agent_history { - view.update(cx, |view, cx| { - view.history() - .update(cx, |history, cx| history.refresh_full_history(cx)) - }); - } - } - if focus { self.focus_handle(cx).focus(window, cx); } cx.emit(AgentPanelEvent::ActiveViewChanged); } - fn populate_recently_updated_menu_section( - mut menu: ContextMenu, - panel: Entity, - view: Entity, - cx: &mut Context, - ) -> ContextMenu { - let entries = view - .read(cx) - .history() - .read(cx) - .sessions() - .iter() - .take(RECENTLY_UPDATED_MENU_LIMIT) - .cloned() - .collect::>(); - - if entries.is_empty() { - return menu; - } - - menu = menu.header("Recently Updated"); - - for entry in entries { - let title = entry - .title - .as_ref() - .filter(|title| !title.is_empty()) - .cloned() - .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE)); - - menu = menu.entry(title, None, { - let panel = panel.downgrade(); - let entry = entry.clone(); - move |window, cx| { - let entry = entry.clone(); - panel - .update(cx, move |this, cx| { - if let Some(agent) = this.selected_agent() { - this.load_agent_thread( - agent, - entry.session_id.clone(), - entry.work_dirs.clone(), - entry.title.clone(), - true, - window, - cx, - ); - } - }) - .ok(); - } - }); - } - - menu.separator() - } - fn subscribe_to_active_thread_view( server_view: &Entity, window: &mut Window, @@ -3324,7 +3086,6 @@ impl Focusable for AgentPanel { ActiveView::AgentThread { conversation_view, .. } => conversation_view.focus_handle(cx), - ActiveView::History { view } => view.read(cx).focus_handle(cx), ActiveView::Configuration => { if let Some(configuration) = self.configuration.as_ref() { configuration.focus_handle(cx) @@ -3507,7 +3268,6 @@ impl AgentPanel { .into_any_element() } } - ActiveView::History { .. } => Label::new("History").truncate().into_any_element(), ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(), ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(), }; @@ -3774,9 +3534,7 @@ impl AgentPanel { ActiveView::AgentThread { conversation_view } => { conversation_view.read(cx).as_native_thread(cx) } - ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => { - None - } + ActiveView::Uninitialized | ActiveView::Configuration => None, }; let new_thread_menu_builder: Rc< @@ -4005,10 +3763,7 @@ impl AgentPanel { let is_empty_state = !self.active_thread_has_messages(cx); - let is_in_history_or_config = matches!( - &self.active_view, - ActiveView::History { .. } | ActiveView::Configuration - ); + let is_in_history_or_config = matches!(&self.active_view, ActiveView::Configuration); let is_full_screen = self.is_zoomed(window, cx); let full_screen_button = if is_full_screen { @@ -4143,7 +3898,7 @@ impl AgentPanel { .gap(DynamicSpacing::Base04.rems(cx)) .pl(DynamicSpacing::Base04.rems(cx)) .child(match &self.active_view { - ActiveView::History { .. } | ActiveView::Configuration => { + ActiveView::Configuration => { self.render_toolbar_back_button(cx).into_any_element() } _ => selected_agent.into_any_element(), @@ -4238,7 +3993,7 @@ impl AgentPanel { return false; } } - ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => { + ActiveView::Uninitialized | ActiveView::Configuration => { return false; } } @@ -4265,9 +4020,7 @@ impl AgentPanel { } match &self.active_view { - ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => { - false - } + ActiveView::Uninitialized | ActiveView::Configuration => false, ActiveView::AgentThread { .. } => { let existing_user = self .new_user_onboarding_upsell_dismissed @@ -4338,17 +4091,12 @@ impl AgentPanel { }); match &self.active_view { - ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => { - false - } + ActiveView::Uninitialized | ActiveView::Configuration => false, ActiveView::AgentThread { conversation_view, .. } if conversation_view.read(cx).as_native_thread(cx).is_none() => false, - ActiveView::AgentThread { conversation_view } => { - let history_is_empty = conversation_view - .read(cx) - .history() - .is_none_or(|h| h.read(cx).is_empty()); + ActiveView::AgentThread { .. } => { + let history_is_empty = ThreadStore::global(cx).read(cx).is_empty(); history_is_empty || !has_configured_non_zed_providers } } @@ -4471,7 +4219,7 @@ impl AgentPanel { conversation_view.insert_dragged_files(paths, added_worktrees, window, cx); }); } - ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {} + ActiveView::Uninitialized | ActiveView::Configuration => {} } } @@ -4512,7 +4260,7 @@ impl AgentPanel { key_context.add("AgentPanel"); match &self.active_view { ActiveView::AgentThread { .. } => key_context.add("acp_thread"), - ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {} + ActiveView::Uninitialized | ActiveView::Configuration => {} } key_context } @@ -4537,16 +4285,12 @@ impl Render for AgentPanel { .on_action(cx.listener(|this, action: &NewThread, window, cx| { this.new_thread(action, window, cx); })) - .on_action(cx.listener(|this, _: &OpenHistory, window, cx| { - this.open_history(window, cx); - })) .on_action(cx.listener(|this, _: &OpenSettings, window, cx| { this.open_configuration(window, cx); })) .on_action(cx.listener(Self::open_active_thread_as_markdown)) .on_action(cx.listener(Self::deploy_rules_library)) .on_action(cx.listener(Self::go_back)) - .on_action(cx.listener(Self::toggle_navigation_menu)) .on_action(cx.listener(Self::toggle_options_menu)) .on_action(cx.listener(Self::increase_font_size)) .on_action(cx.listener(Self::decrease_font_size)) @@ -4570,7 +4314,6 @@ impl Render for AgentPanel { } => parent .child(conversation_view.clone()) .child(self.render_drag_target(cx)), - ActiveView::History { view } => parent.child(view.clone()), ActiveView::Configuration => parent.children(self.configuration.clone()), }) .children(self.render_worktree_creation_status(cx)) @@ -4732,14 +4475,6 @@ impl AgentPanel { cx.notify(); } - /// Opens the history view. - /// - /// This is a test-only helper that exposes the private `open_history()` - /// method for visual tests. - pub fn open_history_for_tests(&mut self, window: &mut Window, cx: &mut Context) { - self.open_history(window, cx); - } - /// Opens the start_thread_in selector popover menu. /// /// This is a test-only helper for visual tests. diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 2cf4218719a0412534d9832c3cb54587f4c45a73..3a4b5b2ed4dd80a480097a0d6e29c08e247d0d50 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -30,7 +30,6 @@ mod terminal_inline_assistant; pub mod test_support; mod thread_branch_picker; mod thread_history; -mod thread_history_view; mod thread_import; pub mod thread_metadata_store; pub mod thread_worktree_archive; @@ -75,7 +74,6 @@ pub(crate) use mode_selector::ModeSelector; pub(crate) use model_selector::ModelSelector; pub(crate) use model_selector_popover::ModelSelectorPopover; pub(crate) use thread_history::ThreadHistory; -pub(crate) use thread_history_view::*; pub use thread_import::{AcpThreadImportOnboarding, ThreadImportModal}; use zed_actions; @@ -88,8 +86,6 @@ actions!( ToggleNewThreadMenu, /// Cycles through the options for where new threads start (current project or new worktree). CycleStartThreadIn, - /// Toggles the navigation menu for switching between threads and views. - ToggleNavigationMenu, /// Toggles the options menu for agent settings and preferences. ToggleOptionsMenu, /// Toggles the profile or mode selector for switching between agent profiles. @@ -100,10 +96,6 @@ actions!( CycleFavoriteModels, /// 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. AddContextServer, /// Removes the currently selected thread. diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index d38e1344701fc8681b0feaf2fa7843611750532d..1687470448cd79b332d24d57d2a16ab929909568 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -2644,10 +2644,6 @@ impl ConversationView { Self::handle_auth_required(this, AuthRequired::new(), agent_id, connection, window, cx); }) } - - pub fn history(&self) -> Option<&Entity> { - self.as_connected().and_then(|c| c.history.as_ref()) - } } fn loading_contents_spinner(size: IconSize) -> AnyElement { @@ -2797,9 +2793,7 @@ fn plan_label_markdown_style( #[cfg(test)] pub(crate) mod tests { - use acp_thread::{ - AgentSessionList, AgentSessionListRequest, AgentSessionListResponse, StubAgentConnection, - }; + use acp_thread::StubAgentConnection; use action_log::ActionLog; use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext}; use agent_client_protocol::SessionId; @@ -2934,66 +2928,6 @@ pub(crate) mod tests { ); } - #[gpui::test] - async fn test_recent_history_refreshes_when_history_cache_updated(cx: &mut TestAppContext) { - init_test(cx); - - let session_a = AgentSessionInfo::new(SessionId::new("session-a")); - let session_b = AgentSessionInfo::new(SessionId::new("session-b")); - - // Use a connection that provides a session list so ThreadHistory is created - let (conversation_view, history, cx) = setup_thread_view_with_history( - StubAgentServer::new(SessionHistoryConnection::new(vec![session_a.clone()])), - cx, - ) - .await; - - // Initially has session_a from the connection's session list - active_thread(&conversation_view, cx).read_with(cx, |view, _cx| { - assert_eq!(view.recent_history_entries.len(), 1); - assert_eq!( - view.recent_history_entries[0].session_id, - session_a.session_id - ); - }); - - // Swap to a different session list - let list_b: Rc = - Rc::new(StubSessionList::new(vec![session_b.clone()])); - history.update(cx, |history, cx| { - history.set_session_list(list_b, cx); - }); - cx.run_until_parked(); - - active_thread(&conversation_view, cx).read_with(cx, |view, _cx| { - assert_eq!(view.recent_history_entries.len(), 1); - assert_eq!( - view.recent_history_entries[0].session_id, - session_b.session_id - ); - }); - } - - #[gpui::test] - async fn test_new_thread_creation_triggers_session_list_refresh(cx: &mut TestAppContext) { - init_test(cx); - - let session = AgentSessionInfo::new(SessionId::new("history-session")); - let (conversation_view, _history, cx) = setup_thread_view_with_history( - StubAgentServer::new(SessionHistoryConnection::new(vec![session.clone()])), - cx, - ) - .await; - - active_thread(&conversation_view, cx).read_with(cx, |view, _cx| { - assert_eq!(view.recent_history_entries.len(), 1); - assert_eq!( - view.recent_history_entries[0].session_id, - session.session_id - ); - }); - } - #[gpui::test] async fn test_resume_without_history_adds_notice(cx: &mut TestAppContext) { init_test(cx); @@ -3852,19 +3786,6 @@ pub(crate) mod tests { (conversation_view, cx) } - async fn setup_thread_view_with_history( - agent: impl AgentServer + 'static, - cx: &mut TestAppContext, - ) -> ( - Entity, - Entity, - &mut VisualTestContext, - ) { - let (conversation_view, history, cx) = - setup_conversation_view_with_history_and_initial_content(agent, None, cx).await; - (conversation_view, history.expect("Missing history"), cx) - } - async fn setup_conversation_view_with_initial_content( agent: impl AgentServer + 'static, initial_content: AgentInitialContent, @@ -4061,42 +3982,6 @@ pub(crate) mod tests { } } - #[derive(Clone)] - struct StubSessionList { - sessions: Vec, - } - - impl StubSessionList { - fn new(sessions: Vec) -> Self { - Self { sessions } - } - } - - impl AgentSessionList for StubSessionList { - fn list_sessions( - &self, - _request: AgentSessionListRequest, - _cx: &mut App, - ) -> Task> { - Task::ready(Ok(AgentSessionListResponse::new(self.sessions.clone()))) - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - #[derive(Clone)] - struct SessionHistoryConnection { - sessions: Vec, - } - - impl SessionHistoryConnection { - fn new(sessions: Vec) -> Self { - Self { sessions } - } - } - fn build_test_thread( connection: Rc, project: Entity, @@ -4125,67 +4010,6 @@ pub(crate) mod tests { }) } - impl AgentConnection for SessionHistoryConnection { - fn agent_id(&self) -> AgentId { - AgentId::new("history-connection") - } - - fn telemetry_id(&self) -> SharedString { - "history-connection".into() - } - - fn new_session( - self: Rc, - project: Entity, - _work_dirs: PathList, - cx: &mut App, - ) -> Task>> { - let thread = build_test_thread( - self, - project, - "SessionHistoryConnection", - SessionId::new("history-session"), - cx, - ); - Task::ready(Ok(thread)) - } - - fn supports_load_session(&self) -> bool { - true - } - - fn session_list(&self, _cx: &mut App) -> Option> { - Some(Rc::new(StubSessionList::new(self.sessions.clone()))) - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] - } - - fn authenticate( - &self, - _method_id: acp::AuthMethodId, - _cx: &mut App, - ) -> Task> { - Task::ready(Ok(())) - } - - fn prompt( - &self, - _id: Option, - _params: acp::PromptRequest, - _cx: &mut App, - ) -> Task> { - Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))) - } - - fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {} - - fn into_any(self: Rc) -> Rc { - self - } - } - #[derive(Clone)] struct ResumeOnlyAgentConnection; diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index ae9bf17c76bde99cdacea9d5bb205074a1a4ee39..e04119f645b70021ef7b46b2888654f90775dd53 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -326,13 +326,9 @@ pub struct ThreadView { pub add_context_menu_handle: PopoverMenuHandle, pub thinking_effort_menu_handle: PopoverMenuHandle, pub project: WeakEntity, - pub recent_history_entries: Vec, - pub hovered_recent_history_item: Option, pub show_external_source_prompt_warning: bool, pub show_codex_windows_warning: bool, pub generating_indicator_in_list: bool, - pub history: Option>, - pub _history_subscription: Option, } impl Focusable for ThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { @@ -387,12 +383,6 @@ impl ThreadView { let has_commands = !session_capabilities.read().available_commands().is_empty(); let placeholder = placeholder_text(agent_display_name.as_ref(), has_commands); - let history_subscription = history.as_ref().map(|h| { - cx.observe(h, |this, history, cx| { - this.update_recent_history_from_cache(&history, cx); - }) - }); - let mut should_auto_submit = false; let mut show_external_source_prompt_warning = false; @@ -500,11 +490,6 @@ impl ThreadView { })); })); - let recent_history_entries = history - .as_ref() - .map(|h| h.read(cx).get_recent_sessions(3)) - .unwrap_or_default(); - let mut this = Self { id, parent_id, @@ -567,11 +552,7 @@ impl ThreadView { add_context_menu_handle: PopoverMenuHandle::default(), thinking_effort_menu_handle: PopoverMenuHandle::default(), project, - recent_history_entries, - hovered_recent_history_item: None, show_external_source_prompt_warning, - history, - _history_subscription: history_subscription, show_codex_windows_warning, generating_indicator_in_list: false, }; @@ -8276,16 +8257,6 @@ impl ThreadView { .into_any_element() } - fn update_recent_history_from_cache( - &mut self, - history: &Entity, - cx: &mut Context, - ) { - self.recent_history_entries = history.read(cx).get_recent_sessions(3); - self.hovered_recent_history_item = None; - cx.notify(); - } - fn render_codex_windows_warning(&self, cx: &mut Context) -> Callout { Callout::new() .icon(IconName::Warning) diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index 7b7a3e60211896bf717fb3dfb2670d92b7409281..70cacc13dd178a7ad8512ec6d80a97af7911203f 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/crates/agent_ui/src/thread_history.rs @@ -1,6 +1,6 @@ use acp_thread::{AgentSessionInfo, AgentSessionList, AgentSessionListRequest, SessionListUpdate}; use agent_client_protocol as acp; -use gpui::{App, Task}; +use gpui::Task; use std::rc::Rc; use ui::prelude::*; @@ -75,10 +75,6 @@ impl ThreadHistory { })); } - pub(crate) fn refresh_full_history(&mut self, cx: &mut Context) { - self.refresh_sessions(true, cx); - } - fn apply_info_update( &mut self, session_id: acp::SessionId, @@ -178,10 +174,6 @@ impl ThreadHistory { }); } - pub(crate) fn is_empty(&self) -> bool { - self.sessions.is_empty() - } - pub fn refresh(&mut self, _cx: &mut Context) { self.session_list.notify_refresh(); } @@ -196,26 +188,6 @@ impl ThreadHistory { pub(crate) fn sessions(&self) -> &[AgentSessionInfo] { &self.sessions } - - pub(crate) fn get_recent_sessions(&self, limit: usize) -> Vec { - self.sessions.iter().take(limit).cloned().collect() - } - - pub fn supports_delete(&self) -> bool { - self.session_list.supports_delete() - } - - pub(crate) fn delete_session( - &self, - session_id: &acp::SessionId, - cx: &mut App, - ) -> Task> { - self.session_list.delete_session(session_id, cx) - } - - pub(crate) fn delete_sessions(&self, cx: &mut App) -> Task> { - self.session_list.delete_sessions(cx) - } } #[cfg(test)] @@ -420,7 +392,7 @@ mod tests { cx.run_until_parked(); session_list.clear_requested_cursors(); - history.update(cx, |history, cx| history.refresh_full_history(cx)); + history.update(cx, |history, cx| history.refresh_sessions(true, cx)); cx.run_until_parked(); history.update(cx, |history, _cx| { @@ -454,7 +426,7 @@ mod tests { let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); - history.update(cx, |history, cx| history.refresh_full_history(cx)); + history.update(cx, |history, cx| history.refresh_sessions(true, cx)); cx.run_until_parked(); session_list.clear_requested_cursors(); @@ -485,11 +457,11 @@ mod tests { let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); - history.update(cx, |history, cx| history.refresh_full_history(cx)); + history.update(cx, |history, cx| history.refresh_sessions(true, cx)); cx.run_until_parked(); session_list.clear_requested_cursors(); - history.update(cx, |history, cx| history.refresh_full_history(cx)); + history.update(cx, |history, cx| history.refresh_sessions(true, cx)); cx.run_until_parked(); history.update(cx, |history, _cx| { @@ -514,7 +486,7 @@ mod tests { let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); - history.update(cx, |history, cx| history.refresh_full_history(cx)); + history.update(cx, |history, cx| history.refresh_sessions(true, cx)); cx.run_until_parked(); session_list.clear_requested_cursors(); @@ -558,7 +530,7 @@ mod tests { cx.run_until_parked(); session_list.clear_requested_cursors(); - history.update(cx, |history, cx| history.refresh_full_history(cx)); + history.update(cx, |history, cx| history.refresh_sessions(true, cx)); cx.run_until_parked(); history.update(cx, |history, _cx| { diff --git a/crates/agent_ui/src/thread_history_view.rs b/crates/agent_ui/src/thread_history_view.rs deleted file mode 100644 index a4a00455be471c2a76fd8b2598402dc6e925ad86..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/thread_history_view.rs +++ /dev/null @@ -1,751 +0,0 @@ -use crate::thread_history::ThreadHistory; -use crate::{DEFAULT_THREAD_TITLE, RemoveHistory, RemoveSelectedThread}; -use acp_thread::AgentSessionInfo; -use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc}; -use editor::{Editor, EditorEvent}; -use fuzzy::StringMatchCandidate; -use gpui::{ - AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task, - UniformListScrollHandle, Window, uniform_list, -}; -use std::{fmt::Display, ops::Range}; -use text::Bias; -use time::{OffsetDateTime, UtcOffset}; -use ui::{ - HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar, - prelude::*, -}; - -pub(crate) fn thread_title(entry: &AgentSessionInfo) -> SharedString { - entry - .title - .clone() - .filter(|title| !title.is_empty()) - .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into()) -} - -pub struct ThreadHistoryView { - history: Entity, - scroll_handle: UniformListScrollHandle, - selected_index: usize, - hovered_index: Option, - search_editor: Entity, - search_query: SharedString, - visible_items: Vec, - local_timezone: UtcOffset, - confirming_delete_history: bool, - _visible_items_task: Task<()>, - _subscriptions: Vec, -} - -enum ListItemType { - BucketSeparator(TimeBucket), - Entry { - entry: AgentSessionInfo, - format: EntryTimeFormat, - }, - SearchResult { - entry: AgentSessionInfo, - positions: Vec, - }, -} - -impl ListItemType { - fn history_entry(&self) -> Option<&AgentSessionInfo> { - match self { - ListItemType::Entry { entry, .. } => Some(entry), - ListItemType::SearchResult { entry, .. } => Some(entry), - _ => None, - } - } -} - -pub enum ThreadHistoryViewEvent { - Open(AgentSessionInfo), -} - -impl EventEmitter for ThreadHistoryView {} - -impl ThreadHistoryView { - pub fn new( - history: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let search_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Search threads...", window, cx); - editor - }); - - let search_editor_subscription = - cx.subscribe(&search_editor, |this, search_editor, event, cx| { - if let EditorEvent::BufferEdited = event { - let query = search_editor.read(cx).text(cx); - if this.search_query != query { - this.search_query = query.into(); - this.update_visible_items(false, cx); - } - } - }); - - let history_subscription = cx.observe(&history, |this, _, cx| { - this.update_visible_items(true, cx); - }); - - let scroll_handle = UniformListScrollHandle::default(); - - let mut this = Self { - history, - scroll_handle, - selected_index: 0, - hovered_index: None, - visible_items: Default::default(), - search_editor, - local_timezone: UtcOffset::from_whole_seconds( - chrono::Local::now().offset().local_minus_utc(), - ) - .unwrap(), - search_query: SharedString::default(), - confirming_delete_history: false, - _subscriptions: vec![search_editor_subscription, history_subscription], - _visible_items_task: Task::ready(()), - }; - this.update_visible_items(false, cx); - this - } - - pub fn history(&self) -> &Entity { - &self.history - } - - fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context) { - let entries = self.history.read(cx).sessions().to_vec(); - let new_list_items = if self.search_query.is_empty() { - self.add_list_separators(entries, cx) - } else { - self.filter_search_results(entries, cx) - }; - let selected_history_entry = if preserve_selected_item { - self.selected_history_entry().cloned() - } else { - None - }; - - self._visible_items_task = cx.spawn(async move |this, cx| { - let new_visible_items = new_list_items.await; - this.update(cx, |this, cx| { - let new_selected_index = if let Some(history_entry) = selected_history_entry { - new_visible_items - .iter() - .position(|visible_entry| { - visible_entry - .history_entry() - .is_some_and(|entry| entry.session_id == history_entry.session_id) - }) - .unwrap_or(0) - } else { - 0 - }; - - this.visible_items = new_visible_items; - this.set_selected_index(new_selected_index, Bias::Right, cx); - cx.notify(); - }) - .ok(); - }); - } - - fn add_list_separators( - &self, - entries: Vec, - cx: &App, - ) -> Task> { - cx.background_spawn(async move { - let mut items = Vec::with_capacity(entries.len() + 1); - let mut bucket = None; - let today = Local::now().naive_local().date(); - - for entry in entries.into_iter() { - let entry_bucket = entry - .updated_at - .map(|timestamp| { - let entry_date = timestamp.with_timezone(&Local).naive_local().date(); - TimeBucket::from_dates(today, entry_date) - }) - .unwrap_or(TimeBucket::All); - - if Some(entry_bucket) != bucket { - bucket = Some(entry_bucket); - items.push(ListItemType::BucketSeparator(entry_bucket)); - } - - items.push(ListItemType::Entry { - entry, - format: entry_bucket.into(), - }); - } - items - }) - } - - fn filter_search_results( - &self, - entries: Vec, - cx: &App, - ) -> Task> { - let query = self.search_query.clone(); - cx.background_spawn({ - let executor = cx.background_executor().clone(); - async move { - let mut candidates = Vec::with_capacity(entries.len()); - - for (idx, entry) in entries.iter().enumerate() { - candidates.push(StringMatchCandidate::new(idx, &thread_title(entry))); - } - - const MAX_MATCHES: usize = 100; - - let matches = fuzzy::match_strings( - &candidates, - &query, - false, - true, - MAX_MATCHES, - &Default::default(), - executor, - ) - .await; - - matches - .into_iter() - .map(|search_match| ListItemType::SearchResult { - entry: entries[search_match.candidate_id].clone(), - positions: search_match.positions, - }) - .collect() - } - }) - } - - fn search_produced_no_matches(&self) -> bool { - self.visible_items.is_empty() && !self.search_query.is_empty() - } - - fn selected_history_entry(&self) -> Option<&AgentSessionInfo> { - self.get_history_entry(self.selected_index) - } - - fn get_history_entry(&self, visible_items_ix: usize) -> Option<&AgentSessionInfo> { - self.visible_items.get(visible_items_ix)?.history_entry() - } - - fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context) { - if self.visible_items.len() == 0 { - self.selected_index = 0; - return; - } - while matches!( - self.visible_items.get(index), - None | Some(ListItemType::BucketSeparator(..)) - ) { - index = match bias { - Bias::Left => { - if index == 0 { - self.visible_items.len() - 1 - } else { - index - 1 - } - } - Bias::Right => { - if index >= self.visible_items.len() - 1 { - 0 - } else { - index + 1 - } - } - }; - } - self.selected_index = index; - self.scroll_handle - .scroll_to_item(index, ScrollStrategy::Top); - cx.notify() - } - - fn select_previous( - &mut self, - _: &menu::SelectPrevious, - _window: &mut Window, - cx: &mut Context, - ) { - if self.selected_index == 0 { - self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx); - } else { - self.set_selected_index(self.selected_index - 1, Bias::Left, cx); - } - } - - fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context) { - if self.selected_index == self.visible_items.len() - 1 { - self.set_selected_index(0, Bias::Right, cx); - } else { - self.set_selected_index(self.selected_index + 1, Bias::Right, cx); - } - } - - fn select_first( - &mut self, - _: &menu::SelectFirst, - _window: &mut Window, - cx: &mut Context, - ) { - self.set_selected_index(0, Bias::Right, cx); - } - - fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx); - } - - fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { - self.confirm_entry(self.selected_index, cx); - } - - fn confirm_entry(&mut self, ix: usize, cx: &mut Context) { - let Some(entry) = self.get_history_entry(ix) else { - return; - }; - cx.emit(ThreadHistoryViewEvent::Open(entry.clone())); - } - - fn remove_selected_thread( - &mut self, - _: &RemoveSelectedThread, - _window: &mut Window, - cx: &mut Context, - ) { - self.remove_thread(self.selected_index, cx) - } - - fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context) { - let Some(entry) = self.get_history_entry(visible_item_ix) else { - return; - }; - if !self.history.read(cx).supports_delete() { - return; - } - let session_id = entry.session_id.clone(); - self.history.update(cx, |history, cx| { - history - .delete_session(&session_id, cx) - .detach_and_log_err(cx); - }); - } - - fn remove_history(&mut self, _window: &mut Window, cx: &mut Context) { - if !self.history.read(cx).supports_delete() { - return; - } - self.history.update(cx, |history, cx| { - history.delete_sessions(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, - _window: &mut Window, - cx: &mut Context, - ) -> Vec { - self.visible_items - .get(range.clone()) - .into_iter() - .flatten() - .enumerate() - .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx)) - .collect() - } - - fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context) -> AnyElement { - match item { - ListItemType::Entry { entry, format } => self - .render_history_entry(entry, *format, ix, Vec::default(), cx) - .into_any(), - ListItemType::SearchResult { entry, positions } => self.render_history_entry( - entry, - EntryTimeFormat::DateAndTime, - ix, - positions.clone(), - cx, - ), - ListItemType::BucketSeparator(bucket) => div() - .px(DynamicSpacing::Base06.rems(cx)) - .pt_2() - .pb_1() - .child( - Label::new(bucket.to_string()) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .into_any_element(), - } - } - - fn render_history_entry( - &self, - entry: &AgentSessionInfo, - format: EntryTimeFormat, - ix: usize, - highlight_positions: Vec, - cx: &Context, - ) -> AnyElement { - let selected = ix == self.selected_index; - let hovered = Some(ix) == self.hovered_index; - let entry_time = entry.updated_at; - let display_text = match (format, entry_time) { - (EntryTimeFormat::DateAndTime, Some(entry_time)) => { - let now = Utc::now(); - let duration = now.signed_duration_since(entry_time); - let days = duration.num_days(); - - format!("{}d", days) - } - (EntryTimeFormat::TimeOnly, Some(entry_time)) => { - format.format_timestamp(entry_time.timestamp(), self.local_timezone) - } - (_, None) => "—".to_string(), - }; - - let title = thread_title(entry); - let full_date = entry_time - .map(|time| { - EntryTimeFormat::DateAndTime.format_timestamp(time.timestamp(), self.local_timezone) - }) - .unwrap_or_else(|| "Unknown".to_string()); - - let supports_delete = self.history.read(cx).supports_delete(); - - h_flex() - .w_full() - .pb_1() - .child( - ListItem::new(ix) - .rounded() - .toggle_state(selected) - .spacing(ListItemSpacing::Sparse) - .start_slot( - h_flex() - .w_full() - .gap_2() - .justify_between() - .child( - HighlightedLabel::new(thread_title(entry), highlight_positions) - .size(LabelSize::Small) - .truncate(), - ) - .child( - Label::new(display_text) - .color(Color::Muted) - .size(LabelSize::XSmall), - ), - ) - .tooltip(move |_, cx| { - Tooltip::with_meta(title.clone(), None, full_date.clone(), cx) - }) - .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::(if hovered && supports_delete { - Some( - IconButton::new("delete", IconName::Trash) - .shape(IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip(move |_window, cx| { - Tooltip::for_action("Delete", &RemoveSelectedThread, cx) - }) - .on_click(cx.listener(move |this, _, _, cx| { - this.remove_thread(ix, cx); - cx.stop_propagation() - })), - ) - } else { - None - }) - .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))), - ) - .into_any_element() - } -} - -impl Focusable for ThreadHistoryView { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.search_editor.focus_handle(cx) - } -} - -impl Render for ThreadHistoryView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let has_no_history = self.history.read(cx).is_empty(); - let supports_delete = self.history.read(cx).supports_delete(); - - v_flex() - .key_context("ThreadHistory") - .size_full() - .bg(cx.theme().colors().panel_background) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_next)) - .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)) - .on_action(cx.listener(|this, _: &RemoveHistory, window, cx| { - this.remove_history(window, cx); - })) - .child( - h_flex() - .h(Tab::container_height(cx)) - .w_full() - .py_1() - .px_2() - .gap_2() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - Icon::new(IconName::MagnifyingGlass) - .color(Color::Muted) - .size(IconSize::Small), - ) - .child(self.search_editor.clone()), - ) - .child({ - let view = v_flex() - .id("list-container") - .relative() - .overflow_hidden() - .flex_grow(); - - if has_no_history { - view.justify_center().items_center().child( - Label::new("You don't have any past threads yet.") - .size(LabelSize::Small) - .color(Color::Muted), - ) - } else if self.search_produced_no_matches() { - view.justify_center() - .items_center() - .child(Label::new("No threads match your search.").size(LabelSize::Small)) - } else { - view.child( - uniform_list( - "thread-history", - self.visible_items.len(), - cx.processor(|this, range: Range, window, cx| { - this.render_list_items(range, window, cx) - }), - ) - .p_1() - .pr_4() - .track_scroll(&self.scroll_handle) - .flex_grow(), - ) - .vertical_scrollbar_for(&self.scroll_handle, window, cx) - } - }) - .when(!has_no_history && supports_delete, |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, - ); - })), - ), - ) - }), - ) - }) - } -} - -#[derive(Clone, Copy)] -pub enum EntryTimeFormat { - DateAndTime, - TimeOnly, -} - -impl EntryTimeFormat { - fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String { - let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap(); - - match self { - EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp( - timestamp, - OffsetDateTime::now_utc(), - timezone, - time_format::TimestampFormat::EnhancedAbsolute, - ), - EntryTimeFormat::TimeOnly => time_format::format_time(timestamp.to_offset(timezone)), - } - } -} - -impl From for EntryTimeFormat { - fn from(bucket: TimeBucket) -> Self { - match bucket { - TimeBucket::Today => EntryTimeFormat::TimeOnly, - TimeBucket::Yesterday => EntryTimeFormat::TimeOnly, - TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime, - TimeBucket::PastWeek => EntryTimeFormat::DateAndTime, - TimeBucket::All => EntryTimeFormat::DateAndTime, - } - } -} - -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -enum TimeBucket { - Today, - Yesterday, - ThisWeek, - PastWeek, - All, -} - -impl TimeBucket { - fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self { - if date == reference { - return TimeBucket::Today; - } - - if date == reference - TimeDelta::days(1) { - return TimeBucket::Yesterday; - } - - let week = date.iso_week(); - - if reference.iso_week() == week { - return TimeBucket::ThisWeek; - } - - let last_week = (reference - TimeDelta::days(7)).iso_week(); - - if week == last_week { - return TimeBucket::PastWeek; - } - - TimeBucket::All - } -} - -impl Display for TimeBucket { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TimeBucket::Today => write!(f, "Today"), - TimeBucket::Yesterday => write!(f, "Yesterday"), - TimeBucket::ThisWeek => write!(f, "This Week"), - TimeBucket::PastWeek => write!(f, "Past Week"), - TimeBucket::All => write!(f, "All"), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::NaiveDate; - - #[test] - fn test_time_bucket_from_dates() { - let today = NaiveDate::from_ymd_opt(2025, 1, 15).unwrap(); - - assert_eq!(TimeBucket::from_dates(today, today), TimeBucket::Today); - - let yesterday = NaiveDate::from_ymd_opt(2025, 1, 14).unwrap(); - assert_eq!( - TimeBucket::from_dates(today, yesterday), - TimeBucket::Yesterday - ); - - let this_week = NaiveDate::from_ymd_opt(2025, 1, 13).unwrap(); - assert_eq!( - TimeBucket::from_dates(today, this_week), - TimeBucket::ThisWeek - ); - - let past_week = NaiveDate::from_ymd_opt(2025, 1, 7).unwrap(); - assert_eq!( - TimeBucket::from_dates(today, past_week), - TimeBucket::PastWeek - ); - - let old = NaiveDate::from_ymd_opt(2024, 12, 1).unwrap(); - assert_eq!(TimeBucket::from_dates(today, old), TimeBucket::All); - } -}