agent: Improve attached context display and show hovers for symbol / selection / rules / thread (#29551)

* Brings back hover popover of selection context.

* Adds hover popover for symbol, rules, and thread context.

* Makes context attached to messages display the names / content at
attachment time.

* Adds the file name as the displayed parent of symbol context.

* Brings back `impl Component for AddedContext`

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
This commit is contained in:
Michael Sloan 2025-04-28 10:58:18 -06:00 committed by GitHub
parent 8afac388bb
commit abb48b7711
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 946 additions and 556 deletions

View file

@ -1,4 +1,4 @@
use crate::context::{AgentContext, RULES_ICON}; use crate::context::{AgentContextHandle, RULES_ICON};
use crate::context_picker::MentionLink; use crate::context_picker::MentionLink;
use crate::thread::{ use crate::thread::{
LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent, LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent,
@ -1491,19 +1491,13 @@ impl ActiveThread {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
let thread = self.thread.read(cx); 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 // Get all the data we need from thread before we start using it in closures
let checkpoint = thread.checkpoint_for_message(message_id); let checkpoint = thread.checkpoint_for_message(message_id);
let added_context = if let Some(workspace) = workspace.upgrade() { let added_context = thread
let project = workspace.read(cx).project().read(cx); .context_for_message(message_id)
thread .map(|context| AddedContext::new_attached(context, cx))
.context_for_message(message_id) .collect::<Vec<_>>();
.flat_map(|context| AddedContext::new(context.clone(), prompt_store, project, cx))
.collect::<Vec<_>>()
} else {
return Empty.into_any();
};
let tool_uses = thread.tool_uses_for_message(message_id, cx); let tool_uses = thread.tool_uses_for_message(message_id, cx);
let has_tool_uses = !tool_uses.is_empty(); let has_tool_uses = !tool_uses.is_empty();
@ -1713,7 +1707,7 @@ impl ActiveThread {
.when(!added_context.is_empty(), |parent| { .when(!added_context.is_empty(), |parent| {
parent.child(h_flex().flex_wrap().gap_1().children( parent.child(h_flex().flex_wrap().gap_1().children(
added_context.into_iter().map(|added_context| { 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( ContextPill::added(added_context, false, false, None).on_click(Rc::new(
cx.listener({ cx.listener({
let workspace = workspace.clone(); let workspace = workspace.clone();
@ -3188,13 +3182,13 @@ impl Render for ActiveThread {
} }
pub(crate) fn open_context( pub(crate) fn open_context(
context: &AgentContext, context: &AgentContextHandle,
workspace: Entity<Workspace>, workspace: Entity<Workspace>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) { ) {
match context { match context {
AgentContext::File(file_context) => { AgentContextHandle::File(file_context) => {
if let Some(project_path) = file_context.project_path(cx) { if let Some(project_path) = file_context.project_path(cx) {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
workspace 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; let entry_id = directory_context.entry_id;
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |_project, 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); let buffer = symbol_context.buffer.read(cx);
if let Some(project_path) = buffer.project_path(cx) { if let Some(project_path) = buffer.project_path(cx) {
let snapshot = buffer.snapshot(); 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); let buffer = selection_context.buffer.read(cx);
if let Some(project_path) = buffer.project_path(cx) { if let Some(project_path) = buffer.project_path(cx) {
let snapshot = buffer.snapshot(); 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); 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::<AssistantPanel>(cx) { if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
let thread_id = thread_context.thread.read(cx).id().clone(); 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 { Box::new(OpenRulesLibrary {
prompt_to_select: Some(rules_context.prompt_id.0), prompt_to_select: Some(rules_context.prompt_id.0),
}), }),
cx, cx,
), ),
AgentContext::Image(_) => {} AgentContextHandle::Image(_) => {}
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -17,8 +17,9 @@ use util::ResultExt as _;
use crate::ThreadStore; use crate::ThreadStore;
use crate::context::{ use crate::context::{
AgentContext, AgentContextKey, ContextId, DirectoryContext, FetchedUrlContext, FileContext, AgentContextHandle, AgentContextKey, ContextId, DirectoryContextHandle, FetchedUrlContext,
ImageContext, RulesContext, SelectionContext, SymbolContext, ThreadContext, FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle,
SymbolContextHandle, ThreadContextHandle,
}; };
use crate::context_strip::SuggestedContext; use crate::context_strip::SuggestedContext;
use crate::thread::{Thread, ThreadId}; use crate::thread::{Thread, ThreadId};
@ -47,7 +48,7 @@ impl ContextStore {
} }
} }
pub fn context(&self) -> impl Iterator<Item = &AgentContext> { pub fn context(&self) -> impl Iterator<Item = &AgentContextHandle> {
self.context_set.iter().map(|entry| entry.as_ref()) self.context_set.iter().map(|entry| entry.as_ref())
} }
@ -56,11 +57,16 @@ impl ContextStore {
self.context_thread_ids.clear(); self.context_thread_ids.clear();
} }
pub fn new_context_for_thread(&self, thread: &Thread) -> Vec<AgentContext> { pub fn new_context_for_thread(&self, thread: &Thread) -> Vec<AgentContextHandle> {
let existing_context = thread let existing_context = thread
.messages() .messages()
.flat_map(|message| &message.loaded_context.contexts) .flat_map(|message| {
.map(AgentContextKey::ref_cast) message
.loaded_context
.contexts
.iter()
.map(|context| AgentContextKey(context.handle()))
})
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
self.context_set self.context_set
.iter() .iter()
@ -98,7 +104,7 @@ impl ContextStore {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let context_id = self.next_context_id.post_inc(); 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) { let already_included = if self.has_context(&context) {
if remove_if_exists { if remove_if_exists {
@ -133,7 +139,7 @@ impl ContextStore {
}; };
let context_id = self.next_context_id.post_inc(); let context_id = self.next_context_id.post_inc();
let context = AgentContext::Directory(DirectoryContext { let context = AgentContextHandle::Directory(DirectoryContextHandle {
entry_id, entry_id,
context_id, context_id,
}); });
@ -159,7 +165,7 @@ impl ContextStore {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> bool { ) -> bool {
let context_id = self.next_context_id.post_inc(); let context_id = self.next_context_id.post_inc();
let context = AgentContext::Symbol(SymbolContext { let context = AgentContextHandle::Symbol(SymbolContextHandle {
buffer, buffer,
symbol, symbol,
range, range,
@ -184,7 +190,7 @@ impl ContextStore {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let context_id = self.next_context_id.post_inc(); 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 self.has_context(&context) {
if remove_if_exists { if remove_if_exists {
@ -237,7 +243,7 @@ impl ContextStore {
cx: &mut Context<ContextStore>, cx: &mut Context<ContextStore>,
) { ) {
let context_id = self.next_context_id.post_inc(); let context_id = self.next_context_id.post_inc();
let context = AgentContext::Rules(RulesContext { let context = AgentContextHandle::Rules(RulesContextHandle {
prompt_id, prompt_id,
context_id, context_id,
}); });
@ -257,7 +263,7 @@ impl ContextStore {
text: impl Into<SharedString>, text: impl Into<SharedString>,
cx: &mut Context<ContextStore>, cx: &mut Context<ContextStore>,
) { ) {
let context = AgentContext::FetchedUrl(FetchedUrlContext { let context = AgentContextHandle::FetchedUrl(FetchedUrlContext {
url: url.into(), url: url.into(),
text: text.into(), text: text.into(),
context_id: self.next_context_id.post_inc(), context_id: self.next_context_id.post_inc(),
@ -268,7 +274,7 @@ impl ContextStore {
pub fn add_image(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) { pub fn add_image(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
let image_task = LanguageModelImage::from_image(image.clone(), cx).shared(); let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
let context = AgentContext::Image(ImageContext { let context = AgentContextHandle::Image(ImageContext {
original_image: image, original_image: image,
image_task, image_task,
context_id: self.next_context_id.post_inc(), context_id: self.next_context_id.post_inc(),
@ -283,7 +289,7 @@ impl ContextStore {
cx: &mut Context<ContextStore>, cx: &mut Context<ContextStore>,
) { ) {
let context_id = self.next_context_id.post_inc(); let context_id = self.next_context_id.post_inc();
let context = AgentContext::Selection(SelectionContext { let context = AgentContextHandle::Selection(SelectionContextHandle {
buffer, buffer,
range, range,
context_id, context_id,
@ -304,14 +310,17 @@ impl ContextStore {
} => { } => {
if let Some(buffer) = buffer.upgrade() { if let Some(buffer) = buffer.upgrade() {
let context_id = self.next_context_id.post_inc(); 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: _ } => { SuggestedContext::Thread { thread, name: _ } => {
if let Some(thread) = thread.upgrade() { if let Some(thread) = thread.upgrade() {
let context_id = self.next_context_id.post_inc(); let context_id = self.next_context_id.post_inc();
self.insert_context( self.insert_context(
AgentContext::Thread(ThreadContext { thread, context_id }), AgentContextHandle::Thread(ThreadContextHandle { thread, context_id }),
cx, cx,
); );
} }
@ -319,9 +328,9 @@ impl ContextStore {
} }
} }
fn insert_context(&mut self, context: AgentContext, cx: &mut Context<Self>) -> bool { fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context<Self>) -> bool {
match &context { match &context {
AgentContext::Thread(thread_context) => { AgentContextHandle::Thread(thread_context) => {
self.context_thread_ids self.context_thread_ids
.insert(thread_context.thread.read(cx).id().clone()); .insert(thread_context.thread.read(cx).id().clone());
self.start_summarizing_thread_if_needed(&thread_context.thread, cx); self.start_summarizing_thread_if_needed(&thread_context.thread, cx);
@ -335,13 +344,13 @@ impl ContextStore {
inserted inserted
} }
pub fn remove_context(&mut self, context: &AgentContext, cx: &mut Context<Self>) { pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context<Self>) {
if self if self
.context_set .context_set
.shift_remove(AgentContextKey::ref_cast(context)) .shift_remove(AgentContextKey::ref_cast(context))
{ {
match context { match context {
AgentContext::Thread(thread_context) => { AgentContextHandle::Thread(thread_context) => {
self.context_thread_ids self.context_thread_ids
.remove(thread_context.thread.read(cx).id()); .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 self.context_set
.contains(AgentContextKey::ref_cast(context)) .contains(AgentContextKey::ref_cast(context))
} }
@ -361,8 +370,10 @@ impl ContextStore {
pub fn file_path_included(&self, path: &ProjectPath, cx: &App) -> Option<FileInclusion> { pub fn file_path_included(&self, path: &ProjectPath, cx: &App) -> Option<FileInclusion> {
let project = self.project.upgrade()?.read(cx); let project = self.project.upgrade()?.read(cx);
self.context().find_map(|context| match context { self.context().find_map(|context| match context {
AgentContext::File(file_context) => FileInclusion::check_file(file_context, path, cx), AgentContextHandle::File(file_context) => {
AgentContext::Directory(directory_context) => { FileInclusion::check_file(file_context, path, cx)
}
AgentContextHandle::Directory(directory_context) => {
FileInclusion::check_directory(directory_context, path, project, cx) FileInclusion::check_directory(directory_context, path, project, cx)
} }
_ => None, _ => None,
@ -376,7 +387,7 @@ impl ContextStore {
) -> Option<FileInclusion> { ) -> Option<FileInclusion> {
let project = self.project.upgrade()?.read(cx); let project = self.project.upgrade()?.read(cx);
self.context().find_map(|context| match context { self.context().find_map(|context| match context {
AgentContext::Directory(directory_context) => { AgentContextHandle::Directory(directory_context) => {
FileInclusion::check_directory(directory_context, path, project, cx) FileInclusion::check_directory(directory_context, path, project, cx)
} }
_ => None, _ => None,
@ -385,7 +396,7 @@ impl ContextStore {
pub fn includes_symbol(&self, symbol: &Symbol, cx: &App) -> bool { pub fn includes_symbol(&self, symbol: &Symbol, cx: &App) -> bool {
self.context().any(|context| match context { self.context().any(|context| match context {
AgentContext::Symbol(context) => { AgentContextHandle::Symbol(context) => {
if context.symbol != symbol.name { if context.symbol != symbol.name {
return false; return false;
} }
@ -410,7 +421,7 @@ impl ContextStore {
pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool { pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool {
self.context_set self.context_set
.contains(&RulesContext::lookup_key(prompt_id)) .contains(&RulesContextHandle::lookup_key(prompt_id))
} }
pub fn includes_url(&self, url: impl Into<SharedString>) -> bool { pub fn includes_url(&self, url: impl Into<SharedString>) -> bool {
@ -421,17 +432,17 @@ impl ContextStore {
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> { pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
self.context() self.context()
.filter_map(|context| match context { .filter_map(|context| match context {
AgentContext::File(file) => { AgentContextHandle::File(file) => {
let buffer = file.buffer.read(cx); let buffer = file.buffer.read(cx);
buffer.project_path(cx) buffer.project_path(cx)
} }
AgentContext::Directory(_) AgentContextHandle::Directory(_)
| AgentContext::Symbol(_) | AgentContextHandle::Symbol(_)
| AgentContext::Selection(_) | AgentContextHandle::Selection(_)
| AgentContext::FetchedUrl(_) | AgentContextHandle::FetchedUrl(_)
| AgentContext::Thread(_) | AgentContextHandle::Thread(_)
| AgentContext::Rules(_) | AgentContextHandle::Rules(_)
| AgentContext::Image(_) => None, | AgentContextHandle::Image(_) => None,
}) })
.collect() .collect()
} }
@ -447,7 +458,7 @@ pub enum FileInclusion {
} }
impl FileInclusion { impl FileInclusion {
fn check_file(file_context: &FileContext, path: &ProjectPath, cx: &App) -> Option<Self> { fn check_file(file_context: &FileContextHandle, path: &ProjectPath, cx: &App) -> Option<Self> {
let file_path = file_context.buffer.read(cx).project_path(cx)?; let file_path = file_context.buffer.read(cx).project_path(cx)?;
if path == &file_path { if path == &file_path {
Some(FileInclusion::Direct) Some(FileInclusion::Direct)
@ -457,7 +468,7 @@ impl FileInclusion {
} }
fn check_directory( fn check_directory(
directory_context: &DirectoryContext, directory_context: &DirectoryContextHandle,
path: &ProjectPath, path: &ProjectPath,
project: &Project, project: &Project,
cx: &App, cx: &App,

View file

@ -14,7 +14,7 @@ use project::ProjectItem;
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use workspace::Workspace; use workspace::Workspace;
use crate::context::{AgentContext, ContextKind}; use crate::context::{AgentContextHandle, ContextKind};
use crate::context_picker::ContextPicker; use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore; use crate::context_store::ContextStore;
use crate::thread::Thread; use crate::thread::Thread;
@ -92,7 +92,9 @@ impl ContextStrip {
self.context_store self.context_store
.read(cx) .read(cx)
.context() .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::<Vec<_>>() .collect::<Vec<_>>()
} else { } else {
Vec::new() Vec::new()
@ -288,7 +290,7 @@ impl ContextStrip {
best.map(|(index, _, _)| index) 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 { let Some(workspace) = self.workspace.upgrade() else {
return; return;
}; };
@ -309,7 +311,7 @@ impl ContextStrip {
}; };
self.context_store.update(cx, |this, cx| { 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; let is_now_empty = added_contexts.len() == 1;
@ -462,7 +464,7 @@ impl Render for ContextStrip {
.enumerate() .enumerate()
.map(|(i, added_context)| { .map(|(i, added_context)| {
let name = added_context.name.clone(); let name = added_context.name.clone();
let context = added_context.context.clone(); let context = added_context.handle.clone();
ContextPill::added( ContextPill::added(
added_context, added_context,
dupe_names.contains(&name), dupe_names.contains(&name),

View file

@ -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 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 project::Project;
use prompt_store::PromptStore; use prompt_store::PromptStore;
use text::OffsetRangeExt; use rope::Point;
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container}; 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)] #[derive(IntoElement)]
pub enum ContextPill { pub enum ContextPill {
@ -72,7 +82,7 @@ impl ContextPill {
pub fn id(&self) -> ElementId { pub fn id(&self) -> ElementId {
match self { 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(), Self::Suggested { .. } => "suggested-context-pill".into(),
} }
} }
@ -165,16 +175,11 @@ impl RenderOnce for ContextPill {
.map(|element| match &context.status { .map(|element| match &context.status {
ContextStatus::Ready => element ContextStatus::Ready => element
.when_some( .when_some(
context.render_preview.as_ref(), context.render_hover.as_ref(),
|element, render_preview| { |element, render_hover| {
element.hoverable_tooltip({ let render_hover = render_hover.clone();
let render_preview = render_preview.clone(); element.hoverable_tooltip(move |window, cx| {
move |_, cx| { render_hover(window, cx)
cx.new(|_| ContextPillPreview {
render_preview: render_preview.clone(),
})
.into()
}
}) })
}, },
) )
@ -197,7 +202,7 @@ impl RenderOnce for ContextPill {
.when_some(on_remove.as_ref(), |element, on_remove| { .when_some(on_remove.as_ref(), |element, on_remove| {
element.child( element.child(
IconButton::new( IconButton::new(
context.context.element_id("remove".into()), context.handle.element_id("remove".into()),
IconName::Close, IconName::Close,
) )
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
@ -262,18 +267,16 @@ pub enum ContextStatus {
Error { message: SharedString }, Error { message: SharedString },
} }
// TODO: Component commented out due to new dependency on `Project`. #[derive(RegisterComponent)]
//
// #[derive(RegisterComponent)]
pub struct AddedContext { pub struct AddedContext {
pub context: AgentContext, pub handle: AgentContextHandle,
pub kind: ContextKind, pub kind: ContextKind,
pub name: SharedString, pub name: SharedString,
pub parent: Option<SharedString>, pub parent: Option<SharedString>,
pub tooltip: Option<SharedString>, pub tooltip: Option<SharedString>,
pub icon_path: Option<SharedString>, pub icon_path: Option<SharedString>,
pub status: ContextStatus, pub status: ContextStatus,
pub render_preview: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>, pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
} }
impl AddedContext { impl AddedContext {
@ -281,221 +284,430 @@ impl AddedContext {
/// `None` if `DirectoryContext` or `RulesContext` no longer exist. /// `None` if `DirectoryContext` or `RulesContext` no longer exist.
/// ///
/// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak. /// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
pub fn new( pub fn new_pending(
context: AgentContext, handle: AgentContextHandle,
prompt_store: Option<&Entity<PromptStore>>, prompt_store: Option<&Entity<PromptStore>>,
project: &Project, project: &Project,
cx: &App, cx: &App,
) -> Option<AddedContext> { ) -> Option<AddedContext> {
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 { match context {
AgentContext::File(ref file_context) => { AgentContext::File(context) => Self::attached_file(context, cx),
let full_path = file_context.buffer.read(cx).file()?.full_path(cx); AgentContext::Directory(context) => Self::attached_directory(context),
let full_path_string: SharedString = AgentContext::Symbol(context) => Self::attached_symbol(context, cx),
full_path.to_string_lossy().into_owned().into(); AgentContext::Selection(context) => Self::attached_selection(context, cx),
let name = full_path AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()),
.file_name() AgentContext::Thread(context) => Self::attached_thread(context),
.map(|n| n.to_string_lossy().into_owned().into()) AgentContext::Rules(context) => Self::attached_rules(context),
.unwrap_or_else(|| full_path_string.clone()); AgentContext::Image(context) => Self::image(context.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::Directory(ref directory_context) => { fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
let worktree = project let full_path = handle.buffer.read(cx).file()?.full_path(cx);
.worktree_for_entry(directory_context.entry_id, cx)? Some(Self::file(handle, &full_path, 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,
})
}
AgentContext::Symbol(ref symbol_context) => Some(AddedContext { fn attached_file(context: &FileContext, cx: &App) -> AddedContext {
kind: ContextKind::Symbol, Self::file(context.handle.clone(), &context.full_path, cx)
name: symbol_context.symbol.clone(), }
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_preview: None,
context,
}),
AgentContext::Selection(ref selection_context) => { fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
let buffer = selection_context.buffer.read(cx); let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let full_path = buffer.file()?.full_path(cx); let name = full_path
let mut full_path_string = full_path.to_string_lossy().into_owned(); .file_name()
let mut name = full_path .map(|n| n.to_string_lossy().into_owned().into())
.file_name() .unwrap_or_else(|| full_path_string.clone());
.map(|n| n.to_string_lossy().into_owned()) let parent = full_path
.unwrap_or_else(|| full_path_string.clone()); .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<AddedContext> {
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 = fn attached_directory(context: &DirectoryContext) -> AddedContext {
format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1); Self::directory(context.handle.clone(), &context.full_path)
}
full_path_string.push_str(&line_range_text); fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
name.push_str(&line_range_text); 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 fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
.parent() let excerpt =
.and_then(|p| p.file_name()) ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
.map(|n| n.to_string_lossy().into_owned().into()); 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 { fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext {
kind: ContextKind::Selection, let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
name: name.into(), AddedContext {
parent, kind: ContextKind::Symbol,
tooltip: None, name: context.handle.symbol.clone(),
icon_path: FileIcons::get_icon(&full_path, cx), parent: Some(excerpt.file_name_and_range.clone()),
status: ContextStatus::Ready, tooltip: None,
render_preview: None, icon_path: None,
/* status: ContextStatus::Ready,
render_preview: Some(Rc::new({ render_hover: {
let content = selection_context.text.clone(); let text = context.text.clone();
move |_, cx| { Some(Rc::new(move |_, cx| {
div() excerpt.hover_view(text.clone(), cx).into()
.id("context-pill-selection-preview") }))
.overflow_scroll() },
.max_w_128() handle: AgentContextHandle::Symbol(context.handle.clone()),
.max_h_96() }
.child(Label::new(content.clone()).buffer_font(cx)) }
.into_any_element()
}
})),
*/
context,
})
}
AgentContext::FetchedUrl(ref fetched_url_context) => Some(AddedContext { fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
kind: ContextKind::FetchedUrl, let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
name: fetched_url_context.url.clone(), Some(AddedContext {
parent: None, kind: ContextKind::Selection,
tooltip: None, name: excerpt.file_name_and_range.clone(),
icon_path: None, parent: excerpt.parent_name.clone(),
status: ContextStatus::Ready, tooltip: None,
render_preview: None, icon_path: excerpt.icon_path.clone(),
context, 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 { fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext {
kind: ContextKind::Thread, let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
name: thread_context.name(cx), AddedContext {
parent: None, kind: ContextKind::Selection,
tooltip: None, name: excerpt.file_name_and_range.clone(),
icon_path: None, parent: excerpt.parent_name.clone(),
status: if thread_context tooltip: None,
.thread icon_path: excerpt.icon_path.clone(),
.read(cx) status: ContextStatus::Ready,
.is_generating_detailed_summary() render_hover: {
{ let text = context.text.clone();
ContextStatus::Loading { Some(Rc::new(move |_, cx| {
message: "Summarizing…".into(), excerpt.hover_view(text.clone(), cx).into()
} }))
} else { },
ContextStatus::Ready 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<PromptStore>>,
cx: &App,
) -> Option<AddedContext> {
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, ImageStatus::Error => ContextStatus::Error {
context, message: "Failed to load image".into(),
}),
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,
}, },
render_preview: Some(Rc::new({ ImageStatus::Ready => ContextStatus::Ready,
let image = image_context.original_image.clone(); },
move |_, _| { 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()) gpui::img(image.clone())
.max_w_96() .max_w_96()
.max_h_96() .max_h_96()
.into_any_element() .into_any_element()
} })
})), .into()
context, }
}), })),
handle: AgentContextHandle::Image(context),
} }
} }
} }
struct ContextPillPreview { #[derive(Debug, Clone)]
render_preview: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>, struct ContextFileExcerpt {
pub file_name_and_range: SharedString,
pub full_path_and_range: SharedString,
pub parent_name: Option<SharedString>,
pub icon_path: Option<SharedString>,
} }
impl Render for ContextPillPreview { impl ContextFileExcerpt {
pub fn new(full_path: &Path, line_range: Range<Point>, 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<ContextPillHover> {
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> {
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<dyn Fn(&mut Window, &mut App) -> AnyElement>,
}
impl ContextPillHover {
fn new(
cx: &mut App,
render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Entity<Self> {
cx.new(|_| Self {
render_hover: Box::new(render_hover),
})
}
}
impl Render for ContextPillHover {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(window, cx, move |this, window, cx| { tooltip_container(window, cx, move |this, window, cx| {
this.occlude() this.occlude()
.on_mouse_move(|_, _, cx| cx.stop_propagation()) .on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, 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 { impl Component for AddedContext {
fn scope() -> ComponentScope { fn scope() -> ComponentScope {
ComponentScope::Agent ComponentScope::Agent
@ -505,47 +717,38 @@ impl Component for AddedContext {
"AddedContext" "AddedContext"
} }
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> { fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let next_context_id = ContextId::zero(); let mut next_context_id = ContextId::zero();
let image_ready = ( let image_ready = (
"Ready", "Ready",
AddedContext::new( AddedContext::image(ImageContext {
AgentContext::Image(ImageContext { context_id: next_context_id.post_inc(),
context_id: next_context_id.post_inc(), original_image: Arc::new(Image::empty()),
original_image: Arc::new(Image::empty()), image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(), }),
}),
cx,
),
); );
let image_loading = ( let image_loading = (
"Loading", "Loading",
AddedContext::new( AddedContext::image(ImageContext {
AgentContext::Image(ImageContext { context_id: next_context_id.post_inc(),
context_id: next_context_id.post_inc(), original_image: Arc::new(Image::empty()),
original_image: Arc::new(Image::empty()), image_task: cx
image_task: cx .background_spawn(async move {
.background_spawn(async move { smol::Timer::after(Duration::from_secs(60 * 5)).await;
smol::Timer::after(Duration::from_secs(60 * 5)).await; Some(LanguageModelImage::empty())
Some(LanguageModelImage::empty()) })
}) .shared(),
.shared(), }),
}),
cx,
),
); );
let image_error = ( let image_error = (
"Error", "Error",
AddedContext::new( AddedContext::image(ImageContext {
AgentContext::Image(ImageContext { context_id: next_context_id.post_inc(),
context_id: next_context_id.post_inc(), original_image: Arc::new(Image::empty()),
original_image: Arc::new(Image::empty()), image_task: Task::ready(None).shared(),
image_task: Task::ready(None).shared(), }),
}),
cx,
),
); );
Some( Some(
@ -563,8 +766,5 @@ impl Component for AddedContext {
) )
.into_any(), .into_any(),
) )
None
} }
} }
*/