diff --git a/Cargo.lock b/Cargo.lock index 5dced73fb9..fdc858ef50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,7 +7,6 @@ name = "acp_thread" version = "0.1.0" dependencies = [ "action_log", - "agent", "agent-client-protocol", "anyhow", "buffer_diff", diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 173f4c4208..eab756db51 100644 --- a/crates/acp_thread/Cargo.toml +++ b/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 diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 4615e9a551..a1e713cffa 100644 --- a/crates/acp_thread/src/mention.rs +++ b/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, }, 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/") { diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 80ed277f10..fc91e1bb62 100644 --- a/crates/agent/src/thread.rs +++ b/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, ); diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 849ea041e9..2a39440af8 100644 --- a/crates/agent2/Cargo.toml +++ b/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"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 212460d690..3c605de803 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -536,6 +536,28 @@ impl NativeAgent { }) } + pub fn thread_summary( + &mut self, + id: acp::SessionId, + cx: &mut Context, + ) -> Task> { + 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, cx: &mut Context) { let database_future = ThreadsDatabase::connect(cx); let (id, db_thread) = diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index 610a2575c4..c6a6c38201 100644 --- a/crates/agent2/src/db.rs +++ b/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, pub updated_at: DateTime, #[serde(default)] - pub summary: DbSummary, + pub detailed_summary: Option, #[serde(default)] pub initial_project_snapshot: Option>, #[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, diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 4ce304ae5f..7eb7da94ba 100644 --- a/crates/agent2/src/history_store.rs +++ b/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), @@ -120,6 +134,16 @@ impl HistoryStore { }) } + pub fn load_text_thread( + &self, + path: Arc, + cx: &mut Context, + ) -> Task>> { + self.context_store.update(cx, |context_store, cx| { + context_store.open_local_context(path, cx) + }) + } + pub fn reload(&self, cx: &mut Context) { 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) -> Vec { + pub fn entries(&self, cx: &App) -> Vec { 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) -> Vec { - self.entries(cx).into_iter().take(limit).collect() - } - pub fn recently_opened_entries(&self, cx: &App) -> Vec { #[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>> { 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()); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 4bc45f1544..c1778bf38b 100644 --- a/crates/agent2/src/thread.rs +++ b/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, title: Option, - #[allow(unused)] - summary: DetailedSummaryState, + summary: Option, messages: Vec, 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) -> Task> { + 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() } diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index afc834cdd8..1fe41d002c 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/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); diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index fbf8590e68..43e3b25124 100644 --- a/crates/agent_ui/Cargo.toml +++ b/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 diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 1a5e9c7d81..999e469d30 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/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, - query: String, - cancellation_flag: Arc, - recent_entries: Vec, - prompt_store: Option>, - thread_store: WeakEntity, - text_thread_context_store: WeakEntity, - workspace: Entity, - cx: &mut App, -) -> Task> { - 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::>() - }) - } 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::>(); - - 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::>(); - - cx.background_spawn(async move { - let mut matches = search_files_task - .await - .into_iter() - .map(Match::File) - .collect::>(); - - 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, - thread_store: WeakEntity, - text_thread_store: WeakEntity, message_editor: WeakEntity, + workspace: WeakEntity, + history_store: Entity, + prompt_store: Option>, } impl ContextPickerCompletionProvider { pub fn new( - workspace: WeakEntity, - thread_store: WeakEntity, - text_thread_store: WeakEntity, message_editor: WeakEntity, + workspace: WeakEntity, + history_store: Entity, + prompt_store: Option>, ) -> 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, recent: bool, editor: WeakEntity, 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, + query: String, + cancellation_flag: Arc, + cx: &mut App, + ) -> Task> { + 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::>() + }) + } 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::>(); + + cx.background_spawn(async move { + let mut matches = search_files_task + .await + .into_iter() + .map(Match::File) + .collect::>(); + + 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, + cx: &mut App, + ) -> Vec { + 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::(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::>(); + + recent.extend(threads.into_iter().map(Match::RecentThread)); + + recent + } + + fn available_context_picker_entries( + &self, + workspace: &Entity, + cx: &mut App, + ) -> Vec { + 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::()) + .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::::default(), - recent_entries, - prompt_store, - thread_store.clone(), - text_thread_store.clone(), - workspace.clone(), - cx, - ); + let search_task = self.search(mode, query, Arc::::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(), - is_recent, + false, + editor.clone(), + cx, + )), + + Match::RecentThread(thread) => Some(Self::completion_for_thread( + thread, + source_range.clone(), + true, editor.clone(), cx, )), @@ -748,6 +771,42 @@ impl CompletionProvider for ContextPickerCompletionProvider { } } +pub(crate) fn search_threads( + query: String, + cancellation_flag: Arc, + history_store: &Entity, + cx: &mut App, +) -> Task> { + 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::>(); + 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, diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 98af9bf838..67acbb8b5b 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/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, project: Entity, - thread_store: Entity, - text_thread_store: Entity, + history_store: Entity, + prompt_store: Option>, entries: Vec, prevent_slash_commands: bool, } @@ -31,15 +32,15 @@ impl EntryViewState { pub fn new( workspace: WeakEntity, project: Entity, - thread_store: Entity, - text_thread_store: Entity, + history_store: Entity, + prompt_store: Option>, 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, ) }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 01a81c8cce..c87c824015 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/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, project: Entity, workspace: WeakEntity, - thread_store: Entity, - text_thread_store: Entity, + history_store: Entity, + prompt_store: Option>, prevent_slash_commands: bool, _subscriptions: Vec, _parse_slash_command_task: Task<()>, @@ -79,8 +81,8 @@ impl MessageEditor { pub fn new( workspace: WeakEntity, project: Entity, - thread_store: Entity, - text_thread_store: Entity, + history_store: Entity, + prompt_store: Option>, placeholder: impl Into>, 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, HashSet) { - 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 { + 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, @@ -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::().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>> { 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, fetch_results: HashMap>>>, images: HashMap>>>, - thread_summaries: HashMap>>>, + thread_summaries: HashMap>>>, text_thread_summaries: HashMap>>>, directories: HashMap>>>, } @@ -1338,7 +1337,11 @@ impl MentionSet { self.images.insert(crease_id, task); } - fn insert_thread(&mut self, id: ThreadId, task: Shared>>) { + fn insert_thread( + &mut self, + id: acp::SessionId, + task: Shared>>, + ) { self.thread_summaries.insert(id, task); } @@ -1358,8 +1361,8 @@ impl MentionSet { pub fn contents( &self, - project: Entity, - thread_store: Entity, + project: &Entity, + prompt_store: Option<&Entity>, _window: &mut Window, cx: &mut App, ) -> Task>> { @@ -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() diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index ee033bf1f6..3be88ee3c3 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/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, project: Entity, history_store: Entity, - thread_store: Entity, - text_thread_store: Entity, + prompt_store: Option>, window: &mut Window, cx: &mut Context, ) -> 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::(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, ) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c89dc56795..b857052d69 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/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> { + 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.external_thread(Some(ExternalAgent::NativeAgent), Some(thread), window, cx); + } } impl Focusable for AgentPanel { diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index c5b5e99a52..6d13531a57 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/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(),