assistant2: Refine empty states design (#27812)

Danilo Leal created

| No LLM provider | Fresh Start | No ToS |
|--------|--------|--------|
| ![CleanShot 2025-03-31 at 7  04
17@2x](https://github.com/user-attachments/assets/aab5987c-1530-401d-acc6-65e4f2fc13b8)
| ![CleanShot 2025-03-31 at 7  04
39@2x](https://github.com/user-attachments/assets/b2c7a2e0-5178-4bcb-a917-da7bf8e6246c)
| ![CleanShot 2025-03-31 at 7  05
10@2x](https://github.com/user-attachments/assets/4a656e82-0e1d-4d11-8d34-8eeeadd4814c)
|

Release Notes:

- N/A

Change summary

crates/assistant2/src/assistant_panel.rs     | 193 +++++++++++++++------
crates/language_models/src/provider/cloud.rs |  22 +
2 files changed, 154 insertions(+), 61 deletions(-)

Detailed changes

crates/assistant2/src/assistant_panel.rs 🔗

@@ -20,6 +20,7 @@ use gpui::{
 };
 use language::LanguageRegistry;
 use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
+use language_model_selector::ToggleModelSelector;
 use project::Project;
 use prompt_library::{PromptLibrary, open_prompt_library};
 use prompt_store::PromptBuilder;
@@ -40,7 +41,7 @@ use crate::thread_history::{PastContext, PastThread, ThreadHistory};
 use crate::thread_store::ThreadStore;
 use crate::{
     AssistantDiff, InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown,
-    OpenAssistantDiff, OpenConfiguration, OpenHistory,
+    OpenAssistantDiff, OpenConfiguration, OpenHistory, ToggleContextPicker,
 };
 
 action_with_deprecated_aliases!(
@@ -830,66 +831,150 @@ impl AssistantPanel {
             .history_store
             .update(cx, |this, cx| this.recent_entries(6, cx));
 
-        let create_welcome_heading = || {
-            h_flex()
-                .w_full()
-                .child(Headline::new("Welcome to the Assistant Panel").size(HeadlineSize::Small))
-        };
-
         let configuration_error = self.configuration_error(cx);
         let no_error = configuration_error.is_none();
+        let focus_handle = self.focus_handle(cx);
 
         v_flex()
-            .p_1p5()
             .size_full()
-            .justify_end()
-            .gap_1()
-            .map(|parent| {
-                match configuration_error {
-                    Some(ConfigurationError::ProviderNotAuthenticated)
-                    | Some(ConfigurationError::NoProvider) => {
-                        parent.child(
-                            v_flex()
-                                .px_1p5()
-                                .gap_0p5()
-                                .child(create_welcome_heading())
-                                .child(
-                                    Label::new(
-                                        "To start using the assistant, configure at least one LLM provider.",
-                                    )
-                                    .color(Color::Muted),
-                                )
-                                .child(
-                                    h_flex().mt_1().w_full().child(
-                                        Button::new("open-configuration", "Configure a Provider")
-                                            .size(ButtonSize::Compact)
-                                            .icon(Some(IconName::Sliders))
-                                            .icon_size(IconSize::Small)
-                                            .icon_position(IconPosition::Start)
-                                            .on_click(cx.listener(|this, _, window, cx| {
-                                                this.open_configuration(window, cx);
-                                            })),
-                                    ),
-                                ),
-                        )
-                    }
-                    Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
-                        .child(v_flex().px_1p5().gap_0p5().child(create_welcome_heading()).children(
-                            provider.render_accept_terms(
-                                LanguageModelProviderTosView::ThreadEmptyState,
-                                cx,
+            .when(recent_history.is_empty(), |this| {
+                this.child(
+                    v_flex()
+                        .size_full()
+                        .max_w_80()
+                        .mx_auto()
+                        .justify_center()
+                        .items_center()
+                        .gap_1()
+                        .child(
+                            h_flex().child(
+                                Headline::new("Welcome to the Assistant Panel")
                             ),
-                        )),
-                    None => parent,
-                }
-            })
-            .when(recent_history.is_empty() && no_error, |parent| {
-                parent.child(v_flex().gap_0p5().child(create_welcome_heading()).child(
-                    Label::new("Start typing to chat with your codebase").color(Color::Muted),
-                ))
+                        )
+                        .when(no_error, |parent| {
+                            parent.child(
+                                h_flex().child(
+                                    Label::new("Ask and build anything.")
+                                        .color(Color::Muted)
+                                        .mb_2p5(),
+                                ),
+                            )
+                            .child(
+                                Button::new("new-thread", "Start New Thread")
+                                    .icon(IconName::Plus)
+                                    .icon_position(IconPosition::Start)
+                                    .icon_size(IconSize::Small)
+                                    .icon_color(Color::Muted)
+                                    .full_width()
+                                    .key_binding(KeyBinding::for_action_in(
+                                        &NewThread,
+                                        &focus_handle,
+                                        window,
+                                        cx,
+                                    ))
+                                    .on_click(|_event, window, cx| {
+                                        window.dispatch_action(NewThread.boxed_clone(), cx)
+                                    }),
+                            )
+                            .child(
+                                Button::new("context", "Add Context")
+                                    .icon(IconName::FileCode)
+                                    .icon_position(IconPosition::Start)
+                                    .icon_size(IconSize::Small)
+                                    .icon_color(Color::Muted)
+                                    .full_width()
+                                    .key_binding(KeyBinding::for_action_in(
+                                        &ToggleContextPicker,
+                                        &focus_handle,
+                                        window,
+                                        cx,
+                                    ))
+                                    .on_click(|_event, window, cx| {
+                                        window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
+                                    }),
+                            )
+                            .child(
+                                Button::new("mode", "Switch Model")
+                                    .icon(IconName::DatabaseZap)
+                                    .icon_position(IconPosition::Start)
+                                    .icon_size(IconSize::Small)
+                                    .icon_color(Color::Muted)
+                                    .full_width()
+                                    .key_binding(KeyBinding::for_action_in(
+                                        &ToggleModelSelector,
+                                        &focus_handle,
+                                        window,
+                                        cx,
+                                    ))
+                                    .on_click(|_event, window, cx| {
+                                        window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
+                                    }),
+                            )
+                            .child(
+                                Button::new("settings", "View Settings")
+                                    .icon(IconName::Settings)
+                                    .icon_position(IconPosition::Start)
+                                    .icon_size(IconSize::Small)
+                                    .icon_color(Color::Muted)
+                                    .full_width()
+                                    .key_binding(KeyBinding::for_action_in(
+                                        &OpenConfiguration,
+                                        &focus_handle,
+                                        window,
+                                        cx,
+                                    ))
+                                    .on_click(|_event, window, cx| {
+                                        window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
+                                    }),
+                            )
+                        })
+                        .map(|parent| {
+                            match configuration_error {
+                                Some(ConfigurationError::ProviderNotAuthenticated)
+                                | Some(ConfigurationError::NoProvider) => {
+                                    parent
+                                        .child(
+                                            h_flex().child(
+                                                Label::new("To start using the assistant, configure at least one LLM provider.")
+                                                    .color(Color::Muted)
+                                                    .mb_2p5()
+                                            )
+                                        )
+                                        .child(
+                                            Button::new("settings", "Configure a Provider")
+                                                .icon(IconName::Settings)
+                                                .icon_position(IconPosition::Start)
+                                                .icon_size(IconSize::Small)
+                                                .icon_color(Color::Muted)
+                                                .full_width()
+                                                .key_binding(KeyBinding::for_action_in(
+                                                    &OpenConfiguration,
+                                                    &focus_handle,
+                                                    window,
+                                                    cx,
+                                                ))
+                                                .on_click(|_event, window, cx| {
+                                                    window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
+                                                }),
+                                        )
+                                }
+                                Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
+                                    .children(
+                                        provider.render_accept_terms(
+                                            LanguageModelProviderTosView::ThreadEmptyState,
+                                            cx,
+                                        ),
+                                    ),
+                                None => parent,
+                            }
+                        })
+                )
             })
             .when(!recent_history.is_empty(), |parent| {
                 parent
+                    .p_1p5()
+                          .justify_end()
+                          .gap_1()
                     .child(
                         h_flex()
                             .pl_1p5()
@@ -912,7 +997,7 @@ impl AssistantPanel {
                                         &self.focus_handle(cx),
                                         window,
                                         cx,
-                                    ))
+                                    ).map(|kb| kb.size(rems_from_px(12.))),)
                                     .on_click(move |_event, window, cx| {
                                         window.dispatch_action(OpenHistory.boxed_clone(), cx);
                                     }),
@@ -920,7 +1005,7 @@ impl AssistantPanel {
                     )
                     .child(v_flex().gap_1().children(
                         recent_history.into_iter().map(|entry| {
-                            // TODO: Add keyboard navigation.
+                             // TODO: Add keyboard navigation.
                             match entry {
                                 HistoryEntry::Thread(thread) => {
                                     PastThread::new(thread, cx.entity().downgrade(), false)

crates/language_models/src/provider/cloud.rs 🔗

@@ -410,7 +410,11 @@ fn render_accept_terms(
         .icon_size(IconSize::XSmall)
         .on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service"));
 
-    let text = "To start using Zed AI, please read and accept the";
+    let thread_view = match view_kind {
+        LanguageModelProviderTosView::ThreadEmptyState => true,
+        LanguageModelProviderTosView::PromptEditorPopup => false,
+        LanguageModelProviderTosView::Configuration => false,
+    };
 
     let form = v_flex()
         .w_full()
@@ -418,14 +422,20 @@ fn render_accept_terms(
         .child(
             h_flex()
                 .flex_wrap()
-                .items_start()
-                .child(Label::new(text))
+                .when(thread_view, |this| this.justify_center())
+                .child(Label::new(
+                    "To start using Zed AI, please read and accept the",
+                ))
                 .child(terms_button),
         )
         .child({
             let button_container = h_flex().w_full().child(
                 Button::new("accept_terms", "I accept the Terms of Service")
                     .style(ButtonStyle::Tinted(TintColor::Accent))
+                    .icon(IconName::Check)
+                    .icon_position(IconPosition::Start)
+                    .icon_size(IconSize::Small)
+                    .full_width()
                     .disabled(accept_terms_disabled)
                     .on_click({
                         let state = state.downgrade();
@@ -439,10 +449,8 @@ fn render_accept_terms(
 
             match view_kind {
                 LanguageModelProviderTosView::PromptEditorPopup => button_container.justify_end(),
-                LanguageModelProviderTosView::Configuration
-                | LanguageModelProviderTosView::ThreadEmptyState => {
-                    button_container.justify_start()
-                }
+                LanguageModelProviderTosView::Configuration => button_container.justify_start(),
+                LanguageModelProviderTosView::ThreadEmptyState => button_container.justify_center(),
             }
         });