context_store.rs

  1use crate::{
  2    AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
  3    SavedContextMetadata,
  4};
  5use anyhow::{Context as _, Result, anyhow};
  6use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
  7use client::{Client, TypedEnvelope, proto, telemetry::Telemetry};
  8use clock::ReplicaId;
  9use collections::HashMap;
 10use context_server::ContextServerId;
 11use fs::{Fs, RemoveOptions};
 12use futures::StreamExt;
 13use fuzzy::StringMatchCandidate;
 14use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity};
 15use language::LanguageRegistry;
 16use paths::contexts_dir;
 17use project::{
 18    Project,
 19    context_server_store::{ContextServerStatus, ContextServerStore},
 20};
 21use prompt_store::PromptBuilder;
 22use regex::Regex;
 23use rpc::AnyProtoClient;
 24use std::sync::LazyLock;
 25use std::{cmp::Reverse, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration};
 26use util::{ResultExt, TryFutureExt};
 27
 28pub(crate) fn init(client: &AnyProtoClient) {
 29    client.add_entity_message_handler(ContextStore::handle_advertise_contexts);
 30    client.add_entity_request_handler(ContextStore::handle_open_context);
 31    client.add_entity_request_handler(ContextStore::handle_create_context);
 32    client.add_entity_message_handler(ContextStore::handle_update_context);
 33    client.add_entity_request_handler(ContextStore::handle_synchronize_contexts);
 34}
 35
 36#[derive(Clone)]
 37pub struct RemoteContextMetadata {
 38    pub id: ContextId,
 39    pub summary: Option<String>,
 40}
 41
 42pub struct ContextStore {
 43    contexts: Vec<ContextHandle>,
 44    contexts_metadata: Vec<SavedContextMetadata>,
 45    context_server_slash_command_ids: HashMap<ContextServerId, Vec<SlashCommandId>>,
 46    host_contexts: Vec<RemoteContextMetadata>,
 47    fs: Arc<dyn Fs>,
 48    languages: Arc<LanguageRegistry>,
 49    slash_commands: Arc<SlashCommandWorkingSet>,
 50    telemetry: Arc<Telemetry>,
 51    _watch_updates: Task<Option<()>>,
 52    client: Arc<Client>,
 53    project: Entity<Project>,
 54    project_is_shared: bool,
 55    client_subscription: Option<client::Subscription>,
 56    _project_subscriptions: Vec<gpui::Subscription>,
 57    prompt_builder: Arc<PromptBuilder>,
 58}
 59
 60pub enum ContextStoreEvent {
 61    ContextCreated(ContextId),
 62}
 63
 64impl EventEmitter<ContextStoreEvent> for ContextStore {}
 65
 66enum ContextHandle {
 67    Weak(WeakEntity<AssistantContext>),
 68    Strong(Entity<AssistantContext>),
 69}
 70
 71impl ContextHandle {
 72    fn upgrade(&self) -> Option<Entity<AssistantContext>> {
 73        match self {
 74            ContextHandle::Weak(weak) => weak.upgrade(),
 75            ContextHandle::Strong(strong) => Some(strong.clone()),
 76        }
 77    }
 78
 79    fn downgrade(&self) -> WeakEntity<AssistantContext> {
 80        match self {
 81            ContextHandle::Weak(weak) => weak.clone(),
 82            ContextHandle::Strong(strong) => strong.downgrade(),
 83        }
 84    }
 85}
 86
 87impl ContextStore {
 88    pub fn new(
 89        project: Entity<Project>,
 90        prompt_builder: Arc<PromptBuilder>,
 91        slash_commands: Arc<SlashCommandWorkingSet>,
 92        cx: &mut App,
 93    ) -> Task<Result<Entity<Self>>> {
 94        let fs = project.read(cx).fs().clone();
 95        let languages = project.read(cx).languages().clone();
 96        let telemetry = project.read(cx).client().telemetry().clone();
 97        cx.spawn(async move |cx| {
 98            const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
 99            let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
100
101            let this = cx.new(|cx: &mut Context<Self>| {
102                let mut this = Self {
103                    contexts: Vec::new(),
104                    contexts_metadata: Vec::new(),
105                    context_server_slash_command_ids: HashMap::default(),
106                    host_contexts: Vec::new(),
107                    fs,
108                    languages,
109                    slash_commands,
110                    telemetry,
111                    _watch_updates: cx.spawn(async move |this, cx| {
112                        async move {
113                            while events.next().await.is_some() {
114                                this.update(cx, |this, cx| this.reload(cx))?.await.log_err();
115                            }
116                            anyhow::Ok(())
117                        }
118                        .log_err()
119                        .await
120                    }),
121                    client_subscription: None,
122                    _project_subscriptions: vec![
123                        cx.subscribe(&project, Self::handle_project_event),
124                    ],
125                    project_is_shared: false,
126                    client: project.read(cx).client(),
127                    project: project.clone(),
128                    prompt_builder,
129                };
130                this.handle_project_shared(project.clone(), cx);
131                this.synchronize_contexts(cx);
132                this.register_context_server_handlers(cx);
133                this.reload(cx).detach_and_log_err(cx);
134                this
135            })?;
136
137            Ok(this)
138        })
139    }
140
141    async fn handle_advertise_contexts(
142        this: Entity<Self>,
143        envelope: TypedEnvelope<proto::AdvertiseContexts>,
144        mut cx: AsyncApp,
145    ) -> Result<()> {
146        this.update(&mut cx, |this, cx| {
147            this.host_contexts = envelope
148                .payload
149                .contexts
150                .into_iter()
151                .map(|context| RemoteContextMetadata {
152                    id: ContextId::from_proto(context.context_id),
153                    summary: context.summary,
154                })
155                .collect();
156            cx.notify();
157        })
158    }
159
160    async fn handle_open_context(
161        this: Entity<Self>,
162        envelope: TypedEnvelope<proto::OpenContext>,
163        mut cx: AsyncApp,
164    ) -> Result<proto::OpenContextResponse> {
165        let context_id = ContextId::from_proto(envelope.payload.context_id);
166        let operations = this.update(&mut cx, |this, cx| {
167            if this.project.read(cx).is_via_collab() {
168                return Err(anyhow!("only the host contexts can be opened"));
169            }
170
171            let context = this
172                .loaded_context_for_id(&context_id, cx)
173                .context("context not found")?;
174            if context.read(cx).replica_id() != ReplicaId::default() {
175                return Err(anyhow!("context must be opened via the host"));
176            }
177
178            anyhow::Ok(
179                context
180                    .read(cx)
181                    .serialize_ops(&ContextVersion::default(), cx),
182            )
183        })??;
184        let operations = operations.await;
185        Ok(proto::OpenContextResponse {
186            context: Some(proto::Context { operations }),
187        })
188    }
189
190    async fn handle_create_context(
191        this: Entity<Self>,
192        _: TypedEnvelope<proto::CreateContext>,
193        mut cx: AsyncApp,
194    ) -> Result<proto::CreateContextResponse> {
195        let (context_id, operations) = this.update(&mut cx, |this, cx| {
196            if this.project.read(cx).is_via_collab() {
197                return Err(anyhow!("can only create contexts as the host"));
198            }
199
200            let context = this.create(cx);
201            let context_id = context.read(cx).id().clone();
202            cx.emit(ContextStoreEvent::ContextCreated(context_id.clone()));
203
204            anyhow::Ok((
205                context_id,
206                context
207                    .read(cx)
208                    .serialize_ops(&ContextVersion::default(), cx),
209            ))
210        })??;
211        let operations = operations.await;
212        Ok(proto::CreateContextResponse {
213            context_id: context_id.to_proto(),
214            context: Some(proto::Context { operations }),
215        })
216    }
217
218    async fn handle_update_context(
219        this: Entity<Self>,
220        envelope: TypedEnvelope<proto::UpdateContext>,
221        mut cx: AsyncApp,
222    ) -> Result<()> {
223        this.update(&mut cx, |this, cx| {
224            let context_id = ContextId::from_proto(envelope.payload.context_id);
225            if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
226                let operation_proto = envelope.payload.operation.context("invalid operation")?;
227                let operation = ContextOperation::from_proto(operation_proto)?;
228                context.update(cx, |context, cx| context.apply_ops([operation], cx));
229            }
230            Ok(())
231        })?
232    }
233
234    async fn handle_synchronize_contexts(
235        this: Entity<Self>,
236        envelope: TypedEnvelope<proto::SynchronizeContexts>,
237        mut cx: AsyncApp,
238    ) -> Result<proto::SynchronizeContextsResponse> {
239        this.update(&mut cx, |this, cx| {
240            if this.project.read(cx).is_via_collab() {
241                return Err(anyhow!("only the host can synchronize contexts"));
242            }
243
244            let mut local_versions = Vec::new();
245            for remote_version_proto in envelope.payload.contexts {
246                let remote_version = ContextVersion::from_proto(&remote_version_proto);
247                let context_id = ContextId::from_proto(remote_version_proto.context_id);
248                if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
249                    let context = context.read(cx);
250                    let operations = context.serialize_ops(&remote_version, cx);
251                    local_versions.push(context.version(cx).to_proto(context_id.clone()));
252                    let client = this.client.clone();
253                    let project_id = envelope.payload.project_id;
254                    cx.background_spawn(async move {
255                        let operations = operations.await;
256                        for operation in operations {
257                            client.send(proto::UpdateContext {
258                                project_id,
259                                context_id: context_id.to_proto(),
260                                operation: Some(operation),
261                            })?;
262                        }
263                        anyhow::Ok(())
264                    })
265                    .detach_and_log_err(cx);
266                }
267            }
268
269            this.advertise_contexts(cx);
270
271            anyhow::Ok(proto::SynchronizeContextsResponse {
272                contexts: local_versions,
273            })
274        })?
275    }
276
277    fn handle_project_shared(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
278        let is_shared = self.project.read(cx).is_shared();
279        let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
280        if is_shared == was_shared {
281            return;
282        }
283
284        if is_shared {
285            self.contexts.retain_mut(|context| {
286                if let Some(strong_context) = context.upgrade() {
287                    *context = ContextHandle::Strong(strong_context);
288                    true
289                } else {
290                    false
291                }
292            });
293            let remote_id = self.project.read(cx).remote_id().unwrap();
294            self.client_subscription = self
295                .client
296                .subscribe_to_entity(remote_id)
297                .log_err()
298                .map(|subscription| subscription.set_entity(&cx.entity(), &mut cx.to_async()));
299            self.advertise_contexts(cx);
300        } else {
301            self.client_subscription = None;
302        }
303    }
304
305    fn handle_project_event(
306        &mut self,
307        project: Entity<Project>,
308        event: &project::Event,
309        cx: &mut Context<Self>,
310    ) {
311        match event {
312            project::Event::RemoteIdChanged(_) => {
313                self.handle_project_shared(project, cx);
314            }
315            project::Event::Reshared => {
316                self.advertise_contexts(cx);
317            }
318            project::Event::HostReshared | project::Event::Rejoined => {
319                self.synchronize_contexts(cx);
320            }
321            project::Event::DisconnectedFromHost => {
322                self.contexts.retain_mut(|context| {
323                    if let Some(strong_context) = context.upgrade() {
324                        *context = ContextHandle::Weak(context.downgrade());
325                        strong_context.update(cx, |context, cx| {
326                            if context.replica_id() != ReplicaId::default() {
327                                context.set_capability(language::Capability::ReadOnly, cx);
328                            }
329                        });
330                        true
331                    } else {
332                        false
333                    }
334                });
335                self.host_contexts.clear();
336                cx.notify();
337            }
338            _ => {}
339        }
340    }
341
342    pub fn unordered_contexts(&self) -> impl Iterator<Item = &SavedContextMetadata> {
343        self.contexts_metadata.iter()
344    }
345
346    pub fn reverse_chronological_contexts(&self) -> Vec<SavedContextMetadata> {
347        let mut contexts = self.contexts_metadata.iter().cloned().collect::<Vec<_>>();
348        contexts.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.mtime));
349        contexts
350    }
351
352    pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<AssistantContext> {
353        let context = cx.new(|cx| {
354            AssistantContext::local(
355                self.languages.clone(),
356                Some(self.project.clone()),
357                Some(self.telemetry.clone()),
358                self.prompt_builder.clone(),
359                self.slash_commands.clone(),
360                cx,
361            )
362        });
363        self.register_context(&context, cx);
364        context
365    }
366
367    pub fn create_remote_context(
368        &mut self,
369        cx: &mut Context<Self>,
370    ) -> Task<Result<Entity<AssistantContext>>> {
371        let project = self.project.read(cx);
372        let Some(project_id) = project.remote_id() else {
373            return Task::ready(Err(anyhow!("project was not remote")));
374        };
375
376        let replica_id = project.replica_id();
377        let capability = project.capability();
378        let language_registry = self.languages.clone();
379        let project = self.project.clone();
380        let telemetry = self.telemetry.clone();
381        let prompt_builder = self.prompt_builder.clone();
382        let slash_commands = self.slash_commands.clone();
383        let request = self.client.request(proto::CreateContext { project_id });
384        cx.spawn(async move |this, cx| {
385            let response = request.await?;
386            let context_id = ContextId::from_proto(response.context_id);
387            let context_proto = response.context.context("invalid context")?;
388            let context = cx.new(|cx| {
389                AssistantContext::new(
390                    context_id.clone(),
391                    replica_id,
392                    capability,
393                    language_registry,
394                    prompt_builder,
395                    slash_commands,
396                    Some(project),
397                    Some(telemetry),
398                    cx,
399                )
400            })?;
401            let operations = cx
402                .background_spawn(async move {
403                    context_proto
404                        .operations
405                        .into_iter()
406                        .map(ContextOperation::from_proto)
407                        .collect::<Result<Vec<_>>>()
408                })
409                .await?;
410            context.update(cx, |context, cx| context.apply_ops(operations, cx))?;
411            this.update(cx, |this, cx| {
412                if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
413                    existing_context
414                } else {
415                    this.register_context(&context, cx);
416                    this.synchronize_contexts(cx);
417                    context
418                }
419            })
420        })
421    }
422
423    pub fn open_local_context(
424        &mut self,
425        path: Arc<Path>,
426        cx: &Context<Self>,
427    ) -> Task<Result<Entity<AssistantContext>>> {
428        if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
429            return Task::ready(Ok(existing_context));
430        }
431
432        let fs = self.fs.clone();
433        let languages = self.languages.clone();
434        let project = self.project.clone();
435        let telemetry = self.telemetry.clone();
436        let load = cx.background_spawn({
437            let path = path.clone();
438            async move {
439                let saved_context = fs.load(&path).await?;
440                SavedContext::from_json(&saved_context)
441            }
442        });
443        let prompt_builder = self.prompt_builder.clone();
444        let slash_commands = self.slash_commands.clone();
445
446        cx.spawn(async move |this, cx| {
447            let saved_context = load.await?;
448            let context = cx.new(|cx| {
449                AssistantContext::deserialize(
450                    saved_context,
451                    path.clone(),
452                    languages,
453                    prompt_builder,
454                    slash_commands,
455                    Some(project),
456                    Some(telemetry),
457                    cx,
458                )
459            })?;
460            this.update(cx, |this, cx| {
461                if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
462                    existing_context
463                } else {
464                    this.register_context(&context, cx);
465                    context
466                }
467            })
468        })
469    }
470
471    pub fn delete_local_context(
472        &mut self,
473        path: Arc<Path>,
474        cx: &mut Context<Self>,
475    ) -> Task<Result<()>> {
476        let fs = self.fs.clone();
477
478        cx.spawn(async move |this, cx| {
479            fs.remove_file(
480                &path,
481                RemoveOptions {
482                    recursive: false,
483                    ignore_if_not_exists: true,
484                },
485            )
486            .await?;
487
488            this.update(cx, |this, cx| {
489                this.contexts.retain(|context| {
490                    context
491                        .upgrade()
492                        .and_then(|context| context.read(cx).path())
493                        != Some(&path)
494                });
495                this.contexts_metadata
496                    .retain(|context| context.path.as_ref() != path.as_ref());
497            })?;
498
499            Ok(())
500        })
501    }
502
503    fn loaded_context_for_path(&self, path: &Path, cx: &App) -> Option<Entity<AssistantContext>> {
504        self.contexts.iter().find_map(|context| {
505            let context = context.upgrade()?;
506            if context.read(cx).path().map(Arc::as_ref) == Some(path) {
507                Some(context)
508            } else {
509                None
510            }
511        })
512    }
513
514    pub fn loaded_context_for_id(
515        &self,
516        id: &ContextId,
517        cx: &App,
518    ) -> Option<Entity<AssistantContext>> {
519        self.contexts.iter().find_map(|context| {
520            let context = context.upgrade()?;
521            if context.read(cx).id() == id {
522                Some(context)
523            } else {
524                None
525            }
526        })
527    }
528
529    pub fn open_remote_context(
530        &mut self,
531        context_id: ContextId,
532        cx: &mut Context<Self>,
533    ) -> Task<Result<Entity<AssistantContext>>> {
534        let project = self.project.read(cx);
535        let Some(project_id) = project.remote_id() else {
536            return Task::ready(Err(anyhow!("project was not remote")));
537        };
538
539        if let Some(context) = self.loaded_context_for_id(&context_id, cx) {
540            return Task::ready(Ok(context));
541        }
542
543        let replica_id = project.replica_id();
544        let capability = project.capability();
545        let language_registry = self.languages.clone();
546        let project = self.project.clone();
547        let telemetry = self.telemetry.clone();
548        let request = self.client.request(proto::OpenContext {
549            project_id,
550            context_id: context_id.to_proto(),
551        });
552        let prompt_builder = self.prompt_builder.clone();
553        let slash_commands = self.slash_commands.clone();
554        cx.spawn(async move |this, cx| {
555            let response = request.await?;
556            let context_proto = response.context.context("invalid context")?;
557            let context = cx.new(|cx| {
558                AssistantContext::new(
559                    context_id.clone(),
560                    replica_id,
561                    capability,
562                    language_registry,
563                    prompt_builder,
564                    slash_commands,
565                    Some(project),
566                    Some(telemetry),
567                    cx,
568                )
569            })?;
570            let operations = cx
571                .background_spawn(async move {
572                    context_proto
573                        .operations
574                        .into_iter()
575                        .map(ContextOperation::from_proto)
576                        .collect::<Result<Vec<_>>>()
577                })
578                .await?;
579            context.update(cx, |context, cx| context.apply_ops(operations, cx))?;
580            this.update(cx, |this, cx| {
581                if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
582                    existing_context
583                } else {
584                    this.register_context(&context, cx);
585                    this.synchronize_contexts(cx);
586                    context
587                }
588            })
589        })
590    }
591
592    fn register_context(&mut self, context: &Entity<AssistantContext>, cx: &mut Context<Self>) {
593        let handle = if self.project_is_shared {
594            ContextHandle::Strong(context.clone())
595        } else {
596            ContextHandle::Weak(context.downgrade())
597        };
598        self.contexts.push(handle);
599        self.advertise_contexts(cx);
600        cx.subscribe(context, Self::handle_context_event).detach();
601    }
602
603    fn handle_context_event(
604        &mut self,
605        context: Entity<AssistantContext>,
606        event: &ContextEvent,
607        cx: &mut Context<Self>,
608    ) {
609        let Some(project_id) = self.project.read(cx).remote_id() else {
610            return;
611        };
612
613        match event {
614            ContextEvent::SummaryChanged => {
615                self.advertise_contexts(cx);
616            }
617            ContextEvent::Operation(operation) => {
618                let context_id = context.read(cx).id().to_proto();
619                let operation = operation.to_proto();
620                self.client
621                    .send(proto::UpdateContext {
622                        project_id,
623                        context_id,
624                        operation: Some(operation),
625                    })
626                    .log_err();
627            }
628            _ => {}
629        }
630    }
631
632    fn advertise_contexts(&self, cx: &App) {
633        let Some(project_id) = self.project.read(cx).remote_id() else {
634            return;
635        };
636
637        // For now, only the host can advertise their open contexts.
638        if self.project.read(cx).is_via_collab() {
639            return;
640        }
641
642        let contexts = self
643            .contexts
644            .iter()
645            .rev()
646            .filter_map(|context| {
647                let context = context.upgrade()?.read(cx);
648                if context.replica_id() == ReplicaId::default() {
649                    Some(proto::ContextMetadata {
650                        context_id: context.id().to_proto(),
651                        summary: context
652                            .summary()
653                            .content()
654                            .map(|summary| summary.text.clone()),
655                    })
656                } else {
657                    None
658                }
659            })
660            .collect();
661        self.client
662            .send(proto::AdvertiseContexts {
663                project_id,
664                contexts,
665            })
666            .ok();
667    }
668
669    fn synchronize_contexts(&mut self, cx: &mut Context<Self>) {
670        let Some(project_id) = self.project.read(cx).remote_id() else {
671            return;
672        };
673
674        let contexts = self
675            .contexts
676            .iter()
677            .filter_map(|context| {
678                let context = context.upgrade()?.read(cx);
679                if context.replica_id() != ReplicaId::default() {
680                    Some(context.version(cx).to_proto(context.id().clone()))
681                } else {
682                    None
683                }
684            })
685            .collect();
686
687        let client = self.client.clone();
688        let request = self.client.request(proto::SynchronizeContexts {
689            project_id,
690            contexts,
691        });
692        cx.spawn(async move |this, cx| {
693            let response = request.await?;
694
695            let mut context_ids = Vec::new();
696            let mut operations = Vec::new();
697            this.read_with(cx, |this, cx| {
698                for context_version_proto in response.contexts {
699                    let context_version = ContextVersion::from_proto(&context_version_proto);
700                    let context_id = ContextId::from_proto(context_version_proto.context_id);
701                    if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
702                        context_ids.push(context_id);
703                        operations.push(context.read(cx).serialize_ops(&context_version, cx));
704                    }
705                }
706            })?;
707
708            let operations = futures::future::join_all(operations).await;
709            for (context_id, operations) in context_ids.into_iter().zip(operations) {
710                for operation in operations {
711                    client.send(proto::UpdateContext {
712                        project_id,
713                        context_id: context_id.to_proto(),
714                        operation: Some(operation),
715                    })?;
716                }
717            }
718
719            anyhow::Ok(())
720        })
721        .detach_and_log_err(cx);
722    }
723
724    pub fn search(&self, query: String, cx: &App) -> Task<Vec<SavedContextMetadata>> {
725        let metadata = self.contexts_metadata.clone();
726        let executor = cx.background_executor().clone();
727        cx.background_spawn(async move {
728            if query.is_empty() {
729                metadata
730            } else {
731                let candidates = metadata
732                    .iter()
733                    .enumerate()
734                    .map(|(id, metadata)| StringMatchCandidate::new(id, &metadata.title))
735                    .collect::<Vec<_>>();
736                let matches = fuzzy::match_strings(
737                    &candidates,
738                    &query,
739                    false,
740                    100,
741                    &Default::default(),
742                    executor,
743                )
744                .await;
745
746                matches
747                    .into_iter()
748                    .map(|mat| metadata[mat.candidate_id].clone())
749                    .collect()
750            }
751        })
752    }
753
754    pub fn host_contexts(&self) -> &[RemoteContextMetadata] {
755        &self.host_contexts
756    }
757
758    fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
759        let fs = self.fs.clone();
760        cx.spawn(async move |this, cx| {
761            fs.create_dir(contexts_dir()).await?;
762
763            let mut paths = fs.read_dir(contexts_dir()).await?;
764            let mut contexts = Vec::<SavedContextMetadata>::new();
765            while let Some(path) = paths.next().await {
766                let path = path?;
767                if path.extension() != Some(OsStr::new("json")) {
768                    continue;
769                }
770
771                static ASSISTANT_CONTEXT_REGEX: LazyLock<Regex> =
772                    LazyLock::new(|| Regex::new(r" - \d+.zed.json$").unwrap());
773
774                let metadata = fs.metadata(&path).await?;
775                if let Some((file_name, metadata)) = path
776                    .file_name()
777                    .and_then(|name| name.to_str())
778                    .zip(metadata)
779                {
780                    // This is used to filter out contexts saved by the new assistant.
781                    if !ASSISTANT_CONTEXT_REGEX.is_match(file_name) {
782                        continue;
783                    }
784
785                    if let Some(title) = ASSISTANT_CONTEXT_REGEX
786                        .replace(file_name, "")
787                        .lines()
788                        .next()
789                    {
790                        contexts.push(SavedContextMetadata {
791                            title: title.to_string(),
792                            path: path.into(),
793                            mtime: metadata.mtime.timestamp_for_user().into(),
794                        });
795                    }
796                }
797            }
798            contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
799
800            this.update(cx, |this, cx| {
801                this.contexts_metadata = contexts;
802                cx.notify();
803            })
804        })
805    }
806
807    fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
808        cx.subscribe(
809            &self.project.read(cx).context_server_store(),
810            Self::handle_context_server_event,
811        )
812        .detach();
813    }
814
815    fn handle_context_server_event(
816        &mut self,
817        context_server_manager: Entity<ContextServerStore>,
818        event: &project::context_server_store::Event,
819        cx: &mut Context<Self>,
820    ) {
821        let slash_command_working_set = self.slash_commands.clone();
822        match event {
823            project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
824                match status {
825                    ContextServerStatus::Running => {
826                        if let Some(server) = context_server_manager
827                            .read(cx)
828                            .get_running_server(server_id)
829                        {
830                            let context_server_manager = context_server_manager.clone();
831                            cx.spawn({
832                                let server = server.clone();
833                                let server_id = server_id.clone();
834                                async move |this, cx| {
835                                    let Some(protocol) = server.client() else {
836                                        return;
837                                    };
838
839                                    if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
840                                        if let Some(prompts) = protocol.list_prompts().await.log_err() {
841                                            let slash_command_ids = prompts
842                                                .into_iter()
843                                                .filter(assistant_slash_commands::acceptable_prompt)
844                                                .map(|prompt| {
845                                                    log::info!(
846                                                        "registering context server command: {:?}",
847                                                        prompt.name
848                                                    );
849                                                    slash_command_working_set.insert(Arc::new(
850                                                        assistant_slash_commands::ContextServerSlashCommand::new(
851                                                            context_server_manager.clone(),
852                                                            server.id(),
853                                                            prompt,
854                                                        ),
855                                                    ))
856                                                })
857                                                .collect::<Vec<_>>();
858
859                                            this.update( cx, |this, _cx| {
860                                                this.context_server_slash_command_ids
861                                                    .insert(server_id.clone(), slash_command_ids);
862                                            })
863                                            .log_err();
864                                        }
865                                    }
866                                }
867                            })
868                            .detach();
869                        }
870                    }
871                    ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
872                        if let Some(slash_command_ids) =
873                            self.context_server_slash_command_ids.remove(server_id)
874                        {
875                            slash_command_working_set.remove(&slash_command_ids);
876                        }
877                    }
878                    _ => {}
879                }
880            }
881        }
882    }
883}