ACP history mentions (#36551)

Conrad Irwin created

- **TEMP**
- **Update @-mentions to use new history**

Closes #ISSUE

Release Notes:

- N/A

Change summary

Cargo.lock                                     |   1 
crates/acp_thread/Cargo.toml                   |   1 
crates/acp_thread/src/mention.rs               |   8 
crates/agent/src/thread.rs                     |  11 
crates/agent2/Cargo.toml                       |   4 
crates/agent2/src/agent.rs                     |  22 
crates/agent2/src/db.rs                        |  13 
crates/agent2/src/history_store.rs             |  41 +
crates/agent2/src/thread.rs                    |  77 ++
crates/agent_settings/src/agent_settings.rs    |   2 
crates/agent_ui/Cargo.toml                     |   2 
crates/agent_ui/src/acp/completion_provider.rs | 559 +++++++++++--------
crates/agent_ui/src/acp/entry_view_state.rs    |  30 
crates/agent_ui/src/acp/message_editor.rs      | 133 ++--
crates/agent_ui/src/acp/thread_view.rs         |  44 
crates/agent_ui/src/agent_panel.rs             |  23 
crates/assistant_context/src/context_store.rs  |   2 
17 files changed, 581 insertions(+), 392 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7,7 +7,6 @@ name = "acp_thread"
 version = "0.1.0"
 dependencies = [
  "action_log",
- "agent",
  "agent-client-protocol",
  "anyhow",
  "buffer_diff",

crates/acp_thread/Cargo.toml 🔗

@@ -18,7 +18,6 @@ test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"]
 [dependencies]
 action_log.workspace = true
 agent-client-protocol.workspace = true
-agent.workspace = true
 anyhow.workspace = true
 buffer_diff.workspace = true
 collections.workspace = true

crates/acp_thread/src/mention.rs 🔗

@@ -1,4 +1,4 @@
-use agent::ThreadId;
+use agent_client_protocol as acp;
 use anyhow::{Context as _, Result, bail};
 use file_icons::FileIcons;
 use prompt_store::{PromptId, UserPromptId};
@@ -12,7 +12,7 @@ use std::{
 use ui::{App, IconName, SharedString};
 use url::Url;
 
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
 pub enum MentionUri {
     File {
         abs_path: PathBuf,
@@ -26,7 +26,7 @@ pub enum MentionUri {
         line_range: Range<u32>,
     },
     Thread {
-        id: ThreadId,
+        id: acp::SessionId,
         name: String,
     },
     TextThread {
@@ -89,7 +89,7 @@ impl MentionUri {
                 if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
                     let name = single_query_param(&url, "name")?.context("Missing thread name")?;
                     Ok(Self::Thread {
-                        id: thread_id.into(),
+                        id: acp::SessionId(thread_id.into()),
                         name,
                     })
                 } else if let Some(path) = path.strip_prefix("/agent/text-thread/") {

crates/agent/src/thread.rs 🔗

@@ -9,7 +9,10 @@ use crate::{
     tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState},
 };
 use action_log::ActionLog;
-use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT};
+use agent_settings::{
+    AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT,
+    SUMMARIZE_THREAD_PROMPT,
+};
 use anyhow::{Result, anyhow};
 use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet};
 use chrono::{DateTime, Utc};
@@ -107,7 +110,7 @@ impl std::fmt::Display for PromptId {
 }
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
-pub struct MessageId(pub(crate) usize);
+pub struct MessageId(pub usize);
 
 impl MessageId {
     fn post_inc(&mut self) -> Self {
@@ -2425,12 +2428,10 @@ impl Thread {
             return;
         }
 
-        let added_user_message = include_str!("./prompts/summarize_thread_detailed_prompt.txt");
-
         let request = self.to_summarize_request(
             &model,
             CompletionIntent::ThreadContextSummarization,
-            added_user_message.into(),
+            SUMMARIZE_THREAD_DETAILED_PROMPT.into(),
             cx,
         );
 

crates/agent2/Cargo.toml 🔗

@@ -8,6 +8,9 @@ license = "GPL-3.0-or-later"
 [lib]
 path = "src/agent2.rs"
 
+[features]
+test-support = ["db/test-support"]
+
 [lints]
 workspace = true
 
@@ -72,6 +75,7 @@ ctor.workspace = true
 client = { workspace = true, "features" = ["test-support"] }
 clock = { workspace = true, "features" = ["test-support"] }
 context_server = { workspace = true, "features" = ["test-support"] }
+db = { workspace = true, "features" = ["test-support"] }
 editor = { workspace = true, "features" = ["test-support"] }
 env_logger.workspace = true
 fs = { workspace = true, "features" = ["test-support"] }

crates/agent2/src/agent.rs 🔗

@@ -536,6 +536,28 @@ impl NativeAgent {
         })
     }
 
+    pub fn thread_summary(
+        &mut self,
+        id: acp::SessionId,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<SharedString>> {
+        let thread = self.open_thread(id.clone(), cx);
+        cx.spawn(async move |this, cx| {
+            let acp_thread = thread.await?;
+            let result = this
+                .update(cx, |this, cx| {
+                    this.sessions
+                        .get(&id)
+                        .unwrap()
+                        .thread
+                        .update(cx, |thread, cx| thread.summary(cx))
+                })?
+                .await?;
+            drop(acp_thread);
+            Ok(result)
+        })
+    }
+
     fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
         let database_future = ThreadsDatabase::connect(cx);
         let (id, db_thread) =

crates/agent2/src/db.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent};
 use acp_thread::UserMessageId;
-use agent::thread_store;
+use agent::{thread::DetailedSummaryState, thread_store};
 use agent_client_protocol as acp;
 use agent_settings::{AgentProfileId, CompletionMode};
 use anyhow::{Result, anyhow};
@@ -20,7 +20,7 @@ use std::sync::Arc;
 use ui::{App, SharedString};
 
 pub type DbMessage = crate::Message;
-pub type DbSummary = agent::thread::DetailedSummaryState;
+pub type DbSummary = DetailedSummaryState;
 pub type DbLanguageModel = thread_store::SerializedLanguageModel;
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -37,7 +37,7 @@ pub struct DbThread {
     pub messages: Vec<DbMessage>,
     pub updated_at: DateTime<Utc>,
     #[serde(default)]
-    pub summary: DbSummary,
+    pub detailed_summary: Option<SharedString>,
     #[serde(default)]
     pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>,
     #[serde(default)]
@@ -185,7 +185,12 @@ impl DbThread {
             title: thread.summary,
             messages,
             updated_at: thread.updated_at,
-            summary: thread.detailed_summary_state,
+            detailed_summary: match thread.detailed_summary_state {
+                DetailedSummaryState::NotGenerated | DetailedSummaryState::Generating { .. } => {
+                    None
+                }
+                DetailedSummaryState::Generated { text, .. } => Some(text),
+            },
             initial_project_snapshot: thread.initial_project_snapshot,
             cumulative_token_usage: thread.cumulative_token_usage,
             request_token_usage,

crates/agent2/src/history_store.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{DbThreadMetadata, ThreadsDatabase};
+use acp_thread::MentionUri;
 use agent_client_protocol as acp;
 use anyhow::{Context as _, Result, anyhow};
-use assistant_context::SavedContextMetadata;
+use assistant_context::{AssistantContext, SavedContextMetadata};
 use chrono::{DateTime, Utc};
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*};
@@ -38,6 +39,19 @@ impl HistoryEntry {
         }
     }
 
+    pub fn mention_uri(&self) -> MentionUri {
+        match self {
+            HistoryEntry::AcpThread(thread) => MentionUri::Thread {
+                id: thread.id.clone(),
+                name: thread.title.to_string(),
+            },
+            HistoryEntry::TextThread(context) => MentionUri::TextThread {
+                path: context.path.as_ref().to_owned(),
+                name: context.title.to_string(),
+            },
+        }
+    }
+
     pub fn title(&self) -> &SharedString {
         match self {
             HistoryEntry::AcpThread(thread) if thread.title.is_empty() => DEFAULT_TITLE,
@@ -48,7 +62,7 @@ impl HistoryEntry {
 }
 
 /// Generic identifier for a history entry.
-#[derive(Clone, PartialEq, Eq, Debug)]
+#[derive(Clone, PartialEq, Eq, Debug, Hash)]
 pub enum HistoryEntryId {
     AcpThread(acp::SessionId),
     TextThread(Arc<Path>),
@@ -120,6 +134,16 @@ impl HistoryStore {
         })
     }
 
+    pub fn load_text_thread(
+        &self,
+        path: Arc<Path>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Entity<AssistantContext>>> {
+        self.context_store.update(cx, |context_store, cx| {
+            context_store.open_local_context(path, cx)
+        })
+    }
+
     pub fn reload(&self, cx: &mut Context<Self>) {
         let database_future = ThreadsDatabase::connect(cx);
         cx.spawn(async move |this, cx| {
@@ -149,7 +173,7 @@ impl HistoryStore {
         .detach_and_log_err(cx);
     }
 
-    pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
+    pub fn entries(&self, cx: &App) -> Vec<HistoryEntry> {
         let mut history_entries = Vec::new();
 
         #[cfg(debug_assertions)]
@@ -180,10 +204,6 @@ impl HistoryStore {
                 .is_none()
     }
 
-    pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
-        self.entries(cx).into_iter().take(limit).collect()
-    }
-
     pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
         #[cfg(debug_assertions)]
         if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
@@ -246,6 +266,10 @@ impl HistoryStore {
             cx.background_executor()
                 .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
                 .await;
+
+            if cfg!(any(feature = "test-support", test)) {
+                return;
+            }
             KEY_VALUE_STORE
                 .write_kvp(RECENTLY_OPENED_THREADS_KEY.to_owned(), content)
                 .await
@@ -255,6 +279,9 @@ impl HistoryStore {
 
     fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
         cx.background_spawn(async move {
+            if cfg!(any(feature = "test-support", test)) {
+                anyhow::bail!("history store does not persist in tests");
+            }
             let json = KEY_VALUE_STORE
                 .read_kvp(RECENTLY_OPENED_THREADS_KEY)?
                 .unwrap_or("[]".to_string());

crates/agent2/src/thread.rs 🔗

@@ -6,9 +6,12 @@ use crate::{
 };
 use acp_thread::{MentionUri, UserMessageId};
 use action_log::ActionLog;
-use agent::thread::{DetailedSummaryState, GitState, ProjectSnapshot, WorktreeSnapshot};
+use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot};
 use agent_client_protocol as acp;
-use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT};
+use agent_settings::{
+    AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT,
+    SUMMARIZE_THREAD_PROMPT,
+};
 use anyhow::{Context as _, Result, anyhow};
 use assistant_tool::adapt_schema_to_format;
 use chrono::{DateTime, Utc};
@@ -499,8 +502,7 @@ pub struct Thread {
     prompt_id: PromptId,
     updated_at: DateTime<Utc>,
     title: Option<SharedString>,
-    #[allow(unused)]
-    summary: DetailedSummaryState,
+    summary: Option<SharedString>,
     messages: Vec<Message>,
     completion_mode: CompletionMode,
     /// Holds the task that handles agent interaction until the end of the turn.
@@ -541,7 +543,7 @@ impl Thread {
             prompt_id: PromptId::new(),
             updated_at: Utc::now(),
             title: None,
-            summary: DetailedSummaryState::default(),
+            summary: None,
             messages: Vec::new(),
             completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
             running_turn: None,
@@ -691,7 +693,7 @@ impl Thread {
             } else {
                 Some(db_thread.title.clone())
             },
-            summary: db_thread.summary,
+            summary: db_thread.detailed_summary,
             messages: db_thread.messages,
             completion_mode: db_thread.completion_mode.unwrap_or_default(),
             running_turn: None,
@@ -719,7 +721,7 @@ impl Thread {
             title: self.title.clone().unwrap_or_default(),
             messages: self.messages.clone(),
             updated_at: self.updated_at,
-            summary: self.summary.clone(),
+            detailed_summary: self.summary.clone(),
             initial_project_snapshot: None,
             cumulative_token_usage: self.cumulative_token_usage,
             request_token_usage: self.request_token_usage.clone(),
@@ -976,7 +978,7 @@ impl Thread {
                 Message::Agent(_) | Message::Resume => {}
             }
         }
-
+        self.summary = None;
         cx.notify();
         Ok(())
     }
@@ -1047,6 +1049,7 @@ impl Thread {
         let event_stream = ThreadEventStream(events_tx);
         let message_ix = self.messages.len().saturating_sub(1);
         self.tool_use_limit_reached = false;
+        self.summary = None;
         self.running_turn = Some(RunningTurn {
             event_stream: event_stream.clone(),
             _task: cx.spawn(async move |this, cx| {
@@ -1507,6 +1510,63 @@ impl Thread {
         self.title.clone().unwrap_or("New Thread".into())
     }
 
+    pub fn summary(&mut self, cx: &mut Context<Self>) -> Task<Result<SharedString>> {
+        if let Some(summary) = self.summary.as_ref() {
+            return Task::ready(Ok(summary.clone()));
+        }
+        let Some(model) = self.summarization_model.clone() else {
+            return Task::ready(Err(anyhow!("No summarization model available")));
+        };
+        let mut request = LanguageModelRequest {
+            intent: Some(CompletionIntent::ThreadSummarization),
+            temperature: AgentSettings::temperature_for_model(&model, cx),
+            ..Default::default()
+        };
+
+        for message in &self.messages {
+            request.messages.extend(message.to_request());
+        }
+
+        request.messages.push(LanguageModelRequestMessage {
+            role: Role::User,
+            content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
+            cache: false,
+        });
+        cx.spawn(async move |this, cx| {
+            let mut summary = String::new();
+            let mut messages = model.stream_completion(request, cx).await?;
+            while let Some(event) = messages.next().await {
+                let event = event?;
+                let text = match event {
+                    LanguageModelCompletionEvent::Text(text) => text,
+                    LanguageModelCompletionEvent::StatusUpdate(
+                        CompletionRequestStatus::UsageUpdated { .. },
+                    ) => {
+                        // this.update(cx, |thread, cx| {
+                        //     thread.update_model_request_usage(amount as u32, limit, cx);
+                        // })?;
+                        // TODO: handle usage update
+                        continue;
+                    }
+                    _ => continue,
+                };
+
+                let mut lines = text.lines();
+                summary.extend(lines.next());
+            }
+
+            log::info!("Setting summary: {}", summary);
+            let summary = SharedString::from(summary);
+
+            this.update(cx, |this, cx| {
+                this.summary = Some(summary.clone());
+                cx.notify()
+            })?;
+
+            Ok(summary)
+        })
+    }
+
     fn update_title(
         &mut self,
         event_stream: &ThreadEventStream,
@@ -1617,6 +1677,7 @@ impl Thread {
 
         self.messages.push(Message::Agent(message));
         self.updated_at = Utc::now();
+        self.summary = None;
         cx.notify()
     }
 

crates/agent_settings/src/agent_settings.rs 🔗

@@ -15,6 +15,8 @@ pub use crate::agent_profile::*;
 
 pub const SUMMARIZE_THREAD_PROMPT: &str =
     include_str!("../../agent/src/prompts/summarize_thread_prompt.txt");
+pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str =
+    include_str!("../../agent/src/prompts/summarize_thread_detailed_prompt.txt");
 
 pub fn init(cx: &mut App) {
     AgentSettings::register(cx);

crates/agent_ui/Cargo.toml 🔗

@@ -104,9 +104,11 @@ zed_actions.workspace = true
 [dev-dependencies]
 acp_thread = { workspace = true, features = ["test-support"] }
 agent = { workspace = true, features = ["test-support"] }
+agent2 = { workspace = true, features = ["test-support"] }
 assistant_context = { workspace = true, features = ["test-support"] }
 assistant_tools.workspace = true
 buffer_diff = { workspace = true, features = ["test-support"] }
+db = { workspace = true, features = ["test-support"] }
 editor = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, "features" = ["test-support"] }
 indoc.workspace = true

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

@@ -3,6 +3,7 @@ use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
 use acp_thread::MentionUri;
+use agent2::{HistoryEntry, HistoryStore};
 use anyhow::Result;
 use editor::{CompletionProvider, Editor, ExcerptId};
 use fuzzy::{StringMatch, StringMatchCandidate};
@@ -18,25 +19,21 @@ use text::{Anchor, ToPoint as _};
 use ui::prelude::*;
 use workspace::Workspace;
 
-use agent::thread_store::{TextThreadStore, ThreadStore};
-
+use crate::AgentPanel;
 use crate::acp::message_editor::MessageEditor;
 use crate::context_picker::file_context_picker::{FileMatch, search_files};
 use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
 use crate::context_picker::symbol_context_picker::SymbolMatch;
 use crate::context_picker::symbol_context_picker::search_symbols;
-use crate::context_picker::thread_context_picker::{
-    ThreadContextEntry, ThreadMatch, search_threads,
-};
 use crate::context_picker::{
-    ContextPickerAction, ContextPickerEntry, ContextPickerMode, RecentEntry,
-    available_context_picker_entries, recent_context_picker_entries, selection_ranges,
+    ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
 };
 
 pub(crate) enum Match {
     File(FileMatch),
     Symbol(SymbolMatch),
-    Thread(ThreadMatch),
+    Thread(HistoryEntry),
+    RecentThread(HistoryEntry),
     Fetch(SharedString),
     Rules(RulesContextEntry),
     Entry(EntryMatch),
@@ -53,6 +50,7 @@ impl Match {
             Match::File(file) => file.mat.score,
             Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
             Match::Thread(_) => 1.,
+            Match::RecentThread(_) => 1.,
             Match::Symbol(_) => 1.,
             Match::Rules(_) => 1.,
             Match::Fetch(_) => 1.,
@@ -60,209 +58,25 @@ impl Match {
     }
 }
 
-fn search(
-    mode: Option<ContextPickerMode>,
-    query: String,
-    cancellation_flag: Arc<AtomicBool>,
-    recent_entries: Vec<RecentEntry>,
-    prompt_store: Option<Entity<PromptStore>>,
-    thread_store: WeakEntity<ThreadStore>,
-    text_thread_context_store: WeakEntity<assistant_context::ContextStore>,
-    workspace: Entity<Workspace>,
-    cx: &mut App,
-) -> Task<Vec<Match>> {
-    match mode {
-        Some(ContextPickerMode::File) => {
-            let search_files_task =
-                search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
-            cx.background_spawn(async move {
-                search_files_task
-                    .await
-                    .into_iter()
-                    .map(Match::File)
-                    .collect()
-            })
-        }
-
-        Some(ContextPickerMode::Symbol) => {
-            let search_symbols_task =
-                search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
-            cx.background_spawn(async move {
-                search_symbols_task
-                    .await
-                    .into_iter()
-                    .map(Match::Symbol)
-                    .collect()
-            })
-        }
-
-        Some(ContextPickerMode::Thread) => {
-            if let Some((thread_store, context_store)) = thread_store
-                .upgrade()
-                .zip(text_thread_context_store.upgrade())
-            {
-                let search_threads_task = search_threads(
-                    query.clone(),
-                    cancellation_flag.clone(),
-                    thread_store,
-                    context_store,
-                    cx,
-                );
-                cx.background_spawn(async move {
-                    search_threads_task
-                        .await
-                        .into_iter()
-                        .map(Match::Thread)
-                        .collect()
-                })
-            } else {
-                Task::ready(Vec::new())
-            }
-        }
-
-        Some(ContextPickerMode::Fetch) => {
-            if !query.is_empty() {
-                Task::ready(vec![Match::Fetch(query.into())])
-            } else {
-                Task::ready(Vec::new())
-            }
-        }
-
-        Some(ContextPickerMode::Rules) => {
-            if let Some(prompt_store) = prompt_store.as_ref() {
-                let search_rules_task =
-                    search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
-                cx.background_spawn(async move {
-                    search_rules_task
-                        .await
-                        .into_iter()
-                        .map(Match::Rules)
-                        .collect::<Vec<_>>()
-                })
-            } else {
-                Task::ready(Vec::new())
-            }
-        }
-
-        None => {
-            if query.is_empty() {
-                let mut matches = recent_entries
-                    .into_iter()
-                    .map(|entry| match entry {
-                        RecentEntry::File {
-                            project_path,
-                            path_prefix,
-                        } => Match::File(FileMatch {
-                            mat: fuzzy::PathMatch {
-                                score: 1.,
-                                positions: Vec::new(),
-                                worktree_id: project_path.worktree_id.to_usize(),
-                                path: project_path.path,
-                                path_prefix,
-                                is_dir: false,
-                                distance_to_relative_ancestor: 0,
-                            },
-                            is_recent: true,
-                        }),
-                        RecentEntry::Thread(thread_context_entry) => Match::Thread(ThreadMatch {
-                            thread: thread_context_entry,
-                            is_recent: true,
-                        }),
-                    })
-                    .collect::<Vec<_>>();
-
-                matches.extend(
-                    available_context_picker_entries(
-                        &prompt_store,
-                        &Some(thread_store.clone()),
-                        &workspace,
-                        cx,
-                    )
-                    .into_iter()
-                    .map(|mode| {
-                        Match::Entry(EntryMatch {
-                            entry: mode,
-                            mat: None,
-                        })
-                    }),
-                );
-
-                Task::ready(matches)
-            } else {
-                let executor = cx.background_executor().clone();
-
-                let search_files_task =
-                    search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
-
-                let entries = available_context_picker_entries(
-                    &prompt_store,
-                    &Some(thread_store.clone()),
-                    &workspace,
-                    cx,
-                );
-                let entry_candidates = entries
-                    .iter()
-                    .enumerate()
-                    .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
-                    .collect::<Vec<_>>();
-
-                cx.background_spawn(async move {
-                    let mut matches = search_files_task
-                        .await
-                        .into_iter()
-                        .map(Match::File)
-                        .collect::<Vec<_>>();
-
-                    let entry_matches = fuzzy::match_strings(
-                        &entry_candidates,
-                        &query,
-                        false,
-                        true,
-                        100,
-                        &Arc::new(AtomicBool::default()),
-                        executor,
-                    )
-                    .await;
-
-                    matches.extend(entry_matches.into_iter().map(|mat| {
-                        Match::Entry(EntryMatch {
-                            entry: entries[mat.candidate_id],
-                            mat: Some(mat),
-                        })
-                    }));
-
-                    matches.sort_by(|a, b| {
-                        b.score()
-                            .partial_cmp(&a.score())
-                            .unwrap_or(std::cmp::Ordering::Equal)
-                    });
-
-                    matches
-                })
-            }
-        }
-    }
-}
-
 pub struct ContextPickerCompletionProvider {
-    workspace: WeakEntity<Workspace>,
-    thread_store: WeakEntity<ThreadStore>,
-    text_thread_store: WeakEntity<TextThreadStore>,
     message_editor: WeakEntity<MessageEditor>,
+    workspace: WeakEntity<Workspace>,
+    history_store: Entity<HistoryStore>,
+    prompt_store: Option<Entity<PromptStore>>,
 }
 
 impl ContextPickerCompletionProvider {
     pub fn new(
-        workspace: WeakEntity<Workspace>,
-        thread_store: WeakEntity<ThreadStore>,
-        text_thread_store: WeakEntity<TextThreadStore>,
         message_editor: WeakEntity<MessageEditor>,
+        workspace: WeakEntity<Workspace>,
+        history_store: Entity<HistoryStore>,
+        prompt_store: Option<Entity<PromptStore>>,
     ) -> Self {
         Self {
-            workspace,
-            thread_store,
-            text_thread_store,
             message_editor,
+            workspace,
+            history_store,
+            prompt_store,
         }
     }
 
@@ -349,22 +163,13 @@ impl ContextPickerCompletionProvider {
     }
 
     fn completion_for_thread(
-        thread_entry: ThreadContextEntry,
+        thread_entry: HistoryEntry,
         source_range: Range<Anchor>,
         recent: bool,
         editor: WeakEntity<MessageEditor>,
         cx: &mut App,
     ) -> Completion {
-        let uri = match &thread_entry {
-            ThreadContextEntry::Thread { id, title } => MentionUri::Thread {
-                id: id.clone(),
-                name: title.to_string(),
-            },
-            ThreadContextEntry::Context { path, title } => MentionUri::TextThread {
-                path: path.to_path_buf(),
-                name: title.to_string(),
-            },
-        };
+        let uri = thread_entry.mention_uri();
 
         let icon_for_completion = if recent {
             IconName::HistoryRerun.path().into()
@@ -547,6 +352,251 @@ impl ContextPickerCompletionProvider {
             )),
         })
     }
+
+    fn search(
+        &self,
+        mode: Option<ContextPickerMode>,
+        query: String,
+        cancellation_flag: Arc<AtomicBool>,
+        cx: &mut App,
+    ) -> Task<Vec<Match>> {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return Task::ready(Vec::default());
+        };
+        match mode {
+            Some(ContextPickerMode::File) => {
+                let search_files_task =
+                    search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
+                cx.background_spawn(async move {
+                    search_files_task
+                        .await
+                        .into_iter()
+                        .map(Match::File)
+                        .collect()
+                })
+            }
+
+            Some(ContextPickerMode::Symbol) => {
+                let search_symbols_task =
+                    search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
+                cx.background_spawn(async move {
+                    search_symbols_task
+                        .await
+                        .into_iter()
+                        .map(Match::Symbol)
+                        .collect()
+                })
+            }
+
+            Some(ContextPickerMode::Thread) => {
+                let search_threads_task = search_threads(
+                    query.clone(),
+                    cancellation_flag.clone(),
+                    &self.history_store,
+                    cx,
+                );
+                cx.background_spawn(async move {
+                    search_threads_task
+                        .await
+                        .into_iter()
+                        .map(Match::Thread)
+                        .collect()
+                })
+            }
+
+            Some(ContextPickerMode::Fetch) => {
+                if !query.is_empty() {
+                    Task::ready(vec![Match::Fetch(query.into())])
+                } else {
+                    Task::ready(Vec::new())
+                }
+            }
+
+            Some(ContextPickerMode::Rules) => {
+                if let Some(prompt_store) = self.prompt_store.as_ref() {
+                    let search_rules_task =
+                        search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
+                    cx.background_spawn(async move {
+                        search_rules_task
+                            .await
+                            .into_iter()
+                            .map(Match::Rules)
+                            .collect::<Vec<_>>()
+                    })
+                } else {
+                    Task::ready(Vec::new())
+                }
+            }
+
+            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,
+                            })
+                        }),
+                );
+
+                Task::ready(matches)
+            }
+            None => {
+                let executor = cx.background_executor().clone();
+
+                let search_files_task =
+                    search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
+
+                let entries = self.available_context_picker_entries(&workspace, cx);
+                let entry_candidates = entries
+                    .iter()
+                    .enumerate()
+                    .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
+                    .collect::<Vec<_>>();
+
+                cx.background_spawn(async move {
+                    let mut matches = search_files_task
+                        .await
+                        .into_iter()
+                        .map(Match::File)
+                        .collect::<Vec<_>>();
+
+                    let entry_matches = fuzzy::match_strings(
+                        &entry_candidates,
+                        &query,
+                        false,
+                        true,
+                        100,
+                        &Arc::new(AtomicBool::default()),
+                        executor,
+                    )
+                    .await;
+
+                    matches.extend(entry_matches.into_iter().map(|mat| {
+                        Match::Entry(EntryMatch {
+                            entry: entries[mat.candidate_id],
+                            mat: Some(mat),
+                        })
+                    }));
+
+                    matches.sort_by(|a, b| {
+                        b.score()
+                            .partial_cmp(&a.score())
+                            .unwrap_or(std::cmp::Ordering::Equal)
+                    });
+
+                    matches
+                })
+            }
+        }
+    }
+
+    fn recent_context_picker_entries(
+        &self,
+        workspace: &Entity<Workspace>,
+        cx: &mut App,
+    ) -> Vec<Match> {
+        let mut recent = Vec::with_capacity(6);
+
+        let mut mentions = self
+            .message_editor
+            .read_with(cx, |message_editor, _cx| message_editor.mentions())
+            .unwrap_or_default();
+        let workspace = workspace.read(cx);
+        let project = workspace.project().read(cx);
+
+        if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx)
+            && let Some(thread) = agent_panel.read(cx).active_agent_thread(cx)
+        {
+            let thread = thread.read(cx);
+            mentions.insert(MentionUri::Thread {
+                id: thread.session_id().clone(),
+                name: thread.title().into(),
+            });
+        }
+
+        recent.extend(
+            workspace
+                .recent_navigation_history_iter(cx)
+                .filter(|(_, abs_path)| {
+                    abs_path.as_ref().is_none_or(|path| {
+                        !mentions.contains(&MentionUri::File {
+                            abs_path: path.clone(),
+                        })
+                    })
+                })
+                .take(4)
+                .filter_map(|(project_path, _)| {
+                    project
+                        .worktree_for_id(project_path.worktree_id, cx)
+                        .map(|worktree| {
+                            let path_prefix = worktree.read(cx).root_name().into();
+                            Match::File(FileMatch {
+                                mat: fuzzy::PathMatch {
+                                    score: 1.,
+                                    positions: Vec::new(),
+                                    worktree_id: project_path.worktree_id.to_usize(),
+                                    path: project_path.path,
+                                    path_prefix,
+                                    is_dir: false,
+                                    distance_to_relative_ancestor: 0,
+                                },
+                                is_recent: true,
+                            })
+                        })
+                }),
+        );
+
+        const RECENT_COUNT: usize = 2;
+        let threads = self
+            .history_store
+            .read(cx)
+            .recently_opened_entries(cx)
+            .into_iter()
+            .filter(|thread| !mentions.contains(&thread.mention_uri()))
+            .take(RECENT_COUNT)
+            .collect::<Vec<_>>();
+
+        recent.extend(threads.into_iter().map(Match::RecentThread));
+
+        recent
+    }
+
+    fn available_context_picker_entries(
+        &self,
+        workspace: &Entity<Workspace>,
+        cx: &mut App,
+    ) -> Vec<ContextPickerEntry> {
+        let mut entries = vec![
+            ContextPickerEntry::Mode(ContextPickerMode::File),
+            ContextPickerEntry::Mode(ContextPickerMode::Symbol),
+            ContextPickerEntry::Mode(ContextPickerMode::Thread),
+        ];
+
+        let has_selection = workspace
+            .read(cx)
+            .active_item(cx)
+            .and_then(|item| item.downcast::<Editor>())
+            .is_some_and(|editor| {
+                editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
+            });
+        if has_selection {
+            entries.push(ContextPickerEntry::Action(
+                ContextPickerAction::AddSelections,
+            ));
+        }
+
+        if self.prompt_store.is_some() {
+            entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
+        }
+
+        entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
+
+        entries
+    }
 }
 
 fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
@@ -596,45 +646,12 @@ impl CompletionProvider for ContextPickerCompletionProvider {
         let source_range = snapshot.anchor_before(state.source_range.start)
             ..snapshot.anchor_after(state.source_range.end);
 
-        let thread_store = self.thread_store.clone();
-        let text_thread_store = self.text_thread_store.clone();
         let editor = self.message_editor.clone();
-        let Ok((exclude_paths, exclude_threads)) =
-            self.message_editor.update(cx, |message_editor, _cx| {
-                message_editor.mentioned_path_and_threads()
-            })
-        else {
-            return Task::ready(Ok(Vec::new()));
-        };
 
         let MentionCompletion { mode, argument, .. } = state;
         let query = argument.unwrap_or_else(|| "".to_string());
 
-        let recent_entries = recent_context_picker_entries(
-            Some(thread_store.clone()),
-            Some(text_thread_store.clone()),
-            workspace.clone(),
-            &exclude_paths,
-            &exclude_threads,
-            cx,
-        );
-
-        let prompt_store = thread_store
-            .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
-            .ok()
-            .flatten();
-
-        let search_task = search(
-            mode,
-            query,
-            Arc::<AtomicBool>::default(),
-            recent_entries,
-            prompt_store,
-            thread_store.clone(),
-            text_thread_store.clone(),
-            workspace.clone(),
-            cx,
-        );
+        let search_task = self.search(mode, query, Arc::<AtomicBool>::default(), cx);
 
         cx.spawn(async move |_, cx| {
             let matches = search_task.await;
@@ -669,12 +686,18 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                             cx,
                         ),
 
-                        Match::Thread(ThreadMatch {
-                            thread, is_recent, ..
-                        }) => Some(Self::completion_for_thread(
+                        Match::Thread(thread) => Some(Self::completion_for_thread(
+                            thread,
+                            source_range.clone(),
+                            false,
+                            editor.clone(),
+                            cx,
+                        )),
+
+                        Match::RecentThread(thread) => Some(Self::completion_for_thread(
                             thread,
                             source_range.clone(),
-                            is_recent,
+                            true,
                             editor.clone(),
                             cx,
                         )),
@@ -748,6 +771,42 @@ impl CompletionProvider for ContextPickerCompletionProvider {
     }
 }
 
+pub(crate) fn search_threads(
+    query: String,
+    cancellation_flag: Arc<AtomicBool>,
+    history_store: &Entity<HistoryStore>,
+    cx: &mut App,
+) -> Task<Vec<HistoryEntry>> {
+    let threads = history_store.read(cx).entries(cx);
+    if query.is_empty() {
+        return Task::ready(threads);
+    }
+
+    let executor = cx.background_executor().clone();
+    cx.background_spawn(async move {
+        let candidates = threads
+            .iter()
+            .enumerate()
+            .map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
+            .collect::<Vec<_>>();
+        let matches = fuzzy::match_strings(
+            &candidates,
+            &query,
+            false,
+            true,
+            100,
+            &cancellation_flag,
+            executor,
+        )
+        .await;
+
+        matches
+            .into_iter()
+            .map(|mat| threads[mat.candidate_id].clone())
+            .collect()
+    })
+}
+
 fn confirm_completion_callback(
     crease_text: SharedString,
     start: Anchor,

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

@@ -1,7 +1,7 @@
 use std::ops::Range;
 
 use acp_thread::{AcpThread, AgentThreadEntry};
-use agent::{TextThreadStore, ThreadStore};
+use agent2::HistoryStore;
 use collections::HashMap;
 use editor::{Editor, EditorMode, MinimapVisibility};
 use gpui::{
@@ -10,6 +10,7 @@ use gpui::{
 };
 use language::language_settings::SoftWrap;
 use project::Project;
+use prompt_store::PromptStore;
 use settings::Settings as _;
 use terminal_view::TerminalView;
 use theme::ThemeSettings;
@@ -21,8 +22,8 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
 pub struct EntryViewState {
     workspace: WeakEntity<Workspace>,
     project: Entity<Project>,
-    thread_store: Entity<ThreadStore>,
-    text_thread_store: Entity<TextThreadStore>,
+    history_store: Entity<HistoryStore>,
+    prompt_store: Option<Entity<PromptStore>>,
     entries: Vec<Entry>,
     prevent_slash_commands: bool,
 }
@@ -31,15 +32,15 @@ impl EntryViewState {
     pub fn new(
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
-        thread_store: Entity<ThreadStore>,
-        text_thread_store: Entity<TextThreadStore>,
+        history_store: Entity<HistoryStore>,
+        prompt_store: Option<Entity<PromptStore>>,
         prevent_slash_commands: bool,
     ) -> Self {
         Self {
             workspace,
             project,
-            thread_store,
-            text_thread_store,
+            history_store,
+            prompt_store,
             entries: Vec::new(),
             prevent_slash_commands,
         }
@@ -77,8 +78,8 @@ impl EntryViewState {
                         let mut editor = MessageEditor::new(
                             self.workspace.clone(),
                             self.project.clone(),
-                            self.thread_store.clone(),
-                            self.text_thread_store.clone(),
+                            self.history_store.clone(),
+                            self.prompt_store.clone(),
                             "Edit message - @ to include context",
                             self.prevent_slash_commands,
                             editor::EditorMode::AutoHeight {
@@ -313,9 +314,10 @@ mod tests {
     use std::{path::Path, rc::Rc};
 
     use acp_thread::{AgentConnection, StubAgentConnection};
-    use agent::{TextThreadStore, ThreadStore};
     use agent_client_protocol as acp;
     use agent_settings::AgentSettings;
+    use agent2::HistoryStore;
+    use assistant_context::ContextStore;
     use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
     use editor::{EditorSettings, RowInfo};
     use fs::FakeFs;
@@ -378,15 +380,15 @@ mod tests {
             connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx)
         });
 
-        let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
-        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
+        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
+        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
 
         let view_state = cx.new(|_cx| {
             EntryViewState::new(
                 workspace.downgrade(),
                 project.clone(),
-                thread_store,
-                text_thread_store,
+                history_store,
+                None,
                 false,
             )
         });

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

@@ -3,8 +3,9 @@ use crate::{
     context_picker::fetch_context_picker::fetch_url_content,
 };
 use acp_thread::{MentionUri, selection_name};
-use agent::{TextThreadStore, ThreadId, ThreadStore};
 use agent_client_protocol as acp;
+use agent_servers::AgentServer;
+use agent2::HistoryStore;
 use anyhow::{Context as _, Result, anyhow};
 use assistant_slash_commands::codeblock_fence_for_path;
 use collections::{HashMap, HashSet};
@@ -27,6 +28,7 @@ use gpui::{
 use language::{Buffer, Language};
 use language_model::LanguageModelImage;
 use project::{Project, ProjectPath, Worktree};
+use prompt_store::PromptStore;
 use rope::Point;
 use settings::Settings;
 use std::{
@@ -59,8 +61,8 @@ pub struct MessageEditor {
     editor: Entity<Editor>,
     project: Entity<Project>,
     workspace: WeakEntity<Workspace>,
-    thread_store: Entity<ThreadStore>,
-    text_thread_store: Entity<TextThreadStore>,
+    history_store: Entity<HistoryStore>,
+    prompt_store: Option<Entity<PromptStore>>,
     prevent_slash_commands: bool,
     _subscriptions: Vec<Subscription>,
     _parse_slash_command_task: Task<()>,
@@ -79,8 +81,8 @@ impl MessageEditor {
     pub fn new(
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
-        thread_store: Entity<ThreadStore>,
-        text_thread_store: Entity<TextThreadStore>,
+        history_store: Entity<HistoryStore>,
+        prompt_store: Option<Entity<PromptStore>>,
         placeholder: impl Into<Arc<str>>,
         prevent_slash_commands: bool,
         mode: EditorMode,
@@ -95,10 +97,10 @@ impl MessageEditor {
             None,
         );
         let completion_provider = ContextPickerCompletionProvider::new(
-            workspace.clone(),
-            thread_store.downgrade(),
-            text_thread_store.downgrade(),
             cx.weak_entity(),
+            workspace.clone(),
+            history_store.clone(),
+            prompt_store.clone(),
         );
         let semantics_provider = Rc::new(SlashCommandSemanticsProvider {
             range: Cell::new(None),
@@ -152,9 +154,9 @@ impl MessageEditor {
             editor,
             project,
             mention_set,
-            thread_store,
-            text_thread_store,
             workspace,
+            history_store,
+            prompt_store,
             prevent_slash_commands,
             _subscriptions: subscriptions,
             _parse_slash_command_task: Task::ready(()),
@@ -175,23 +177,12 @@ impl MessageEditor {
         self.editor.read(cx).is_empty(cx)
     }
 
-    pub fn mentioned_path_and_threads(&self) -> (HashSet<PathBuf>, HashSet<ThreadId>) {
-        let mut excluded_paths = HashSet::default();
-        let mut excluded_threads = HashSet::default();
-
-        for uri in self.mention_set.uri_by_crease_id.values() {
-            match uri {
-                MentionUri::File { abs_path, .. } => {
-                    excluded_paths.insert(abs_path.clone());
-                }
-                MentionUri::Thread { id, .. } => {
-                    excluded_threads.insert(id.clone());
-                }
-                _ => {}
-            }
-        }
-
-        (excluded_paths, excluded_threads)
+    pub fn mentions(&self) -> HashSet<MentionUri> {
+        self.mention_set
+            .uri_by_crease_id
+            .values()
+            .cloned()
+            .collect()
     }
 
     pub fn confirm_completion(
@@ -529,7 +520,7 @@ impl MessageEditor {
         &mut self,
         crease_id: CreaseId,
         anchor: Anchor,
-        id: ThreadId,
+        id: acp::SessionId,
         name: String,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -538,17 +529,25 @@ impl MessageEditor {
             id: id.clone(),
             name,
         };
-        let open_task = self.thread_store.update(cx, |thread_store, cx| {
-            thread_store.open_thread(&id, window, cx)
+        let server = Rc::new(agent2::NativeAgentServer::new(
+            self.project.read(cx).fs().clone(),
+            self.history_store.clone(),
+        ));
+        let connection = server.connect(Path::new(""), &self.project, cx);
+        let load_summary = cx.spawn({
+            let id = id.clone();
+            async move |_, cx| {
+                let agent = connection.await?;
+                let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
+                let summary = agent
+                    .0
+                    .update(cx, |agent, cx| agent.thread_summary(id, cx))?
+                    .await?;
+                anyhow::Ok(summary)
+            }
         });
         let task = cx
-            .spawn(async move |_, cx| {
-                let thread = open_task.await.map_err(|e| e.to_string())?;
-                let content = thread
-                    .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text())
-                    .map_err(|e| e.to_string())?;
-                Ok(content)
-            })
+            .spawn(async move |_, _| load_summary.await.map_err(|e| format!("{e}")))
             .shared();
 
         self.mention_set.insert_thread(id.clone(), task.clone());
@@ -590,8 +589,8 @@ impl MessageEditor {
             path: path.clone(),
             name,
         };
-        let context = self.text_thread_store.update(cx, |text_thread_store, cx| {
-            text_thread_store.open_local_context(path.as_path().into(), cx)
+        let context = self.history_store.update(cx, |text_thread_store, cx| {
+            text_thread_store.load_text_thread(path.as_path().into(), cx)
         });
         let task = cx
             .spawn(async move |_, cx| {
@@ -637,7 +636,7 @@ impl MessageEditor {
     ) -> Task<Result<Vec<acp::ContentBlock>>> {
         let contents =
             self.mention_set
-                .contents(self.project.clone(), self.thread_store.clone(), window, cx);
+                .contents(&self.project, self.prompt_store.as_ref(), window, cx);
         let editor = self.editor.clone();
         let prevent_slash_commands = self.prevent_slash_commands;
 
@@ -1316,7 +1315,7 @@ pub struct MentionSet {
     uri_by_crease_id: HashMap<CreaseId, MentionUri>,
     fetch_results: HashMap<Url, Shared<Task<Result<String, String>>>>,
     images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
-    thread_summaries: HashMap<ThreadId, Shared<Task<Result<SharedString, String>>>>,
+    thread_summaries: HashMap<acp::SessionId, Shared<Task<Result<SharedString, String>>>>,
     text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
     directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
 }
@@ -1338,7 +1337,11 @@ impl MentionSet {
         self.images.insert(crease_id, task);
     }
 
-    fn insert_thread(&mut self, id: ThreadId, task: Shared<Task<Result<SharedString, String>>>) {
+    fn insert_thread(
+        &mut self,
+        id: acp::SessionId,
+        task: Shared<Task<Result<SharedString, String>>>,
+    ) {
         self.thread_summaries.insert(id, task);
     }
 
@@ -1358,8 +1361,8 @@ impl MentionSet {
 
     pub fn contents(
         &self,
-        project: Entity<Project>,
-        thread_store: Entity<ThreadStore>,
+        project: &Entity<Project>,
+        prompt_store: Option<&Entity<PromptStore>>,
         _window: &mut Window,
         cx: &mut App,
     ) -> Task<Result<HashMap<CreaseId, Mention>>> {
@@ -1484,8 +1487,7 @@ impl MentionSet {
                         })
                     }
                     MentionUri::Rule { id: prompt_id, .. } => {
-                        let Some(prompt_store) = thread_store.read(cx).prompt_store().clone()
-                        else {
+                        let Some(prompt_store) = prompt_store else {
                             return Task::ready(Err(anyhow!("missing prompt store")));
                         };
                         let text_task = prompt_store.read(cx).load(*prompt_id, cx);
@@ -1678,8 +1680,9 @@ impl Addon for MessageEditorAddon {
 mod tests {
     use std::{ops::Range, path::Path, sync::Arc};
 
-    use agent::{TextThreadStore, ThreadStore};
     use agent_client_protocol as acp;
+    use agent2::HistoryStore;
+    use assistant_context::ContextStore;
     use editor::{AnchorRangeExt as _, Editor, EditorMode};
     use fs::FakeFs;
     use futures::StreamExt as _;
@@ -1710,16 +1713,16 @@ mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
-        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
+        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
+        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
                 MessageEditor::new(
                     workspace.downgrade(),
                     project.clone(),
-                    thread_store.clone(),
-                    text_thread_store.clone(),
+                    history_store.clone(),
+                    None,
                     "Test",
                     false,
                     EditorMode::AutoHeight {
@@ -1908,8 +1911,8 @@ mod tests {
             opened_editors.push(buffer);
         }
 
-        let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
-        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
+        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
+        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
 
         let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
             let workspace_handle = cx.weak_entity();
@@ -1917,8 +1920,8 @@ mod tests {
                 MessageEditor::new(
                     workspace_handle,
                     project.clone(),
-                    thread_store.clone(),
-                    text_thread_store.clone(),
+                    history_store.clone(),
+                    None,
                     "Test",
                     false,
                     EditorMode::AutoHeight {
@@ -2011,12 +2014,9 @@ mod tests {
 
         let contents = message_editor
             .update_in(&mut cx, |message_editor, window, cx| {
-                message_editor.mention_set().contents(
-                    project.clone(),
-                    thread_store.clone(),
-                    window,
-                    cx,
-                )
+                message_editor
+                    .mention_set()
+                    .contents(&project, None, window, cx)
             })
             .await
             .unwrap()
@@ -2066,12 +2066,9 @@ mod tests {
 
         let contents = message_editor
             .update_in(&mut cx, |message_editor, window, cx| {
-                message_editor.mention_set().contents(
-                    project.clone(),
-                    thread_store.clone(),
-                    window,
-                    cx,
-                )
+                message_editor
+                    .mention_set()
+                    .contents(&project, None, window, cx)
             })
             .await
             .unwrap()
@@ -2181,7 +2178,7 @@ mod tests {
             .update_in(&mut cx, |message_editor, window, cx| {
                 message_editor
                     .mention_set()
-                    .contents(project.clone(), thread_store, window, cx)
+                    .contents(&project, None, window, cx)
             })
             .await
             .unwrap()

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

@@ -5,7 +5,6 @@ use acp_thread::{
 };
 use acp_thread::{AgentConnection, Plan};
 use action_log::ActionLog;
-use agent::{TextThreadStore, ThreadStore};
 use agent_client_protocol::{self as acp};
 use agent_servers::{AgentServer, ClaudeCode};
 use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
@@ -32,7 +31,7 @@ use language::Buffer;
 use language_model::LanguageModelRegistry;
 use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
 use project::{Project, ProjectEntryId};
-use prompt_store::PromptId;
+use prompt_store::{PromptId, PromptStore};
 use rope::Point;
 use settings::{Settings as _, SettingsStore};
 use std::sync::Arc;
@@ -158,8 +157,7 @@ impl AcpThreadView {
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
         history_store: Entity<HistoryStore>,
-        thread_store: Entity<ThreadStore>,
-        text_thread_store: Entity<TextThreadStore>,
+        prompt_store: Option<Entity<PromptStore>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -168,8 +166,8 @@ impl AcpThreadView {
             MessageEditor::new(
                 workspace.clone(),
                 project.clone(),
-                thread_store.clone(),
-                text_thread_store.clone(),
+                history_store.clone(),
+                prompt_store.clone(),
                 "Message the agent — @ to include context",
                 prevent_slash_commands,
                 editor::EditorMode::AutoHeight {
@@ -187,8 +185,8 @@ impl AcpThreadView {
             EntryViewState::new(
                 workspace.clone(),
                 project.clone(),
-                thread_store.clone(),
-                text_thread_store.clone(),
+                history_store.clone(),
+                prompt_store.clone(),
                 prevent_slash_commands,
             )
         });
@@ -3201,12 +3199,18 @@ impl AcpThreadView {
                         })
                         .detach_and_log_err(cx);
                 }
-                MentionUri::Thread { id, .. } => {
+                MentionUri::Thread { id, name } => {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         panel.update(cx, |panel, cx| {
-                            panel
-                                .open_thread_by_id(&id, window, cx)
-                                .detach_and_log_err(cx)
+                            panel.load_agent_thread(
+                                DbThreadMetadata {
+                                    id,
+                                    title: name.into(),
+                                    updated_at: Default::default(),
+                                },
+                                window,
+                                cx,
+                            )
                         });
                     }
                 }
@@ -4075,7 +4079,6 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 #[cfg(test)]
 pub(crate) mod tests {
     use acp_thread::StubAgentConnection;
-    use agent::{TextThreadStore, ThreadStore};
     use agent_client_protocol::SessionId;
     use assistant_context::ContextStore;
     use editor::EditorSettings;
@@ -4211,10 +4214,6 @@ pub(crate) mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let thread_store =
-            cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx)));
-        let text_thread_store =
-            cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx)));
         let context_store =
             cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx)));
         let history_store =
@@ -4228,8 +4227,7 @@ pub(crate) mod tests {
                     workspace.downgrade(),
                     project,
                     history_store,
-                    thread_store.clone(),
-                    text_thread_store.clone(),
+                    None,
                     window,
                     cx,
                 )
@@ -4400,6 +4398,7 @@ pub(crate) mod tests {
             ThemeSettings::register(cx);
             release_channel::init(SemanticVersion::default(), cx);
             EditorSettings::register(cx);
+            prompt_store::init(cx)
         });
     }
 
@@ -4420,10 +4419,6 @@ pub(crate) mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let thread_store =
-            cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx)));
-        let text_thread_store =
-            cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx)));
         let context_store =
             cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx)));
         let history_store =
@@ -4438,8 +4433,7 @@ pub(crate) mod tests {
                     workspace.downgrade(),
                     project.clone(),
                     history_store.clone(),
-                    thread_store.clone(),
-                    text_thread_store.clone(),
+                    None,
                     window,
                     cx,
                 )

crates/agent_ui/src/agent_panel.rs 🔗

@@ -4,6 +4,7 @@ use std::rc::Rc;
 use std::sync::Arc;
 use std::time::Duration;
 
+use acp_thread::AcpThread;
 use agent2::{DbThreadMetadata, HistoryEntry};
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use serde::{Deserialize, Serialize};
@@ -1016,8 +1017,6 @@ impl AgentPanel {
             agent: crate::ExternalAgent,
         }
 
-        let thread_store = self.thread_store.clone();
-        let text_thread_store = self.context_store.clone();
         let history = self.acp_history_store.clone();
 
         cx.spawn_in(window, async move |this, cx| {
@@ -1075,8 +1074,7 @@ impl AgentPanel {
                         workspace.clone(),
                         project,
                         this.acp_history_store.clone(),
-                        thread_store.clone(),
-                        text_thread_store.clone(),
+                        this.prompt_store.clone(),
                         window,
                         cx,
                     )
@@ -1499,6 +1497,14 @@ impl AgentPanel {
             _ => None,
         }
     }
+    pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
+        match &self.active_view {
+            ActiveView::ExternalAgentThread { thread_view, .. } => {
+                thread_view.read(cx).thread().cloned()
+            }
+            _ => None,
+        }
+    }
 
     pub(crate) fn delete_thread(
         &mut self,
@@ -1816,6 +1822,15 @@ impl AgentPanel {
             }
         }
     }
+
+    pub fn load_agent_thread(
+        &mut self,
+        thread: DbThreadMetadata,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.external_thread(Some(ExternalAgent::NativeAgent), Some(thread), window, cx);
+    }
 }
 
 impl Focusable for AgentPanel {

crates/assistant_context/src/context_store.rs 🔗

@@ -905,7 +905,7 @@ impl ContextStore {
                     .into_iter()
                     .filter(assistant_slash_commands::acceptable_prompt)
                     .map(|prompt| {
-                        log::debug!("registering context server command: {:?}", prompt.name);
+                        log::info!("registering context server command: {:?}", prompt.name);
                         slash_command_working_set.insert(Arc::new(
                             assistant_slash_commands::ContextServerSlashCommand::new(
                                 context_server_store.clone(),