ACP history mentions (#36551)

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

Closes #ISSUE

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2025-08-20 00:25:07 -06:00 committed by GitHub
parent 159b5e9fb5
commit 5d2bb2466e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 581 additions and 392 deletions

View file

@ -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

View file

@ -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(),
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<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,

View file

@ -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,
)
});

View file

@ -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()

View file

@ -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,
)

View file

@ -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 {