assistant2: Suggest recent files and threads as context (#22959)

The context picker will now display up to 6 recent files/threads to add
as a context:

<img
src="https://github.com/user-attachments/assets/80c87bf9-70ad-4e81-ba24-7a624378b991"
width=400>



Note: We decided to use a `ContextMenu` instead of `Picker` for the
initial one since the latter didn't quite fit the design for the
"Recent" section.

Release Notes:

- N/A

---------

Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Agus Zubiaga 2025-01-10 11:26:53 -03:00 committed by GitHub
parent 49198a7961
commit a267911e83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 649 additions and 350 deletions

View file

@ -222,7 +222,7 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
this.reset_mode(cx);
cx.emit(DismissEvent);
})
.ok();

View file

@ -225,7 +225,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
this.reset_mode(cx);
cx.emit(DismissEvent);
})
.ok();

View file

@ -4,7 +4,9 @@ use std::sync::Arc;
use file_icons::FileIcons;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use gpui::{
AppContext, DismissEvent, FocusHandle, FocusableView, Stateful, Task, View, WeakModel, WeakView,
};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{prelude::*, ListItem, Tooltip};
@ -238,7 +240,7 @@ impl PickerDelegate for FileContextPickerDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
this.reset_mode(cx);
cx.emit(DismissEvent);
})
.ok();
@ -252,82 +254,97 @@ impl PickerDelegate for FileContextPickerDelegate {
) -> Option<Self::ListItem> {
let path_match = &self.matches[ix];
let (file_name, directory) = if path_match.path.as_ref() == Path::new("") {
(SharedString::from(path_match.path_prefix.clone()), None)
} else {
let file_name = path_match
.path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
.into();
let mut directory = format!("{}/", path_match.path_prefix);
if let Some(parent) = path_match
.path
.parent()
.filter(|parent| parent != &Path::new(""))
{
directory.push_str(&parent.to_string_lossy());
directory.push('/');
}
(file_name, Some(directory))
};
let added = self.context_store.upgrade().and_then(|context_store| {
context_store
.read(cx)
.will_include_file_path(&path_match.path, cx)
});
let file_icon = FileIcons::get_icon(&path_match.path.clone(), cx)
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(
h_flex()
.gap_2()
.child(file_icon.size(IconSize::Small))
.child(Label::new(file_name))
.children(directory.map(|directory| {
Label::new(directory)
.size(LabelSize::Small)
.color(Color::Muted)
})),
)
.when_some(added, |el, added| match added {
FileInclusion::Direct(_) => el.end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
),
FileInclusion::InDirectory(dir_name) => {
let dir_name = dir_name.to_string_lossy().into_owned();
el.end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Included").size(LabelSize::Small)),
)
.tooltip(move |cx| Tooltip::text(format!("in {dir_name}"), cx))
}
}),
.child(render_file_context_entry(
ElementId::NamedInteger("file-ctx-picker".into(), ix),
&path_match.path,
&path_match.path_prefix,
self.context_store.clone(),
cx,
)),
)
}
}
pub fn render_file_context_entry(
id: ElementId,
path: &Path,
path_prefix: &Arc<str>,
context_store: WeakModel<ContextStore>,
cx: &WindowContext,
) -> Stateful<Div> {
let (file_name, directory) = if path == Path::new("") {
(SharedString::from(path_prefix.clone()), None)
} else {
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
.into();
let mut directory = format!("{}/", path_prefix);
if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
directory.push_str(&parent.to_string_lossy());
directory.push('/');
}
(file_name, Some(directory))
};
let added = context_store
.upgrade()
.and_then(|context_store| context_store.read(cx).will_include_file_path(path, cx));
let file_icon = FileIcons::get_icon(&path, cx)
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));
h_flex()
.id(id)
.gap_1()
.w_full()
.child(file_icon.size(IconSize::Small))
.child(
h_flex()
.gap_2()
.child(Label::new(file_name))
.children(directory.map(|directory| {
Label::new(directory)
.size(LabelSize::Small)
.color(Color::Muted)
})),
)
.child(div().w_full())
.when_some(added, |el, added| match added {
FileInclusion::Direct(_) => el.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
),
FileInclusion::InDirectory(dir_name) => {
let dir_name = dir_name.to_string_lossy().into_owned();
el.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Included").size(LabelSize::Small)),
)
.tooltip(move |cx| Tooltip::text(format!("in {dir_name}"), cx))
}
})
}

View file

@ -6,7 +6,7 @@ use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem};
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store;
use crate::context_store::{self, ContextStore};
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
@ -47,9 +47,9 @@ impl Render for ThreadContextPicker {
}
#[derive(Debug, Clone)]
struct ThreadContextEntry {
id: ThreadId,
summary: SharedString,
pub struct ThreadContextEntry {
pub id: ThreadId,
pub summary: SharedString,
}
pub struct ThreadContextPickerDelegate {
@ -103,10 +103,8 @@ impl PickerDelegate for ThreadContextPickerDelegate {
this.threads(cx)
.into_iter()
.map(|thread| {
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
let id = thread.read(cx).id().clone();
let summary = thread.read(cx).summary().unwrap_or(DEFAULT_SUMMARY);
let summary = thread.read(cx).summary_or_default();
ThreadContextEntry { id, summary }
})
.collect::<Vec<_>>()
@ -179,7 +177,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
this.reset_mode(cx);
cx.emit(DismissEvent);
})
.ok();
@ -193,27 +191,37 @@ impl PickerDelegate for ThreadContextPickerDelegate {
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
let added = self.context_store.upgrade().map_or(false, |context_store| {
context_store.read(cx).includes_thread(&thread.id).is_some()
});
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(Label::new(thread.summary.clone()))
.when(added, |el| {
el.end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
}),
)
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),
))
}
}
pub fn render_thread_context_entry(
thread: &ThreadContextEntry,
context_store: WeakModel<ContextStore>,
cx: &mut WindowContext,
) -> Div {
let added = context_store.upgrade().map_or(false, |ctx_store| {
ctx_store.read(cx).includes_thread(&thread.id).is_some()
});
h_flex()
.gap_1()
.w_full()
.child(Icon::new(IconName::MessageCircle).size(IconSize::Small))
.child(Label::new(thread.summary.clone()))
.child(div().w_full())
.when(added, |el| {
el.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}