assistant panel: Show Zed AI notice & configuration hint (#15798)

Thorsten Ball and Bennet created

This adds two possible notices to the assistant panel:

- Shows notice if currently selected provider is not configured
- Shows notice if user is signed-out and (does not have provider OR
provider is zed.dev) and tells user to sign in

Design needs to be tweaked. cc @iamnbutler 

![screenshot-2024-08-05-13 11
21@2x](https://github.com/user-attachments/assets/ada2d881-2f81-49ed-bebf-2efbf06e7d82)


Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>

Change summary

crates/assistant/src/assistant_panel.rs | 172 +++++++++++++++++++++-----
1 file changed, 136 insertions(+), 36 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -18,7 +18,7 @@ use crate::{
 use crate::{ContextStoreEvent, ShowConfiguration};
 use anyhow::{anyhow, Result};
 use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
-use client::proto;
+use client::{proto, Client, Status};
 use collections::{BTreeSet, HashMap, HashSet};
 use editor::{
     actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
@@ -147,7 +147,7 @@ pub struct AssistantPanel {
     authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
     configuration_subscription: Option<Subscription>,
     watch_client_status: Option<Task<()>>,
-    nudge_sign_in: bool,
+    show_zed_ai_notice: bool,
 }
 
 #[derive(Clone)]
@@ -419,37 +419,7 @@ impl AssistantPanel {
             ),
         ];
 
-        let mut status_rx = workspace.client().clone().status();
-
-        let watch_client_status = cx.spawn(|this, mut cx| async move {
-            let mut old_status = None;
-            while let Some(status) = status_rx.next().await {
-                if old_status.is_none()
-                    || old_status.map_or(false, |old_status| old_status != status)
-                {
-                    if status.is_signed_out() {
-                        this.update(&mut cx, |this, cx| {
-                            let active_provider =
-                                LanguageModelRegistry::read_global(cx).active_provider();
-
-                            // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
-                            // the provider, we want to show a nudge to sign in.
-                            if active_provider
-                                .map_or(true, |provider| provider.id().0 == PROVIDER_ID)
-                            {
-                                println!("TODO: Nudge the user to sign in and use Zed AI");
-                                this.nudge_sign_in = true;
-                            }
-                        })
-                        .log_err();
-                    };
-
-                    old_status = Some(status);
-                }
-            }
-            this.update(&mut cx, |this, _cx| this.watch_client_status = None)
-                .log_err();
-        });
+        let watch_client_status = Self::watch_client_status(workspace.client().clone(), cx);
 
         let mut this = Self {
             pane,
@@ -466,13 +436,34 @@ impl AssistantPanel {
             authenticate_provider_task: None,
             configuration_subscription: None,
             watch_client_status: Some(watch_client_status),
-            // TODO: This is unused!
-            nudge_sign_in: false,
+            show_zed_ai_notice: false,
         };
         this.new_context(cx);
         this
     }
 
+    fn watch_client_status(client: Arc<Client>, cx: &mut ViewContext<Self>) -> Task<()> {
+        let mut status_rx = client.status();
+
+        cx.spawn(|this, mut cx| async move {
+            let mut old_status = None;
+            while let Some(status) = status_rx.next().await {
+                if old_status.is_none()
+                    || old_status.map_or(false, |old_status| old_status != status)
+                {
+                    this.update(&mut cx, |this, cx| {
+                        this.handle_client_status_change(status, cx)
+                    })
+                    .log_err();
+
+                    old_status = Some(status);
+                }
+            }
+            this.update(&mut cx, |this, _cx| this.watch_client_status = None)
+                .log_err();
+        })
+    }
+
     fn handle_pane_event(
         &mut self,
         pane: View<Pane>,
@@ -563,6 +554,18 @@ impl AssistantPanel {
         }
     }
 
+    fn handle_client_status_change(&mut self, client_status: Status, cx: &mut ViewContext<Self>) {
+        let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
+
+        // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
+        // the provider, we want to show a nudge to sign in.
+        let show_zed_ai_notice = client_status.is_signed_out()
+            && active_provider.map_or(true, |provider| provider.id().0 == PROVIDER_ID);
+
+        self.show_zed_ai_notice = show_zed_ai_notice;
+        cx.notify();
+    }
+
     fn handle_toolbar_event(
         &mut self,
         _: View<ContextEditorToolbarItem>,
@@ -2314,6 +2317,69 @@ impl ContextEditor {
             .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
     }
 
+    fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
+        let nudge = self
+            .assistant_panel
+            .upgrade()
+            .map(|assistant_panel| assistant_panel.read(cx).show_zed_ai_notice);
+
+        if nudge.unwrap_or(false) {
+            Some(
+                v_flex()
+                    .elevation_3(cx)
+                    .p_4()
+                    .gap_2()
+                    .child(Label::new("Use Zed AI"))
+                    .child(
+                        div()
+                            .id("sign-in")
+                            .child(Label::new("Sign in to use Zed AI"))
+                            .cursor_pointer()
+                            .on_click(cx.listener(|this, _event, cx| {
+                                let client = this
+                                    .workspace
+                                    .update(cx, |workspace, _| workspace.client().clone())
+                                    .log_err();
+
+                                if let Some(client) = client {
+                                    cx.spawn(|this, mut cx| async move {
+                                        client.authenticate_and_connect(true, &mut cx).await?;
+                                        this.update(&mut cx, |_, cx| cx.notify())
+                                    })
+                                    .detach_and_log_err(cx)
+                                }
+                            })),
+                    ),
+            )
+        } else if let Some(configuration_error) = configuration_error(cx) {
+            let label = match configuration_error {
+                ConfigurationError::NoProvider => "No provider configured",
+                ConfigurationError::ProviderNotAuthenticated => "Provider is not configured",
+            };
+            Some(
+                v_flex()
+                    .elevation_3(cx)
+                    .p_4()
+                    .gap_2()
+                    .child(Label::new(label))
+                    .child(
+                        div()
+                            .id("open-configuration")
+                            .child(Label::new("Open configuration"))
+                            .cursor_pointer()
+                            .on_click({
+                                let focus_handle = self.focus_handle(cx).clone();
+                                move |_event, cx| {
+                                    focus_handle.dispatch_action(&ShowConfiguration, cx);
+                                }
+                            }),
+                    ),
+            )
+        } else {
+            None
+        }
+    }
+
     fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let focus_handle = self.focus_handle(cx).clone();
         let button_text = match self.workflow_step_for_cursor(cx) {
@@ -2419,7 +2485,15 @@ impl Render for ContextEditor {
                             .bottom_0()
                             .p_4()
                             .justify_end()
-                            .child(self.render_send_button(cx)),
+                            .child(
+                                v_flex()
+                                    .gap_2()
+                                    .items_end()
+                                    .when_some(self.render_notice(cx), |this, notice| {
+                                        this.child(notice)
+                                    })
+                                    .child(self.render_send_button(cx)),
+                            ),
                     ),
             )
     }
@@ -3305,3 +3379,29 @@ fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState>
     };
     Some(token_state)
 }
+
+enum ConfigurationError {
+    NoProvider,
+    ProviderNotAuthenticated,
+}
+
+fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
+    let provider = LanguageModelRegistry::read_global(cx).active_provider();
+    let is_authenticated = provider
+        .as_ref()
+        .map_or(false, |provider| provider.is_authenticated(cx));
+
+    if provider.is_some() && is_authenticated {
+        return None;
+    }
+
+    if provider.is_none() {
+        return Some(ConfigurationError::NoProvider);
+    }
+
+    if !is_authenticated {
+        return Some(ConfigurationError::ProviderNotAuthenticated);
+    }
+
+    None
+}