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}