context_store.rs

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