agent: Move thread history access behind AgentConnection trait (#46631)

Ben Brandt and Zed Zippy created

This should move almost all access of the threadstore behind the trait,
which unlocks external agents supplying the list

Release Notes:

- N/A

---------

Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>

Change summary

Cargo.lock                                  |   3 
crates/acp_thread/Cargo.toml                |   1 
crates/acp_thread/src/connection.rs         |  87 ++++++
crates/agent/src/agent.rs                   |  78 +++++
crates/agent_ui/src/acp/entry_view_state.rs |  23 +
crates/agent_ui/src/acp/message_editor.rs   | 276 +++++++++++++++++--
crates/agent_ui/src/acp/thread_history.rs   | 246 +++++++++++++-----
crates/agent_ui/src/acp/thread_view.rs      | 273 +++++++++++++++----
crates/agent_ui/src/agent_panel.rs          |  63 +++-
crates/agent_ui/src/completion_provider.rs  | 308 +++++++++++++++++-----
crates/agent_ui/src/inline_prompt_editor.rs |  12 
crates/agent_ui/src/mention_set.rs          |  66 ++++
crates/agent_ui_v2/Cargo.toml               |   2 
crates/agent_ui_v2/src/agent_thread_pane.rs |   9 
crates/agent_ui_v2/src/agents_panel.rs      |  95 ++++++
crates/agent_ui_v2/src/thread_history.rs    | 207 +++++++++++---
crates/collab/Cargo.toml                    |   2 
crates/zed/Cargo.toml                       |   3 
crates/zed/src/main.rs                      |  10 
19 files changed, 1,405 insertions(+), 359 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -12,6 +12,7 @@ dependencies = [
  "anyhow",
  "base64 0.22.1",
  "buffer_diff",
+ "chrono",
  "collections",
  "editor",
  "env_logger 0.11.8",
@@ -428,6 +429,7 @@ dependencies = [
 name = "agent_ui_v2"
 version = "0.1.0"
 dependencies = [
+ "acp_thread",
  "agent",
  "agent-client-protocol",
  "agent_servers",
@@ -441,6 +443,7 @@ dependencies = [
  "fs",
  "fuzzy",
  "gpui",
+ "log",
  "menu",
  "project",
  "prompt_store",

crates/acp_thread/Cargo.toml 🔗

@@ -22,6 +22,7 @@ base64.workspace = true
 agent_settings.workspace = true
 anyhow.workspace = true
 buffer_diff.workspace = true
+chrono.workspace = true
 collections.workspace = true
 editor.workspace = true
 file_icons.workspace = true

crates/acp_thread/src/connection.rs 🔗

@@ -1,12 +1,20 @@
 use crate::AcpThread;
 use agent_client_protocol::{self as acp};
 use anyhow::Result;
+use chrono::{DateTime, Utc};
 use collections::IndexMap;
 use gpui::{Entity, SharedString, Task};
 use language_model::LanguageModelProviderId;
 use project::Project;
 use serde::{Deserialize, Serialize};
-use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc};
+use std::{
+    any::Any,
+    error::Error,
+    fmt,
+    path::{Path, PathBuf},
+    rc::Rc,
+    sync::Arc,
+};
 use ui::{App, IconName};
 use uuid::Uuid;
 
@@ -94,6 +102,10 @@ pub trait AgentConnection {
         None
     }
 
+    fn session_list(&self, _cx: &mut App) -> Option<Rc<dyn AgentSessionList>> {
+        None
+    }
+
     fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
 }
 
@@ -153,6 +165,79 @@ pub trait AgentSessionConfigOptions {
     }
 }
 
+#[derive(Debug, Clone, Default)]
+pub struct AgentSessionListRequest {
+    pub cwd: Option<PathBuf>,
+    pub cursor: Option<String>,
+    pub meta: Option<acp::Meta>,
+}
+
+#[derive(Debug, Clone)]
+pub struct AgentSessionListResponse {
+    pub sessions: Vec<AgentSessionInfo>,
+    pub next_cursor: Option<String>,
+    pub meta: Option<acp::Meta>,
+}
+
+impl AgentSessionListResponse {
+    pub fn new(sessions: Vec<AgentSessionInfo>) -> Self {
+        Self {
+            sessions,
+            next_cursor: None,
+            meta: None,
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct AgentSessionInfo {
+    pub session_id: acp::SessionId,
+    pub cwd: Option<PathBuf>,
+    pub title: Option<SharedString>,
+    pub updated_at: Option<DateTime<Utc>>,
+    pub meta: Option<acp::Meta>,
+}
+
+impl AgentSessionInfo {
+    pub fn new(session_id: impl Into<acp::SessionId>) -> Self {
+        Self {
+            session_id: session_id.into(),
+            cwd: None,
+            title: None,
+            updated_at: None,
+            meta: None,
+        }
+    }
+}
+
+pub trait AgentSessionList {
+    fn list_sessions(
+        &self,
+        request: AgentSessionListRequest,
+        cx: &mut App,
+    ) -> Task<Result<AgentSessionListResponse>>;
+
+    fn delete_session(&self, _session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<()>> {
+        Task::ready(Ok(()))
+    }
+
+    fn delete_sessions(&self, _cx: &mut App) -> Task<Result<()>> {
+        Task::ready(Ok(()))
+    }
+
+    fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
+        None
+    }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
+}
+
+impl dyn AgentSessionList {
+    pub fn downcast<T: 'static + AgentSessionList + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
+        self.into_any().downcast().ok()
+    }
+}
+
 #[derive(Debug)]
 pub struct AuthRequired {
     pub description: Option<String>,

crates/agent/src/agent.rs 🔗

@@ -20,7 +20,10 @@ pub use thread_store::*;
 pub use tool_permissions::*;
 pub use tools::*;
 
-use acp_thread::{AcpThread, AgentModelSelector, UserMessageId};
+use acp_thread::{
+    AcpThread, AgentModelSelector, AgentSessionInfo, AgentSessionList, AgentSessionListRequest,
+    AgentSessionListResponse, UserMessageId,
+};
 use agent_client_protocol as acp;
 use anyhow::{Context as _, Result, anyhow};
 use chrono::{DateTime, Utc};
@@ -1345,6 +1348,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
         }) as _)
     }
 
+    fn session_list(&self, cx: &mut App) -> Option<Rc<dyn AgentSessionList>> {
+        let thread_store = self.0.read(cx).thread_store.clone();
+        Some(Rc::new(NativeAgentSessionList::new(thread_store, cx)) as _)
+    }
+
     fn telemetry(&self) -> Option<Rc<dyn acp_thread::AgentTelemetry>> {
         Some(Rc::new(self.clone()) as Rc<dyn acp_thread::AgentTelemetry>)
     }
@@ -1371,6 +1379,74 @@ impl acp_thread::AgentTelemetry for NativeAgentConnection {
     }
 }
 
+pub struct NativeAgentSessionList {
+    thread_store: Entity<ThreadStore>,
+    updates_rx: watch::Receiver<()>,
+    _subscription: Subscription,
+}
+
+impl NativeAgentSessionList {
+    fn new(thread_store: Entity<ThreadStore>, cx: &mut App) -> Self {
+        let (mut tx, rx) = watch::channel(());
+        let subscription = cx.observe(&thread_store, move |_, _| {
+            tx.send(()).ok();
+        });
+        Self {
+            thread_store,
+            updates_rx: rx,
+            _subscription: subscription,
+        }
+    }
+
+    fn to_session_info(entry: DbThreadMetadata) -> AgentSessionInfo {
+        AgentSessionInfo {
+            session_id: entry.id,
+            cwd: None,
+            title: Some(entry.title),
+            updated_at: Some(entry.updated_at),
+            meta: None,
+        }
+    }
+
+    pub fn thread_store(&self) -> &Entity<ThreadStore> {
+        &self.thread_store
+    }
+}
+
+impl AgentSessionList for NativeAgentSessionList {
+    fn list_sessions(
+        &self,
+        _request: AgentSessionListRequest,
+        cx: &mut App,
+    ) -> Task<Result<AgentSessionListResponse>> {
+        let sessions = self
+            .thread_store
+            .read(cx)
+            .entries()
+            .map(Self::to_session_info)
+            .collect();
+        Task::ready(Ok(AgentSessionListResponse::new(sessions)))
+    }
+
+    fn delete_session(&self, session_id: &acp::SessionId, cx: &mut App) -> Task<Result<()>> {
+        self.thread_store
+            .update(cx, |store, cx| store.delete_thread(session_id.clone(), cx))
+    }
+
+    fn delete_sessions(&self, cx: &mut App) -> Task<Result<()>> {
+        self.thread_store
+            .update(cx, |store, cx| store.delete_threads(cx))
+    }
+
+    fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
+        Some(self.updates_rx.clone())
+    }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+        self
+    }
+}
+
 struct NativeAgentSessionTruncate {
     thread: Entity<Thread>,
     acp_thread: WeakEntity<AcpThread>,

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

@@ -1,6 +1,6 @@
 use std::{cell::RefCell, ops::Range, rc::Rc};
 
-use acp_thread::{AcpThread, AgentThreadEntry};
+use acp_thread::{AcpThread, AgentSessionList, AgentThreadEntry};
 use agent::ThreadStore;
 use agent_client_protocol::{self as acp, ToolCallId};
 use collections::HashMap;
@@ -23,7 +23,8 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
 pub struct EntryViewState {
     workspace: WeakEntity<Workspace>,
     project: WeakEntity<Project>,
-    history_store: Entity<ThreadStore>,
+    thread_store: Option<Entity<ThreadStore>>,
+    session_list: Rc<RefCell<Option<Rc<dyn AgentSessionList>>>>,
     prompt_store: Option<Entity<PromptStore>>,
     entries: Vec<Entry>,
     prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
@@ -35,7 +36,8 @@ impl EntryViewState {
     pub fn new(
         workspace: WeakEntity<Workspace>,
         project: WeakEntity<Project>,
-        history_store: Entity<ThreadStore>,
+        thread_store: Option<Entity<ThreadStore>>,
+        session_list: Rc<RefCell<Option<Rc<dyn AgentSessionList>>>>,
         prompt_store: Option<Entity<PromptStore>>,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
@@ -44,7 +46,8 @@ impl EntryViewState {
         Self {
             workspace,
             project,
-            history_store,
+            thread_store,
+            session_list,
             prompt_store,
             entries: Vec::new(),
             prompt_capabilities,
@@ -85,7 +88,8 @@ impl EntryViewState {
                         let mut editor = MessageEditor::new(
                             self.workspace.clone(),
                             self.project.clone(),
-                            self.history_store.clone(),
+                            self.thread_store.clone(),
+                            self.session_list.clone(),
                             self.prompt_store.clone(),
                             self.prompt_capabilities.clone(),
                             self.available_commands.clone(),
@@ -396,10 +400,9 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
 
 #[cfg(test)]
 mod tests {
-    use std::{path::Path, rc::Rc};
+    use std::{cell::RefCell, path::Path, rc::Rc};
 
     use acp_thread::{AgentConnection, StubAgentConnection};
-    use agent::ThreadStore;
     use agent_client_protocol as acp;
     use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
     use editor::RowInfo;
@@ -451,13 +454,15 @@ mod tests {
             connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx)
         });
 
-        let history_store = cx.new(|cx| ThreadStore::new(cx));
+        let thread_store = None;
+        let session_list = Rc::new(RefCell::new(None));
 
         let view_state = cx.new(|_cx| {
             EntryViewState::new(
                 workspace.downgrade(),
                 project.downgrade(),
-                history_store,
+                thread_store,
+                session_list,
                 None,
                 Default::default(),
                 Default::default(),

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

@@ -9,7 +9,7 @@ use crate::{
         Mention, MentionImage, MentionSet, insert_crease_for_mention, paste_images_as_context,
     },
 };
-use acp_thread::MentionUri;
+use acp_thread::{AgentSessionInfo, AgentSessionList, MentionUri};
 use agent::ThreadStore;
 use agent_client_protocol as acp;
 use anyhow::{Result, anyhow};
@@ -44,6 +44,7 @@ pub struct MessageEditor {
     prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
     available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
     agent_name: SharedString,
+    thread_store: Option<Entity<ThreadStore>>,
     _subscriptions: Vec<Subscription>,
     _parse_slash_command_task: Task<()>,
 }
@@ -69,11 +70,10 @@ impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
     fn supported_modes(&self, cx: &App) -> Vec<PromptContextType> {
         let mut supported = vec![PromptContextType::File, PromptContextType::Symbol];
         if self.read(cx).prompt_capabilities.borrow().embedded_context {
-            supported.extend(&[
-                PromptContextType::Thread,
-                PromptContextType::Fetch,
-                PromptContextType::Rules,
-            ]);
+            if self.read(cx).thread_store.is_some() {
+                supported.push(PromptContextType::Thread);
+            }
+            supported.extend(&[PromptContextType::Fetch, PromptContextType::Rules]);
         }
         supported
     }
@@ -100,7 +100,8 @@ impl MessageEditor {
     pub fn new(
         workspace: WeakEntity<Workspace>,
         project: WeakEntity<Project>,
-        history_store: Entity<ThreadStore>,
+        thread_store: Option<Entity<ThreadStore>>,
+        session_list: Rc<RefCell<Option<Rc<dyn AgentSessionList>>>>,
         prompt_store: Option<Entity<PromptStore>>,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
@@ -152,12 +153,13 @@ impl MessageEditor {
             editor
         });
         let mention_set =
-            cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
+            cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));
         let completion_provider = Rc::new(PromptCompletionProvider::new(
             cx.entity(),
             editor.downgrade(),
             mention_set.clone(),
-            history_store.clone(),
+            thread_store.clone(),
+            session_list,
             prompt_store.clone(),
             workspace.clone(),
         ));
@@ -215,6 +217,7 @@ impl MessageEditor {
             prompt_capabilities,
             available_commands,
             agent_name,
+            thread_store,
             _subscriptions: subscriptions,
             _parse_slash_command_task: Task::ready(()),
         }
@@ -269,16 +272,24 @@ impl MessageEditor {
 
     pub fn insert_thread_summary(
         &mut self,
-        thread: agent::DbThreadMetadata,
+        thread: AgentSessionInfo,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        if self.thread_store.is_none() {
+            return;
+        }
         let Some(workspace) = self.workspace.upgrade() else {
             return;
         };
+        let thread_title = thread
+            .title
+            .clone()
+            .filter(|title| !title.is_empty())
+            .unwrap_or_else(|| SharedString::new_static("New Thread"));
         let uri = MentionUri::Thread {
-            id: thread.id.clone(),
-            name: thread.title.to_string(),
+            id: thread.session_id,
+            name: thread_title.to_string(),
         };
         let content = format!("{}\n", uri.as_link());
 
@@ -299,7 +310,7 @@ impl MessageEditor {
         self.mention_set
             .update(cx, |mention_set, cx| {
                 mention_set.confirm_mention_completion(
-                    thread.title,
+                    thread_title,
                     start,
                     content_len,
                     uri,
@@ -1061,7 +1072,7 @@ impl Addon for MessageEditorAddon {
 mod tests {
     use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
 
-    use acp_thread::MentionUri;
+    use acp_thread::{AgentSessionInfo, MentionUri};
     use agent::{ThreadStore, outline};
     use agent_client_protocol as acp;
     use editor::{AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset};
@@ -1083,6 +1094,7 @@ mod tests {
         message_editor::{Mention, MessageEditor},
         thread_view::tests::init_test,
     };
+    use crate::completion_provider::{PromptCompletionProviderDelegate, PromptContextType};
 
     #[gpui::test]
     async fn test_at_mention_removal(cx: &mut TestAppContext) {
@@ -1095,14 +1107,16 @@ mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let history_store = cx.new(|cx| ThreadStore::new(cx));
+        let thread_store = None;
+        let session_list = Rc::new(RefCell::new(None));
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
                 MessageEditor::new(
                     workspace.downgrade(),
                     project.downgrade(),
-                    history_store.clone(),
+                    thread_store.clone(),
+                    session_list.clone(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -1199,7 +1213,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
-        let history_store = cx.new(|cx| ThreadStore::new(cx));
+        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![]));
@@ -1212,7 +1227,8 @@ mod tests {
                 MessageEditor::new(
                     workspace_handle.clone(),
                     project.downgrade(),
-                    history_store.clone(),
+                    thread_store.clone(),
+                    session_list.clone(),
                     None,
                     prompt_capabilities.clone(),
                     available_commands.clone(),
@@ -1355,7 +1371,8 @@ mod tests {
 
         let mut cx = VisualTestContext::from_window(*window, cx);
 
-        let history_store = cx.new(|cx| ThreadStore::new(cx));
+        let thread_store = None;
+        let session_list = Rc::new(RefCell::new(None));
         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"),
@@ -1372,7 +1389,8 @@ mod tests {
                 MessageEditor::new(
                     workspace_handle,
                     project.downgrade(),
-                    history_store.clone(),
+                    thread_store.clone(),
+                    session_list.clone(),
                     None,
                     prompt_capabilities.clone(),
                     available_commands.clone(),
@@ -1584,7 +1602,8 @@ mod tests {
             opened_editors.push(buffer);
         }
 
-        let history_store = cx.new(|cx| ThreadStore::new(cx));
+        let thread_store = cx.new(|cx| ThreadStore::new(cx));
+        let session_list = Rc::new(RefCell::new(None));
         let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
 
         let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
@@ -1593,7 +1612,8 @@ mod tests {
                 MessageEditor::new(
                     workspace_handle,
                     project.downgrade(),
-                    history_store.clone(),
+                    Some(thread_store),
+                    session_list.clone(),
                     None,
                     prompt_capabilities.clone(),
                     Default::default(),
@@ -2076,14 +2096,16 @@ mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let history_store = cx.new(|cx| ThreadStore::new(cx));
+        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
+        let session_list = Rc::new(RefCell::new(None));
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
                 let editor = MessageEditor::new(
                     workspace.downgrade(),
                     project.downgrade(),
-                    history_store.clone(),
+                    thread_store.clone(),
+                    session_list.clone(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -2173,13 +2195,16 @@ mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let history_store = cx.new(|cx| ThreadStore::new(cx));
+        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
+        let session_list = Rc::new(RefCell::new(None));
 
         // Create a thread metadata to insert as summary
-        let thread_metadata = agent::DbThreadMetadata {
-            id: acp::SessionId::new("thread-123"),
-            title: "Previous Conversation".into(),
-            updated_at: chrono::Utc::now(),
+        let thread_metadata = AgentSessionInfo {
+            session_id: acp::SessionId::new("thread-123"),
+            cwd: None,
+            title: Some("Previous Conversation".into()),
+            updated_at: Some(chrono::Utc::now()),
+            meta: None,
         };
 
         let message_editor = cx.update(|window, cx| {
@@ -2187,7 +2212,8 @@ mod tests {
                 let mut editor = MessageEditor::new(
                     workspace.downgrade(),
                     project.downgrade(),
-                    history_store.clone(),
+                    thread_store.clone(),
+                    session_list.clone(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -2207,10 +2233,11 @@ mod tests {
 
         // Construct expected values for verification
         let expected_uri = MentionUri::Thread {
-            id: thread_metadata.id.clone(),
-            name: thread_metadata.title.to_string(),
+            id: thread_metadata.session_id.clone(),
+            name: thread_metadata.title.as_ref().unwrap().to_string(),
         };
-        let expected_link = format!("[@{}]({})", thread_metadata.title, expected_uri.to_uri());
+        let expected_title = thread_metadata.title.as_ref().unwrap();
+        let expected_link = format!("[@{}]({})", expected_title, expected_uri.to_uri());
 
         message_editor.read_with(cx, |editor, cx| {
             let text = editor.text(cx);
@@ -2236,6 +2263,171 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_insert_thread_summary_skipped_for_external_agents(cx: &mut TestAppContext) {
+        init_test(cx);
+        cx.update(LanguageModelRegistry::test);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/project", json!({"file": ""})).await;
+        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+        let (workspace, cx) =
+            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 thread_metadata = AgentSessionInfo {
+            session_id: acp::SessionId::new("thread-123"),
+            cwd: None,
+            title: Some("Previous Conversation".into()),
+            updated_at: Some(chrono::Utc::now()),
+            meta: None,
+        };
+
+        let message_editor = cx.update(|window, cx| {
+            cx.new(|cx| {
+                let mut editor = MessageEditor::new(
+                    workspace.downgrade(),
+                    project.downgrade(),
+                    thread_store.clone(),
+                    session_list.clone(),
+                    None,
+                    Default::default(),
+                    Default::default(),
+                    "Test Agent".into(),
+                    "Test",
+                    EditorMode::AutoHeight {
+                        min_lines: 1,
+                        max_lines: None,
+                    },
+                    window,
+                    cx,
+                );
+                editor.insert_thread_summary(thread_metadata, window, cx);
+                editor
+            })
+        });
+
+        message_editor.read_with(cx, |editor, cx| {
+            assert!(
+                editor.text(cx).is_empty(),
+                "Expected thread summary to be skipped for external agents"
+            );
+            assert!(
+                editor.mention_set().read(cx).mentions().is_empty(),
+                "Expected no mentions when thread summary is skipped"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_thread_mode_hidden_when_disabled(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/project", json!({"file": ""})).await;
+        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+        let (workspace, cx) =
+            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 message_editor = cx.update(|window, cx| {
+            cx.new(|cx| {
+                MessageEditor::new(
+                    workspace.downgrade(),
+                    project.downgrade(),
+                    thread_store.clone(),
+                    session_list.clone(),
+                    None,
+                    Default::default(),
+                    Default::default(),
+                    "Test Agent".into(),
+                    "Test",
+                    EditorMode::AutoHeight {
+                        min_lines: 1,
+                        max_lines: None,
+                    },
+                    window,
+                    cx,
+                )
+            })
+        });
+
+        message_editor.update(cx, |editor, _cx| {
+            editor
+                .prompt_capabilities
+                .replace(acp::PromptCapabilities::new().embedded_context(true));
+        });
+
+        let supported_modes = {
+            let app = cx.app.borrow();
+            message_editor.supported_modes(&app)
+        };
+
+        assert!(
+            !supported_modes.contains(&PromptContextType::Thread),
+            "Expected thread mode to be hidden when thread mentions are disabled"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_thread_mode_visible_when_enabled(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/project", json!({"file": ""})).await;
+        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+        let (workspace, cx) =
+            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 message_editor = cx.update(|window, cx| {
+            cx.new(|cx| {
+                MessageEditor::new(
+                    workspace.downgrade(),
+                    project.downgrade(),
+                    thread_store.clone(),
+                    session_list.clone(),
+                    None,
+                    Default::default(),
+                    Default::default(),
+                    "Test Agent".into(),
+                    "Test",
+                    EditorMode::AutoHeight {
+                        min_lines: 1,
+                        max_lines: None,
+                    },
+                    window,
+                    cx,
+                )
+            })
+        });
+
+        message_editor.update(cx, |editor, _cx| {
+            editor
+                .prompt_capabilities
+                .replace(acp::PromptCapabilities::new().embedded_context(true));
+        });
+
+        let supported_modes = {
+            let app = cx.app.borrow();
+            message_editor.supported_modes(&app)
+        };
+
+        assert!(
+            supported_modes.contains(&PromptContextType::Thread),
+            "Expected thread mode to be visible when enabled"
+        );
+    }
+
     #[gpui::test]
     async fn test_whitespace_trimming(cx: &mut TestAppContext) {
         init_test(cx);
@@ -2248,14 +2440,16 @@ mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let history_store = cx.new(|cx| ThreadStore::new(cx));
+        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
+        let session_list = Rc::new(RefCell::new(None));
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
                 MessageEditor::new(
                     workspace.downgrade(),
                     project.downgrade(),
-                    history_store.clone(),
+                    thread_store.clone(),
+                    session_list.clone(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -2309,7 +2503,8 @@ mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let history_store = cx.new(|cx| ThreadStore::new(cx));
+        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
+        let session_list = Rc::new(RefCell::new(None));
 
         let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
             let workspace_handle = cx.weak_entity();
@@ -2317,7 +2512,8 @@ mod tests {
                 MessageEditor::new(
                     workspace_handle,
                     project.downgrade(),
-                    history_store.clone(),
+                    thread_store.clone(),
+                    session_list.clone(),
                     None,
                     Default::default(),
                     Default::default(),
@@ -2463,7 +2659,8 @@ mod tests {
             });
         });
 
-        let history_store = cx.new(|cx| ThreadStore::new(cx));
+        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
+        let session_list = Rc::new(RefCell::new(None));
 
         // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
         // to ensure we have a fixed viewport, so we can eventually actually
@@ -2474,7 +2671,8 @@ mod tests {
                 MessageEditor::new(
                     workspace_handle,
                     project.downgrade(),
-                    history_store.clone(),
+                    thread_store.clone(),
+                    session_list.clone(),
                     None,
                     Default::default(),
                     Default::default(),

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

@@ -1,6 +1,7 @@
 use crate::acp::AcpThreadView;
 use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
-use agent::{DbThreadMetadata, ThreadStore};
+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;
@@ -8,7 +9,7 @@ use gpui::{
     App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
     UniformListScrollHandle, WeakEntity, Window, uniform_list,
 };
-use std::{fmt::Display, ops::Range};
+use std::{fmt::Display, ops::Range, rc::Rc};
 use text::Bias;
 use time::{OffsetDateTime, UtcOffset};
 use ui::{
@@ -18,16 +19,17 @@ use ui::{
 
 const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread");
 
-fn thread_title(entry: &DbThreadMetadata) -> &SharedString {
-    if entry.title.is_empty() {
-        DEFAULT_TITLE
-    } else {
-        &entry.title
-    }
+fn thread_title(entry: &AgentSessionInfo) -> &SharedString {
+    entry
+        .title
+        .as_ref()
+        .filter(|title| !title.is_empty())
+        .unwrap_or(DEFAULT_TITLE)
 }
 
 pub struct AcpThreadHistory {
-    pub(crate) thread_store: Entity<ThreadStore>,
+    session_list: Option<Rc<dyn AgentSessionList>>,
+    sessions: Vec<AgentSessionInfo>,
     scroll_handle: UniformListScrollHandle,
     selected_index: usize,
     hovered_index: Option<usize>,
@@ -37,23 +39,24 @@ pub struct AcpThreadHistory {
     local_timezone: UtcOffset,
     confirming_delete_history: bool,
     _update_task: Task<()>,
+    _watch_task: Option<Task<()>>,
     _subscriptions: Vec<gpui::Subscription>,
 }
 
 enum ListItemType {
     BucketSeparator(TimeBucket),
     Entry {
-        entry: DbThreadMetadata,
+        entry: AgentSessionInfo,
         format: EntryTimeFormat,
     },
     SearchResult {
-        entry: DbThreadMetadata,
+        entry: AgentSessionInfo,
         positions: Vec<usize>,
     },
 }
 
 impl ListItemType {
-    fn history_entry(&self) -> Option<&DbThreadMetadata> {
+    fn history_entry(&self) -> Option<&AgentSessionInfo> {
         match self {
             ListItemType::Entry { entry, .. } => Some(entry),
             ListItemType::SearchResult { entry, .. } => Some(entry),
@@ -63,14 +66,14 @@ impl ListItemType {
 }
 
 pub enum ThreadHistoryEvent {
-    Open(DbThreadMetadata),
+    Open(AgentSessionInfo),
 }
 
 impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
 
 impl AcpThreadHistory {
     pub(crate) fn new(
-        thread_store: Entity<ThreadStore>,
+        session_list: Option<Rc<dyn AgentSessionList>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -91,14 +94,11 @@ impl AcpThreadHistory {
                 }
             });
 
-        let thread_store_subscription = cx.observe(&thread_store, |this, _, cx| {
-            this.update_visible_items(true, cx);
-        });
-
         let scroll_handle = UniformListScrollHandle::default();
 
         let mut this = Self {
-            thread_store,
+            session_list: None,
+            sessions: Vec::new(),
             scroll_handle,
             selected_index: 0,
             hovered_index: None,
@@ -110,17 +110,16 @@ impl AcpThreadHistory {
             .unwrap(),
             search_query: SharedString::default(),
             confirming_delete_history: false,
-            _subscriptions: vec![search_editor_subscription, thread_store_subscription],
+            _subscriptions: vec![search_editor_subscription],
             _update_task: Task::ready(()),
+            _watch_task: None,
         };
-        this.update_visible_items(false, cx);
+        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
-            .thread_store
-            .update(cx, |store, _| store.entries().collect());
+        let entries = self.sessions.clone();
         let new_list_items = if self.search_query.is_empty() {
             self.add_list_separators(entries, cx)
         } else {
@@ -141,7 +140,7 @@ impl AcpThreadHistory {
                         .position(|visible_entry| {
                             visible_entry
                                 .history_entry()
-                                .is_some_and(|entry| entry.id == history_entry.id)
+                                .is_some_and(|entry| entry.session_id == history_entry.session_id)
                         })
                         .unwrap_or(0)
                 } else {
@@ -156,9 +155,111 @@ impl AcpThreadHistory {
         });
     }
 
+    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()
+    }
+
+    pub(crate) fn sessions(&self) -> &[AgentSessionInfo] {
+        &self.sessions
+    }
+
     fn add_list_separators(
         &self,
-        entries: Vec<DbThreadMetadata>,
+        entries: Vec<AgentSessionInfo>,
         cx: &App,
     ) -> Task<Vec<ListItemType>> {
         cx.background_spawn(async move {
@@ -167,8 +268,13 @@ impl AcpThreadHistory {
             let today = Local::now().naive_local().date();
 
             for entry in entries.into_iter() {
-                let entry_date = entry.updated_at.with_timezone(&Local).naive_local().date();
-                let entry_bucket = TimeBucket::from_dates(today, entry_date);
+                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);
@@ -186,7 +292,7 @@ impl AcpThreadHistory {
 
     fn filter_search_results(
         &self,
-        entries: Vec<DbThreadMetadata>,
+        entries: Vec<AgentSessionInfo>,
         cx: &App,
     ) -> Task<Vec<ListItemType>> {
         let query = self.search_query.clone();
@@ -227,11 +333,11 @@ impl AcpThreadHistory {
         self.visible_items.is_empty() && !self.search_query.is_empty()
     }
 
-    fn selected_history_entry(&self) -> Option<&DbThreadMetadata> {
+    fn selected_history_entry(&self) -> Option<&AgentSessionInfo> {
         self.get_history_entry(self.selected_index)
     }
 
-    fn get_history_entry(&self, visible_items_ix: usize) -> Option<&DbThreadMetadata> {
+    fn get_history_entry(&self, visible_items_ix: usize) -> Option<&AgentSessionInfo> {
         self.visible_items.get(visible_items_ix)?.history_entry()
     }
 
@@ -330,17 +436,17 @@ impl AcpThreadHistory {
         let Some(entry) = self.get_history_entry(visible_item_ix) else {
             return;
         };
-
-        let task = self
-            .thread_store
-            .update(cx, |store, cx| store.delete_thread(entry.id.clone(), cx));
+        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>) {
-        self.thread_store.update(cx, |store, cx| {
-            store.delete_threads(cx).detach_and_log_err(cx)
-        });
+        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();
     }
@@ -397,7 +503,7 @@ impl AcpThreadHistory {
 
     fn render_history_entry(
         &self,
-        entry: &DbThreadMetadata,
+        entry: &AgentSessionInfo,
         format: EntryTimeFormat,
         ix: usize,
         highlight_positions: Vec<usize>,
@@ -405,23 +511,27 @@ impl AcpThreadHistory {
     ) -> AnyElement {
         let selected = ix == self.selected_index;
         let hovered = Some(ix) == self.hovered_index;
-        let timestamp = entry.updated_at.timestamp();
-
-        let display_text = match format {
-            EntryTimeFormat::DateAndTime => {
-                let entry_time = entry.updated_at;
+        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 => format.format_timestamp(timestamp, self.local_timezone),
+            (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 =
-            EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
+        let full_date = entry_time
+            .map(|time| {
+                EntryTimeFormat::DateAndTime.format_timestamp(time.timestamp(), self.local_timezone)
+            })
+            .unwrap_or_else(|| "Unknown".to_string());
 
         h_flex()
             .w_full()
@@ -490,7 +600,7 @@ impl Focusable for AcpThreadHistory {
 
 impl Render for AcpThreadHistory {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let has_no_history = self.thread_store.read(cx).is_empty();
+        let has_no_history = self.is_empty();
 
         v_flex()
             .key_context("ThreadHistory")
@@ -623,7 +733,7 @@ impl Render for AcpThreadHistory {
 
 #[derive(IntoElement)]
 pub struct AcpHistoryEntryElement {
-    entry: DbThreadMetadata,
+    entry: AgentSessionInfo,
     thread_view: WeakEntity<AcpThreadView>,
     selected: bool,
     hovered: bool,
@@ -631,7 +741,7 @@ pub struct AcpHistoryEntryElement {
 }
 
 impl AcpHistoryEntryElement {
-    pub fn new(entry: DbThreadMetadata, thread_view: WeakEntity<AcpThreadView>) -> Self {
+    pub fn new(entry: AgentSessionInfo, thread_view: WeakEntity<AcpThreadView>) -> Self {
         Self {
             entry,
             thread_view,
@@ -654,24 +764,26 @@ impl AcpHistoryEntryElement {
 
 impl RenderOnce for AcpHistoryEntryElement {
     fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
-        let id = ElementId::Name(self.entry.id.0.clone().into());
+        let id = ElementId::Name(self.entry.session_id.0.clone().into());
         let title = thread_title(&self.entry).clone();
-        let timestamp = self.entry.updated_at;
-
-        let formatted_time = {
-            let now = chrono::Utc::now();
-            let duration = now.signed_duration_since(timestamp);
-
-            if duration.num_days() > 0 {
-                format!("{}d", duration.num_days())
-            } else if duration.num_hours() > 0 {
-                format!("{}h ago", duration.num_hours())
-            } else if duration.num_minutes() > 0 {
-                format!("{}m ago", duration.num_minutes())
-            } else {
-                "Just now".to_string()
-            }
-        };
+        let formatted_time = self
+            .entry
+            .updated_at
+            .map(|timestamp| {
+                let now = chrono::Utc::now();
+                let duration = now.signed_duration_since(timestamp);
+
+                if duration.num_days() > 0 {
+                    format!("{}d", duration.num_days())
+                } else if duration.num_hours() > 0 {
+                    format!("{}h ago", duration.num_hours())
+                } else if duration.num_minutes() > 0 {
+                    format!("{}m ago", duration.num_minutes())
+                } else {
+                    "Just now".to_string()
+                }
+            })
+            .unwrap_or_else(|| "Unknown".to_string());
 
         ListItem::new(id)
             .rounded()

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

@@ -1,11 +1,11 @@
 use acp_thread::{
-    AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
-    AuthRequired, LoadError, MentionUri, RetryStatus, ThreadStatus, ToolCall, ToolCallContent,
-    ToolCallStatus, UserMessageId,
+    AcpThread, AcpThreadEvent, AgentSessionInfo, AgentSessionList, AgentSessionListRequest,
+    AgentThreadEntry, AssistantMessage, AssistantMessageChunk, AuthRequired, LoadError, MentionUri,
+    RetryStatus, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, UserMessageId,
 };
 use acp_thread::{AgentConnection, Plan};
 use action_log::ActionLogTelemetry;
-use agent::{DbThreadMetadata, NativeAgentServer, SharedThread, ThreadStore};
+use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};
 use agent_client_protocol::{self as acp, PromptCapabilities};
 use agent_servers::{AgentServer, AgentServerDelegate};
 use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
@@ -310,7 +310,11 @@ pub struct AcpThreadView {
     project: Entity<Project>,
     thread_state: ThreadState,
     login: Option<task::SpawnInTerminal>,
-    thread_store: Entity<ThreadStore>,
+    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<()>>,
     hovered_recent_history_item: Option<usize>,
     entry_view_state: Entity<EntryViewState>,
     message_editor: Entity<MessageEditor>,
@@ -340,7 +344,7 @@ pub struct AcpThreadView {
     available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
     is_loading_contents: bool,
     new_server_version_available: Option<SharedString>,
-    resume_thread_metadata: Option<DbThreadMetadata>,
+    resume_thread_metadata: Option<AgentSessionInfo>,
     _cancel_task: Option<Task<()>>,
     _subscriptions: [Subscription; 5],
     show_codex_windows_warning: bool,
@@ -388,11 +392,11 @@ struct LoadingView {
 impl AcpThreadView {
     pub fn new(
         agent: Rc<dyn AgentServer>,
-        resume_thread: Option<DbThreadMetadata>,
-        summarize_thread: Option<DbThreadMetadata>,
+        resume_thread: Option<AgentSessionInfo>,
+        summarize_thread: Option<AgentSessionInfo>,
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
-        thread_store: Entity<ThreadStore>,
+        thread_store: Option<Entity<ThreadStore>>,
         prompt_store: Option<Entity<PromptStore>>,
         track_load_event: bool,
         window: &mut Window,
@@ -400,6 +404,7 @@ impl AcpThreadView {
     ) -> 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
@@ -414,6 +419,7 @@ impl AcpThreadView {
                 workspace.clone(),
                 project.downgrade(),
                 thread_store.clone(),
+                session_list_state.clone(),
                 prompt_store.clone(),
                 prompt_capabilities.clone(),
                 available_commands.clone(),
@@ -439,6 +445,7 @@ impl AcpThreadView {
                 workspace.clone(),
                 project.downgrade(),
                 thread_store.clone(),
+                session_list_state.clone(),
                 prompt_store.clone(),
                 prompt_capabilities.clone(),
                 available_commands.clone(),
@@ -513,7 +520,11 @@ impl AcpThreadView {
             available_commands,
             editor_expanded: false,
             should_be_following: false,
-            thread_store,
+            session_list: None,
+            session_list_state,
+            recent_history_entries: Vec::new(),
+            _recent_history_task: Task::ready(()),
+            _recent_history_watch_task: None,
             hovered_recent_history_item: None,
             is_loading_contents: false,
             _subscriptions: subscriptions,
@@ -548,6 +559,11 @@ 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;
@@ -558,7 +574,7 @@ impl AcpThreadView {
 
     fn initial_state(
         agent: Rc<dyn AgentServer>,
-        resume_thread: Option<DbThreadMetadata>,
+        resume_thread: Option<AgentSessionInfo>,
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
         track_load_event: bool,
@@ -632,7 +648,7 @@ impl AcpThreadView {
                 cx.update(|_, cx| {
                     native_agent
                         .0
-                        .update(cx, |agent, cx| agent.open_thread(resume.id, cx))
+                        .update(cx, |agent, cx| agent.open_thread(resume.session_id, cx))
                 })
                 .log_err()
             } else {
@@ -684,13 +700,16 @@ impl AcpThreadView {
 
                         AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
 
+                        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);
+
                         // Check for config options first
                         // Config options take precedence over legacy mode/model selectors
                         // (feature flag gating happens at the data layer)
-                        let config_options_provider = thread
-                            .read(cx)
-                            .connection()
-                            .session_config_options(thread.read(cx).session_id(), cx);
+                        let config_options_provider =
+                            connection.session_config_options(&session_id, cx);
 
                         let mode_selector;
                         if let Some(config_options) = config_options_provider {
@@ -705,11 +724,8 @@ impl AcpThreadView {
                         } else {
                             // Fall back to legacy mode/model selectors
                             this.config_options_view = None;
-                            this.model_selector = thread
-                                .read(cx)
-                                .connection()
-                                .model_selector(thread.read(cx).session_id())
-                                .map(|selector| {
+                            this.model_selector =
+                                connection.model_selector(&session_id).map(|selector| {
                                     let agent_server = this.agent.clone();
                                     let fs = this.project.read(cx).fs().clone();
                                     cx.new(|cx| {
@@ -725,22 +741,21 @@ impl AcpThreadView {
                                     })
                                 });
 
-                            mode_selector = thread
-                                .read(cx)
-                                .connection()
-                                .session_modes(thread.read(cx).session_id(), cx)
-                                .map(|session_modes| {
-                                    let fs = this.project.read(cx).fs().clone();
-                                    let focus_handle = this.focus_handle(cx);
-                                    cx.new(|_cx| {
-                                        ModeSelector::new(
-                                            session_modes,
-                                            this.agent.clone(),
-                                            fs,
-                                            focus_handle,
-                                        )
-                                    })
-                                });
+                            mode_selector =
+                                connection
+                                    .session_modes(&session_id, cx)
+                                    .map(|session_modes| {
+                                        let fs = this.project.read(cx).fs().clone();
+                                        let focus_handle = this.focus_handle(cx);
+                                        cx.new(|_cx| {
+                                            ModeSelector::new(
+                                                session_modes,
+                                                this.agent.clone(),
+                                                fs,
+                                                focus_handle,
+                                            )
+                                        })
+                                    });
                         }
 
                         let mut subscriptions = vec![
@@ -942,6 +957,10 @@ 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(),
@@ -1047,9 +1066,16 @@ impl AcpThreadView {
         let Some(thread) = self.as_native_thread(cx) else {
             return;
         };
+        let Some(session_list) = self
+            .session_list
+            .clone()
+            .and_then(|list| list.downcast::<NativeAgentSessionList>())
+        else {
+            return;
+        };
+        let thread_store = session_list.thread_store().clone();
 
         let client = self.project.read(cx).client();
-        let thread_store = self.thread_store.clone();
         let session_id = thread.read(cx).id().clone();
 
         cx.spawn_in(window, async move |this, cx| {
@@ -1069,10 +1095,12 @@ impl AcpThreadView {
                 })
                 .await?;
 
-            let thread_metadata = agent::DbThreadMetadata {
-                id: session_id,
-                title: format!("🔗 {}", response.title).into(),
-                updated_at: chrono::Utc::now(),
+            let thread_metadata = AgentSessionInfo {
+                session_id,
+                cwd: None,
+                title: Some(format!("🔗 {}", response.title).into()),
+                updated_at: Some(chrono::Utc::now()),
+                meta: None,
             };
 
             this.update_in(cx, |this, window, cx| {
@@ -4052,20 +4080,64 @@ impl AcpThreadView {
         )
     }
 
+    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.clone();
+        *self.session_list_state.borrow_mut() = session_list;
+        self.recent_history_entries.clear();
+        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:#}");
+            }
+        });
+    }
+
     fn render_recent_history(&self, cx: &mut Context<Self>) -> AnyElement {
-        let render_history = self
-            .agent
-            .clone()
-            .downcast::<agent::NativeAgentServer>()
-            .is_some()
-            && !self.thread_store.read(cx).is_empty();
+        let render_history = self.session_list.is_some() && !self.recent_history_entries.is_empty();
 
         v_flex()
             .size_full()
             .when(render_history, |this| {
-                let recent_history: Vec<_> = self.thread_store.update(cx, |thread_store, _| {
-                    thread_store.entries().take(3).collect()
-                });
+                let recent_history = self.recent_history_entries.clone();
                 this.justify_end().child(
                     v_flex()
                         .child(
@@ -5527,10 +5599,12 @@ impl AcpThreadView {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         panel.update(cx, |panel, cx| {
                             panel.load_agent_thread(
-                                DbThreadMetadata {
-                                    id,
-                                    title: name.into(),
-                                    updated_at: Default::default(),
+                                AgentSessionInfo {
+                                    session_id: id,
+                                    cwd: None,
+                                    title: Some(name.into()),
+                                    updated_at: None,
+                                    meta: None,
                                 },
                                 window,
                                 cx,
@@ -6784,10 +6858,11 @@ impl AcpThreadView {
             }))
     }
 
-    pub fn delete_history_entry(&mut self, entry: DbThreadMetadata, cx: &mut Context<Self>) {
-        let task = self
-            .thread_store
-            .update(cx, |store, cx| store.delete_thread(entry.id.clone(), cx));
+    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);
         task.detach_and_log_err(cx);
     }
 
@@ -7213,7 +7288,7 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 
 #[cfg(test)]
 pub(crate) mod tests {
-    use acp_thread::StubAgentConnection;
+    use acp_thread::{AgentSessionListResponse, StubAgentConnection};
     use action_log::ActionLog;
     use agent_client_protocol::SessionId;
     use editor::MultiBufferOffset;
@@ -7224,6 +7299,7 @@ pub(crate) mod tests {
     use settings::SettingsStore;
     use std::any::Any;
     use std::path::Path;
+    use std::rc::Rc;
     use workspace::Item;
 
     use super::*;
@@ -7291,6 +7367,55 @@ pub(crate) mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_recent_history_refreshes_when_session_list_swapped(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 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);
+        });
+        cx.run_until_parked();
+
+        thread_view.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
+            );
+
+            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);
+        });
+        cx.run_until_parked();
+
+        thread_view.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
+            );
+
+            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));
+        });
+    }
+
     #[gpui::test]
     async fn test_refusal_handling(cx: &mut TestAppContext) {
         init_test(cx);
@@ -7531,7 +7656,7 @@ pub(crate) mod tests {
                     None,
                     workspace.downgrade(),
                     project,
-                    thread_store,
+                    Some(thread_store),
                     None,
                     false,
                     window,
@@ -7607,6 +7732,30 @@ pub(crate) mod tests {
         }
     }
 
+    #[derive(Clone)]
+    struct StubSessionList {
+        sessions: Vec<AgentSessionInfo>,
+    }
+
+    impl StubSessionList {
+        fn new(sessions: Vec<AgentSessionInfo>) -> Self {
+            Self { sessions }
+        }
+    }
+
+    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
+        }
+    }
+
     impl<C> AgentServer for StubAgentServer<C>
     where
         C: 'static + AgentConnection + Send + Clone,
@@ -7798,7 +7947,7 @@ pub(crate) mod tests {
                     None,
                     workspace.downgrade(),
                     project.clone(),
-                    thread_store.clone(),
+                    Some(thread_store.clone()),
                     None,
                     false,
                     window,

crates/agent_ui/src/agent_panel.rs 🔗

@@ -1,7 +1,7 @@
 use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
 
-use acp_thread::AcpThread;
-use agent::{ContextServerRegistry, DbThreadMetadata, ThreadStore};
+use acp_thread::{AcpThread, AgentSessionInfo};
+use agent::{ContextServerRegistry, ThreadStore};
 use agent_servers::AgentServer;
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use project::{
@@ -315,7 +315,7 @@ impl ActiveView {
                 None,
                 workspace,
                 project,
-                thread_store,
+                Some(thread_store),
                 prompt_store,
                 false,
                 window,
@@ -429,6 +429,7 @@ 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>,
@@ -543,7 +544,7 @@ impl AgentPanel {
             cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 
         let thread_store = cx.new(|cx| ThreadStore::new(cx));
-        let acp_history = cx.new(|cx| AcpThreadHistory::new(thread_store.clone(), window, cx));
+        let acp_history = cx.new(|cx| AcpThreadHistory::new(None, window, cx));
         let text_thread_history =
             cx.new(|cx| TextThreadHistory::new(text_thread_store.clone(), window, cx));
         cx.subscribe_in(
@@ -683,6 +684,7 @@ 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,7 +734,7 @@ impl AgentPanel {
 
     pub fn open_thread(
         &mut self,
-        thread: DbThreadMetadata,
+        thread: AgentSessionInfo,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -788,9 +790,9 @@ impl AgentPanel {
         cx: &mut Context<Self>,
     ) {
         let Some(thread) = self
-            .thread_store
+            .acp_history
             .read(cx)
-            .thread_from_session_id(&action.from_session_id)
+            .session_for_id(&action.from_session_id)
         else {
             return;
         };
@@ -798,7 +800,7 @@ impl AgentPanel {
         self.external_thread(
             Some(ExternalAgent::NativeAgent),
             None,
-            Some(thread.clone()),
+            Some(thread),
             window,
             cx,
         );
@@ -850,8 +852,8 @@ impl AgentPanel {
     fn external_thread(
         &mut self,
         agent_choice: Option<crate::ExternalAgent>,
-        resume_thread: Option<DbThreadMetadata>,
-        summarize_thread: Option<DbThreadMetadata>,
+        resume_thread: Option<AgentSessionInfo>,
+        summarize_thread: Option<AgentSessionInfo>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -1349,10 +1351,12 @@ impl AgentPanel {
             HistoryKind::AgentThreads => {
                 let entries = panel
                     .read(cx)
-                    .thread_store
+                    .acp_history
                     .read(cx)
-                    .entries()
+                    .sessions()
+                    .iter()
                     .take(RECENTLY_UPDATED_MENU_LIMIT)
+                    .cloned()
                     .collect::<Vec<_>>();
 
                 if entries.is_empty() {
@@ -1362,11 +1366,12 @@ impl AgentPanel {
                 menu = menu.header("Recently Updated");
 
                 for entry in entries {
-                    let title = if entry.title.is_empty() {
-                        SharedString::new_static(DEFAULT_THREAD_TITLE)
-                    } else {
-                        entry.title.clone()
-                    };
+                    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();
@@ -1508,7 +1513,7 @@ impl AgentPanel {
 
     pub fn load_agent_thread(
         &mut self,
-        thread: DbThreadMetadata,
+        thread: AgentSessionInfo,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -1524,8 +1529,8 @@ impl AgentPanel {
     fn _external_thread(
         &mut self,
         server: Rc<dyn AgentServer>,
-        resume_thread: Option<DbThreadMetadata>,
-        summarize_thread: Option<DbThreadMetadata>,
+        resume_thread: Option<AgentSessionInfo>,
+        summarize_thread: Option<AgentSessionInfo>,
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
         loading: bool,
@@ -1538,6 +1543,11 @@ impl AgentPanel {
             self.selected_agent = selected_agent;
             self.serialize(cx);
         }
+        let thread_store = server
+            .clone()
+            .downcast::<agent::NativeAgentServer>()
+            .is_some()
+            .then(|| self.thread_store.clone());
 
         let thread_view = cx.new(|cx| {
             crate::acp::AcpThreadView::new(
@@ -1546,7 +1556,7 @@ impl AgentPanel {
                 summarize_thread,
                 workspace.clone(),
                 project,
-                self.thread_store.clone(),
+                thread_store,
                 self.prompt_store.clone(),
                 !loading,
                 window,
@@ -1554,6 +1564,15 @@ impl AgentPanel {
             )
         });
 
+        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,
@@ -2495,7 +2514,7 @@ impl AgentPanel {
                 false
             }
             _ => {
-                let history_is_empty = self.thread_store.read(cx).is_empty();
+                let history_is_empty = self.acp_history.read(cx).is_empty();
 
                 let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
                     .visible_providers()

crates/agent_ui/src/completion_provider.rs 🔗

@@ -1,17 +1,19 @@
+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::MentionUri;
-use agent::{DbThreadMetadata, ThreadStore};
+use acp_thread::{AgentSessionInfo, AgentSessionList, AgentSessionListRequest, MentionUri};
+use agent::ThreadStore;
 use anyhow::Result;
 use editor::{
     CompletionProvider, Editor, ExcerptId, code_context_menus::COMPLETION_MENU_MAX_WIDTH,
 };
 use fuzzy::{PathMatch, StringMatch, StringMatchCandidate};
-use gpui::{App, Entity, SharedString, Task, WeakEntity};
+use gpui::{App, BackgroundExecutor, Entity, SharedString, Task, WeakEntity};
 use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
 use lsp::CompletionContext;
 use ordered_float::OrderedFloat;
@@ -132,8 +134,8 @@ impl PromptContextType {
 pub(crate) enum Match {
     File(FileMatch),
     Symbol(SymbolMatch),
-    Thread(DbThreadMetadata),
-    RecentThread(DbThreadMetadata),
+    Thread(AgentSessionInfo),
+    RecentThread(AgentSessionInfo),
     Fetch(SharedString),
     Rules(RulesContextEntry),
     Entry(EntryMatch),
@@ -158,12 +160,12 @@ pub struct EntryMatch {
     entry: PromptContextEntry,
 }
 
-fn thread_title(thread: &DbThreadMetadata) -> SharedString {
-    if thread.title.is_empty() {
-        "New Thread".into()
-    } else {
-        thread.title.clone()
-    }
+fn session_title(session: &AgentSessionInfo) -> SharedString {
+    session
+        .title
+        .clone()
+        .filter(|title| !title.is_empty())
+        .unwrap_or_else(|| SharedString::new_static("New Thread"))
 }
 
 #[derive(Debug, Clone)]
@@ -194,7 +196,8 @@ pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> {
     source: Arc<T>,
     editor: WeakEntity<Editor>,
     mention_set: Entity<MentionSet>,
-    thread_store: Entity<ThreadStore>,
+    thread_store: Option<Entity<ThreadStore>>,
+    session_list: Rc<RefCell<Option<Rc<dyn AgentSessionList>>>>,
     prompt_store: Option<Entity<PromptStore>>,
     workspace: WeakEntity<Workspace>,
 }
@@ -204,7 +207,8 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
         source: T,
         editor: WeakEntity<Editor>,
         mention_set: Entity<MentionSet>,
-        thread_store: Entity<ThreadStore>,
+        thread_store: Option<Entity<ThreadStore>>,
+        session_list: Rc<RefCell<Option<Rc<dyn AgentSessionList>>>>,
         prompt_store: Option<Entity<PromptStore>>,
         workspace: WeakEntity<Workspace>,
     ) -> Self {
@@ -214,6 +218,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
             mention_set,
             workspace,
             thread_store,
+            session_list,
             prompt_store,
         }
     }
@@ -254,7 +259,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
     }
 
     fn completion_for_thread(
-        thread_entry: DbThreadMetadata,
+        thread_entry: AgentSessionInfo,
         source_range: Range<Anchor>,
         recent: bool,
         source: Arc<T>,
@@ -263,9 +268,9 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
         workspace: Entity<Workspace>,
         cx: &mut App,
     ) -> Completion {
-        let title = thread_title(&thread_entry);
+        let title = session_title(&thread_entry);
         let uri = MentionUri::Thread {
-            id: thread_entry.id,
+            id: thread_entry.session_id,
             name: title.to_string(),
         };
 
@@ -648,15 +653,29 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
             }
 
             Some(PromptContextType::Thread) => {
-                let search_threads_task =
-                    search_threads(query, cancellation_flag, &self.thread_store, cx);
-                cx.background_spawn(async move {
-                    search_threads_task
-                        .await
-                        .into_iter()
-                        .map(Match::Thread)
-                        .collect()
-                })
+                if let Some(session_list) = self.session_list.borrow().clone() {
+                    let search_sessions_task =
+                        search_sessions(query, cancellation_flag, session_list, 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()
+                    })
+                } else {
+                    Task::ready(Vec::new())
+                }
             }
 
             Some(PromptContextType::Fetch) => {
@@ -684,20 +703,23 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
             }
 
             None if query.is_empty() => {
-                let mut matches = self.recent_context_picker_entries(&workspace, cx);
-
-                matches.extend(
-                    self.available_context_picker_entries(&workspace, cx)
-                        .into_iter()
-                        .map(|mode| {
-                            Match::Entry(EntryMatch {
-                                entry: mode,
-                                mat: None,
-                            })
-                        }),
-                );
+                let recent_task = self.recent_context_picker_entries(&workspace, cx);
+                let entries = self
+                    .available_context_picker_entries(&workspace, cx)
+                    .into_iter()
+                    .map(|mode| {
+                        Match::Entry(EntryMatch {
+                            entry: mode,
+                            mat: None,
+                        })
+                    })
+                    .collect::<Vec<_>>();
 
-                Task::ready(matches)
+                cx.spawn(async move |_cx| {
+                    let mut matches = recent_task.await;
+                    matches.extend(entries);
+                    matches
+                })
             }
             None => {
                 let executor = cx.background_executor().clone();
@@ -753,7 +775,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
         &self,
         workspace: &Entity<Workspace>,
         cx: &mut App,
-    ) -> Vec<Match> {
+    ) -> Task<Vec<Match>> {
         let mut recent = Vec::with_capacity(6);
 
         let mut mentions = self
@@ -809,26 +831,61 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
                 }),
         );
 
-        if self.source.supports_context(PromptContextType::Thread, cx) {
-            const RECENT_COUNT: usize = 2;
-            let threads = self
-                .thread_store
-                .read(cx)
-                .entries()
-                .filter(|thread| {
-                    let uri = MentionUri::Thread {
-                        id: thread.id.clone(),
-                        name: thread_title(thread).to_string(),
-                    };
-                    !mentions.contains(&uri)
-                })
-                .take(RECENT_COUNT)
-                .collect::<Vec<_>>();
+        if !self.source.supports_context(PromptContextType::Thread, cx) {
+            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
+                    .into_iter()
+                    .filter(|session| {
+                        let uri = MentionUri::Thread {
+                            id: session.session_id.clone(),
+                            name: session_title(session).to_string(),
+                        };
+                        !mentions.contains(&uri)
+                    })
+                    .take(RECENT_COUNT)
+                    .collect::<Vec<_>>();
 
-            recent.extend(threads.into_iter().map(Match::RecentThread));
+                recent.extend(threads.into_iter().map(Match::RecentThread));
+                recent
+            });
         }
 
-        recent
+        let Some(thread_store) = self.thread_store.as_ref() else {
+            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)
     }
 
     fn available_context_picker_entries(
@@ -1548,37 +1605,84 @@ pub(crate) fn search_threads(
     cancellation_flag: Arc<AtomicBool>,
     thread_store: &Entity<ThreadStore>,
     cx: &mut App,
-) -> Task<Vec<DbThreadMetadata>> {
-    let threads = thread_store.read(cx).entries().collect();
+) -> 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(threads);
+        return Task::ready(sessions);
     }
 
     let executor = cx.background_executor().clone();
     cx.background_spawn(async move {
-        let candidates = threads
-            .iter()
-            .enumerate()
-            .map(|(id, thread)| StringMatchCandidate::new(id, thread_title(thread).as_ref()))
-            .collect::<Vec<_>>();
-        let matches = fuzzy::match_strings(
-            &candidates,
-            &query,
-            false,
-            true,
-            100,
-            &cancellation_flag,
-            executor,
-        )
-        .await;
+        filter_sessions(query, cancellation_flag, sessions, executor).await
+    })
+}
 
-        matches
-            .into_iter()
-            .map(|mat| threads[mat.candidate_id].clone())
-            .collect()
+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>,
+    sessions: Vec<AgentSessionInfo>,
+    executor: BackgroundExecutor,
+) -> Vec<AgentSessionInfo> {
+    let titles = sessions.iter().map(session_title).collect::<Vec<_>>();
+    let candidates = titles
+        .iter()
+        .enumerate()
+        .map(|(id, title)| StringMatchCandidate::new(id, title.as_ref()))
+        .collect::<Vec<_>>();
+    let matches = fuzzy::match_strings(
+        &candidates,
+        &query,
+        false,
+        true,
+        100,
+        &cancellation_flag,
+        executor,
+    )
+    .await;
+
+    matches
+        .into_iter()
+        .map(|mat| sessions[mat.candidate_id].clone())
+        .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>,
@@ -1703,6 +1807,9 @@ 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() {
@@ -1929,4 +2036,49 @@ mod tests {
             "Should not parse with a space after @ at the start of the line"
         );
     }
+
+    #[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
+            }
+        }
+
+        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 task = {
+            let mut app = cx.app.borrow_mut();
+            search_sessions(
+                "Alpha".into(),
+                Arc::new(AtomicBool::default()),
+                session_list,
+                &mut app,
+            )
+        };
+
+        let results = task.await;
+        assert_eq!(results.len(), 1);
+        assert_eq!(results[0].session_id, alpha.session_id);
+    }
 }

crates/agent_ui/src/inline_prompt_editor.rs 🔗

@@ -19,6 +19,7 @@ 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;
@@ -331,7 +332,8 @@ impl<T: 'static> PromptEditor<T> {
                 PromptEditorCompletionProviderDelegate,
                 cx.weak_entity(),
                 self.mention_set.clone(),
-                self.thread_store.clone(),
+                Some(self.thread_store.clone()),
+                Rc::new(RefCell::new(None)),
                 self.prompt_store.clone(),
                 self.workspace.clone(),
             ))));
@@ -1249,8 +1251,8 @@ impl PromptEditor<BufferCodegen> {
             editor
         });
 
-        let mention_set =
-            cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));
+        let mention_set = cx
+            .new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
 
         let model_selector_menu_handle = PopoverMenuHandle::default();
 
@@ -1402,8 +1404,8 @@ impl PromptEditor<TerminalCodegen> {
             editor
         });
 
-        let mention_set =
-            cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));
+        let mention_set = cx
+            .new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
 
         let model_selector_menu_handle = PopoverMenuHandle::default();
 

crates/agent_ui/src/mention_set.rs 🔗

@@ -60,7 +60,7 @@ pub struct MentionImage {
 
 pub struct MentionSet {
     project: WeakEntity<Project>,
-    thread_store: Entity<ThreadStore>,
+    thread_store: Option<Entity<ThreadStore>>,
     prompt_store: Option<Entity<PromptStore>>,
     mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
 }
@@ -68,7 +68,7 @@ pub struct MentionSet {
 impl MentionSet {
     pub fn new(
         project: WeakEntity<Project>,
-        thread_store: Entity<ThreadStore>,
+        thread_store: Option<Entity<ThreadStore>>,
         prompt_store: Option<Entity<PromptStore>>,
     ) -> Self {
         Self {
@@ -138,6 +138,11 @@ impl MentionSet {
         self.mentions.drain()
     }
 
+    #[cfg(test)]
+    pub fn has_thread_store(&self) -> bool {
+        self.thread_store.is_some()
+    }
+
     pub fn confirm_mention_completion(
         &mut self,
         crease_text: SharedString,
@@ -473,13 +478,18 @@ impl MentionSet {
         id: acp::SessionId,
         cx: &mut Context<Self>,
     ) -> Task<Result<Mention>> {
+        let Some(thread_store) = self.thread_store.clone() else {
+            return Task::ready(Err(anyhow!(
+                "Thread mentions are only supported for the native agent"
+            )));
+        };
         let Some(project) = self.project.upgrade() else {
             return Task::ready(Err(anyhow!("project not found")));
         };
 
         let server = Rc::new(agent::NativeAgentServer::new(
             project.read(cx).fs().clone(),
-            self.thread_store.clone(),
+            thread_store,
         ));
         let delegate = AgentServerDelegate::new(
             project.read(cx).agent_server_store().clone(),
@@ -503,6 +513,56 @@ impl MentionSet {
     }
 }
 
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use fs::FakeFs;
+    use gpui::TestAppContext;
+    use project::Project;
+    use prompt_store;
+    use release_channel;
+    use semver::Version;
+    use serde_json::json;
+    use settings::SettingsStore;
+    use std::path::Path;
+    use theme;
+    use util::path;
+
+    fn init_test(cx: &mut TestAppContext) {
+        let settings_store = cx.update(SettingsStore::test);
+        cx.set_global(settings_store);
+        cx.update(|cx| {
+            theme::init(theme::LoadThemes::JustBase, cx);
+            release_channel::init(Version::new(0, 0, 0), cx);
+            prompt_store::init(cx);
+        });
+    }
+
+    #[gpui::test]
+    async fn test_thread_mentions_disabled(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/project", json!({"file": ""})).await;
+        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+        let thread_store = None;
+        let mention_set = cx.new(|_cx| MentionSet::new(project.downgrade(), thread_store, None));
+
+        let task = mention_set.update(cx, |mention_set, cx| {
+            mention_set.confirm_mention_for_thread(acp::SessionId::new("thread-1"), cx)
+        });
+
+        let error = task.await.unwrap_err();
+        assert!(
+            error
+                .to_string()
+                .contains("Thread mentions are only supported for the native agent"),
+            "Unexpected error: {error:#}"
+        );
+    }
+}
+
 pub(crate) fn paste_images_as_context(
     editor: Entity<Editor>,
     mention_set: Entity<MentionSet>,

crates/agent_ui_v2/Cargo.toml 🔗

@@ -18,6 +18,7 @@ test-support = ["agent/test-support"]
 
 [dependencies]
 agent.workspace = true
+acp_thread.workspace = true
 agent-client-protocol.workspace = true
 agent_servers.workspace = true
 agent_settings.workspace = true
@@ -30,6 +31,7 @@ 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

crates/agent_ui_v2/src/agent_thread_pane.rs 🔗

@@ -1,4 +1,5 @@
-use agent::{DbThreadMetadata, NativeAgentServer, ThreadStore};
+use acp_thread::AgentSessionInfo;
+use agent::{NativeAgentServer, ThreadStore};
 use agent_client_protocol as acp;
 use agent_servers::AgentServer;
 use agent_settings::AgentSettings;
@@ -89,7 +90,7 @@ impl AgentThreadPane {
 
     pub fn open_thread(
         &mut self,
-        entry: DbThreadMetadata,
+        entry: AgentSessionInfo,
         fs: Arc<dyn Fs>,
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
@@ -98,7 +99,7 @@ impl AgentThreadPane {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let thread_id = entry.id.clone();
+        let thread_id = entry.session_id.clone();
         let resume_thread = Some(entry);
 
         let agent: Rc<dyn AgentServer> = Rc::new(NativeAgentServer::new(fs, thread_store.clone()));
@@ -110,7 +111,7 @@ impl AgentThreadPane {
                 None,
                 workspace,
                 project,
-                thread_store,
+                Some(thread_store),
                 prompt_store,
                 true,
                 window,

crates/agent_ui_v2/src/agents_panel.rs 🔗

@@ -1,4 +1,7 @@
-use agent::{DbThreadMetadata, ThreadStore};
+use acp_thread::AgentSessionInfo;
+use agent::{NativeAgentServer, ThreadStore};
+use agent_client_protocol as acp;
+use agent_servers::{AgentServer, AgentServerDelegate};
 use agent_settings::AgentSettings;
 use anyhow::Result;
 use db::kvp::KEY_VALUE_STORE;
@@ -61,6 +64,7 @@ pub struct AgentsPanel {
     prompt_store: Option<Entity<PromptStore>>,
     fs: Arc<dyn Fs>,
     width: Option<Pixels>,
+    pending_restore: Option<SerializedAgentThreadPane>,
     pending_serialization: Task<Option<()>>,
     _subscriptions: Vec<Subscription>,
 }
@@ -121,11 +125,45 @@ impl AgentsPanel {
         let focus_handle = cx.focus_handle();
 
         let thread_store = cx.new(|cx| ThreadStore::new(cx));
-        let history = cx.new(|cx| AcpThreadHistory::new(thread_store.clone(), window, cx));
+        let history = cx.new(|cx| AcpThreadHistory::new(None, window, cx));
+
+        let history_handle = history.clone();
+        let connect_project = project.clone();
+        let connect_thread_store = thread_store.clone();
+        let connect_fs = fs.clone();
+        cx.spawn(async move |_, cx| {
+            let connect_task = cx.update(|cx| {
+                let delegate = AgentServerDelegate::new(
+                    connect_project.read(cx).agent_server_store().clone(),
+                    connect_project.clone(),
+                    None,
+                    None,
+                );
+                let server = NativeAgentServer::new(connect_fs, connect_thread_store);
+                server.connect(None, delegate, cx)
+            });
+            let connection = match connect_task.await {
+                Ok((connection, _)) => connection,
+                Err(error) => {
+                    log::error!("Failed to connect native agent for history: {error:#}");
+                    return;
+                }
+            };
+
+            cx.update(|cx| {
+                if let Some(session_list) = connection.session_list(cx) {
+                    history_handle.update(cx, |history, cx| {
+                        history.set_session_list(Some(session_list), cx);
+                    });
+                }
+            });
+        })
+        .detach();
 
         let this = cx.weak_entity();
         let subscriptions = vec![
             cx.subscribe_in(&history, window, Self::handle_history_event),
+            cx.observe_in(&history, window, Self::handle_history_updated),
             cx.on_flags_ready(move |_, cx| {
                 this.update(cx, |_, cx| {
                     cx.notify();
@@ -144,6 +182,7 @@ impl AgentsPanel {
             prompt_store,
             fs,
             width: None,
+            pending_restore: None,
             pending_serialization: Task::ready(None),
             _subscriptions: subscriptions,
         }
@@ -159,15 +198,9 @@ impl AgentsPanel {
             return;
         };
 
-        let entry = self
-            .thread_store
-            .read(cx)
-            .entries()
-            .find(|e| match thread_id {
-                SerializedHistoryEntryId::AcpThread(id) => e.id.to_string() == *id,
-            });
-
-        if let Some(entry) = entry {
+        let SerializedHistoryEntryId::AcpThread(id) = thread_id;
+        let session_id = acp::SessionId::new(id.clone());
+        if let Some(entry) = self.history.read(cx).session_for_id(&session_id) {
             self.open_thread(
                 entry,
                 serialized_pane.expanded,
@@ -175,6 +208,8 @@ impl AgentsPanel {
                 window,
                 cx,
             );
+        } else {
+            self.pending_restore = Some(serialized_pane);
         }
     }
 
@@ -203,6 +238,15 @@ impl AgentsPanel {
         cx.notify();
     }
 
+    fn handle_history_updated(
+        &mut self,
+        _history: Entity<AcpThreadHistory>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.maybe_restore_pending(window, cx);
+    }
+
     fn handle_history_event(
         &mut self,
         _history: &Entity<AcpThreadHistory>,
@@ -217,15 +261,40 @@ impl AgentsPanel {
         }
     }
 
+    fn maybe_restore_pending(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        if self.agent_thread_pane.is_some() {
+            self.pending_restore = None;
+            return;
+        }
+
+        let Some(pending) = self.pending_restore.as_ref() else {
+            return;
+        };
+        let Some(thread_id) = &pending.thread_id else {
+            self.pending_restore = None;
+            return;
+        };
+
+        let SerializedHistoryEntryId::AcpThread(id) = thread_id;
+        let session_id = acp::SessionId::new(id.clone());
+        let Some(entry) = self.history.read(cx).session_for_id(&session_id) else {
+            return;
+        };
+
+        let pending = self.pending_restore.take().expect("pending restore");
+        self.open_thread(entry, pending.expanded, pending.width, window, cx);
+    }
+
     fn open_thread(
         &mut self,
-        entry: DbThreadMetadata,
+        entry: AgentSessionInfo,
         expanded: bool,
         width: Option<Pixels>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let entry_id = entry.id.clone();
+        let entry_id = entry.session_id.clone();
+        self.pending_restore = None;
 
         if let Some(existing_pane) = &self.agent_thread_pane {
             if existing_pane.read(cx).thread_id() == Some(entry_id) {

crates/agent_ui_v2/src/thread_history.rs 🔗

@@ -1,4 +1,5 @@
-use agent::{DbThreadMetadata, ThreadStore};
+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;
@@ -6,7 +7,7 @@ use gpui::{
     App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
     UniformListScrollHandle, Window, actions, uniform_list,
 };
-use std::{fmt::Display, ops::Range};
+use std::{fmt::Display, ops::Range, rc::Rc};
 use text::Bias;
 use time::{OffsetDateTime, UtcOffset};
 use ui::{
@@ -16,12 +17,12 @@ use ui::{
 
 const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread");
 
-fn thread_title(entry: &DbThreadMetadata) -> &SharedString {
-    if entry.title.is_empty() {
-        DEFAULT_TITLE
-    } else {
-        &entry.title
-    }
+fn thread_title(entry: &AgentSessionInfo) -> &SharedString {
+    entry
+        .title
+        .as_ref()
+        .filter(|title| !title.is_empty())
+        .unwrap_or(DEFAULT_TITLE)
 }
 
 actions!(
@@ -35,7 +36,8 @@ actions!(
 );
 
 pub struct AcpThreadHistory {
-    pub(crate) thread_store: Entity<ThreadStore>,
+    session_list: Option<Rc<dyn AgentSessionList>>,
+    sessions: Vec<AgentSessionInfo>,
     scroll_handle: UniformListScrollHandle,
     selected_index: usize,
     hovered_index: Option<usize>,
@@ -45,23 +47,24 @@ pub struct AcpThreadHistory {
     local_timezone: UtcOffset,
     confirming_delete_history: bool,
     _update_task: Task<()>,
+    _watch_task: Option<Task<()>>,
     _subscriptions: Vec<gpui::Subscription>,
 }
 
 enum ListItemType {
     BucketSeparator(TimeBucket),
     Entry {
-        entry: DbThreadMetadata,
+        entry: AgentSessionInfo,
         format: EntryTimeFormat,
     },
     SearchResult {
-        entry: DbThreadMetadata,
+        entry: AgentSessionInfo,
         positions: Vec<usize>,
     },
 }
 
 impl ListItemType {
-    fn history_entry(&self) -> Option<&DbThreadMetadata> {
+    fn history_entry(&self) -> Option<&AgentSessionInfo> {
         match self {
             ListItemType::Entry { entry, .. } => Some(entry),
             ListItemType::SearchResult { entry, .. } => Some(entry),
@@ -72,14 +75,14 @@ impl ListItemType {
 
 #[allow(dead_code)]
 pub enum ThreadHistoryEvent {
-    Open(DbThreadMetadata),
+    Open(AgentSessionInfo),
 }
 
 impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
 
 impl AcpThreadHistory {
     pub fn new(
-        thread_store: Entity<ThreadStore>,
+        session_list: Option<Rc<dyn AgentSessionList>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -100,14 +103,11 @@ impl AcpThreadHistory {
                 }
             });
 
-        let thread_store_subscription = cx.observe(&thread_store, |this, _, cx| {
-            this.update_visible_items(true, cx);
-        });
-
         let scroll_handle = UniformListScrollHandle::default();
 
         let mut this = Self {
-            thread_store,
+            session_list: None,
+            sessions: Vec::new(),
             scroll_handle,
             selected_index: 0,
             hovered_index: None,
@@ -119,17 +119,16 @@ impl AcpThreadHistory {
             .unwrap(),
             search_query: SharedString::default(),
             confirming_delete_history: false,
-            _subscriptions: vec![search_editor_subscription, thread_store_subscription],
+            _subscriptions: vec![search_editor_subscription],
             _update_task: Task::ready(()),
+            _watch_task: None,
         };
-        this.update_visible_items(false, cx);
+        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
-            .thread_store
-            .update(cx, |store, _| store.entries().collect());
+        let entries = self.sessions.clone();
         let new_list_items = if self.search_query.is_empty() {
             self.add_list_separators(entries, cx)
         } else {
@@ -150,7 +149,7 @@ impl AcpThreadHistory {
                         .position(|visible_entry| {
                             visible_entry
                                 .history_entry()
-                                .is_some_and(|entry| entry.id == history_entry.id)
+                                .is_some_and(|entry| entry.session_id == history_entry.session_id)
                         })
                         .unwrap_or(0)
                 } else {
@@ -165,9 +164,112 @@ impl AcpThreadHistory {
         });
     }
 
+    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<DbThreadMetadata>,
+        entries: Vec<AgentSessionInfo>,
         cx: &App,
     ) -> Task<Vec<ListItemType>> {
         cx.background_spawn(async move {
@@ -176,8 +278,13 @@ impl AcpThreadHistory {
             let today = Local::now().naive_local().date();
 
             for entry in entries.into_iter() {
-                let entry_date = entry.updated_at.with_timezone(&Local).naive_local().date();
-                let entry_bucket = TimeBucket::from_dates(today, entry_date);
+                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);
@@ -195,7 +302,7 @@ impl AcpThreadHistory {
 
     fn filter_search_results(
         &self,
-        entries: Vec<DbThreadMetadata>,
+        entries: Vec<AgentSessionInfo>,
         cx: &App,
     ) -> Task<Vec<ListItemType>> {
         let query = self.search_query.clone();
@@ -236,11 +343,11 @@ impl AcpThreadHistory {
         self.visible_items.is_empty() && !self.search_query.is_empty()
     }
 
-    fn selected_history_entry(&self) -> Option<&DbThreadMetadata> {
+    fn selected_history_entry(&self) -> Option<&AgentSessionInfo> {
         self.get_history_entry(self.selected_index)
     }
 
-    fn get_history_entry(&self, visible_items_ix: usize) -> Option<&DbThreadMetadata> {
+    fn get_history_entry(&self, visible_items_ix: usize) -> Option<&AgentSessionInfo> {
         self.visible_items.get(visible_items_ix)?.history_entry()
     }
 
@@ -339,17 +446,17 @@ impl AcpThreadHistory {
         let Some(entry) = self.get_history_entry(visible_item_ix) else {
             return;
         };
-
-        let task = self
-            .thread_store
-            .update(cx, |store, cx| store.delete_thread(entry.id.clone(), cx));
+        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>) {
-        self.thread_store.update(cx, |store, cx| {
-            store.delete_threads(cx).detach_and_log_err(cx)
-        });
+        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();
     }
@@ -406,7 +513,7 @@ impl AcpThreadHistory {
 
     fn render_history_entry(
         &self,
-        entry: &DbThreadMetadata,
+        entry: &AgentSessionInfo,
         format: EntryTimeFormat,
         ix: usize,
         highlight_positions: Vec<usize>,
@@ -414,23 +521,27 @@ impl AcpThreadHistory {
     ) -> AnyElement {
         let selected = ix == self.selected_index;
         let hovered = Some(ix) == self.hovered_index;
-        let timestamp = entry.updated_at.timestamp();
-
-        let display_text = match format {
-            EntryTimeFormat::DateAndTime => {
-                let entry_time = entry.updated_at;
+        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 => format.format_timestamp(timestamp, self.local_timezone),
+            (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 =
-            EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
+        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()
@@ -499,7 +610,7 @@ impl Focusable for AcpThreadHistory {
 
 impl Render for AcpThreadHistory {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let has_no_history = self.thread_store.read(cx).is_empty();
+        let has_no_history = self.is_empty();
 
         v_flex()
             .key_context("ThreadHistory")

crates/collab/Cargo.toml 🔗

@@ -112,7 +112,7 @@ notifications = { workspace = true, features = ["test-support"] }
 pretty_assertions.workspace = true
 project = { workspace = true, features = ["test-support"] }
 prompt_store.workspace = true
-recent_projects.workspace = true
+recent_projects = { workspace = true, features = ["test-support"] }
 release_channel.workspace = true
 remote = { workspace = true, features = ["test-support"] }
 remote_server.workspace = true

crates/zed/Cargo.toml 🔗

@@ -31,7 +31,6 @@ visual-tests = [
     "dep:image",
     "dep:semver",
     "dep:tempfile",
-    "dep:acp_thread",
     "dep:action_log",
     "dep:agent_servers",
     "workspace/test-support",
@@ -120,7 +119,7 @@ image = { workspace = true, optional = true }
 semver = { workspace = true, optional = true }
 tempfile = { workspace = true, optional = true }
 clock = { workspace = true, optional = true }
-acp_thread = { workspace = true, optional = true }
+acp_thread.workspace = true
 action_log = { workspace = true, optional = true }
 agent_servers = { workspace = true, optional = true }
 gpui_tokio.workspace = true

crates/zed/src/main.rs 🔗

@@ -877,10 +877,12 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
                         })
                         .await?;
 
-                    let thread_metadata = agent::DbThreadMetadata {
-                        id: session_id,
-                        title: format!("🔗 {}", response.title).into(),
-                        updated_at: chrono::Utc::now(),
+                    let thread_metadata = acp_thread::AgentSessionInfo {
+                        session_id,
+                        cwd: None,
+                        title: Some(format!("🔗 {}", response.title).into()),
+                        updated_at: Some(chrono::Utc::now()),
+                        meta: None,
                     };
 
                     workspace.update(cx, |workspace, window, cx| {