assistant2: Add empty state for new threads (#21542)

Marshall Bowers created

This PR adds an empty state for new threads in Assistant2:

<img width="1138" alt="Screenshot 2024-12-04 at 12 17 46 PM"
src="https://github.com/user-attachments/assets/ff7b4533-d3b8-4722-bd4b-43fac6d35a77">

This is mostly just a sketch in its current state.

Release Notes:

- N/A

Change summary

assets/keymaps/default-macos.json        |   3 
crates/assistant2/src/assistant.rs       |   8 +
crates/assistant2/src/assistant_panel.rs | 142 ++++++++++++++++++++++++-
3 files changed, 141 insertions(+), 12 deletions(-)

Detailed changes

assets/keymaps/default-macos.json 🔗

@@ -210,7 +210,8 @@
   {
     "context": "AssistantPanel2",
     "bindings": {
-      "cmd-n": "assistant2::NewThread"
+      "cmd-n": "assistant2::NewThread",
+      "cmd-shift-h": "assistant2::OpenHistory"
     }
   },
   {

crates/assistant2/src/assistant.rs 🔗

@@ -11,7 +11,13 @@ pub use crate::assistant_panel::AssistantPanel;
 
 actions!(
     assistant2,
-    [ToggleFocus, NewThread, ToggleModelSelector, Chat]
+    [
+        ToggleFocus,
+        NewThread,
+        ToggleModelSelector,
+        OpenHistory,
+        Chat
+    ]
 );
 
 const NAMESPACE: &str = "assistant2";

crates/assistant2/src/assistant_panel.rs 🔗

@@ -5,8 +5,8 @@ use assistant_tool::ToolWorkingSet;
 use client::zed_urls;
 use collections::HashMap;
 use gpui::{
-    list, prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, Empty, EventEmitter,
-    FocusHandle, FocusableView, FontWeight, ListAlignment, ListState, Model, Pixels,
+    list, prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, Empty,
+    EventEmitter, FocusHandle, FocusableView, FontWeight, ListAlignment, ListState, Model, Pixels,
     StyleRefinement, Subscription, Task, TextStyleRefinement, View, ViewContext, WeakView,
     WindowContext,
 };
@@ -16,14 +16,14 @@ use language_model_selector::LanguageModelSelector;
 use markdown::{Markdown, MarkdownStyle};
 use settings::Settings;
 use theme::ThemeSettings;
-use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip};
+use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, ListItem, Tab, Tooltip};
 use workspace::dock::{DockPosition, Panel, PanelEvent};
 use workspace::Workspace;
 
 use crate::message_editor::MessageEditor;
 use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
 use crate::thread_store::ThreadStore;
-use crate::{NewThread, ToggleFocus, ToggleModelSelector};
+use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
 
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(
@@ -311,8 +311,8 @@ impl AssistantPanel {
                                     )
                                 }
                             })
-                            .on_click(move |_event, _cx| {
-                                println!("New Thread");
+                            .on_click(move |_event, cx| {
+                                cx.dispatch_action(NewThread.boxed_clone());
                             }),
                     )
                     .child(
@@ -320,9 +320,19 @@ impl AssistantPanel {
                             .shape(IconButtonShape::Square)
                             .icon_size(IconSize::Small)
                             .style(ButtonStyle::Subtle)
-                            .tooltip(move |cx| Tooltip::text("Open History", cx))
-                            .on_click(move |_event, _cx| {
-                                println!("Open History");
+                            .tooltip({
+                                let focus_handle = focus_handle.clone();
+                                move |cx| {
+                                    Tooltip::for_action_in(
+                                        "Open History",
+                                        &OpenHistory,
+                                        &focus_handle,
+                                        cx,
+                                    )
+                                }
+                            })
+                            .on_click(move |_event, cx| {
+                                cx.dispatch_action(OpenHistory.boxed_clone());
                             }),
                     )
                     .child(
@@ -389,6 +399,99 @@ impl AssistantPanel {
         )
     }
 
+    fn render_message_list(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+        if self.thread_messages.is_empty() {
+            #[allow(clippy::useless_vec)]
+            let recent_threads = vec![1, 2, 3];
+
+            return v_flex()
+                .gap_2()
+                .mx_auto()
+                .child(
+                    v_flex().w_full().child(
+                        svg()
+                            .path("icons/logo_96.svg")
+                            .text_color(cx.theme().colors().text)
+                            .w(px(40.))
+                            .h(px(40.))
+                            .mx_auto()
+                            .mb_4(),
+                    ),
+                )
+                .child(v_flex())
+                .child(
+                    h_flex()
+                        .w_full()
+                        .justify_center()
+                        .child(Label::new("Context Examples:").size(LabelSize::Small)),
+                )
+                .child(
+                    h_flex()
+                        .gap_2()
+                        .justify_center()
+                        .child(
+                            h_flex()
+                                .gap_1()
+                                .p_0p5()
+                                .rounded_md()
+                                .border_1()
+                                .border_color(cx.theme().colors().border_variant)
+                                .child(
+                                    Icon::new(IconName::Terminal)
+                                        .size(IconSize::Small)
+                                        .color(Color::Disabled),
+                                )
+                                .child(Label::new("Terminal").size(LabelSize::Small)),
+                        )
+                        .child(
+                            h_flex()
+                                .gap_1()
+                                .p_0p5()
+                                .rounded_md()
+                                .border_1()
+                                .border_color(cx.theme().colors().border_variant)
+                                .child(
+                                    Icon::new(IconName::Folder)
+                                        .size(IconSize::Small)
+                                        .color(Color::Disabled),
+                                )
+                                .child(Label::new("/src/components").size(LabelSize::Small)),
+                        ),
+                )
+                .child(
+                    h_flex()
+                        .w_full()
+                        .justify_center()
+                        .child(Label::new("Recent Threads:").size(LabelSize::Small)),
+                )
+                .child(
+                    v_flex().gap_2().children(
+                        recent_threads
+                            .iter()
+                            .map(|_thread| self.render_past_thread(cx)),
+                    ),
+                )
+                .child(
+                    h_flex().w_full().justify_center().child(
+                        Button::new("view-all-past-threads", "View All Past Threads")
+                            .style(ButtonStyle::Subtle)
+                            .label_size(LabelSize::Small)
+                            .key_binding(KeyBinding::for_action_in(
+                                &OpenHistory,
+                                &self.focus_handle(cx),
+                                cx,
+                            ))
+                            .on_click(move |_event, cx| {
+                                cx.dispatch_action(OpenHistory.boxed_clone());
+                            }),
+                    ),
+                )
+                .into_any();
+        }
+
+        list(self.thread_list_state.clone()).flex_1().into_any()
+    }
+
     fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
         let message_id = self.thread_messages[ix];
         let Some(message) = self.thread.read(cx).message(message_id) else {
@@ -431,6 +534,22 @@ impl AssistantPanel {
             .into_any()
     }
 
+    fn render_past_thread(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        ListItem::new("temp")
+            .start_slot(Icon::new(IconName::MessageBubbles))
+            .child(Label::new("Some Thread Title"))
+            .end_slot(
+                h_flex()
+                    .gap_2()
+                    .child(Label::new("1 hour ago").color(Color::Disabled))
+                    .child(
+                        IconButton::new("delete", IconName::TrashAlt)
+                            .shape(IconButtonShape::Square)
+                            .icon_size(IconSize::Small),
+                    ),
+            )
+    }
+
     fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
         let last_error = self.last_error.as_ref()?;
 
@@ -587,8 +706,11 @@ impl Render for AssistantPanel {
             .on_action(cx.listener(|this, _: &NewThread, cx| {
                 this.new_thread(cx);
             }))
+            .on_action(cx.listener(|_this, _: &OpenHistory, _cx| {
+                println!("Open History");
+            }))
             .child(self.render_toolbar(cx))
-            .child(list(self.thread_list_state.clone()).flex_1())
+            .child(self.render_message_list(cx))
             .child(
                 h_flex()
                     .border_t_1()