context.rs

  1use std::hash::{Hash, Hasher};
  2use std::{ops::Range, path::Path, sync::Arc};
  3
  4use collections::HashSet;
  5use futures::future;
  6use futures::{FutureExt, future::Shared};
  7use gpui::{App, AppContext as _, Entity, SharedString, Task};
  8use language::Buffer;
  9use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
 10use project::{Project, ProjectEntryId, ProjectPath, Worktree};
 11use prompt_store::{PromptStore, UserPromptId};
 12use ref_cast::RefCast;
 13use rope::{Point, Rope};
 14use text::{Anchor, OffsetRangeExt as _};
 15use ui::{ElementId, IconName};
 16use util::{ResultExt as _, post_inc};
 17
 18use crate::thread::Thread;
 19
 20pub const RULES_ICON: IconName = IconName::Context;
 21
 22pub enum ContextKind {
 23    File,
 24    Directory,
 25    Symbol,
 26    Selection,
 27    FetchedUrl,
 28    Thread,
 29    Rules,
 30    Image,
 31}
 32
 33impl ContextKind {
 34    pub fn icon(&self) -> IconName {
 35        match self {
 36            ContextKind::File => IconName::File,
 37            ContextKind::Directory => IconName::Folder,
 38            ContextKind::Symbol => IconName::Code,
 39            ContextKind::Selection => IconName::Context,
 40            ContextKind::FetchedUrl => IconName::Globe,
 41            ContextKind::Thread => IconName::MessageBubbles,
 42            ContextKind::Rules => RULES_ICON,
 43            ContextKind::Image => IconName::Image,
 44        }
 45    }
 46}
 47
 48/// Handle for context that can be added to a user message.
 49///
 50/// This uses IDs that are stable enough for tracking renames and identifying when context has
 51/// already been added to the thread. To use this in a set, wrap it in `AgentContextKey` to opt in
 52/// to `PartialEq` and `Hash` impls that use the subset of the fields used for this stable identity.
 53#[derive(Debug, Clone)]
 54pub enum AgentContext {
 55    File(FileContext),
 56    Directory(DirectoryContext),
 57    Symbol(SymbolContext),
 58    Selection(SelectionContext),
 59    FetchedUrl(FetchedUrlContext),
 60    Thread(ThreadContext),
 61    Rules(RulesContext),
 62    Image(ImageContext),
 63}
 64
 65impl AgentContext {
 66    fn id(&self) -> ContextId {
 67        match self {
 68            Self::File(context) => context.context_id,
 69            Self::Directory(context) => context.context_id,
 70            Self::Symbol(context) => context.context_id,
 71            Self::Selection(context) => context.context_id,
 72            Self::FetchedUrl(context) => context.context_id,
 73            Self::Thread(context) => context.context_id,
 74            Self::Rules(context) => context.context_id,
 75            Self::Image(context) => context.context_id,
 76        }
 77    }
 78
 79    pub fn element_id(&self, name: SharedString) -> ElementId {
 80        ElementId::NamedInteger(name, self.id().0)
 81    }
 82}
 83
 84/// ID created at time of context add, for use in ElementId. This is not the stable identity of a
 85/// context, instead that's handled by the `PartialEq` and `Hash` impls of `AgentContextKey`.
 86#[derive(Debug, Copy, Clone)]
 87pub struct ContextId(u64);
 88
 89impl ContextId {
 90    pub fn zero() -> Self {
 91        ContextId(0)
 92    }
 93
 94    fn for_lookup() -> Self {
 95        ContextId(u64::MAX)
 96    }
 97
 98    pub fn post_inc(&mut self) -> Self {
 99        Self(post_inc(&mut self.0))
100    }
101}
102
103/// File context provides the entire contents of a file.
104///
105/// This holds an `Entity<Buffer>` so that file path renames affect its display and so that it can
106/// be opened even if the file has been deleted. An alternative might be to use `ProjectEntryId`,
107/// but then when deleted there is no path info or ability to open.
108#[derive(Debug, Clone)]
109pub struct FileContext {
110    pub buffer: Entity<Buffer>,
111    pub context_id: ContextId,
112}
113
114impl FileContext {
115    pub fn eq_for_key(&self, other: &Self) -> bool {
116        self.buffer == other.buffer
117    }
118
119    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
120        self.buffer.hash(state)
121    }
122
123    pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
124        let file = self.buffer.read(cx).file()?;
125        Some(ProjectPath {
126            worktree_id: file.worktree_id(cx),
127            path: file.path().clone(),
128        })
129    }
130
131    fn load(&self, cx: &App) -> Option<Task<(String, Entity<Buffer>)>> {
132        let buffer_ref = self.buffer.read(cx);
133        let Some(file) = buffer_ref.file() else {
134            log::error!("file context missing path");
135            return None;
136        };
137        let full_path = file.full_path(cx);
138        let rope = buffer_ref.as_rope().clone();
139        let buffer = self.buffer.clone();
140        Some(
141            cx.background_spawn(
142                async move { (to_fenced_codeblock(&full_path, rope, None), buffer) },
143            ),
144        )
145    }
146}
147
148/// Directory contents provides the entire contents of text files in a directory.
149///
150/// This has a `ProjectEntryId` so that it follows renames.
151#[derive(Debug, Clone)]
152pub struct DirectoryContext {
153    pub entry_id: ProjectEntryId,
154    pub context_id: ContextId,
155}
156
157impl DirectoryContext {
158    pub fn eq_for_key(&self, other: &Self) -> bool {
159        self.entry_id == other.entry_id
160    }
161
162    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
163        self.entry_id.hash(state)
164    }
165
166    fn load(
167        &self,
168        project: Entity<Project>,
169        cx: &mut App,
170    ) -> Option<Task<Vec<(String, Entity<Buffer>)>>> {
171        let worktree = project.read(cx).worktree_for_entry(self.entry_id, cx)?;
172        let worktree_ref = worktree.read(cx);
173        let entry = worktree_ref.entry_for_id(self.entry_id)?;
174        if entry.is_file() {
175            log::error!("DirectoryContext unexpectedly refers to a file.");
176            return None;
177        }
178
179        let file_paths = collect_files_in_path(worktree_ref, entry.path.as_ref());
180        let texts_future = future::join_all(file_paths.into_iter().map(|path| {
181            load_file_path_text_as_fenced_codeblock(project.clone(), worktree.clone(), path, cx)
182        }));
183
184        Some(cx.background_spawn(async move {
185            texts_future.await.into_iter().flatten().collect::<Vec<_>>()
186        }))
187    }
188}
189
190#[derive(Debug, Clone)]
191pub struct SymbolContext {
192    pub buffer: Entity<Buffer>,
193    pub symbol: SharedString,
194    pub range: Range<Anchor>,
195    /// The range that fully contain the symbol. e.g. for function symbol, this will include not
196    /// only the signature, but also the body. Not used by `PartialEq` or `Hash` for `AgentContextKey`.
197    pub enclosing_range: Range<Anchor>,
198    pub context_id: ContextId,
199}
200
201impl SymbolContext {
202    pub fn eq_for_key(&self, other: &Self) -> bool {
203        self.buffer == other.buffer && self.symbol == other.symbol && self.range == other.range
204    }
205
206    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
207        self.buffer.hash(state);
208        self.symbol.hash(state);
209        self.range.hash(state);
210    }
211
212    fn load(&self, cx: &App) -> Option<Task<(String, Entity<Buffer>)>> {
213        let buffer_ref = self.buffer.read(cx);
214        let Some(file) = buffer_ref.file() else {
215            log::error!("symbol context's file has no path");
216            return None;
217        };
218        let full_path = file.full_path(cx);
219        let rope = buffer_ref
220            .text_for_range(self.enclosing_range.clone())
221            .collect::<Rope>();
222        let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot());
223        let buffer = self.buffer.clone();
224        Some(cx.background_spawn(async move {
225            (
226                to_fenced_codeblock(&full_path, rope, Some(line_range)),
227                buffer,
228            )
229        }))
230    }
231}
232
233#[derive(Debug, Clone)]
234pub struct SelectionContext {
235    pub buffer: Entity<Buffer>,
236    pub range: Range<Anchor>,
237    pub context_id: ContextId,
238}
239
240impl SelectionContext {
241    pub fn eq_for_key(&self, other: &Self) -> bool {
242        self.buffer == other.buffer && self.range == other.range
243    }
244
245    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
246        self.buffer.hash(state);
247        self.range.hash(state);
248    }
249
250    fn load(&self, cx: &App) -> Option<Task<(String, Entity<Buffer>)>> {
251        let buffer_ref = self.buffer.read(cx);
252        let Some(file) = buffer_ref.file() else {
253            log::error!("selection context's file has no path");
254            return None;
255        };
256        let full_path = file.full_path(cx);
257        let rope = buffer_ref
258            .text_for_range(self.range.clone())
259            .collect::<Rope>();
260        let line_range = self.range.to_point(&buffer_ref.snapshot());
261        let buffer = self.buffer.clone();
262        Some(cx.background_spawn(async move {
263            (
264                to_fenced_codeblock(&full_path, rope, Some(line_range)),
265                buffer,
266            )
267        }))
268    }
269}
270
271#[derive(Debug, Clone)]
272pub struct FetchedUrlContext {
273    pub url: SharedString,
274    /// Text contents of the fetched url. Unlike other context types, the contents of this gets
275    /// populated when added rather than when sending the message. Not used by `PartialEq` or `Hash`
276    /// for `AgentContextKey`.
277    pub text: SharedString,
278    pub context_id: ContextId,
279}
280
281impl FetchedUrlContext {
282    pub fn eq_for_key(&self, other: &Self) -> bool {
283        self.url == other.url
284    }
285
286    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
287        self.url.hash(state);
288    }
289
290    pub fn lookup_key(url: SharedString) -> AgentContextKey {
291        AgentContextKey(AgentContext::FetchedUrl(FetchedUrlContext {
292            url,
293            text: "".into(),
294            context_id: ContextId::for_lookup(),
295        }))
296    }
297}
298
299#[derive(Debug, Clone)]
300pub struct ThreadContext {
301    pub thread: Entity<Thread>,
302    pub context_id: ContextId,
303}
304
305impl ThreadContext {
306    pub fn eq_for_key(&self, other: &Self) -> bool {
307        self.thread == other.thread
308    }
309
310    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
311        self.thread.hash(state)
312    }
313
314    pub fn name(&self, cx: &App) -> SharedString {
315        self.thread
316            .read(cx)
317            .summary()
318            .unwrap_or_else(|| "New thread".into())
319    }
320
321    pub fn load(&self, cx: &App) -> String {
322        let name = self.name(cx);
323        let contents = self.thread.read(cx).latest_detailed_summary_or_text();
324        let mut text = String::new();
325        text.push_str(&name);
326        text.push('\n');
327        text.push_str(&contents.trim());
328        text.push('\n');
329        text
330    }
331}
332
333#[derive(Debug, Clone)]
334pub struct RulesContext {
335    pub prompt_id: UserPromptId,
336    pub context_id: ContextId,
337}
338
339impl RulesContext {
340    pub fn eq_for_key(&self, other: &Self) -> bool {
341        self.prompt_id == other.prompt_id
342    }
343
344    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
345        self.prompt_id.hash(state)
346    }
347
348    pub fn lookup_key(prompt_id: UserPromptId) -> AgentContextKey {
349        AgentContextKey(AgentContext::Rules(RulesContext {
350            prompt_id,
351            context_id: ContextId::for_lookup(),
352        }))
353    }
354
355    pub fn load(
356        &self,
357        prompt_store: &Option<Entity<PromptStore>>,
358        cx: &App,
359    ) -> Task<Option<String>> {
360        let Some(prompt_store) = prompt_store.as_ref() else {
361            return Task::ready(None);
362        };
363        let prompt_store = prompt_store.read(cx);
364        let prompt_id = self.prompt_id.into();
365        let Some(metadata) = prompt_store.metadata(prompt_id) else {
366            return Task::ready(None);
367        };
368        let contents_task = prompt_store.load(prompt_id, cx);
369        cx.background_spawn(async move {
370            let contents = contents_task.await.ok()?;
371            let mut text = String::new();
372            if let Some(title) = metadata.title {
373                text.push_str("Rules title: ");
374                text.push_str(&title);
375                text.push('\n');
376            }
377            text.push_str("``````\n");
378            text.push_str(contents.trim());
379            text.push_str("\n``````\n");
380            Some(text)
381        })
382    }
383}
384
385#[derive(Debug, Clone)]
386pub struct ImageContext {
387    pub original_image: Arc<gpui::Image>,
388    // TODO: handle this elsewhere and remove `ignore-interior-mutability` opt-out in clippy.toml
389    // needed due to a false positive of `clippy::mutable_key_type`.
390    pub image_task: Shared<Task<Option<LanguageModelImage>>>,
391    pub context_id: ContextId,
392}
393
394pub enum ImageStatus {
395    Loading,
396    Error,
397    Ready,
398}
399
400impl ImageContext {
401    pub fn eq_for_key(&self, other: &Self) -> bool {
402        self.original_image.id == other.original_image.id
403    }
404
405    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
406        self.original_image.id.hash(state);
407    }
408
409    pub fn image(&self) -> Option<LanguageModelImage> {
410        self.image_task.clone().now_or_never().flatten()
411    }
412
413    pub fn status(&self) -> ImageStatus {
414        match self.image_task.clone().now_or_never() {
415            None => ImageStatus::Loading,
416            Some(None) => ImageStatus::Error,
417            Some(Some(_)) => ImageStatus::Ready,
418        }
419    }
420}
421
422#[derive(Debug, Clone, Default)]
423pub struct ContextLoadResult {
424    pub loaded_context: LoadedContext,
425    pub referenced_buffers: HashSet<Entity<Buffer>>,
426}
427
428#[derive(Debug, Clone, Default)]
429pub struct LoadedContext {
430    pub contexts: Vec<AgentContext>,
431    pub text: String,
432    pub images: Vec<LanguageModelImage>,
433}
434
435impl LoadedContext {
436    pub fn is_empty(&self) -> bool {
437        self.text.is_empty() && self.images.is_empty()
438    }
439
440    pub fn add_to_request_message(&self, request_message: &mut LanguageModelRequestMessage) {
441        if !self.text.is_empty() {
442            request_message
443                .content
444                .push(MessageContent::Text(self.text.to_string()));
445        }
446
447        if !self.images.is_empty() {
448            // Some providers only support image parts after an initial text part
449            if request_message.content.is_empty() {
450                request_message
451                    .content
452                    .push(MessageContent::Text("Images attached by user:".to_string()));
453            }
454
455            for image in &self.images {
456                request_message
457                    .content
458                    .push(MessageContent::Image(image.clone()))
459            }
460        }
461    }
462}
463
464/// Loads and formats a collection of contexts.
465pub fn load_context(
466    contexts: Vec<AgentContext>,
467    project: &Entity<Project>,
468    prompt_store: &Option<Entity<PromptStore>>,
469    cx: &mut App,
470) -> Task<ContextLoadResult> {
471    let mut file_tasks = Vec::new();
472    let mut directory_tasks = Vec::new();
473    let mut symbol_tasks = Vec::new();
474    let mut selection_tasks = Vec::new();
475    let mut fetch_context = Vec::new();
476    let mut thread_context = Vec::new();
477    let mut rules_tasks = Vec::new();
478    let mut image_tasks = Vec::new();
479
480    for context in contexts.iter().cloned() {
481        match context {
482            AgentContext::File(context) => file_tasks.extend(context.load(cx)),
483            AgentContext::Directory(context) => {
484                directory_tasks.extend(context.load(project.clone(), cx))
485            }
486            AgentContext::Symbol(context) => symbol_tasks.extend(context.load(cx)),
487            AgentContext::Selection(context) => selection_tasks.extend(context.load(cx)),
488            AgentContext::FetchedUrl(context) => fetch_context.push(context),
489            AgentContext::Thread(context) => thread_context.push(context.load(cx)),
490            AgentContext::Rules(context) => rules_tasks.push(context.load(prompt_store, cx)),
491            AgentContext::Image(context) => image_tasks.push(context.image_task.clone()),
492        }
493    }
494
495    cx.background_spawn(async move {
496        let (
497            file_context,
498            directory_context,
499            symbol_context,
500            selection_context,
501            rules_context,
502            images,
503        ) = futures::join!(
504            future::join_all(file_tasks),
505            future::join_all(directory_tasks),
506            future::join_all(symbol_tasks),
507            future::join_all(selection_tasks),
508            future::join_all(rules_tasks),
509            future::join_all(image_tasks)
510        );
511
512        let directory_context = directory_context.into_iter().flatten().collect::<Vec<_>>();
513        let rules_context = rules_context.into_iter().flatten().collect::<Vec<_>>();
514        let images = images.into_iter().flatten().collect::<Vec<_>>();
515
516        let mut referenced_buffers = HashSet::default();
517        let mut text = String::new();
518
519        if file_context.is_empty()
520            && directory_context.is_empty()
521            && symbol_context.is_empty()
522            && selection_context.is_empty()
523            && fetch_context.is_empty()
524            && thread_context.is_empty()
525            && rules_context.is_empty()
526        {
527            return ContextLoadResult {
528                loaded_context: LoadedContext {
529                    contexts,
530                    text,
531                    images,
532                },
533                referenced_buffers,
534            };
535        }
536
537        text.push_str(
538            "\n<context>\n\
539            The following items were attached by the user. \
540            You don't need to use other tools to read them.\n\n",
541        );
542
543        if !file_context.is_empty() {
544            text.push_str("<files>");
545            for (file_text, buffer) in file_context {
546                text.push('\n');
547                text.push_str(&file_text);
548                referenced_buffers.insert(buffer);
549            }
550            text.push_str("</files>\n");
551        }
552
553        if !directory_context.is_empty() {
554            text.push_str("<directories>");
555            for (file_text, buffer) in directory_context {
556                text.push('\n');
557                text.push_str(&file_text);
558                referenced_buffers.insert(buffer);
559            }
560            text.push_str("</directories>\n");
561        }
562
563        if !symbol_context.is_empty() {
564            text.push_str("<symbols>");
565            for (symbol_text, buffer) in symbol_context {
566                text.push('\n');
567                text.push_str(&symbol_text);
568                referenced_buffers.insert(buffer);
569            }
570            text.push_str("</symbols>\n");
571        }
572
573        if !selection_context.is_empty() {
574            text.push_str("<selections>");
575            for (selection_text, buffer) in selection_context {
576                text.push('\n');
577                text.push_str(&selection_text);
578                referenced_buffers.insert(buffer);
579            }
580            text.push_str("</selections>\n");
581        }
582
583        if !fetch_context.is_empty() {
584            text.push_str("<fetched_urls>");
585            for context in fetch_context {
586                text.push('\n');
587                text.push_str(&context.url);
588                text.push('\n');
589                text.push_str(&context.text);
590            }
591            text.push_str("</fetched_urls>\n");
592        }
593
594        if !thread_context.is_empty() {
595            text.push_str("<conversation_threads>");
596            for thread_text in thread_context {
597                text.push('\n');
598                text.push_str(&thread_text);
599            }
600            text.push_str("</conversation_threads>\n");
601        }
602
603        if !rules_context.is_empty() {
604            text.push_str(
605                "<user_rules>\n\
606                The user has specified the following rules that should be applied:\n",
607            );
608            for rules_text in rules_context {
609                text.push('\n');
610                text.push_str(&rules_text);
611            }
612            text.push_str("</user_rules>\n");
613        }
614
615        text.push_str("</context>\n");
616
617        ContextLoadResult {
618            loaded_context: LoadedContext {
619                contexts,
620                text,
621                images,
622            },
623            referenced_buffers,
624        }
625    })
626}
627
628fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
629    let mut files = Vec::new();
630
631    for entry in worktree.child_entries(path) {
632        if entry.is_dir() {
633            files.extend(collect_files_in_path(worktree, &entry.path));
634        } else if entry.is_file() {
635            files.push(entry.path.clone());
636        }
637    }
638
639    files
640}
641
642fn load_file_path_text_as_fenced_codeblock(
643    project: Entity<Project>,
644    worktree: Entity<Worktree>,
645    path: Arc<Path>,
646    cx: &mut App,
647) -> Task<Option<(String, Entity<Buffer>)>> {
648    let worktree_ref = worktree.read(cx);
649    let worktree_id = worktree_ref.id();
650    let full_path = worktree_ref.full_path(&path);
651
652    let open_task = project.update(cx, |project, cx| {
653        project.buffer_store().update(cx, |buffer_store, cx| {
654            let project_path = ProjectPath { worktree_id, path };
655            buffer_store.open_buffer(project_path, cx)
656        })
657    });
658
659    let rope_task = cx.spawn(async move |cx| {
660        let buffer = open_task.await.log_err()?;
661        let rope = buffer
662            .read_with(cx, |buffer, _cx| buffer.as_rope().clone())
663            .log_err()?;
664        Some((rope, buffer))
665    });
666
667    cx.background_spawn(async move {
668        let (rope, buffer) = rope_task.await?;
669        Some((to_fenced_codeblock(&full_path, rope, None), buffer))
670    })
671}
672
673fn to_fenced_codeblock(
674    full_path: &Path,
675    content: Rope,
676    line_range: Option<Range<Point>>,
677) -> String {
678    let line_range_text = line_range.map(|range| {
679        if range.start.row == range.end.row {
680            format!(":{}", range.start.row + 1)
681        } else {
682            format!(":{}-{}", range.start.row + 1, range.end.row + 1)
683        }
684    });
685
686    let path_extension = full_path.extension().and_then(|ext| ext.to_str());
687    let path_string = full_path.to_string_lossy();
688    let capacity = 3
689        + path_extension.map_or(0, |extension| extension.len() + 1)
690        + path_string.len()
691        + line_range_text.as_ref().map_or(0, |text| text.len())
692        + 1
693        + content.len()
694        + 5;
695    let mut buffer = String::with_capacity(capacity);
696
697    buffer.push_str("```");
698
699    if let Some(extension) = path_extension {
700        buffer.push_str(extension);
701        buffer.push(' ');
702    }
703    buffer.push_str(&path_string);
704
705    if let Some(line_range_text) = line_range_text {
706        buffer.push_str(&line_range_text);
707    }
708
709    buffer.push('\n');
710    for chunk in content.chunks() {
711        buffer.push_str(chunk);
712    }
713
714    if !buffer.ends_with('\n') {
715        buffer.push('\n');
716    }
717
718    buffer.push_str("```\n");
719
720    debug_assert!(
721        buffer.len() == capacity - 1 || buffer.len() == capacity,
722        "to_fenced_codeblock calculated capacity of {}, but length was {}",
723        capacity,
724        buffer.len(),
725    );
726
727    buffer
728}
729
730/// Wraps `AgentContext` to opt-in to `PartialEq` and `Hash` impls which use a subset of fields
731/// needed for stable context identity.
732#[derive(Debug, Clone, RefCast)]
733#[repr(transparent)]
734pub struct AgentContextKey(pub AgentContext);
735
736impl AsRef<AgentContext> for AgentContextKey {
737    fn as_ref(&self) -> &AgentContext {
738        &self.0
739    }
740}
741
742impl Eq for AgentContextKey {}
743
744impl PartialEq for AgentContextKey {
745    fn eq(&self, other: &Self) -> bool {
746        match &self.0 {
747            AgentContext::File(context) => {
748                if let AgentContext::File(other_context) = &other.0 {
749                    return context.eq_for_key(other_context);
750                }
751            }
752            AgentContext::Directory(context) => {
753                if let AgentContext::Directory(other_context) = &other.0 {
754                    return context.eq_for_key(other_context);
755                }
756            }
757            AgentContext::Symbol(context) => {
758                if let AgentContext::Symbol(other_context) = &other.0 {
759                    return context.eq_for_key(other_context);
760                }
761            }
762            AgentContext::Selection(context) => {
763                if let AgentContext::Selection(other_context) = &other.0 {
764                    return context.eq_for_key(other_context);
765                }
766            }
767            AgentContext::FetchedUrl(context) => {
768                if let AgentContext::FetchedUrl(other_context) = &other.0 {
769                    return context.eq_for_key(other_context);
770                }
771            }
772            AgentContext::Thread(context) => {
773                if let AgentContext::Thread(other_context) = &other.0 {
774                    return context.eq_for_key(other_context);
775                }
776            }
777            AgentContext::Rules(context) => {
778                if let AgentContext::Rules(other_context) = &other.0 {
779                    return context.eq_for_key(other_context);
780                }
781            }
782            AgentContext::Image(context) => {
783                if let AgentContext::Image(other_context) = &other.0 {
784                    return context.eq_for_key(other_context);
785                }
786            }
787        }
788        false
789    }
790}
791
792impl Hash for AgentContextKey {
793    fn hash<H: Hasher>(&self, state: &mut H) {
794        match &self.0 {
795            AgentContext::File(context) => context.hash_for_key(state),
796            AgentContext::Directory(context) => context.hash_for_key(state),
797            AgentContext::Symbol(context) => context.hash_for_key(state),
798            AgentContext::Selection(context) => context.hash_for_key(state),
799            AgentContext::FetchedUrl(context) => context.hash_for_key(state),
800            AgentContext::Thread(context) => context.hash_for_key(state),
801            AgentContext::Rules(context) => context.hash_for_key(state),
802            AgentContext::Image(context) => context.hash_for_key(state),
803        }
804    }
805}