assistant2: Render saved conversations inline instead of in a modal (#11630)

Marshall Bowers created

This PR reworks how saved conversations are rendered in the new
assistant panel.

Instead of rendering them in a modal we now display them in the panel
itself:

<img width="402" alt="Screenshot 2024-05-09 at 6 18 40 PM"
src="https://github.com/zed-industries/zed/assets/1486634/82decc04-cb31-4d83-a942-7e8426e02679">

Release Notes:

- N/A

Change summary

crates/assistant2/src/assistant2.rs          | 76 ++++++++++++++-------
crates/assistant2/src/saved_conversations.rs | 74 +++++++++------------
2 files changed, 83 insertions(+), 67 deletions(-)

Detailed changes

crates/assistant2/src/assistant2.rs 🔗

@@ -2,10 +2,11 @@ mod assistant_settings;
 mod attachments;
 mod completion_provider;
 mod saved_conversation;
-mod saved_conversation_picker;
+mod saved_conversations;
 mod tools;
 pub mod ui;
 
+use crate::saved_conversation::SavedConversationMetadata;
 use crate::ui::UserOrAssistant;
 use ::ui::{div, prelude::*, Color, Tooltip, ViewContext};
 use anyhow::{Context, Result};
@@ -29,7 +30,7 @@ use language::{language_settings::SoftWrap, LanguageRegistry};
 use open_ai::{FunctionContent, ToolCall, ToolCallContent};
 use rich_text::RichText;
 use saved_conversation::{SavedAssistantMessagePart, SavedChatMessage, SavedConversation};
-use saved_conversation_picker::SavedConversationPicker;
+use saved_conversations::SavedConversations;
 use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
 use serde::{Deserialize, Serialize};
 use settings::Settings;
@@ -61,15 +62,7 @@ pub enum SubmitMode {
     Codebase,
 }
 
-gpui::actions!(
-    assistant2,
-    [
-        Cancel,
-        ToggleFocus,
-        DebugProjectIndex,
-        ToggleSavedConversations
-    ]
-);
+gpui::actions!(assistant2, [Cancel, ToggleFocus, DebugProjectIndex,]);
 gpui::impl_actions!(assistant2, [Submit]);
 
 pub fn init(client: Arc<Client>, cx: &mut AppContext) {
@@ -109,8 +102,6 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
         },
     )
     .detach();
-    cx.observe_new_views(SavedConversationPicker::register)
-        .detach();
 }
 
 pub fn enabled(cx: &AppContext) -> bool {
@@ -262,6 +253,8 @@ pub struct AssistantChat {
     fs: Arc<dyn Fs>,
     language_registry: Arc<LanguageRegistry>,
     composer_editor: View<Editor>,
+    saved_conversations: View<SavedConversations>,
+    saved_conversations_open: bool,
     project_index_button: View<ProjectIndexButton>,
     active_file_button: Option<View<ActiveFileButton>>,
     user_store: Model<UserStore>,
@@ -317,6 +310,24 @@ impl AssistantChat {
             _ => None,
         };
 
+        let saved_conversations = cx.new_view(|cx| SavedConversations::new(cx));
+        cx.spawn({
+            let fs = fs.clone();
+            let saved_conversations = saved_conversations.downgrade();
+            |_assistant_chat, mut cx| async move {
+                let saved_conversation_metadata = SavedConversationMetadata::list(fs).await?;
+
+                cx.update(|cx| {
+                    saved_conversations.update(cx, |this, cx| {
+                        this.init(saved_conversation_metadata, cx);
+                    })
+                })??;
+
+                anyhow::Ok(())
+            }
+        })
+        .detach_and_log_err(cx);
+
         Self {
             model,
             messages: Vec::new(),
@@ -326,6 +337,8 @@ impl AssistantChat {
                 editor.set_placeholder_text("Send a message…", cx);
                 editor
             }),
+            saved_conversations,
+            saved_conversations_open: false,
             list_state,
             user_store,
             fs,
@@ -357,6 +370,10 @@ impl AssistantChat {
         })
     }
 
+    fn toggle_saved_conversations(&mut self) {
+        self.saved_conversations_open = !self.saved_conversations_open;
+    }
+
     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
         // If we're currently editing a message, cancel the edit.
         if let Some(editing_message) = self.editing_message.take() {
@@ -1017,18 +1034,18 @@ impl Render for AssistantChat {
                     .h(header_height)
                     .p(Spacing::Small.rems(cx))
                     .child(
-                        IconButton::new("open-saved-conversations", IconName::ChevronLeft)
-                            .on_click(|_event, cx| {
-                                cx.dispatch_action(Box::new(ToggleSavedConversations))
-                            })
-                            .tooltip(move |cx| {
-                                Tooltip::with_meta(
-                                    "Switch Conversations",
-                                    Some(&ToggleSavedConversations),
-                                    "UI will change, temporary.",
-                                    cx,
-                                )
-                            }),
+                        IconButton::new(
+                            "toggle-saved-conversations",
+                            if self.saved_conversations_open {
+                                IconName::ChevronRight
+                            } else {
+                                IconName::ChevronLeft
+                            },
+                        )
+                        .on_click(cx.listener(|this, _event, _cx| {
+                            this.toggle_saved_conversations();
+                        }))
+                        .tooltip(move |cx| Tooltip::text("Switch Conversations", cx)),
                     )
                     .child(
                         h_flex()
@@ -1052,6 +1069,15 @@ impl Render for AssistantChat {
                             ),
                     ),
             )
+            .when(self.saved_conversations_open, |element| {
+                element.child(
+                    h_flex()
+                        .absolute()
+                        .top(header_height)
+                        .w_full()
+                        .child(self.saved_conversations.clone()),
+                )
+            })
             .child(Composer::new(
                 self.composer_editor.clone(),
                 self.project_index_button.clone(),

crates/assistant2/src/saved_conversation_picker.rs → crates/assistant2/src/saved_conversations.rs 🔗

@@ -5,65 +5,56 @@ use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, V
 use picker::{Picker, PickerDelegate};
 use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
 use util::ResultExt;
-use workspace::{ModalView, Workspace};
 
 use crate::saved_conversation::SavedConversationMetadata;
-use crate::ToggleSavedConversations;
 
-pub struct SavedConversationPicker {
-    picker: View<Picker<SavedConversationPickerDelegate>>,
+pub struct SavedConversations {
+    focus_handle: FocusHandle,
+    picker: Option<View<Picker<SavedConversationPickerDelegate>>>,
 }
 
-impl EventEmitter<DismissEvent> for SavedConversationPicker {}
+impl EventEmitter<DismissEvent> for SavedConversations {}
 
-impl ModalView for SavedConversationPicker {}
-
-impl FocusableView for SavedConversationPicker {
+impl FocusableView for SavedConversations {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
-        self.picker.focus_handle(cx)
+        if let Some(picker) = self.picker.as_ref() {
+            picker.focus_handle(cx)
+        } else {
+            self.focus_handle.clone()
+        }
     }
 }
 
-impl SavedConversationPicker {
-    pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
-        workspace.register_action(|workspace, _: &ToggleSavedConversations, cx| {
-            let fs = workspace.project().read(cx).fs().clone();
-
-            cx.spawn(|workspace, mut cx| async move {
-                let saved_conversations = SavedConversationMetadata::list(fs).await?;
-
-                cx.update(|cx| {
-                    workspace.update(cx, |workspace, cx| {
-                        workspace.toggle_modal(cx, move |cx| {
-                            let delegate = SavedConversationPickerDelegate::new(
-                                cx.view().downgrade(),
-                                saved_conversations,
-                            );
-                            Self::new(delegate, cx)
-                        });
-                    })
-                })??;
-
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
-        });
+impl SavedConversations {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+        Self {
+            focus_handle: cx.focus_handle(),
+            picker: None,
+        }
     }
 
-    pub fn new(delegate: SavedConversationPickerDelegate, cx: &mut ViewContext<Self>) -> Self {
-        let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
-        Self { picker }
+    pub fn init(
+        &mut self,
+        saved_conversations: Vec<SavedConversationMetadata>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let delegate =
+            SavedConversationPickerDelegate::new(cx.view().downgrade(), saved_conversations);
+        self.picker = Some(cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false)));
     }
 }
 
-impl Render for SavedConversationPicker {
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_flex().w(rems(34.)).child(self.picker.clone())
+impl Render for SavedConversations {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        v_flex()
+            .w_full()
+            .bg(cx.theme().colors().panel_background)
+            .children(self.picker.clone())
     }
 }
 
 pub struct SavedConversationPickerDelegate {
-    view: WeakView<SavedConversationPicker>,
+    view: WeakView<SavedConversations>,
     saved_conversations: Vec<SavedConversationMetadata>,
     selected_index: usize,
     matches: Vec<StringMatch>,
@@ -71,7 +62,7 @@ pub struct SavedConversationPickerDelegate {
 
 impl SavedConversationPickerDelegate {
     pub fn new(
-        weak_view: WeakView<SavedConversationPicker>,
+        weak_view: WeakView<SavedConversations>,
         saved_conversations: Vec<SavedConversationMetadata>,
     ) -> Self {
         let matches = saved_conversations
@@ -194,7 +185,6 @@ impl PickerDelegate for SavedConversationPickerDelegate {
 
         Some(
             ListItem::new(ix)
-                .inset(true)
                 .spacing(ListItemSpacing::Sparse)
                 .selected(selected)
                 .child(HighlightedLabel::new(