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