Cargo.lock 🔗
@@ -470,6 +470,7 @@ dependencies = [
"language_models",
"log",
"markdown",
+ "picker",
"project",
"proto",
"serde",
Marshall Bowers created
This PR sketches in a context picker into the message editor in
Assistant 2. Not functional yet.
<img width="1138" alt="Screenshot 2024-12-04 at 5 45 19 PM"
src="https://github.com/user-attachments/assets/053d6224-de76-4fde-914b-41fe835761eb">
Release Notes:
- N/A
Cargo.lock | 1
crates/assistant2/Cargo.toml | 1
crates/assistant2/src/assistant.rs | 1
crates/assistant2/src/context_picker.rs | 197 +++++++++++++++++++++++++++
crates/assistant2/src/message_editor.rs | 48 +++--
5 files changed, 227 insertions(+), 21 deletions(-)
@@ -470,6 +470,7 @@ dependencies = [
"language_models",
"log",
"markdown",
+ "picker",
"project",
"proto",
"serde",
@@ -29,6 +29,7 @@ language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
markdown.workspace = true
+picker.workspace = true
project.workspace = true
proto.workspace = true
serde.workspace = true
@@ -1,5 +1,6 @@
mod active_thread;
mod assistant_panel;
+mod context_picker;
mod message_editor;
mod thread;
mod thread_store;
@@ -0,0 +1,197 @@
+use std::sync::Arc;
+
+use gpui::{DismissEvent, SharedString, Task, WeakView};
+use picker::{Picker, PickerDelegate, PickerEditorPosition};
+use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
+
+use crate::message_editor::MessageEditor;
+
+#[derive(IntoElement)]
+pub(super) struct ContextPicker<T: PopoverTrigger> {
+ message_editor: WeakView<MessageEditor>,
+ trigger: T,
+}
+
+#[derive(Clone)]
+struct ContextPickerEntry {
+ name: SharedString,
+ description: SharedString,
+ icon: IconName,
+}
+
+pub(crate) struct ContextPickerDelegate {
+ all_entries: Vec<ContextPickerEntry>,
+ filtered_entries: Vec<ContextPickerEntry>,
+ message_editor: WeakView<MessageEditor>,
+ selected_ix: usize,
+}
+
+impl<T: PopoverTrigger> ContextPicker<T> {
+ pub(crate) fn new(message_editor: WeakView<MessageEditor>, trigger: T) -> Self {
+ ContextPicker {
+ message_editor,
+ trigger,
+ }
+ }
+}
+
+impl PickerDelegate for ContextPickerDelegate {
+ type ListItem = ListItem;
+
+ fn match_count(&self) -> usize {
+ self.filtered_entries.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_ix
+ }
+
+ fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
+ self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
+ cx.notify();
+ }
+
+ fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
+ "Select a context source…".into()
+ }
+
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+ let all_commands = self.all_entries.clone();
+ cx.spawn(|this, mut cx| async move {
+ let filtered_commands = cx
+ .background_executor()
+ .spawn(async move {
+ if query.is_empty() {
+ all_commands
+ } else {
+ all_commands
+ .into_iter()
+ .filter(|model_info| {
+ model_info
+ .name
+ .to_lowercase()
+ .contains(&query.to_lowercase())
+ })
+ .collect()
+ }
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ this.delegate.filtered_entries = filtered_commands;
+ this.delegate.set_selected_index(0, cx);
+ cx.notify();
+ })
+ .ok();
+ })
+ }
+
+ fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
+ if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
+ self.message_editor
+ .update(cx, |_message_editor, _cx| {
+ println!("Insert context from {}", entry.name);
+ })
+ .ok();
+ cx.emit(DismissEvent);
+ }
+ }
+
+ fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
+
+ fn editor_position(&self) -> PickerEditorPosition {
+ PickerEditorPosition::End
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ _cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let entry = self.filtered_entries.get(ix)?;
+
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Dense)
+ .selected(selected)
+ .tooltip({
+ let description = entry.description.clone();
+ move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
+ })
+ .child(
+ v_flex()
+ .group(format!("context-entry-label-{ix}"))
+ .w_full()
+ .py_0p5()
+ .min_w(px(250.))
+ .max_w(px(400.))
+ .child(
+ h_flex()
+ .gap_1p5()
+ .child(Icon::new(entry.icon).size(IconSize::XSmall))
+ .child(
+ Label::new(entry.name.clone())
+ .single_line()
+ .size(LabelSize::Small),
+ ),
+ )
+ .child(
+ div().overflow_hidden().text_ellipsis().child(
+ Label::new(entry.description.clone())
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ ),
+ ),
+ )
+ }
+}
+
+impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+ let entries = vec![
+ ContextPickerEntry {
+ name: "directory".into(),
+ description: "Insert any directory".into(),
+ icon: IconName::Folder,
+ },
+ ContextPickerEntry {
+ name: "file".into(),
+ description: "Insert any file".into(),
+ icon: IconName::File,
+ },
+ ContextPickerEntry {
+ name: "web".into(),
+ description: "Fetch content from URL".into(),
+ icon: IconName::Globe,
+ },
+ ];
+
+ let delegate = ContextPickerDelegate {
+ all_entries: entries.clone(),
+ message_editor: self.message_editor.clone(),
+ filtered_entries: entries,
+ selected_ix: 0,
+ };
+
+ let picker =
+ cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
+
+ let handle = self
+ .message_editor
+ .update(cx, |this, _| this.context_picker_handle.clone())
+ .ok();
+ PopoverMenu::new("context-picker")
+ .menu(move |_cx| Some(picker.clone()))
+ .trigger(self.trigger)
+ .attach(gpui::AnchorCorner::TopLeft)
+ .anchor(gpui::AnchorCorner::BottomLeft)
+ .offset(gpui::Point {
+ x: px(0.0),
+ y: px(-16.0),
+ })
+ .when_some(handle, |this, handle| this.with_handle(handle))
+ }
+}
@@ -1,16 +1,22 @@
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
+use picker::Picker;
use settings::Settings;
use theme::ThemeSettings;
-use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding};
+use ui::{
+ prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
+ PopoverMenuHandle,
+};
+use crate::context_picker::{ContextPicker, ContextPickerDelegate};
use crate::thread::{RequestKind, Thread};
use crate::Chat;
pub struct MessageEditor {
thread: Model<Thread>,
editor: View<Editor>,
+ pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
use_tools: bool,
}
@@ -24,6 +30,7 @@ impl MessageEditor {
editor
}),
+ context_picker_handle: PopoverMenuHandle::default(),
use_tools: false,
}
}
@@ -98,6 +105,14 @@ impl Render for MessageEditor {
.gap_2()
.p_2()
.bg(cx.theme().colors().editor_background)
+ .child(
+ h_flex().gap_2().child(ContextPicker::new(
+ cx.view().downgrade(),
+ IconButton::new("add-context", IconName::Plus)
+ .shape(IconButtonShape::Square)
+ .icon_size(IconSize::Small),
+ )),
+ )
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
@@ -123,26 +138,17 @@ impl Render for MessageEditor {
.child(
h_flex()
.justify_between()
- .child(
- h_flex()
- .child(
- Button::new("add-context", "Add Context")
- .style(ButtonStyle::Filled)
- .icon(IconName::Plus)
- .icon_position(IconPosition::Start),
- )
- .child(CheckboxWithLabel::new(
- "use-tools",
- Label::new("Tools"),
- self.use_tools.into(),
- cx.listener(|this, selection, _cx| {
- this.use_tools = match selection {
- Selection::Selected => true,
- Selection::Unselected | Selection::Indeterminate => false,
- };
- }),
- )),
- )
+ .child(h_flex().gap_2().child(CheckboxWithLabel::new(
+ "use-tools",
+ Label::new("Tools"),
+ self.use_tools.into(),
+ cx.listener(|this, selection, _cx| {
+ this.use_tools = match selection {
+ Selection::Selected => true,
+ Selection::Unselected | Selection::Indeterminate => false,
+ };
+ }),
+ )))
.child(
h_flex()
.gap_2()