@@ -383,6 +383,7 @@ dependencies = [
"env_logger",
"feature_flags",
"futures 0.3.28",
+ "fuzzy",
"gpui",
"language",
"languages",
@@ -390,6 +391,7 @@ dependencies = [
"nanoid",
"node_runtime",
"open_ai",
+ "picker",
"project",
"rand 0.8.5",
"release_channel",
@@ -1,9 +1,12 @@
mod assistant_settings;
mod attachments;
mod completion_provider;
+mod saved_conversation;
+mod saved_conversation_picker;
mod tools;
pub mod ui;
+use crate::saved_conversation_picker::SavedConversationPicker;
use crate::{
attachments::ActiveEditorAttachmentTool,
tools::{CreateBufferTool, ProjectIndexTool},
@@ -57,7 +60,15 @@ pub enum SubmitMode {
Codebase,
}
-gpui::actions!(assistant2, [Cancel, ToggleFocus, DebugProjectIndex]);
+gpui::actions!(
+ assistant2,
+ [
+ Cancel,
+ ToggleFocus,
+ DebugProjectIndex,
+ ToggleSavedConversations
+ ]
+);
gpui::impl_actions!(assistant2, [Submit]);
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
@@ -97,6 +108,8 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
},
)
.detach();
+ cx.observe_new_views(SavedConversationPicker::register)
+ .detach();
}
pub fn enabled(cx: &AppContext) -> bool {
@@ -891,6 +904,10 @@ impl Render for AssistantChat {
.on_action(cx.listener(Self::submit))
.on_action(cx.listener(Self::cancel))
.text_color(Color::Default.color(cx))
+ .child(
+ Button::new("open-saved-conversations", "Saved Conversations")
+ .on_click(|_event, cx| cx.dispatch_action(Box::new(ToggleSavedConversations))),
+ )
.child(list(self.list_state.clone()).flex_1())
.child(Composer::new(
self.composer_editor.clone(),
@@ -0,0 +1,29 @@
+pub struct SavedConversation {
+ /// The title of the conversation, generated by the Assistant.
+ pub title: String,
+ pub messages: Vec<SavedMessage>,
+}
+
+pub struct SavedMessage {
+ pub text: String,
+}
+
+/// Returns a list of placeholder conversations for mocking the UI.
+///
+/// Once we have real saved conversations to pull from we can use those instead.
+pub fn placeholder_conversations() -> Vec<SavedConversation> {
+ vec![
+ SavedConversation {
+ title: "How to get a list of exported functions in an Erlang module".to_string(),
+ messages: vec![],
+ },
+ SavedConversation {
+ title: "7 wonders of the ancient world".to_string(),
+ messages: vec![],
+ },
+ SavedConversation {
+ title: "Size difference between u8 and a reference to u8 in Rust".to_string(),
+ messages: vec![],
+ },
+ ]
+}
@@ -0,0 +1,188 @@
+use std::sync::Arc;
+
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakView};
+use picker::{Picker, PickerDelegate};
+use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
+use util::ResultExt;
+use workspace::{ModalView, Workspace};
+
+use crate::saved_conversation::{self, SavedConversation};
+use crate::ToggleSavedConversations;
+
+pub struct SavedConversationPicker {
+ picker: View<Picker<SavedConversationPickerDelegate>>,
+}
+
+impl EventEmitter<DismissEvent> for SavedConversationPicker {}
+
+impl ModalView for SavedConversationPicker {}
+
+impl FocusableView for SavedConversationPicker {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl SavedConversationPicker {
+ pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
+ workspace.register_action(|workspace, _: &ToggleSavedConversations, cx| {
+ workspace.toggle_modal(cx, move |cx| {
+ let delegate = SavedConversationPickerDelegate::new(cx.view().downgrade());
+ Self::new(delegate, cx)
+ });
+ });
+ }
+
+ pub fn new(delegate: SavedConversationPickerDelegate, cx: &mut ViewContext<Self>) -> Self {
+ let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
+ Self { picker }
+ }
+}
+
+impl Render for SavedConversationPicker {
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+ v_flex().w(rems(34.)).child(self.picker.clone())
+ }
+}
+
+pub struct SavedConversationPickerDelegate {
+ view: WeakView<SavedConversationPicker>,
+ saved_conversations: Vec<SavedConversation>,
+ selected_index: usize,
+ matches: Vec<StringMatch>,
+}
+
+impl SavedConversationPickerDelegate {
+ pub fn new(weak_view: WeakView<SavedConversationPicker>) -> Self {
+ let saved_conversations = saved_conversation::placeholder_conversations();
+ let matches = saved_conversations
+ .iter()
+ .map(|conversation| StringMatch {
+ candidate_id: 0,
+ score: 0.0,
+ positions: Default::default(),
+ string: conversation.title.clone(),
+ })
+ .collect();
+
+ Self {
+ view: weak_view,
+ saved_conversations,
+ selected_index: 0,
+ matches,
+ }
+ }
+}
+
+impl PickerDelegate for SavedConversationPickerDelegate {
+ type ListItem = ui::ListItem;
+
+ fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
+ "Select saved conversation...".into()
+ }
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
+ self.selected_index = ix;
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> gpui::Task<()> {
+ let background_executor = cx.background_executor().clone();
+ let candidates = self
+ .saved_conversations
+ .iter()
+ .enumerate()
+ .map(|(id, conversation)| {
+ let text = conversation.title.clone();
+
+ StringMatchCandidate {
+ id,
+ char_bag: text.as_str().into(),
+ string: text,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ cx.spawn(move |this, mut cx| async move {
+ let matches = if query.is_empty() {
+ candidates
+ .into_iter()
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
+ string: candidate.string,
+ positions: Vec::new(),
+ score: 0.0,
+ })
+ .collect()
+ } else {
+ match_strings(
+ &candidates,
+ &query,
+ false,
+ 100,
+ &Default::default(),
+ background_executor,
+ )
+ .await
+ };
+
+ this.update(&mut cx, |this, _cx| {
+ this.delegate.matches = matches;
+ this.delegate.selected_index = this
+ .delegate
+ .selected_index
+ .min(this.delegate.matches.len().saturating_sub(1));
+ })
+ .log_err();
+ })
+ }
+
+ fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
+ if self.matches.is_empty() {
+ self.dismissed(cx);
+ return;
+ }
+
+ // TODO: Implement selecting a saved conversation.
+ }
+
+ fn dismissed(&mut self, cx: &mut ui::prelude::ViewContext<Picker<Self>>) {
+ self.view
+ .update(cx, |_, cx| cx.emit(DismissEvent))
+ .log_err();
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ _cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let conversation_match = &self.matches[ix];
+ let _conversation = &self.saved_conversations[conversation_match.candidate_id];
+
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .selected(selected)
+ .child(HighlightedLabel::new(
+ conversation_match.string.clone(),
+ conversation_match.positions.clone(),
+ )),
+ )
+ }
+}