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}