context.rs

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