assistant2: Ensure errors are also displayed in populated new thread view (#27869)

Danilo Leal created

Follow-up to https://github.com/zed-industries/zed/pull/27812

This PR makes sure these errors cases also show up in the panel's empty
state even when there is past data.

| No ToS | Missing Provider |
|--------|--------|
| ![CleanShot 2025-04-01 at 4  49
36@2x](https://github.com/user-attachments/assets/6da6bdc9-daa6-4a7b-a224-989eb845e205)
| ![CleanShot 2025-04-01 at 4  50
04@2x](https://github.com/user-attachments/assets/bddf62cb-3727-44b5-b115-9a88313c6d85)
|

Release Notes:

- N/A

Change summary

crates/assistant2/src/assistant_panel.rs     | 276 +++++++++++++--------
crates/language_model/src/language_model.rs  |   5 
crates/language_models/src/provider/cloud.rs | 105 +++++---
crates/ui/src/components/banner.rs           |   2 
4 files changed, 244 insertions(+), 144 deletions(-)

Detailed changes

crates/assistant2/src/assistant_panel.rs 🔗

@@ -26,7 +26,9 @@ use prompt_library::{PromptLibrary, open_prompt_library};
 use prompt_store::PromptBuilder;
 use settings::{Settings, update_settings_file};
 use time::UtcOffset;
-use ui::{ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*};
+use ui::{
+    Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
+};
 use util::ResultExt as _;
 use workspace::Workspace;
 use workspace::dock::{DockPosition, Panel, PanelEvent};
@@ -838,6 +840,7 @@ impl AssistantPanel {
         v_flex()
             .size_full()
             .when(recent_history.is_empty(), |this| {
+                let configuration_error_ref = &configuration_error;
                 this.child(
                     v_flex()
                         .size_full()
@@ -852,84 +855,85 @@ impl AssistantPanel {
                             ),
                         )
                         .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)
-                                    }),
-                            )
+                            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 {
+                            match configuration_error_ref {
                                 Some(ConfigurationError::ProviderNotAuthenticated)
                                 | Some(ConfigurationError::NoProvider) => {
                                     parent
@@ -958,23 +962,27 @@ impl AssistantPanel {
                                                 }),
                                         )
                                 }
-                                Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
-                                    .children(
+                                Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
+                                    parent.children(
                                         provider.render_accept_terms(
-                                            LanguageModelProviderTosView::ThreadEmptyState,
+                                            LanguageModelProviderTosView::ThreadFreshStart,
                                             cx,
                                         ),
-                                    ),
+                                    )
+                                }
                                 None => parent,
                             }
                         })
                 )
             })
             .when(!recent_history.is_empty(), |parent| {
+                let focus_handle = focus_handle.clone();
+                let configuration_error_ref = &configuration_error;
+
                 parent
                     .p_1p5()
-                          .justify_end()
-                          .gap_1()
+                    .justify_end()
+                    .gap_1()
                     .child(
                         h_flex()
                             .pl_1p5()
@@ -992,32 +1000,94 @@ impl AssistantPanel {
                                 Button::new("view-history", "View All")
                                     .style(ButtonStyle::Subtle)
                                     .label_size(LabelSize::Small)
-                                    .key_binding(KeyBinding::for_action_in(
-                                        &OpenHistory,
-                                        &self.focus_handle(cx),
-                                        window,
-                                        cx,
-                                    ).map(|kb| kb.size(rems_from_px(12.))),)
+                                    .key_binding(
+                                        KeyBinding::for_action_in(
+                                            &OpenHistory,
+                                            &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);
                                     }),
                             ),
                     )
-                    .child(v_flex().gap_1().children(
-                        recent_history.into_iter().map(|entry| {
-                             // TODO: Add keyboard navigation.
-                            match entry {
-                                HistoryEntry::Thread(thread) => {
-                                    PastThread::new(thread, cx.entity().downgrade(), false)
-                                        .into_any_element()
-                                }
-                                HistoryEntry::Context(context) => {
-                                    PastContext::new(context, cx.entity().downgrade(), false)
-                                        .into_any_element()
-                                }
+                    .child(
+                        v_flex()
+                            .gap_1()
+                            .children(
+                                recent_history.into_iter().map(|entry| {
+                                    // TODO: Add keyboard navigation.
+                                    match entry {
+                                        HistoryEntry::Thread(thread) => {
+                                            PastThread::new(thread, cx.entity().downgrade(), false)
+                                                .into_any_element()
+                                        }
+                                        HistoryEntry::Context(context) => {
+                                            PastContext::new(context, cx.entity().downgrade(), false)
+                                                .into_any_element()
+                                        }
+                                    }
+                                }),
+                            )
+                    )
+                    .map(|parent| {
+                        match configuration_error_ref {
+                            Some(ConfigurationError::ProviderNotAuthenticated)
+                            | Some(ConfigurationError::NoProvider) => {
+                                parent
+                                    .child(
+                                        Banner::new()
+                                            .severity(ui::Severity::Warning)
+                                            .children(
+                                                Label::new(
+                                                    "Configure at least one LLM provider to start using the panel.",
+                                                )
+                                                .size(LabelSize::Small),
+                                            )
+                                            .action_slot(
+                                                Button::new("settings", "Configure Provider")
+                                                    .style(ButtonStyle::Tinted(ui::TintColor::Warning))
+                                                    .label_size(LabelSize::Small)
+                                                    .key_binding(
+                                                        KeyBinding::for_action_in(
+                                                            &OpenConfiguration,
+                                                            &focus_handle,
+                                                            window,
+                                                            cx,
+                                                        )
+                                                        .map(|kb| kb.size(rems_from_px(12.))),
+                                                    )
+                                                    .on_click(|_event, window, cx| {
+                                                        window.dispatch_action(
+                                                            OpenConfiguration.boxed_clone(),
+                                                            cx,
+                                                        )
+                                                    }),
+                                            ),
+                                    )
                             }
-                        }),
-                    ))
+                            Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
+                                parent
+                                    .child(
+                                        Banner::new()
+                                            .severity(ui::Severity::Warning)
+                                            .children(
+                                                h_flex()
+                                                    .w_full()
+                                                    .children(
+                                                        provider.render_accept_terms(
+                                                            LanguageModelProviderTosView::ThreadtEmptyState,
+                                                            cx,
+                                                        ),
+                                                    ),
+                                            ),
+                                    )
+                            }
+                            None => parent,
+                        }
+                    })
             })
     }
 

crates/language_model/src/language_model.rs 🔗

@@ -353,7 +353,10 @@ pub trait LanguageModelProvider: 'static {
 
 #[derive(PartialEq, Eq)]
 pub enum LanguageModelProviderTosView {
-    ThreadEmptyState,
+    /// When there are some past interactions in the Agent Panel.
+    ThreadtEmptyState,
+    /// When there are no past interactions in the Agent Panel.
+    ThreadFreshStart,
     PromptEditorPopup,
     Configuration,
 }

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

@@ -401,56 +401,83 @@ fn render_accept_terms(
 
     let accept_terms_disabled = state.read(cx).accept_terms.is_some();
 
+    let thread_fresh_start = matches!(view_kind, LanguageModelProviderTosView::ThreadFreshStart);
+    let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadtEmptyState);
+
     let terms_button = Button::new("terms_of_service", "Terms of Service")
         .style(ButtonStyle::Subtle)
         .icon(IconName::ArrowUpRight)
         .icon_color(Color::Muted)
         .icon_size(IconSize::XSmall)
+        .when(thread_empty_state, |this| this.label_size(LabelSize::Small))
         .on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service"));
 
-    let thread_view = match view_kind {
-        LanguageModelProviderTosView::ThreadEmptyState => true,
-        LanguageModelProviderTosView::PromptEditorPopup => false,
-        LanguageModelProviderTosView::Configuration => false,
-    };
-
-    let form = v_flex()
-        .w_full()
-        .gap_2()
-        .child(
-            h_flex()
-                .flex_wrap()
-                .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")
+    let button_container = h_flex().child(
+        Button::new("accept_terms", "I accept the Terms of Service")
+            .when(!thread_empty_state, |this| {
+                this.full_width()
                     .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();
-                        move |_, _window, cx| {
-                            state
-                                .update(cx, |state, cx| state.accept_terms_of_service(cx))
-                                .ok();
-                        }
-                    }),
-            );
-
-            match view_kind {
-                LanguageModelProviderTosView::PromptEditorPopup => button_container.justify_end(),
-                LanguageModelProviderTosView::Configuration => button_container.justify_start(),
-                LanguageModelProviderTosView::ThreadEmptyState => button_container.justify_center(),
-            }
-        });
+            })
+            .when(thread_empty_state, |this| {
+                this.style(ButtonStyle::Tinted(TintColor::Warning))
+                    .label_size(LabelSize::Small)
+            })
+            .disabled(accept_terms_disabled)
+            .on_click({
+                let state = state.downgrade();
+                move |_, _window, cx| {
+                    state
+                        .update(cx, |state, cx| state.accept_terms_of_service(cx))
+                        .ok();
+                }
+            }),
+    );
+
+    let form = if thread_empty_state {
+        h_flex()
+            .w_full()
+            .flex_wrap()
+            .justify_between()
+            .child(
+                h_flex()
+                    .child(
+                        Label::new("To start using Zed AI, please read and accept the")
+                            .size(LabelSize::Small),
+                    )
+                    .child(terms_button),
+            )
+            .child(button_container)
+    } else {
+        v_flex()
+            .w_full()
+            .gap_2()
+            .child(
+                h_flex()
+                    .flex_wrap()
+                    .when(thread_fresh_start, |this| this.justify_center())
+                    .child(Label::new(
+                        "To start using Zed AI, please read and accept the",
+                    ))
+                    .child(terms_button),
+            )
+            .child({
+                match view_kind {
+                    LanguageModelProviderTosView::PromptEditorPopup => {
+                        button_container.w_full().justify_end()
+                    }
+                    LanguageModelProviderTosView::Configuration => {
+                        button_container.w_full().justify_start()
+                    }
+                    LanguageModelProviderTosView::ThreadFreshStart => {
+                        button_container.w_full().justify_center()
+                    }
+                    LanguageModelProviderTosView::ThreadtEmptyState => div().w_0(),
+                }
+            })
+    };
 
     Some(form.into_any())
 }

crates/ui/src/components/banner.rs 🔗

@@ -130,7 +130,7 @@ impl RenderOnce for Banner {
                 .child(content_area)
                 .child(action_slot);
         } else {
-            container = container.px_2().child(content_area);
+            container = container.px_2().child(div().w_full().child(content_area));
         }
 
         container