assistant2: Sketch in context picker (#21560)
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
This commit is contained in:
parent
a30ea2fc68
commit
31796171de
5 changed files with 227 additions and 21 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -470,6 +470,7 @@ dependencies = [
|
||||||
"language_models",
|
"language_models",
|
||||||
"log",
|
"log",
|
||||||
"markdown",
|
"markdown",
|
||||||
|
"picker",
|
||||||
"project",
|
"project",
|
||||||
"proto",
|
"proto",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -29,6 +29,7 @@ language_model_selector.workspace = true
|
||||||
language_models.workspace = true
|
language_models.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
markdown.workspace = true
|
markdown.workspace = true
|
||||||
|
picker.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
proto.workspace = true
|
proto.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
mod active_thread;
|
mod active_thread;
|
||||||
mod assistant_panel;
|
mod assistant_panel;
|
||||||
|
mod context_picker;
|
||||||
mod message_editor;
|
mod message_editor;
|
||||||
mod thread;
|
mod thread;
|
||||||
mod thread_store;
|
mod thread_store;
|
||||||
|
|
197
crates/assistant2/src/context_picker.rs
Normal file
197
crates/assistant2/src/context_picker.rs
Normal file
|
@ -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 editor::{Editor, EditorElement, EditorStyle};
|
||||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
|
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
|
||||||
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
||||||
|
use picker::Picker;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use theme::ThemeSettings;
|
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::thread::{RequestKind, Thread};
|
||||||
use crate::Chat;
|
use crate::Chat;
|
||||||
|
|
||||||
pub struct MessageEditor {
|
pub struct MessageEditor {
|
||||||
thread: Model<Thread>,
|
thread: Model<Thread>,
|
||||||
editor: View<Editor>,
|
editor: View<Editor>,
|
||||||
|
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
|
||||||
use_tools: bool,
|
use_tools: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +30,7 @@ impl MessageEditor {
|
||||||
|
|
||||||
editor
|
editor
|
||||||
}),
|
}),
|
||||||
|
context_picker_handle: PopoverMenuHandle::default(),
|
||||||
use_tools: false,
|
use_tools: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,6 +105,14 @@ impl Render for MessageEditor {
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.p_2()
|
.p_2()
|
||||||
.bg(cx.theme().colors().editor_background)
|
.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({
|
.child({
|
||||||
let settings = ThemeSettings::get_global(cx);
|
let settings = ThemeSettings::get_global(cx);
|
||||||
let text_style = TextStyle {
|
let text_style = TextStyle {
|
||||||
|
@ -123,26 +138,17 @@ impl Render for MessageEditor {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(h_flex().gap_2().child(CheckboxWithLabel::new(
|
||||||
h_flex()
|
"use-tools",
|
||||||
.child(
|
Label::new("Tools"),
|
||||||
Button::new("add-context", "Add Context")
|
self.use_tools.into(),
|
||||||
.style(ButtonStyle::Filled)
|
cx.listener(|this, selection, _cx| {
|
||||||
.icon(IconName::Plus)
|
this.use_tools = match selection {
|
||||||
.icon_position(IconPosition::Start),
|
Selection::Selected => true,
|
||||||
)
|
Selection::Unselected | Selection::Indeterminate => false,
|
||||||
.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(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue