message_editor.rs

   1use crate::{
   2    ChatWithFollow,
   3    acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion},
   4    context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
   5};
   6use acp_thread::{MentionUri, selection_name};
   7use agent::{HistoryStore, outline};
   8use agent_client_protocol as acp;
   9use agent_servers::{AgentServer, AgentServerDelegate};
  10use anyhow::{Result, anyhow};
  11use assistant_slash_commands::codeblock_fence_for_path;
  12use collections::{HashMap, HashSet};
  13use editor::{
  14    Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
  15    EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
  16    MultiBuffer, ToOffset,
  17    actions::Paste,
  18    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
 721                    .char_indices()
 722                    .find(|(_, c)| !c.is_whitespace())
 723                    .unwrap_or((0, '\0'));
 724                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
 725                let text = editor.text(cx);
 726                editor.display_map.update(cx, |map, cx| {
 727                    let snapshot = map.snapshot(cx);
 728                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 729                        let Some((uri, mention)) = contents.get(&crease_id) else {
 730                            continue;
 731                        };
 732
 733                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot());
 734                        if crease_range.start > ix {
 735                            let chunk = text[ix..crease_range.start].into();
 736                            chunks.push(chunk);
 737                        }
 738                        let chunk = match mention {
 739                            Mention::Text {
 740                                content,
 741                                tracked_buffers,
 742                            } => {
 743                                all_tracked_buffers.extend(tracked_buffers.iter().cloned());
 744                                acp::ContentBlock::Resource(acp::EmbeddedResource {
 745                                    annotations: None,
 746                                    resource: acp::EmbeddedResourceResource::TextResourceContents(
 747                                        acp::TextResourceContents {
 748                                            mime_type: None,
 749                                            text: content.clone(),
 750                                            uri: uri.to_uri().to_string(),
 751                                            meta: None,
 752                                        },
 753                                    ),
 754                                    meta: None,
 755                                })
 756                            }
 757                            Mention::Image(mention_image) => {
 758                                let uri = match uri {
 759                                    MentionUri::File { .. } => Some(uri.to_uri().to_string()),
 760                                    MentionUri::PastedImage => None,
 761                                    other => {
 762                                        debug_panic!(
 763                                            "unexpected mention uri for image: {:?}",
 764                                            other
 765                                        );
 766                                        None
 767                                    }
 768                                };
 769                                acp::ContentBlock::Image(acp::ImageContent {
 770                                    annotations: None,
 771                                    data: mention_image.data.to_string(),
 772                                    mime_type: mention_image.format.mime_type().into(),
 773                                    uri,
 774                                    meta: None,
 775                                })
 776                            }
 777                            Mention::UriOnly => {
 778                                acp::ContentBlock::ResourceLink(acp::ResourceLink {
 779                                    name: uri.name(),
 780                                    uri: uri.to_uri().to_string(),
 781                                    annotations: None,
 782                                    description: None,
 783                                    mime_type: None,
 784                                    size: None,
 785                                    title: None,
 786                                    meta: None,
 787                                })
 788                            }
 789                        };
 790                        chunks.push(chunk);
 791                        ix = crease_range.end;
 792                    }
 793
 794                    if ix < text.len() {
 795                        let last_chunk = text[ix..].trim_end().to_owned();
 796                        if !last_chunk.is_empty() {
 797                            chunks.push(last_chunk.into());
 798                        }
 799                    }
 800                });
 801                Ok((chunks, all_tracked_buffers))
 802            })?;
 803            result
 804        })
 805    }
 806
 807    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 808        self.editor.update(cx, |editor, cx| {
 809            editor.clear(window, cx);
 810            editor.remove_creases(
 811                self.mention_set
 812                    .mentions
 813                    .drain()
 814                    .map(|(crease_id, _)| crease_id),
 815                cx,
 816            )
 817        });
 818    }
 819
 820    pub fn send(&mut self, cx: &mut Context<Self>) {
 821        if self.is_empty(cx) {
 822            return;
 823        }
 824        self.editor.update(cx, |editor, cx| {
 825            editor.clear_inlay_hints(cx);
 826        });
 827        cx.emit(MessageEditorEvent::Send)
 828    }
 829
 830    fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
 831        self.send(cx);
 832    }
 833
 834    fn chat_with_follow(
 835        &mut self,
 836        _: &ChatWithFollow,
 837        window: &mut Window,
 838        cx: &mut Context<Self>,
 839    ) {
 840        self.workspace
 841            .update(cx, |this, cx| {
 842                this.follow(CollaboratorId::Agent, window, cx)
 843            })
 844            .log_err();
 845
 846        self.send(cx);
 847    }
 848
 849    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 850        cx.emit(MessageEditorEvent::Cancel)
 851    }
 852
 853    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 854        if !self.prompt_capabilities.borrow().image {
 855            return;
 856        }
 857
 858        let images = cx
 859            .read_from_clipboard()
 860            .map(|item| {
 861                item.into_entries()
 862                    .filter_map(|entry| {
 863                        if let ClipboardEntry::Image(image) = entry {
 864                            Some(image)
 865                        } else {
 866                            None
 867                        }
 868                    })
 869                    .collect::<Vec<_>>()
 870            })
 871            .unwrap_or_default();
 872
 873        if images.is_empty() {
 874            return;
 875        }
 876        cx.stop_propagation();
 877
 878        let replacement_text = MentionUri::PastedImage.as_link().to_string();
 879        for image in images {
 880            let (excerpt_id, text_anchor, multibuffer_anchor) =
 881                self.editor.update(cx, |message_editor, cx| {
 882                    let snapshot = message_editor.snapshot(window, cx);
 883                    let (excerpt_id, _, buffer_snapshot) =
 884                        snapshot.buffer_snapshot().as_singleton().unwrap();
 885
 886                    let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
 887                    let multibuffer_anchor = snapshot
 888                        .buffer_snapshot()
 889                        .anchor_in_excerpt(*excerpt_id, text_anchor);
 890                    message_editor.edit(
 891                        [(
 892                            multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 893                            format!("{replacement_text} "),
 894                        )],
 895                        cx,
 896                    );
 897                    (*excerpt_id, text_anchor, multibuffer_anchor)
 898                });
 899
 900            let content_len = replacement_text.len();
 901            let Some(start_anchor) = multibuffer_anchor else {
 902                continue;
 903            };
 904            let end_anchor = self.editor.update(cx, |editor, cx| {
 905                let snapshot = editor.buffer().read(cx).snapshot(cx);
 906                snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
 907            });
 908            let image = Arc::new(image);
 909            let Some((crease_id, tx)) = insert_crease_for_mention(
 910                excerpt_id,
 911                text_anchor,
 912                content_len,
 913                MentionUri::PastedImage.name().into(),
 914                IconName::Image.path().into(),
 915                Some(Task::ready(Ok(image.clone())).shared()),
 916                self.editor.clone(),
 917                window,
 918                cx,
 919            ) else {
 920                continue;
 921            };
 922            let task = cx
 923                .spawn_in(window, {
 924                    async move |_, cx| {
 925                        let format = image.format;
 926                        let image = cx
 927                            .update(|_, cx| LanguageModelImage::from_image(image, cx))
 928                            .map_err(|e| e.to_string())?
 929                            .await;
 930                        drop(tx);
 931                        if let Some(image) = image {
 932                            Ok(Mention::Image(MentionImage {
 933                                data: image.source,
 934                                format,
 935                            }))
 936                        } else {
 937                            Err("Failed to convert image".into())
 938                        }
 939                    }
 940                })
 941                .shared();
 942
 943            self.mention_set
 944                .mentions
 945                .insert(crease_id, (MentionUri::PastedImage, task.clone()));
 946
 947            cx.spawn_in(window, async move |this, cx| {
 948                if task.await.notify_async_err(cx).is_none() {
 949                    this.update(cx, |this, cx| {
 950                        this.editor.update(cx, |editor, cx| {
 951                            editor.edit([(start_anchor..end_anchor, "")], cx);
 952                        });
 953                        this.mention_set.mentions.remove(&crease_id);
 954                    })
 955                    .ok();
 956                }
 957            })
 958            .detach();
 959        }
 960    }
 961
 962    pub fn insert_dragged_files(
 963        &mut self,
 964        paths: Vec<project::ProjectPath>,
 965        added_worktrees: Vec<Entity<Worktree>>,
 966        window: &mut Window,
 967        cx: &mut Context<Self>,
 968    ) {
 969        let path_style = self.project.read(cx).path_style(cx);
 970        let buffer = self.editor.read(cx).buffer().clone();
 971        let Some(buffer) = buffer.read(cx).as_singleton() else {
 972            return;
 973        };
 974        let mut tasks = Vec::new();
 975        for path in paths {
 976            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
 977                continue;
 978            };
 979            let Some(worktree) = self.project.read(cx).worktree_for_id(path.worktree_id, cx) else {
 980                continue;
 981            };
 982            let abs_path = worktree.read(cx).absolutize(&path.path);
 983            let (file_name, _) =
 984                crate::context_picker::file_context_picker::extract_file_name_and_directory(
 985                    &path.path,
 986                    worktree.read(cx).root_name(),
 987                    path_style,
 988                );
 989
 990            let uri = if entry.is_dir() {
 991                MentionUri::Directory { abs_path }
 992            } else {
 993                MentionUri::File { abs_path }
 994            };
 995
 996            let new_text = format!("{} ", uri.as_link());
 997            let content_len = new_text.len() - 1;
 998
 999            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
1000
1001            self.editor.update(cx, |message_editor, cx| {
1002                message_editor.edit(
1003                    [(
1004                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
1005                        new_text,
1006                    )],
1007                    cx,
1008                );
1009            });
1010            tasks.push(self.confirm_mention_completion(
1011                file_name,
1012                anchor,
1013                content_len,
1014                uri,
1015                window,
1016                cx,
1017            ));
1018        }
1019        cx.spawn(async move |_, _| {
1020            join_all(tasks).await;
1021            drop(added_worktrees);
1022        })
1023        .detach();
1024    }
1025
1026    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1027        let editor = self.editor.read(cx);
1028        let editor_buffer = editor.buffer().read(cx);
1029        let Some(buffer) = editor_buffer.as_singleton() else {
1030            return;
1031        };
1032        let cursor_anchor = editor.selections.newest_anchor().head();
1033        let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
1034        let anchor = buffer.update(cx, |buffer, _cx| {
1035            buffer.anchor_before(cursor_offset.min(buffer.len()))
1036        });
1037        let Some(workspace) = self.workspace.upgrade() else {
1038            return;
1039        };
1040        let Some(completion) = ContextPickerCompletionProvider::completion_for_action(
1041            ContextPickerAction::AddSelections,
1042            anchor..anchor,
1043            cx.weak_entity(),
1044            &workspace,
1045            cx,
1046        ) else {
1047            return;
1048        };
1049
1050        self.editor.update(cx, |message_editor, cx| {
1051            message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
1052            message_editor.request_autoscroll(Autoscroll::fit(), cx);
1053        });
1054        if let Some(confirm) = completion.confirm {
1055            confirm(CompletionIntent::Complete, window, cx);
1056        }
1057    }
1058
1059    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
1060        self.editor.update(cx, |message_editor, cx| {
1061            message_editor.set_read_only(read_only);
1062            cx.notify()
1063        })
1064    }
1065
1066    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
1067        self.editor.update(cx, |editor, cx| {
1068            editor.set_mode(mode);
1069            cx.notify()
1070        });
1071    }
1072
1073    pub fn set_message(
1074        &mut self,
1075        message: Vec<acp::ContentBlock>,
1076        window: &mut Window,
1077        cx: &mut Context<Self>,
1078    ) {
1079        self.clear(window, cx);
1080
1081        let path_style = self.project.read(cx).path_style(cx);
1082        let mut text = String::new();
1083        let mut mentions = Vec::new();
1084
1085        for chunk in message {
1086            match chunk {
1087                acp::ContentBlock::Text(text_content) => {
1088                    text.push_str(&text_content.text);
1089                }
1090                acp::ContentBlock::Resource(acp::EmbeddedResource {
1091                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
1092                    ..
1093                }) => {
1094                    let Some(mention_uri) = MentionUri::parse(&resource.uri, path_style).log_err()
1095                    else {
1096                        continue;
1097                    };
1098                    let start = text.len();
1099                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1100                    let end = text.len();
1101                    mentions.push((
1102                        start..end,
1103                        mention_uri,
1104                        Mention::Text {
1105                            content: resource.text,
1106                            tracked_buffers: Vec::new(),
1107                        },
1108                    ));
1109                }
1110                acp::ContentBlock::ResourceLink(resource) => {
1111                    if let Some(mention_uri) =
1112                        MentionUri::parse(&resource.uri, path_style).log_err()
1113                    {
1114                        let start = text.len();
1115                        write!(&mut text, "{}", mention_uri.as_link()).ok();
1116                        let end = text.len();
1117                        mentions.push((start..end, mention_uri, Mention::UriOnly));
1118                    }
1119                }
1120                acp::ContentBlock::Image(acp::ImageContent {
1121                    uri,
1122                    data,
1123                    mime_type,
1124                    annotations: _,
1125                    meta: _,
1126                }) => {
1127                    let mention_uri = if let Some(uri) = uri {
1128                        MentionUri::parse(&uri, path_style)
1129                    } else {
1130                        Ok(MentionUri::PastedImage)
1131                    };
1132                    let Some(mention_uri) = mention_uri.log_err() else {
1133                        continue;
1134                    };
1135                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
1136                        log::error!("failed to parse MIME type for image: {mime_type:?}");
1137                        continue;
1138                    };
1139                    let start = text.len();
1140                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1141                    let end = text.len();
1142                    mentions.push((
1143                        start..end,
1144                        mention_uri,
1145                        Mention::Image(MentionImage {
1146                            data: data.into(),
1147                            format,
1148                        }),
1149                    ));
1150                }
1151                acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {}
1152            }
1153        }
1154
1155        let snapshot = self.editor.update(cx, |editor, cx| {
1156            editor.set_text(text, window, cx);
1157            editor.buffer().read(cx).snapshot(cx)
1158        });
1159
1160        for (range, mention_uri, mention) in mentions {
1161            let anchor = snapshot.anchor_before(range.start);
1162            let Some((crease_id, tx)) = insert_crease_for_mention(
1163                anchor.excerpt_id,
1164                anchor.text_anchor,
1165                range.end - range.start,
1166                mention_uri.name().into(),
1167                mention_uri.icon_path(cx),
1168                None,
1169                self.editor.clone(),
1170                window,
1171                cx,
1172            ) else {
1173                continue;
1174            };
1175            drop(tx);
1176
1177            self.mention_set.mentions.insert(
1178                crease_id,
1179                (mention_uri.clone(), Task::ready(Ok(mention)).shared()),
1180            );
1181        }
1182        cx.notify();
1183    }
1184
1185    pub fn text(&self, cx: &App) -> String {
1186        self.editor.read(cx).text(cx)
1187    }
1188
1189    #[cfg(test)]
1190    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1191        self.editor.update(cx, |editor, cx| {
1192            editor.set_text(text, window, cx);
1193        });
1194    }
1195}
1196
1197fn full_mention_for_directory(
1198    project: &Entity<Project>,
1199    abs_path: &Path,
1200    cx: &mut App,
1201) -> Task<Result<Mention>> {
1202    fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
1203        let mut files = Vec::new();
1204
1205        for entry in worktree.child_entries(path) {
1206            if entry.is_dir() {
1207                files.extend(collect_files_in_path(worktree, &entry.path));
1208            } else if entry.is_file() {
1209                files.push((
1210                    entry.path.clone(),
1211                    worktree
1212                        .full_path(&entry.path)
1213                        .to_string_lossy()
1214                        .to_string(),
1215                ));
1216            }
1217        }
1218
1219        files
1220    }
1221
1222    let Some(project_path) = project
1223        .read(cx)
1224        .project_path_for_absolute_path(&abs_path, cx)
1225    else {
1226        return Task::ready(Err(anyhow!("project path not found")));
1227    };
1228    let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
1229        return Task::ready(Err(anyhow!("project entry not found")));
1230    };
1231    let directory_path = entry.path.clone();
1232    let worktree_id = project_path.worktree_id;
1233    let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
1234        return Task::ready(Err(anyhow!("worktree not found")));
1235    };
1236    let project = project.clone();
1237    cx.spawn(async move |cx| {
1238        let file_paths = worktree.read_with(cx, |worktree, _cx| {
1239            collect_files_in_path(worktree, &directory_path)
1240        })?;
1241        let descendants_future = cx.update(|cx| {
1242            join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
1243                let rel_path = worktree_path
1244                    .strip_prefix(&directory_path)
1245                    .log_err()
1246                    .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
1247
1248                let open_task = project.update(cx, |project, cx| {
1249                    project.buffer_store().update(cx, |buffer_store, cx| {
1250                        let project_path = ProjectPath {
1251                            worktree_id,
1252                            path: worktree_path,
1253                        };
1254                        buffer_store.open_buffer(project_path, cx)
1255                    })
1256                });
1257
1258                cx.spawn(async move |cx| {
1259                    let buffer = open_task.await.log_err()?;
1260                    let buffer_content = outline::get_buffer_content_or_outline(
1261                        buffer.clone(),
1262                        Some(&full_path),
1263                        &cx,
1264                    )
1265                    .await
1266                    .ok()?;
1267
1268                    Some((rel_path, full_path, buffer_content.text, buffer))
1269                })
1270            }))
1271        })?;
1272
1273        let contents = cx
1274            .background_spawn(async move {
1275                let (contents, tracked_buffers) = descendants_future
1276                    .await
1277                    .into_iter()
1278                    .flatten()
1279                    .map(|(rel_path, full_path, rope, buffer)| {
1280                        ((rel_path, full_path, rope), buffer)
1281                    })
1282                    .unzip();
1283                Mention::Text {
1284                    content: render_directory_contents(contents),
1285                    tracked_buffers,
1286                }
1287            })
1288            .await;
1289        anyhow::Ok(contents)
1290    })
1291}
1292
1293fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
1294    let mut output = String::new();
1295    for (_relative_path, full_path, content) in entries {
1296        let fence = codeblock_fence_for_path(Some(&full_path), None);
1297        write!(output, "\n{fence}\n{content}\n```").unwrap();
1298    }
1299    output
1300}
1301
1302impl Focusable for MessageEditor {
1303    fn focus_handle(&self, cx: &App) -> FocusHandle {
1304        self.editor.focus_handle(cx)
1305    }
1306}
1307
1308impl Render for MessageEditor {
1309    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1310        div()
1311            .key_context("MessageEditor")
1312            .on_action(cx.listener(Self::chat))
1313            .on_action(cx.listener(Self::chat_with_follow))
1314            .on_action(cx.listener(Self::cancel))
1315            .capture_action(cx.listener(Self::paste))
1316            .flex_1()
1317            .child({
1318                let settings = ThemeSettings::get_global(cx);
1319
1320                let text_style = TextStyle {
1321                    color: cx.theme().colors().text,
1322                    font_family: settings.buffer_font.family.clone(),
1323                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
1324                    font_features: settings.buffer_font.features.clone(),
1325                    font_size: settings.agent_buffer_font_size(cx).into(),
1326                    line_height: relative(settings.buffer_line_height.value()),
1327                    ..Default::default()
1328                };
1329
1330                EditorElement::new(
1331                    &self.editor,
1332                    EditorStyle {
1333                        background: cx.theme().colors().editor_background,
1334                        local_player: cx.theme().players().local(),
1335                        text: text_style,
1336                        syntax: cx.theme().syntax().clone(),
1337                        inlay_hints_style: editor::make_inlay_hints_style(cx),
1338                        ..Default::default()
1339                    },
1340                )
1341            })
1342    }
1343}
1344
1345pub(crate) fn insert_crease_for_mention(
1346    excerpt_id: ExcerptId,
1347    anchor: text::Anchor,
1348    content_len: usize,
1349    crease_label: SharedString,
1350    crease_icon: SharedString,
1351    // abs_path: Option<Arc<Path>>,
1352    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1353    editor: Entity<Editor>,
1354    window: &mut Window,
1355    cx: &mut App,
1356) -> Option<(CreaseId, postage::barrier::Sender)> {
1357    let (tx, rx) = postage::barrier::channel();
1358
1359    let crease_id = editor.update(cx, |editor, cx| {
1360        let snapshot = editor.buffer().read(cx).snapshot(cx);
1361
1362        let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
1363
1364        let start = start.bias_right(&snapshot);
1365        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
1366
1367        let placeholder = FoldPlaceholder {
1368            render: render_mention_fold_button(
1369                crease_label,
1370                crease_icon,
1371                start..end,
1372                rx,
1373                image,
1374                cx.weak_entity(),
1375                cx,
1376            ),
1377            merge_adjacent: false,
1378            ..Default::default()
1379        };
1380
1381        let crease = Crease::Inline {
1382            range: start..end,
1383            placeholder,
1384            render_toggle: None,
1385            render_trailer: None,
1386            metadata: None,
1387        };
1388
1389        let ids = editor.insert_creases(vec![crease.clone()], cx);
1390        editor.fold_creases(vec![crease], false, window, cx);
1391
1392        Some(ids[0])
1393    })?;
1394
1395    Some((crease_id, tx))
1396}
1397
1398fn render_mention_fold_button(
1399    label: SharedString,
1400    icon: SharedString,
1401    range: Range<Anchor>,
1402    mut loading_finished: postage::barrier::Receiver,
1403    image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1404    editor: WeakEntity<Editor>,
1405    cx: &mut App,
1406) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
1407    let loading = cx.new(|cx| {
1408        let loading = cx.spawn(async move |this, cx| {
1409            loading_finished.recv().await;
1410            this.update(cx, |this: &mut LoadingContext, cx| {
1411                this.loading = None;
1412                cx.notify();
1413            })
1414            .ok();
1415        });
1416        LoadingContext {
1417            id: cx.entity_id(),
1418            label,
1419            icon,
1420            range,
1421            editor,
1422            loading: Some(loading),
1423            image: image_task.clone(),
1424        }
1425    });
1426    Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
1427}
1428
1429struct LoadingContext {
1430    id: EntityId,
1431    label: SharedString,
1432    icon: SharedString,
1433    range: Range<Anchor>,
1434    editor: WeakEntity<Editor>,
1435    loading: Option<Task<()>>,
1436    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1437}
1438
1439impl Render for LoadingContext {
1440    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1441        let is_in_text_selection = self
1442            .editor
1443            .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
1444            .unwrap_or_default();
1445        ButtonLike::new(("loading-context", self.id))
1446            .style(ButtonStyle::Filled)
1447            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1448            .toggle_state(is_in_text_selection)
1449            .when_some(self.image.clone(), |el, image_task| {
1450                el.hoverable_tooltip(move |_, cx| {
1451                    let image = image_task.peek().cloned().transpose().ok().flatten();
1452                    let image_task = image_task.clone();
1453                    cx.new::<ImageHover>(|cx| ImageHover {
1454                        image,
1455                        _task: cx.spawn(async move |this, cx| {
1456                            if let Ok(image) = image_task.clone().await {
1457                                this.update(cx, |this, cx| {
1458                                    if this.image.replace(image).is_none() {
1459                                        cx.notify();
1460                                    }
1461                                })
1462                                .ok();
1463                            }
1464                        }),
1465                    })
1466                    .into()
1467                })
1468            })
1469            .child(
1470                h_flex()
1471                    .gap_1()
1472                    .child(
1473                        Icon::from_path(self.icon.clone())
1474                            .size(IconSize::XSmall)
1475                            .color(Color::Muted),
1476                    )
1477                    .child(
1478                        Label::new(self.label.clone())
1479                            .size(LabelSize::Small)
1480                            .buffer_font(cx)
1481                            .single_line(),
1482                    )
1483                    .map(|el| {
1484                        if self.loading.is_some() {
1485                            el.with_animation(
1486                                "loading-context-crease",
1487                                Animation::new(Duration::from_secs(2))
1488                                    .repeat()
1489                                    .with_easing(pulsating_between(0.4, 0.8)),
1490                                |label, delta| label.opacity(delta),
1491                            )
1492                            .into_any()
1493                        } else {
1494                            el.into_any()
1495                        }
1496                    }),
1497            )
1498    }
1499}
1500
1501struct ImageHover {
1502    image: Option<Arc<Image>>,
1503    _task: Task<()>,
1504}
1505
1506impl Render for ImageHover {
1507    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1508        if let Some(image) = self.image.clone() {
1509            gpui::img(image).max_w_96().max_h_96().into_any_element()
1510        } else {
1511            gpui::Empty.into_any_element()
1512        }
1513    }
1514}
1515
1516#[derive(Debug, Clone, Eq, PartialEq)]
1517pub enum Mention {
1518    Text {
1519        content: String,
1520        tracked_buffers: Vec<Entity<Buffer>>,
1521    },
1522    Image(MentionImage),
1523    UriOnly,
1524}
1525
1526#[derive(Clone, Debug, Eq, PartialEq)]
1527pub struct MentionImage {
1528    pub data: SharedString,
1529    pub format: ImageFormat,
1530}
1531
1532#[derive(Default)]
1533pub struct MentionSet {
1534    mentions: HashMap<CreaseId, (MentionUri, Shared<Task<Result<Mention, String>>>)>,
1535}
1536
1537impl MentionSet {
1538    fn contents(
1539        &self,
1540        prompt_capabilities: &acp::PromptCapabilities,
1541        full_mention_content: bool,
1542        project: Entity<Project>,
1543        cx: &mut App,
1544    ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
1545        if !prompt_capabilities.embedded_context {
1546            let mentions = self
1547                .mentions
1548                .iter()
1549                .map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly)))
1550                .collect();
1551
1552            return Task::ready(Ok(mentions));
1553        }
1554
1555        let mentions = self.mentions.clone();
1556        cx.spawn(async move |cx| {
1557            let mut contents = HashMap::default();
1558            for (crease_id, (mention_uri, task)) in mentions {
1559                let content = if full_mention_content
1560                    && let MentionUri::Directory { abs_path } = &mention_uri
1561                {
1562                    cx.update(|cx| full_mention_for_directory(&project, abs_path, cx))?
1563                        .await?
1564                } else {
1565                    task.await.map_err(|e| anyhow!("{e}"))?
1566                };
1567
1568                contents.insert(crease_id, (mention_uri, content));
1569            }
1570            Ok(contents)
1571        })
1572    }
1573
1574    fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
1575        for (crease_id, crease) in snapshot.crease_snapshot.creases() {
1576            if !crease.range().start.is_valid(&snapshot.buffer_snapshot()) {
1577                self.mentions.remove(&crease_id);
1578            }
1579        }
1580    }
1581}
1582
1583pub struct MessageEditorAddon {}
1584
1585impl MessageEditorAddon {
1586    pub fn new() -> Self {
1587        Self {}
1588    }
1589}
1590
1591impl Addon for MessageEditorAddon {
1592    fn to_any(&self) -> &dyn std::any::Any {
1593        self
1594    }
1595
1596    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1597        Some(self)
1598    }
1599
1600    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1601        let settings = agent_settings::AgentSettings::get_global(cx);
1602        if settings.use_modifier_to_send {
1603            key_context.add("use_modifier_to_send");
1604        }
1605    }
1606}
1607
1608#[cfg(test)]
1609mod tests {
1610    use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
1611
1612    use acp_thread::MentionUri;
1613    use agent::{HistoryStore, outline};
1614    use agent_client_protocol as acp;
1615    use assistant_text_thread::TextThreadStore;
1616    use editor::{AnchorRangeExt as _, Editor, EditorMode};
1617    use fs::FakeFs;
1618    use futures::StreamExt as _;
1619    use gpui::{
1620        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1621    };
1622    use language_model::LanguageModelRegistry;
1623    use lsp::{CompletionContext, CompletionTriggerKind};
1624    use project::{CompletionIntent, Project, ProjectPath};
1625    use serde_json::json;
1626    use text::Point;
1627    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1628    use util::{path, paths::PathStyle, rel_path::rel_path};
1629    use workspace::{AppState, Item, Workspace};
1630
1631    use crate::acp::{
1632        message_editor::{Mention, MessageEditor},
1633        thread_view::tests::init_test,
1634    };
1635
1636    #[gpui::test]
1637    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1638        init_test(cx);
1639
1640        let fs = FakeFs::new(cx.executor());
1641        fs.insert_tree("/project", json!({"file": ""})).await;
1642        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1643
1644        let (workspace, cx) =
1645            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1646
1647        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1648        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
1649
1650        let message_editor = cx.update(|window, cx| {
1651            cx.new(|cx| {
1652                MessageEditor::new(
1653                    workspace.downgrade(),
1654                    project.clone(),
1655                    history_store.clone(),
1656                    None,
1657                    Default::default(),
1658                    Default::default(),
1659                    "Test Agent".into(),
1660                    "Test",
1661                    EditorMode::AutoHeight {
1662                        min_lines: 1,
1663                        max_lines: None,
1664                    },
1665                    window,
1666                    cx,
1667                )
1668            })
1669        });
1670        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1671
1672        cx.run_until_parked();
1673
1674        let excerpt_id = editor.update(cx, |editor, cx| {
1675            editor
1676                .buffer()
1677                .read(cx)
1678                .excerpt_ids()
1679                .into_iter()
1680                .next()
1681                .unwrap()
1682        });
1683        let completions = editor.update_in(cx, |editor, window, cx| {
1684            editor.set_text("Hello @file ", window, cx);
1685            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1686            let completion_provider = editor.completion_provider().unwrap();
1687            completion_provider.completions(
1688                excerpt_id,
1689                &buffer,
1690                text::Anchor::MAX,
1691                CompletionContext {
1692                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1693                    trigger_character: Some("@".into()),
1694                },
1695                window,
1696                cx,
1697            )
1698        });
1699        let [_, completion]: [_; 2] = completions
1700            .await
1701            .unwrap()
1702            .into_iter()
1703            .flat_map(|response| response.completions)
1704            .collect::<Vec<_>>()
1705            .try_into()
1706            .unwrap();
1707
1708        editor.update_in(cx, |editor, window, cx| {
1709            let snapshot = editor.buffer().read(cx).snapshot(cx);
1710            let range = snapshot
1711                .anchor_range_in_excerpt(excerpt_id, completion.replace_range)
1712                .unwrap();
1713            editor.edit([(range, completion.new_text)], cx);
1714            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1715        });
1716
1717        cx.run_until_parked();
1718
1719        // Backspace over the inserted crease (and the following space).
1720        editor.update_in(cx, |editor, window, cx| {
1721            editor.backspace(&Default::default(), window, cx);
1722            editor.backspace(&Default::default(), window, cx);
1723        });
1724
1725        let (content, _) = message_editor
1726            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1727            .await
1728            .unwrap();
1729
1730        // We don't send a resource link for the deleted crease.
1731        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1732    }
1733
1734    #[gpui::test]
1735    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
1736        init_test(cx);
1737        let fs = FakeFs::new(cx.executor());
1738        fs.insert_tree(
1739            "/test",
1740            json!({
1741                ".zed": {
1742                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
1743                },
1744                "src": {
1745                    "main.rs": "fn main() {}",
1746                },
1747            }),
1748        )
1749        .await;
1750
1751        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1752        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1753        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
1754        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1755        // Start with no available commands - simulating Claude which doesn't support slash commands
1756        let available_commands = Rc::new(RefCell::new(vec![]));
1757
1758        let (workspace, cx) =
1759            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1760        let workspace_handle = workspace.downgrade();
1761        let message_editor = workspace.update_in(cx, |_, window, cx| {
1762            cx.new(|cx| {
1763                MessageEditor::new(
1764                    workspace_handle.clone(),
1765                    project.clone(),
1766                    history_store.clone(),
1767                    None,
1768                    prompt_capabilities.clone(),
1769                    available_commands.clone(),
1770                    "Claude Code".into(),
1771                    "Test",
1772                    EditorMode::AutoHeight {
1773                        min_lines: 1,
1774                        max_lines: None,
1775                    },
1776                    window,
1777                    cx,
1778                )
1779            })
1780        });
1781        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1782
1783        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
1784        editor.update_in(cx, |editor, window, cx| {
1785            editor.set_text("/file test.txt", window, cx);
1786        });
1787
1788        let contents_result = message_editor
1789            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1790            .await;
1791
1792        // Should fail because available_commands is empty (no commands supported)
1793        assert!(contents_result.is_err());
1794        let error_message = contents_result.unwrap_err().to_string();
1795        assert!(error_message.contains("not supported by Claude Code"));
1796        assert!(error_message.contains("Available commands: none"));
1797
1798        // Now simulate Claude providing its list of available commands (which doesn't include file)
1799        available_commands.replace(vec![acp::AvailableCommand {
1800            name: "help".to_string(),
1801            description: "Get help".to_string(),
1802            input: None,
1803            meta: None,
1804        }]);
1805
1806        // Test that unsupported slash commands trigger an error when we have a list of available commands
1807        editor.update_in(cx, |editor, window, cx| {
1808            editor.set_text("/file test.txt", window, cx);
1809        });
1810
1811        let contents_result = message_editor
1812            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1813            .await;
1814
1815        assert!(contents_result.is_err());
1816        let error_message = contents_result.unwrap_err().to_string();
1817        assert!(error_message.contains("not supported by Claude Code"));
1818        assert!(error_message.contains("/file"));
1819        assert!(error_message.contains("Available commands: /help"));
1820
1821        // Test that supported commands work fine
1822        editor.update_in(cx, |editor, window, cx| {
1823            editor.set_text("/help", window, cx);
1824        });
1825
1826        let contents_result = message_editor
1827            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1828            .await;
1829
1830        // Should succeed because /help is in available_commands
1831        assert!(contents_result.is_ok());
1832
1833        // Test that regular text works fine
1834        editor.update_in(cx, |editor, window, cx| {
1835            editor.set_text("Hello Claude!", window, cx);
1836        });
1837
1838        let (content, _) = message_editor
1839            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1840            .await
1841            .unwrap();
1842
1843        assert_eq!(content.len(), 1);
1844        if let acp::ContentBlock::Text(text) = &content[0] {
1845            assert_eq!(text.text, "Hello Claude!");
1846        } else {
1847            panic!("Expected ContentBlock::Text");
1848        }
1849
1850        // Test that @ mentions still work
1851        editor.update_in(cx, |editor, window, cx| {
1852            editor.set_text("Check this @", window, cx);
1853        });
1854
1855        // The @ mention functionality should not be affected
1856        let (content, _) = message_editor
1857            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1858            .await
1859            .unwrap();
1860
1861        assert_eq!(content.len(), 1);
1862        if let acp::ContentBlock::Text(text) = &content[0] {
1863            assert_eq!(text.text, "Check this @");
1864        } else {
1865            panic!("Expected ContentBlock::Text");
1866        }
1867    }
1868
1869    struct MessageEditorItem(Entity<MessageEditor>);
1870
1871    impl Item for MessageEditorItem {
1872        type Event = ();
1873
1874        fn include_in_nav_history() -> bool {
1875            false
1876        }
1877
1878        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1879            "Test".into()
1880        }
1881    }
1882
1883    impl EventEmitter<()> for MessageEditorItem {}
1884
1885    impl Focusable for MessageEditorItem {
1886        fn focus_handle(&self, cx: &App) -> FocusHandle {
1887            self.0.read(cx).focus_handle(cx)
1888        }
1889    }
1890
1891    impl Render for MessageEditorItem {
1892        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1893            self.0.clone().into_any_element()
1894        }
1895    }
1896
1897    #[gpui::test]
1898    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
1899        init_test(cx);
1900
1901        let app_state = cx.update(AppState::test);
1902
1903        cx.update(|cx| {
1904            language::init(cx);
1905            editor::init(cx);
1906            workspace::init(app_state.clone(), cx);
1907            Project::init_settings(cx);
1908        });
1909
1910        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1911        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1912        let workspace = window.root(cx).unwrap();
1913
1914        let mut cx = VisualTestContext::from_window(*window, cx);
1915
1916        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1917        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
1918        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1919        let available_commands = Rc::new(RefCell::new(vec![
1920            acp::AvailableCommand {
1921                name: "quick-math".to_string(),
1922                description: "2 + 2 = 4 - 1 = 3".to_string(),
1923                input: None,
1924                meta: None,
1925            },
1926            acp::AvailableCommand {
1927                name: "say-hello".to_string(),
1928                description: "Say hello to whoever you want".to_string(),
1929                input: Some(acp::AvailableCommandInput::Unstructured {
1930                    hint: "<name>".to_string(),
1931                }),
1932                meta: None,
1933            },
1934        ]));
1935
1936        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
1937            let workspace_handle = cx.weak_entity();
1938            let message_editor = cx.new(|cx| {
1939                MessageEditor::new(
1940                    workspace_handle,
1941                    project.clone(),
1942                    history_store.clone(),
1943                    None,
1944                    prompt_capabilities.clone(),
1945                    available_commands.clone(),
1946                    "Test Agent".into(),
1947                    "Test",
1948                    EditorMode::AutoHeight {
1949                        max_lines: None,
1950                        min_lines: 1,
1951                    },
1952                    window,
1953                    cx,
1954                )
1955            });
1956            workspace.active_pane().update(cx, |pane, cx| {
1957                pane.add_item(
1958                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1959                    true,
1960                    true,
1961                    None,
1962                    window,
1963                    cx,
1964                );
1965            });
1966            message_editor.read(cx).focus_handle(cx).focus(window);
1967            message_editor.read(cx).editor().clone()
1968        });
1969
1970        cx.simulate_input("/");
1971
1972        editor.update_in(&mut cx, |editor, window, cx| {
1973            assert_eq!(editor.text(cx), "/");
1974            assert!(editor.has_visible_completions_menu());
1975
1976            assert_eq!(
1977                current_completion_labels_with_documentation(editor),
1978                &[
1979                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
1980                    ("say-hello".into(), "Say hello to whoever you want".into())
1981                ]
1982            );
1983            editor.set_text("", window, cx);
1984        });
1985
1986        cx.simulate_input("/qui");
1987
1988        editor.update_in(&mut cx, |editor, window, cx| {
1989            assert_eq!(editor.text(cx), "/qui");
1990            assert!(editor.has_visible_completions_menu());
1991
1992            assert_eq!(
1993                current_completion_labels_with_documentation(editor),
1994                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
1995            );
1996            editor.set_text("", window, cx);
1997        });
1998
1999        editor.update_in(&mut cx, |editor, window, cx| {
2000            assert!(editor.has_visible_completions_menu());
2001            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2002        });
2003
2004        cx.run_until_parked();
2005
2006        editor.update_in(&mut cx, |editor, window, cx| {
2007            assert_eq!(editor.display_text(cx), "/quick-math ");
2008            assert!(!editor.has_visible_completions_menu());
2009            editor.set_text("", window, cx);
2010        });
2011
2012        cx.simulate_input("/say");
2013
2014        editor.update_in(&mut cx, |editor, _window, cx| {
2015            assert_eq!(editor.display_text(cx), "/say");
2016            assert!(editor.has_visible_completions_menu());
2017
2018            assert_eq!(
2019                current_completion_labels_with_documentation(editor),
2020                &[("say-hello".into(), "Say hello to whoever you want".into())]
2021            );
2022        });
2023
2024        editor.update_in(&mut cx, |editor, window, cx| {
2025            assert!(editor.has_visible_completions_menu());
2026            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2027        });
2028
2029        cx.run_until_parked();
2030
2031        editor.update_in(&mut cx, |editor, _window, cx| {
2032            assert_eq!(editor.text(cx), "/say-hello ");
2033            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2034            assert!(!editor.has_visible_completions_menu());
2035        });
2036
2037        cx.simulate_input("GPT5");
2038
2039        cx.run_until_parked();
2040
2041        editor.update_in(&mut cx, |editor, window, cx| {
2042            assert_eq!(editor.text(cx), "/say-hello GPT5");
2043            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
2044            assert!(!editor.has_visible_completions_menu());
2045
2046            // Delete argument
2047            for _ in 0..5 {
2048                editor.backspace(&editor::actions::Backspace, window, cx);
2049            }
2050        });
2051
2052        cx.run_until_parked();
2053
2054        editor.update_in(&mut cx, |editor, window, cx| {
2055            assert_eq!(editor.text(cx), "/say-hello");
2056            // Hint is visible because argument was deleted
2057            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2058
2059            // Delete last command letter
2060            editor.backspace(&editor::actions::Backspace, window, cx);
2061        });
2062
2063        cx.run_until_parked();
2064
2065        editor.update_in(&mut cx, |editor, _window, cx| {
2066            // Hint goes away once command no longer matches an available one
2067            assert_eq!(editor.text(cx), "/say-hell");
2068            assert_eq!(editor.display_text(cx), "/say-hell");
2069            assert!(!editor.has_visible_completions_menu());
2070        });
2071    }
2072
2073    #[gpui::test]
2074    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
2075        init_test(cx);
2076
2077        let app_state = cx.update(AppState::test);
2078
2079        cx.update(|cx| {
2080            language::init(cx);
2081            editor::init(cx);
2082            workspace::init(app_state.clone(), cx);
2083            Project::init_settings(cx);
2084        });
2085
2086        app_state
2087            .fs
2088            .as_fake()
2089            .insert_tree(
2090                path!("/dir"),
2091                json!({
2092                    "editor": "",
2093                    "a": {
2094                        "one.txt": "1",
2095                        "two.txt": "2",
2096                        "three.txt": "3",
2097                        "four.txt": "4"
2098                    },
2099                    "b": {
2100                        "five.txt": "5",
2101                        "six.txt": "6",
2102                        "seven.txt": "7",
2103                        "eight.txt": "8",
2104                    },
2105                    "x.png": "",
2106                }),
2107            )
2108            .await;
2109
2110        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2111        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2112        let workspace = window.root(cx).unwrap();
2113
2114        let worktree = project.update(cx, |project, cx| {
2115            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2116            assert_eq!(worktrees.len(), 1);
2117            worktrees.pop().unwrap()
2118        });
2119        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2120
2121        let mut cx = VisualTestContext::from_window(*window, cx);
2122
2123        let paths = vec![
2124            rel_path("a/one.txt"),
2125            rel_path("a/two.txt"),
2126            rel_path("a/three.txt"),
2127            rel_path("a/four.txt"),
2128            rel_path("b/five.txt"),
2129            rel_path("b/six.txt"),
2130            rel_path("b/seven.txt"),
2131            rel_path("b/eight.txt"),
2132        ];
2133
2134        let slash = PathStyle::local().separator();
2135
2136        let mut opened_editors = Vec::new();
2137        for path in paths {
2138            let buffer = workspace
2139                .update_in(&mut cx, |workspace, window, cx| {
2140                    workspace.open_path(
2141                        ProjectPath {
2142                            worktree_id,
2143                            path: path.into(),
2144                        },
2145                        None,
2146                        false,
2147                        window,
2148                        cx,
2149                    )
2150                })
2151                .await
2152                .unwrap();
2153            opened_editors.push(buffer);
2154        }
2155
2156        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2157        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2158        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
2159
2160        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
2161            let workspace_handle = cx.weak_entity();
2162            let message_editor = cx.new(|cx| {
2163                MessageEditor::new(
2164                    workspace_handle,
2165                    project.clone(),
2166                    history_store.clone(),
2167                    None,
2168                    prompt_capabilities.clone(),
2169                    Default::default(),
2170                    "Test Agent".into(),
2171                    "Test",
2172                    EditorMode::AutoHeight {
2173                        max_lines: None,
2174                        min_lines: 1,
2175                    },
2176                    window,
2177                    cx,
2178                )
2179            });
2180            workspace.active_pane().update(cx, |pane, cx| {
2181                pane.add_item(
2182                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2183                    true,
2184                    true,
2185                    None,
2186                    window,
2187                    cx,
2188                );
2189            });
2190            message_editor.read(cx).focus_handle(cx).focus(window);
2191            let editor = message_editor.read(cx).editor().clone();
2192            (message_editor, editor)
2193        });
2194
2195        cx.simulate_input("Lorem @");
2196
2197        editor.update_in(&mut cx, |editor, window, cx| {
2198            assert_eq!(editor.text(cx), "Lorem @");
2199            assert!(editor.has_visible_completions_menu());
2200
2201            assert_eq!(
2202                current_completion_labels(editor),
2203                &[
2204                    format!("eight.txt b{slash}"),
2205                    format!("seven.txt b{slash}"),
2206                    format!("six.txt b{slash}"),
2207                    format!("five.txt b{slash}"),
2208                ]
2209            );
2210            editor.set_text("", window, cx);
2211        });
2212
2213        prompt_capabilities.replace(acp::PromptCapabilities {
2214            image: true,
2215            audio: true,
2216            embedded_context: true,
2217            meta: None,
2218        });
2219
2220        cx.simulate_input("Lorem ");
2221
2222        editor.update(&mut cx, |editor, cx| {
2223            assert_eq!(editor.text(cx), "Lorem ");
2224            assert!(!editor.has_visible_completions_menu());
2225        });
2226
2227        cx.simulate_input("@");
2228
2229        editor.update(&mut cx, |editor, cx| {
2230            assert_eq!(editor.text(cx), "Lorem @");
2231            assert!(editor.has_visible_completions_menu());
2232            assert_eq!(
2233                current_completion_labels(editor),
2234                &[
2235                    format!("eight.txt b{slash}"),
2236                    format!("seven.txt b{slash}"),
2237                    format!("six.txt b{slash}"),
2238                    format!("five.txt b{slash}"),
2239                    "Files & Directories".into(),
2240                    "Symbols".into(),
2241                    "Threads".into(),
2242                    "Fetch".into()
2243                ]
2244            );
2245        });
2246
2247        // Select and confirm "File"
2248        editor.update_in(&mut cx, |editor, window, cx| {
2249            assert!(editor.has_visible_completions_menu());
2250            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2251            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2252            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2253            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2254            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2255        });
2256
2257        cx.run_until_parked();
2258
2259        editor.update(&mut cx, |editor, cx| {
2260            assert_eq!(editor.text(cx), "Lorem @file ");
2261            assert!(editor.has_visible_completions_menu());
2262        });
2263
2264        cx.simulate_input("one");
2265
2266        editor.update(&mut cx, |editor, cx| {
2267            assert_eq!(editor.text(cx), "Lorem @file one");
2268            assert!(editor.has_visible_completions_menu());
2269            assert_eq!(
2270                current_completion_labels(editor),
2271                vec![format!("one.txt a{slash}")]
2272            );
2273        });
2274
2275        editor.update_in(&mut cx, |editor, window, cx| {
2276            assert!(editor.has_visible_completions_menu());
2277            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2278        });
2279
2280        let url_one = MentionUri::File {
2281            abs_path: path!("/dir/a/one.txt").into(),
2282        }
2283        .to_uri()
2284        .to_string();
2285        editor.update(&mut cx, |editor, cx| {
2286            let text = editor.text(cx);
2287            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2288            assert!(!editor.has_visible_completions_menu());
2289            assert_eq!(fold_ranges(editor, cx).len(), 1);
2290        });
2291
2292        let all_prompt_capabilities = acp::PromptCapabilities {
2293            image: true,
2294            audio: true,
2295            embedded_context: true,
2296            meta: None,
2297        };
2298
2299        let contents = message_editor
2300            .update(&mut cx, |message_editor, cx| {
2301                message_editor.mention_set().contents(
2302                    &all_prompt_capabilities,
2303                    false,
2304                    project.clone(),
2305                    cx,
2306                )
2307            })
2308            .await
2309            .unwrap()
2310            .into_values()
2311            .collect::<Vec<_>>();
2312
2313        {
2314            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
2315                panic!("Unexpected mentions");
2316            };
2317            pretty_assertions::assert_eq!(content, "1");
2318            pretty_assertions::assert_eq!(
2319                uri,
2320                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
2321            );
2322        }
2323
2324        let contents = message_editor
2325            .update(&mut cx, |message_editor, cx| {
2326                message_editor.mention_set().contents(
2327                    &acp::PromptCapabilities::default(),
2328                    false,
2329                    project.clone(),
2330                    cx,
2331                )
2332            })
2333            .await
2334            .unwrap()
2335            .into_values()
2336            .collect::<Vec<_>>();
2337
2338        {
2339            let [(uri, Mention::UriOnly)] = contents.as_slice() else {
2340                panic!("Unexpected mentions");
2341            };
2342            pretty_assertions::assert_eq!(
2343                uri,
2344                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
2345            );
2346        }
2347
2348        cx.simulate_input(" ");
2349
2350        editor.update(&mut cx, |editor, cx| {
2351            let text = editor.text(cx);
2352            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
2353            assert!(!editor.has_visible_completions_menu());
2354            assert_eq!(fold_ranges(editor, cx).len(), 1);
2355        });
2356
2357        cx.simulate_input("Ipsum ");
2358
2359        editor.update(&mut cx, |editor, cx| {
2360            let text = editor.text(cx);
2361            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
2362            assert!(!editor.has_visible_completions_menu());
2363            assert_eq!(fold_ranges(editor, cx).len(), 1);
2364        });
2365
2366        cx.simulate_input("@file ");
2367
2368        editor.update(&mut cx, |editor, cx| {
2369            let text = editor.text(cx);
2370            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
2371            assert!(editor.has_visible_completions_menu());
2372            assert_eq!(fold_ranges(editor, cx).len(), 1);
2373        });
2374
2375        editor.update_in(&mut cx, |editor, window, cx| {
2376            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2377        });
2378
2379        cx.run_until_parked();
2380
2381        let contents = message_editor
2382            .update(&mut cx, |message_editor, cx| {
2383                message_editor.mention_set().contents(
2384                    &all_prompt_capabilities,
2385                    false,
2386                    project.clone(),
2387                    cx,
2388                )
2389            })
2390            .await
2391            .unwrap()
2392            .into_values()
2393            .collect::<Vec<_>>();
2394
2395        let url_eight = MentionUri::File {
2396            abs_path: path!("/dir/b/eight.txt").into(),
2397        }
2398        .to_uri()
2399        .to_string();
2400
2401        {
2402            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2403                panic!("Unexpected mentions");
2404            };
2405            pretty_assertions::assert_eq!(content, "8");
2406            pretty_assertions::assert_eq!(
2407                uri,
2408                &MentionUri::parse(&url_eight, PathStyle::local()).unwrap()
2409            );
2410        }
2411
2412        editor.update(&mut cx, |editor, cx| {
2413            assert_eq!(
2414                editor.text(cx),
2415                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
2416            );
2417            assert!(!editor.has_visible_completions_menu());
2418            assert_eq!(fold_ranges(editor, cx).len(), 2);
2419        });
2420
2421        let plain_text_language = Arc::new(language::Language::new(
2422            language::LanguageConfig {
2423                name: "Plain Text".into(),
2424                matcher: language::LanguageMatcher {
2425                    path_suffixes: vec!["txt".to_string()],
2426                    ..Default::default()
2427                },
2428                ..Default::default()
2429            },
2430            None,
2431        ));
2432
2433        // Register the language and fake LSP
2434        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2435        language_registry.add(plain_text_language);
2436
2437        let mut fake_language_servers = language_registry.register_fake_lsp(
2438            "Plain Text",
2439            language::FakeLspAdapter {
2440                capabilities: lsp::ServerCapabilities {
2441                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2442                    ..Default::default()
2443                },
2444                ..Default::default()
2445            },
2446        );
2447
2448        // Open the buffer to trigger LSP initialization
2449        let buffer = project
2450            .update(&mut cx, |project, cx| {
2451                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2452            })
2453            .await
2454            .unwrap();
2455
2456        // Register the buffer with language servers
2457        let _handle = project.update(&mut cx, |project, cx| {
2458            project.register_buffer_with_language_servers(&buffer, cx)
2459        });
2460
2461        cx.run_until_parked();
2462
2463        let fake_language_server = fake_language_servers.next().await.unwrap();
2464        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2465            move |_, _| async move {
2466                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2467                    #[allow(deprecated)]
2468                    lsp::SymbolInformation {
2469                        name: "MySymbol".into(),
2470                        location: lsp::Location {
2471                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2472                            range: lsp::Range::new(
2473                                lsp::Position::new(0, 0),
2474                                lsp::Position::new(0, 1),
2475                            ),
2476                        },
2477                        kind: lsp::SymbolKind::CONSTANT,
2478                        tags: None,
2479                        container_name: None,
2480                        deprecated: None,
2481                    },
2482                ])))
2483            },
2484        );
2485
2486        cx.simulate_input("@symbol ");
2487
2488        editor.update(&mut cx, |editor, cx| {
2489            assert_eq!(
2490                editor.text(cx),
2491                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
2492            );
2493            assert!(editor.has_visible_completions_menu());
2494            assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]);
2495        });
2496
2497        editor.update_in(&mut cx, |editor, window, cx| {
2498            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2499        });
2500
2501        let symbol = MentionUri::Symbol {
2502            abs_path: path!("/dir/a/one.txt").into(),
2503            name: "MySymbol".into(),
2504            line_range: 0..=0,
2505        };
2506
2507        let contents = message_editor
2508            .update(&mut cx, |message_editor, cx| {
2509                message_editor.mention_set().contents(
2510                    &all_prompt_capabilities,
2511                    false,
2512                    project.clone(),
2513                    cx,
2514                )
2515            })
2516            .await
2517            .unwrap()
2518            .into_values()
2519            .collect::<Vec<_>>();
2520
2521        {
2522            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2523                panic!("Unexpected mentions");
2524            };
2525            pretty_assertions::assert_eq!(content, "1");
2526            pretty_assertions::assert_eq!(uri, &symbol);
2527        }
2528
2529        cx.run_until_parked();
2530
2531        editor.read_with(&cx, |editor, cx| {
2532            assert_eq!(
2533                editor.text(cx),
2534                format!(
2535                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2536                    symbol.to_uri(),
2537                )
2538            );
2539        });
2540
2541        // Try to mention an "image" file that will fail to load
2542        cx.simulate_input("@file x.png");
2543
2544        editor.update(&mut cx, |editor, cx| {
2545            assert_eq!(
2546                editor.text(cx),
2547                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2548            );
2549            assert!(editor.has_visible_completions_menu());
2550            assert_eq!(current_completion_labels(editor), &["x.png "]);
2551        });
2552
2553        editor.update_in(&mut cx, |editor, window, cx| {
2554            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2555        });
2556
2557        // Getting the message contents fails
2558        message_editor
2559            .update(&mut cx, |message_editor, cx| {
2560                message_editor.mention_set().contents(
2561                    &all_prompt_capabilities,
2562                    false,
2563                    project.clone(),
2564                    cx,
2565                )
2566            })
2567            .await
2568            .expect_err("Should fail to load x.png");
2569
2570        cx.run_until_parked();
2571
2572        // Mention was removed
2573        editor.read_with(&cx, |editor, cx| {
2574            assert_eq!(
2575                editor.text(cx),
2576                format!(
2577                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2578                    symbol.to_uri()
2579                )
2580            );
2581        });
2582
2583        // Once more
2584        cx.simulate_input("@file x.png");
2585
2586        editor.update(&mut cx, |editor, cx| {
2587                    assert_eq!(
2588                        editor.text(cx),
2589                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2590                    );
2591                    assert!(editor.has_visible_completions_menu());
2592                    assert_eq!(current_completion_labels(editor), &["x.png "]);
2593                });
2594
2595        editor.update_in(&mut cx, |editor, window, cx| {
2596            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2597        });
2598
2599        // This time don't immediately get the contents, just let the confirmed completion settle
2600        cx.run_until_parked();
2601
2602        // Mention was removed
2603        editor.read_with(&cx, |editor, cx| {
2604            assert_eq!(
2605                editor.text(cx),
2606                format!(
2607                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2608                    symbol.to_uri()
2609                )
2610            );
2611        });
2612
2613        // Now getting the contents succeeds, because the invalid mention was removed
2614        let contents = message_editor
2615            .update(&mut cx, |message_editor, cx| {
2616                message_editor.mention_set().contents(
2617                    &all_prompt_capabilities,
2618                    false,
2619                    project.clone(),
2620                    cx,
2621                )
2622            })
2623            .await
2624            .unwrap();
2625        assert_eq!(contents.len(), 3);
2626    }
2627
2628    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2629        let snapshot = editor.buffer().read(cx).snapshot(cx);
2630        editor.display_map.update(cx, |display_map, cx| {
2631            display_map
2632                .snapshot(cx)
2633                .folds_in_range(0..snapshot.len())
2634                .map(|fold| fold.range.to_point(&snapshot))
2635                .collect()
2636        })
2637    }
2638
2639    fn current_completion_labels(editor: &Editor) -> Vec<String> {
2640        let completions = editor.current_completions().expect("Missing completions");
2641        completions
2642            .into_iter()
2643            .map(|completion| completion.label.text)
2644            .collect::<Vec<_>>()
2645    }
2646
2647    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
2648        let completions = editor.current_completions().expect("Missing completions");
2649        completions
2650            .into_iter()
2651            .map(|completion| {
2652                (
2653                    completion.label.text,
2654                    completion
2655                        .documentation
2656                        .map(|d| d.text().to_string())
2657                        .unwrap_or_default(),
2658                )
2659            })
2660            .collect::<Vec<_>>()
2661    }
2662
2663    #[gpui::test]
2664    async fn test_large_file_mention_uses_outline(cx: &mut TestAppContext) {
2665        init_test(cx);
2666
2667        let fs = FakeFs::new(cx.executor());
2668
2669        // Create a large file that exceeds AUTO_OUTLINE_SIZE
2670        const LINE: &str = "fn example_function() { /* some code */ }\n";
2671        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
2672        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
2673
2674        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
2675        let small_content = "fn small_function() { /* small */ }\n";
2676        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
2677
2678        fs.insert_tree(
2679            "/project",
2680            json!({
2681                "large_file.rs": large_content.clone(),
2682                "small_file.rs": small_content,
2683            }),
2684        )
2685        .await;
2686
2687        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2688
2689        let (workspace, cx) =
2690            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2691
2692        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2693        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2694
2695        let message_editor = cx.update(|window, cx| {
2696            cx.new(|cx| {
2697                let editor = MessageEditor::new(
2698                    workspace.downgrade(),
2699                    project.clone(),
2700                    history_store.clone(),
2701                    None,
2702                    Default::default(),
2703                    Default::default(),
2704                    "Test Agent".into(),
2705                    "Test",
2706                    EditorMode::AutoHeight {
2707                        min_lines: 1,
2708                        max_lines: None,
2709                    },
2710                    window,
2711                    cx,
2712                );
2713                // Enable embedded context so files are actually included
2714                editor.prompt_capabilities.replace(acp::PromptCapabilities {
2715                    embedded_context: true,
2716                    meta: None,
2717                    ..Default::default()
2718                });
2719                editor
2720            })
2721        });
2722
2723        // Test large file mention
2724        // Get the absolute path using the project's worktree
2725        let large_file_abs_path = project.read_with(cx, |project, cx| {
2726            let worktree = project.worktrees(cx).next().unwrap();
2727            let worktree_root = worktree.read(cx).abs_path();
2728            worktree_root.join("large_file.rs")
2729        });
2730        let large_file_task = message_editor.update(cx, |editor, cx| {
2731            editor.confirm_mention_for_file(large_file_abs_path, cx)
2732        });
2733
2734        let large_file_mention = large_file_task.await.unwrap();
2735        match large_file_mention {
2736            Mention::Text { content, .. } => {
2737                // Should contain outline header for large files
2738                assert!(content.contains("File outline for"));
2739                assert!(content.contains("file too large to show full content"));
2740                // Should not contain the full repeated content
2741                assert!(!content.contains(&LINE.repeat(100)));
2742            }
2743            _ => panic!("Expected Text mention for large file"),
2744        }
2745
2746        // Test small file mention
2747        // Get the absolute path using the project's worktree
2748        let small_file_abs_path = project.read_with(cx, |project, cx| {
2749            let worktree = project.worktrees(cx).next().unwrap();
2750            let worktree_root = worktree.read(cx).abs_path();
2751            worktree_root.join("small_file.rs")
2752        });
2753        let small_file_task = message_editor.update(cx, |editor, cx| {
2754            editor.confirm_mention_for_file(small_file_abs_path, cx)
2755        });
2756
2757        let small_file_mention = small_file_task.await.unwrap();
2758        match small_file_mention {
2759            Mention::Text { content, .. } => {
2760                // Should contain the actual content
2761                assert_eq!(content, small_content);
2762                // Should not contain outline header
2763                assert!(!content.contains("File outline for"));
2764            }
2765            _ => panic!("Expected Text mention for small file"),
2766        }
2767    }
2768
2769    #[gpui::test]
2770    async fn test_insert_thread_summary(cx: &mut TestAppContext) {
2771        init_test(cx);
2772        cx.update(LanguageModelRegistry::test);
2773
2774        let fs = FakeFs::new(cx.executor());
2775        fs.insert_tree("/project", json!({"file": ""})).await;
2776        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2777
2778        let (workspace, cx) =
2779            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2780
2781        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2782        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2783
2784        // Create a thread metadata to insert as summary
2785        let thread_metadata = agent::DbThreadMetadata {
2786            id: acp::SessionId("thread-123".into()),
2787            title: "Previous Conversation".into(),
2788            updated_at: chrono::Utc::now(),
2789        };
2790
2791        let message_editor = cx.update(|window, cx| {
2792            cx.new(|cx| {
2793                let mut editor = MessageEditor::new(
2794                    workspace.downgrade(),
2795                    project.clone(),
2796                    history_store.clone(),
2797                    None,
2798                    Default::default(),
2799                    Default::default(),
2800                    "Test Agent".into(),
2801                    "Test",
2802                    EditorMode::AutoHeight {
2803                        min_lines: 1,
2804                        max_lines: None,
2805                    },
2806                    window,
2807                    cx,
2808                );
2809                editor.insert_thread_summary(thread_metadata.clone(), window, cx);
2810                editor
2811            })
2812        });
2813
2814        // Construct expected values for verification
2815        let expected_uri = MentionUri::Thread {
2816            id: thread_metadata.id.clone(),
2817            name: thread_metadata.title.to_string(),
2818        };
2819        let expected_link = format!("[@{}]({})", thread_metadata.title, expected_uri.to_uri());
2820
2821        message_editor.read_with(cx, |editor, cx| {
2822            let text = editor.text(cx);
2823
2824            assert!(
2825                text.contains(&expected_link),
2826                "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}",
2827                expected_link,
2828                text
2829            );
2830
2831            let mentions = editor.mentions();
2832            assert_eq!(
2833                mentions.len(),
2834                1,
2835                "Expected exactly one mention after inserting thread summary"
2836            );
2837
2838            assert!(
2839                mentions.contains(&expected_uri),
2840                "Expected mentions to contain the thread URI"
2841            );
2842        });
2843    }
2844
2845    #[gpui::test]
2846    async fn test_whitespace_trimming(cx: &mut TestAppContext) {
2847        init_test(cx);
2848
2849        let fs = FakeFs::new(cx.executor());
2850        fs.insert_tree("/project", json!({"file.rs": "fn main() {}"}))
2851            .await;
2852        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2853
2854        let (workspace, cx) =
2855            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2856
2857        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2858        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2859
2860        let message_editor = cx.update(|window, cx| {
2861            cx.new(|cx| {
2862                MessageEditor::new(
2863                    workspace.downgrade(),
2864                    project.clone(),
2865                    history_store.clone(),
2866                    None,
2867                    Default::default(),
2868                    Default::default(),
2869                    "Test Agent".into(),
2870                    "Test",
2871                    EditorMode::AutoHeight {
2872                        min_lines: 1,
2873                        max_lines: None,
2874                    },
2875                    window,
2876                    cx,
2877                )
2878            })
2879        });
2880        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
2881
2882        cx.run_until_parked();
2883
2884        editor.update_in(cx, |editor, window, cx| {
2885            editor.set_text("  \u{A0}してhello world  ", window, cx);
2886        });
2887
2888        let (content, _) = message_editor
2889            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2890            .await
2891            .unwrap();
2892
2893        assert_eq!(
2894            content,
2895            vec![acp::ContentBlock::Text(acp::TextContent {
2896                text: "してhello world".into(),
2897                annotations: None,
2898                meta: None
2899            })]
2900        );
2901    }
2902
2903    #[gpui::test]
2904    async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
2905        init_test(cx);
2906
2907        let app_state = cx.update(AppState::test);
2908
2909        cx.update(|cx| {
2910            language::init(cx);
2911            editor::init(cx);
2912            workspace::init(app_state.clone(), cx);
2913            Project::init_settings(cx);
2914        });
2915
2916        app_state
2917            .fs
2918            .as_fake()
2919            .insert_tree(
2920                path!("/dir"),
2921                json!({
2922                    "test.txt": "line1\nline2\nline3\nline4\nline5\n",
2923                }),
2924            )
2925            .await;
2926
2927        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2928        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2929        let workspace = window.root(cx).unwrap();
2930
2931        let worktree = project.update(cx, |project, cx| {
2932            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2933            assert_eq!(worktrees.len(), 1);
2934            worktrees.pop().unwrap()
2935        });
2936        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2937
2938        let mut cx = VisualTestContext::from_window(*window, cx);
2939
2940        // Open a regular editor with the created file, and select a portion of
2941        // the text that will be used for the selections that are meant to be
2942        // inserted in the agent panel.
2943        let editor = workspace
2944            .update_in(&mut cx, |workspace, window, cx| {
2945                workspace.open_path(
2946                    ProjectPath {
2947                        worktree_id,
2948                        path: rel_path("test.txt").into(),
2949                    },
2950                    None,
2951                    false,
2952                    window,
2953                    cx,
2954                )
2955            })
2956            .await
2957            .unwrap()
2958            .downcast::<Editor>()
2959            .unwrap();
2960
2961        editor.update_in(&mut cx, |editor, window, cx| {
2962            editor.change_selections(Default::default(), window, cx, |selections| {
2963                selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
2964            });
2965        });
2966
2967        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2968        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2969
2970        // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
2971        // to ensure we have a fixed viewport, so we can eventually actually
2972        // place the cursor outside of the visible area.
2973        let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
2974            let workspace_handle = cx.weak_entity();
2975            let message_editor = cx.new(|cx| {
2976                MessageEditor::new(
2977                    workspace_handle,
2978                    project.clone(),
2979                    history_store.clone(),
2980                    None,
2981                    Default::default(),
2982                    Default::default(),
2983                    "Test Agent".into(),
2984                    "Test",
2985                    EditorMode::full(),
2986                    window,
2987                    cx,
2988                )
2989            });
2990            workspace.active_pane().update(cx, |pane, cx| {
2991                pane.add_item(
2992                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2993                    true,
2994                    true,
2995                    None,
2996                    window,
2997                    cx,
2998                );
2999            });
3000
3001            message_editor
3002        });
3003
3004        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3005            message_editor.editor.update(cx, |editor, cx| {
3006                // Update the Agent Panel's Message Editor text to have 100
3007                // lines, ensuring that the cursor is set at line 90 and that we
3008                // then scroll all the way to the top, so the cursor's position
3009                // remains off screen.
3010                let mut lines = String::new();
3011                for _ in 1..=100 {
3012                    lines.push_str(&"Another line in the agent panel's message editor\n");
3013                }
3014                editor.set_text(lines.as_str(), window, cx);
3015                editor.change_selections(Default::default(), window, cx, |selections| {
3016                    selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
3017                });
3018                editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
3019            });
3020        });
3021
3022        cx.run_until_parked();
3023
3024        // Before proceeding, let's assert that the cursor is indeed off screen,
3025        // otherwise the rest of the test doesn't make sense.
3026        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3027            message_editor.editor.update(cx, |editor, cx| {
3028                let snapshot = editor.snapshot(window, cx);
3029                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3030                let scroll_top = snapshot.scroll_position().y as u32;
3031                let visible_lines = editor.visible_line_count().unwrap() as u32;
3032                let visible_range = scroll_top..(scroll_top + visible_lines);
3033
3034                assert!(!visible_range.contains(&cursor_row));
3035            })
3036        });
3037
3038        // Now let's insert the selection in the Agent Panel's editor and
3039        // confirm that, after the insertion, the cursor is now in the visible
3040        // range.
3041        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3042            message_editor.insert_selections(window, cx);
3043        });
3044
3045        cx.run_until_parked();
3046
3047        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3048            message_editor.editor.update(cx, |editor, cx| {
3049                let snapshot = editor.snapshot(window, cx);
3050                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3051                let scroll_top = snapshot.scroll_position().y as u32;
3052                let visible_lines = editor.visible_line_count().unwrap() as u32;
3053                let visible_range = scroll_top..(scroll_top + visible_lines);
3054
3055                assert!(visible_range.contains(&cursor_row));
3056            })
3057        });
3058    }
3059}