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