message_editor.rs

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