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}