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}