Cargo.lock 🔗
@@ -7,7 +7,6 @@ name = "acp_thread"
version = "0.1.0"
dependencies = [
"action_log",
- "agent",
"agent-client-protocol",
"anyhow",
"buffer_diff",
Conrad Irwin created
- **TEMP**
- **Update @-mentions to use new history**
Closes #ISSUE
Release Notes:
- N/A
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(-)
@@ -7,7 +7,6 @@ name = "acp_thread"
version = "0.1.0"
dependencies = [
"action_log",
- "agent",
"agent-client-protocol",
"anyhow",
"buffer_diff",
@@ -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
@@ -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/") {
@@ -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,
);
@@ -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"] }
@@ -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) =
@@ -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,
@@ -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());
@@ -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()
}
@@ -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);
@@ -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
@@ -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,
@@ -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,
)
});
@@ -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()
@@ -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,
)
@@ -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 {
@@ -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(),