message_editor.rs

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