From dd79c29af91275cf371bda3f54a7078cfe860ce3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 5 May 2025 13:59:21 -0700 Subject: [PATCH] Allow attaching text threads as context (#29947) Release Notes: - N/A --------- Co-authored-by: Michael Sloan --- Cargo.lock | 3 + crates/agent/Cargo.toml | 1 + crates/agent/src/active_thread.rs | 49 ++++- crates/agent/src/assistant.rs | 2 +- crates/agent/src/assistant_panel.rs | 90 +++++---- crates/agent/src/context.rs | 78 ++++++++ crates/agent/src/context_picker.rs | 147 +++++++++++---- .../src/context_picker/completion_provider.rs | 91 ++++++--- .../context_picker/thread_context_picker.rs | 176 +++++++++++++----- crates/agent/src/context_store.rs | 58 +++++- crates/agent/src/context_strip.rs | 63 +++++-- crates/agent/src/history_store.rs | 5 +- crates/agent/src/inline_assistant.rs | 22 ++- crates/agent/src/inline_prompt_editor.rs | 8 +- crates/agent/src/message_editor.rs | 19 +- crates/agent/src/terminal_inline_assistant.rs | 4 +- crates/agent/src/thread_store.rs | 6 + crates/agent/src/ui/context_pill.rs | 42 ++++- crates/agent/src/ui/preview/agent_preview.rs | 15 +- .../assistant_context_editor/src/context.rs | 22 ++- .../src/context_store.rs | 6 +- crates/component_preview/Cargo.toml | 2 + .../src/component_preview.rs | 70 +++---- .../src/preview_support/active_thread.rs | 50 +++-- 24 files changed, 784 insertions(+), 245 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dbfd36f6da..ea774070c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,7 @@ dependencies = [ "time_format", "ui", "ui_input", + "urlencoding", "util", "uuid", "workspace", @@ -3251,6 +3252,7 @@ dependencies = [ "collections", "component", "db", + "futures 0.3.31", "gpui", "languages", "log", @@ -3260,6 +3262,7 @@ dependencies = [ "serde", "ui", "ui_input", + "util", "workspace", "workspace-hack", ] diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 6ffa5880a3..2a11519c49 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -90,6 +90,7 @@ time.workspace = true time_format.workspace = true ui.workspace = true ui_input.workspace = true +urlencoding.workspace = true util.workspace = true uuid.workspace = true workspace-hack.workspace = true diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 76d649d1ca..52d329bfdc 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -6,7 +6,7 @@ use crate::thread::{ LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent, ThreadFeedback, }; -use crate::thread_store::{RulesLoadingError, ThreadStore}; +use crate::thread_store::{RulesLoadingError, TextThreadStore, ThreadStore}; use crate::tool_use::{PendingToolUseStatus, ToolUse}; use crate::ui::{ AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill, @@ -56,6 +56,7 @@ pub struct ActiveThread { context_store: Entity, language_registry: Arc, thread_store: Entity, + text_thread_store: Entity, thread: Entity, workspace: WeakEntity, save_thread_task: Option>, @@ -719,6 +720,15 @@ fn open_markdown_link( }); } }), + Some(MentionLink::TextThread(path)) => workspace.update(cx, |workspace, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel + .open_saved_prompt_editor(path, window, cx) + .detach_and_log_err(cx); + }); + } + }), Some(MentionLink::Fetch(url)) => cx.open_url(&url), Some(MentionLink::Rule(prompt_id)) => window.dispatch_action( Box::new(OpenRulesLibrary { @@ -743,6 +753,7 @@ impl ActiveThread { pub fn new( thread: Entity, thread_store: Entity, + text_thread_store: Entity, context_store: Entity, language_registry: Arc, workspace: WeakEntity, @@ -765,6 +776,7 @@ impl ActiveThread { let mut this = Self { language_registry, thread_store, + text_thread_store, context_store, thread: thread.clone(), workspace, @@ -844,6 +856,14 @@ impl ActiveThread { .map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0))) } + pub fn thread_store(&self) -> &Entity { + &self.thread_store + } + + pub fn text_thread_store(&self) -> &Entity { + &self.text_thread_store + } + fn push_message( &mut self, id: &MessageId, @@ -1264,6 +1284,7 @@ impl ActiveThread { self.workspace.clone(), self.context_store.downgrade(), self.thread_store.downgrade(), + self.text_thread_store.downgrade(), window, cx, ); @@ -1285,6 +1306,7 @@ impl ActiveThread { self.context_store.clone(), self.workspace.clone(), Some(self.thread_store.downgrade()), + Some(self.text_thread_store.downgrade()), context_picker_menu_handle.clone(), SuggestContextKind::File, window, @@ -3439,14 +3461,21 @@ pub(crate) fn open_context( AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { - let thread_id = thread_context.thread.read(cx).id().clone(); - panel - .open_thread_by_id(&thread_id, window, cx) - .detach_and_log_err(cx) + panel.open_thread(thread_context.thread.clone(), window, cx); }); } }), + AgentContextHandle::TextThread(text_thread_context) => { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel.open_prompt_editor(text_thread_context.context.clone(), window, cx) + }); + } + }) + } + AgentContextHandle::Rules(rules_context) => window.dispatch_action( Box::new(OpenRulesLibrary { prompt_to_select: Some(rules_context.prompt_id.0), @@ -3585,18 +3614,25 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); let thread_store = cx .update(|_, cx| { ThreadStore::load( project.clone(), cx.new(|_| ToolWorkingSet::default()), None, - Arc::new(PromptBuilder::new(None).unwrap()), + prompt_builder.clone(), cx, ) }) .await .unwrap(); + let text_thread_store = cx + .update(|_, cx| { + TextThreadStore::new(project.clone(), prompt_builder, Default::default(), cx) + }) + .await + .unwrap(); let thread = thread_store.update(cx, |store, cx| store.create_thread(cx)); let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None)); @@ -3612,6 +3648,7 @@ mod tests { ActiveThread::new( thread.clone(), thread_store.clone(), + text_thread_store.clone(), context_store.clone(), language_registry.clone(), workspace.downgrade(), diff --git a/crates/agent/src/assistant.rs b/crates/agent/src/assistant.rs index 3e34797db8..815f20b5b8 100644 --- a/crates/agent/src/assistant.rs +++ b/crates/agent/src/assistant.rs @@ -46,7 +46,7 @@ pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate} pub use crate::context::{ContextLoadResult, LoadedContext}; pub use crate::inline_assistant::InlineAssistant; pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent}; -pub use crate::thread_store::ThreadStore; +pub use crate::thread_store::{TextThreadStore, ThreadStore}; pub use agent_diff::{AgentDiffPane, AgentDiffToolbar}; pub use context_store::ContextStore; pub use ui::preview::{all_agent_previews, get_agent_preview}; diff --git a/crates/agent/src/assistant_panel.rs b/crates/agent/src/assistant_panel.rs index 4641d1efe0..1cbdc1e333 100644 --- a/crates/agent/src/assistant_panel.rs +++ b/crates/agent/src/assistant_panel.rs @@ -52,7 +52,7 @@ use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry}; use crate::message_editor::{MessageEditor, MessageEditorEvent}; use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio}; use crate::thread_history::{PastContext, PastThread, ThreadHistory}; -use crate::thread_store::ThreadStore; +use crate::thread_store::{TextThreadStore, ThreadStore}; use crate::{ AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, @@ -313,7 +313,7 @@ pub struct AssistantPanel { message_editor: Entity, _active_thread_subscriptions: Vec, _default_model_subscription: Subscription, - context_store: Entity, + context_store: Entity, prompt_store: Option>, configuration: Option>, configuration_subscription: Option, @@ -419,7 +419,7 @@ impl AssistantPanel { fn new( workspace: &Workspace, thread_store: Entity, - context_store: Entity, + context_store: Entity, prompt_store: Option>, window: &mut Window, cx: &mut Context, @@ -447,6 +447,7 @@ impl AssistantPanel { message_editor_context_store.clone(), prompt_store.clone(), thread_store.downgrade(), + context_store.downgrade(), thread.clone(), window, cx, @@ -483,6 +484,7 @@ impl AssistantPanel { ActiveThread::new( thread.clone(), thread_store.clone(), + context_store.clone(), message_editor_context_store.clone(), language_registry.clone(), workspace.clone(), @@ -676,6 +678,10 @@ impl AssistantPanel { &self.thread_store } + pub(crate) fn text_thread_store(&self) -> &Entity { + &self.context_store + } + fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context) { self.thread .update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); @@ -727,6 +733,7 @@ impl AssistantPanel { ActiveThread::new( thread.clone(), self.thread_store.clone(), + self.context_store.clone(), context_store.clone(), self.language_registry.clone(), self.workspace.clone(), @@ -751,6 +758,7 @@ impl AssistantPanel { context_store, self.prompt_store.clone(), self.thread_store.downgrade(), + self.context_store.downgrade(), thread, window, cx, @@ -854,44 +862,41 @@ impl AssistantPanel { let context = self .context_store .update(cx, |store, cx| store.open_local_context(path, cx)); - let fs = self.fs.clone(); - let project = self.project.clone(); - let workspace = self.workspace.clone(); - - let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten(); - cx.spawn_in(window, async move |this, cx| { let context = context.await?; this.update_in(cx, |this, window, cx| { - let editor = cx.new(|cx| { - ContextEditor::for_context( - context, - fs, - workspace, - project, - lsp_adapter_delegate, - window, - cx, - ) - }); - - this.set_active_view( - ActiveView::prompt_editor( - editor.clone(), - this.language_registry.clone(), - window, - cx, - ), - window, - cx, - ); - - anyhow::Ok(()) - })??; - Ok(()) + this.open_prompt_editor(context, window, cx); + }) }) } + pub(crate) fn open_prompt_editor( + &mut self, + context: Entity, + window: &mut Window, + cx: &mut Context, + ) { + let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx) + .log_err() + .flatten(); + let editor = cx.new(|cx| { + ContextEditor::for_context( + context, + self.fs.clone(), + self.workspace.clone(), + self.project.clone(), + lsp_adapter_delegate, + window, + cx, + ) + }); + self.set_active_view( + ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx), + window, + cx, + ); + } + pub(crate) fn open_thread_by_id( &mut self, thread_id: &ThreadId, @@ -936,6 +941,7 @@ impl AssistantPanel { ActiveThread::new( thread.clone(), self.thread_store.clone(), + self.context_store.clone(), context_store.clone(), self.language_registry.clone(), self.workspace.clone(), @@ -960,6 +966,7 @@ impl AssistantPanel { context_store, self.prompt_store.clone(), self.thread_store.downgrade(), + self.context_store.downgrade(), thread, window, cx, @@ -1067,7 +1074,9 @@ impl AssistantPanel { .app_state() .languages .language_for_name("Markdown"); - let thread = self.active_thread(cx); + let Some(thread) = self.active_thread() else { + return; + }; cx.spawn_in(window, async move |_this, cx| { let markdown_language = markdown_language_task.await?; @@ -1133,8 +1142,11 @@ impl AssistantPanel { } } - pub(crate) fn active_thread(&self, cx: &App) -> Entity { - self.thread.read(cx).thread().clone() + pub(crate) fn active_thread(&self) -> Option> { + match &self.active_view { + ActiveView::Thread { thread, .. } => thread.upgrade(), + _ => None, + } } pub(crate) fn delete_thread( @@ -2423,12 +2435,14 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { }; let prompt_store = None; let thread_store = None; + let text_thread_store = None; assistant.assist( &prompt_editor, self.workspace.clone(), project, prompt_store, thread_store, + text_thread_store, window, cx, ) diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index 07f8496237..09e5c92396 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -3,6 +3,7 @@ use std::hash::{Hash, Hasher}; use std::path::PathBuf; use std::{ops::Range, path::Path, sync::Arc}; +use assistant_context_editor::AssistantContext; use assistant_tool::outline; use collections::{HashMap, HashSet}; use editor::display_map::CreaseId; @@ -33,6 +34,7 @@ pub enum ContextKind { Selection, FetchedUrl, Thread, + TextThread, Rules, Image, } @@ -46,6 +48,7 @@ impl ContextKind { ContextKind::Selection => IconName::Context, ContextKind::FetchedUrl => IconName::Globe, ContextKind::Thread => IconName::MessageBubbles, + ContextKind::TextThread => IconName::MessageBubbles, ContextKind::Rules => RULES_ICON, ContextKind::Image => IconName::Image, } @@ -65,6 +68,7 @@ pub enum AgentContextHandle { Selection(SelectionContextHandle), FetchedUrl(FetchedUrlContext), Thread(ThreadContextHandle), + TextThread(TextThreadContextHandle), Rules(RulesContextHandle), Image(ImageContext), } @@ -78,6 +82,7 @@ impl AgentContextHandle { Self::Selection(context) => context.context_id, Self::FetchedUrl(context) => context.context_id, Self::Thread(context) => context.context_id, + Self::TextThread(context) => context.context_id, Self::Rules(context) => context.context_id, Self::Image(context) => context.context_id, } @@ -98,6 +103,7 @@ pub enum AgentContext { Selection(SelectionContext), FetchedUrl(FetchedUrlContext), Thread(ThreadContext), + TextThread(TextThreadContext), Rules(RulesContext), Image(ImageContext), } @@ -115,6 +121,9 @@ impl AgentContext { } AgentContext::FetchedUrl(context) => AgentContextHandle::FetchedUrl(context.clone()), AgentContext::Thread(context) => AgentContextHandle::Thread(context.handle.clone()), + AgentContext::TextThread(context) => { + AgentContextHandle::TextThread(context.handle.clone()) + } AgentContext::Rules(context) => AgentContextHandle::Rules(context.handle.clone()), AgentContext::Image(context) => AgentContextHandle::Image(context.clone()), } @@ -609,6 +618,54 @@ impl Display for ThreadContext { } } +#[derive(Debug, Clone)] +pub struct TextThreadContextHandle { + pub context: Entity, + pub context_id: ContextId, +} + +#[derive(Debug, Clone)] +pub struct TextThreadContext { + pub handle: TextThreadContextHandle, + pub title: SharedString, + pub text: SharedString, +} + +impl TextThreadContextHandle { + // pub fn lookup_key() -> + pub fn eq_for_key(&self, other: &Self) -> bool { + self.context == other.context + } + + pub fn hash_for_key(&self, state: &mut H) { + self.context.hash(state) + } + + pub fn title(&self, cx: &App) -> SharedString { + self.context.read(cx).summary_or_default() + } + + fn load(self, cx: &App) -> Task>)>> { + let title = self.title(cx); + let text = self.context.read(cx).to_xml(cx); + let context = AgentContext::TextThread(TextThreadContext { + title, + text: text.into(), + handle: self, + }); + Task::ready(Some((context, vec![]))) + } +} + +impl Display for TextThreadContext { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + // TODO: escape title? + write!(f, "\n", self.title)?; + write!(f, "{}", self.text.trim())?; + write!(f, "\n") + } +} + #[derive(Debug, Clone)] pub struct RulesContextHandle { pub prompt_id: UserPromptId, @@ -785,6 +842,7 @@ pub fn load_context( AgentContextHandle::Selection(context) => load_tasks.push(context.load(cx)), AgentContextHandle::FetchedUrl(context) => load_tasks.push(context.load()), AgentContextHandle::Thread(context) => load_tasks.push(context.load(cx)), + AgentContextHandle::TextThread(context) => load_tasks.push(context.load(cx)), AgentContextHandle::Rules(context) => load_tasks.push(context.load(prompt_store, cx)), AgentContextHandle::Image(context) => load_tasks.push(context.load(cx)), } @@ -810,6 +868,7 @@ pub fn load_context( let mut selection_context = Vec::new(); let mut fetched_url_context = Vec::new(); let mut thread_context = Vec::new(); + let mut text_thread_context = Vec::new(); let mut rules_context = Vec::new(); let mut images = Vec::new(); for context in &contexts { @@ -820,17 +879,21 @@ pub fn load_context( AgentContext::Selection(context) => selection_context.push(context), AgentContext::FetchedUrl(context) => fetched_url_context.push(context), AgentContext::Thread(context) => thread_context.push(context), + AgentContext::TextThread(context) => text_thread_context.push(context), AgentContext::Rules(context) => rules_context.push(context), AgentContext::Image(context) => images.extend(context.image()), } } + // Use empty text if there are no contexts that contribute to text (everything but image + // context). if file_context.is_empty() && directory_context.is_empty() && symbol_context.is_empty() && selection_context.is_empty() && fetched_url_context.is_empty() && thread_context.is_empty() + && text_thread_context.is_empty() && rules_context.is_empty() { return ContextLoadResult { @@ -903,6 +966,15 @@ pub fn load_context( text.push_str("\n"); } + if !text_thread_context.is_empty() { + text.push_str(""); + for context in text_thread_context { + text.push('\n'); + let _ = writeln!(text, "{context}"); + } + text.push_str(""); + } + if !rules_context.is_empty() { text.push_str( "\n\ @@ -1019,6 +1091,11 @@ impl PartialEq for AgentContextKey { return context.eq_for_key(other_context); } } + AgentContextHandle::TextThread(context) => { + if let AgentContextHandle::TextThread(other_context) = &other.0 { + return context.eq_for_key(other_context); + } + } } false } @@ -1033,6 +1110,7 @@ impl Hash for AgentContextKey { AgentContextHandle::Selection(context) => context.hash_for_key(state), AgentContextHandle::FetchedUrl(context) => context.hash_for_key(state), AgentContextHandle::Thread(context) => context.hash_for_key(state), + AgentContextHandle::TextThread(context) => context.hash_for_key(state), AgentContextHandle::Rules(context) => context.hash_for_key(state), AgentContextHandle::Image(context) => context.hash_for_key(state), } diff --git a/crates/agent/src/context_picker.rs b/crates/agent/src/context_picker.rs index 2b11f29130..ab4f4d2d9c 100644 --- a/crates/agent/src/context_picker.rs +++ b/crates/agent/src/context_picker.rs @@ -6,7 +6,7 @@ mod symbol_context_picker; mod thread_context_picker; use std::ops::Range; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Result, anyhow}; @@ -22,11 +22,14 @@ use gpui::{ }; use language::Buffer; use multi_buffer::MultiBufferRow; +use paths::contexts_dir; use project::{Entry, ProjectPath}; use prompt_store::{PromptStore, UserPromptId}; use rules_context_picker::{RulesContextEntry, RulesContextPicker}; use symbol_context_picker::SymbolContextPicker; -use thread_context_picker::{ThreadContextEntry, ThreadContextPicker, render_thread_context_entry}; +use thread_context_picker::{ + ThreadContextEntry, ThreadContextPicker, render_thread_context_entry, unordered_thread_entries, +}; use ui::{ ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*, }; @@ -37,7 +40,7 @@ use crate::AssistantPanel; use crate::context::RULES_ICON; use crate::context_store::ContextStore; use crate::thread::ThreadId; -use crate::thread_store::ThreadStore; +use crate::thread_store::{TextThreadStore, ThreadStore}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ContextPickerEntry { @@ -164,6 +167,7 @@ pub(super) struct ContextPicker { workspace: WeakEntity, context_store: WeakEntity, thread_store: Option>, + text_thread_store: Option>, prompt_store: Option>, _subscriptions: Vec, } @@ -172,6 +176,7 @@ impl ContextPicker { pub fn new( workspace: WeakEntity, thread_store: Option>, + text_thread_store: Option>, context_store: WeakEntity, window: &mut Window, cx: &mut Context, @@ -208,6 +213,7 @@ impl ContextPicker { workspace, context_store, thread_store, + text_thread_store, prompt_store, _subscriptions: subscriptions, } @@ -340,10 +346,15 @@ impl ContextPicker { })); } ContextPickerMode::Thread => { - if let Some(thread_store) = self.thread_store.as_ref() { + if let Some((thread_store, text_thread_store)) = self + .thread_store + .as_ref() + .zip(self.text_thread_store.as_ref()) + { self.mode = ContextPickerState::Thread(cx.new(|cx| { ThreadContextPicker::new( thread_store.clone(), + text_thread_store.clone(), context_picker.clone(), self.context_store.clone(), window, @@ -447,30 +458,53 @@ impl ContextPicker { fn add_recent_thread( &self, - thread: ThreadContextEntry, + entry: ThreadContextEntry, cx: &mut Context, ) -> Task> { let Some(context_store) = self.context_store.upgrade() else { return Task::ready(Err(anyhow!("context store not available"))); }; - let Some(thread_store) = self - .thread_store - .as_ref() - .and_then(|thread_store| thread_store.upgrade()) - else { - return Task::ready(Err(anyhow!("thread store not available"))); - }; + match entry { + ThreadContextEntry::Thread { id, .. } => { + let Some(thread_store) = self + .thread_store + .as_ref() + .and_then(|thread_store| thread_store.upgrade()) + else { + return Task::ready(Err(anyhow!("thread store not available"))); + }; - let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&thread.id, cx)); - cx.spawn(async move |this, cx| { - let thread = open_thread_task.await?; - context_store.update(cx, |context_store, cx| { - context_store.add_thread(thread, true, cx); - })?; + let open_thread_task = + thread_store.update(cx, |this, cx| this.open_thread(&id, cx)); + cx.spawn(async move |this, cx| { + let thread = open_thread_task.await?; + context_store.update(cx, |context_store, cx| { + context_store.add_thread(thread, true, cx); + })?; + this.update(cx, |_this, cx| cx.notify()) + }) + } + ThreadContextEntry::Context { path, .. } => { + let Some(text_thread_store) = self + .text_thread_store + .as_ref() + .and_then(|thread_store| thread_store.upgrade()) + else { + return Task::ready(Err(anyhow!("text thread store not available"))); + }; - this.update(cx, |_this, cx| cx.notify()) - }) + let task = text_thread_store + .update(cx, |this, cx| this.open_local_context(path.clone(), cx)); + cx.spawn(async move |this, cx| { + let thread = task.await?; + context_store.update(cx, |context_store, cx| { + context_store.add_text_thread(thread, true, cx); + })?; + this.update(cx, |_this, cx| cx.notify()) + }) + } + } } fn recent_entries(&self, cx: &mut App) -> Vec { @@ -485,6 +519,7 @@ impl ContextPicker { recent_context_picker_entries( context_store, self.thread_store.clone(), + self.text_thread_store.clone(), workspace, None, cx, @@ -583,6 +618,7 @@ fn available_context_picker_entries( fn recent_context_picker_entries( context_store: Entity, thread_store: Option>, + text_thread_store: Option>, workspace: Entity, exclude_path: Option, cx: &App, @@ -612,24 +648,34 @@ fn recent_context_picker_entries( let active_thread_id = workspace .panel::(cx) - .map(|panel| panel.read(cx).active_thread(cx).read(cx).id()); + .and_then(|panel| Some(panel.read(cx).active_thread()?.read(cx).id())); + + if let Some((thread_store, text_thread_store)) = thread_store + .and_then(|store| store.upgrade()) + .zip(text_thread_store.and_then(|store| store.upgrade())) + { + let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx) + .filter(|(_, thread)| match thread { + ThreadContextEntry::Thread { id, .. } => { + Some(id) != active_thread_id && !current_threads.contains(id) + } + ThreadContextEntry::Context { .. } => true, + }) + .collect::>(); + + const RECENT_COUNT: usize = 2; + if threads.len() > RECENT_COUNT { + threads.select_nth_unstable_by_key(RECENT_COUNT - 1, |(updated_at, _)| { + std::cmp::Reverse(*updated_at) + }); + threads.truncate(RECENT_COUNT); + } + threads.sort_unstable_by_key(|(updated_at, _)| std::cmp::Reverse(*updated_at)); - if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) { recent.extend( - thread_store - .read(cx) - .reverse_chronological_threads() + threads .into_iter() - .filter(|thread| { - Some(&thread.id) != active_thread_id && !current_threads.contains(&thread.id) - }) - .take(2) - .map(|thread| { - RecentEntry::Thread(ThreadContextEntry { - id: thread.id, - summary: thread.summary, - }) - }), + .map(|(_, thread)| RecentEntry::Thread(thread)), ); } @@ -827,6 +873,7 @@ pub enum MentionLink { Selection(ProjectPath, Range), Fetch(String), Thread(ThreadId), + TextThread(Arc), Rule(UserPromptId), } @@ -838,6 +885,8 @@ impl MentionLink { const FETCH: &str = "@fetch"; const RULE: &str = "@rule"; + const TEXT_THREAD_URL_PREFIX: &str = "text-thread://"; + const SEPARATOR: &str = ":"; pub fn is_valid(url: &str) -> bool { @@ -877,7 +926,22 @@ impl MentionLink { } pub fn for_thread(thread: &ThreadContextEntry) -> String { - format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id) + match thread { + ThreadContextEntry::Thread { id, title } => { + format!("[@{}]({}:{})", title, Self::THREAD, id) + } + ThreadContextEntry::Context { path, title } => { + let filename = path.file_name().unwrap_or_default(); + let escaped_filename = urlencoding::encode(&filename.to_string_lossy()).to_string(); + format!( + "[@{}]({}:{}{})", + title, + Self::THREAD, + Self::TEXT_THREAD_URL_PREFIX, + escaped_filename + ) + } + } } pub fn for_fetch(url: &str) -> String { @@ -939,8 +1003,15 @@ impl MentionLink { Some(MentionLink::Selection(project_path, line_range)) } Self::THREAD => { - let thread_id = ThreadId::from(argument); - Some(MentionLink::Thread(thread_id)) + if let Some(encoded_filename) = argument.strip_prefix(Self::TEXT_THREAD_URL_PREFIX) + { + let filename = urlencoding::decode(encoded_filename).ok()?; + let path = contexts_dir().join(filename.as_ref()).into(); + Some(MentionLink::TextThread(path)) + } else { + let thread_id = ThreadId::from(argument); + Some(MentionLink::Thread(thread_id)) + } } Self::FETCH => Some(MentionLink::Fetch(argument.to_string())), Self::RULE => { diff --git a/crates/agent/src/context_picker/completion_provider.rs b/crates/agent/src/context_picker/completion_provider.rs index e1dee21e42..b886725913 100644 --- a/crates/agent/src/context_picker/completion_provider.rs +++ b/crates/agent/src/context_picker/completion_provider.rs @@ -25,7 +25,7 @@ use workspace::Workspace; use crate::Thread; use crate::context::{AgentContextHandle, AgentContextKey, ContextCreasesAddon, RULES_ICON}; use crate::context_store::ContextStore; -use crate::thread_store::ThreadStore; +use crate::thread_store::{TextThreadStore, ThreadStore}; use super::fetch_context_picker::fetch_url_content; use super::file_context_picker::{FileMatch, search_files}; @@ -72,6 +72,7 @@ fn search( recent_entries: Vec, prompt_store: Option>, thread_store: Option>, + text_thread_context_store: Option>, workspace: Entity, cx: &mut App, ) -> Task> { @@ -101,9 +102,18 @@ fn search( } Some(ContextPickerMode::Thread) => { - if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) { - let search_threads_task = - search_threads(query.clone(), cancellation_flag.clone(), thread_store, cx); + if let Some((thread_store, context_store)) = thread_store + .as_ref() + .and_then(|t| t.upgrade()) + .zip(text_thread_context_store.as_ref().and_then(|t| t.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 @@ -236,6 +246,7 @@ pub struct ContextPickerCompletionProvider { workspace: WeakEntity, context_store: WeakEntity, thread_store: Option>, + text_thread_store: Option>, editor: WeakEntity, excluded_buffer: Option>, } @@ -245,6 +256,7 @@ impl ContextPickerCompletionProvider { workspace: WeakEntity, context_store: WeakEntity, thread_store: Option>, + text_thread_store: Option>, editor: WeakEntity, exclude_buffer: Option>, ) -> Self { @@ -252,6 +264,7 @@ impl ContextPickerCompletionProvider { workspace, context_store, thread_store, + text_thread_store, editor, excluded_buffer: exclude_buffer, } @@ -400,6 +413,7 @@ impl ContextPickerCompletionProvider { editor: Entity, context_store: Entity, thread_store: Entity, + text_thread_store: Entity, ) -> Completion { let icon_for_completion = if recent { IconName::HistoryRerun @@ -411,38 +425,58 @@ impl ContextPickerCompletionProvider { Completion { replace_range: source_range.clone(), new_text, - label: CodeLabel::plain(thread_entry.summary.to_string(), None), + label: CodeLabel::plain(thread_entry.title().to_string(), None), documentation: None, insert_text_mode: None, source: project::CompletionSource::Custom, icon_path: Some(icon_for_completion.path().into()), confirm: Some(confirm_completion_callback( IconName::MessageBubbles.path().into(), - thread_entry.summary.clone(), + thread_entry.title().clone(), excerpt_id, source_range.start, new_text_len, editor.clone(), context_store.clone(), - move |cx| { - let thread_id = thread_entry.id.clone(); - let context_store = context_store.clone(); - let thread_store = thread_store.clone(); - cx.spawn::<_, Option<_>>(async move |cx| { - let thread: Entity = thread_store - .update(cx, |thread_store, cx| { - thread_store.open_thread(&thread_id, cx) - }) - .ok()? - .await - .log_err()?; - let context = context_store - .update(cx, |context_store, cx| { - context_store.add_thread(thread, false, cx) - }) - .ok()??; - Some(context) - }) + move |cx| match &thread_entry { + ThreadContextEntry::Thread { id, .. } => { + let thread_id = id.clone(); + let context_store = context_store.clone(); + let thread_store = thread_store.clone(); + cx.spawn::<_, Option<_>>(async move |cx| { + let thread: Entity = thread_store + .update(cx, |thread_store, cx| { + thread_store.open_thread(&thread_id, cx) + }) + .ok()? + .await + .log_err()?; + let context = context_store + .update(cx, |context_store, cx| { + context_store.add_thread(thread, false, cx) + }) + .ok()??; + Some(context) + }) + } + ThreadContextEntry::Context { path, .. } => { + let path = path.clone(); + let context_store = context_store.clone(); + let text_thread_store = text_thread_store.clone(); + cx.spawn::<_, Option<_>>(async move |cx| { + let thread = text_thread_store + .update(cx, |store, cx| store.open_local_context(path, cx)) + .ok()? + .await + .log_err()?; + let context = context_store + .update(cx, |context_store, cx| { + context_store.add_text_thread(thread, false, cx) + }) + .ok()??; + Some(context) + }) + } }, )), } @@ -733,6 +767,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { ..snapshot.anchor_before(state.source_range.end); let thread_store = self.thread_store.clone(); + let text_thread_store = self.text_thread_store.clone(); let editor = self.editor.clone(); let http_client = workspace.read(cx).client().http_client(); @@ -749,6 +784,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { let recent_entries = recent_context_picker_entries( context_store.clone(), thread_store.clone(), + text_thread_store.clone(), workspace.clone(), excluded_path.clone(), cx, @@ -768,6 +804,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { recent_entries, prompt_store, thread_store.clone(), + text_thread_store.clone(), workspace.clone(), cx, ); @@ -819,6 +856,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { thread, is_recent, .. }) => { let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?; + let text_thread_store = + text_thread_store.as_ref().and_then(|t| t.upgrade())?; Some(Self::completion_for_thread( thread, excerpt_id, @@ -827,6 +866,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { editor.clone(), context_store.clone(), thread_store, + text_thread_store, )) } @@ -1247,6 +1287,7 @@ mod tests { workspace.downgrade(), context_store.downgrade(), None, + None, editor_entity, last_opened_buffer, )))); diff --git a/crates/agent/src/context_picker/thread_context_picker.rs b/crates/agent/src/context_picker/thread_context_picker.rs index 90c21b1c93..96ee399a7f 100644 --- a/crates/agent/src/context_picker/thread_context_picker.rs +++ b/crates/agent/src/context_picker/thread_context_picker.rs @@ -1,6 +1,8 @@ +use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; +use chrono::{DateTime, Utc}; use fuzzy::StringMatchCandidate; use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity}; use picker::{Picker, PickerDelegate}; @@ -9,7 +11,7 @@ use ui::{ListItem, prelude::*}; use crate::context_picker::ContextPicker; use crate::context_store::{self, ContextStore}; use crate::thread::ThreadId; -use crate::thread_store::ThreadStore; +use crate::thread_store::{TextThreadStore, ThreadStore}; pub struct ThreadContextPicker { picker: Entity>, @@ -18,13 +20,18 @@ pub struct ThreadContextPicker { impl ThreadContextPicker { pub fn new( thread_store: WeakEntity, + text_thread_context_store: WeakEntity, context_picker: WeakEntity, context_store: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { - let delegate = - ThreadContextPickerDelegate::new(thread_store, context_picker, context_store); + let delegate = ThreadContextPickerDelegate::new( + thread_store, + text_thread_context_store, + context_picker, + context_store, + ); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); ThreadContextPicker { picker } @@ -44,13 +51,29 @@ impl Render for ThreadContextPicker { } #[derive(Debug, Clone)] -pub struct ThreadContextEntry { - pub id: ThreadId, - pub summary: SharedString, +pub enum ThreadContextEntry { + Thread { + id: ThreadId, + title: SharedString, + }, + Context { + path: Arc, + title: SharedString, + }, +} + +impl ThreadContextEntry { + pub fn title(&self) -> &SharedString { + match self { + Self::Thread { title, .. } => title, + Self::Context { title, .. } => title, + } + } } pub struct ThreadContextPickerDelegate { thread_store: WeakEntity, + text_thread_store: WeakEntity, context_picker: WeakEntity, context_store: WeakEntity, matches: Vec, @@ -60,6 +83,7 @@ pub struct ThreadContextPickerDelegate { impl ThreadContextPickerDelegate { pub fn new( thread_store: WeakEntity, + text_thread_store: WeakEntity, context_picker: WeakEntity, context_store: WeakEntity, ) -> Self { @@ -67,6 +91,7 @@ impl ThreadContextPickerDelegate { thread_store, context_picker, context_store, + text_thread_store, matches: Vec::new(), selected_index: 0, } @@ -103,11 +128,21 @@ impl PickerDelegate for ThreadContextPickerDelegate { window: &mut Window, cx: &mut Context>, ) -> Task<()> { - let Some(thread_store) = self.thread_store.upgrade() else { + let Some((thread_store, text_thread_context_store)) = self + .thread_store + .upgrade() + .zip(self.text_thread_store.upgrade()) + else { return Task::ready(()); }; - let search_task = search_threads(query, Arc::new(AtomicBool::default()), thread_store, cx); + let search_task = search_threads( + query, + Arc::new(AtomicBool::default()), + thread_store, + text_thread_context_store, + cx, + ); cx.spawn_in(window, async move |this, cx| { let matches = search_task.await; this.update(cx, |this, cx| { @@ -124,24 +159,48 @@ impl PickerDelegate for ThreadContextPickerDelegate { return; }; - let Some(thread_store) = self.thread_store.upgrade() else { - return; - }; + match entry { + ThreadContextEntry::Thread { id, .. } => { + let Some(thread_store) = self.thread_store.upgrade() else { + return; + }; + let open_thread_task = + thread_store.update(cx, |this, cx| this.open_thread(&id, cx)); - let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx)); - - cx.spawn(async move |this, cx| { - let thread = open_thread_task.await?; - this.update(cx, |this, cx| { - this.delegate - .context_store - .update(cx, |context_store, cx| { - context_store.add_thread(thread, true, cx) + cx.spawn(async move |this, cx| { + let thread = open_thread_task.await?; + this.update(cx, |this, cx| { + this.delegate + .context_store + .update(cx, |context_store, cx| { + context_store.add_thread(thread, true, cx) + }) + .ok(); }) - .ok(); - }) - }) - .detach_and_log_err(cx); + }) + .detach_and_log_err(cx); + } + ThreadContextEntry::Context { path, .. } => { + let Some(text_thread_store) = self.text_thread_store.upgrade() else { + return; + }; + let task = text_thread_store + .update(cx, |this, cx| this.open_local_context(path.clone(), cx)); + + cx.spawn(async move |this, cx| { + let thread = task.await?; + this.update(cx, |this, cx| { + this.delegate + .context_store + .update(cx, |context_store, cx| { + context_store.add_text_thread(thread, true, cx) + }) + .ok(); + }) + }) + .detach_and_log_err(cx); + } + } } fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { @@ -168,13 +227,20 @@ impl PickerDelegate for ThreadContextPickerDelegate { } pub fn render_thread_context_entry( - thread: &ThreadContextEntry, + entry: &ThreadContextEntry, context_store: WeakEntity, cx: &mut App, ) -> Div { - let added = context_store.upgrade().map_or(false, |ctx_store| { - ctx_store.read(cx).includes_thread(&thread.id) - }); + let is_added = match entry { + ThreadContextEntry::Thread { id, .. } => context_store + .upgrade() + .map_or(false, |ctx_store| ctx_store.read(cx).includes_thread(&id)), + ThreadContextEntry::Context { path, .. } => { + context_store.upgrade().map_or(false, |ctx_store| { + ctx_store.read(cx).includes_text_thread(path) + }) + } + }; h_flex() .gap_1p5() @@ -189,9 +255,9 @@ pub fn render_thread_context_entry( .size(IconSize::XSmall) .color(Color::Muted), ) - .child(Label::new(thread.summary.clone()).truncate()), + .child(Label::new(entry.title().clone()).truncate()), ) - .when(added, |el| { + .when(is_added, |el| { el.child( h_flex() .gap_1() @@ -211,28 +277,54 @@ pub struct ThreadMatch { pub is_recent: bool, } +pub fn unordered_thread_entries( + thread_store: Entity, + text_thread_store: Entity, + cx: &App, +) -> impl Iterator, ThreadContextEntry)> { + let threads = thread_store.read(cx).unordered_threads().map(|thread| { + ( + thread.updated_at, + ThreadContextEntry::Thread { + id: thread.id.clone(), + title: thread.summary.clone(), + }, + ) + }); + + let text_threads = text_thread_store + .read(cx) + .unordered_contexts() + .map(|context| { + ( + context.mtime.to_utc(), + ThreadContextEntry::Context { + path: context.path.clone(), + title: context.title.clone().into(), + }, + ) + }); + + threads.chain(text_threads) +} + pub(crate) fn search_threads( query: String, cancellation_flag: Arc, thread_store: Entity, + text_thread_store: Entity, cx: &mut App, ) -> Task> { - let threads = thread_store - .read(cx) - .reverse_chronological_threads() - .into_iter() - .map(|thread| ThreadContextEntry { - id: thread.id, - summary: thread.summary, - }) - .collect::>(); + let mut threads = + unordered_thread_entries(thread_store, text_thread_store, cx).collect::>(); + threads.sort_unstable_by_key(|(updated_at, _)| std::cmp::Reverse(*updated_at)); let executor = cx.background_executor().clone(); cx.background_spawn(async move { if query.is_empty() { threads .into_iter() - .map(|thread| ThreadMatch { + .map(|(_, thread)| ThreadMatch { thread, is_recent: false, }) @@ -241,7 +333,7 @@ pub(crate) fn search_threads( let candidates = threads .iter() .enumerate() - .map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary)) + .map(|(id, (_, thread))| StringMatchCandidate::new(id, &thread.title())) .collect::>(); let matches = fuzzy::match_strings( &candidates, @@ -256,7 +348,7 @@ pub(crate) fn search_threads( matches .into_iter() .map(|mat| ThreadMatch { - thread: threads[mat.candidate_id].clone(), + thread: threads[mat.candidate_id].1.clone(), is_recent: false, }) .collect() diff --git a/crates/agent/src/context_store.rs b/crates/agent/src/context_store.rs index 188c4ae01f..56da77f9c2 100644 --- a/crates/agent/src/context_store.rs +++ b/crates/agent/src/context_store.rs @@ -1,8 +1,9 @@ use std::ops::Range; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Result, anyhow}; +use assistant_context_editor::AssistantContext; use collections::{HashSet, IndexSet}; use futures::{self, FutureExt}; use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity}; @@ -18,7 +19,7 @@ use crate::ThreadStore; use crate::context::{ AgentContextHandle, AgentContextKey, ContextId, DirectoryContextHandle, FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle, - SymbolContextHandle, ThreadContextHandle, + SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle, }; use crate::context_strip::SuggestedContext; use crate::thread::{MessageId, Thread, ThreadId}; @@ -29,6 +30,7 @@ pub struct ContextStore { next_context_id: ContextId, context_set: IndexSet, context_thread_ids: HashSet, + context_text_thread_paths: HashSet>, } pub enum ContextStoreEvent { @@ -48,6 +50,7 @@ impl ContextStore { next_context_id: ContextId::zero(), context_set: IndexSet::default(), context_thread_ids: HashSet::default(), + context_text_thread_paths: HashSet::default(), } } @@ -227,6 +230,31 @@ impl ContextStore { } } + pub fn add_text_thread( + &mut self, + context: Entity, + remove_if_exists: bool, + cx: &mut Context, + ) -> Option { + let context_id = self.next_context_id.post_inc(); + let context = AgentContextHandle::TextThread(TextThreadContextHandle { + context, + context_id, + }); + + if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) { + if remove_if_exists { + self.remove_context(&context, cx); + None + } else { + Some(existing.as_ref().clone()) + } + } else { + self.insert_context(context.clone(), cx); + Some(context) + } + } + pub fn add_rules( &mut self, prompt_id: UserPromptId, @@ -364,6 +392,18 @@ impl ContextStore { ); } } + SuggestedContext::TextThread { context, name: _ } => { + if let Some(context) = context.upgrade() { + let context_id = self.next_context_id.post_inc(); + self.insert_context( + AgentContextHandle::TextThread(TextThreadContextHandle { + context, + context_id, + }), + cx, + ); + } + } } } @@ -380,6 +420,10 @@ impl ContextStore { return false; } } + AgentContextHandle::TextThread(text_thread_context) => { + self.context_text_thread_paths + .extend(text_thread_context.context.read(cx).path().cloned()); + } _ => {} } let inserted = self.context_set.insert(AgentContextKey(context)); @@ -399,6 +443,11 @@ impl ContextStore { self.context_thread_ids .remove(thread_context.thread.read(cx).id()); } + AgentContextHandle::TextThread(text_thread_context) => { + if let Some(path) = text_thread_context.context.read(cx).path() { + self.context_text_thread_paths.remove(path); + } + } _ => {} } cx.emit(ContextStoreEvent::ContextRemoved(key)); @@ -468,6 +517,10 @@ impl ContextStore { self.context_thread_ids.contains(thread_id) } + pub fn includes_text_thread(&self, path: &Arc) -> bool { + self.context_text_thread_paths.contains(path) + } + pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool { self.context_set .contains(&RulesContextHandle::lookup_key(prompt_id)) @@ -496,6 +549,7 @@ impl ContextStore { | AgentContextHandle::Selection(_) | AgentContextHandle::FetchedUrl(_) | AgentContextHandle::Thread(_) + | AgentContextHandle::TextThread(_) | AgentContextHandle::Rules(_) | AgentContextHandle::Image(_) => None, }) diff --git a/crates/agent/src/context_strip.rs b/crates/agent/src/context_strip.rs index b06446483c..a0c22de66b 100644 --- a/crates/agent/src/context_strip.rs +++ b/crates/agent/src/context_strip.rs @@ -1,6 +1,7 @@ use std::path::Path; use std::rc::Rc; +use assistant_context_editor::AssistantContext; use collections::HashSet; use editor::Editor; use file_icons::FileIcons; @@ -18,7 +19,7 @@ use crate::context::{AgentContextHandle, ContextKind}; use crate::context_picker::ContextPicker; use crate::context_store::ContextStore; use crate::thread::Thread; -use crate::thread_store::ThreadStore; +use crate::thread_store::{TextThreadStore, ThreadStore}; use crate::ui::{AddedContext, ContextPill}; use crate::{ AcceptSuggestedContext, AssistantPanel, FocusDown, FocusLeft, FocusRight, FocusUp, @@ -43,6 +44,7 @@ impl ContextStrip { context_store: Entity, workspace: WeakEntity, thread_store: Option>, + text_thread_store: Option>, context_picker_menu_handle: PopoverMenuHandle, suggest_context_kind: SuggestContextKind, window: &mut Window, @@ -52,6 +54,7 @@ impl ContextStrip { ContextPicker::new( workspace.clone(), thread_store.clone(), + text_thread_store, context_store.downgrade(), window, cx, @@ -141,27 +144,42 @@ impl ContextStrip { } let workspace = self.workspace.upgrade()?; - let active_thread = workspace - .read(cx) - .panel::(cx)? - .read(cx) - .active_thread(cx); - let weak_active_thread = active_thread.downgrade(); + let panel = workspace.read(cx).panel::(cx)?.read(cx); - let active_thread = active_thread.read(cx); + if let Some(active_thread) = panel.active_thread() { + let weak_active_thread = active_thread.downgrade(); - if self - .context_store - .read(cx) - .includes_thread(active_thread.id()) - { - return None; + let active_thread = active_thread.read(cx); + + if self + .context_store + .read(cx) + .includes_thread(active_thread.id()) + { + return None; + } + + Some(SuggestedContext::Thread { + name: active_thread.summary_or_default(), + thread: weak_active_thread, + }) + } else if let Some(active_context_editor) = panel.active_context_editor() { + let context = active_context_editor.read(cx).context(); + let weak_context = context.downgrade(); + let context = context.read(cx); + let path = context.path()?; + + if self.context_store.read(cx).includes_text_thread(path) { + return None; + } + + Some(SuggestedContext::TextThread { + name: context.summary_or_default(), + context: weak_context, + }) + } else { + None } - - Some(SuggestedContext::Thread { - name: active_thread.summary_or_default(), - thread: weak_active_thread, - }) } fn handle_context_picker_event( @@ -538,6 +556,10 @@ pub enum SuggestedContext { name: SharedString, thread: WeakEntity, }, + TextThread { + name: SharedString, + context: WeakEntity, + }, } impl SuggestedContext { @@ -545,6 +567,7 @@ impl SuggestedContext { match self { Self::File { name, .. } => name, Self::Thread { name, .. } => name, + Self::TextThread { name, .. } => name, } } @@ -552,6 +575,7 @@ impl SuggestedContext { match self { Self::File { icon_path, .. } => icon_path.clone(), Self::Thread { .. } => None, + Self::TextThread { .. } => None, } } @@ -559,6 +583,7 @@ impl SuggestedContext { match self { Self::File { .. } => ContextKind::File, Self::Thread { .. } => ContextKind::Thread, + Self::TextThread { .. } => ContextKind::TextThread, } } } diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs index 43ca6c2473..688f9557a9 100644 --- a/crates/agent/src/history_store.rs +++ b/crates/agent/src/history_store.rs @@ -163,7 +163,10 @@ impl HistoryStore { history_entries.push(HistoryEntry::Thread(thread)); } - for context in self.context_store.update(cx, |this, _cx| this.contexts()) { + for context in self + .context_store + .update(cx, |this, _cx| this.reverse_chronological_contexts()) + { history_entries.push(HistoryEntry::Context(context)); } diff --git a/crates/agent/src/inline_assistant.rs b/crates/agent/src/inline_assistant.rs index 5ab69f15ad..d07126d64d 100644 --- a/crates/agent/src/inline_assistant.rs +++ b/crates/agent/src/inline_assistant.rs @@ -48,6 +48,7 @@ use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent}; use crate::context_store::ContextStore; use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent}; use crate::terminal_inline_assistant::TerminalInlineAssistant; +use crate::thread_store::TextThreadStore; use crate::thread_store::ThreadStore; pub fn init( @@ -192,16 +193,20 @@ impl InlineAssistant { if let Some(editor) = item.act_as::(cx) { editor.update(cx, |editor, cx| { if is_assistant2_enabled { - let thread_store = workspace - .read(cx) - .panel::(cx) + let panel = workspace.read(cx).panel::(cx); + let thread_store = panel + .as_ref() .map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade()); + let text_thread_store = panel.map(|assistant_panel| { + assistant_panel.read(cx).text_thread_store().downgrade() + }); editor.add_code_action_provider( Rc::new(AssistantCodeActionProvider { editor: cx.entity().downgrade(), workspace: workspace.downgrade(), thread_store, + text_thread_store, }), window, cx, @@ -253,6 +258,8 @@ impl InlineAssistant { .and_then(|assistant_panel| assistant_panel.prompt_store().as_ref().cloned()); let thread_store = assistant_panel.map(|assistant_panel| assistant_panel.thread_store().downgrade()); + let text_thread_store = + assistant_panel.map(|assistant_panel| assistant_panel.text_thread_store().downgrade()); let handle_assist = |window: &mut Window, cx: &mut Context| match inline_assist_target { @@ -264,6 +271,7 @@ impl InlineAssistant { workspace.project().downgrade(), prompt_store, thread_store, + text_thread_store, window, cx, ) @@ -277,6 +285,7 @@ impl InlineAssistant { workspace.project().downgrade(), prompt_store, thread_store, + text_thread_store, window, cx, ) @@ -332,6 +341,7 @@ impl InlineAssistant { project: WeakEntity, prompt_store: Option>, thread_store: Option>, + text_thread_store: Option>, window: &mut Window, cx: &mut App, ) { @@ -465,6 +475,7 @@ impl InlineAssistant { context_store, workspace.clone(), thread_store.clone(), + text_thread_store.clone(), window, cx, ) @@ -537,6 +548,7 @@ impl InlineAssistant { workspace: Entity, prompt_store: Option>, thread_store: Option>, + text_thread_store: Option>, window: &mut Window, cx: &mut App, ) -> InlineAssistId { @@ -582,6 +594,7 @@ impl InlineAssistant { context_store, workspace.downgrade(), thread_store, + text_thread_store, window, cx, ) @@ -1729,6 +1742,7 @@ struct AssistantCodeActionProvider { editor: WeakEntity, workspace: WeakEntity, thread_store: Option>, + text_thread_store: Option>, } const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2"; @@ -1803,6 +1817,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { let editor = self.editor.clone(); let workspace = self.workspace.clone(); let thread_store = self.thread_store.clone(); + let text_thread_store = self.text_thread_store.clone(); let prompt_store = PromptStore::global(cx); window.spawn(cx, async move |cx| { let workspace = workspace.upgrade().context("workspace was released")?; @@ -1855,6 +1870,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { workspace, prompt_store, thread_store, + text_thread_store, window, cx, ); diff --git a/crates/agent/src/inline_prompt_editor.rs b/crates/agent/src/inline_prompt_editor.rs index 0320b542c9..f98a39e2cd 100644 --- a/crates/agent/src/inline_prompt_editor.rs +++ b/crates/agent/src/inline_prompt_editor.rs @@ -6,7 +6,7 @@ use crate::context_store::ContextStore; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::message_editor::{extract_message_creases, insert_message_creases}; use crate::terminal_codegen::TerminalCodegen; -use crate::thread_store::ThreadStore; +use crate::thread_store::{TextThreadStore, ThreadStore}; use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist}; use crate::{RemoveAllContext, ToggleContextPicker}; use client::ErrorExt; @@ -846,6 +846,7 @@ impl PromptEditor { context_store: Entity, workspace: WeakEntity, thread_store: Option>, + text_thread_store: Option>, window: &mut Window, cx: &mut Context>, ) -> PromptEditor { @@ -889,6 +890,7 @@ impl PromptEditor { workspace.clone(), context_store.downgrade(), thread_store.clone(), + text_thread_store.clone(), prompt_editor_entity, codegen_buffer.as_ref().map(Entity::downgrade), )))); @@ -902,6 +904,7 @@ impl PromptEditor { context_store.clone(), workspace.clone(), thread_store.clone(), + text_thread_store.clone(), context_picker_menu_handle.clone(), SuggestContextKind::Thread, window, @@ -1023,6 +1026,7 @@ impl PromptEditor { context_store: Entity, workspace: WeakEntity, thread_store: Option>, + text_thread_store: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -1059,6 +1063,7 @@ impl PromptEditor { workspace.clone(), context_store.downgrade(), thread_store.clone(), + text_thread_store.clone(), prompt_editor_entity, None, )))); @@ -1072,6 +1077,7 @@ impl PromptEditor { context_store.clone(), workspace.clone(), thread_store.clone(), + text_thread_store.clone(), context_picker_menu_handle.clone(), SuggestContextKind::Thread, window, diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index 52326e3cd1..b6af855217 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -45,7 +45,7 @@ use crate::context_store::ContextStore; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::profile_selector::ProfileSelector; use crate::thread::{MessageCrease, Thread, TokenUsageRatio}; -use crate::thread_store::ThreadStore; +use crate::thread_store::{TextThreadStore, ThreadStore}; use crate::{ ActiveThread, AgentDiffPane, Chat, ExpandMessageEditor, Follow, NewThread, OpenAgentDiff, RemoveAllContext, ToggleContextPicker, ToggleProfileSelector, register_agent_preview, @@ -80,6 +80,7 @@ pub(crate) fn create_editor( workspace: WeakEntity, context_store: WeakEntity, thread_store: WeakEntity, + text_thread_store: WeakEntity, window: &mut Window, cx: &mut App, ) -> Entity { @@ -121,6 +122,7 @@ pub(crate) fn create_editor( workspace, context_store, Some(thread_store), + Some(text_thread_store), editor_entity, None, )))); @@ -136,6 +138,7 @@ impl MessageEditor { context_store: Entity, prompt_store: Option>, thread_store: WeakEntity, + text_thread_store: WeakEntity, thread: Entity, window: &mut Window, cx: &mut Context, @@ -147,6 +150,7 @@ impl MessageEditor { workspace.clone(), context_store.downgrade(), thread_store.clone(), + text_thread_store.clone(), window, cx, ); @@ -156,6 +160,7 @@ impl MessageEditor { context_store.clone(), workspace.clone(), Some(thread_store.clone()), + Some(text_thread_store.clone()), context_picker_menu_handle.clone(), SuggestContextKind::File, window, @@ -1400,16 +1405,19 @@ impl AgentPreview for MessageEditor { fn agent_preview( workspace: WeakEntity, active_thread: Entity, - thread_store: WeakEntity, window: &mut Window, cx: &mut App, ) -> Option { if let Some(workspace) = workspace.upgrade() { let fs = workspace.read(cx).app_state().fs.clone(); let user_store = workspace.read(cx).app_state().user_store.clone(); - let weak_project = workspace.read(cx).project().clone().downgrade(); + let project = workspace.read(cx).project().clone(); + let weak_project = project.downgrade(); let context_store = cx.new(|_cx| ContextStore::new(weak_project, None)); - let thread = active_thread.read(cx).thread().clone(); + let active_thread = active_thread.read(cx); + let thread = active_thread.thread().clone(); + let thread_store = active_thread.thread_store().clone(); + let text_thread_store = active_thread.text_thread_store().clone(); let default_message_editor = cx.new(|cx| { MessageEditor::new( @@ -1418,7 +1426,8 @@ impl AgentPreview for MessageEditor { user_store, context_store, None, - thread_store, + thread_store.downgrade(), + text_thread_store.downgrade(), thread, window, cx, diff --git a/crates/agent/src/terminal_inline_assistant.rs b/crates/agent/src/terminal_inline_assistant.rs index 1056feb3fb..6f2aa0c504 100644 --- a/crates/agent/src/terminal_inline_assistant.rs +++ b/crates/agent/src/terminal_inline_assistant.rs @@ -4,7 +4,7 @@ use crate::inline_prompt_editor::{ CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId, }; use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen}; -use crate::thread_store::ThreadStore; +use crate::thread_store::{TextThreadStore, ThreadStore}; use anyhow::{Context as _, Result}; use client::telemetry::Telemetry; use collections::{HashMap, VecDeque}; @@ -71,6 +71,7 @@ impl TerminalInlineAssistant { project: WeakEntity, prompt_store: Option>, thread_store: Option>, + text_thread_store: Option>, window: &mut Window, cx: &mut App, ) { @@ -91,6 +92,7 @@ impl TerminalInlineAssistant { context_store.clone(), workspace.clone(), thread_store.clone(), + text_thread_store.clone(), window, cx, ) diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 09f8498af6..8e43042c1a 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -58,6 +58,8 @@ impl SharedProjectContext { } } +pub type TextThreadStore = assistant_context_editor::ContextStore; + pub struct ThreadStore { project: Entity, tools: Entity, @@ -361,6 +363,10 @@ impl ThreadStore { self.threads.len() } + pub fn unordered_threads(&self) -> impl Iterator { + self.threads.iter() + } + pub fn reverse_chronological_threads(&self) -> Vec { let mut threads = self.threads.iter().cloned().collect::>(); threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at)); diff --git a/crates/agent/src/ui/context_pill.rs b/crates/agent/src/ui/context_pill.rs index 49303344f3..fe59889d25 100644 --- a/crates/agent/src/ui/context_pill.rs +++ b/crates/agent/src/ui/context_pill.rs @@ -16,7 +16,8 @@ use crate::context::{ AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext, DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext, ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle, - SymbolContext, SymbolContextHandle, ThreadContext, ThreadContextHandle, + SymbolContext, SymbolContextHandle, TextThreadContext, TextThreadContextHandle, ThreadContext, + ThreadContextHandle, }; #[derive(IntoElement)] @@ -301,6 +302,7 @@ impl AddedContext { AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx), AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)), AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)), + AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)), AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx), AgentContextHandle::Image(handle) => Some(Self::image(handle)), } @@ -314,6 +316,7 @@ impl AddedContext { AgentContext::Selection(context) => Self::attached_selection(context, cx), AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()), AgentContext::Thread(context) => Self::attached_thread(context), + AgentContext::TextThread(context) => Self::attached_text_thread(context), AgentContext::Rules(context) => Self::attached_rules(context), AgentContext::Image(context) => Self::image(context.clone()), } @@ -520,6 +523,43 @@ impl AddedContext { } } + fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext { + AddedContext { + kind: ContextKind::TextThread, + name: handle.title(cx), + parent: None, + tooltip: None, + icon_path: None, + status: ContextStatus::Ready, + render_hover: { + let context = handle.context.clone(); + Some(Rc::new(move |_, cx| { + let text = context.read(cx).to_xml(cx); + ContextPillHover::new_text(text.into(), cx).into() + })) + }, + handle: AgentContextHandle::TextThread(handle), + } + } + + fn attached_text_thread(context: &TextThreadContext) -> AddedContext { + AddedContext { + kind: ContextKind::TextThread, + name: context.title.clone(), + parent: None, + tooltip: None, + icon_path: None, + status: ContextStatus::Ready, + render_hover: { + let text = context.text.clone(); + Some(Rc::new(move |_, cx| { + ContextPillHover::new_text(text.clone(), cx).into() + })) + }, + handle: AgentContextHandle::TextThread(context.handle.clone()), + } + } + fn pending_rules( handle: RulesContextHandle, prompt_store: Option<&Entity>, diff --git a/crates/agent/src/ui/preview/agent_preview.rs b/crates/agent/src/ui/preview/agent_preview.rs index 360f01fd86..4b2163f26c 100644 --- a/crates/agent/src/ui/preview/agent_preview.rs +++ b/crates/agent/src/ui/preview/agent_preview.rs @@ -6,16 +6,11 @@ use std::sync::OnceLock; use ui::{AnyElement, Component, ComponentScope, Window}; use workspace::Workspace; -use crate::{ActiveThread, ThreadStore}; +use crate::ActiveThread; /// Function type for creating agent component previews -pub type PreviewFn = fn( - WeakEntity, - Entity, - WeakEntity, - &mut Window, - &mut App, -) -> Option; +pub type PreviewFn = + fn(WeakEntity, Entity, &mut Window, &mut App) -> Option; /// Distributed slice for preview registration functions #[distributed_slice] @@ -32,7 +27,6 @@ pub trait AgentPreview: Component + Sized { fn agent_preview( workspace: WeakEntity, active_thread: Entity, - thread_store: WeakEntity, window: &mut Window, cx: &mut App, ) -> Option; @@ -75,14 +69,13 @@ pub fn get_agent_preview( id: &ComponentId, workspace: WeakEntity, active_thread: Entity, - thread_store: WeakEntity, window: &mut Window, cx: &mut App, ) -> Option { let registry = get_or_init_registry(); registry .get(id) - .and_then(|preview_fn| preview_fn(workspace, active_thread, thread_store, window, cx)) + .and_then(|preview_fn| preview_fn(workspace, active_thread, window, cx)) } /// Get all registered agent previews. diff --git a/crates/assistant_context_editor/src/context.rs b/crates/assistant_context_editor/src/context.rs index fe607fb291..d0b8c32cc2 100644 --- a/crates/assistant_context_editor/src/context.rs +++ b/crates/assistant_context_editor/src/context.rs @@ -32,7 +32,7 @@ use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{ cmp::{Ordering, max}, - fmt::Debug, + fmt::{Debug, Write as _}, iter, mem, ops::Range, path::Path, @@ -2539,6 +2539,26 @@ impl AssistantContext { Some(user_message) } + pub fn to_xml(&self, cx: &App) -> String { + let mut output = String::new(); + let buffer = self.buffer.read(cx); + for message in self.messages(cx) { + if message.status != MessageStatus::Done { + continue; + } + + writeln!(&mut output, "<{}>", message.role).unwrap(); + for chunk in buffer.text_for_range(message.offset_range) { + output.push_str(chunk); + } + if !output.ends_with('\n') { + output.push('\n'); + } + writeln!(&mut output, "", message.role).unwrap(); + } + output + } + pub fn to_completion_request( &self, request_type: RequestType, diff --git a/crates/assistant_context_editor/src/context_store.rs b/crates/assistant_context_editor/src/context_store.rs index 2cf5a3b654..fe89d76109 100644 --- a/crates/assistant_context_editor/src/context_store.rs +++ b/crates/assistant_context_editor/src/context_store.rs @@ -339,7 +339,11 @@ impl ContextStore { } } - pub fn contexts(&self) -> Vec { + pub fn unordered_contexts(&self) -> impl Iterator { + self.contexts_metadata.iter() + } + + pub fn reverse_chronological_contexts(&self) -> Vec { let mut contexts = self.contexts_metadata.iter().cloned().collect::>(); contexts.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.mtime)); contexts diff --git a/crates/component_preview/Cargo.toml b/crates/component_preview/Cargo.toml index 61d207d0dc..e3f04d8cba 100644 --- a/crates/component_preview/Cargo.toml +++ b/crates/component_preview/Cargo.toml @@ -21,6 +21,7 @@ client.workspace = true collections.workspace = true component.workspace = true db.workspace = true +futures.workspace = true gpui.workspace = true languages.workspace = true log.workspace = true @@ -30,6 +31,7 @@ prompt_store.workspace = true serde.workspace = true ui.workspace = true ui_input.workspace = true +util.workspace = true workspace-hack.workspace = true workspace.workspace = true assistant_tool.workspace = true diff --git a/crates/component_preview/src/component_preview.rs b/crates/component_preview/src/component_preview.rs index 2891a5a6cf..a87a517815 100644 --- a/crates/component_preview/src/component_preview.rs +++ b/crates/component_preview/src/component_preview.rs @@ -8,7 +8,7 @@ mod preview_support; use std::iter::Iterator; use std::sync::Arc; -use agent::{ActiveThread, ThreadStore}; +use agent::{ActiveThread, TextThreadStore, ThreadStore}; use client::UserStore; use component::{ComponentId, ComponentMetadata, components}; use gpui::{ @@ -21,11 +21,13 @@ use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle}; use languages::LanguageRegistry; use notifications::status_toast::{StatusToast, ToastIcon}; use persistence::COMPONENT_PREVIEW_DB; -use preview_support::active_thread::{load_preview_thread_store, static_active_thread}; +use preview_support::active_thread::{ + load_preview_text_thread_store, load_preview_thread_store, static_active_thread, +}; use project::Project; use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*}; - use ui_input::SingleLineInput; +use util::ResultExt as _; use workspace::{AppState, ItemId, SerializableItem, delete_unloaded_items}; use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent}; @@ -120,6 +122,7 @@ struct ComponentPreview { // preview support thread_store: Option>, + text_thread_store: Option>, active_thread: Option>, } @@ -137,23 +140,29 @@ impl ComponentPreview { let workspace_clone = workspace.clone(); let project_clone = project.clone(); - let entity = cx.weak_entity(); - window - .spawn(cx, async move |cx| { - let thread_store_task = - load_preview_thread_store(workspace_clone.clone(), project_clone.clone(), cx) - .await; + cx.spawn_in(window, async move |entity, cx| { + let thread_store_future = + load_preview_thread_store(workspace_clone.clone(), project_clone.clone(), cx); + let text_thread_store_future = + load_preview_text_thread_store(workspace_clone.clone(), project_clone.clone(), cx); - if let Ok(thread_store) = thread_store_task.await { - entity - .update_in(cx, |this, window, cx| { - this.thread_store = Some(thread_store.clone()); - this.create_active_thread(window, cx); - }) - .ok(); - } - }) - .detach(); + let (thread_store_result, text_thread_store_result) = + futures::join!(thread_store_future, text_thread_store_future); + + if let (Some(thread_store), Some(text_thread_store)) = ( + thread_store_result.log_err(), + text_thread_store_result.log_err(), + ) { + entity + .update_in(cx, |this, window, cx| { + this.thread_store = Some(thread_store.clone()); + this.text_thread_store = Some(text_thread_store.clone()); + this.create_active_thread(window, cx); + }) + .ok(); + } + }) + .detach(); let sorted_components = components().all_sorted(); let selected_index = selected_index.into().unwrap_or(0); @@ -195,6 +204,7 @@ impl ComponentPreview { filter_editor, filter_text: String::new(), thread_store: None, + text_thread_store: None, active_thread: None, }; @@ -220,12 +230,17 @@ impl ComponentPreview { let weak_handle = self.workspace.clone(); if let Some(workspace) = workspace.upgrade() { let project = workspace.read(cx).project().clone(); - if let Some(thread_store) = self.thread_store.clone() { + if let Some((thread_store, text_thread_store)) = self + .thread_store + .clone() + .zip(self.text_thread_store.clone()) + { let active_thread = static_active_thread( weak_handle, project, language_registry, thread_store, + text_thread_store, window, cx, ); @@ -625,15 +640,11 @@ impl ComponentPreview { // Check if the component's scope is Agent if scope == ComponentScope::Agent { - if let (Some(thread_store), Some(active_thread)) = ( - self.thread_store.as_ref().map(|ts| ts.downgrade()), - self.active_thread.clone(), - ) { + if let Some(active_thread) = self.active_thread.clone() { if let Some(element) = agent::get_agent_preview( &component.id(), self.workspace.clone(), active_thread, - thread_store, window, cx, ) { @@ -688,7 +699,6 @@ impl ComponentPreview { .child(ComponentPreviewPage::new( component.clone(), self.workspace.clone(), - self.thread_store.as_ref().map(|ts| ts.downgrade()), self.active_thread.clone(), )) .into_any_element() @@ -1037,7 +1047,6 @@ pub struct ComponentPreviewPage { // languages: Arc, component: ComponentMetadata, workspace: WeakEntity, - thread_store: Option>, active_thread: Option>, } @@ -1045,7 +1054,6 @@ impl ComponentPreviewPage { pub fn new( component: ComponentMetadata, workspace: WeakEntity, - thread_store: Option>, active_thread: Option>, // languages: Arc ) -> Self { @@ -1053,7 +1061,6 @@ impl ComponentPreviewPage { // languages, component, workspace, - thread_store, active_thread, } } @@ -1086,14 +1093,11 @@ impl ComponentPreviewPage { fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { // Try to get agent preview first if we have an active thread - let maybe_agent_preview = if let (Some(thread_store), Some(active_thread)) = - (self.thread_store.as_ref(), self.active_thread.as_ref()) - { + let maybe_agent_preview = if let Some(active_thread) = self.active_thread.as_ref() { agent::get_agent_preview( &self.component.id(), self.workspace.clone(), active_thread.clone(), - thread_store.clone(), window, cx, ) diff --git a/crates/component_preview/src/preview_support/active_thread.rs b/crates/component_preview/src/preview_support/active_thread.rs index 725b9acdae..b66f7b6446 100644 --- a/crates/component_preview/src/preview_support/active_thread.rs +++ b/crates/component_preview/src/preview_support/active_thread.rs @@ -2,31 +2,47 @@ use languages::LanguageRegistry; use project::Project; use std::sync::Arc; -use agent::{ActiveThread, ContextStore, MessageSegment, ThreadStore}; +use agent::{ActiveThread, ContextStore, MessageSegment, TextThreadStore, ThreadStore}; +use anyhow::{Result, anyhow}; use assistant_tool::ToolWorkingSet; use gpui::{AppContext, AsyncApp, Entity, Task, WeakEntity}; use prompt_store::PromptBuilder; use ui::{App, Window}; use workspace::Workspace; -pub async fn load_preview_thread_store( +pub fn load_preview_thread_store( workspace: WeakEntity, project: Entity, cx: &mut AsyncApp, -) -> Task>> { - cx.spawn(async move |cx| { - workspace - .update(cx, |_, cx| { - ThreadStore::load( - project.clone(), - cx.new(|_| ToolWorkingSet::default()), - None, - Arc::new(PromptBuilder::new(None).unwrap()), - cx, - ) - })? - .await - }) +) -> Task>> { + workspace + .update(cx, |_, cx| { + ThreadStore::load( + project.clone(), + cx.new(|_| ToolWorkingSet::default()), + None, + Arc::new(PromptBuilder::new(None).unwrap()), + cx, + ) + }) + .unwrap_or(Task::ready(Err(anyhow!("workspace dropped")))) +} + +pub fn load_preview_text_thread_store( + workspace: WeakEntity, + project: Entity, + cx: &mut AsyncApp, +) -> Task>> { + workspace + .update(cx, |_, cx| { + TextThreadStore::new( + project.clone(), + Arc::new(PromptBuilder::new(None).unwrap()), + Default::default(), + cx, + ) + }) + .unwrap_or(Task::ready(Err(anyhow!("workspace dropped")))) } pub fn static_active_thread( @@ -34,6 +50,7 @@ pub fn static_active_thread( project: Entity, language_registry: Arc, thread_store: Entity, + text_thread_store: Entity, window: &mut Window, cx: &mut App, ) -> Entity { @@ -59,6 +76,7 @@ pub fn static_active_thread( ActiveThread::new( thread, thread_store, + text_thread_store, context_store, language_registry, workspace.clone(),