context.rs

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