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