message_editor.rs

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