agent: One Thread History (#46785)

Ben Brandt created

This makes sure that all of the work we do for caching/refreshing
session history is reused everywhere we need to access the list.

Very important for external agents so we aren't rerequesting history on
every search or recent list update. Puts the reloading of history in one
place

Release Notes:

- N/A

Change summary

Cargo.lock                                       |   7 
crates/agent_ui/src/acp/entry_view_state.rs      |  21 
crates/agent_ui/src/acp/message_editor.rs        |  68 
crates/agent_ui/src/acp/thread_history.rs        |  22 
crates/agent_ui/src/acp/thread_view.rs           | 181 +--
crates/agent_ui/src/agent_panel.rs               |  24 
crates/agent_ui/src/completion_provider.rs       | 161 --
crates/agent_ui/src/inline_assistant.rs          |  34 
crates/agent_ui/src/inline_prompt_editor.rs      |  13 
crates/agent_ui/src/terminal_inline_assistant.rs |   3 
crates/agent_ui_v2/Cargo.toml                    |   7 
crates/agent_ui_v2/src/agent_thread_pane.rs      |  12 
crates/agent_ui_v2/src/agent_ui_v2.rs            |   1 
crates/agent_ui_v2/src/agents_panel.rs           |   5 
crates/agent_ui_v2/src/thread_history.rs         | 868 ------------------
15 files changed, 238 insertions(+), 1,189 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -435,23 +435,16 @@ dependencies = [
  "agent_settings",
  "agent_ui",
  "anyhow",
- "chrono",
  "db",
- "editor",
  "feature_flags",
  "fs",
- "fuzzy",
  "gpui",
  "log",
- "menu",
  "project",
  "prompt_store",
  "serde",
  "serde_json",
  "settings",
- "text",
- "time",
- "time_format",
  "ui",
  "util",
  "workspace",

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

@@ -1,6 +1,7 @@
 use std::{cell::RefCell, ops::Range, rc::Rc};
 
-use acp_thread::{AcpThread, AgentSessionList, AgentThreadEntry};
+use super::thread_history::AcpThreadHistory;
+use acp_thread::{AcpThread, AgentThreadEntry};
 use agent::ThreadStore;
 use agent_client_protocol::{self as acp, ToolCallId};
 use collections::HashMap;
@@ -24,7 +25,7 @@ pub struct EntryViewState {
     workspace: WeakEntity<Workspace>,
     project: WeakEntity<Project>,
     thread_store: Option<Entity<ThreadStore>>,
-    session_list: Rc<RefCell<Option<Rc<dyn AgentSessionList>>>>,
+    history: WeakEntity<AcpThreadHistory>,
     prompt_store: Option<Entity<PromptStore>>,
     entries: Vec<Entry>,
     prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
@@ -37,7 +38,7 @@ impl EntryViewState {
         workspace: WeakEntity<Workspace>,
         project: WeakEntity<Project>,
         thread_store: Option<Entity<ThreadStore>>,
-        session_list: Rc<RefCell<Option<Rc<dyn AgentSessionList>>>>,
+        history: WeakEntity<AcpThreadHistory>,
         prompt_store: Option<Entity<PromptStore>>,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
@@ -47,7 +48,7 @@ impl EntryViewState {
             workspace,
             project,
             thread_store,
-            session_list,
+            history,
             prompt_store,
             entries: Vec::new(),
             prompt_capabilities,
@@ -89,7 +90,7 @@ impl EntryViewState {
                             self.workspace.clone(),
                             self.project.clone(),
                             self.thread_store.clone(),
-                            self.session_list.clone(),
+                            self.history.clone(),
                             self.prompt_store.clone(),
                             self.prompt_capabilities.clone(),
                             self.available_commands.clone(),
@@ -400,7 +401,8 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
 
 #[cfg(test)]
 mod tests {
-    use std::{cell::RefCell, path::Path, rc::Rc};
+    use std::path::Path;
+    use std::rc::Rc;
 
     use acp_thread::{AgentConnection, StubAgentConnection};
     use agent_client_protocol as acp;
@@ -455,14 +457,15 @@ mod tests {
         });
 
         let thread_store = None;
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
 
         let view_state = cx.new(|_cx| {
             EntryViewState::new(
                 workspace.downgrade(),
                 project.downgrade(),
                 thread_store,
-                session_list,
+                history.downgrade(),
                 None,
                 Default::default(),
                 Default::default(),
@@ -474,7 +477,7 @@ mod tests {
             view_state.sync_entry(0, &thread, window, cx)
         });
 
-        let diff = thread.read_with(cx, |thread, _cx| {
+        let diff = thread.read_with(cx, |thread, _| {
             thread
                 .entries()
                 .get(0)

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

@@ -1,4 +1,5 @@
 use crate::QueueMessage;
+use crate::acp::AcpThreadHistory;
 use crate::{
     ChatWithFollow,
     completion_provider::{
@@ -9,7 +10,7 @@ use crate::{
         Mention, MentionImage, MentionSet, insert_crease_for_mention, paste_images_as_context,
     },
 };
-use acp_thread::{AgentSessionInfo, AgentSessionList, MentionUri};
+use acp_thread::{AgentSessionInfo, MentionUri};
 use agent::ThreadStore;
 use agent_client_protocol as acp;
 use anyhow::{Result, anyhow};
@@ -101,7 +102,7 @@ impl MessageEditor {
         workspace: WeakEntity<Workspace>,
         project: WeakEntity<Project>,
         thread_store: Option<Entity<ThreadStore>>,
-        session_list: Rc<RefCell<Option<Rc<dyn AgentSessionList>>>>,
+        history: WeakEntity<AcpThreadHistory>,
         prompt_store: Option<Entity<PromptStore>>,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
@@ -158,8 +159,7 @@ impl MessageEditor {
             cx.entity(),
             editor.downgrade(),
             mention_set.clone(),
-            thread_store.clone(),
-            session_list,
+            history,
             prompt_store.clone(),
             workspace.clone(),
         ));
@@ -1108,7 +1108,8 @@ mod tests {
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let thread_store = None;
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
@@ -1116,7 +1117,7 @@ mod tests {
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -1214,13 +1215,14 @@ mod tests {
 
         let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
         let thread_store = None;
-        let session_list = Rc::new(RefCell::new(None));
         let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
         // Start with no available commands - simulating Claude which doesn't support slash commands
         let available_commands = Rc::new(RefCell::new(vec![]));
 
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
         let workspace_handle = workspace.downgrade();
         let message_editor = workspace.update_in(cx, |_, window, cx| {
             cx.new(|cx| {
@@ -1228,7 +1230,7 @@ mod tests {
                     workspace_handle.clone(),
                     project.downgrade(),
                     thread_store.clone(),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     prompt_capabilities.clone(),
                     available_commands.clone(),
@@ -1372,7 +1374,8 @@ mod tests {
         let mut cx = VisualTestContext::from_window(*window, cx);
 
         let thread_store = None;
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
         let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
         let available_commands = Rc::new(RefCell::new(vec![
             acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
@@ -1390,7 +1393,7 @@ mod tests {
                     workspace_handle,
                     project.downgrade(),
                     thread_store.clone(),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     prompt_capabilities.clone(),
                     available_commands.clone(),
@@ -1603,7 +1606,8 @@ mod tests {
         }
 
         let thread_store = cx.new(|cx| ThreadStore::new(cx));
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
         let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
 
         let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
@@ -1613,7 +1617,7 @@ mod tests {
                     workspace_handle,
                     project.downgrade(),
                     Some(thread_store),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     prompt_capabilities.clone(),
                     Default::default(),
@@ -2097,7 +2101,8 @@ mod tests {
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
@@ -2105,7 +2110,7 @@ mod tests {
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -2196,7 +2201,8 @@ mod tests {
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
 
         // Create a thread metadata to insert as summary
         let thread_metadata = AgentSessionInfo {
@@ -2213,7 +2219,7 @@ mod tests {
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -2276,7 +2282,8 @@ mod tests {
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let thread_store = None;
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
 
         let thread_metadata = AgentSessionInfo {
             session_id: acp::SessionId::new("thread-123"),
@@ -2292,7 +2299,7 @@ mod tests {
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -2334,7 +2341,8 @@ mod tests {
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let thread_store = None;
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
@@ -2342,7 +2350,7 @@ mod tests {
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -2387,7 +2395,8 @@ mod tests {
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
@@ -2395,7 +2404,7 @@ mod tests {
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -2441,7 +2450,8 @@ mod tests {
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
@@ -2449,7 +2459,7 @@ mod tests {
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -2504,7 +2514,8 @@ mod tests {
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
 
         let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
             let workspace_handle = cx.weak_entity();
@@ -2513,7 +2524,7 @@ mod tests {
                     workspace_handle,
                     project.downgrade(),
                     thread_store.clone(),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -2660,7 +2671,8 @@ mod tests {
         });
 
         let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
-        let session_list = Rc::new(RefCell::new(None));
+        let history = cx
+            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
 
         // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
         // to ensure we have a fixed viewport, so we can eventually actually
@@ -2672,7 +2684,7 @@ mod tests {
                     workspace_handle,
                     project.downgrade(),
                     thread_store.clone(),
-                    session_list.clone(),
+                    history.downgrade(),
                     None,
                     Default::default(),
                     Default::default(),

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

@@ -72,7 +72,7 @@ pub enum ThreadHistoryEvent {
 impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
 
 impl AcpThreadHistory {
-    pub(crate) fn new(
+    pub fn new(
         session_list: Option<Rc<dyn AgentSessionList>>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -155,7 +155,7 @@ impl AcpThreadHistory {
         });
     }
 
-    pub(crate) fn set_session_list(
+    pub fn set_session_list(
         &mut self,
         session_list: Option<Rc<dyn AgentSessionList>>,
         cx: &mut Context<Self>,
@@ -246,7 +246,7 @@ impl AcpThreadHistory {
         self.sessions.is_empty()
     }
 
-    pub(crate) fn session_for_id(&self, session_id: &acp::SessionId) -> Option<AgentSessionInfo> {
+    pub fn session_for_id(&self, session_id: &acp::SessionId) -> Option<AgentSessionInfo> {
         self.sessions
             .iter()
             .find(|entry| &entry.session_id == session_id)
@@ -257,6 +257,22 @@ impl AcpThreadHistory {
         &self.sessions
     }
 
+    pub(crate) fn get_recent_sessions(&self, limit: usize) -> Vec<AgentSessionInfo> {
+        self.sessions.iter().take(limit).cloned().collect()
+    }
+
+    pub(crate) fn delete_session(
+        &self,
+        session_id: &acp::SessionId,
+        cx: &mut App,
+    ) -> Task<anyhow::Result<()>> {
+        if let Some(session_list) = self.session_list.as_ref() {
+            session_list.delete_session(session_id, cx)
+        } else {
+            Task::ready(Ok(()))
+        }
+    }
+
     fn add_list_separators(
         &self,
         entries: Vec<AgentSessionInfo>,

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

@@ -1,7 +1,7 @@
 use acp_thread::{
-    AcpThread, AcpThreadEvent, AgentSessionInfo, AgentSessionList, AgentSessionListRequest,
-    AgentThreadEntry, AssistantMessage, AssistantMessageChunk, AuthRequired, LoadError, MentionUri,
-    RetryStatus, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, UserMessageId,
+    AcpThread, AcpThreadEvent, AgentSessionInfo, AgentThreadEntry, AssistantMessage,
+    AssistantMessageChunk, AuthRequired, LoadError, MentionUri, RetryStatus, ThreadStatus,
+    ToolCall, ToolCallContent, ToolCallStatus, UserMessageId,
 };
 use acp_thread::{AgentConnection, Plan};
 use action_log::{ActionLog, ActionLogTelemetry};
@@ -61,6 +61,7 @@ use zed_actions::assistant::OpenRulesLibrary;
 
 use super::config_options::ConfigOptionsView;
 use super::entry_view_state::EntryViewState;
+use super::thread_history::AcpThreadHistory;
 use crate::acp::AcpModelSelectorPopover;
 use crate::acp::ModeSelector;
 use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent};
@@ -311,11 +312,9 @@ pub struct AcpThreadView {
     project: Entity<Project>,
     thread_state: ThreadState,
     login: Option<task::SpawnInTerminal>,
-    session_list: Option<Rc<dyn AgentSessionList>>,
-    session_list_state: Rc<RefCell<Option<Rc<dyn AgentSessionList>>>>,
     recent_history_entries: Vec<AgentSessionInfo>,
-    _recent_history_task: Task<()>,
-    _recent_history_watch_task: Option<Task<()>>,
+    history: Entity<AcpThreadHistory>,
+    _history_subscription: Subscription,
     hovered_recent_history_item: Option<usize>,
     entry_view_state: Entity<EntryViewState>,
     message_editor: Entity<MessageEditor>,
@@ -400,13 +399,13 @@ impl AcpThreadView {
         project: Entity<Project>,
         thread_store: Option<Entity<ThreadStore>>,
         prompt_store: Option<Entity<PromptStore>>,
+        history: Entity<AcpThreadHistory>,
         track_load_event: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
         let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
         let available_commands = Rc::new(RefCell::new(vec![]));
-        let session_list_state = Rc::new(RefCell::new(None));
 
         let agent_server_store = project.read(cx).agent_server_store().clone();
         let agent_display_name = agent_server_store
@@ -421,7 +420,7 @@ impl AcpThreadView {
                 workspace.clone(),
                 project.downgrade(),
                 thread_store.clone(),
-                session_list_state.clone(),
+                history.downgrade(),
                 prompt_store.clone(),
                 prompt_capabilities.clone(),
                 available_commands.clone(),
@@ -447,7 +446,7 @@ impl AcpThreadView {
                 workspace.clone(),
                 project.downgrade(),
                 thread_store.clone(),
-                session_list_state.clone(),
+                history.downgrade(),
                 prompt_store.clone(),
                 prompt_capabilities.clone(),
                 available_commands.clone(),
@@ -482,6 +481,11 @@ impl AcpThreadView {
             && project.read(cx).is_local()
             && agent.clone().downcast::<agent_servers::Codex>().is_some();
 
+        let recent_history_entries = history.read(cx).get_recent_sessions(3);
+        let history_subscription = cx.observe(&history, |this, history, cx| {
+            this.update_recent_history_from_cache(&history, cx);
+        });
+
         Self {
             agent: agent.clone(),
             agent_server_store,
@@ -523,11 +527,9 @@ impl AcpThreadView {
             available_commands,
             editor_expanded: false,
             should_be_following: false,
-            session_list: None,
-            session_list_state,
-            recent_history_entries: Vec::new(),
-            _recent_history_task: Task::ready(()),
-            _recent_history_watch_task: None,
+            recent_history_entries,
+            history,
+            _history_subscription: history_subscription,
             hovered_recent_history_item: None,
             is_loading_contents: false,
             _subscriptions: subscriptions,
@@ -562,11 +564,7 @@ impl AcpThreadView {
         self.available_commands.replace(vec![]);
         self.new_server_version_available.take();
         self.message_queue.clear();
-        self.session_list = None;
-        *self.session_list_state.borrow_mut() = None;
         self.recent_history_entries.clear();
-        self._recent_history_watch_task = None;
-        self._recent_history_task = Task::ready(());
         self.turn_tokens = None;
         self.last_turn_tokens = None;
         self.turn_started_at = None;
@@ -706,7 +704,9 @@ impl AcpThreadView {
                         let connection = thread.read(cx).connection().clone();
                         let session_id = thread.read(cx).session_id().clone();
                         let session_list = connection.session_list(cx);
-                        this.set_session_list(session_list, cx);
+                        this.history.update(cx, |history, cx| {
+                            history.set_session_list(session_list, cx);
+                        });
 
                         // Check for config options first
                         // Config options take precedence over legacy mode/model selectors
@@ -960,10 +960,6 @@ impl AcpThreadView {
         }
     }
 
-    pub(crate) fn session_list(&self) -> Option<Rc<dyn AgentSessionList>> {
-        self.session_list.clone()
-    }
-
     pub fn mode_selector(&self) -> Option<&Entity<ModeSelector>> {
         match &self.thread_state {
             ThreadState::Ready { mode_selector, .. } => mode_selector.as_ref(),
@@ -1066,12 +1062,13 @@ impl AcpThreadView {
             return;
         }
 
-        let Some(thread) = self.as_native_thread(cx) else {
+        let Some(thread) = self.thread() else {
             return;
         };
+
         let Some(session_list) = self
-            .session_list
-            .clone()
+            .as_native_connection(cx)
+            .and_then(|connection| connection.session_list(cx))
             .and_then(|list| list.downcast::<NativeAgentSessionList>())
         else {
             return;
@@ -1079,7 +1076,7 @@ impl AcpThreadView {
         let thread_store = session_list.thread_store().clone();
 
         let client = self.project.read(cx).client();
-        let session_id = thread.read(cx).id().clone();
+        let session_id = thread.read(cx).session_id().clone();
 
         cx.spawn_in(window, async move |this, cx| {
             let response = client
@@ -4203,59 +4200,18 @@ impl AcpThreadView {
         )
     }
 
-    fn set_session_list(
+    fn update_recent_history_from_cache(
         &mut self,
-        session_list: Option<Rc<dyn AgentSessionList>>,
+        history: &Entity<AcpThreadHistory>,
         cx: &mut Context<Self>,
     ) {
-        if let (Some(current), Some(next)) = (&self.session_list, &session_list)
-            && Rc::ptr_eq(current, next)
-        {
-            return;
-        }
-
-        self.session_list = session_list.clone();
-        *self.session_list_state.borrow_mut() = session_list;
-        self.recent_history_entries.clear();
+        self.recent_history_entries = history.read(cx).get_recent_sessions(3);
         self.hovered_recent_history_item = None;
-        self.refresh_recent_history(cx);
-
-        self._recent_history_watch_task = self.session_list.as_ref().and_then(|session_list| {
-            let mut rx = session_list.watch(cx)?;
-            Some(cx.spawn(async move |this, cx| {
-                while let Ok(()) = rx.recv().await {
-                    this.update(cx, |this, cx| {
-                        this.refresh_recent_history(cx);
-                    })
-                    .ok();
-                }
-            }))
-        });
-    }
-
-    fn refresh_recent_history(&mut self, cx: &mut Context<Self>) {
-        let Some(session_list) = self.session_list.clone() else {
-            return;
-        };
-
-        let task = session_list.list_sessions(AgentSessionListRequest::default(), cx);
-        self._recent_history_task = cx.spawn(async move |this, cx| match task.await {
-            Ok(response) => {
-                this.update(cx, |this, cx| {
-                    this.recent_history_entries = response.sessions.into_iter().take(3).collect();
-                    this.hovered_recent_history_item = None;
-                    cx.notify();
-                })
-                .ok();
-            }
-            Err(error) => {
-                log::error!("Failed to load recent session history: {error:#}");
-            }
-        });
+        cx.notify();
     }
 
     fn render_recent_history(&self, cx: &mut Context<Self>) -> AnyElement {
-        let render_history = self.session_list.is_some() && !self.recent_history_entries.is_empty();
+        let render_history = !self.recent_history_entries.is_empty();
 
         v_flex()
             .size_full()
@@ -7149,10 +7105,9 @@ impl AcpThreadView {
     }
 
     pub fn delete_history_entry(&mut self, entry: AgentSessionInfo, cx: &mut Context<Self>) {
-        let Some(session_list) = self.session_list.as_ref() else {
-            return;
-        };
-        let task = session_list.delete_session(&entry.session_id, cx);
+        let task = self.history.update(cx, |history, cx| {
+            history.delete_session(&entry.session_id, cx)
+        });
         task.detach_and_log_err(cx);
     }
 
@@ -7578,7 +7533,9 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 
 #[cfg(test)]
 pub(crate) mod tests {
-    use acp_thread::{AgentSessionListResponse, StubAgentConnection};
+    use acp_thread::{
+        AgentSessionList, AgentSessionListRequest, AgentSessionListResponse, StubAgentConnection,
+    };
     use action_log::ActionLog;
     use agent_client_protocol::SessionId;
     use editor::MultiBufferOffset;
@@ -7658,21 +7615,52 @@ pub(crate) mod tests {
     }
 
     #[gpui::test]
-    async fn test_recent_history_refreshes_when_session_list_swapped(cx: &mut TestAppContext) {
+    async fn test_recent_history_refreshes_when_history_cache_updated(cx: &mut TestAppContext) {
         init_test(cx);
 
-        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
-
         let session_a = AgentSessionInfo::new(SessionId::new("session-a"));
         let session_b = AgentSessionInfo::new(SessionId::new("session-b"));
 
+        let fs = FakeFs::new(cx.executor());
+        let project = Project::test(fs, [], cx).await;
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
+        // Create history without an initial session list - it will be set after connection
+        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
+
+        let thread_view = cx.update(|window, cx| {
+            cx.new(|cx| {
+                AcpThreadView::new(
+                    Rc::new(StubAgentServer::default_response()),
+                    None,
+                    None,
+                    workspace.downgrade(),
+                    project,
+                    Some(thread_store),
+                    None,
+                    history.clone(),
+                    false,
+                    window,
+                    cx,
+                )
+            })
+        });
+
+        // Wait for connection to establish
+        cx.run_until_parked();
+
+        // Initially empty because StubAgentConnection.session_list() returns None
+        thread_view.read_with(cx, |view, _cx| {
+            assert_eq!(view.recent_history_entries.len(), 0);
+        });
+
+        // Now set the session list - this simulates external agents providing their history
         let list_a: Rc<dyn AgentSessionList> =
             Rc::new(StubSessionList::new(vec![session_a.clone()]));
-        let list_b: Rc<dyn AgentSessionList> =
-            Rc::new(StubSessionList::new(vec![session_b.clone()]));
-
-        thread_view.update(cx, |view, cx| {
-            view.set_session_list(Some(list_a.clone()), cx);
+        history.update(cx, |history, cx| {
+            history.set_session_list(Some(list_a), cx);
         });
         cx.run_until_parked();
 
@@ -7682,14 +7670,13 @@ pub(crate) mod tests {
                 view.recent_history_entries[0].session_id,
                 session_a.session_id
             );
-
-            let session_list = view.session_list_state.borrow();
-            let session_list = session_list.as_ref().expect("session list should be set");
-            assert!(Rc::ptr_eq(session_list, &list_a));
         });
 
-        thread_view.update(cx, |view, cx| {
-            view.set_session_list(Some(list_b.clone()), cx);
+        // Update to a different session list
+        let list_b: Rc<dyn AgentSessionList> =
+            Rc::new(StubSessionList::new(vec![session_b.clone()]));
+        history.update(cx, |history, cx| {
+            history.set_session_list(Some(list_b), cx);
         });
         cx.run_until_parked();
 
@@ -7699,10 +7686,6 @@ pub(crate) mod tests {
                 view.recent_history_entries[0].session_id,
                 session_b.session_id
             );
-
-            let session_list = view.session_list_state.borrow();
-            let session_list = session_list.as_ref().expect("session list should be set");
-            assert!(Rc::ptr_eq(session_list, &list_b));
         });
     }
 
@@ -7937,6 +7920,7 @@ pub(crate) mod tests {
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
+        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
 
         let thread_view = cx.update(|window, cx| {
             cx.new(|cx| {
@@ -7948,6 +7932,7 @@ pub(crate) mod tests {
                     project,
                     Some(thread_store),
                     None,
+                    history,
                     false,
                     window,
                     cx,
@@ -8227,6 +8212,7 @@ pub(crate) mod tests {
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
+        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
 
         let connection = Rc::new(StubAgentConnection::new());
         let thread_view = cx.update(|window, cx| {
@@ -8239,6 +8225,7 @@ pub(crate) mod tests {
                     project.clone(),
                     Some(thread_store.clone()),
                     None,
+                    history,
                     false,
                     window,
                     cx,

crates/agent_ui/src/agent_panel.rs 🔗

@@ -305,6 +305,7 @@ impl ActiveView {
         thread_store: Entity<ThreadStore>,
         project: Entity<Project>,
         workspace: WeakEntity<Workspace>,
+        history: Entity<AcpThreadHistory>,
         window: &mut Window,
         cx: &mut App,
     ) -> Self {
@@ -317,6 +318,7 @@ impl ActiveView {
                 project,
                 Some(thread_store),
                 prompt_store,
+                history,
                 false,
                 window,
                 cx,
@@ -429,7 +431,6 @@ pub struct AgentPanel {
     context_server_registry: Entity<ContextServerRegistry>,
     configuration: Option<Entity<AgentConfiguration>>,
     configuration_subscription: Option<Subscription>,
-    history_subscription: Option<Subscription>,
     active_view: ActiveView,
     previous_view: Option<ActiveView>,
     new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -583,6 +584,7 @@ impl AgentPanel {
                 thread_store.clone(),
                 project.clone(),
                 workspace.clone(),
+                acp_history.clone(),
                 window,
                 cx,
             ),
@@ -684,7 +686,6 @@ impl AgentPanel {
             prompt_store,
             configuration: None,
             configuration_subscription: None,
-            history_subscription: None,
             context_server_registry,
             previous_view: None,
             new_thread_menu_handle: PopoverMenuHandle::default(),
@@ -732,6 +733,10 @@ impl AgentPanel {
         &self.thread_store
     }
 
+    pub fn history(&self) -> &Entity<AcpThreadHistory> {
+        &self.acp_history
+    }
+
     pub fn open_thread(
         &mut self,
         thread: AgentSessionInfo,
@@ -1558,21 +1563,13 @@ impl AgentPanel {
                 project,
                 thread_store,
                 self.prompt_store.clone(),
+                self.acp_history.clone(),
                 !loading,
                 window,
                 cx,
             )
         });
 
-        let acp_history = self.acp_history.clone();
-        self.history_subscription = Some(cx.observe(&thread_view, move |_, thread_view, cx| {
-            if let Some(session_list) = thread_view.read(cx).session_list() {
-                acp_history.update(cx, |history, cx| {
-                    history.set_session_list(Some(session_list), cx);
-                });
-            }
-        }));
-
         self.set_active_view(
             ActiveView::ExternalAgentThread { thread_view },
             !loading,
@@ -2947,13 +2944,16 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
                 return;
             };
             let project = workspace.read(cx).project().downgrade();
-            let thread_store = panel.read(cx).thread_store().clone();
+            let panel = panel.read(cx);
+            let thread_store = panel.thread_store().clone();
+            let history = panel.history().downgrade();
             assistant.assist(
                 prompt_editor,
                 self.workspace.clone(),
                 project,
                 thread_store,
                 None,
+                history,
                 initial_prompt,
                 window,
                 cx,

crates/agent_ui/src/completion_provider.rs 🔗

@@ -1,13 +1,11 @@
-use std::cell::RefCell;
 use std::cmp::Reverse;
 use std::ops::Range;
 use std::path::PathBuf;
-use std::rc::Rc;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
-use acp_thread::{AgentSessionInfo, AgentSessionList, AgentSessionListRequest, MentionUri};
-use agent::ThreadStore;
+use crate::acp::AcpThreadHistory;
+use acp_thread::{AgentSessionInfo, MentionUri};
 use anyhow::Result;
 use editor::{
     CompletionProvider, Editor, ExcerptId, code_context_menus::COMPLETION_MENU_MAX_WIDTH,
@@ -196,8 +194,7 @@ pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> {
     source: Arc<T>,
     editor: WeakEntity<Editor>,
     mention_set: Entity<MentionSet>,
-    thread_store: Option<Entity<ThreadStore>>,
-    session_list: Rc<RefCell<Option<Rc<dyn AgentSessionList>>>>,
+    history: WeakEntity<AcpThreadHistory>,
     prompt_store: Option<Entity<PromptStore>>,
     workspace: WeakEntity<Workspace>,
 }
@@ -207,8 +204,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
         source: T,
         editor: WeakEntity<Editor>,
         mention_set: Entity<MentionSet>,
-        thread_store: Option<Entity<ThreadStore>>,
-        session_list: Rc<RefCell<Option<Rc<dyn AgentSessionList>>>>,
+        history: WeakEntity<AcpThreadHistory>,
         prompt_store: Option<Entity<PromptStore>>,
         workspace: WeakEntity<Workspace>,
     ) -> Self {
@@ -217,8 +213,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
             editor,
             mention_set,
             workspace,
-            thread_store,
-            session_list,
+            history,
             prompt_store,
         }
     }
@@ -653,25 +648,12 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
             }
 
             Some(PromptContextType::Thread) => {
-                if let Some(session_list) = self.session_list.borrow().clone() {
-                    let search_sessions_task =
-                        search_sessions(query, cancellation_flag, session_list, cx);
+                if let Some(history) = self.history.upgrade() {
+                    let sessions = history.read(cx).sessions().to_vec();
+                    let search_task =
+                        filter_sessions_by_query(query, cancellation_flag, sessions, cx);
                     cx.spawn(async move |_cx| {
-                        search_sessions_task
-                            .await
-                            .into_iter()
-                            .map(Match::Thread)
-                            .collect()
-                    })
-                } else if let Some(thread_store) = self.thread_store.as_ref() {
-                    let search_threads_task =
-                        search_threads(query, cancellation_flag, thread_store, cx);
-                    cx.background_spawn(async move {
-                        search_threads_task
-                            .await
-                            .into_iter()
-                            .map(Match::Thread)
-                            .collect()
+                        search_task.await.into_iter().map(Match::Thread).collect()
                     })
                 } else {
                     Task::ready(Vec::new())
@@ -835,19 +817,12 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
             return Task::ready(recent);
         }
 
-        if let Some(session_list) = self.session_list.borrow().clone() {
-            let task = session_list.list_sessions(AgentSessionListRequest::default(), cx);
-            return cx.spawn(async move |_cx| {
-                let sessions = match task.await {
-                    Ok(response) => response.sessions,
-                    Err(error) => {
-                        log::error!("Failed to load recent sessions: {error:#}");
-                        return recent;
-                    }
-                };
-
-                const RECENT_COUNT: usize = 2;
-                let threads = sessions
+        if let Some(history) = self.history.upgrade() {
+            const RECENT_COUNT: usize = 2;
+            recent.extend(
+                history
+                    .read(cx)
+                    .sessions()
                     .into_iter()
                     .filter(|session| {
                         let uri = MentionUri::Thread {
@@ -857,33 +832,11 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
                         !mentions.contains(&uri)
                     })
                     .take(RECENT_COUNT)
-                    .collect::<Vec<_>>();
-
-                recent.extend(threads.into_iter().map(Match::RecentThread));
-                recent
-            });
-        }
-
-        let Some(thread_store) = self.thread_store.as_ref() else {
+                    .cloned()
+                    .map(Match::RecentThread),
+            );
             return Task::ready(recent);
-        };
-
-        const RECENT_COUNT: usize = 2;
-        let threads = thread_store
-            .read(cx)
-            .entries()
-            .map(thread_metadata_to_session_info)
-            .filter(|thread| {
-                let uri = MentionUri::Thread {
-                    id: thread.session_id.clone(),
-                    name: session_title(thread).to_string(),
-                };
-                !mentions.contains(&uri)
-            })
-            .take(RECENT_COUNT)
-            .collect::<Vec<_>>();
-
-        recent.extend(threads.into_iter().map(Match::RecentThread));
+        }
 
         Task::ready(recent)
     }
@@ -1608,50 +1561,21 @@ pub(crate) fn search_symbols(
     })
 }
 
-pub(crate) fn search_threads(
+fn filter_sessions_by_query(
     query: String,
     cancellation_flag: Arc<AtomicBool>,
-    thread_store: &Entity<ThreadStore>,
+    sessions: Vec<AgentSessionInfo>,
     cx: &mut App,
 ) -> Task<Vec<AgentSessionInfo>> {
-    let sessions = thread_store
-        .read(cx)
-        .entries()
-        .map(thread_metadata_to_session_info)
-        .collect::<Vec<_>>();
     if query.is_empty() {
         return Task::ready(sessions);
     }
-
     let executor = cx.background_executor().clone();
     cx.background_spawn(async move {
         filter_sessions(query, cancellation_flag, sessions, executor).await
     })
 }
 
-pub(crate) fn search_sessions(
-    query: String,
-    cancellation_flag: Arc<AtomicBool>,
-    session_list: Rc<dyn AgentSessionList>,
-    cx: &mut App,
-) -> Task<Vec<AgentSessionInfo>> {
-    let task = session_list.list_sessions(AgentSessionListRequest::default(), cx);
-    let executor = cx.background_executor().clone();
-    cx.spawn(async move |_cx| {
-        let sessions = match task.await {
-            Ok(response) => response.sessions,
-            Err(error) => {
-                log::error!("Failed to list sessions: {error:#}");
-                return Vec::new();
-            }
-        };
-        if query.is_empty() {
-            return sessions;
-        }
-        filter_sessions(query, cancellation_flag, sessions, executor).await
-    })
-}
-
 async fn filter_sessions(
     query: String,
     cancellation_flag: Arc<AtomicBool>,
@@ -1681,16 +1605,6 @@ async fn filter_sessions(
         .collect()
 }
 
-fn thread_metadata_to_session_info(entry: agent::DbThreadMetadata) -> AgentSessionInfo {
-    AgentSessionInfo {
-        session_id: entry.id,
-        cwd: None,
-        title: Some(entry.title),
-        updated_at: Some(entry.updated_at),
-        meta: None,
-    }
-}
-
 pub(crate) fn search_rules(
     query: String,
     cancellation_flag: Arc<AtomicBool>,
@@ -1815,9 +1729,7 @@ fn selection_ranges(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use acp_thread::AgentSessionListResponse;
     use gpui::TestAppContext;
-    use std::{any::Any, rc::Rc};
 
     #[test]
     fn test_prompt_completion_parse() {
@@ -2059,41 +1971,20 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_search_sessions_filters_results(cx: &mut TestAppContext) {
-        #[derive(Clone)]
-        struct StubSessionList {
-            sessions: Vec<AgentSessionInfo>,
-        }
-
-        impl AgentSessionList for StubSessionList {
-            fn list_sessions(
-                &self,
-                _request: AgentSessionListRequest,
-                _cx: &mut App,
-            ) -> Task<anyhow::Result<AgentSessionListResponse>> {
-                Task::ready(Ok(AgentSessionListResponse::new(self.sessions.clone())))
-            }
-
-            fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
-                self
-            }
-        }
-
+    async fn test_filter_sessions_by_query(cx: &mut TestAppContext) {
         let mut alpha = AgentSessionInfo::new("session-alpha");
         alpha.title = Some("Alpha Session".into());
         let mut beta = AgentSessionInfo::new("session-beta");
         beta.title = Some("Beta Session".into());
 
-        let session_list: Rc<dyn AgentSessionList> = Rc::new(StubSessionList {
-            sessions: vec![alpha.clone(), beta],
-        });
+        let sessions = vec![alpha.clone(), beta];
 
         let task = {
             let mut app = cx.app.borrow_mut();
-            search_sessions(
+            filter_sessions_by_query(
                 "Alpha".into(),
                 Arc::new(AtomicBool::default()),
-                session_list,
+                sessions,
                 &mut app,
             )
         };

crates/agent_ui/src/inline_assistant.rs 🔗

@@ -7,6 +7,7 @@ use std::rc::Rc;
 use std::sync::Arc;
 use uuid::Uuid;
 
+use crate::acp::AcpThreadHistory;
 use crate::context::load_context;
 use crate::mention_set::MentionSet;
 use crate::{
@@ -264,6 +265,7 @@ impl InlineAssistant {
 
         let prompt_store = agent_panel.prompt_store().as_ref().cloned();
         let thread_store = agent_panel.thread_store().clone();
+        let history = agent_panel.history().downgrade();
 
         let handle_assist =
             |window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -275,6 +277,7 @@ impl InlineAssistant {
                             workspace.project().downgrade(),
                             thread_store,
                             prompt_store,
+                            history,
                             action.prompt.clone(),
                             window,
                             cx,
@@ -289,6 +292,7 @@ impl InlineAssistant {
                             workspace.project().downgrade(),
                             thread_store,
                             prompt_store,
+                            history,
                             action.prompt.clone(),
                             window,
                             cx,
@@ -470,6 +474,7 @@ impl InlineAssistant {
         project: WeakEntity<Project>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
+        history: WeakEntity<AcpThreadHistory>,
         initial_prompt: Option<String>,
         window: &mut Window,
         codegen_ranges: &[Range<Anchor>],
@@ -516,6 +521,7 @@ impl InlineAssistant {
                     self.fs.clone(),
                     thread_store.clone(),
                     prompt_store.clone(),
+                    history.clone(),
                     project.clone(),
                     workspace.clone(),
                     window,
@@ -607,6 +613,7 @@ impl InlineAssistant {
         project: WeakEntity<Project>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
+        history: WeakEntity<AcpThreadHistory>,
         initial_prompt: Option<String>,
         window: &mut Window,
         cx: &mut App,
@@ -625,6 +632,7 @@ impl InlineAssistant {
             project,
             thread_store,
             prompt_store,
+            history,
             initial_prompt,
             window,
             &codegen_ranges,
@@ -650,6 +658,7 @@ impl InlineAssistant {
         workspace: Entity<Workspace>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
+        history: WeakEntity<AcpThreadHistory>,
         window: &mut Window,
         cx: &mut App,
     ) -> InlineAssistId {
@@ -669,6 +678,7 @@ impl InlineAssistant {
                 project,
                 thread_store,
                 prompt_store,
+                history,
                 Some(initial_prompt),
                 window,
                 &[range],
@@ -1937,16 +1947,13 @@ impl CodeActionProvider for AssistantCodeActionProvider {
         let prompt_store = PromptStore::global(cx);
         window.spawn(cx, async move |cx| {
             let workspace = workspace.upgrade().context("workspace was released")?;
-            let thread_store = cx.update(|_window, cx| {
-                anyhow::Ok(
-                    workspace
-                        .read(cx)
-                        .panel::<AgentPanel>(cx)
-                        .context("missing agent panel")?
-                        .read(cx)
-                        .thread_store()
-                        .clone(),
-                )
+            let (thread_store, history) = cx.update(|_window, cx| {
+                let panel = workspace
+                    .read(cx)
+                    .panel::<AgentPanel>(cx)
+                    .context("missing agent panel")?
+                    .read(cx);
+                anyhow::Ok((panel.thread_store().clone(), panel.history().downgrade()))
             })??;
             let editor = editor.upgrade().context("editor was released")?;
             let range = editor
@@ -1992,6 +1999,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
                     workspace,
                     thread_store,
                     prompt_store,
+                    history,
                     window,
                     cx,
                 );
@@ -2114,7 +2122,7 @@ pub mod test {
 
         setup(cx);
 
-        let (_editor, buffer) = cx.update(|window, cx| {
+        let (_editor, buffer, _history) = cx.update(|window, cx| {
             let buffer = cx.new(|cx| Buffer::local("", cx));
             let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
             let editor = cx.new(|cx| Editor::for_multibuffer(multibuffer, None, window, cx));
@@ -2131,6 +2139,7 @@ pub mod test {
             });
 
             let thread_store = cx.new(|cx| ThreadStore::new(cx));
+            let history = cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx));
 
             // Add editor to workspace
             workspace.update(cx, |workspace, cx| {
@@ -2146,6 +2155,7 @@ pub mod test {
                         project.downgrade(),
                         thread_store,
                         None,
+                        history.downgrade(),
                         Some(prompt),
                         window,
                         cx,
@@ -2155,7 +2165,7 @@ pub mod test {
                 inline_assistant.start_assist(assist_id, window, cx);
             });
 
-            (editor, buffer)
+            (editor, buffer, history)
         });
 
         cx.run_until_parked();

crates/agent_ui/src/inline_prompt_editor.rs 🔗

@@ -1,3 +1,4 @@
+use crate::acp::AcpThreadHistory;
 use agent::ThreadStore;
 use collections::{HashMap, VecDeque};
 use editor::actions::Paste;
@@ -19,7 +20,6 @@ use parking_lot::Mutex;
 use project::Project;
 use prompt_store::PromptStore;
 use settings::Settings;
-use std::cell::RefCell;
 use std::cmp;
 use std::ops::Range;
 use std::rc::Rc;
@@ -61,7 +61,7 @@ pub struct PromptEditor<T> {
     pub editor: Entity<Editor>,
     mode: PromptEditorMode,
     mention_set: Entity<MentionSet>,
-    thread_store: Entity<ThreadStore>,
+    history: WeakEntity<AcpThreadHistory>,
     prompt_store: Option<Entity<PromptStore>>,
     workspace: WeakEntity<Workspace>,
     model_selector: Entity<AgentModelSelector>,
@@ -332,8 +332,7 @@ impl<T: 'static> PromptEditor<T> {
                 PromptEditorCompletionProviderDelegate,
                 cx.weak_entity(),
                 self.mention_set.clone(),
-                Some(self.thread_store.clone()),
-                Rc::new(RefCell::new(None)),
+                self.history.clone(),
                 self.prompt_store.clone(),
                 self.workspace.clone(),
             ))));
@@ -1213,6 +1212,7 @@ impl PromptEditor<BufferCodegen> {
         fs: Arc<dyn Fs>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
+        history: WeakEntity<AcpThreadHistory>,
         project: WeakEntity<Project>,
         workspace: WeakEntity<Workspace>,
         window: &mut Window,
@@ -1259,7 +1259,7 @@ impl PromptEditor<BufferCodegen> {
         let mut this: PromptEditor<BufferCodegen> = PromptEditor {
             editor: prompt_editor.clone(),
             mention_set,
-            thread_store,
+            history,
             prompt_store,
             workspace,
             model_selector: cx.new(|cx| {
@@ -1371,6 +1371,7 @@ impl PromptEditor<TerminalCodegen> {
         fs: Arc<dyn Fs>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
+        history: WeakEntity<AcpThreadHistory>,
         project: WeakEntity<Project>,
         workspace: WeakEntity<Workspace>,
         window: &mut Window,
@@ -1412,7 +1413,7 @@ impl PromptEditor<TerminalCodegen> {
         let mut this = Self {
             editor: prompt_editor.clone(),
             mention_set,
-            thread_store,
+            history,
             prompt_store,
             workspace,
             model_selector: cx.new(|cx| {

crates/agent_ui/src/terminal_inline_assistant.rs 🔗

@@ -1,4 +1,5 @@
 use crate::{
+    acp::AcpThreadHistory,
     context::load_context,
     inline_prompt_editor::{
         CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
@@ -63,6 +64,7 @@ impl TerminalInlineAssistant {
         project: WeakEntity<Project>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
+        history: WeakEntity<AcpThreadHistory>,
         initial_prompt: Option<String>,
         window: &mut Window,
         cx: &mut App,
@@ -88,6 +90,7 @@ impl TerminalInlineAssistant {
                 self.fs.clone(),
                 thread_store.clone(),
                 prompt_store.clone(),
+                history,
                 project.clone(),
                 workspace.clone(),
                 window,

crates/agent_ui_v2/Cargo.toml 🔗

@@ -24,23 +24,16 @@ agent_servers.workspace = true
 agent_settings.workspace = true
 agent_ui.workspace = true
 anyhow.workspace = true
-chrono.workspace = true
 db.workspace = true
-editor.workspace = true
 feature_flags.workspace = true
 fs.workspace = true
-fuzzy.workspace = true
 gpui.workspace = true
 log.workspace = true
-menu.workspace = true
 project.workspace = true
 prompt_store.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
-text.workspace = true
-time.workspace = true
-time_format.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true

crates/agent_ui_v2/src/agent_thread_pane.rs 🔗

@@ -3,7 +3,7 @@ use agent::{NativeAgentServer, ThreadStore};
 use agent_client_protocol as acp;
 use agent_servers::AgentServer;
 use agent_settings::AgentSettings;
-use agent_ui::acp::AcpThreadView;
+use agent_ui::acp::{AcpThreadHistory, AcpThreadView};
 use fs::Fs;
 use gpui::{
     Entity, EventEmitter, Focusable, Pixels, SharedString, Subscription, WeakEntity, prelude::*,
@@ -62,10 +62,15 @@ pub struct AgentThreadPane {
     width: Option<Pixels>,
     thread_view: Option<ActiveThreadView>,
     workspace: WeakEntity<Workspace>,
+    history: Entity<AcpThreadHistory>,
 }
 
 impl AgentThreadPane {
-    pub fn new(workspace: WeakEntity<Workspace>, cx: &mut ui::Context<Self>) -> Self {
+    pub fn new(
+        workspace: WeakEntity<Workspace>,
+        history: Entity<AcpThreadHistory>,
+        cx: &mut ui::Context<Self>,
+    ) -> Self {
         let focus_handle = cx.focus_handle();
         Self {
             focus_handle,
@@ -73,6 +78,7 @@ impl AgentThreadPane {
             width: None,
             thread_view: None,
             workspace,
+            history,
         }
     }
 
@@ -104,6 +110,7 @@ impl AgentThreadPane {
 
         let agent: Rc<dyn AgentServer> = Rc::new(NativeAgentServer::new(fs, thread_store.clone()));
 
+        let history = self.history.clone();
         let thread_view = cx.new(|cx| {
             AcpThreadView::new(
                 agent,
@@ -113,6 +120,7 @@ impl AgentThreadPane {
                 project,
                 Some(thread_store),
                 prompt_store,
+                history,
                 true,
                 window,
                 cx,

crates/agent_ui_v2/src/agents_panel.rs 🔗

@@ -27,7 +27,7 @@ use workspace::{
 use crate::agent_thread_pane::{
     AgentThreadPane, AgentsUtilityPaneEvent, SerializedAgentThreadPane, SerializedHistoryEntryId,
 };
-use crate::thread_history::{AcpThreadHistory, ThreadHistoryEvent};
+use agent_ui::acp::{AcpThreadHistory, ThreadHistoryEvent};
 
 const AGENTS_PANEL_KEY: &str = "agents_panel";
 
@@ -310,9 +310,10 @@ impl AgentsPanel {
         let project = self.project.clone();
         let thread_store = self.thread_store.clone();
         let prompt_store = self.prompt_store.clone();
+        let history = self.history.clone();
 
         let agent_thread_pane = cx.new(|cx| {
-            let mut pane = AgentThreadPane::new(workspace.clone(), cx);
+            let mut pane = AgentThreadPane::new(workspace.clone(), history, cx);
             pane.open_thread(
                 entry,
                 fs,

crates/agent_ui_v2/src/thread_history.rs 🔗

@@ -1,868 +0,0 @@
-use acp_thread::{AgentSessionInfo, AgentSessionList, AgentSessionListRequest};
-use agent_client_protocol as acp;
-use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
-use editor::{Editor, EditorEvent};
-use fuzzy::StringMatchCandidate;
-use gpui::{
-    App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
-    UniformListScrollHandle, Window, actions, uniform_list,
-};
-use std::{fmt::Display, ops::Range, rc::Rc};
-use text::Bias;
-use time::{OffsetDateTime, UtcOffset};
-use ui::{
-    HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar,
-    prelude::*,
-};
-
-const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread");
-
-fn thread_title(entry: &AgentSessionInfo) -> &SharedString {
-    entry
-        .title
-        .as_ref()
-        .filter(|title| !title.is_empty())
-        .unwrap_or(DEFAULT_TITLE)
-}
-
-actions!(
-    agents,
-    [
-        /// Removes all thread history.
-        RemoveHistory,
-        /// Removes the currently selected thread.
-        RemoveSelectedThread,
-    ]
-);
-
-pub struct AcpThreadHistory {
-    session_list: Option<Rc<dyn AgentSessionList>>,
-    sessions: Vec<AgentSessionInfo>,
-    scroll_handle: UniformListScrollHandle,
-    selected_index: usize,
-    hovered_index: Option<usize>,
-    search_editor: Entity<Editor>,
-    search_query: SharedString,
-    visible_items: Vec<ListItemType>,
-    local_timezone: UtcOffset,
-    confirming_delete_history: bool,
-    _update_task: Task<()>,
-    _watch_task: Option<Task<()>>,
-    _subscriptions: Vec<gpui::Subscription>,
-}
-
-enum ListItemType {
-    BucketSeparator(TimeBucket),
-    Entry {
-        entry: AgentSessionInfo,
-        format: EntryTimeFormat,
-    },
-    SearchResult {
-        entry: AgentSessionInfo,
-        positions: Vec<usize>,
-    },
-}
-
-impl ListItemType {
-    fn history_entry(&self) -> Option<&AgentSessionInfo> {
-        match self {
-            ListItemType::Entry { entry, .. } => Some(entry),
-            ListItemType::SearchResult { entry, .. } => Some(entry),
-            _ => None,
-        }
-    }
-}
-
-#[allow(dead_code)]
-pub enum ThreadHistoryEvent {
-    Open(AgentSessionInfo),
-}
-
-impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
-
-impl AcpThreadHistory {
-    pub fn new(
-        session_list: Option<Rc<dyn AgentSessionList>>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let search_editor = cx.new(|cx| {
-            let mut editor = Editor::single_line(window, cx);
-            editor.set_placeholder_text("Search threads...", window, cx);
-            editor
-        });
-
-        let search_editor_subscription =
-            cx.subscribe(&search_editor, |this, search_editor, event, cx| {
-                if let EditorEvent::BufferEdited = event {
-                    let query = search_editor.read(cx).text(cx);
-                    if this.search_query != query {
-                        this.search_query = query.into();
-                        this.update_visible_items(false, cx);
-                    }
-                }
-            });
-
-        let scroll_handle = UniformListScrollHandle::default();
-
-        let mut this = Self {
-            session_list: None,
-            sessions: Vec::new(),
-            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],
-            _update_task: Task::ready(()),
-            _watch_task: None,
-        };
-        this.set_session_list(session_list, cx);
-        this
-    }
-
-    fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
-        let entries = self.sessions.clone();
-        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._update_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();
-        });
-    }
-
-    pub(crate) fn set_session_list(
-        &mut self,
-        session_list: Option<Rc<dyn AgentSessionList>>,
-        cx: &mut Context<Self>,
-    ) {
-        if let (Some(current), Some(next)) = (&self.session_list, &session_list)
-            && Rc::ptr_eq(current, next)
-        {
-            return;
-        }
-
-        self.session_list = session_list;
-        self.sessions.clear();
-        self.visible_items.clear();
-        self.selected_index = 0;
-        self.refresh_sessions(false, cx);
-
-        self._watch_task = self.session_list.as_ref().and_then(|session_list| {
-            let mut rx = session_list.watch(cx)?;
-            Some(cx.spawn(async move |this, cx| {
-                while let Ok(()) = rx.recv().await {
-                    this.update(cx, |this, cx| {
-                        this.refresh_sessions(true, cx);
-                    })
-                    .ok();
-                }
-            }))
-        });
-    }
-
-    fn refresh_sessions(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
-        let Some(session_list) = self.session_list.clone() else {
-            self.update_visible_items(preserve_selected_item, cx);
-            return;
-        };
-
-        self._update_task = cx.spawn(async move |this, cx| {
-            let mut cursor: Option<String> = 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);
-                    }
-                    this.update_visible_items(preserve_selected_item, cx);
-                })
-                .ok();
-
-                is_first_page = false;
-                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(crate) fn session_for_id(&self, session_id: &acp::SessionId) -> Option<AgentSessionInfo> {
-        self.sessions
-            .iter()
-            .find(|entry| &entry.session_id == session_id)
-            .cloned()
-    }
-
-    #[allow(dead_code)]
-    pub(crate) fn sessions(&self) -> &[AgentSessionInfo] {
-        &self.sessions
-    }
-
-    fn add_list_separators(
-        &self,
-        entries: Vec<AgentSessionInfo>,
-        cx: &App,
-    ) -> Task<Vec<ListItemType>> {
-        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<AgentSessionInfo>,
-        cx: &App,
-    ) -> Task<Vec<ListItemType>> {
-        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<Self>) {
-        if self.visible_items.is_empty() {
-            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()
-    }
-
-    pub fn select_previous(
-        &mut self,
-        _: &menu::SelectPrevious,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        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);
-        }
-    }
-
-    pub fn select_next(
-        &mut self,
-        _: &menu::SelectNext,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        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>,
-    ) {
-        self.set_selected_index(0, Bias::Right, cx);
-    }
-
-    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
-        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>) {
-        self.confirm_entry(self.selected_index, cx);
-    }
-
-    fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
-        let Some(entry) = self.get_history_entry(ix) else {
-            return;
-        };
-        cx.emit(ThreadHistoryEvent::Open(entry.clone()));
-    }
-
-    fn remove_selected_thread(
-        &mut self,
-        _: &RemoveSelectedThread,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.remove_thread(self.selected_index, cx)
-    }
-
-    fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
-        let Some(entry) = self.get_history_entry(visible_item_ix) else {
-            return;
-        };
-        let Some(session_list) = self.session_list.as_ref() else {
-            return;
-        };
-        let task = session_list.delete_session(&entry.session_id, cx);
-        task.detach_and_log_err(cx);
-    }
-
-    fn remove_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
-        if let Some(session_list) = self.session_list.as_ref() {
-            session_list.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>) {
-        self.confirming_delete_history = true;
-        cx.notify();
-    }
-
-    fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
-        self.confirming_delete_history = false;
-        cx.notify();
-    }
-
-    fn render_list_items(
-        &mut self,
-        range: Range<usize>,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Vec<AnyElement> {
-        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<Self>) -> 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<usize>,
-        cx: &Context<Self>,
-    ) -> AnyElement {
-        let selected = ix == self.selected_index;
-        let hovered = Some(ix) == self.hovered_index;
-        let display_text = match (format, entry.updated_at) {
-            (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).clone();
-        let full_date = entry
-            .updated_at
-            .map(|time| {
-                EntryTimeFormat::DateAndTime.format_timestamp(time.timestamp(), self.local_timezone)
-            })
-            .unwrap_or_else(|| "Unknown".to_string());
-
-        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::<IconButton>(if hovered {
-                        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 AcpThreadHistory {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.search_editor.focus_handle(cx)
-    }
-}
-
-impl Render for AcpThreadHistory {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let has_no_history = self.is_empty();
-
-        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<usize>, 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, |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<TimeBucket> 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(2023, 1, 15).unwrap();
-
-        let date = today;
-        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today);
-
-        let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap();
-        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday);
-
-        let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap();
-        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
-
-        let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap();
-        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
-
-        let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap();
-        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
-
-        let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap();
-        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
-
-        // All: not in this week or last week
-        let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
-        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All);
-
-        // Test year boundary cases
-        let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
-
-        let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap();
-        assert_eq!(
-            TimeBucket::from_dates(new_year, date),
-            TimeBucket::Yesterday
-        );
-
-        let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap();
-        assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek);
-    }
-}