diff --git a/Cargo.lock b/Cargo.lock index f3c0fa3176f2a1783beab8407f33ab5659a574e6..c47c2fd126ed3695a40daa5235bc4319c0e3875d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -470,6 +470,7 @@ dependencies = [ "language_models", "log", "markdown", + "picker", "project", "proto", "serde", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index fb7dcbe520b2aa2d034ebc949659b10883a0e0af..e5253adbce0181dbdfb286d650971d0ac8e2f599 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -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 diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index dfa361ad8c50d106145a9e2adf201dcc4cf352a7..13ac2d821bb8be636ce52b38c31f124ea4446160 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,5 +1,6 @@ mod active_thread; mod assistant_panel; +mod context_picker; mod message_editor; mod thread; mod thread_store; diff --git a/crates/assistant2/src/context_picker.rs b/crates/assistant2/src/context_picker.rs new file mode 100644 index 0000000000000000000000000000000000000000..679ba8b9e764a29bb4d218fb57f208f6a3e19734 --- /dev/null +++ b/crates/assistant2/src/context_picker.rs @@ -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 { + message_editor: WeakView, + trigger: T, +} + +#[derive(Clone)] +struct ContextPickerEntry { + name: SharedString, + description: SharedString, + icon: IconName, +} + +pub(crate) struct ContextPickerDelegate { + all_entries: Vec, + filtered_entries: Vec, + message_editor: WeakView, + selected_ix: usize, +} + +impl ContextPicker { + pub(crate) fn new(message_editor: WeakView, 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>) { + self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1)); + cx.notify(); + } + + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + "Select a context source…".into() + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> 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>) { + 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>) {} + + fn editor_position(&self) -> PickerEditorPosition { + PickerEditorPosition::End + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _cx: &mut ViewContext>, + ) -> Option { + 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 RenderOnce for ContextPicker { + 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)) + } +} diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index d1b1cf55e465774687ccfc7c2fc1256c395e1a76..f3e618067b3af7ab492a4b1bb1a5d3f9bfc298b4 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -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, editor: View, + pub(crate) context_picker_handle: PopoverMenuHandle>, 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()