From cfebe3a85af0fab9688c275ecf22aa30c3501cc6 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 21 Apr 2026 14:33:38 +0200 Subject: [PATCH] agent_ui: Remove history view (#54402) Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --- Cargo.lock | 1 - assets/keymaps/default-linux.json | 2 - assets/keymaps/default-macos.json | 2 - assets/keymaps/default-windows.json | 2 - crates/agent_ui/Cargo.toml | 1 - crates/agent_ui/src/agent_connection_store.rs | 22 +- crates/agent_ui/src/agent_panel.rs | 363 +------- crates/agent_ui/src/agent_ui.rs | 10 - crates/agent_ui/src/completion_provider.rs | 77 +- crates/agent_ui/src/conversation_view.rs | 240 +----- .../src/conversation_view/thread_view.rs | 31 - crates/agent_ui/src/entry_view_state.rs | 7 - crates/agent_ui/src/inline_assistant.rs | 15 - crates/agent_ui/src/inline_prompt_editor.rs | 7 - crates/agent_ui/src/message_editor.rs | 21 - .../agent_ui/src/terminal_inline_assistant.rs | 3 - crates/agent_ui/src/thread_history.rs | 772 ------------------ crates/agent_ui/src/thread_history_view.rs | 751 ----------------- docs/src/ai/agent-panel.md | 7 +- 19 files changed, 85 insertions(+), 2249 deletions(-) delete mode 100644 crates/agent_ui/src/thread_history.rs delete mode 100644 crates/agent_ui/src/thread_history_view.rs diff --git a/Cargo.lock b/Cargo.lock index c662bfe5d8d2f1d8d9ea3281ddf9373419b82a9d..20695db73fb83c9fc22c0b9338968beecee0086f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,7 +409,6 @@ dependencies = [ "theme", "theme_settings", "time", - "time_format", "tree-sitter-md", "ui", "ui_input", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 15a0d516b11e4132f4c641a023612a55438b987b..280e14f13ca880cfe8a89a47b1acdea3ee050586 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", "shift-alt-escape": "agent::ExpandMessageEditor", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index f8575f30d620a8708977988d192e62599c4f7f87..f8b296e3e82feb90a9b3124ebc7919b3312d3070 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", "shift-alt-escape": "agent::ExpandMessageEditor", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index a38b6625a6104d470405d6b7554c8f2ea204f808..a4196bd8e0cf01617f76e5893cf520c7375468f6 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", "shift-alt-escape": "agent::ExpandMessageEditor", diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 9317c11dd200b0f5705794f5506bf7ff69272400..a48f3a79e80e5cd9224d07c306b7be2abcf18e23 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -102,7 +102,6 @@ text.workspace = true theme.workspace = true theme_settings.workspace = true time.workspace = true -time_format.workspace = true ui.workspace = true ui_input.workspace = true url.workspace = true diff --git a/crates/agent_ui/src/agent_connection_store.rs b/crates/agent_ui/src/agent_connection_store.rs index d903a6435d87690b7ffd5f06b89b57c680930a36..218347d5c57c2102136e9f8de097c414fb08f15e 100644 --- a/crates/agent_ui/src/agent_connection_store.rs +++ b/crates/agent_ui/src/agent_connection_store.rs @@ -10,7 +10,7 @@ use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Subscri use project::{AgentServerStore, AgentServersUpdated, Project}; use watch::Receiver; -use crate::{Agent, ThreadHistory}; +use crate::Agent; pub enum AgentConnectionEntry { Connecting { @@ -25,7 +25,6 @@ pub enum AgentConnectionEntry { #[derive(Clone)] pub struct AgentConnectedState { pub connection: Rc, - pub history: Option>, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -44,13 +43,6 @@ impl AgentConnectionEntry { } } - pub fn history(&self) -> Option<&Entity> { - match self { - AgentConnectionEntry::Connected(state) => state.history.as_ref(), - _ => None, - } - } - pub fn status(&self) -> AgentConnectionStatus { match self { AgentConnectionEntry::Connecting { .. } => AgentConnectionStatus::Connecting, @@ -241,16 +233,8 @@ impl AgentConnectionStore { let delegate = AgentServerDelegate::new(agent_server_store, Some(new_version_tx)); let connect_task = server.connect(delegate, self.project.clone(), cx); - let connect_task = cx.spawn(async move |_this, cx| match connect_task.await { - Ok(connection) => cx.update(|cx| { - let history = connection - .session_list(cx) - .map(|session_list| cx.new(|cx| ThreadHistory::new(session_list, cx))); - Ok(AgentConnectedState { - connection, - history, - }) - }), + let connect_task = cx.spawn(async move |_this, _cx| match connect_task.await { + Ok(connection) => Ok(AgentConnectedState { connection }), Err(err) => match err.downcast::() { Ok(load_error) => Err(load_error), Err(err) => Err(LoadError::Other(SharedString::from(err.to_string()))), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 653e6c11898b68605b1028b11918f83618beabe8..be90f92f4cbf26bcba473e8d1cbb6a05926a8d79 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -30,12 +30,15 @@ use zed_actions::{ }; use crate::DEFAULT_THREAD_TITLE; +use crate::ExpandMessageEditor; +use crate::ManageProfiles; +use crate::agent_connection_store::AgentConnectionStore; use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore}; use crate::{ AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, - OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, ShowAllSidebarThreadMetadata, - ShowThreadMetadata, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu, + ResetTrialEndUpsell, ResetTrialUpsell, ShowAllSidebarThreadMetadata, ShowThreadMetadata, + ToggleNewThreadMenu, ToggleOptionsMenu, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, conversation_view::{AcpThreadViewEvent, ThreadView}, ui::EndTrialUpsell, @@ -44,12 +47,9 @@ use crate::{ Agent, AgentInitialContent, ExternalSourcePrompt, NewExternalAgentThread, NewNativeAgentThreadFromSummary, }; -use crate::{ExpandMessageEditor, ThreadHistoryView}; -use crate::{ManageProfiles, ThreadHistoryViewEvent}; -use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore}; use agent_settings::AgentSettings; use ai_onboarding::AgentPanelOnboarding; -use anyhow::{Context as _, Result}; +use anyhow::Result; use chrono::{DateTime, Utc}; use client::UserStore; use cloud_api_types::Plan; @@ -60,8 +60,8 @@ use extension_host::ExtensionStore; use fs::Fs; use gpui::{ Action, Anchor, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, - 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; @@ -78,7 +78,7 @@ use ui::{ Button, Callout, ContextMenu, ContextMenuEntry, IconButton, 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, @@ -87,7 +87,6 @@ use workspace::{ const AGENT_PANEL_KEY: &str = "agent_panel"; const MIN_PANEL_WIDTH: Pixels = px(300.); -const RECENTLY_UPDATED_MENU_LIMIT: usize = 6; const LAST_USED_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; fn agent_v2_enabled(cx: &App) -> bool { @@ -202,12 +201,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); @@ -248,14 +241,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); @@ -666,14 +651,12 @@ impl From for BaseView { } enum OverlayView { - History { view: Entity }, Configuration, } enum VisibleSurface<'a> { Uninitialized, AgentThread(&'a Entity), - History(&'a Entity), Configuration(Option<&'a Entity>), } @@ -691,7 +674,6 @@ impl BaseView { impl OverlayView { pub fn which_font_size_used(&self) -> WhichFontSize { match self { - OverlayView::History { .. } => WhichFontSize::AgentFont, OverlayView::Configuration => WhichFontSize::None, } } @@ -718,8 +700,6 @@ pub struct AgentPanel { retained_threads: HashMap>, new_thread_menu_handle: PopoverMenuHandle, agent_panel_menu_handle: PopoverMenuHandle, - agent_navigation_menu_handle: PopoverMenuHandle, - agent_navigation_menu: Option>, _extension_subscription: Option, _project_subscription: Subscription, zoomed: bool, @@ -950,7 +930,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(); @@ -968,48 +948,6 @@ impl AgentPanel { let base_view = BaseView::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( @@ -1082,8 +1020,7 @@ impl AgentPanel { retained_threads: HashMap::default(), new_thread_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, zoomed: false, @@ -1426,31 +1363,30 @@ 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; + }; + + let Some(parent_session_id) = thread.parent_session_id else { + log::error!("Session {} has no parent session", 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, + session_id: parent_session_id, + title: Some(thread.title), }), true, "agent_panel", @@ -1521,79 +1457,6 @@ 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()), - #[cfg(any(test, feature = "test-support"))] - Agent::Stub => false, - } - } - - 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, - "agent_panel", - window, - cx, - ); - } - }, - ) - .detach(); - view - } - - fn open_history(&mut self, window: &mut Window, cx: &mut Context) { - if matches!(self.overlay_view, Some(OverlayView::History { .. })) { - self.clear_overlay(true, window, cx); - return; - } - - let Some(view) = self.history_for_selected_agent(window, cx) else { - return; - }; - - self.set_overlay(OverlayView::History { view }, true, window, cx); - cx.notify(); - } - pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context) { if self.overlay_view.is_some() { self.clear_overlay(true, window, cx); @@ -1601,18 +1464,6 @@ impl AgentPanel { } } - pub fn toggle_navigation_menu( - &mut self, - _: &ToggleNavigationMenu, - window: &mut Window, - cx: &mut Context, - ) { - if !self.has_history_for_selected_agent(cx) { - return; - } - self.agent_navigation_menu_handle.toggle(window, cx); - } - pub fn toggle_options_menu( &mut self, _: &ToggleOptionsMenu, @@ -2256,18 +2107,7 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - let was_in_history = matches!(self.overlay_view, Some(OverlayView::History { .. })); self.overlay_view = Some(overlay); - - if let Some(OverlayView::History { view }) = &self.overlay_view - && !was_in_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); } @@ -2324,7 +2164,6 @@ impl AgentPanel { fn visible_surface(&self) -> VisibleSurface<'_> { if let Some(overlay_view) = &self.overlay_view { return match overlay_view { - OverlayView::History { view } => VisibleSurface::History(view), OverlayView::Configuration => { VisibleSurface::Configuration(self.configuration.as_ref()) } @@ -2343,10 +2182,6 @@ impl AgentPanel { self.overlay_view.is_some() } - fn is_history_or_configuration_visible(&self) -> bool { - self.is_overlay_open() - } - fn visible_font_size(&self) -> WhichFontSize { self.overlay_view.as_ref().map_or_else( || self.base_view.which_font_size_used(), @@ -2354,63 +2189,6 @@ impl AgentPanel { ) } - 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| { - let agent = this.selected_agent(cx); - this.load_agent_thread( - agent, - entry.session_id.clone(), - entry.work_dirs.clone(), - entry.title.clone(), - true, - "agent_panel", - window, - cx, - ); - }) - .ok(); - } - }); - } - - menu.separator() - } - fn subscribe_to_active_thread_view( server_view: &Entity, window: &mut Window, @@ -2680,7 +2458,6 @@ impl Focusable for AgentPanel { match self.visible_surface() { VisibleSurface::Uninitialized => self.focus_handle.clone(), VisibleSurface::AgentThread(conversation_view) => conversation_view.focus_handle(cx), - VisibleSurface::History(view) => view.read(cx).focus_handle(cx), VisibleSurface::Configuration(configuration) => { if let Some(configuration) = configuration { configuration.focus_handle(cx) @@ -3016,7 +2793,6 @@ impl AgentPanel { .into_any_element() } } - VisibleSurface::History(_) => Label::new("History").truncate().into_any_element(), VisibleSurface::Configuration(_) => { Label::new("Settings").truncate().into_any_element() } @@ -3143,47 +2919,6 @@ impl AgentPanel { }) } - fn render_recent_entries_menu( - &self, - icon: IconName, - corner: Anchor, - cx: &mut Context, - ) -> impl IntoElement { - let focus_handle = self.focus_handle(cx); - - PopoverMenu::new("agent-nav-menu") - .trigger_with_tooltip( - IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), - { - move |_window, cx| { - Tooltip::for_action_in( - "Toggle Recently Updated Threads", - &ToggleNavigationMenu, - &focus_handle, - cx, - ) - } - }, - ) - .anchor(corner) - .with_handle(self.agent_navigation_menu_handle.clone()) - .menu({ - let menu = self.agent_navigation_menu.clone(); - move |window, cx| { - telemetry::event!("View Thread History Clicked"); - - if let Some(menu) = menu.as_ref() { - menu.update(cx, |_, cx| { - cx.defer_in(window, |menu, window, cx| { - menu.rebuild(window, cx); - }); - }) - } - menu.clone() - } - }) - } - fn render_toolbar_back_button(&self, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle(cx); @@ -3453,11 +3188,10 @@ impl AgentPanel { selected_agent.into_any_element() }; - let show_history_menu = self.has_history_for_selected_agent(cx); let agent_v2_enabled = agent_v2_enabled(cx); let is_empty_state = !self.active_thread_has_messages(cx); - let is_in_history_or_config = self.is_history_or_configuration_visible(); + let is_in_history_or_config = self.is_overlay_open(); let is_full_screen = self.is_zoomed(window, cx); let full_screen_button = if is_full_screen { @@ -3552,13 +3286,6 @@ impl AgentPanel { .gap_1() .pl_1() .pr_1() - .when(show_history_menu && !agent_v2_enabled, |this| { - this.child(self.render_recent_entries_menu( - IconName::MenuAltTemp, - Anchor::TopRight, - cx, - )) - }) .child(full_screen_button) .child(self.render_panel_options_menu(window, cx)), ) @@ -3604,13 +3331,6 @@ impl AgentPanel { .pl_1() .pr_1() .child(new_thread_menu) - .when(show_history_menu && !agent_v2_enabled, |this| { - this.child(self.render_recent_entries_menu( - IconName::MenuAltTemp, - Anchor::TopRight, - cx, - )) - }) .child(full_screen_button) .child(self.render_panel_options_menu(window, cx)), ) @@ -3698,17 +3418,13 @@ impl AgentPanel { match &self.base_view { BaseView::Uninitialized => false, - BaseView::AgentThread { conversation_view } - if conversation_view.read(cx).as_native_thread(cx).is_none() => - { - false - } BaseView::AgentThread { conversation_view } => { - let history_is_empty = conversation_view - .read(cx) - .history() - .is_none_or(|h| h.read(cx).is_empty()); - history_is_empty || !has_configured_non_zed_providers + if conversation_view.read(cx).as_native_thread(cx).is_some() { + let history_is_empty = ThreadStore::global(cx).read(cx).is_empty(); + history_is_empty || !has_configured_non_zed_providers + } else { + false + } } } } @@ -3896,16 +3612,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)) @@ -3926,7 +3638,6 @@ impl Render for AgentPanel { VisibleSurface::AgentThread(conversation_view) => parent .child(conversation_view.clone()) .child(self.render_drag_target(cx)), - VisibleSurface::History(view) => parent.child(view.clone()), VisibleSurface::Configuration(configuration) => { parent.children(configuration.cloned()) } @@ -3970,13 +3681,6 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { let Some(panel) = workspace.read(cx).panel::(cx) else { return; }; - let history = panel - .read(cx) - .connection_store() - .read(cx) - .entry(&crate::Agent::NativeAgent) - .and_then(|s| s.read(cx).history()) - .map(|h| h.downgrade()); let project = workspace.read(cx).project().downgrade(); let panel = panel.read(cx); let thread_store = panel.thread_store().clone(); @@ -3986,7 +3690,6 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { project, thread_store, None, - history, initial_prompt, window, cx, @@ -4060,14 +3763,6 @@ impl AgentPanel { self.active_conversation_view() } - /// 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); - } - /// Creates a draft thread using a stub server and sets it as the active view. #[cfg(any(test, feature = "test-support"))] pub fn open_draft_with_server( diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 437627105c6a1837555518c33447c6797f89f098..fa9fa244ef887930906ab48ef49ee69f3573ed45 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -27,8 +27,6 @@ mod terminal_codegen; mod terminal_inline_assistant; #[cfg(any(test, feature = "test-support"))] pub mod test_support; -mod thread_history; -mod thread_history_view; mod thread_import; pub mod thread_metadata_store; pub mod thread_worktree_archive; @@ -73,8 +71,6 @@ pub use external_source_prompt::ExternalSourcePrompt; 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, CrossChannelImportOnboarding, ThreadImportModal, channels_with_threads, import_threads_from_other_channels, @@ -89,8 +85,6 @@ actions!( [ /// Toggles the menu to create new agent threads. ToggleNewThreadMenu, - /// 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. @@ -101,10 +95,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, /// Archives the currently selected thread. diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs index 47fd7b0295adbcd2ecea768c3bd9e321a5f551b9..308494409bd9bf4a2cf4346dcd56d06ad13f20be 100644 --- a/crates/agent_ui/src/completion_provider.rs +++ b/crates/agent_ui/src/completion_provider.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::sync::atomic::AtomicBool; use crate::DEFAULT_THREAD_TITLE; -use crate::ThreadHistory; +use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore}; use acp_thread::MentionUri; use agent_client_protocol as acp; use anyhow::Result; @@ -222,7 +222,6 @@ pub struct PromptCompletionProvider { source: Arc, editor: WeakEntity, mention_set: Entity, - history: Option>, prompt_store: Option>, workspace: WeakEntity, } @@ -232,7 +231,6 @@ impl PromptCompletionProvider { source: T, editor: WeakEntity, mention_set: Entity, - history: Option>, prompt_store: Option>, workspace: WeakEntity, ) -> Self { @@ -241,7 +239,6 @@ impl PromptCompletionProvider { editor, mention_set, workspace, - history, prompt_store, } } @@ -918,16 +915,8 @@ impl PromptCompletionProvider { } Some(PromptContextType::Thread) => { - if let Some(history) = self.history.as_ref().and_then(|h| h.upgrade()) { - let sessions = history - .read(cx) - .sessions() - .iter() - .map(|session| SessionMatch { - session_id: session.session_id.clone(), - title: session_title(session.title.clone()), - }) - .collect::>(); + let sessions = collect_session_matches(cx); + if !sessions.is_empty() { let search_task = filter_sessions_by_query(query, cancellation_flag, sessions, cx); cx.spawn(async move |_cx| { @@ -1144,29 +1133,21 @@ impl PromptCompletionProvider { return Task::ready(recent); } - if let Some(history) = self.history.as_ref().and_then(|h| h.upgrade()) { - const RECENT_COUNT: usize = 2; - recent.extend( - history - .read(cx) - .sessions() - .into_iter() - .map(|session| SessionMatch { - session_id: session.session_id.clone(), - title: session_title(session.title.clone()), - }) - .filter(|session| { - let uri = MentionUri::Thread { - id: session.session_id.clone(), - name: session.title.to_string(), - }; - !mentions.contains(&uri) - }) - .take(RECENT_COUNT) - .map(Match::RecentThread), - ); - return Task::ready(recent); - } + let sessions = collect_session_matches(cx); + const RECENT_COUNT: usize = 2; + recent.extend( + sessions + .into_iter() + .filter(|session| { + let uri = MentionUri::Thread { + id: session.session_id.clone(), + name: session.title.to_string(), + }; + !mentions.contains(&uri) + }) + .take(RECENT_COUNT) + .map(Match::RecentThread), + ); Task::ready(recent) } @@ -2030,6 +2011,28 @@ pub(crate) fn search_symbols( }) } +fn collect_session_matches(cx: &App) -> Vec { + let Some(store) = ThreadMetadataStore::try_global(cx) else { + return Vec::new(); + }; + let mut entries: Vec<&ThreadMetadata> = store + .read(cx) + .entries() + .filter(|t| !t.archived && t.agent_id == *agent::ZED_AGENT_ID) + .collect(); + entries.sort_by_key(|t| Reverse(t.updated_at)); + entries + .into_iter() + .map(|metadata| { + let info = acp_thread::AgentSessionInfo::from(metadata); + SessionMatch { + session_id: info.session_id, + title: session_title(info.title), + } + }) + .collect() +} + fn filter_sessions_by_query( query: String, cancellation_flag: Arc, diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index b796f6dcfcf26d054ba79079f8dc8a885b911991..181e439cf6bc01922570c8ff81169bda7146e8ba 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -74,7 +74,6 @@ use zed_actions::assistant::OpenRulesLibrary; use super::config_options::ConfigOptionsView; use super::entry_view_state::EntryViewState; -use super::thread_history::ThreadHistory; use crate::ModeSelector; use crate::ModelSelectorPopover; use crate::agent_connection_store::{ @@ -556,7 +555,6 @@ pub struct ConnectedServerState { active_id: Option, pub(crate) threads: HashMap>, connection: Rc, - history: Option>, conversation: Entity, _connection_entry_subscription: Subscription, } @@ -791,11 +789,8 @@ impl ConversationView { }; let load_task = cx.spawn_in(window, async move |this, cx| { - let (connection, history) = match connect_result.await { - Ok(AgentConnectedState { - connection, - history, - }) => (connection, history), + let connection = match connect_result.await { + Ok(AgentConnectedState { connection, .. }) => connection, Err(err) => { this.update_in(cx, |this, window, cx| { this.handle_load_error(err, window, cx); @@ -891,7 +886,6 @@ impl ConversationView { conversation.clone(), resumed_without_history, initial_content, - history.clone(), window, cx, ); @@ -912,7 +906,6 @@ impl ConversationView { active_id: Some(root_session_id.clone()), threads: HashMap::from_iter([(root_session_id, current)]), conversation, - history, _connection_entry_subscription: connection_entry_subscription, }), cx, @@ -945,7 +938,6 @@ impl ConversationView { conversation: Entity, resumed_without_history: bool, initial_content: Option, - history: Option>, window: &mut Window, cx: &mut Context, ) -> Entity { @@ -962,7 +954,6 @@ impl ConversationView { self.workspace.clone(), self.project.downgrade(), self.thread_store.clone(), - history.as_ref().map(|h| h.downgrade()), self.prompt_store.clone(), session_capabilities.clone(), self.agent.agent_id(), @@ -1130,7 +1121,6 @@ impl ConversationView { resumed_without_history, self.project.downgrade(), self.thread_store.clone(), - history, self.prompt_store.clone(), initial_content, subscriptions, @@ -1212,7 +1202,6 @@ impl ConversationView { threads: HashMap::default(), connection, conversation: cx.new(|_cx| Conversation::default()), - history: None, _connection_entry_subscription: Subscription::new(|| {}), }), cx, @@ -1787,9 +1776,9 @@ impl ConversationView { cx.spawn_in(window, async move |this, cx| { let subagent_thread = subagent_thread_task.await?; this.update_in(cx, |this, window, cx| { - let Some((conversation, history)) = this + let Some(conversation) = this .as_connected() - .map(|connected| (connected.conversation.clone(), connected.history.clone())) + .map(|connected| connected.conversation.clone()) else { return; }; @@ -1797,15 +1786,8 @@ impl ConversationView { conversation.update(cx, |conversation, cx| { conversation.register_thread(subagent_thread.clone(), cx); }); - let view = this.new_thread_view( - subagent_thread, - conversation, - false, - None, - history, - window, - cx, - ); + let view = + this.new_thread_view(subagent_thread, conversation, false, None, window, cx); let Some(connected) = this.as_connected_mut() else { return; }; @@ -2264,7 +2246,6 @@ impl ConversationView { let Some(connected) = self.as_connected() else { return; }; - let history = connected.history.as_ref().map(|h| h.downgrade()); let Some(thread) = connected.active_view() else { return; }; @@ -2305,7 +2286,6 @@ impl ConversationView { workspace.clone(), project.clone(), None, - history.clone(), None, session_capabilities.clone(), agent_name.clone(), @@ -2709,10 +2689,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 { @@ -2862,9 +2838,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; @@ -3029,66 +3003,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); @@ -4040,22 +3954,7 @@ pub(crate) mod tests { agent: impl AgentServer + 'static, cx: &mut TestAppContext, ) -> (Entity, &mut VisualTestContext) { - let (conversation_view, _history, cx) = - setup_conversation_view_with_history_and_initial_content(agent, None, cx).await; - (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) + setup_conversation_view_with_initial_content_opt(agent, None, cx).await } async fn setup_conversation_view_with_initial_content( @@ -4063,25 +3962,14 @@ pub(crate) mod tests { initial_content: AgentInitialContent, cx: &mut TestAppContext, ) -> (Entity, &mut VisualTestContext) { - let (conversation_view, _history, cx) = - setup_conversation_view_with_history_and_initial_content( - agent, - Some(initial_content), - cx, - ) - .await; - (conversation_view, cx) + setup_conversation_view_with_initial_content_opt(agent, Some(initial_content), cx).await } - async fn setup_conversation_view_with_history_and_initial_content( + async fn setup_conversation_view_with_initial_content_opt( agent: impl AgentServer + 'static, initial_content: Option, cx: &mut TestAppContext, - ) -> ( - Entity, - Option>, - &mut VisualTestContext, - ) { + ) -> (Entity, &mut VisualTestContext) { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; let (multi_workspace, cx) = @@ -4117,14 +4005,7 @@ pub(crate) mod tests { }); cx.run_until_parked(); - let history = cx.update(|_window, cx| { - connection_store - .read(cx) - .entry(&agent_key) - .and_then(|e| e.read(cx).history().cloned()) - }); - - (conversation_view, history, cx) + (conversation_view, cx) } fn add_to_workspace(conversation_view: Entity, cx: &mut VisualTestContext) { @@ -4256,42 +4137,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, @@ -4320,67 +4165,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: acp_thread::UserMessageId, - _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 db8a15c62719322921bdd2d0f307651a5efb8572..8457e036450021afb2e4838898e8c618471a53e4 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -326,14 +326,10 @@ 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 multi_root_callout_dismissed: 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 { @@ -375,7 +371,6 @@ impl ThreadView { resumed_without_history: bool, project: WeakEntity, thread_store: Option>, - history: Option>, prompt_store: Option>, initial_content: Option, mut subscriptions: Vec, @@ -388,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; @@ -402,7 +391,6 @@ impl ThreadView { workspace.clone(), project.clone(), thread_store, - history.as_ref().map(|h| h.downgrade()), prompt_store, session_capabilities.clone(), agent_id.clone(), @@ -501,11 +489,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 { session_id, parent_session_id, @@ -568,11 +551,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, multi_root_callout_dismissed: false, generating_indicator_in_list: false, @@ -8666,16 +8645,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/entry_view_state.rs b/crates/agent_ui/src/entry_view_state.rs index 8543b3c96199e7b303971b476ffaade9484384a7..b1476ea93e073609b7ed4435de177ede28047ed9 100644 --- a/crates/agent_ui/src/entry_view_state.rs +++ b/crates/agent_ui/src/entry_view_state.rs @@ -1,6 +1,5 @@ use std::ops::Range; -use super::thread_history::ThreadHistory; use acp_thread::{AcpThread, AgentThreadEntry}; use agent::ThreadStore; use agent_client_protocol::ToolCallId; @@ -26,7 +25,6 @@ pub struct EntryViewState { workspace: WeakEntity, project: WeakEntity, thread_store: Option>, - history: Option>, prompt_store: Option>, entries: Vec, session_capabilities: SharedSessionCapabilities, @@ -38,7 +36,6 @@ impl EntryViewState { workspace: WeakEntity, project: WeakEntity, thread_store: Option>, - history: Option>, prompt_store: Option>, session_capabilities: SharedSessionCapabilities, agent_id: AgentId, @@ -47,7 +44,6 @@ impl EntryViewState { workspace, project, thread_store, - history, prompt_store, entries: Vec::new(), session_capabilities, @@ -90,7 +86,6 @@ impl EntryViewState { self.workspace.clone(), self.project.clone(), self.thread_store.clone(), - self.history.clone(), self.prompt_store.clone(), self.session_capabilities.clone(), self.agent_id.clone(), @@ -544,14 +539,12 @@ mod tests { }); let thread_store = None; - let history: Option> = None; let view_state = cx.new(|_cx| { EntryViewState::new( workspace.downgrade(), project.downgrade(), thread_store, - history, None, Arc::new(RwLock::new(SessionCapabilities::default())), "Test Agent".into(), diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index ce74b7f78cda0ea14a79593f83e5666795f80e5e..71aa7baf7816b3ab62e11b3468579d13f79cebbe 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -6,7 +6,6 @@ use std::ops::Range; use std::sync::Arc; use uuid::Uuid; -use crate::ThreadHistory; use crate::context::load_context; use crate::mention_set::MentionSet; use crate::{ @@ -231,11 +230,6 @@ impl InlineAssistant { let prompt_store = agent_panel.prompt_store().as_ref().cloned(); let thread_store = agent_panel.thread_store().clone(); - let history = agent_panel - .connection_store() - .read(cx) - .entry(&crate::Agent::NativeAgent) - .and_then(|s| s.read(cx).history().cloned()); let handle_assist = |window: &mut Window, cx: &mut Context| match inline_assist_target { @@ -247,7 +241,6 @@ impl InlineAssistant { workspace.project().downgrade(), thread_store, prompt_store, - history.as_ref().map(|h| h.downgrade()), action.prompt.clone(), window, cx, @@ -262,7 +255,6 @@ impl InlineAssistant { workspace.project().downgrade(), thread_store, prompt_store, - history.as_ref().map(|h| h.downgrade()), action.prompt.clone(), window, cx, @@ -446,7 +438,6 @@ impl InlineAssistant { project: WeakEntity, thread_store: Entity, prompt_store: Option>, - history: Option>, initial_prompt: Option, window: &mut Window, codegen_ranges: &[Range], @@ -493,7 +484,6 @@ impl InlineAssistant { self.fs.clone(), thread_store.clone(), prompt_store.clone(), - history.clone(), project.clone(), workspace.clone(), window, @@ -585,7 +575,6 @@ impl InlineAssistant { project: WeakEntity, thread_store: Entity, prompt_store: Option>, - history: Option>, initial_prompt: Option, window: &mut Window, cx: &mut App, @@ -604,7 +593,6 @@ impl InlineAssistant { project, thread_store, prompt_store, - history, initial_prompt, window, &codegen_ranges, @@ -630,7 +618,6 @@ impl InlineAssistant { workspace: Entity, thread_store: Entity, prompt_store: Option>, - history: Option>, window: &mut Window, cx: &mut App, ) -> InlineAssistId { @@ -650,7 +637,6 @@ impl InlineAssistant { project, thread_store, prompt_store, - history, Some(initial_prompt), window, &[range], @@ -1975,7 +1961,6 @@ pub mod evals { project.downgrade(), thread_store, None, - None, Some(prompt), window, cx, diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 5d168d410476b1a367042d886715c2d57d50477e..8a41dc60e88eb525a4dd6b75d5a15a1fd2db7787 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -1,4 +1,3 @@ -use crate::ThreadHistory; use agent::ThreadStore; use agent_settings::AgentSettings; use collections::{HashMap, VecDeque}; @@ -64,7 +63,6 @@ pub struct PromptEditor { pub editor: Entity, mode: PromptEditorMode, mention_set: Entity, - history: Option>, prompt_store: Option>, workspace: WeakEntity, model_selector: Entity, @@ -335,7 +333,6 @@ impl PromptEditor { PromptEditorCompletionProviderDelegate, cx.weak_entity(), self.mention_set.clone(), - self.history.clone(), self.prompt_store.clone(), self.workspace.clone(), )))); @@ -1227,7 +1224,6 @@ impl PromptEditor { fs: Arc, thread_store: Entity, prompt_store: Option>, - history: Option>, project: WeakEntity, workspace: WeakEntity, window: &mut Window, @@ -1274,7 +1270,6 @@ impl PromptEditor { let mut this: PromptEditor = PromptEditor { editor: prompt_editor.clone(), mention_set, - history, prompt_store, workspace, model_selector: cx.new(|cx| { @@ -1386,7 +1381,6 @@ impl PromptEditor { fs: Arc, thread_store: Entity, prompt_store: Option>, - history: Option>, project: WeakEntity, workspace: WeakEntity, window: &mut Window, @@ -1428,7 +1422,6 @@ impl PromptEditor { let mut this = Self { editor: prompt_editor.clone(), mention_set, - history, prompt_store, workspace, model_selector: cx.new(|cx| { diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 3b93439b62305f63596abcaebe562e7b3f2a65f3..3bd40fd2537ff881b1779ecd2d32c4fb029885f2 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1,6 +1,5 @@ use crate::DEFAULT_THREAD_TITLE; use crate::SendImmediately; -use crate::ThreadHistory; use crate::{ ChatWithFollow, completion_provider::{ @@ -394,7 +393,6 @@ impl MessageEditor { workspace: WeakEntity, project: WeakEntity, thread_store: Option>, - history: Option>, prompt_store: Option>, session_capabilities: SharedSessionCapabilities, agent_id: AgentId, @@ -458,7 +456,6 @@ impl MessageEditor { }, editor.downgrade(), mention_set.clone(), - history, prompt_store.clone(), workspace.clone(), )); @@ -2053,7 +2050,6 @@ mod tests { project.downgrade(), thread_store.clone(), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -2155,7 +2151,6 @@ mod tests { project.downgrade(), thread_store.clone(), None, - None, session_capabilities.clone(), "Claude Agent".into(), "Test", @@ -2322,7 +2317,6 @@ mod tests { project.downgrade(), thread_store.clone(), None, - None, session_capabilities.clone(), "Test Agent".into(), "Test", @@ -2549,7 +2543,6 @@ mod tests { project.downgrade(), Some(thread_store), None, - None, session_capabilities.clone(), "Test Agent".into(), "Test", @@ -3042,7 +3035,6 @@ mod tests { project.downgrade(), thread_store.clone(), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -3144,7 +3136,6 @@ mod tests { project.downgrade(), thread_store.clone(), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -3214,7 +3205,6 @@ mod tests { project.downgrade(), thread_store.clone(), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -3268,7 +3258,6 @@ mod tests { project.downgrade(), thread_store.clone(), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -3326,7 +3315,6 @@ mod tests { project.downgrade(), thread_store.clone(), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -3385,7 +3373,6 @@ mod tests { project.downgrade(), thread_store.clone(), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -3448,7 +3435,6 @@ mod tests { project.downgrade(), thread_store.clone(), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -3609,7 +3595,6 @@ mod tests { project.downgrade(), thread_store.clone(), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -3724,7 +3709,6 @@ mod tests { project.downgrade(), Some(thread_store.clone()), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -3804,7 +3788,6 @@ mod tests { project.downgrade(), Some(thread_store), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -3903,7 +3886,6 @@ mod tests { project.downgrade(), Some(thread_store), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -4159,7 +4141,6 @@ mod tests { project.downgrade(), Some(thread_store), None, - None, Default::default(), "Test Agent".into(), "Test", @@ -4253,7 +4234,6 @@ mod tests { project.downgrade(), None, None, - None, Default::default(), "Test Agent".into(), "Test", @@ -4403,7 +4383,6 @@ mod tests { project.downgrade(), None, None, - None, Default::default(), "Test Agent".into(), "Test", diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs index 89c1ec431386e548dc9188b46fe2f88ffef77668..c4db6a088da105fc3f73322fccc57b30f3d6d733 100644 --- a/crates/agent_ui/src/terminal_inline_assistant.rs +++ b/crates/agent_ui/src/terminal_inline_assistant.rs @@ -1,5 +1,4 @@ use crate::{ - ThreadHistory, context::load_context, inline_prompt_editor::{ CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId, @@ -66,7 +65,6 @@ impl TerminalInlineAssistant { project: WeakEntity, thread_store: Entity, prompt_store: Option>, - history: Option>, initial_prompt: Option, window: &mut Window, cx: &mut App, @@ -92,7 +90,6 @@ impl TerminalInlineAssistant { self.fs.clone(), thread_store.clone(), prompt_store.clone(), - history, project.clone(), workspace.clone(), window, diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs deleted file mode 100644 index 7b7a3e60211896bf717fb3dfb2670d92b7409281..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/thread_history.rs +++ /dev/null @@ -1,772 +0,0 @@ -use acp_thread::{AgentSessionInfo, AgentSessionList, AgentSessionListRequest, SessionListUpdate}; -use agent_client_protocol as acp; -use gpui::{App, Task}; -use std::rc::Rc; -use ui::prelude::*; - -pub struct ThreadHistory { - session_list: Rc, - sessions: Vec, - _refresh_task: Task<()>, - _watch_task: Option>, -} - -impl ThreadHistory { - pub fn new(session_list: Rc, cx: &mut Context) -> Self { - let mut this = Self { - session_list, - sessions: Vec::new(), - _refresh_task: Task::ready(()), - _watch_task: None, - }; - - this.start_watching(cx); - this - } - - #[cfg(any(test, feature = "test-support"))] - pub fn set_session_list( - &mut self, - session_list: Rc, - cx: &mut Context, - ) { - if Rc::ptr_eq(&self.session_list, &session_list) { - return; - } - - self.session_list = session_list; - self.sessions.clear(); - self._refresh_task = Task::ready(()); - self.start_watching(cx); - } - - fn start_watching(&mut self, cx: &mut Context) { - let Some(rx) = self.session_list.watch(cx) else { - self._watch_task = None; - self.refresh_sessions(false, cx); - return; - }; - self.session_list.notify_refresh(); - - self._watch_task = Some(cx.spawn(async move |this, cx| { - while let Ok(first_update) = rx.recv().await { - let mut updates = vec![first_update]; - while let Ok(update) = rx.try_recv() { - updates.push(update); - } - - this.update(cx, |this, cx| { - let needs_refresh = updates - .iter() - .any(|u| matches!(u, SessionListUpdate::Refresh)); - - if needs_refresh { - this.refresh_sessions(false, cx); - } else { - for update in updates { - if let SessionListUpdate::SessionInfo { session_id, update } = update { - this.apply_info_update(session_id, update, cx); - } - } - } - }) - .ok(); - } - })); - } - - 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, - info_update: acp::SessionInfoUpdate, - cx: &mut Context, - ) { - let Some(session) = self - .sessions - .iter_mut() - .find(|s| s.session_id == session_id) - else { - return; - }; - - match info_update.title { - acp::MaybeUndefined::Value(title) => { - session.title = Some(title.into()); - } - acp::MaybeUndefined::Null => { - session.title = None; - } - acp::MaybeUndefined::Undefined => {} - } - match info_update.updated_at { - acp::MaybeUndefined::Value(date_str) => { - if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&date_str) { - session.updated_at = Some(dt.with_timezone(&chrono::Utc)); - } - } - acp::MaybeUndefined::Null => { - session.updated_at = None; - } - acp::MaybeUndefined::Undefined => {} - } - if let Some(meta) = info_update.meta { - session.meta = Some(meta); - } - - cx.notify(); - } - - fn refresh_sessions(&mut self, load_all_pages: bool, cx: &mut Context) { - let session_list = self.session_list.clone(); - - self._refresh_task = cx.spawn(async move |this, cx| { - let mut cursor: Option = None; - let mut is_first_page = true; - - loop { - let request = AgentSessionListRequest { - cursor: cursor.clone(), - ..Default::default() - }; - let task = cx.update(|cx| session_list.list_sessions(request, cx)); - let response = match task.await { - Ok(response) => response, - Err(error) => { - log::error!("Failed to load session history: {error:#}"); - return; - } - }; - - let acp_thread::AgentSessionListResponse { - sessions: page_sessions, - next_cursor, - .. - } = response; - - this.update(cx, |this, cx| { - if is_first_page { - this.sessions = page_sessions; - } else { - this.sessions.extend(page_sessions); - } - cx.notify(); - }) - .ok(); - - is_first_page = false; - if !load_all_pages { - break; - } - - match next_cursor { - Some(next_cursor) => { - if cursor.as_ref() == Some(&next_cursor) { - log::warn!( - "Session list pagination returned the same cursor; stopping to avoid a loop." - ); - break; - } - cursor = Some(next_cursor); - } - None => break, - } - } - }); - } - - pub(crate) fn is_empty(&self) -> bool { - self.sessions.is_empty() - } - - pub fn refresh(&mut self, _cx: &mut Context) { - self.session_list.notify_refresh(); - } - - pub fn session_for_id(&self, session_id: &acp::SessionId) -> Option { - self.sessions - .iter() - .find(|entry| &entry.session_id == session_id) - .cloned() - } - - 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)] -mod tests { - use super::*; - use acp_thread::AgentSessionListResponse; - use gpui::TestAppContext; - use std::{ - any::Any, - sync::{Arc, Mutex}, - }; - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = settings::SettingsStore::test(cx); - cx.set_global(settings_store); - theme_settings::init(theme::LoadThemes::JustBase, cx); - }); - } - - #[derive(Clone)] - struct TestSessionList { - sessions: Vec, - updates_tx: smol::channel::Sender, - updates_rx: smol::channel::Receiver, - } - - impl TestSessionList { - fn new(sessions: Vec) -> Self { - let (tx, rx) = smol::channel::unbounded(); - Self { - sessions, - updates_tx: tx, - updates_rx: rx, - } - } - - fn send_update(&self, update: SessionListUpdate) { - self.updates_tx.try_send(update).ok(); - } - } - - impl AgentSessionList for TestSessionList { - fn list_sessions( - &self, - _request: AgentSessionListRequest, - _cx: &mut App, - ) -> Task> { - Task::ready(Ok(AgentSessionListResponse::new(self.sessions.clone()))) - } - - fn watch(&self, _cx: &mut App) -> Option> { - Some(self.updates_rx.clone()) - } - - fn notify_refresh(&self) { - self.send_update(SessionListUpdate::Refresh); - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - #[derive(Clone)] - struct PaginatedTestSessionList { - first_page_sessions: Vec, - second_page_sessions: Vec, - requested_cursors: Arc>>>, - async_responses: bool, - updates_tx: smol::channel::Sender, - updates_rx: smol::channel::Receiver, - } - - impl PaginatedTestSessionList { - fn new( - first_page_sessions: Vec, - second_page_sessions: Vec, - ) -> Self { - let (tx, rx) = smol::channel::unbounded(); - Self { - first_page_sessions, - second_page_sessions, - requested_cursors: Arc::new(Mutex::new(Vec::new())), - async_responses: false, - updates_tx: tx, - updates_rx: rx, - } - } - - fn with_async_responses(mut self) -> Self { - self.async_responses = true; - self - } - - fn requested_cursors(&self) -> Vec> { - self.requested_cursors.lock().unwrap().clone() - } - - fn clear_requested_cursors(&self) { - self.requested_cursors.lock().unwrap().clear() - } - - fn send_update(&self, update: SessionListUpdate) { - self.updates_tx.try_send(update).ok(); - } - } - - impl AgentSessionList for PaginatedTestSessionList { - fn list_sessions( - &self, - request: AgentSessionListRequest, - cx: &mut App, - ) -> Task> { - let requested_cursors = self.requested_cursors.clone(); - let first_page_sessions = self.first_page_sessions.clone(); - let second_page_sessions = self.second_page_sessions.clone(); - - let respond = move || { - requested_cursors - .lock() - .unwrap() - .push(request.cursor.clone()); - - match request.cursor.as_deref() { - None => AgentSessionListResponse { - sessions: first_page_sessions, - next_cursor: Some("page-2".to_string()), - meta: None, - }, - Some("page-2") => AgentSessionListResponse::new(second_page_sessions), - _ => AgentSessionListResponse::new(Vec::new()), - } - }; - - if self.async_responses { - cx.foreground_executor().spawn(async move { - smol::future::yield_now().await; - Ok(respond()) - }) - } else { - Task::ready(Ok(respond())) - } - } - - fn watch(&self, _cx: &mut App) -> Option> { - Some(self.updates_rx.clone()) - } - - fn notify_refresh(&self) { - self.send_update(SessionListUpdate::Refresh); - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - fn test_session(session_id: &str, title: &str) -> AgentSessionInfo { - AgentSessionInfo { - session_id: acp::SessionId::new(session_id), - work_dirs: None, - title: Some(title.to_string().into()), - updated_at: None, - created_at: None, - meta: None, - } - } - - #[gpui::test] - async fn test_refresh_only_loads_first_page_by_default(cx: &mut TestAppContext) { - init_test(cx); - - let session_list = Rc::new(PaginatedTestSessionList::new( - vec![test_session("session-1", "First")], - vec![test_session("session-2", "Second")], - )); - - let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - assert_eq!(history.sessions.len(), 1); - assert_eq!( - history.sessions[0].session_id, - acp::SessionId::new("session-1") - ); - }); - assert_eq!(session_list.requested_cursors(), vec![None]); - } - - #[gpui::test] - async fn test_enabling_full_pagination_loads_all_pages(cx: &mut TestAppContext) { - init_test(cx); - - let session_list = Rc::new(PaginatedTestSessionList::new( - vec![test_session("session-1", "First")], - vec![test_session("session-2", "Second")], - )); - - let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); - cx.run_until_parked(); - session_list.clear_requested_cursors(); - - history.update(cx, |history, cx| history.refresh_full_history(cx)); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - assert_eq!(history.sessions.len(), 2); - assert_eq!( - history.sessions[0].session_id, - acp::SessionId::new("session-1") - ); - assert_eq!( - history.sessions[1].session_id, - acp::SessionId::new("session-2") - ); - }); - assert_eq!( - session_list.requested_cursors(), - vec![None, Some("page-2".to_string())] - ); - } - - #[gpui::test] - async fn test_standard_refresh_replaces_with_first_page_after_full_history_refresh( - cx: &mut TestAppContext, - ) { - init_test(cx); - - let session_list = Rc::new(PaginatedTestSessionList::new( - vec![test_session("session-1", "First")], - vec![test_session("session-2", "Second")], - )); - - 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)); - cx.run_until_parked(); - session_list.clear_requested_cursors(); - - history.update(cx, |history, cx| { - history.refresh(cx); - }); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - assert_eq!(history.sessions.len(), 1); - assert_eq!( - history.sessions[0].session_id, - acp::SessionId::new("session-1") - ); - }); - assert_eq!(session_list.requested_cursors(), vec![None]); - } - - #[gpui::test] - async fn test_re_entering_full_pagination_reloads_all_pages(cx: &mut TestAppContext) { - init_test(cx); - - let session_list = Rc::new(PaginatedTestSessionList::new( - vec![test_session("session-1", "First")], - vec![test_session("session-2", "Second")], - )); - - 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)); - cx.run_until_parked(); - session_list.clear_requested_cursors(); - - history.update(cx, |history, cx| history.refresh_full_history(cx)); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - assert_eq!(history.sessions.len(), 2); - }); - assert_eq!( - session_list.requested_cursors(), - vec![None, Some("page-2".to_string())] - ); - } - - #[gpui::test] - async fn test_partial_refresh_batch_drops_non_first_page_sessions(cx: &mut TestAppContext) { - init_test(cx); - - let second_page_session_id = acp::SessionId::new("session-2"); - let session_list = Rc::new(PaginatedTestSessionList::new( - vec![test_session("session-1", "First")], - vec![test_session("session-2", "Second")], - )); - - 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)); - cx.run_until_parked(); - - session_list.clear_requested_cursors(); - - session_list.send_update(SessionListUpdate::SessionInfo { - session_id: second_page_session_id.clone(), - update: acp::SessionInfoUpdate::new().title("Updated Second"), - }); - session_list.send_update(SessionListUpdate::Refresh); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - assert_eq!(history.sessions.len(), 1); - assert_eq!( - history.sessions[0].session_id, - acp::SessionId::new("session-1") - ); - assert!( - history - .sessions - .iter() - .all(|session| session.session_id != second_page_session_id) - ); - }); - assert_eq!(session_list.requested_cursors(), vec![None]); - } - - #[gpui::test] - async fn test_full_pagination_works_with_async_page_fetches(cx: &mut TestAppContext) { - init_test(cx); - - let session_list = Rc::new( - PaginatedTestSessionList::new( - vec![test_session("session-1", "First")], - vec![test_session("session-2", "Second")], - ) - .with_async_responses(), - ); - - let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); - cx.run_until_parked(); - session_list.clear_requested_cursors(); - - history.update(cx, |history, cx| history.refresh_full_history(cx)); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - assert_eq!(history.sessions.len(), 2); - }); - assert_eq!( - session_list.requested_cursors(), - vec![None, Some("page-2".to_string())] - ); - } - - #[gpui::test] - async fn test_apply_info_update_title(cx: &mut TestAppContext) { - init_test(cx); - - let session_id = acp::SessionId::new("test-session"); - let sessions = vec![AgentSessionInfo { - session_id: session_id.clone(), - work_dirs: None, - title: Some("Original Title".into()), - updated_at: None, - created_at: None, - meta: None, - }]; - let session_list = Rc::new(TestSessionList::new(sessions)); - - let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); - cx.run_until_parked(); - - session_list.send_update(SessionListUpdate::SessionInfo { - session_id: session_id.clone(), - update: acp::SessionInfoUpdate::new().title("New Title"), - }); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - let session = history.sessions.iter().find(|s| s.session_id == session_id); - assert_eq!( - session.unwrap().title.as_ref().map(|s| s.as_ref()), - Some("New Title") - ); - }); - } - - #[gpui::test] - async fn test_apply_info_update_clears_title_with_null(cx: &mut TestAppContext) { - init_test(cx); - - let session_id = acp::SessionId::new("test-session"); - let sessions = vec![AgentSessionInfo { - session_id: session_id.clone(), - work_dirs: None, - title: Some("Original Title".into()), - updated_at: None, - created_at: None, - meta: None, - }]; - let session_list = Rc::new(TestSessionList::new(sessions)); - - let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); - cx.run_until_parked(); - - session_list.send_update(SessionListUpdate::SessionInfo { - session_id: session_id.clone(), - update: acp::SessionInfoUpdate::new().title(None::), - }); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - let session = history.sessions.iter().find(|s| s.session_id == session_id); - assert_eq!(session.unwrap().title, None); - }); - } - - #[gpui::test] - async fn test_apply_info_update_ignores_undefined_fields(cx: &mut TestAppContext) { - init_test(cx); - - let session_id = acp::SessionId::new("test-session"); - let sessions = vec![AgentSessionInfo { - session_id: session_id.clone(), - work_dirs: None, - title: Some("Original Title".into()), - updated_at: None, - created_at: None, - meta: None, - }]; - let session_list = Rc::new(TestSessionList::new(sessions)); - - let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); - cx.run_until_parked(); - - session_list.send_update(SessionListUpdate::SessionInfo { - session_id: session_id.clone(), - update: acp::SessionInfoUpdate::new(), - }); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - let session = history.sessions.iter().find(|s| s.session_id == session_id); - assert_eq!( - session.unwrap().title.as_ref().map(|s| s.as_ref()), - Some("Original Title") - ); - }); - } - - #[gpui::test] - async fn test_multiple_info_updates_applied_in_order(cx: &mut TestAppContext) { - init_test(cx); - - let session_id = acp::SessionId::new("test-session"); - let sessions = vec![AgentSessionInfo { - session_id: session_id.clone(), - work_dirs: None, - title: None, - updated_at: None, - created_at: None, - meta: None, - }]; - let session_list = Rc::new(TestSessionList::new(sessions)); - - let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); - cx.run_until_parked(); - - session_list.send_update(SessionListUpdate::SessionInfo { - session_id: session_id.clone(), - update: acp::SessionInfoUpdate::new().title("First Title"), - }); - session_list.send_update(SessionListUpdate::SessionInfo { - session_id: session_id.clone(), - update: acp::SessionInfoUpdate::new().title("Second Title"), - }); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - let session = history.sessions.iter().find(|s| s.session_id == session_id); - assert_eq!( - session.unwrap().title.as_ref().map(|s| s.as_ref()), - Some("Second Title") - ); - }); - } - - #[gpui::test] - async fn test_refresh_supersedes_info_updates(cx: &mut TestAppContext) { - init_test(cx); - - let session_id = acp::SessionId::new("test-session"); - let sessions = vec![AgentSessionInfo { - session_id: session_id.clone(), - work_dirs: None, - title: Some("Server Title".into()), - updated_at: None, - created_at: None, - meta: None, - }]; - let session_list = Rc::new(TestSessionList::new(sessions)); - - let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); - cx.run_until_parked(); - - session_list.send_update(SessionListUpdate::SessionInfo { - session_id: session_id.clone(), - update: acp::SessionInfoUpdate::new().title("Local Update"), - }); - session_list.send_update(SessionListUpdate::Refresh); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - let session = history.sessions.iter().find(|s| s.session_id == session_id); - assert_eq!( - session.unwrap().title.as_ref().map(|s| s.as_ref()), - Some("Server Title") - ); - }); - } - - #[gpui::test] - async fn test_info_update_for_unknown_session_is_ignored(cx: &mut TestAppContext) { - init_test(cx); - - let session_id = acp::SessionId::new("known-session"); - let sessions = vec![AgentSessionInfo { - session_id, - work_dirs: None, - title: Some("Original".into()), - updated_at: None, - created_at: None, - meta: None, - }]; - let session_list = Rc::new(TestSessionList::new(sessions)); - - let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); - cx.run_until_parked(); - - session_list.send_update(SessionListUpdate::SessionInfo { - session_id: acp::SessionId::new("unknown-session"), - update: acp::SessionInfoUpdate::new().title("Should Be Ignored"), - }); - cx.run_until_parked(); - - history.update(cx, |history, _cx| { - assert_eq!(history.sessions.len(), 1); - assert_eq!( - history.sessions[0].title.as_ref().map(|s| s.as_ref()), - Some("Original") - ); - }); - } -} 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 1cebd175be46eaabd420853a3997ae1fd6ce7a50..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() - .and_then(|title| if title.is_empty() { None } else { Some(title) }) - .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 all 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); - } -} diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md index 89b0126c55a12b08d4f21a01fea38758c4d509b7..5ac27c907356893dd686a11481091378c1b92f80 100644 --- a/docs/src/ai/agent-panel.md +++ b/docs/src/ai/agent-panel.md @@ -71,12 +71,7 @@ In long conversations, use the scroll arrow buttons at the bottom of the panel t When focus is in the message editor, you can also use {#kb agent::ScrollOutputPageUp}, {#kb agent::ScrollOutputPageDown}, {#kb agent::ScrollOutputToTop}, {#kb agent::ScrollOutputToBottom}, {#kb agent::ScrollOutputLineUp}, and {#kb agent::ScrollOutputLineDown} to navigate the thread, or {#kb agent::ScrollOutputToPreviousMessage} and {#kb agent::ScrollOutputToNextMessage} to jump between your prompts. -### Navigating History {#navigating-history} - -To quickly navigate through recently updated threads, use the {#kb agent::ToggleNavigationMenu} binding when focused on the panel's editor, or click the menu icon button at the top right of the panel. -Doing that will open a dropdown that shows you your six most recently updated threads. - -To view all historical conversations, reach for the `View All` option from within the same menu or via the {#kb agent::OpenHistory} binding. +### Thread titles {#thread-titles} Thread titles are auto-generated based on the content of the conversation. But you can also edit them manually by clicking the title and typing, or regenerate them by clicking the "Regenerate Thread Title" button in the ellipsis menu in the top right of the panel.