message_editor.rs

   1use crate::{
   2    acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion},
   3    context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
   4};
   5use acp_thread::{MentionUri, selection_name};
   6use agent_client_protocol as acp;
   7use agent_servers::{AgentServer, AgentServerDelegate};
   8use agent2::HistoryStore;
   9use anyhow::{Result, anyhow};
  10use assistant_slash_commands::codeblock_fence_for_path;
  11use assistant_tool::outline;
  12use collections::{HashMap, HashSet};
  13use editor::{
  14    Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
  15    EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
  16    MultiBuffer, ToOffset,
  17    actions::Paste,
  18    display_map::{Crease, CreaseId, FoldId, Inlay},
  19};
  20use futures::{
  21    FutureExt as _,
  22    future::{Shared, join_all},
  23};
  24use gpui::{
  25    Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
  26    EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
  27    Subscription, Task, TextStyle, WeakEntity, pulsating_between,
  28};
  29use language::{Buffer, Language, language_settings::InlayHintKind};
  30use language_model::LanguageModelImage;
  31use postage::stream::Stream as _;
  32use project::{
  33    CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
  34};
  35use prompt_store::{PromptId, PromptStore};
  36use rope::Point;
  37use settings::Settings;
  38use std::{
  39    cell::RefCell,
  40    ffi::OsStr,
  41    fmt::Write,
  42    ops::{Range, RangeInclusive},
  43    path::{Path, PathBuf},
  44    rc::Rc,
  45    sync::Arc,
  46    time::Duration,
  47};
  48use text::OffsetRangeExt;
  49use theme::ThemeSettings;
  50use ui::{ButtonLike, TintColor, Toggleable, prelude::*};
  51use util::{ResultExt, debug_panic, rel_path::RelPath};
  52use workspace::{Workspace, notifications::NotifyResultExt as _};
  53use zed_actions::agent::Chat;
  54
  55pub struct MessageEditor {
  56    mention_set: MentionSet,
  57    editor: Entity<Editor>,
  58    project: Entity<Project>,
  59    workspace: WeakEntity<Workspace>,
  60    history_store: Entity<HistoryStore>,
  61    prompt_store: Option<Entity<PromptStore>>,
  62    prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
  63    available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
  64    agent_name: SharedString,
  65    _subscriptions: Vec<Subscription>,
  66    _parse_slash_command_task: Task<()>,
  67}
  68
  69#[derive(Clone, Copy, Debug)]
  70pub enum MessageEditorEvent {
  71    Send,
  72    Cancel,
  73    Focus,
  74    LostFocus,
  75}
  76
  77impl EventEmitter<MessageEditorEvent> for MessageEditor {}
  78
  79const COMMAND_HINT_INLAY_ID: u32 = 0;
  80
  81impl MessageEditor {
  82    pub fn new(
  83        workspace: WeakEntity<Workspace>,
  84        project: Entity<Project>,
  85        history_store: Entity<HistoryStore>,
  86        prompt_store: Option<Entity<PromptStore>>,
  87        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
  88        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
  89        agent_name: SharedString,
  90        placeholder: &str,
  91        mode: EditorMode,
  92        window: &mut Window,
  93        cx: &mut Context<Self>,
  94    ) -> Self {
  95        let language = Language::new(
  96            language::LanguageConfig {
  97                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
  98                ..Default::default()
  99            },
 100            None,
 101        );
 102        let completion_provider = Rc::new(ContextPickerCompletionProvider::new(
 103            cx.weak_entity(),
 104            workspace.clone(),
 105            history_store.clone(),
 106            prompt_store.clone(),
 107            prompt_capabilities.clone(),
 108            available_commands.clone(),
 109        ));
 110        let mention_set = MentionSet::default();
 111        let editor = cx.new(|cx| {
 112            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
 113            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 114
 115            let mut editor = Editor::new(mode, buffer, None, window, cx);
 116            editor.set_placeholder_text(placeholder, window, cx);
 117            editor.set_show_indent_guides(false, cx);
 118            editor.set_soft_wrap();
 119            editor.set_use_modal_editing(true);
 120            editor.set_completion_provider(Some(completion_provider.clone()));
 121            editor.set_context_menu_options(ContextMenuOptions {
 122                min_entries_visible: 12,
 123                max_entries_visible: 12,
 124                placement: Some(ContextMenuPlacement::Above),
 125            });
 126            editor.register_addon(MessageEditorAddon::new());
 127            editor
 128        });
 129
 130        cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
 131            cx.emit(MessageEditorEvent::Focus)
 132        })
 133        .detach();
 134        cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
 135            cx.emit(MessageEditorEvent::LostFocus)
 136        })
 137        .detach();
 138
 139        let mut has_hint = false;
 140        let mut subscriptions = Vec::new();
 141
 142        subscriptions.push(cx.subscribe_in(&editor, window, {
 143            move |this, editor, event, window, cx| {
 144                if let EditorEvent::Edited { .. } = event {
 145                    let snapshot = editor.update(cx, |editor, cx| {
 146                        let new_hints = this
 147                            .command_hint(editor.buffer(), cx)
 148                            .into_iter()
 149                            .collect::<Vec<_>>();
 150                        let has_new_hint = !new_hints.is_empty();
 151                        editor.splice_inlays(
 152                            if has_hint {
 153                                &[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
 154                            } else {
 155                                &[]
 156                            },
 157                            new_hints,
 158                            cx,
 159                        );
 160                        has_hint = has_new_hint;
 161
 162                        editor.snapshot(window, cx)
 163                    });
 164                    this.mention_set.remove_invalid(snapshot);
 165
 166                    cx.notify();
 167                }
 168            }
 169        }));
 170
 171        Self {
 172            editor,
 173            project,
 174            mention_set,
 175            workspace,
 176            history_store,
 177            prompt_store,
 178            prompt_capabilities,
 179            available_commands,
 180            agent_name,
 181            _subscriptions: subscriptions,
 182            _parse_slash_command_task: Task::ready(()),
 183        }
 184    }
 185
 186    fn command_hint(&self, buffer: &Entity<MultiBuffer>, cx: &App) -> Option<Inlay> {
 187        let available_commands = self.available_commands.borrow();
 188        if available_commands.is_empty() {
 189            return None;
 190        }
 191
 192        let snapshot = buffer.read(cx).snapshot(cx);
 193        let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
 194        if parsed_command.argument.is_some() {
 195            return None;
 196        }
 197
 198        let command_name = parsed_command.command?;
 199        let available_command = available_commands
 200            .iter()
 201            .find(|command| command.name == command_name)?;
 202
 203        let acp::AvailableCommandInput::Unstructured { mut hint } =
 204            available_command.input.clone()?;
 205
 206        let mut hint_pos = parsed_command.source_range.end + 1;
 207        if hint_pos > snapshot.len() {
 208            hint_pos = snapshot.len();
 209            hint.insert(0, ' ');
 210        }
 211
 212        let hint_pos = snapshot.anchor_after(hint_pos);
 213
 214        Some(Inlay::hint(
 215            COMMAND_HINT_INLAY_ID,
 216            hint_pos,
 217            &InlayHint {
 218                position: hint_pos.text_anchor,
 219                label: InlayHintLabel::String(hint),
 220                kind: Some(InlayHintKind::Parameter),
 221                padding_left: false,
 222                padding_right: false,
 223                tooltip: None,
 224                resolve_state: project::ResolveState::Resolved,
 225            },
 226        ))
 227    }
 228
 229    pub fn insert_thread_summary(
 230        &mut self,
 231        thread: agent2::DbThreadMetadata,
 232        window: &mut Window,
 233        cx: &mut Context<Self>,
 234    ) {
 235        let start = self.editor.update(cx, |editor, cx| {
 236            editor.set_text(format!("{}\n", thread.title), window, cx);
 237            editor
 238                .buffer()
 239                .read(cx)
 240                .snapshot(cx)
 241                .anchor_before(Point::zero())
 242                .text_anchor
 243        });
 244
 245        self.confirm_mention_completion(
 246            thread.title.clone(),
 247            start,
 248            thread.title.len(),
 249            MentionUri::Thread {
 250                id: thread.id.clone(),
 251                name: thread.title.to_string(),
 252            },
 253            window,
 254            cx,
 255        )
 256        .detach();
 257    }
 258
 259    #[cfg(test)]
 260    pub(crate) fn editor(&self) -> &Entity<Editor> {
 261        &self.editor
 262    }
 263
 264    #[cfg(test)]
 265    pub(crate) fn mention_set(&mut self) -> &mut MentionSet {
 266        &mut self.mention_set
 267    }
 268
 269    pub fn is_empty(&self, cx: &App) -> bool {
 270        self.editor.read(cx).is_empty(cx)
 271    }
 272
 273    pub fn mentions(&self) -> HashSet<MentionUri> {
 274        self.mention_set
 275            .mentions
 276            .values()
 277            .map(|(uri, _)| uri.clone())
 278            .collect()
 279    }
 280
 281    pub fn confirm_mention_completion(
 282        &mut self,
 283        crease_text: SharedString,
 284        start: text::Anchor,
 285        content_len: usize,
 286        mention_uri: MentionUri,
 287        window: &mut Window,
 288        cx: &mut Context<Self>,
 289    ) -> Task<()> {
 290        let snapshot = self
 291            .editor
 292            .update(cx, |editor, cx| editor.snapshot(window, cx));
 293        let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else {
 294            return Task::ready(());
 295        };
 296        let Some(start_anchor) = snapshot
 297            .buffer_snapshot
 298            .anchor_in_excerpt(*excerpt_id, start)
 299        else {
 300            return Task::ready(());
 301        };
 302        let end_anchor = snapshot
 303            .buffer_snapshot
 304            .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1);
 305
 306        let crease = if let MentionUri::File { abs_path } = &mention_uri
 307            && let Some(extension) = abs_path.extension()
 308            && let Some(extension) = extension.to_str()
 309            && Img::extensions().contains(&extension)
 310            && !extension.contains("svg")
 311        {
 312            let Some(project_path) = self
 313                .project
 314                .read(cx)
 315                .project_path_for_absolute_path(&abs_path, cx)
 316            else {
 317                log::error!("project path not found");
 318                return Task::ready(());
 319            };
 320            let image = self
 321                .project
 322                .update(cx, |project, cx| project.open_image(project_path, cx));
 323            let image = cx
 324                .spawn(async move |_, cx| {
 325                    let image = image.await.map_err(|e| e.to_string())?;
 326                    let image = image
 327                        .update(cx, |image, _| image.image.clone())
 328                        .map_err(|e| e.to_string())?;
 329                    Ok(image)
 330                })
 331                .shared();
 332            insert_crease_for_mention(
 333                *excerpt_id,
 334                start,
 335                content_len,
 336                mention_uri.name().into(),
 337                IconName::Image.path().into(),
 338                Some(image),
 339                self.editor.clone(),
 340                window,
 341                cx,
 342            )
 343        } else {
 344            insert_crease_for_mention(
 345                *excerpt_id,
 346                start,
 347                content_len,
 348                crease_text,
 349                mention_uri.icon_path(cx),
 350                None,
 351                self.editor.clone(),
 352                window,
 353                cx,
 354            )
 355        };
 356        let Some((crease_id, tx)) = crease else {
 357            return Task::ready(());
 358        };
 359
 360        let task = match mention_uri.clone() {
 361            MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx),
 362            MentionUri::Directory { .. } => Task::ready(Ok(Mention::UriOnly)),
 363            MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
 364            MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
 365            MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx),
 366            MentionUri::Symbol {
 367                abs_path,
 368                line_range,
 369                ..
 370            } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
 371            MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
 372            MentionUri::PastedImage => {
 373                debug_panic!("pasted image URI should not be included in completions");
 374                Task::ready(Err(anyhow!(
 375                    "pasted imaged URI should not be included in completions"
 376                )))
 377            }
 378            MentionUri::Selection { .. } => {
 379                // Handled elsewhere
 380                debug_panic!("unexpected selection URI");
 381                Task::ready(Err(anyhow!("unexpected selection URI")))
 382            }
 383        };
 384        let task = cx
 385            .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
 386            .shared();
 387        self.mention_set
 388            .mentions
 389            .insert(crease_id, (mention_uri, task.clone()));
 390
 391        // Notify the user if we failed to load the mentioned context
 392        cx.spawn_in(window, async move |this, cx| {
 393            let result = task.await.notify_async_err(cx);
 394            drop(tx);
 395            if result.is_none() {
 396                this.update(cx, |this, cx| {
 397                    this.editor.update(cx, |editor, cx| {
 398                        // Remove mention
 399                        editor.edit([(start_anchor..end_anchor, "")], cx);
 400                    });
 401                    this.mention_set.mentions.remove(&crease_id);
 402                })
 403                .ok();
 404            }
 405        })
 406    }
 407
 408    fn confirm_mention_for_file(
 409        &mut self,
 410        abs_path: PathBuf,
 411        cx: &mut Context<Self>,
 412    ) -> Task<Result<Mention>> {
 413        let Some(project_path) = self
 414            .project
 415            .read(cx)
 416            .project_path_for_absolute_path(&abs_path, cx)
 417        else {
 418            return Task::ready(Err(anyhow!("project path not found")));
 419        };
 420        let extension = abs_path
 421            .extension()
 422            .and_then(OsStr::to_str)
 423            .unwrap_or_default();
 424
 425        if Img::extensions().contains(&extension) && !extension.contains("svg") {
 426            if !self.prompt_capabilities.borrow().image {
 427                return Task::ready(Err(anyhow!("This model does not support images yet")));
 428            }
 429            let task = self
 430                .project
 431                .update(cx, |project, cx| project.open_image(project_path, cx));
 432            return cx.spawn(async move |_, cx| {
 433                let image = task.await?;
 434                let image = image.update(cx, |image, _| image.image.clone())?;
 435                let format = image.format;
 436                let image = cx
 437                    .update(|cx| LanguageModelImage::from_image(image, cx))?
 438                    .await;
 439                if let Some(image) = image {
 440                    Ok(Mention::Image(MentionImage {
 441                        data: image.source,
 442                        format,
 443                    }))
 444                } else {
 445                    Err(anyhow!("Failed to convert image"))
 446                }
 447            });
 448        }
 449
 450        let buffer = self
 451            .project
 452            .update(cx, |project, cx| project.open_buffer(project_path, cx));
 453        cx.spawn(async move |_, cx| {
 454            let buffer = buffer.await?;
 455            let buffer_content = outline::get_buffer_content_or_outline(
 456                buffer.clone(),
 457                Some(&abs_path.to_string_lossy()),
 458                &cx,
 459            )
 460            .await?;
 461
 462            Ok(Mention::Text {
 463                content: buffer_content.text,
 464                tracked_buffers: vec![buffer],
 465            })
 466        })
 467    }
 468
 469    fn confirm_mention_for_fetch(
 470        &mut self,
 471        url: url::Url,
 472        cx: &mut Context<Self>,
 473    ) -> Task<Result<Mention>> {
 474        let http_client = match self
 475            .workspace
 476            .update(cx, |workspace, _| workspace.client().http_client())
 477        {
 478            Ok(http_client) => http_client,
 479            Err(e) => return Task::ready(Err(e)),
 480        };
 481        cx.background_executor().spawn(async move {
 482            let content = fetch_url_content(http_client, url.to_string()).await?;
 483            Ok(Mention::Text {
 484                content,
 485                tracked_buffers: Vec::new(),
 486            })
 487        })
 488    }
 489
 490    fn confirm_mention_for_symbol(
 491        &mut self,
 492        abs_path: PathBuf,
 493        line_range: RangeInclusive<u32>,
 494        cx: &mut Context<Self>,
 495    ) -> Task<Result<Mention>> {
 496        let Some(project_path) = self
 497            .project
 498            .read(cx)
 499            .project_path_for_absolute_path(&abs_path, cx)
 500        else {
 501            return Task::ready(Err(anyhow!("project path not found")));
 502        };
 503        let buffer = self
 504            .project
 505            .update(cx, |project, cx| project.open_buffer(project_path, cx));
 506        cx.spawn(async move |_, cx| {
 507            let buffer = buffer.await?;
 508            let mention = buffer.update(cx, |buffer, cx| {
 509                let start = Point::new(*line_range.start(), 0).min(buffer.max_point());
 510                let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());
 511                let content = buffer.text_for_range(start..end).collect();
 512                Mention::Text {
 513                    content,
 514                    tracked_buffers: vec![cx.entity()],
 515                }
 516            })?;
 517            anyhow::Ok(mention)
 518        })
 519    }
 520
 521    fn confirm_mention_for_rule(
 522        &mut self,
 523        id: PromptId,
 524        cx: &mut Context<Self>,
 525    ) -> Task<Result<Mention>> {
 526        let Some(prompt_store) = self.prompt_store.clone() else {
 527            return Task::ready(Err(anyhow!("missing prompt store")));
 528        };
 529        let prompt = prompt_store.read(cx).load(id, cx);
 530        cx.spawn(async move |_, _| {
 531            let prompt = prompt.await?;
 532            Ok(Mention::Text {
 533                content: prompt,
 534                tracked_buffers: Vec::new(),
 535            })
 536        })
 537    }
 538
 539    pub fn confirm_mention_for_selection(
 540        &mut self,
 541        source_range: Range<text::Anchor>,
 542        selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
 543        window: &mut Window,
 544        cx: &mut Context<Self>,
 545    ) {
 546        let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
 547        let Some((&excerpt_id, _, _)) = snapshot.as_singleton() else {
 548            return;
 549        };
 550        let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, source_range.start) else {
 551            return;
 552        };
 553
 554        let offset = start.to_offset(&snapshot);
 555
 556        for (buffer, selection_range, range_to_fold) in selections {
 557            let range = snapshot.anchor_after(offset + range_to_fold.start)
 558                ..snapshot.anchor_after(offset + range_to_fold.end);
 559
 560            let abs_path = buffer
 561                .read(cx)
 562                .project_path(cx)
 563                .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx));
 564            let snapshot = buffer.read(cx).snapshot();
 565
 566            let text = snapshot
 567                .text_for_range(selection_range.clone())
 568                .collect::<String>();
 569            let point_range = selection_range.to_point(&snapshot);
 570            let line_range = point_range.start.row..=point_range.end.row;
 571
 572            let uri = MentionUri::Selection {
 573                abs_path: abs_path.clone(),
 574                line_range: line_range.clone(),
 575            };
 576            let crease = crate::context_picker::crease_for_mention(
 577                selection_name(abs_path.as_deref(), &line_range).into(),
 578                uri.icon_path(cx),
 579                range,
 580                self.editor.downgrade(),
 581            );
 582
 583            let crease_id = self.editor.update(cx, |editor, cx| {
 584                let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
 585                editor.fold_creases(vec![crease], false, window, cx);
 586                crease_ids.first().copied().unwrap()
 587            });
 588
 589            self.mention_set.mentions.insert(
 590                crease_id,
 591                (
 592                    uri,
 593                    Task::ready(Ok(Mention::Text {
 594                        content: text,
 595                        tracked_buffers: vec![buffer],
 596                    }))
 597                    .shared(),
 598                ),
 599            );
 600        }
 601    }
 602
 603    fn confirm_mention_for_thread(
 604        &mut self,
 605        id: acp::SessionId,
 606        cx: &mut Context<Self>,
 607    ) -> Task<Result<Mention>> {
 608        let server = Rc::new(agent2::NativeAgentServer::new(
 609            self.project.read(cx).fs().clone(),
 610            self.history_store.clone(),
 611        ));
 612        let delegate = AgentServerDelegate::new(
 613            self.project.read(cx).agent_server_store().clone(),
 614            self.project.clone(),
 615            None,
 616            None,
 617        );
 618        let connection = server.connect(None, delegate, cx);
 619        cx.spawn(async move |_, cx| {
 620            let (agent, _) = connection.await?;
 621            let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
 622            let summary = agent
 623                .0
 624                .update(cx, |agent, cx| agent.thread_summary(id, cx))?
 625                .await?;
 626            anyhow::Ok(Mention::Text {
 627                content: summary.to_string(),
 628                tracked_buffers: Vec::new(),
 629            })
 630        })
 631    }
 632
 633    fn confirm_mention_for_text_thread(
 634        &mut self,
 635        path: PathBuf,
 636        cx: &mut Context<Self>,
 637    ) -> Task<Result<Mention>> {
 638        let context = self.history_store.update(cx, |text_thread_store, cx| {
 639            text_thread_store.load_text_thread(path.as_path().into(), cx)
 640        });
 641        cx.spawn(async move |_, cx| {
 642            let context = context.await?;
 643            let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
 644            Ok(Mention::Text {
 645                content: xml,
 646                tracked_buffers: Vec::new(),
 647            })
 648        })
 649    }
 650
 651    fn validate_slash_commands(
 652        text: &str,
 653        available_commands: &[acp::AvailableCommand],
 654        agent_name: &str,
 655    ) -> Result<()> {
 656        if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
 657            if let Some(command_name) = parsed_command.command {
 658                // Check if this command is in the list of available commands from the server
 659                let is_supported = available_commands
 660                    .iter()
 661                    .any(|cmd| cmd.name == command_name);
 662
 663                if !is_supported {
 664                    return Err(anyhow!(
 665                        "The /{} command is not supported by {}.\n\nAvailable commands: {}",
 666                        command_name,
 667                        agent_name,
 668                        if available_commands.is_empty() {
 669                            "none".to_string()
 670                        } else {
 671                            available_commands
 672                                .iter()
 673                                .map(|cmd| format!("/{}", cmd.name))
 674                                .collect::<Vec<_>>()
 675                                .join(", ")
 676                        }
 677                    ));
 678                }
 679            }
 680        }
 681        Ok(())
 682    }
 683
 684    pub fn contents(
 685        &self,
 686        full_mention_content: bool,
 687        cx: &mut Context<Self>,
 688    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 689        // Check for unsupported slash commands before spawning async task
 690        let text = self.editor.read(cx).text(cx);
 691        let available_commands = self.available_commands.borrow().clone();
 692        if let Err(err) =
 693            Self::validate_slash_commands(&text, &available_commands, &self.agent_name)
 694        {
 695            return Task::ready(Err(err));
 696        }
 697
 698        let contents = self.mention_set.contents(
 699            &self.prompt_capabilities.borrow(),
 700            full_mention_content,
 701            self.project.clone(),
 702            cx,
 703        );
 704        let editor = self.editor.clone();
 705
 706        cx.spawn(async move |_, cx| {
 707            let contents = contents.await?;
 708            let mut all_tracked_buffers = Vec::new();
 709
 710            let result = editor.update(cx, |editor, cx| {
 711                let mut ix = 0;
 712                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
 713                let text = editor.text(cx);
 714                editor.display_map.update(cx, |map, cx| {
 715                    let snapshot = map.snapshot(cx);
 716                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 717                        let Some((uri, mention)) = contents.get(&crease_id) else {
 718                            continue;
 719                        };
 720
 721                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
 722                        if crease_range.start > ix {
 723                            //todo(): Custom slash command ContentBlock?
 724                            // let chunk = if prevent_slash_commands
 725                            //     && ix == 0
 726                            //     && parse_slash_command(&text[ix..]).is_some()
 727                            // {
 728                            //     format!(" {}", &text[ix..crease_range.start]).into()
 729                            // } else {
 730                            //     text[ix..crease_range.start].into()
 731                            // };
 732                            let chunk = text[ix..crease_range.start].into();
 733                            chunks.push(chunk);
 734                        }
 735                        let chunk = match mention {
 736                            Mention::Text {
 737                                content,
 738                                tracked_buffers,
 739                            } => {
 740                                all_tracked_buffers.extend(tracked_buffers.iter().cloned());
 741                                acp::ContentBlock::Resource(acp::EmbeddedResource {
 742                                    annotations: None,
 743                                    resource: acp::EmbeddedResourceResource::TextResourceContents(
 744                                        acp::TextResourceContents {
 745                                            mime_type: None,
 746                                            text: content.clone(),
 747                                            uri: uri.to_uri().to_string(),
 748                                            meta: None,
 749                                        },
 750                                    ),
 751                                    meta: None,
 752                                })
 753                            }
 754                            Mention::Image(mention_image) => {
 755                                let uri = match uri {
 756                                    MentionUri::File { .. } => Some(uri.to_uri().to_string()),
 757                                    MentionUri::PastedImage => None,
 758                                    other => {
 759                                        debug_panic!(
 760                                            "unexpected mention uri for image: {:?}",
 761                                            other
 762                                        );
 763                                        None
 764                                    }
 765                                };
 766                                acp::ContentBlock::Image(acp::ImageContent {
 767                                    annotations: None,
 768                                    data: mention_image.data.to_string(),
 769                                    mime_type: mention_image.format.mime_type().into(),
 770                                    uri,
 771                                    meta: None,
 772                                })
 773                            }
 774                            Mention::UriOnly => {
 775                                acp::ContentBlock::ResourceLink(acp::ResourceLink {
 776                                    name: uri.name(),
 777                                    uri: uri.to_uri().to_string(),
 778                                    annotations: None,
 779                                    description: None,
 780                                    mime_type: None,
 781                                    size: None,
 782                                    title: None,
 783                                    meta: None,
 784                                })
 785                            }
 786                        };
 787                        chunks.push(chunk);
 788                        ix = crease_range.end;
 789                    }
 790
 791                    if ix < text.len() {
 792                        //todo(): Custom slash command ContentBlock?
 793                        // let last_chunk = if prevent_slash_commands
 794                        //     && ix == 0
 795                        //     && parse_slash_command(&text[ix..]).is_some()
 796                        // {
 797                        //     format!(" {}", text[ix..].trim_end())
 798                        // } else {
 799                        //     text[ix..].trim_end().to_owned()
 800                        // };
 801                        let last_chunk = text[ix..].trim_end().to_owned();
 802                        if !last_chunk.is_empty() {
 803                            chunks.push(last_chunk.into());
 804                        }
 805                    }
 806                });
 807                Ok((chunks, all_tracked_buffers))
 808            })?;
 809            result
 810        })
 811    }
 812
 813    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 814        self.editor.update(cx, |editor, cx| {
 815            editor.clear(window, cx);
 816            editor.remove_creases(
 817                self.mention_set
 818                    .mentions
 819                    .drain()
 820                    .map(|(crease_id, _)| crease_id),
 821                cx,
 822            )
 823        });
 824    }
 825
 826    fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
 827        if self.is_empty(cx) {
 828            return;
 829        }
 830        cx.emit(MessageEditorEvent::Send)
 831    }
 832
 833    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 834        cx.emit(MessageEditorEvent::Cancel)
 835    }
 836
 837    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 838        if !self.prompt_capabilities.borrow().image {
 839            return;
 840        }
 841
 842        let images = cx
 843            .read_from_clipboard()
 844            .map(|item| {
 845                item.into_entries()
 846                    .filter_map(|entry| {
 847                        if let ClipboardEntry::Image(image) = entry {
 848                            Some(image)
 849                        } else {
 850                            None
 851                        }
 852                    })
 853                    .collect::<Vec<_>>()
 854            })
 855            .unwrap_or_default();
 856
 857        if images.is_empty() {
 858            return;
 859        }
 860        cx.stop_propagation();
 861
 862        let replacement_text = MentionUri::PastedImage.as_link().to_string();
 863        for image in images {
 864            let (excerpt_id, text_anchor, multibuffer_anchor) =
 865                self.editor.update(cx, |message_editor, cx| {
 866                    let snapshot = message_editor.snapshot(window, cx);
 867                    let (excerpt_id, _, buffer_snapshot) =
 868                        snapshot.buffer_snapshot.as_singleton().unwrap();
 869
 870                    let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
 871                    let multibuffer_anchor = snapshot
 872                        .buffer_snapshot
 873                        .anchor_in_excerpt(*excerpt_id, text_anchor);
 874                    message_editor.edit(
 875                        [(
 876                            multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 877                            format!("{replacement_text} "),
 878                        )],
 879                        cx,
 880                    );
 881                    (*excerpt_id, text_anchor, multibuffer_anchor)
 882                });
 883
 884            let content_len = replacement_text.len();
 885            let Some(start_anchor) = multibuffer_anchor else {
 886                continue;
 887            };
 888            let end_anchor = self.editor.update(cx, |editor, cx| {
 889                let snapshot = editor.buffer().read(cx).snapshot(cx);
 890                snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
 891            });
 892            let image = Arc::new(image);
 893            let Some((crease_id, tx)) = insert_crease_for_mention(
 894                excerpt_id,
 895                text_anchor,
 896                content_len,
 897                MentionUri::PastedImage.name().into(),
 898                IconName::Image.path().into(),
 899                Some(Task::ready(Ok(image.clone())).shared()),
 900                self.editor.clone(),
 901                window,
 902                cx,
 903            ) else {
 904                continue;
 905            };
 906            let task = cx
 907                .spawn_in(window, {
 908                    async move |_, cx| {
 909                        let format = image.format;
 910                        let image = cx
 911                            .update(|_, cx| LanguageModelImage::from_image(image, cx))
 912                            .map_err(|e| e.to_string())?
 913                            .await;
 914                        drop(tx);
 915                        if let Some(image) = image {
 916                            Ok(Mention::Image(MentionImage {
 917                                data: image.source,
 918                                format,
 919                            }))
 920                        } else {
 921                            Err("Failed to convert image".into())
 922                        }
 923                    }
 924                })
 925                .shared();
 926
 927            self.mention_set
 928                .mentions
 929                .insert(crease_id, (MentionUri::PastedImage, task.clone()));
 930
 931            cx.spawn_in(window, async move |this, cx| {
 932                if task.await.notify_async_err(cx).is_none() {
 933                    this.update(cx, |this, cx| {
 934                        this.editor.update(cx, |editor, cx| {
 935                            editor.edit([(start_anchor..end_anchor, "")], cx);
 936                        });
 937                        this.mention_set.mentions.remove(&crease_id);
 938                    })
 939                    .ok();
 940                }
 941            })
 942            .detach();
 943        }
 944    }
 945
 946    pub fn insert_dragged_files(
 947        &mut self,
 948        paths: Vec<project::ProjectPath>,
 949        added_worktrees: Vec<Entity<Worktree>>,
 950        window: &mut Window,
 951        cx: &mut Context<Self>,
 952    ) {
 953        let path_style = self.project.read(cx).path_style(cx);
 954        let buffer = self.editor.read(cx).buffer().clone();
 955        let Some(buffer) = buffer.read(cx).as_singleton() else {
 956            return;
 957        };
 958        let mut tasks = Vec::new();
 959        for path in paths {
 960            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
 961                continue;
 962            };
 963            let Some(worktree) = self.project.read(cx).worktree_for_id(path.worktree_id, cx) else {
 964                continue;
 965            };
 966            let abs_path = worktree.read(cx).absolutize(&path.path);
 967            let (file_name, _) =
 968                crate::context_picker::file_context_picker::extract_file_name_and_directory(
 969                    &path.path,
 970                    worktree.read(cx).root_name(),
 971                    path_style,
 972                );
 973
 974            let uri = if entry.is_dir() {
 975                MentionUri::Directory { abs_path }
 976            } else {
 977                MentionUri::File { abs_path }
 978            };
 979
 980            let new_text = format!("{} ", uri.as_link());
 981            let content_len = new_text.len() - 1;
 982
 983            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
 984
 985            self.editor.update(cx, |message_editor, cx| {
 986                message_editor.edit(
 987                    [(
 988                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 989                        new_text,
 990                    )],
 991                    cx,
 992                );
 993            });
 994            tasks.push(self.confirm_mention_completion(
 995                file_name,
 996                anchor,
 997                content_len,
 998                uri,
 999                window,
1000                cx,
1001            ));
1002        }
1003        cx.spawn(async move |_, _| {
1004            join_all(tasks).await;
1005            drop(added_worktrees);
1006        })
1007        .detach();
1008    }
1009
1010    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1011        let editor = self.editor.read(cx);
1012        let editor_buffer = editor.buffer().read(cx);
1013        let Some(buffer) = editor_buffer.as_singleton() else {
1014            return;
1015        };
1016        let cursor_anchor = editor.selections.newest_anchor().head();
1017        let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
1018        let anchor = buffer.update(cx, |buffer, _cx| {
1019            buffer.anchor_before(cursor_offset.min(buffer.len()))
1020        });
1021        let Some(workspace) = self.workspace.upgrade() else {
1022            return;
1023        };
1024        let Some(completion) = ContextPickerCompletionProvider::completion_for_action(
1025            ContextPickerAction::AddSelections,
1026            anchor..anchor,
1027            cx.weak_entity(),
1028            &workspace,
1029            cx,
1030        ) else {
1031            return;
1032        };
1033        self.editor.update(cx, |message_editor, cx| {
1034            message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
1035        });
1036        if let Some(confirm) = completion.confirm {
1037            confirm(CompletionIntent::Complete, window, cx);
1038        }
1039    }
1040
1041    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
1042        self.editor.update(cx, |message_editor, cx| {
1043            message_editor.set_read_only(read_only);
1044            cx.notify()
1045        })
1046    }
1047
1048    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
1049        self.editor.update(cx, |editor, cx| {
1050            editor.set_mode(mode);
1051            cx.notify()
1052        });
1053    }
1054
1055    pub fn set_message(
1056        &mut self,
1057        message: Vec<acp::ContentBlock>,
1058        window: &mut Window,
1059        cx: &mut Context<Self>,
1060    ) {
1061        self.clear(window, cx);
1062
1063        let mut text = String::new();
1064        let mut mentions = Vec::new();
1065
1066        for chunk in message {
1067            match chunk {
1068                acp::ContentBlock::Text(text_content) => {
1069                    text.push_str(&text_content.text);
1070                }
1071                acp::ContentBlock::Resource(acp::EmbeddedResource {
1072                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
1073                    ..
1074                }) => {
1075                    let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() else {
1076                        continue;
1077                    };
1078                    let start = text.len();
1079                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1080                    let end = text.len();
1081                    mentions.push((
1082                        start..end,
1083                        mention_uri,
1084                        Mention::Text {
1085                            content: resource.text,
1086                            tracked_buffers: Vec::new(),
1087                        },
1088                    ));
1089                }
1090                acp::ContentBlock::ResourceLink(resource) => {
1091                    if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
1092                        let start = text.len();
1093                        write!(&mut text, "{}", mention_uri.as_link()).ok();
1094                        let end = text.len();
1095                        mentions.push((start..end, mention_uri, Mention::UriOnly));
1096                    }
1097                }
1098                acp::ContentBlock::Image(acp::ImageContent {
1099                    uri,
1100                    data,
1101                    mime_type,
1102                    annotations: _,
1103                    meta: _,
1104                }) => {
1105                    let mention_uri = if let Some(uri) = uri {
1106                        MentionUri::parse(&uri)
1107                    } else {
1108                        Ok(MentionUri::PastedImage)
1109                    };
1110                    let Some(mention_uri) = mention_uri.log_err() else {
1111                        continue;
1112                    };
1113                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
1114                        log::error!("failed to parse MIME type for image: {mime_type:?}");
1115                        continue;
1116                    };
1117                    let start = text.len();
1118                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1119                    let end = text.len();
1120                    mentions.push((
1121                        start..end,
1122                        mention_uri,
1123                        Mention::Image(MentionImage {
1124                            data: data.into(),
1125                            format,
1126                        }),
1127                    ));
1128                }
1129                acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {}
1130            }
1131        }
1132
1133        let snapshot = self.editor.update(cx, |editor, cx| {
1134            editor.set_text(text, window, cx);
1135            editor.buffer().read(cx).snapshot(cx)
1136        });
1137
1138        for (range, mention_uri, mention) in mentions {
1139            let anchor = snapshot.anchor_before(range.start);
1140            let Some((crease_id, tx)) = insert_crease_for_mention(
1141                anchor.excerpt_id,
1142                anchor.text_anchor,
1143                range.end - range.start,
1144                mention_uri.name().into(),
1145                mention_uri.icon_path(cx),
1146                None,
1147                self.editor.clone(),
1148                window,
1149                cx,
1150            ) else {
1151                continue;
1152            };
1153            drop(tx);
1154
1155            self.mention_set.mentions.insert(
1156                crease_id,
1157                (mention_uri.clone(), Task::ready(Ok(mention)).shared()),
1158            );
1159        }
1160        cx.notify();
1161    }
1162
1163    pub fn text(&self, cx: &App) -> String {
1164        self.editor.read(cx).text(cx)
1165    }
1166
1167    #[cfg(test)]
1168    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1169        self.editor.update(cx, |editor, cx| {
1170            editor.set_text(text, window, cx);
1171        });
1172    }
1173}
1174
1175fn full_mention_for_directory(
1176    project: &Entity<Project>,
1177    abs_path: &Path,
1178    cx: &mut App,
1179) -> Task<Result<Mention>> {
1180    fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
1181        let mut files = Vec::new();
1182
1183        for entry in worktree.child_entries(path) {
1184            if entry.is_dir() {
1185                files.extend(collect_files_in_path(worktree, &entry.path));
1186            } else if entry.is_file() {
1187                files.push((
1188                    entry.path.clone(),
1189                    worktree
1190                        .full_path(&entry.path)
1191                        .to_string_lossy()
1192                        .to_string(),
1193                ));
1194            }
1195        }
1196
1197        files
1198    }
1199
1200    let Some(project_path) = project
1201        .read(cx)
1202        .project_path_for_absolute_path(&abs_path, cx)
1203    else {
1204        return Task::ready(Err(anyhow!("project path not found")));
1205    };
1206    let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
1207        return Task::ready(Err(anyhow!("project entry not found")));
1208    };
1209    let directory_path = entry.path.clone();
1210    let worktree_id = project_path.worktree_id;
1211    let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
1212        return Task::ready(Err(anyhow!("worktree not found")));
1213    };
1214    let project = project.clone();
1215    cx.spawn(async move |cx| {
1216        let file_paths = worktree.read_with(cx, |worktree, _cx| {
1217            collect_files_in_path(worktree, &directory_path)
1218        })?;
1219        let descendants_future = cx.update(|cx| {
1220            join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
1221                let rel_path = worktree_path
1222                    .strip_prefix(&directory_path)
1223                    .log_err()
1224                    .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
1225
1226                let open_task = project.update(cx, |project, cx| {
1227                    project.buffer_store().update(cx, |buffer_store, cx| {
1228                        let project_path = ProjectPath {
1229                            worktree_id,
1230                            path: worktree_path,
1231                        };
1232                        buffer_store.open_buffer(project_path, cx)
1233                    })
1234                });
1235
1236                cx.spawn(async move |cx| {
1237                    let buffer = open_task.await.log_err()?;
1238                    let buffer_content = outline::get_buffer_content_or_outline(
1239                        buffer.clone(),
1240                        Some(&full_path),
1241                        &cx,
1242                    )
1243                    .await
1244                    .ok()?;
1245
1246                    Some((rel_path, full_path, buffer_content.text, buffer))
1247                })
1248            }))
1249        })?;
1250
1251        let contents = cx
1252            .background_spawn(async move {
1253                let (contents, tracked_buffers) = descendants_future
1254                    .await
1255                    .into_iter()
1256                    .flatten()
1257                    .map(|(rel_path, full_path, rope, buffer)| {
1258                        ((rel_path, full_path, rope), buffer)
1259                    })
1260                    .unzip();
1261                Mention::Text {
1262                    content: render_directory_contents(contents),
1263                    tracked_buffers,
1264                }
1265            })
1266            .await;
1267        anyhow::Ok(contents)
1268    })
1269}
1270
1271fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
1272    let mut output = String::new();
1273    for (_relative_path, full_path, content) in entries {
1274        let fence = codeblock_fence_for_path(Some(&full_path), None);
1275        write!(output, "\n{fence}\n{content}\n```").unwrap();
1276    }
1277    output
1278}
1279
1280impl Focusable for MessageEditor {
1281    fn focus_handle(&self, cx: &App) -> FocusHandle {
1282        self.editor.focus_handle(cx)
1283    }
1284}
1285
1286impl Render for MessageEditor {
1287    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1288        div()
1289            .key_context("MessageEditor")
1290            .on_action(cx.listener(Self::send))
1291            .on_action(cx.listener(Self::cancel))
1292            .capture_action(cx.listener(Self::paste))
1293            .flex_1()
1294            .child({
1295                let settings = ThemeSettings::get_global(cx);
1296
1297                let text_style = TextStyle {
1298                    color: cx.theme().colors().text,
1299                    font_family: settings.buffer_font.family.clone(),
1300                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
1301                    font_features: settings.buffer_font.features.clone(),
1302                    font_size: settings.agent_buffer_font_size(cx).into(),
1303                    line_height: relative(settings.buffer_line_height.value()),
1304                    ..Default::default()
1305                };
1306
1307                EditorElement::new(
1308                    &self.editor,
1309                    EditorStyle {
1310                        background: cx.theme().colors().editor_background,
1311                        local_player: cx.theme().players().local(),
1312                        text: text_style,
1313                        syntax: cx.theme().syntax().clone(),
1314                        inlay_hints_style: editor::make_inlay_hints_style(cx),
1315                        ..Default::default()
1316                    },
1317                )
1318            })
1319    }
1320}
1321
1322pub(crate) fn insert_crease_for_mention(
1323    excerpt_id: ExcerptId,
1324    anchor: text::Anchor,
1325    content_len: usize,
1326    crease_label: SharedString,
1327    crease_icon: SharedString,
1328    // abs_path: Option<Arc<Path>>,
1329    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1330    editor: Entity<Editor>,
1331    window: &mut Window,
1332    cx: &mut App,
1333) -> Option<(CreaseId, postage::barrier::Sender)> {
1334    let (tx, rx) = postage::barrier::channel();
1335
1336    let crease_id = editor.update(cx, |editor, cx| {
1337        let snapshot = editor.buffer().read(cx).snapshot(cx);
1338
1339        let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
1340
1341        let start = start.bias_right(&snapshot);
1342        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
1343
1344        let placeholder = FoldPlaceholder {
1345            render: render_mention_fold_button(
1346                crease_label,
1347                crease_icon,
1348                start..end,
1349                rx,
1350                image,
1351                cx.weak_entity(),
1352                cx,
1353            ),
1354            merge_adjacent: false,
1355            ..Default::default()
1356        };
1357
1358        let crease = Crease::Inline {
1359            range: start..end,
1360            placeholder,
1361            render_toggle: None,
1362            render_trailer: None,
1363            metadata: None,
1364        };
1365
1366        let ids = editor.insert_creases(vec![crease.clone()], cx);
1367        editor.fold_creases(vec![crease], false, window, cx);
1368
1369        Some(ids[0])
1370    })?;
1371
1372    Some((crease_id, tx))
1373}
1374
1375fn render_mention_fold_button(
1376    label: SharedString,
1377    icon: SharedString,
1378    range: Range<Anchor>,
1379    mut loading_finished: postage::barrier::Receiver,
1380    image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1381    editor: WeakEntity<Editor>,
1382    cx: &mut App,
1383) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
1384    let loading = cx.new(|cx| {
1385        let loading = cx.spawn(async move |this, cx| {
1386            loading_finished.recv().await;
1387            this.update(cx, |this: &mut LoadingContext, cx| {
1388                this.loading = None;
1389                cx.notify();
1390            })
1391            .ok();
1392        });
1393        LoadingContext {
1394            id: cx.entity_id(),
1395            label,
1396            icon,
1397            range,
1398            editor,
1399            loading: Some(loading),
1400            image: image_task.clone(),
1401        }
1402    });
1403    Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
1404}
1405
1406struct LoadingContext {
1407    id: EntityId,
1408    label: SharedString,
1409    icon: SharedString,
1410    range: Range<Anchor>,
1411    editor: WeakEntity<Editor>,
1412    loading: Option<Task<()>>,
1413    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1414}
1415
1416impl Render for LoadingContext {
1417    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1418        let is_in_text_selection = self
1419            .editor
1420            .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
1421            .unwrap_or_default();
1422        ButtonLike::new(("loading-context", self.id))
1423            .style(ButtonStyle::Filled)
1424            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1425            .toggle_state(is_in_text_selection)
1426            .when_some(self.image.clone(), |el, image_task| {
1427                el.hoverable_tooltip(move |_, cx| {
1428                    let image = image_task.peek().cloned().transpose().ok().flatten();
1429                    let image_task = image_task.clone();
1430                    cx.new::<ImageHover>(|cx| ImageHover {
1431                        image,
1432                        _task: cx.spawn(async move |this, cx| {
1433                            if let Ok(image) = image_task.clone().await {
1434                                this.update(cx, |this, cx| {
1435                                    if this.image.replace(image).is_none() {
1436                                        cx.notify();
1437                                    }
1438                                })
1439                                .ok();
1440                            }
1441                        }),
1442                    })
1443                    .into()
1444                })
1445            })
1446            .child(
1447                h_flex()
1448                    .gap_1()
1449                    .child(
1450                        Icon::from_path(self.icon.clone())
1451                            .size(IconSize::XSmall)
1452                            .color(Color::Muted),
1453                    )
1454                    .child(
1455                        Label::new(self.label.clone())
1456                            .size(LabelSize::Small)
1457                            .buffer_font(cx)
1458                            .single_line(),
1459                    )
1460                    .map(|el| {
1461                        if self.loading.is_some() {
1462                            el.with_animation(
1463                                "loading-context-crease",
1464                                Animation::new(Duration::from_secs(2))
1465                                    .repeat()
1466                                    .with_easing(pulsating_between(0.4, 0.8)),
1467                                |label, delta| label.opacity(delta),
1468                            )
1469                            .into_any()
1470                        } else {
1471                            el.into_any()
1472                        }
1473                    }),
1474            )
1475    }
1476}
1477
1478struct ImageHover {
1479    image: Option<Arc<Image>>,
1480    _task: Task<()>,
1481}
1482
1483impl Render for ImageHover {
1484    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1485        if let Some(image) = self.image.clone() {
1486            gpui::img(image).max_w_96().max_h_96().into_any_element()
1487        } else {
1488            gpui::Empty.into_any_element()
1489        }
1490    }
1491}
1492
1493#[derive(Debug, Clone, Eq, PartialEq)]
1494pub enum Mention {
1495    Text {
1496        content: String,
1497        tracked_buffers: Vec<Entity<Buffer>>,
1498    },
1499    Image(MentionImage),
1500    UriOnly,
1501}
1502
1503#[derive(Clone, Debug, Eq, PartialEq)]
1504pub struct MentionImage {
1505    pub data: SharedString,
1506    pub format: ImageFormat,
1507}
1508
1509#[derive(Default)]
1510pub struct MentionSet {
1511    mentions: HashMap<CreaseId, (MentionUri, Shared<Task<Result<Mention, String>>>)>,
1512}
1513
1514impl MentionSet {
1515    fn contents(
1516        &self,
1517        prompt_capabilities: &acp::PromptCapabilities,
1518        full_mention_content: bool,
1519        project: Entity<Project>,
1520        cx: &mut App,
1521    ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
1522        if !prompt_capabilities.embedded_context {
1523            let mentions = self
1524                .mentions
1525                .iter()
1526                .map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly)))
1527                .collect();
1528
1529            return Task::ready(Ok(mentions));
1530        }
1531
1532        let mentions = self.mentions.clone();
1533        cx.spawn(async move |cx| {
1534            let mut contents = HashMap::default();
1535            for (crease_id, (mention_uri, task)) in mentions {
1536                let content = if full_mention_content
1537                    && let MentionUri::Directory { abs_path } = &mention_uri
1538                {
1539                    cx.update(|cx| full_mention_for_directory(&project, abs_path, cx))?
1540                        .await?
1541                } else {
1542                    task.await.map_err(|e| anyhow!("{e}"))?
1543                };
1544
1545                contents.insert(crease_id, (mention_uri, content));
1546            }
1547            Ok(contents)
1548        })
1549    }
1550
1551    fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
1552        for (crease_id, crease) in snapshot.crease_snapshot.creases() {
1553            if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
1554                self.mentions.remove(&crease_id);
1555            }
1556        }
1557    }
1558}
1559
1560pub struct MessageEditorAddon {}
1561
1562impl MessageEditorAddon {
1563    pub fn new() -> Self {
1564        Self {}
1565    }
1566}
1567
1568impl Addon for MessageEditorAddon {
1569    fn to_any(&self) -> &dyn std::any::Any {
1570        self
1571    }
1572
1573    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1574        Some(self)
1575    }
1576
1577    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1578        let settings = agent_settings::AgentSettings::get_global(cx);
1579        if settings.use_modifier_to_send {
1580            key_context.add("use_modifier_to_send");
1581        }
1582    }
1583}
1584
1585#[cfg(test)]
1586mod tests {
1587    use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
1588
1589    use acp_thread::MentionUri;
1590    use agent_client_protocol as acp;
1591    use agent2::HistoryStore;
1592    use assistant_context::ContextStore;
1593    use assistant_tool::outline;
1594    use editor::{AnchorRangeExt as _, Editor, EditorMode};
1595    use fs::FakeFs;
1596    use futures::StreamExt as _;
1597    use gpui::{
1598        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1599    };
1600    use lsp::{CompletionContext, CompletionTriggerKind};
1601    use project::{CompletionIntent, Project, ProjectPath};
1602    use serde_json::json;
1603    use text::Point;
1604    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1605    use util::{path, paths::PathStyle, rel_path::rel_path};
1606    use workspace::{AppState, Item, Workspace};
1607
1608    use crate::acp::{
1609        message_editor::{Mention, MessageEditor},
1610        thread_view::tests::init_test,
1611    };
1612
1613    #[gpui::test]
1614    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1615        init_test(cx);
1616
1617        let fs = FakeFs::new(cx.executor());
1618        fs.insert_tree("/project", json!({"file": ""})).await;
1619        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1620
1621        let (workspace, cx) =
1622            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1623
1624        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
1625        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
1626
1627        let message_editor = cx.update(|window, cx| {
1628            cx.new(|cx| {
1629                MessageEditor::new(
1630                    workspace.downgrade(),
1631                    project.clone(),
1632                    history_store.clone(),
1633                    None,
1634                    Default::default(),
1635                    Default::default(),
1636                    "Test Agent".into(),
1637                    "Test",
1638                    EditorMode::AutoHeight {
1639                        min_lines: 1,
1640                        max_lines: None,
1641                    },
1642                    window,
1643                    cx,
1644                )
1645            })
1646        });
1647        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1648
1649        cx.run_until_parked();
1650
1651        let excerpt_id = editor.update(cx, |editor, cx| {
1652            editor
1653                .buffer()
1654                .read(cx)
1655                .excerpt_ids()
1656                .into_iter()
1657                .next()
1658                .unwrap()
1659        });
1660        let completions = editor.update_in(cx, |editor, window, cx| {
1661            editor.set_text("Hello @file ", window, cx);
1662            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1663            let completion_provider = editor.completion_provider().unwrap();
1664            completion_provider.completions(
1665                excerpt_id,
1666                &buffer,
1667                text::Anchor::MAX,
1668                CompletionContext {
1669                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1670                    trigger_character: Some("@".into()),
1671                },
1672                window,
1673                cx,
1674            )
1675        });
1676        let [_, completion]: [_; 2] = completions
1677            .await
1678            .unwrap()
1679            .into_iter()
1680            .flat_map(|response| response.completions)
1681            .collect::<Vec<_>>()
1682            .try_into()
1683            .unwrap();
1684
1685        editor.update_in(cx, |editor, window, cx| {
1686            let snapshot = editor.buffer().read(cx).snapshot(cx);
1687            let start = snapshot
1688                .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
1689                .unwrap();
1690            let end = snapshot
1691                .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
1692                .unwrap();
1693            editor.edit([(start..end, completion.new_text)], cx);
1694            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1695        });
1696
1697        cx.run_until_parked();
1698
1699        // Backspace over the inserted crease (and the following space).
1700        editor.update_in(cx, |editor, window, cx| {
1701            editor.backspace(&Default::default(), window, cx);
1702            editor.backspace(&Default::default(), window, cx);
1703        });
1704
1705        let (content, _) = message_editor
1706            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1707            .await
1708            .unwrap();
1709
1710        // We don't send a resource link for the deleted crease.
1711        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1712    }
1713
1714    #[gpui::test]
1715    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
1716        init_test(cx);
1717        let fs = FakeFs::new(cx.executor());
1718        fs.insert_tree(
1719            "/test",
1720            json!({
1721                ".zed": {
1722                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
1723                },
1724                "src": {
1725                    "main.rs": "fn main() {}",
1726                },
1727            }),
1728        )
1729        .await;
1730
1731        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1732        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
1733        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
1734        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1735        // Start with no available commands - simulating Claude which doesn't support slash commands
1736        let available_commands = Rc::new(RefCell::new(vec![]));
1737
1738        let (workspace, cx) =
1739            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1740        let workspace_handle = workspace.downgrade();
1741        let message_editor = workspace.update_in(cx, |_, window, cx| {
1742            cx.new(|cx| {
1743                MessageEditor::new(
1744                    workspace_handle.clone(),
1745                    project.clone(),
1746                    history_store.clone(),
1747                    None,
1748                    prompt_capabilities.clone(),
1749                    available_commands.clone(),
1750                    "Claude Code".into(),
1751                    "Test",
1752                    EditorMode::AutoHeight {
1753                        min_lines: 1,
1754                        max_lines: None,
1755                    },
1756                    window,
1757                    cx,
1758                )
1759            })
1760        });
1761        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1762
1763        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
1764        editor.update_in(cx, |editor, window, cx| {
1765            editor.set_text("/file test.txt", window, cx);
1766        });
1767
1768        let contents_result = message_editor
1769            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1770            .await;
1771
1772        // Should fail because available_commands is empty (no commands supported)
1773        assert!(contents_result.is_err());
1774        let error_message = contents_result.unwrap_err().to_string();
1775        assert!(error_message.contains("not supported by Claude Code"));
1776        assert!(error_message.contains("Available commands: none"));
1777
1778        // Now simulate Claude providing its list of available commands (which doesn't include file)
1779        available_commands.replace(vec![acp::AvailableCommand {
1780            name: "help".to_string(),
1781            description: "Get help".to_string(),
1782            input: None,
1783            meta: None,
1784        }]);
1785
1786        // Test that unsupported slash commands trigger an error when we have a list of available commands
1787        editor.update_in(cx, |editor, window, cx| {
1788            editor.set_text("/file test.txt", window, cx);
1789        });
1790
1791        let contents_result = message_editor
1792            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1793            .await;
1794
1795        assert!(contents_result.is_err());
1796        let error_message = contents_result.unwrap_err().to_string();
1797        assert!(error_message.contains("not supported by Claude Code"));
1798        assert!(error_message.contains("/file"));
1799        assert!(error_message.contains("Available commands: /help"));
1800
1801        // Test that supported commands work fine
1802        editor.update_in(cx, |editor, window, cx| {
1803            editor.set_text("/help", window, cx);
1804        });
1805
1806        let contents_result = message_editor
1807            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1808            .await;
1809
1810        // Should succeed because /help is in available_commands
1811        assert!(contents_result.is_ok());
1812
1813        // Test that regular text works fine
1814        editor.update_in(cx, |editor, window, cx| {
1815            editor.set_text("Hello Claude!", window, cx);
1816        });
1817
1818        let (content, _) = message_editor
1819            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1820            .await
1821            .unwrap();
1822
1823        assert_eq!(content.len(), 1);
1824        if let acp::ContentBlock::Text(text) = &content[0] {
1825            assert_eq!(text.text, "Hello Claude!");
1826        } else {
1827            panic!("Expected ContentBlock::Text");
1828        }
1829
1830        // Test that @ mentions still work
1831        editor.update_in(cx, |editor, window, cx| {
1832            editor.set_text("Check this @", window, cx);
1833        });
1834
1835        // The @ mention functionality should not be affected
1836        let (content, _) = message_editor
1837            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1838            .await
1839            .unwrap();
1840
1841        assert_eq!(content.len(), 1);
1842        if let acp::ContentBlock::Text(text) = &content[0] {
1843            assert_eq!(text.text, "Check this @");
1844        } else {
1845            panic!("Expected ContentBlock::Text");
1846        }
1847    }
1848
1849    struct MessageEditorItem(Entity<MessageEditor>);
1850
1851    impl Item for MessageEditorItem {
1852        type Event = ();
1853
1854        fn include_in_nav_history() -> bool {
1855            false
1856        }
1857
1858        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1859            "Test".into()
1860        }
1861    }
1862
1863    impl EventEmitter<()> for MessageEditorItem {}
1864
1865    impl Focusable for MessageEditorItem {
1866        fn focus_handle(&self, cx: &App) -> FocusHandle {
1867            self.0.read(cx).focus_handle(cx)
1868        }
1869    }
1870
1871    impl Render for MessageEditorItem {
1872        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1873            self.0.clone().into_any_element()
1874        }
1875    }
1876
1877    #[gpui::test]
1878    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
1879        init_test(cx);
1880
1881        let app_state = cx.update(AppState::test);
1882
1883        cx.update(|cx| {
1884            language::init(cx);
1885            editor::init(cx);
1886            workspace::init(app_state.clone(), cx);
1887            Project::init_settings(cx);
1888        });
1889
1890        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1891        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1892        let workspace = window.root(cx).unwrap();
1893
1894        let mut cx = VisualTestContext::from_window(*window, cx);
1895
1896        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
1897        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
1898        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1899        let available_commands = Rc::new(RefCell::new(vec![
1900            acp::AvailableCommand {
1901                name: "quick-math".to_string(),
1902                description: "2 + 2 = 4 - 1 = 3".to_string(),
1903                input: None,
1904                meta: None,
1905            },
1906            acp::AvailableCommand {
1907                name: "say-hello".to_string(),
1908                description: "Say hello to whoever you want".to_string(),
1909                input: Some(acp::AvailableCommandInput::Unstructured {
1910                    hint: "<name>".to_string(),
1911                }),
1912                meta: None,
1913            },
1914        ]));
1915
1916        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
1917            let workspace_handle = cx.weak_entity();
1918            let message_editor = cx.new(|cx| {
1919                MessageEditor::new(
1920                    workspace_handle,
1921                    project.clone(),
1922                    history_store.clone(),
1923                    None,
1924                    prompt_capabilities.clone(),
1925                    available_commands.clone(),
1926                    "Test Agent".into(),
1927                    "Test",
1928                    EditorMode::AutoHeight {
1929                        max_lines: None,
1930                        min_lines: 1,
1931                    },
1932                    window,
1933                    cx,
1934                )
1935            });
1936            workspace.active_pane().update(cx, |pane, cx| {
1937                pane.add_item(
1938                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1939                    true,
1940                    true,
1941                    None,
1942                    window,
1943                    cx,
1944                );
1945            });
1946            message_editor.read(cx).focus_handle(cx).focus(window);
1947            message_editor.read(cx).editor().clone()
1948        });
1949
1950        cx.simulate_input("/");
1951
1952        editor.update_in(&mut cx, |editor, window, cx| {
1953            assert_eq!(editor.text(cx), "/");
1954            assert!(editor.has_visible_completions_menu());
1955
1956            assert_eq!(
1957                current_completion_labels_with_documentation(editor),
1958                &[
1959                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
1960                    ("say-hello".into(), "Say hello to whoever you want".into())
1961                ]
1962            );
1963            editor.set_text("", window, cx);
1964        });
1965
1966        cx.simulate_input("/qui");
1967
1968        editor.update_in(&mut cx, |editor, window, cx| {
1969            assert_eq!(editor.text(cx), "/qui");
1970            assert!(editor.has_visible_completions_menu());
1971
1972            assert_eq!(
1973                current_completion_labels_with_documentation(editor),
1974                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
1975            );
1976            editor.set_text("", window, cx);
1977        });
1978
1979        editor.update_in(&mut cx, |editor, window, cx| {
1980            assert!(editor.has_visible_completions_menu());
1981            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1982        });
1983
1984        cx.run_until_parked();
1985
1986        editor.update_in(&mut cx, |editor, window, cx| {
1987            assert_eq!(editor.display_text(cx), "/quick-math ");
1988            assert!(!editor.has_visible_completions_menu());
1989            editor.set_text("", window, cx);
1990        });
1991
1992        cx.simulate_input("/say");
1993
1994        editor.update_in(&mut cx, |editor, _window, cx| {
1995            assert_eq!(editor.display_text(cx), "/say");
1996            assert!(editor.has_visible_completions_menu());
1997
1998            assert_eq!(
1999                current_completion_labels_with_documentation(editor),
2000                &[("say-hello".into(), "Say hello to whoever you want".into())]
2001            );
2002        });
2003
2004        editor.update_in(&mut cx, |editor, window, cx| {
2005            assert!(editor.has_visible_completions_menu());
2006            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2007        });
2008
2009        cx.run_until_parked();
2010
2011        editor.update_in(&mut cx, |editor, _window, cx| {
2012            assert_eq!(editor.text(cx), "/say-hello ");
2013            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2014            assert!(editor.has_visible_completions_menu());
2015
2016            assert_eq!(
2017                current_completion_labels_with_documentation(editor),
2018                &[("say-hello".into(), "Say hello to whoever you want".into())]
2019            );
2020        });
2021
2022        cx.simulate_input("GPT5");
2023
2024        editor.update_in(&mut cx, |editor, window, cx| {
2025            assert!(editor.has_visible_completions_menu());
2026            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2027        });
2028
2029        cx.run_until_parked();
2030
2031        editor.update_in(&mut cx, |editor, window, cx| {
2032            assert_eq!(editor.text(cx), "/say-hello GPT5");
2033            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
2034            assert!(!editor.has_visible_completions_menu());
2035
2036            // Delete argument
2037            for _ in 0..4 {
2038                editor.backspace(&editor::actions::Backspace, window, cx);
2039            }
2040        });
2041
2042        cx.run_until_parked();
2043
2044        editor.update_in(&mut cx, |editor, window, cx| {
2045            assert_eq!(editor.text(cx), "/say-hello ");
2046            // Hint is visible because argument was deleted
2047            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2048
2049            // Delete last command letter
2050            editor.backspace(&editor::actions::Backspace, window, cx);
2051            editor.backspace(&editor::actions::Backspace, window, cx);
2052        });
2053
2054        cx.run_until_parked();
2055
2056        editor.update_in(&mut cx, |editor, _window, cx| {
2057            // Hint goes away once command no longer matches an available one
2058            assert_eq!(editor.text(cx), "/say-hell");
2059            assert_eq!(editor.display_text(cx), "/say-hell");
2060            assert!(!editor.has_visible_completions_menu());
2061        });
2062    }
2063
2064    #[gpui::test]
2065    async fn test_mention_menu_after_slash_command_with_space(cx: &mut TestAppContext) {
2066        init_test(cx);
2067
2068        let app_state = cx.update(AppState::test);
2069
2070        cx.update(|cx| {
2071            language::init(cx);
2072            editor::init(cx);
2073            workspace::init(app_state.clone(), cx);
2074            Project::init_settings(cx);
2075        });
2076
2077        // Create test files in the fake filesystem
2078        app_state
2079            .fs
2080            .as_fake()
2081            .insert_tree(
2082                path!("/dir"),
2083                json!({
2084                    "test_file.txt": "test content",
2085                    "another.txt": "more content",
2086                }),
2087            )
2088            .await;
2089
2090        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2091        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2092        let workspace = window.root(cx).unwrap();
2093
2094        let worktree = project.update(cx, |project, cx| {
2095            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2096            assert_eq!(worktrees.len(), 1);
2097            worktrees.pop().unwrap()
2098        });
2099        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2100
2101        let mut cx = VisualTestContext::from_window(*window, cx);
2102
2103        // Open the files so they appear in recent file mentions
2104        let paths = vec![rel_path("test_file.txt"), rel_path("another.txt")];
2105        for path in paths {
2106            workspace
2107                .update_in(&mut cx, |workspace, window, cx| {
2108                    workspace.open_path(
2109                        ProjectPath {
2110                            worktree_id,
2111                            path: path.into(),
2112                        },
2113                        None,
2114                        false,
2115                        window,
2116                        cx,
2117                    )
2118                })
2119                .await
2120                .unwrap();
2121        }
2122
2123        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
2124        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
2125        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
2126        let available_commands = Rc::new(RefCell::new(vec![acp::AvailableCommand {
2127            name: "init".to_string(),
2128            description: "Initialize a project".to_string(),
2129            input: Some(acp::AvailableCommandInput::Unstructured {
2130                hint: "<description>".to_string(),
2131            }),
2132            meta: None,
2133        }]));
2134
2135        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
2136            let workspace_handle = cx.weak_entity();
2137            let message_editor = cx.new(|cx| {
2138                MessageEditor::new(
2139                    workspace_handle,
2140                    project.clone(),
2141                    history_store.clone(),
2142                    None,
2143                    prompt_capabilities.clone(),
2144                    available_commands.clone(),
2145                    "Claude Code".into(),
2146                    "Test",
2147                    EditorMode::AutoHeight {
2148                        max_lines: None,
2149                        min_lines: 1,
2150                    },
2151                    window,
2152                    cx,
2153                )
2154            });
2155            workspace.active_pane().update(cx, |pane, cx| {
2156                pane.add_item(
2157                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2158                    true,
2159                    true,
2160                    None,
2161                    window,
2162                    cx,
2163                );
2164            });
2165            message_editor.read(cx).focus_handle(cx).focus(window);
2166            message_editor.read(cx).editor().clone()
2167        });
2168
2169        // Type "/init " (slash command with space)
2170        cx.simulate_input("/init ");
2171
2172        editor.update(&mut cx, |editor, cx| {
2173            assert_eq!(editor.text(cx), "/init ");
2174        });
2175
2176        // Now type "@" - this should open the @ mention menu
2177        cx.simulate_input("@");
2178
2179        editor.update(&mut cx, |editor, cx| {
2180            assert_eq!(editor.text(cx), "/init @");
2181
2182            // The @ mention menu should be visible after typing @ following a slash command with space.
2183            assert!(
2184                editor.has_visible_completions_menu(),
2185                "Completion menu should be visible after typing @"
2186            );
2187
2188            // Check that we have @ mention completions (file mentions from recently opened files)
2189            // not slash command completions (which would be "init")
2190            let labels = current_completion_labels(editor);
2191
2192            // We should see our recently opened files in the completions
2193            let has_file_mention = labels
2194                .iter()
2195                .any(|label| label.contains("test_file.txt") || label.contains("another.txt"));
2196
2197            // We should NOT see the slash command "init"
2198            let has_slash_command = labels.iter().any(|label| label == "init");
2199
2200            assert!(
2201                has_file_mention,
2202                "Expected @ mention completions with file names (test_file.txt, another.txt) but got: {:?}",
2203                labels
2204            );
2205
2206            assert!(
2207                !has_slash_command,
2208                "Expected @ mention completions but got slash command completion 'init': {:?}",
2209                labels
2210            );
2211        });
2212    }
2213
2214    #[gpui::test]
2215    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
2216        init_test(cx);
2217
2218        let app_state = cx.update(AppState::test);
2219
2220        cx.update(|cx| {
2221            language::init(cx);
2222            editor::init(cx);
2223            workspace::init(app_state.clone(), cx);
2224            Project::init_settings(cx);
2225        });
2226
2227        app_state
2228            .fs
2229            .as_fake()
2230            .insert_tree(
2231                path!("/dir"),
2232                json!({
2233                    "editor": "",
2234                    "a": {
2235                        "one.txt": "1",
2236                        "two.txt": "2",
2237                        "three.txt": "3",
2238                        "four.txt": "4"
2239                    },
2240                    "b": {
2241                        "five.txt": "5",
2242                        "six.txt": "6",
2243                        "seven.txt": "7",
2244                        "eight.txt": "8",
2245                    },
2246                    "x.png": "",
2247                }),
2248            )
2249            .await;
2250
2251        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2252        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2253        let workspace = window.root(cx).unwrap();
2254
2255        let worktree = project.update(cx, |project, cx| {
2256            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2257            assert_eq!(worktrees.len(), 1);
2258            worktrees.pop().unwrap()
2259        });
2260        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2261
2262        let mut cx = VisualTestContext::from_window(*window, cx);
2263
2264        let paths = vec![
2265            rel_path("a/one.txt"),
2266            rel_path("a/two.txt"),
2267            rel_path("a/three.txt"),
2268            rel_path("a/four.txt"),
2269            rel_path("b/five.txt"),
2270            rel_path("b/six.txt"),
2271            rel_path("b/seven.txt"),
2272            rel_path("b/eight.txt"),
2273        ];
2274
2275        let slash = PathStyle::local().separator();
2276
2277        let mut opened_editors = Vec::new();
2278        for path in paths {
2279            let buffer = workspace
2280                .update_in(&mut cx, |workspace, window, cx| {
2281                    workspace.open_path(
2282                        ProjectPath {
2283                            worktree_id,
2284                            path: path.into(),
2285                        },
2286                        None,
2287                        false,
2288                        window,
2289                        cx,
2290                    )
2291                })
2292                .await
2293                .unwrap();
2294            opened_editors.push(buffer);
2295        }
2296
2297        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
2298        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
2299        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
2300
2301        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
2302            let workspace_handle = cx.weak_entity();
2303            let message_editor = cx.new(|cx| {
2304                MessageEditor::new(
2305                    workspace_handle,
2306                    project.clone(),
2307                    history_store.clone(),
2308                    None,
2309                    prompt_capabilities.clone(),
2310                    Default::default(),
2311                    "Test Agent".into(),
2312                    "Test",
2313                    EditorMode::AutoHeight {
2314                        max_lines: None,
2315                        min_lines: 1,
2316                    },
2317                    window,
2318                    cx,
2319                )
2320            });
2321            workspace.active_pane().update(cx, |pane, cx| {
2322                pane.add_item(
2323                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2324                    true,
2325                    true,
2326                    None,
2327                    window,
2328                    cx,
2329                );
2330            });
2331            message_editor.read(cx).focus_handle(cx).focus(window);
2332            let editor = message_editor.read(cx).editor().clone();
2333            (message_editor, editor)
2334        });
2335
2336        cx.simulate_input("Lorem @");
2337
2338        editor.update_in(&mut cx, |editor, window, cx| {
2339            assert_eq!(editor.text(cx), "Lorem @");
2340            assert!(editor.has_visible_completions_menu());
2341
2342            assert_eq!(
2343                current_completion_labels(editor),
2344                &[
2345                    format!("eight.txt dir{slash}b{slash}"),
2346                    format!("seven.txt dir{slash}b{slash}"),
2347                    format!("six.txt dir{slash}b{slash}"),
2348                    format!("five.txt dir{slash}b{slash}"),
2349                ]
2350            );
2351            editor.set_text("", window, cx);
2352        });
2353
2354        prompt_capabilities.replace(acp::PromptCapabilities {
2355            image: true,
2356            audio: true,
2357            embedded_context: true,
2358            meta: None,
2359        });
2360
2361        cx.simulate_input("Lorem ");
2362
2363        editor.update(&mut cx, |editor, cx| {
2364            assert_eq!(editor.text(cx), "Lorem ");
2365            assert!(!editor.has_visible_completions_menu());
2366        });
2367
2368        cx.simulate_input("@");
2369
2370        editor.update(&mut cx, |editor, cx| {
2371            assert_eq!(editor.text(cx), "Lorem @");
2372            assert!(editor.has_visible_completions_menu());
2373            assert_eq!(
2374                current_completion_labels(editor),
2375                &[
2376                    format!("eight.txt dir{slash}b{slash}"),
2377                    format!("seven.txt dir{slash}b{slash}"),
2378                    format!("six.txt dir{slash}b{slash}"),
2379                    format!("five.txt dir{slash}b{slash}"),
2380                    "Files & Directories".into(),
2381                    "Symbols".into(),
2382                    "Threads".into(),
2383                    "Fetch".into()
2384                ]
2385            );
2386        });
2387
2388        // Select and confirm "File"
2389        editor.update_in(&mut cx, |editor, window, cx| {
2390            assert!(editor.has_visible_completions_menu());
2391            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2392            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2393            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2394            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2395            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2396        });
2397
2398        cx.run_until_parked();
2399
2400        editor.update(&mut cx, |editor, cx| {
2401            assert_eq!(editor.text(cx), "Lorem @file ");
2402            assert!(editor.has_visible_completions_menu());
2403        });
2404
2405        cx.simulate_input("one");
2406
2407        editor.update(&mut cx, |editor, cx| {
2408            assert_eq!(editor.text(cx), "Lorem @file one");
2409            assert!(editor.has_visible_completions_menu());
2410            assert_eq!(
2411                current_completion_labels(editor),
2412                vec![format!("one.txt dir{slash}a{slash}")]
2413            );
2414        });
2415
2416        editor.update_in(&mut cx, |editor, window, cx| {
2417            assert!(editor.has_visible_completions_menu());
2418            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2419        });
2420
2421        let url_one = MentionUri::File {
2422            abs_path: path!("/dir/a/one.txt").into(),
2423        }
2424        .to_uri()
2425        .to_string();
2426        editor.update(&mut cx, |editor, cx| {
2427            let text = editor.text(cx);
2428            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2429            assert!(!editor.has_visible_completions_menu());
2430            assert_eq!(fold_ranges(editor, cx).len(), 1);
2431        });
2432
2433        let all_prompt_capabilities = acp::PromptCapabilities {
2434            image: true,
2435            audio: true,
2436            embedded_context: true,
2437            meta: None,
2438        };
2439
2440        let contents = message_editor
2441            .update(&mut cx, |message_editor, cx| {
2442                message_editor.mention_set().contents(
2443                    &all_prompt_capabilities,
2444                    false,
2445                    project.clone(),
2446                    cx,
2447                )
2448            })
2449            .await
2450            .unwrap()
2451            .into_values()
2452            .collect::<Vec<_>>();
2453
2454        {
2455            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
2456                panic!("Unexpected mentions");
2457            };
2458            pretty_assertions::assert_eq!(content, "1");
2459            pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
2460        }
2461
2462        let contents = message_editor
2463            .update(&mut cx, |message_editor, cx| {
2464                message_editor.mention_set().contents(
2465                    &acp::PromptCapabilities::default(),
2466                    false,
2467                    project.clone(),
2468                    cx,
2469                )
2470            })
2471            .await
2472            .unwrap()
2473            .into_values()
2474            .collect::<Vec<_>>();
2475
2476        {
2477            let [(uri, Mention::UriOnly)] = contents.as_slice() else {
2478                panic!("Unexpected mentions");
2479            };
2480            pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
2481        }
2482
2483        cx.simulate_input(" ");
2484
2485        editor.update(&mut cx, |editor, cx| {
2486            let text = editor.text(cx);
2487            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
2488            assert!(!editor.has_visible_completions_menu());
2489            assert_eq!(fold_ranges(editor, cx).len(), 1);
2490        });
2491
2492        cx.simulate_input("Ipsum ");
2493
2494        editor.update(&mut cx, |editor, cx| {
2495            let text = editor.text(cx);
2496            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
2497            assert!(!editor.has_visible_completions_menu());
2498            assert_eq!(fold_ranges(editor, cx).len(), 1);
2499        });
2500
2501        cx.simulate_input("@file ");
2502
2503        editor.update(&mut cx, |editor, cx| {
2504            let text = editor.text(cx);
2505            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
2506            assert!(editor.has_visible_completions_menu());
2507            assert_eq!(fold_ranges(editor, cx).len(), 1);
2508        });
2509
2510        editor.update_in(&mut cx, |editor, window, cx| {
2511            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2512        });
2513
2514        cx.run_until_parked();
2515
2516        let contents = message_editor
2517            .update(&mut cx, |message_editor, cx| {
2518                message_editor.mention_set().contents(
2519                    &all_prompt_capabilities,
2520                    false,
2521                    project.clone(),
2522                    cx,
2523                )
2524            })
2525            .await
2526            .unwrap()
2527            .into_values()
2528            .collect::<Vec<_>>();
2529
2530        let url_eight = MentionUri::File {
2531            abs_path: path!("/dir/b/eight.txt").into(),
2532        }
2533        .to_uri()
2534        .to_string();
2535
2536        {
2537            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2538                panic!("Unexpected mentions");
2539            };
2540            pretty_assertions::assert_eq!(content, "8");
2541            pretty_assertions::assert_eq!(uri, &url_eight.parse::<MentionUri>().unwrap());
2542        }
2543
2544        editor.update(&mut cx, |editor, cx| {
2545            assert_eq!(
2546                editor.text(cx),
2547                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
2548            );
2549            assert!(!editor.has_visible_completions_menu());
2550            assert_eq!(fold_ranges(editor, cx).len(), 2);
2551        });
2552
2553        let plain_text_language = Arc::new(language::Language::new(
2554            language::LanguageConfig {
2555                name: "Plain Text".into(),
2556                matcher: language::LanguageMatcher {
2557                    path_suffixes: vec!["txt".to_string()],
2558                    ..Default::default()
2559                },
2560                ..Default::default()
2561            },
2562            None,
2563        ));
2564
2565        // Register the language and fake LSP
2566        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2567        language_registry.add(plain_text_language);
2568
2569        let mut fake_language_servers = language_registry.register_fake_lsp(
2570            "Plain Text",
2571            language::FakeLspAdapter {
2572                capabilities: lsp::ServerCapabilities {
2573                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2574                    ..Default::default()
2575                },
2576                ..Default::default()
2577            },
2578        );
2579
2580        // Open the buffer to trigger LSP initialization
2581        let buffer = project
2582            .update(&mut cx, |project, cx| {
2583                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2584            })
2585            .await
2586            .unwrap();
2587
2588        // Register the buffer with language servers
2589        let _handle = project.update(&mut cx, |project, cx| {
2590            project.register_buffer_with_language_servers(&buffer, cx)
2591        });
2592
2593        cx.run_until_parked();
2594
2595        let fake_language_server = fake_language_servers.next().await.unwrap();
2596        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2597            move |_, _| async move {
2598                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2599                    #[allow(deprecated)]
2600                    lsp::SymbolInformation {
2601                        name: "MySymbol".into(),
2602                        location: lsp::Location {
2603                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2604                            range: lsp::Range::new(
2605                                lsp::Position::new(0, 0),
2606                                lsp::Position::new(0, 1),
2607                            ),
2608                        },
2609                        kind: lsp::SymbolKind::CONSTANT,
2610                        tags: None,
2611                        container_name: None,
2612                        deprecated: None,
2613                    },
2614                ])))
2615            },
2616        );
2617
2618        cx.simulate_input("@symbol ");
2619
2620        editor.update(&mut cx, |editor, cx| {
2621            assert_eq!(
2622                editor.text(cx),
2623                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
2624            );
2625            assert!(editor.has_visible_completions_menu());
2626            assert_eq!(current_completion_labels(editor), &["MySymbol"]);
2627        });
2628
2629        editor.update_in(&mut cx, |editor, window, cx| {
2630            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2631        });
2632
2633        let symbol = MentionUri::Symbol {
2634            abs_path: path!("/dir/a/one.txt").into(),
2635            name: "MySymbol".into(),
2636            line_range: 0..=0,
2637        };
2638
2639        let contents = message_editor
2640            .update(&mut cx, |message_editor, cx| {
2641                message_editor.mention_set().contents(
2642                    &all_prompt_capabilities,
2643                    false,
2644                    project.clone(),
2645                    cx,
2646                )
2647            })
2648            .await
2649            .unwrap()
2650            .into_values()
2651            .collect::<Vec<_>>();
2652
2653        {
2654            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2655                panic!("Unexpected mentions");
2656            };
2657            pretty_assertions::assert_eq!(content, "1");
2658            pretty_assertions::assert_eq!(uri, &symbol);
2659        }
2660
2661        cx.run_until_parked();
2662
2663        editor.read_with(&cx, |editor, cx| {
2664            assert_eq!(
2665                editor.text(cx),
2666                format!(
2667                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2668                    symbol.to_uri(),
2669                )
2670            );
2671        });
2672
2673        // Try to mention an "image" file that will fail to load
2674        cx.simulate_input("@file x.png");
2675
2676        editor.update(&mut cx, |editor, cx| {
2677            assert_eq!(
2678                editor.text(cx),
2679                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2680            );
2681            assert!(editor.has_visible_completions_menu());
2682            assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
2683        });
2684
2685        editor.update_in(&mut cx, |editor, window, cx| {
2686            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2687        });
2688
2689        // Getting the message contents fails
2690        message_editor
2691            .update(&mut cx, |message_editor, cx| {
2692                message_editor.mention_set().contents(
2693                    &all_prompt_capabilities,
2694                    false,
2695                    project.clone(),
2696                    cx,
2697                )
2698            })
2699            .await
2700            .expect_err("Should fail to load x.png");
2701
2702        cx.run_until_parked();
2703
2704        // Mention was removed
2705        editor.read_with(&cx, |editor, cx| {
2706            assert_eq!(
2707                editor.text(cx),
2708                format!(
2709                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2710                    symbol.to_uri()
2711                )
2712            );
2713        });
2714
2715        // Once more
2716        cx.simulate_input("@file x.png");
2717
2718        editor.update(&mut cx, |editor, cx| {
2719                    assert_eq!(
2720                        editor.text(cx),
2721                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2722                    );
2723                    assert!(editor.has_visible_completions_menu());
2724                    assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
2725                });
2726
2727        editor.update_in(&mut cx, |editor, window, cx| {
2728            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2729        });
2730
2731        // This time don't immediately get the contents, just let the confirmed completion settle
2732        cx.run_until_parked();
2733
2734        // Mention was removed
2735        editor.read_with(&cx, |editor, cx| {
2736            assert_eq!(
2737                editor.text(cx),
2738                format!(
2739                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2740                    symbol.to_uri()
2741                )
2742            );
2743        });
2744
2745        // Now getting the contents succeeds, because the invalid mention was removed
2746        let contents = message_editor
2747            .update(&mut cx, |message_editor, cx| {
2748                message_editor.mention_set().contents(
2749                    &all_prompt_capabilities,
2750                    false,
2751                    project.clone(),
2752                    cx,
2753                )
2754            })
2755            .await
2756            .unwrap();
2757        assert_eq!(contents.len(), 3);
2758    }
2759
2760    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2761        let snapshot = editor.buffer().read(cx).snapshot(cx);
2762        editor.display_map.update(cx, |display_map, cx| {
2763            display_map
2764                .snapshot(cx)
2765                .folds_in_range(0..snapshot.len())
2766                .map(|fold| fold.range.to_point(&snapshot))
2767                .collect()
2768        })
2769    }
2770
2771    fn current_completion_labels(editor: &Editor) -> Vec<String> {
2772        let completions = editor.current_completions().expect("Missing completions");
2773        completions
2774            .into_iter()
2775            .map(|completion| completion.label.text)
2776            .collect::<Vec<_>>()
2777    }
2778
2779    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
2780        let completions = editor.current_completions().expect("Missing completions");
2781        completions
2782            .into_iter()
2783            .map(|completion| {
2784                (
2785                    completion.label.text,
2786                    completion
2787                        .documentation
2788                        .map(|d| d.text().to_string())
2789                        .unwrap_or_default(),
2790                )
2791            })
2792            .collect::<Vec<_>>()
2793    }
2794
2795    #[gpui::test]
2796    async fn test_large_file_mention_uses_outline(cx: &mut TestAppContext) {
2797        init_test(cx);
2798
2799        let fs = FakeFs::new(cx.executor());
2800
2801        // Create a large file that exceeds AUTO_OUTLINE_SIZE
2802        const LINE: &str = "fn example_function() { /* some code */ }\n";
2803        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
2804        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
2805
2806        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
2807        let small_content = "fn small_function() { /* small */ }\n";
2808        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
2809
2810        fs.insert_tree(
2811            "/project",
2812            json!({
2813                "large_file.rs": large_content.clone(),
2814                "small_file.rs": small_content,
2815            }),
2816        )
2817        .await;
2818
2819        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2820
2821        let (workspace, cx) =
2822            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2823
2824        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
2825        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
2826
2827        let message_editor = cx.update(|window, cx| {
2828            cx.new(|cx| {
2829                let editor = MessageEditor::new(
2830                    workspace.downgrade(),
2831                    project.clone(),
2832                    history_store.clone(),
2833                    None,
2834                    Default::default(),
2835                    Default::default(),
2836                    "Test Agent".into(),
2837                    "Test",
2838                    EditorMode::AutoHeight {
2839                        min_lines: 1,
2840                        max_lines: None,
2841                    },
2842                    window,
2843                    cx,
2844                );
2845                // Enable embedded context so files are actually included
2846                editor.prompt_capabilities.replace(acp::PromptCapabilities {
2847                    embedded_context: true,
2848                    meta: None,
2849                    ..Default::default()
2850                });
2851                editor
2852            })
2853        });
2854
2855        // Test large file mention
2856        // Get the absolute path using the project's worktree
2857        let large_file_abs_path = project.read_with(cx, |project, cx| {
2858            let worktree = project.worktrees(cx).next().unwrap();
2859            let worktree_root = worktree.read(cx).abs_path();
2860            worktree_root.join("large_file.rs")
2861        });
2862        let large_file_task = message_editor.update(cx, |editor, cx| {
2863            editor.confirm_mention_for_file(large_file_abs_path, cx)
2864        });
2865
2866        let large_file_mention = large_file_task.await.unwrap();
2867        match large_file_mention {
2868            Mention::Text { content, .. } => {
2869                // Should contain outline header for large files
2870                assert!(content.contains("File outline for"));
2871                assert!(content.contains("file too large to show full content"));
2872                // Should not contain the full repeated content
2873                assert!(!content.contains(&LINE.repeat(100)));
2874            }
2875            _ => panic!("Expected Text mention for large file"),
2876        }
2877
2878        // Test small file mention
2879        // Get the absolute path using the project's worktree
2880        let small_file_abs_path = project.read_with(cx, |project, cx| {
2881            let worktree = project.worktrees(cx).next().unwrap();
2882            let worktree_root = worktree.read(cx).abs_path();
2883            worktree_root.join("small_file.rs")
2884        });
2885        let small_file_task = message_editor.update(cx, |editor, cx| {
2886            editor.confirm_mention_for_file(small_file_abs_path, cx)
2887        });
2888
2889        let small_file_mention = small_file_task.await.unwrap();
2890        match small_file_mention {
2891            Mention::Text { content, .. } => {
2892                // Should contain the actual content
2893                assert_eq!(content, small_content);
2894                // Should not contain outline header
2895                assert!(!content.contains("File outline for"));
2896            }
2897            _ => panic!("Expected Text mention for small file"),
2898        }
2899    }
2900}