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