diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 242ceff376..2b60d97697 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -1,4 +1,4 @@ -use crate::context::{AgentContext, RULES_ICON}; +use crate::context::{AgentContextHandle, RULES_ICON}; use crate::context_picker::MentionLink; use crate::thread::{ LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent, @@ -1491,19 +1491,13 @@ impl ActiveThread { let workspace = self.workspace.clone(); let thread = self.thread.read(cx); - let prompt_store = self.thread_store.read(cx).prompt_store().as_ref(); // Get all the data we need from thread before we start using it in closures let checkpoint = thread.checkpoint_for_message(message_id); - let added_context = if let Some(workspace) = workspace.upgrade() { - let project = workspace.read(cx).project().read(cx); - thread - .context_for_message(message_id) - .flat_map(|context| AddedContext::new(context.clone(), prompt_store, project, cx)) - .collect::>() - } else { - return Empty.into_any(); - }; + let added_context = thread + .context_for_message(message_id) + .map(|context| AddedContext::new_attached(context, cx)) + .collect::>(); let tool_uses = thread.tool_uses_for_message(message_id, cx); let has_tool_uses = !tool_uses.is_empty(); @@ -1713,7 +1707,7 @@ impl ActiveThread { .when(!added_context.is_empty(), |parent| { parent.child(h_flex().flex_wrap().gap_1().children( added_context.into_iter().map(|added_context| { - let context = added_context.context.clone(); + let context = added_context.handle.clone(); ContextPill::added(added_context, false, false, None).on_click(Rc::new( cx.listener({ let workspace = workspace.clone(); @@ -3188,13 +3182,13 @@ impl Render for ActiveThread { } pub(crate) fn open_context( - context: &AgentContext, + context: &AgentContextHandle, workspace: Entity, window: &mut Window, cx: &mut App, ) { match context { - AgentContext::File(file_context) => { + AgentContextHandle::File(file_context) => { if let Some(project_path) = file_context.project_path(cx) { workspace.update(cx, |workspace, cx| { workspace @@ -3204,7 +3198,7 @@ pub(crate) fn open_context( } } - AgentContext::Directory(directory_context) => { + AgentContextHandle::Directory(directory_context) => { let entry_id = directory_context.entry_id; workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |_project, cx| { @@ -3213,7 +3207,7 @@ pub(crate) fn open_context( }) } - AgentContext::Symbol(symbol_context) => { + AgentContextHandle::Symbol(symbol_context) => { let buffer = symbol_context.buffer.read(cx); if let Some(project_path) = buffer.project_path(cx) { let snapshot = buffer.snapshot(); @@ -3223,7 +3217,7 @@ pub(crate) fn open_context( } } - AgentContext::Selection(selection_context) => { + AgentContextHandle::Selection(selection_context) => { let buffer = selection_context.buffer.read(cx); if let Some(project_path) = buffer.project_path(cx) { let snapshot = buffer.snapshot(); @@ -3234,11 +3228,11 @@ pub(crate) fn open_context( } } - AgentContext::FetchedUrl(fetched_url_context) => { + AgentContextHandle::FetchedUrl(fetched_url_context) => { cx.open_url(&fetched_url_context.url); } - AgentContext::Thread(thread_context) => workspace.update(cx, |workspace, cx| { + 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(); @@ -3249,14 +3243,14 @@ pub(crate) fn open_context( } }), - AgentContext::Rules(rules_context) => window.dispatch_action( + AgentContextHandle::Rules(rules_context) => window.dispatch_action( Box::new(OpenRulesLibrary { prompt_to_select: Some(rules_context.prompt_id.0), }), cx, ), - AgentContext::Image(_) => {} + AgentContextHandle::Image(_) => {} } } diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index ceed361159..7b19f49313 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -1,4 +1,6 @@ +use std::fmt::{self, Display, Formatter, Write as _}; use std::hash::{Hash, Hasher}; +use std::path::PathBuf; use std::{ops::Range, path::Path, sync::Arc}; use collections::HashSet; @@ -10,9 +12,10 @@ use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageCon use project::{Project, ProjectEntryId, ProjectPath, Worktree}; use prompt_store::{PromptStore, UserPromptId}; use ref_cast::RefCast; -use rope::{Point, Rope}; +use rope::Point; use text::{Anchor, OffsetRangeExt as _}; use ui::{ElementId, IconName}; +use util::markdown::MarkdownCodeBlock; use util::{ResultExt as _, post_inc}; use crate::thread::Thread; @@ -45,24 +48,24 @@ impl ContextKind { } } -/// Handle for context that can be added to a user message. +/// Handle for context that can be attached to a user message. /// /// This uses IDs that are stable enough for tracking renames and identifying when context has /// already been added to the thread. To use this in a set, wrap it in `AgentContextKey` to opt in /// to `PartialEq` and `Hash` impls that use the subset of the fields used for this stable identity. #[derive(Debug, Clone)] -pub enum AgentContext { - File(FileContext), - Directory(DirectoryContext), - Symbol(SymbolContext), - Selection(SelectionContext), +pub enum AgentContextHandle { + File(FileContextHandle), + Directory(DirectoryContextHandle), + Symbol(SymbolContextHandle), + Selection(SelectionContextHandle), FetchedUrl(FetchedUrlContext), - Thread(ThreadContext), - Rules(RulesContext), + Thread(ThreadContextHandle), + Rules(RulesContextHandle), Image(ImageContext), } -impl AgentContext { +impl AgentContextHandle { fn id(&self) -> ContextId { match self { Self::File(context) => context.context_id, @@ -81,6 +84,39 @@ impl AgentContext { } } +/// Loaded context that can be attached to a user message. This can be thought of as a +/// snapshot of the context along with an `AgentContextHandle`. +#[derive(Debug, Clone)] +pub enum AgentContext { + File(FileContext), + Directory(DirectoryContext), + Symbol(SymbolContext), + Selection(SelectionContext), + FetchedUrl(FetchedUrlContext), + Thread(ThreadContext), + Rules(RulesContext), + Image(ImageContext), +} + +impl AgentContext { + pub fn handle(&self) -> AgentContextHandle { + match self { + AgentContext::File(context) => AgentContextHandle::File(context.handle.clone()), + AgentContext::Directory(context) => { + AgentContextHandle::Directory(context.handle.clone()) + } + AgentContext::Symbol(context) => AgentContextHandle::Symbol(context.handle.clone()), + AgentContext::Selection(context) => { + AgentContextHandle::Selection(context.handle.clone()) + } + AgentContext::FetchedUrl(context) => AgentContextHandle::FetchedUrl(context.clone()), + AgentContext::Thread(context) => AgentContextHandle::Thread(context.handle.clone()), + AgentContext::Rules(context) => AgentContextHandle::Rules(context.handle.clone()), + AgentContext::Image(context) => AgentContextHandle::Image(context.clone()), + } + } +} + /// ID created at time of context add, for use in ElementId. This is not the stable identity of a /// context, instead that's handled by the `PartialEq` and `Hash` impls of `AgentContextKey`. #[derive(Debug, Copy, Clone)] @@ -106,12 +142,19 @@ impl ContextId { /// be opened even if the file has been deleted. An alternative might be to use `ProjectEntryId`, /// but then when deleted there is no path info or ability to open. #[derive(Debug, Clone)] -pub struct FileContext { +pub struct FileContextHandle { pub buffer: Entity, pub context_id: ContextId, } -impl FileContext { +#[derive(Debug, Clone)] +pub struct FileContext { + pub handle: FileContextHandle, + pub full_path: Arc, + pub text: SharedString, +} + +impl FileContextHandle { pub fn eq_for_key(&self, other: &Self) -> bool { self.buffer == other.buffer } @@ -128,19 +171,35 @@ impl FileContext { }) } - fn load(&self, cx: &App) -> Option)>> { + fn load(self, cx: &App) -> Task>)>> { let buffer_ref = self.buffer.read(cx); let Some(file) = buffer_ref.file() else { log::error!("file context missing path"); - return None; + return Task::ready(None); }; let full_path = file.full_path(cx); let rope = buffer_ref.as_rope().clone(); let buffer = self.buffer.clone(); - Some( - cx.background_spawn( - async move { (to_fenced_codeblock(&full_path, rope, None), buffer) }, - ), + cx.background_spawn(async move { + let context = AgentContext::File(FileContext { + handle: self, + full_path: full_path.into(), + text: rope.to_string().into(), + }); + Some((context, vec![buffer])) + }) + } +} + +impl Display for FileContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + MarkdownCodeBlock { + tag: &codeblock_tag(&self.full_path, None), + text: &self.text, + } ) } } @@ -149,12 +208,26 @@ impl FileContext { /// /// This has a `ProjectEntryId` so that it follows renames. #[derive(Debug, Clone)] -pub struct DirectoryContext { +pub struct DirectoryContextHandle { pub entry_id: ProjectEntryId, pub context_id: ContextId, } -impl DirectoryContext { +#[derive(Debug, Clone)] +pub struct DirectoryContext { + pub handle: DirectoryContextHandle, + pub full_path: Arc, + pub descendants: Vec, +} + +#[derive(Debug, Clone)] +pub struct DirectoryContextDescendant { + /// Path within the directory. + pub rel_path: Arc, + pub fenced_codeblock: SharedString, +} + +impl DirectoryContextHandle { pub fn eq_for_key(&self, other: &Self) -> bool { self.entry_id == other.entry_id } @@ -164,41 +237,116 @@ impl DirectoryContext { } fn load( - &self, + self, project: Entity, cx: &mut App, - ) -> Option)>>> { - let worktree = project.read(cx).worktree_for_entry(self.entry_id, cx)?; + ) -> Task>)>> { + let Some(worktree) = project.read(cx).worktree_for_entry(self.entry_id, cx) else { + return Task::ready(None); + }; let worktree_ref = worktree.read(cx); - let entry = worktree_ref.entry_for_id(self.entry_id)?; + let Some(entry) = worktree_ref.entry_for_id(self.entry_id) else { + return Task::ready(None); + }; if entry.is_file() { log::error!("DirectoryContext unexpectedly refers to a file."); - return None; + return Task::ready(None); } - let file_paths = collect_files_in_path(worktree_ref, entry.path.as_ref()); - let texts_future = future::join_all(file_paths.into_iter().map(|path| { - load_file_path_text_as_fenced_codeblock(project.clone(), worktree.clone(), path, cx) + let directory_path = entry.path.clone(); + let directory_full_path = worktree_ref.full_path(&directory_path).into(); + + let file_paths = collect_files_in_path(worktree_ref, &directory_path); + let descendants_future = future::join_all(file_paths.into_iter().map(|path| { + let worktree_ref = worktree.read(cx); + let worktree_id = worktree_ref.id(); + let full_path = worktree_ref.full_path(&path); + + let rel_path = path + .strip_prefix(&directory_path) + .log_err() + .map_or_else(|| path.clone(), |rel_path| rel_path.into()); + + let open_task = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + let project_path = ProjectPath { worktree_id, path }; + buffer_store.open_buffer(project_path, cx) + }) + }); + + // TODO: report load errors instead of just logging + let rope_task = cx.spawn(async move |cx| { + let buffer = open_task.await.log_err()?; + let rope = buffer + .read_with(cx, |buffer, _cx| buffer.as_rope().clone()) + .log_err()?; + Some((rope, buffer)) + }); + + cx.background_spawn(async move { + let (rope, buffer) = rope_task.await?; + let fenced_codeblock = MarkdownCodeBlock { + tag: &codeblock_tag(&full_path, None), + text: &rope.to_string(), + } + .to_string() + .into(); + let descendant = DirectoryContextDescendant { + rel_path, + fenced_codeblock, + }; + Some((descendant, buffer)) + }) })); - Some(cx.background_spawn(async move { - texts_future.await.into_iter().flatten().collect::>() - })) + cx.background_spawn(async move { + let (descendants, buffers) = descendants_future.await.into_iter().flatten().unzip(); + let context = AgentContext::Directory(DirectoryContext { + handle: self, + full_path: directory_full_path, + descendants, + }); + Some((context, buffers)) + }) + } +} + +impl Display for DirectoryContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut is_first = true; + for descendant in &self.descendants { + if !is_first { + write!(f, "\n")?; + } else { + is_first = false; + } + write!(f, "{}", descendant.fenced_codeblock)?; + } + Ok(()) } } #[derive(Debug, Clone)] -pub struct SymbolContext { +pub struct SymbolContextHandle { pub buffer: Entity, pub symbol: SharedString, pub range: Range, - /// The range that fully contain the symbol. e.g. for function symbol, this will include not - /// only the signature, but also the body. Not used by `PartialEq` or `Hash` for `AgentContextKey`. + /// The range that fully contains the symbol. e.g. for function symbol, this will include not + /// only the signature, but also the body. Not used by `PartialEq` or `Hash` for + /// `AgentContextKey`. pub enclosing_range: Range, pub context_id: ContextId, } -impl SymbolContext { +#[derive(Debug, Clone)] +pub struct SymbolContext { + pub handle: SymbolContextHandle, + pub full_path: Arc, + pub line_range: Range, + pub text: SharedString, +} + +impl SymbolContextHandle { pub fn eq_for_key(&self, other: &Self) -> bool { self.buffer == other.buffer && self.symbol == other.symbol && self.range == other.range } @@ -209,35 +357,69 @@ impl SymbolContext { self.range.hash(state); } - fn load(&self, cx: &App) -> Option)>> { + pub fn full_path(&self, cx: &App) -> Option { + Some(self.buffer.read(cx).file()?.full_path(cx)) + } + + pub fn enclosing_line_range(&self, cx: &App) -> Range { + self.enclosing_range + .to_point(&self.buffer.read(cx).snapshot()) + } + + pub fn text(&self, cx: &App) -> SharedString { + self.buffer + .read(cx) + .text_for_range(self.enclosing_range.clone()) + .collect::() + .into() + } + + fn load(self, cx: &App) -> Task>)>> { let buffer_ref = self.buffer.read(cx); let Some(file) = buffer_ref.file() else { log::error!("symbol context's file has no path"); - return None; + return Task::ready(None); }; - let full_path = file.full_path(cx); - let rope = buffer_ref - .text_for_range(self.enclosing_range.clone()) - .collect::(); + let full_path = file.full_path(cx).into(); let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot()); + let text = self.text(cx); let buffer = self.buffer.clone(); - Some(cx.background_spawn(async move { - ( - to_fenced_codeblock(&full_path, rope, Some(line_range)), - buffer, - ) - })) + let context = AgentContext::Symbol(SymbolContext { + handle: self, + full_path, + line_range, + text, + }); + Task::ready(Some((context, vec![buffer]))) + } +} + +impl Display for SymbolContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let code_block = MarkdownCodeBlock { + tag: &codeblock_tag(&self.full_path, Some(self.line_range.clone())), + text: &self.text, + }; + write!(f, "{code_block}",) } } #[derive(Debug, Clone)] -pub struct SelectionContext { +pub struct SelectionContextHandle { pub buffer: Entity, pub range: Range, pub context_id: ContextId, } -impl SelectionContext { +#[derive(Debug, Clone)] +pub struct SelectionContext { + pub handle: SelectionContextHandle, + pub full_path: Arc, + pub line_range: Range, + pub text: SharedString, +} + +impl SelectionContextHandle { pub fn eq_for_key(&self, other: &Self) -> bool { self.buffer == other.buffer && self.range == other.range } @@ -247,24 +429,47 @@ impl SelectionContext { self.range.hash(state); } - fn load(&self, cx: &App) -> Option)>> { - let buffer_ref = self.buffer.read(cx); - let Some(file) = buffer_ref.file() else { - log::error!("selection context's file has no path"); - return None; - }; - let full_path = file.full_path(cx); - let rope = buffer_ref + pub fn full_path(&self, cx: &App) -> Option { + Some(self.buffer.read(cx).file()?.full_path(cx)) + } + + pub fn line_range(&self, cx: &App) -> Range { + self.range.to_point(&self.buffer.read(cx).snapshot()) + } + + pub fn text(&self, cx: &App) -> SharedString { + self.buffer + .read(cx) .text_for_range(self.range.clone()) - .collect::(); - let line_range = self.range.to_point(&buffer_ref.snapshot()); + .collect::() + .into() + } + + fn load(self, cx: &App) -> Task>)>> { + let Some(full_path) = self.full_path(cx) else { + log::error!("selection context's file has no path"); + return Task::ready(None); + }; + let text = self.text(cx); let buffer = self.buffer.clone(); - Some(cx.background_spawn(async move { - ( - to_fenced_codeblock(&full_path, rope, Some(line_range)), - buffer, - ) - })) + let context = AgentContext::Selection(SelectionContext { + full_path: full_path.into(), + line_range: self.line_range(cx), + text, + handle: self, + }); + + Task::ready(Some((context, vec![buffer]))) + } +} + +impl Display for SelectionContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let code_block = MarkdownCodeBlock { + tag: &codeblock_tag(&self.full_path, Some(self.line_range.clone())), + text: &self.text, + }; + write!(f, "{code_block}",) } } @@ -288,21 +493,39 @@ impl FetchedUrlContext { } pub fn lookup_key(url: SharedString) -> AgentContextKey { - AgentContextKey(AgentContext::FetchedUrl(FetchedUrlContext { + AgentContextKey(AgentContextHandle::FetchedUrl(FetchedUrlContext { url, text: "".into(), context_id: ContextId::for_lookup(), })) } + + pub fn load(self) -> Task>)>> { + Task::ready(Some((AgentContext::FetchedUrl(self), vec![]))) + } +} + +impl Display for FetchedUrlContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // TODO: Better format - url and contents are not delimited. + write!(f, "{}\n{}\n", self.url, self.text) + } } #[derive(Debug, Clone)] -pub struct ThreadContext { +pub struct ThreadContextHandle { pub thread: Entity, pub context_id: ContextId, } -impl ThreadContext { +#[derive(Debug, Clone)] +pub struct ThreadContext { + pub handle: ThreadContextHandle, + pub title: SharedString, + pub text: SharedString, +} + +impl ThreadContextHandle { pub fn eq_for_key(&self, other: &Self) -> bool { self.thread == other.thread } @@ -311,32 +534,44 @@ impl ThreadContext { self.thread.hash(state) } - pub fn name(&self, cx: &App) -> SharedString { + pub fn title(&self, cx: &App) -> SharedString { self.thread .read(cx) .summary() .unwrap_or_else(|| "New thread".into()) } - pub fn load(&self, cx: &App) -> String { - let name = self.name(cx); - let contents = self.thread.read(cx).latest_detailed_summary_or_text(); - let mut text = String::new(); - text.push_str(&name); - text.push('\n'); - text.push_str(&contents.trim()); - text.push('\n'); - text + fn load(self, cx: &App) -> Task>)>> { + let context = AgentContext::Thread(ThreadContext { + title: self.title(cx), + text: self.thread.read(cx).latest_detailed_summary_or_text(), + handle: self, + }); + Task::ready(Some((context, vec![]))) + } +} + +impl Display for ThreadContext { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + // TODO: Better format for this - doesn't distinguish title and contents. + write!(f, "{}\n{}\n", &self.title, &self.text.trim()) } } #[derive(Debug, Clone)] -pub struct RulesContext { +pub struct RulesContextHandle { pub prompt_id: UserPromptId, pub context_id: ContextId, } -impl RulesContext { +#[derive(Debug, Clone)] +pub struct RulesContext { + pub handle: RulesContextHandle, + pub title: Option, + pub text: SharedString, +} + +impl RulesContextHandle { pub fn eq_for_key(&self, other: &Self) -> bool { self.prompt_id == other.prompt_id } @@ -346,17 +581,17 @@ impl RulesContext { } pub fn lookup_key(prompt_id: UserPromptId) -> AgentContextKey { - AgentContextKey(AgentContext::Rules(RulesContext { + AgentContextKey(AgentContextHandle::Rules(RulesContextHandle { prompt_id, context_id: ContextId::for_lookup(), })) } pub fn load( - &self, + self, prompt_store: &Option>, cx: &App, - ) -> Task> { + ) -> Task>)>> { let Some(prompt_store) = prompt_store.as_ref() else { return Task::ready(None); }; @@ -365,23 +600,34 @@ impl RulesContext { let Some(metadata) = prompt_store.metadata(prompt_id) else { return Task::ready(None); }; - let contents_task = prompt_store.load(prompt_id, cx); + let title = metadata.title; + let text_task = prompt_store.load(prompt_id, cx); cx.background_spawn(async move { - let contents = contents_task.await.ok()?; - let mut text = String::new(); - if let Some(title) = metadata.title { - text.push_str("Rules title: "); - text.push_str(&title); - text.push('\n'); - } - text.push_str("``````\n"); - text.push_str(contents.trim()); - text.push_str("\n``````\n"); - Some(text) + // TODO: report load errors instead of just logging + let text = text_task.await.log_err()?.into(); + let context = AgentContext::Rules(RulesContext { + handle: self, + title, + text, + }); + Some((context, vec![])) }) } } +impl Display for RulesContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(title) = &self.title { + write!(f, "Rules title: {}\n", title)?; + } + let code_block = MarkdownCodeBlock { + tag: "", + text: self.text.trim(), + }; + write!(f, "{code_block}") + } +} + #[derive(Debug, Clone)] pub struct ImageContext { pub original_image: Arc, @@ -417,6 +663,13 @@ impl ImageContext { Some(Some(_)) => ImageStatus::Ready, } } + + pub fn load(self, cx: &App) -> Task>)>> { + cx.background_spawn(async move { + self.image_task.clone().await; + Some((AgentContext::Image(self), vec![])) + }) + } } #[derive(Debug, Clone, Default)] @@ -463,64 +716,68 @@ impl LoadedContext { /// Loads and formats a collection of contexts. pub fn load_context( - contexts: Vec, + contexts: Vec, project: &Entity, prompt_store: &Option>, cx: &mut App, ) -> Task { - let mut file_tasks = Vec::new(); - let mut directory_tasks = Vec::new(); - let mut symbol_tasks = Vec::new(); - let mut selection_tasks = Vec::new(); - let mut fetch_context = Vec::new(); - let mut thread_context = Vec::new(); - let mut rules_tasks = Vec::new(); - let mut image_tasks = Vec::new(); + let mut load_tasks = Vec::new(); for context in contexts.iter().cloned() { match context { - AgentContext::File(context) => file_tasks.extend(context.load(cx)), - AgentContext::Directory(context) => { - directory_tasks.extend(context.load(project.clone(), cx)) + AgentContextHandle::File(context) => load_tasks.push(context.load(cx)), + AgentContextHandle::Directory(context) => { + load_tasks.push(context.load(project.clone(), cx)) } - AgentContext::Symbol(context) => symbol_tasks.extend(context.load(cx)), - AgentContext::Selection(context) => selection_tasks.extend(context.load(cx)), - AgentContext::FetchedUrl(context) => fetch_context.push(context), - AgentContext::Thread(context) => thread_context.push(context.load(cx)), - AgentContext::Rules(context) => rules_tasks.push(context.load(prompt_store, cx)), - AgentContext::Image(context) => image_tasks.push(context.image_task.clone()), + AgentContextHandle::Symbol(context) => load_tasks.push(context.load(cx)), + 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::Rules(context) => load_tasks.push(context.load(prompt_store, cx)), + AgentContextHandle::Image(context) => load_tasks.push(context.load(cx)), } } cx.background_spawn(async move { - let ( - file_context, - directory_context, - symbol_context, - selection_context, - rules_context, - images, - ) = futures::join!( - future::join_all(file_tasks), - future::join_all(directory_tasks), - future::join_all(symbol_tasks), - future::join_all(selection_tasks), - future::join_all(rules_tasks), - future::join_all(image_tasks) - ); + let load_results = future::join_all(load_tasks).await; - let directory_context = directory_context.into_iter().flatten().collect::>(); - let rules_context = rules_context.into_iter().flatten().collect::>(); - let images = images.into_iter().flatten().collect::>(); - - let mut referenced_buffers = HashSet::default(); + let mut contexts = Vec::new(); let mut text = String::new(); + let mut referenced_buffers = HashSet::default(); + for context in load_results { + let Some((context, buffers)) = context else { + continue; + }; + contexts.push(context); + referenced_buffers.extend(buffers); + } + + let mut file_context = Vec::new(); + let mut directory_context = Vec::new(); + let mut symbol_context = Vec::new(); + let mut selection_context = Vec::new(); + let mut fetched_url_context = Vec::new(); + let mut thread_context = Vec::new(); + let mut rules_context = Vec::new(); + let mut images = Vec::new(); + for context in &contexts { + match context { + AgentContext::File(context) => file_context.push(context), + AgentContext::Directory(context) => directory_context.push(context), + AgentContext::Symbol(context) => symbol_context.push(context), + AgentContext::Selection(context) => selection_context.push(context), + AgentContext::FetchedUrl(context) => fetched_url_context.push(context), + AgentContext::Thread(context) => thread_context.push(context), + AgentContext::Rules(context) => rules_context.push(context), + AgentContext::Image(context) => images.extend(context.image()), + } + } if file_context.is_empty() && directory_context.is_empty() && symbol_context.is_empty() && selection_context.is_empty() - && fetch_context.is_empty() + && fetched_url_context.is_empty() && thread_context.is_empty() && rules_context.is_empty() { @@ -542,60 +799,54 @@ pub fn load_context( if !file_context.is_empty() { text.push_str(""); - for (file_text, buffer) in file_context { + for context in file_context { text.push('\n'); - text.push_str(&file_text); - referenced_buffers.insert(buffer); + let _ = write!(text, "{context}"); } text.push_str("\n"); } if !directory_context.is_empty() { text.push_str(""); - for (file_text, buffer) in directory_context { + for context in directory_context { text.push('\n'); - text.push_str(&file_text); - referenced_buffers.insert(buffer); + let _ = write!(text, "{context}"); } text.push_str("\n"); } if !symbol_context.is_empty() { text.push_str(""); - for (symbol_text, buffer) in symbol_context { + for context in symbol_context { text.push('\n'); - text.push_str(&symbol_text); - referenced_buffers.insert(buffer); + let _ = write!(text, "{context}"); } text.push_str("\n"); } if !selection_context.is_empty() { text.push_str(""); - for (selection_text, buffer) in selection_context { + for context in selection_context { text.push('\n'); - text.push_str(&selection_text); - referenced_buffers.insert(buffer); + let _ = write!(text, "{context}"); } text.push_str("\n"); } - if !fetch_context.is_empty() { + if !fetched_url_context.is_empty() { text.push_str(""); - for context in fetch_context { + for context in fetched_url_context { text.push('\n'); - text.push_str(&context.url); - text.push('\n'); - text.push_str(&context.text); + let _ = write!(text, "{context}"); } text.push_str("\n"); } if !thread_context.is_empty() { text.push_str(""); - for thread_text in thread_context { + for context in thread_context { text.push('\n'); - text.push_str(&thread_text); + let _ = write!(text, "{context}"); } text.push_str("\n"); } @@ -605,9 +856,9 @@ pub fn load_context( "\n\ The user has specified the following rules that should be applied:\n", ); - for rules_text in rules_context { + for context in rules_context { text.push('\n'); - text.push_str(&rules_text); + let _ = write!(text, "{context}"); } text.push_str("\n"); } @@ -639,102 +890,34 @@ fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec> { files } -fn load_file_path_text_as_fenced_codeblock( - project: Entity, - worktree: Entity, - path: Arc, - cx: &mut App, -) -> Task)>> { - let worktree_ref = worktree.read(cx); - let worktree_id = worktree_ref.id(); - let full_path = worktree_ref.full_path(&path); +fn codeblock_tag(full_path: &Path, line_range: Option>) -> String { + let mut result = String::new(); - let open_task = project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - let project_path = ProjectPath { worktree_id, path }; - buffer_store.open_buffer(project_path, cx) - }) - }); + if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { + let _ = write!(result, "{} ", extension); + } - let rope_task = cx.spawn(async move |cx| { - let buffer = open_task.await.log_err()?; - let rope = buffer - .read_with(cx, |buffer, _cx| buffer.as_rope().clone()) - .log_err()?; - Some((rope, buffer)) - }); + let _ = write!(result, "{}", full_path.display()); - cx.background_spawn(async move { - let (rope, buffer) = rope_task.await?; - Some((to_fenced_codeblock(&full_path, rope, None), buffer)) - }) -} - -fn to_fenced_codeblock( - full_path: &Path, - content: Rope, - line_range: Option>, -) -> String { - let line_range_text = line_range.map(|range| { + if let Some(range) = line_range { if range.start.row == range.end.row { - format!(":{}", range.start.row + 1) + let _ = write!(result, ":{}", range.start.row + 1); } else { - format!(":{}-{}", range.start.row + 1, range.end.row + 1) + let _ = write!(result, ":{}-{}", range.start.row + 1, range.end.row + 1); } - }); - - let path_extension = full_path.extension().and_then(|ext| ext.to_str()); - let path_string = full_path.to_string_lossy(); - let capacity = 3 - + path_extension.map_or(0, |extension| extension.len() + 1) - + path_string.len() - + line_range_text.as_ref().map_or(0, |text| text.len()) - + 1 - + content.len() - + 5; - let mut buffer = String::with_capacity(capacity); - - buffer.push_str("```"); - - if let Some(extension) = path_extension { - buffer.push_str(extension); - buffer.push(' '); - } - buffer.push_str(&path_string); - - if let Some(line_range_text) = line_range_text { - buffer.push_str(&line_range_text); } - buffer.push('\n'); - for chunk in content.chunks() { - buffer.push_str(chunk); - } - - if !buffer.ends_with('\n') { - buffer.push('\n'); - } - - buffer.push_str("```\n"); - - debug_assert!( - buffer.len() == capacity - 1 || buffer.len() == capacity, - "to_fenced_codeblock calculated capacity of {}, but length was {}", - capacity, - buffer.len(), - ); - - buffer + result } /// Wraps `AgentContext` to opt-in to `PartialEq` and `Hash` impls which use a subset of fields /// needed for stable context identity. #[derive(Debug, Clone, RefCast)] #[repr(transparent)] -pub struct AgentContextKey(pub AgentContext); +pub struct AgentContextKey(pub AgentContextHandle); -impl AsRef for AgentContextKey { - fn as_ref(&self) -> &AgentContext { +impl AsRef for AgentContextKey { + fn as_ref(&self) -> &AgentContextHandle { &self.0 } } @@ -744,43 +927,43 @@ impl Eq for AgentContextKey {} impl PartialEq for AgentContextKey { fn eq(&self, other: &Self) -> bool { match &self.0 { - AgentContext::File(context) => { - if let AgentContext::File(other_context) = &other.0 { + AgentContextHandle::File(context) => { + if let AgentContextHandle::File(other_context) = &other.0 { return context.eq_for_key(other_context); } } - AgentContext::Directory(context) => { - if let AgentContext::Directory(other_context) = &other.0 { + AgentContextHandle::Directory(context) => { + if let AgentContextHandle::Directory(other_context) = &other.0 { return context.eq_for_key(other_context); } } - AgentContext::Symbol(context) => { - if let AgentContext::Symbol(other_context) = &other.0 { + AgentContextHandle::Symbol(context) => { + if let AgentContextHandle::Symbol(other_context) = &other.0 { return context.eq_for_key(other_context); } } - AgentContext::Selection(context) => { - if let AgentContext::Selection(other_context) = &other.0 { + AgentContextHandle::Selection(context) => { + if let AgentContextHandle::Selection(other_context) = &other.0 { return context.eq_for_key(other_context); } } - AgentContext::FetchedUrl(context) => { - if let AgentContext::FetchedUrl(other_context) = &other.0 { + AgentContextHandle::FetchedUrl(context) => { + if let AgentContextHandle::FetchedUrl(other_context) = &other.0 { return context.eq_for_key(other_context); } } - AgentContext::Thread(context) => { - if let AgentContext::Thread(other_context) = &other.0 { + AgentContextHandle::Thread(context) => { + if let AgentContextHandle::Thread(other_context) = &other.0 { return context.eq_for_key(other_context); } } - AgentContext::Rules(context) => { - if let AgentContext::Rules(other_context) = &other.0 { + AgentContextHandle::Rules(context) => { + if let AgentContextHandle::Rules(other_context) = &other.0 { return context.eq_for_key(other_context); } } - AgentContext::Image(context) => { - if let AgentContext::Image(other_context) = &other.0 { + AgentContextHandle::Image(context) => { + if let AgentContextHandle::Image(other_context) = &other.0 { return context.eq_for_key(other_context); } } @@ -792,14 +975,14 @@ impl PartialEq for AgentContextKey { impl Hash for AgentContextKey { fn hash(&self, state: &mut H) { match &self.0 { - AgentContext::File(context) => context.hash_for_key(state), - AgentContext::Directory(context) => context.hash_for_key(state), - AgentContext::Symbol(context) => context.hash_for_key(state), - AgentContext::Selection(context) => context.hash_for_key(state), - AgentContext::FetchedUrl(context) => context.hash_for_key(state), - AgentContext::Thread(context) => context.hash_for_key(state), - AgentContext::Rules(context) => context.hash_for_key(state), - AgentContext::Image(context) => context.hash_for_key(state), + AgentContextHandle::File(context) => context.hash_for_key(state), + AgentContextHandle::Directory(context) => context.hash_for_key(state), + AgentContextHandle::Symbol(context) => context.hash_for_key(state), + 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::Rules(context) => context.hash_for_key(state), + AgentContextHandle::Image(context) => context.hash_for_key(state), } } } diff --git a/crates/agent/src/context_store.rs b/crates/agent/src/context_store.rs index f8f60dc911..6ad9888d43 100644 --- a/crates/agent/src/context_store.rs +++ b/crates/agent/src/context_store.rs @@ -17,8 +17,9 @@ use util::ResultExt as _; use crate::ThreadStore; use crate::context::{ - AgentContext, AgentContextKey, ContextId, DirectoryContext, FetchedUrlContext, FileContext, - ImageContext, RulesContext, SelectionContext, SymbolContext, ThreadContext, + AgentContextHandle, AgentContextKey, ContextId, DirectoryContextHandle, FetchedUrlContext, + FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle, + SymbolContextHandle, ThreadContextHandle, }; use crate::context_strip::SuggestedContext; use crate::thread::{Thread, ThreadId}; @@ -47,7 +48,7 @@ impl ContextStore { } } - pub fn context(&self) -> impl Iterator { + pub fn context(&self) -> impl Iterator { self.context_set.iter().map(|entry| entry.as_ref()) } @@ -56,11 +57,16 @@ impl ContextStore { self.context_thread_ids.clear(); } - pub fn new_context_for_thread(&self, thread: &Thread) -> Vec { + pub fn new_context_for_thread(&self, thread: &Thread) -> Vec { let existing_context = thread .messages() - .flat_map(|message| &message.loaded_context.contexts) - .map(AgentContextKey::ref_cast) + .flat_map(|message| { + message + .loaded_context + .contexts + .iter() + .map(|context| AgentContextKey(context.handle())) + }) .collect::>(); self.context_set .iter() @@ -98,7 +104,7 @@ impl ContextStore { cx: &mut Context, ) { let context_id = self.next_context_id.post_inc(); - let context = AgentContext::File(FileContext { buffer, context_id }); + let context = AgentContextHandle::File(FileContextHandle { buffer, context_id }); let already_included = if self.has_context(&context) { if remove_if_exists { @@ -133,7 +139,7 @@ impl ContextStore { }; let context_id = self.next_context_id.post_inc(); - let context = AgentContext::Directory(DirectoryContext { + let context = AgentContextHandle::Directory(DirectoryContextHandle { entry_id, context_id, }); @@ -159,7 +165,7 @@ impl ContextStore { cx: &mut Context, ) -> bool { let context_id = self.next_context_id.post_inc(); - let context = AgentContext::Symbol(SymbolContext { + let context = AgentContextHandle::Symbol(SymbolContextHandle { buffer, symbol, range, @@ -184,7 +190,7 @@ impl ContextStore { cx: &mut Context, ) { let context_id = self.next_context_id.post_inc(); - let context = AgentContext::Thread(ThreadContext { thread, context_id }); + let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id }); if self.has_context(&context) { if remove_if_exists { @@ -237,7 +243,7 @@ impl ContextStore { cx: &mut Context, ) { let context_id = self.next_context_id.post_inc(); - let context = AgentContext::Rules(RulesContext { + let context = AgentContextHandle::Rules(RulesContextHandle { prompt_id, context_id, }); @@ -257,7 +263,7 @@ impl ContextStore { text: impl Into, cx: &mut Context, ) { - let context = AgentContext::FetchedUrl(FetchedUrlContext { + let context = AgentContextHandle::FetchedUrl(FetchedUrlContext { url: url.into(), text: text.into(), context_id: self.next_context_id.post_inc(), @@ -268,7 +274,7 @@ impl ContextStore { pub fn add_image(&mut self, image: Arc, cx: &mut Context) { let image_task = LanguageModelImage::from_image(image.clone(), cx).shared(); - let context = AgentContext::Image(ImageContext { + let context = AgentContextHandle::Image(ImageContext { original_image: image, image_task, context_id: self.next_context_id.post_inc(), @@ -283,7 +289,7 @@ impl ContextStore { cx: &mut Context, ) { let context_id = self.next_context_id.post_inc(); - let context = AgentContext::Selection(SelectionContext { + let context = AgentContextHandle::Selection(SelectionContextHandle { buffer, range, context_id, @@ -304,14 +310,17 @@ impl ContextStore { } => { if let Some(buffer) = buffer.upgrade() { let context_id = self.next_context_id.post_inc(); - self.insert_context(AgentContext::File(FileContext { buffer, context_id }), cx); + self.insert_context( + AgentContextHandle::File(FileContextHandle { buffer, context_id }), + cx, + ); }; } SuggestedContext::Thread { thread, name: _ } => { if let Some(thread) = thread.upgrade() { let context_id = self.next_context_id.post_inc(); self.insert_context( - AgentContext::Thread(ThreadContext { thread, context_id }), + AgentContextHandle::Thread(ThreadContextHandle { thread, context_id }), cx, ); } @@ -319,9 +328,9 @@ impl ContextStore { } } - fn insert_context(&mut self, context: AgentContext, cx: &mut Context) -> bool { + fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context) -> bool { match &context { - AgentContext::Thread(thread_context) => { + AgentContextHandle::Thread(thread_context) => { self.context_thread_ids .insert(thread_context.thread.read(cx).id().clone()); self.start_summarizing_thread_if_needed(&thread_context.thread, cx); @@ -335,13 +344,13 @@ impl ContextStore { inserted } - pub fn remove_context(&mut self, context: &AgentContext, cx: &mut Context) { + pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context) { if self .context_set .shift_remove(AgentContextKey::ref_cast(context)) { match context { - AgentContext::Thread(thread_context) => { + AgentContextHandle::Thread(thread_context) => { self.context_thread_ids .remove(thread_context.thread.read(cx).id()); } @@ -351,7 +360,7 @@ impl ContextStore { } } - pub fn has_context(&mut self, context: &AgentContext) -> bool { + pub fn has_context(&mut self, context: &AgentContextHandle) -> bool { self.context_set .contains(AgentContextKey::ref_cast(context)) } @@ -361,8 +370,10 @@ impl ContextStore { pub fn file_path_included(&self, path: &ProjectPath, cx: &App) -> Option { let project = self.project.upgrade()?.read(cx); self.context().find_map(|context| match context { - AgentContext::File(file_context) => FileInclusion::check_file(file_context, path, cx), - AgentContext::Directory(directory_context) => { + AgentContextHandle::File(file_context) => { + FileInclusion::check_file(file_context, path, cx) + } + AgentContextHandle::Directory(directory_context) => { FileInclusion::check_directory(directory_context, path, project, cx) } _ => None, @@ -376,7 +387,7 @@ impl ContextStore { ) -> Option { let project = self.project.upgrade()?.read(cx); self.context().find_map(|context| match context { - AgentContext::Directory(directory_context) => { + AgentContextHandle::Directory(directory_context) => { FileInclusion::check_directory(directory_context, path, project, cx) } _ => None, @@ -385,7 +396,7 @@ impl ContextStore { pub fn includes_symbol(&self, symbol: &Symbol, cx: &App) -> bool { self.context().any(|context| match context { - AgentContext::Symbol(context) => { + AgentContextHandle::Symbol(context) => { if context.symbol != symbol.name { return false; } @@ -410,7 +421,7 @@ impl ContextStore { pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool { self.context_set - .contains(&RulesContext::lookup_key(prompt_id)) + .contains(&RulesContextHandle::lookup_key(prompt_id)) } pub fn includes_url(&self, url: impl Into) -> bool { @@ -421,17 +432,17 @@ impl ContextStore { pub fn file_paths(&self, cx: &App) -> HashSet { self.context() .filter_map(|context| match context { - AgentContext::File(file) => { + AgentContextHandle::File(file) => { let buffer = file.buffer.read(cx); buffer.project_path(cx) } - AgentContext::Directory(_) - | AgentContext::Symbol(_) - | AgentContext::Selection(_) - | AgentContext::FetchedUrl(_) - | AgentContext::Thread(_) - | AgentContext::Rules(_) - | AgentContext::Image(_) => None, + AgentContextHandle::Directory(_) + | AgentContextHandle::Symbol(_) + | AgentContextHandle::Selection(_) + | AgentContextHandle::FetchedUrl(_) + | AgentContextHandle::Thread(_) + | AgentContextHandle::Rules(_) + | AgentContextHandle::Image(_) => None, }) .collect() } @@ -447,7 +458,7 @@ pub enum FileInclusion { } impl FileInclusion { - fn check_file(file_context: &FileContext, path: &ProjectPath, cx: &App) -> Option { + fn check_file(file_context: &FileContextHandle, path: &ProjectPath, cx: &App) -> Option { let file_path = file_context.buffer.read(cx).project_path(cx)?; if path == &file_path { Some(FileInclusion::Direct) @@ -457,7 +468,7 @@ impl FileInclusion { } fn check_directory( - directory_context: &DirectoryContext, + directory_context: &DirectoryContextHandle, path: &ProjectPath, project: &Project, cx: &App, diff --git a/crates/agent/src/context_strip.rs b/crates/agent/src/context_strip.rs index 8e0ca981a4..e8184d9055 100644 --- a/crates/agent/src/context_strip.rs +++ b/crates/agent/src/context_strip.rs @@ -14,7 +14,7 @@ use project::ProjectItem; use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; use workspace::Workspace; -use crate::context::{AgentContext, ContextKind}; +use crate::context::{AgentContextHandle, ContextKind}; use crate::context_picker::ContextPicker; use crate::context_store::ContextStore; use crate::thread::Thread; @@ -92,7 +92,9 @@ impl ContextStrip { self.context_store .read(cx) .context() - .flat_map(|context| AddedContext::new(context.clone(), prompt_store, project, cx)) + .flat_map(|context| { + AddedContext::new_pending(context.clone(), prompt_store, project, cx) + }) .collect::>() } else { Vec::new() @@ -288,7 +290,7 @@ impl ContextStrip { best.map(|(index, _, _)| index) } - fn open_context(&mut self, context: &AgentContext, window: &mut Window, cx: &mut App) { + fn open_context(&mut self, context: &AgentContextHandle, window: &mut Window, cx: &mut App) { let Some(workspace) = self.workspace.upgrade() else { return; }; @@ -309,7 +311,7 @@ impl ContextStrip { }; self.context_store.update(cx, |this, cx| { - this.remove_context(&context.context, cx); + this.remove_context(&context.handle, cx); }); let is_now_empty = added_contexts.len() == 1; @@ -462,7 +464,7 @@ impl Render for ContextStrip { .enumerate() .map(|(i, added_context)| { let name = added_context.name.clone(); - let context = added_context.context.clone(); + let context = added_context.handle.clone(); ContextPill::added( added_context, dupe_names.contains(&name), diff --git a/crates/agent/src/ui/context_pill.rs b/crates/agent/src/ui/context_pill.rs index c31af4b642..d7483cb2c6 100644 --- a/crates/agent/src/ui/context_pill.rs +++ b/crates/agent/src/ui/context_pill.rs @@ -1,13 +1,23 @@ -use std::{rc::Rc, time::Duration}; +use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration}; use file_icons::FileIcons; -use gpui::{Animation, AnimationExt as _, ClickEvent, Entity, MouseButton, pulsating_between}; +use futures::FutureExt as _; +use gpui::{ + Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task, + pulsating_between, +}; +use language_model::LanguageModelImage; use project::Project; use prompt_store::PromptStore; -use text::OffsetRangeExt; +use rope::Point; use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container}; -use crate::context::{AgentContext, ContextKind, ImageStatus}; +use crate::context::{ + AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext, + DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext, + ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle, + SymbolContext, SymbolContextHandle, ThreadContext, ThreadContextHandle, +}; #[derive(IntoElement)] pub enum ContextPill { @@ -72,7 +82,7 @@ impl ContextPill { pub fn id(&self) -> ElementId { match self { - Self::Added { context, .. } => context.context.element_id("context-pill".into()), + Self::Added { context, .. } => context.handle.element_id("context-pill".into()), Self::Suggested { .. } => "suggested-context-pill".into(), } } @@ -165,16 +175,11 @@ impl RenderOnce for ContextPill { .map(|element| match &context.status { ContextStatus::Ready => element .when_some( - context.render_preview.as_ref(), - |element, render_preview| { - element.hoverable_tooltip({ - let render_preview = render_preview.clone(); - move |_, cx| { - cx.new(|_| ContextPillPreview { - render_preview: render_preview.clone(), - }) - .into() - } + context.render_hover.as_ref(), + |element, render_hover| { + let render_hover = render_hover.clone(); + element.hoverable_tooltip(move |window, cx| { + render_hover(window, cx) }) }, ) @@ -197,7 +202,7 @@ impl RenderOnce for ContextPill { .when_some(on_remove.as_ref(), |element, on_remove| { element.child( IconButton::new( - context.context.element_id("remove".into()), + context.handle.element_id("remove".into()), IconName::Close, ) .shape(IconButtonShape::Square) @@ -262,18 +267,16 @@ pub enum ContextStatus { Error { message: SharedString }, } -// TODO: Component commented out due to new dependency on `Project`. -// -// #[derive(RegisterComponent)] +#[derive(RegisterComponent)] pub struct AddedContext { - pub context: AgentContext, + pub handle: AgentContextHandle, pub kind: ContextKind, pub name: SharedString, pub parent: Option, pub tooltip: Option, pub icon_path: Option, pub status: ContextStatus, - pub render_preview: Option AnyElement + 'static>>, + pub render_hover: Option AnyView + 'static>>, } impl AddedContext { @@ -281,221 +284,430 @@ impl AddedContext { /// `None` if `DirectoryContext` or `RulesContext` no longer exist. /// /// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak. - pub fn new( - context: AgentContext, + pub fn new_pending( + handle: AgentContextHandle, prompt_store: Option<&Entity>, project: &Project, cx: &App, ) -> Option { + match handle { + AgentContextHandle::File(handle) => Self::pending_file(handle, cx), + AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx), + AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx), + 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::Rules(handle) => Self::pending_rules(handle, prompt_store, cx), + AgentContextHandle::Image(handle) => Some(Self::image(handle)), + } + } + + pub fn new_attached(context: &AgentContext, cx: &App) -> AddedContext { match context { - AgentContext::File(ref file_context) => { - let full_path = file_context.buffer.read(cx).file()?.full_path(cx); - let full_path_string: SharedString = - full_path.to_string_lossy().into_owned().into(); - let name = full_path - .file_name() - .map(|n| n.to_string_lossy().into_owned().into()) - .unwrap_or_else(|| full_path_string.clone()); - let parent = full_path - .parent() - .and_then(|p| p.file_name()) - .map(|n| n.to_string_lossy().into_owned().into()); - Some(AddedContext { - kind: ContextKind::File, - name, - parent, - tooltip: Some(full_path_string), - icon_path: FileIcons::get_icon(&full_path, cx), - status: ContextStatus::Ready, - render_preview: None, - context, - }) - } + AgentContext::File(context) => Self::attached_file(context, cx), + AgentContext::Directory(context) => Self::attached_directory(context), + AgentContext::Symbol(context) => Self::attached_symbol(context, cx), + AgentContext::Selection(context) => Self::attached_selection(context, cx), + AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()), + AgentContext::Thread(context) => Self::attached_thread(context), + AgentContext::Rules(context) => Self::attached_rules(context), + AgentContext::Image(context) => Self::image(context.clone()), + } + } - AgentContext::Directory(ref directory_context) => { - let worktree = project - .worktree_for_entry(directory_context.entry_id, cx)? - .read(cx); - let entry = worktree.entry_for_id(directory_context.entry_id)?; - let full_path = worktree.full_path(&entry.path); - let full_path_string: SharedString = - full_path.to_string_lossy().into_owned().into(); - let name = full_path - .file_name() - .map(|n| n.to_string_lossy().into_owned().into()) - .unwrap_or_else(|| full_path_string.clone()); - let parent = full_path - .parent() - .and_then(|p| p.file_name()) - .map(|n| n.to_string_lossy().into_owned().into()); - Some(AddedContext { - kind: ContextKind::Directory, - name, - parent, - tooltip: Some(full_path_string), - icon_path: None, - status: ContextStatus::Ready, - render_preview: None, - context, - }) - } + fn pending_file(handle: FileContextHandle, cx: &App) -> Option { + let full_path = handle.buffer.read(cx).file()?.full_path(cx); + Some(Self::file(handle, &full_path, cx)) + } - AgentContext::Symbol(ref symbol_context) => Some(AddedContext { - kind: ContextKind::Symbol, - name: symbol_context.symbol.clone(), - parent: None, - tooltip: None, - icon_path: None, - status: ContextStatus::Ready, - render_preview: None, - context, - }), + fn attached_file(context: &FileContext, cx: &App) -> AddedContext { + Self::file(context.handle.clone(), &context.full_path, cx) + } - AgentContext::Selection(ref selection_context) => { - let buffer = selection_context.buffer.read(cx); - let full_path = buffer.file()?.full_path(cx); - let mut full_path_string = full_path.to_string_lossy().into_owned(); - let mut name = full_path - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_else(|| full_path_string.clone()); + fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext { + let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into(); + let name = full_path + .file_name() + .map(|n| n.to_string_lossy().into_owned().into()) + .unwrap_or_else(|| full_path_string.clone()); + let parent = full_path + .parent() + .and_then(|p| p.file_name()) + .map(|n| n.to_string_lossy().into_owned().into()); + AddedContext { + kind: ContextKind::File, + name, + parent, + tooltip: Some(full_path_string), + icon_path: FileIcons::get_icon(&full_path, cx), + status: ContextStatus::Ready, + render_hover: None, + handle: AgentContextHandle::File(handle), + } + } - let line_range = selection_context.range.to_point(&buffer.snapshot()); + fn pending_directory( + handle: DirectoryContextHandle, + project: &Project, + cx: &App, + ) -> Option { + let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx); + let entry = worktree.entry_for_id(handle.entry_id)?; + let full_path = worktree.full_path(&entry.path); + Some(Self::directory(handle, &full_path)) + } - let line_range_text = - format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1); + fn attached_directory(context: &DirectoryContext) -> AddedContext { + Self::directory(context.handle.clone(), &context.full_path) + } - full_path_string.push_str(&line_range_text); - name.push_str(&line_range_text); + fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext { + let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into(); + let name = full_path + .file_name() + .map(|n| n.to_string_lossy().into_owned().into()) + .unwrap_or_else(|| full_path_string.clone()); + let parent = full_path + .parent() + .and_then(|p| p.file_name()) + .map(|n| n.to_string_lossy().into_owned().into()); + AddedContext { + kind: ContextKind::Directory, + name, + parent, + tooltip: Some(full_path_string), + icon_path: None, + status: ContextStatus::Ready, + render_hover: None, + handle: AgentContextHandle::Directory(handle), + } + } - let parent = full_path - .parent() - .and_then(|p| p.file_name()) - .map(|n| n.to_string_lossy().into_owned().into()); + fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option { + let excerpt = + ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx); + Some(AddedContext { + kind: ContextKind::Symbol, + name: handle.symbol.clone(), + parent: Some(excerpt.file_name_and_range.clone()), + tooltip: None, + icon_path: None, + status: ContextStatus::Ready, + render_hover: { + let handle = handle.clone(); + Some(Rc::new(move |_, cx| { + excerpt.hover_view(handle.text(cx), cx).into() + })) + }, + handle: AgentContextHandle::Symbol(handle), + }) + } - Some(AddedContext { - kind: ContextKind::Selection, - name: name.into(), - parent, - tooltip: None, - icon_path: FileIcons::get_icon(&full_path, cx), - status: ContextStatus::Ready, - render_preview: None, - /* - render_preview: Some(Rc::new({ - let content = selection_context.text.clone(); - move |_, cx| { - div() - .id("context-pill-selection-preview") - .overflow_scroll() - .max_w_128() - .max_h_96() - .child(Label::new(content.clone()).buffer_font(cx)) - .into_any_element() - } - })), - */ - context, - }) - } + fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext { + let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx); + AddedContext { + kind: ContextKind::Symbol, + name: context.handle.symbol.clone(), + parent: Some(excerpt.file_name_and_range.clone()), + tooltip: None, + icon_path: None, + status: ContextStatus::Ready, + render_hover: { + let text = context.text.clone(); + Some(Rc::new(move |_, cx| { + excerpt.hover_view(text.clone(), cx).into() + })) + }, + handle: AgentContextHandle::Symbol(context.handle.clone()), + } + } - AgentContext::FetchedUrl(ref fetched_url_context) => Some(AddedContext { - kind: ContextKind::FetchedUrl, - name: fetched_url_context.url.clone(), - parent: None, - tooltip: None, - icon_path: None, - status: ContextStatus::Ready, - render_preview: None, - context, - }), + fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option { + let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx); + Some(AddedContext { + kind: ContextKind::Selection, + name: excerpt.file_name_and_range.clone(), + parent: excerpt.parent_name.clone(), + tooltip: None, + icon_path: excerpt.icon_path.clone(), + status: ContextStatus::Ready, + render_hover: { + let handle = handle.clone(); + Some(Rc::new(move |_, cx| { + excerpt.hover_view(handle.text(cx), cx).into() + })) + }, + handle: AgentContextHandle::Selection(handle), + }) + } - AgentContext::Thread(ref thread_context) => Some(AddedContext { - kind: ContextKind::Thread, - name: thread_context.name(cx), - parent: None, - tooltip: None, - icon_path: None, - status: if thread_context - .thread - .read(cx) - .is_generating_detailed_summary() - { - ContextStatus::Loading { - message: "Summarizing…".into(), - } - } else { - ContextStatus::Ready + fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext { + let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx); + AddedContext { + kind: ContextKind::Selection, + name: excerpt.file_name_and_range.clone(), + parent: excerpt.parent_name.clone(), + tooltip: None, + icon_path: excerpt.icon_path.clone(), + status: ContextStatus::Ready, + render_hover: { + let text = context.text.clone(); + Some(Rc::new(move |_, cx| { + excerpt.hover_view(text.clone(), cx).into() + })) + }, + handle: AgentContextHandle::Selection(context.handle.clone()), + } + } + + fn fetched_url(context: FetchedUrlContext) -> AddedContext { + AddedContext { + kind: ContextKind::FetchedUrl, + name: context.url.clone(), + parent: None, + tooltip: None, + icon_path: None, + status: ContextStatus::Ready, + render_hover: None, + handle: AgentContextHandle::FetchedUrl(context), + } + } + + fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext { + AddedContext { + kind: ContextKind::Thread, + name: handle.title(cx), + parent: None, + tooltip: None, + icon_path: None, + status: if handle.thread.read(cx).is_generating_detailed_summary() { + ContextStatus::Loading { + message: "Summarizing…".into(), + } + } else { + ContextStatus::Ready + }, + render_hover: { + let thread = handle.thread.clone(); + Some(Rc::new(move |_, cx| { + let text = thread.read(cx).latest_detailed_summary_or_text(); + text_hover_view(text.clone(), cx).into() + })) + }, + handle: AgentContextHandle::Thread(handle), + } + } + + fn attached_thread(context: &ThreadContext) -> AddedContext { + AddedContext { + kind: ContextKind::Thread, + 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| { + text_hover_view(text.clone(), cx).into() + })) + }, + handle: AgentContextHandle::Thread(context.handle.clone()), + } + } + + fn pending_rules( + handle: RulesContextHandle, + prompt_store: Option<&Entity>, + cx: &App, + ) -> Option { + let title = prompt_store + .as_ref()? + .read(cx) + .metadata(handle.prompt_id.into())? + .title + .unwrap_or_else(|| "Unnamed Rule".into()); + Some(AddedContext { + kind: ContextKind::Rules, + name: title.clone(), + parent: None, + tooltip: None, + icon_path: None, + status: ContextStatus::Ready, + render_hover: None, + handle: AgentContextHandle::Rules(handle), + }) + } + + fn attached_rules(context: &RulesContext) -> AddedContext { + let title = context + .title + .clone() + .unwrap_or_else(|| "Unnamed Rule".into()); + AddedContext { + kind: ContextKind::Rules, + name: title, + parent: None, + tooltip: None, + icon_path: None, + status: ContextStatus::Ready, + render_hover: { + let text = context.text.clone(); + Some(Rc::new(move |_, cx| { + text_hover_view(text.clone(), cx).into() + })) + }, + handle: AgentContextHandle::Rules(context.handle.clone()), + } + } + + fn image(context: ImageContext) -> AddedContext { + AddedContext { + kind: ContextKind::Image, + name: "Image".into(), + parent: None, + tooltip: None, + icon_path: None, + status: match context.status() { + ImageStatus::Loading => ContextStatus::Loading { + message: "Loading…".into(), }, - render_preview: None, - context, - }), - - AgentContext::Rules(ref user_rules_context) => { - let name = prompt_store - .as_ref()? - .read(cx) - .metadata(user_rules_context.prompt_id.into())? - .title?; - Some(AddedContext { - kind: ContextKind::Rules, - name: name.clone(), - parent: None, - tooltip: None, - icon_path: None, - status: ContextStatus::Ready, - render_preview: None, - context, - }) - } - - AgentContext::Image(ref image_context) => Some(AddedContext { - kind: ContextKind::Image, - name: "Image".into(), - parent: None, - tooltip: None, - icon_path: None, - status: match image_context.status() { - ImageStatus::Loading => ContextStatus::Loading { - message: "Loading…".into(), - }, - ImageStatus::Error => ContextStatus::Error { - message: "Failed to load image".into(), - }, - ImageStatus::Ready => ContextStatus::Ready, + ImageStatus::Error => ContextStatus::Error { + message: "Failed to load image".into(), }, - render_preview: Some(Rc::new({ - let image = image_context.original_image.clone(); - move |_, _| { + ImageStatus::Ready => ContextStatus::Ready, + }, + render_hover: Some(Rc::new({ + let image = context.original_image.clone(); + move |_, cx| { + let image = image.clone(); + ContextPillHover::new(cx, move |_, _| { gpui::img(image.clone()) .max_w_96() .max_h_96() .into_any_element() - } - })), - context, - }), + }) + .into() + } + })), + handle: AgentContextHandle::Image(context), } } } -struct ContextPillPreview { - render_preview: Rc AnyElement>, +#[derive(Debug, Clone)] +struct ContextFileExcerpt { + pub file_name_and_range: SharedString, + pub full_path_and_range: SharedString, + pub parent_name: Option, + pub icon_path: Option, } -impl Render for ContextPillPreview { +impl ContextFileExcerpt { + pub fn new(full_path: &Path, line_range: Range, cx: &App) -> Self { + let full_path_string = full_path.to_string_lossy().into_owned(); + let file_name = full_path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| full_path_string.clone()); + + let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1); + let mut full_path_and_range = full_path_string; + full_path_and_range.push_str(&line_range_text); + let mut file_name_and_range = file_name; + file_name_and_range.push_str(&line_range_text); + + let parent_name = full_path + .parent() + .and_then(|p| p.file_name()) + .map(|n| n.to_string_lossy().into_owned().into()); + + let icon_path = FileIcons::get_icon(&full_path, cx); + + ContextFileExcerpt { + file_name_and_range: file_name_and_range.into(), + full_path_and_range: full_path_and_range.into(), + parent_name, + icon_path, + } + } + + fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity { + let icon_path = self.icon_path.clone(); + let full_path_and_range = self.full_path_and_range.clone(); + ContextPillHover::new(cx, move |_, cx| { + v_flex() + .child( + h_flex() + .gap_0p5() + .w_full() + .max_w_full() + .border_b_1() + .border_color(cx.theme().colors().border.opacity(0.6)) + .children( + icon_path + .clone() + .map(Icon::from_path) + .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)), + ) + .child( + // TODO: make this truncate on the left. + Label::new(full_path_and_range.clone()) + .size(LabelSize::Small) + .ml_1(), + ), + ) + .child( + div() + .id("context-pill-hover-contents") + .overflow_scroll() + .max_w_128() + .max_h_96() + .child(Label::new(text.clone()).buffer_font(cx)), + ) + .into_any_element() + }) + } +} + +fn text_hover_view(content: SharedString, cx: &mut App) -> Entity { + ContextPillHover::new(cx, move |_, _| { + div() + .id("context-pill-hover-contents") + .overflow_scroll() + .max_w_128() + .max_h_96() + .child(content.clone()) + .into_any_element() + }) +} + +struct ContextPillHover { + render_hover: Box AnyElement>, +} + +impl ContextPillHover { + fn new( + cx: &mut App, + render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, + ) -> Entity { + cx.new(|_| Self { + render_hover: Box::new(render_hover), + }) + } +} + +impl Render for ContextPillHover { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { tooltip_container(window, cx, move |this, window, cx| { this.occlude() .on_mouse_move(|_, _, cx| cx.stop_propagation()) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .child((self.render_preview)(window, cx)) + .child((self.render_hover)(window, cx)) }) } } -// TODO: Component commented out due to new dependency on `Project`. -/* impl Component for AddedContext { fn scope() -> ComponentScope { ComponentScope::Agent @@ -505,47 +717,38 @@ impl Component for AddedContext { "AddedContext" } - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - let next_context_id = ContextId::zero(); + fn preview(_window: &mut Window, cx: &mut App) -> Option { + let mut next_context_id = ContextId::zero(); let image_ready = ( "Ready", - AddedContext::new( - AgentContext::Image(ImageContext { - context_id: next_context_id.post_inc(), - original_image: Arc::new(Image::empty()), - image_task: Task::ready(Some(LanguageModelImage::empty())).shared(), - }), - cx, - ), + AddedContext::image(ImageContext { + context_id: next_context_id.post_inc(), + original_image: Arc::new(Image::empty()), + image_task: Task::ready(Some(LanguageModelImage::empty())).shared(), + }), ); let image_loading = ( "Loading", - AddedContext::new( - AgentContext::Image(ImageContext { - context_id: next_context_id.post_inc(), - original_image: Arc::new(Image::empty()), - image_task: cx - .background_spawn(async move { - smol::Timer::after(Duration::from_secs(60 * 5)).await; - Some(LanguageModelImage::empty()) - }) - .shared(), - }), - cx, - ), + AddedContext::image(ImageContext { + context_id: next_context_id.post_inc(), + original_image: Arc::new(Image::empty()), + image_task: cx + .background_spawn(async move { + smol::Timer::after(Duration::from_secs(60 * 5)).await; + Some(LanguageModelImage::empty()) + }) + .shared(), + }), ); let image_error = ( "Error", - AddedContext::new( - AgentContext::Image(ImageContext { - context_id: next_context_id.post_inc(), - original_image: Arc::new(Image::empty()), - image_task: Task::ready(None).shared(), - }), - cx, - ), + AddedContext::image(ImageContext { + context_id: next_context_id.post_inc(), + original_image: Arc::new(Image::empty()), + image_task: Task::ready(None).shared(), + }), ); Some( @@ -563,8 +766,5 @@ impl Component for AddedContext { ) .into_any(), ) - - None } } -*/