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 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            They are up-to-date and don't need to be re-read.\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}