add semantic index status, for non authenticated users (#3005)

Kyle Caverly created

Update project search semantic ui to accommodate for users who have not
set the OPENAI_API_KEY in their environment variables.

Release Notes:

- Expand Semantic Index status to include status for non authenticated
users
- Update Search UI to illustrate this status.

Change summary

crates/search/src/project_search.rs               | 22 +++++++++++++---
crates/semantic_index/src/embedding.rs            |  7 +++++
crates/semantic_index/src/semantic_index.rs       | 17 ++++++++++++
crates/semantic_index/src/semantic_index_tests.rs |  3 ++
4 files changed, 43 insertions(+), 6 deletions(-)

Detailed changes

crates/search/src/project_search.rs 🔗

@@ -322,7 +322,7 @@ impl View for ProjectSearchView {
             // If Text -> Major: "Text search all files and folders", Minor: {...}
 
             let current_mode = self.current_mode;
-            let major_text = if model.pending_search.is_some() {
+            let mut major_text = if model.pending_search.is_some() {
                 Cow::Borrowed("Searching...")
             } else if model.no_results.is_some_and(|v| v) {
                 Cow::Borrowed("No Results")
@@ -336,9 +336,18 @@ impl View for ProjectSearchView {
                 }
             };
 
+            let mut show_minor_text = true;
             let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
                 let status = semantic.index_status;
                 match status {
+                    SemanticIndexStatus::NotAuthenticated => {
+                        major_text = Cow::Borrowed("Not Authenticated");
+                        show_minor_text = false;
+                        Some(
+                            "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables"
+                                .to_string(),
+                        )
+                    }
                     SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
                     SemanticIndexStatus::Indexing {
                         remaining_files,
@@ -380,10 +389,13 @@ impl View for ProjectSearchView {
                         let mut minor_text = Vec::new();
                         minor_text.push("".into());
                         minor_text.extend(semantic_status);
-                        minor_text.push("Simply explain the code you are looking to find.".into());
-                        minor_text.push(
-                            "ex. 'prompt user for permissions to index their project'".into(),
-                        );
+                        if show_minor_text {
+                            minor_text
+                                .push("Simply explain the code you are looking to find.".into());
+                            minor_text.push(
+                                "ex. 'prompt user for permissions to index their project'".into(),
+                            );
+                        }
                         minor_text
                     }
                     _ => vec![

crates/semantic_index/src/embedding.rs 🔗

@@ -117,6 +117,7 @@ struct OpenAIEmbeddingUsage {
 
 #[async_trait]
 pub trait EmbeddingProvider: Sync + Send {
+    fn is_authenticated(&self) -> bool;
     async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
     fn max_tokens_per_batch(&self) -> usize;
     fn truncate(&self, span: &str) -> (String, usize);
@@ -127,6 +128,9 @@ pub struct DummyEmbeddings {}
 
 #[async_trait]
 impl EmbeddingProvider for DummyEmbeddings {
+    fn is_authenticated(&self) -> bool {
+        true
+    }
     fn rate_limit_expiration(&self) -> Option<Instant> {
         None
     }
@@ -229,6 +233,9 @@ impl OpenAIEmbeddings {
 
 #[async_trait]
 impl EmbeddingProvider for OpenAIEmbeddings {
+    fn is_authenticated(&self) -> bool {
+        OPENAI_API_KEY.as_ref().is_some()
+    }
     fn max_tokens_per_batch(&self) -> usize {
         50000
     }

crates/semantic_index/src/semantic_index.rs 🔗

@@ -16,6 +16,7 @@ use embedding_queue::{EmbeddingQueue, FileToEmbed};
 use futures::{future, FutureExt, StreamExt};
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
 use language::{Anchor, Bias, Buffer, Language, LanguageRegistry};
+use lazy_static::lazy_static;
 use ordered_float::OrderedFloat;
 use parking_lot::Mutex;
 use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES};
@@ -24,6 +25,7 @@ use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, Work
 use smol::channel;
 use std::{
     cmp::Reverse,
+    env,
     future::Future,
     mem,
     ops::Range,
@@ -38,6 +40,10 @@ const SEMANTIC_INDEX_VERSION: usize = 11;
 const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60);
 const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250);
 
+lazy_static! {
+    static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
+}
+
 pub fn init(
     fs: Arc<dyn Fs>,
     http_client: Arc<dyn HttpClient>,
@@ -100,6 +106,7 @@ pub fn init(
 
 #[derive(Copy, Clone, Debug)]
 pub enum SemanticIndexStatus {
+    NotAuthenticated,
     NotIndexed,
     Indexed,
     Indexing {
@@ -275,6 +282,10 @@ impl SemanticIndex {
     }
 
     pub fn status(&self, project: &ModelHandle<Project>) -> SemanticIndexStatus {
+        if !self.embedding_provider.is_authenticated() {
+            return SemanticIndexStatus::NotAuthenticated;
+        }
+
         if let Some(project_state) = self.projects.get(&project.downgrade()) {
             if project_state
                 .worktrees
@@ -694,12 +705,12 @@ impl SemanticIndex {
         let embedding_provider = self.embedding_provider.clone();
 
         cx.spawn(|this, mut cx| async move {
+            index.await?;
             let query = embedding_provider
                 .embed_batch(vec![query])
                 .await?
                 .pop()
                 .ok_or_else(|| anyhow!("could not embed query"))?;
-            index.await?;
 
             let search_start = Instant::now();
             let modified_buffer_results = this.update(&mut cx, |this, cx| {
@@ -965,6 +976,10 @@ impl SemanticIndex {
         project: ModelHandle<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
+        if !self.embedding_provider.is_authenticated() {
+            return Task::ready(Err(anyhow!("user is not authenticated")));
+        }
+
         if !self.projects.contains_key(&project.downgrade()) {
             let subscription = cx.subscribe(&project, |this, project, event, cx| match event {
                 project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => {

crates/semantic_index/src/semantic_index_tests.rs 🔗

@@ -1267,6 +1267,9 @@ impl FakeEmbeddingProvider {
 
 #[async_trait]
 impl EmbeddingProvider for FakeEmbeddingProvider {
+    fn is_authenticated(&self) -> bool {
+        true
+    }
     fn truncate(&self, span: &str) -> (String, usize) {
         (span.to_string(), 1)
     }