message_editor.rs

   1use crate::DEFAULT_THREAD_TITLE;
   2use crate::SendImmediately;
   3use crate::ThreadHistory;
   4use crate::{
   5    ChatWithFollow,
   6    completion_provider::{
   7        PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction,
   8        PromptContextType, SlashCommandCompletion,
   9    },
  10    mention_set::{Mention, MentionImage, MentionSet, insert_crease_for_mention},
  11};
  12use acp_thread::MentionUri;
  13use agent::ThreadStore;
  14use agent_client_protocol as acp;
  15use anyhow::{Result, anyhow};
  16use editor::{
  17    Addon, AnchorRangeExt, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode,
  18    EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, MultiBufferSnapshot, ToOffset,
  19    actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll,
  20};
  21use futures::{FutureExt as _, future::join_all};
  22use gpui::{
  23    AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat,
  24    KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity,
  25};
  26use language::{Buffer, language_settings::InlayHintKind};
  27use parking_lot::RwLock;
  28use project::AgentId;
  29use project::{
  30    CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectPath, Worktree,
  31};
  32use prompt_store::PromptStore;
  33use rope::Point;
  34use settings::Settings;
  35use std::{fmt::Write, ops::Range, rc::Rc, sync::Arc};
  36use theme::ThemeSettings;
  37use ui::{ContextMenu, Disclosure, ElevationIndex, prelude::*};
  38use util::paths::PathStyle;
  39use util::{ResultExt, debug_panic};
  40use workspace::{CollaboratorId, Workspace};
  41use zed_actions::agent::{Chat, PasteRaw};
  42
  43#[derive(Default)]
  44pub struct SessionCapabilities {
  45    prompt_capabilities: acp::PromptCapabilities,
  46    available_commands: Vec<acp::AvailableCommand>,
  47}
  48
  49impl SessionCapabilities {
  50    pub fn new(
  51        prompt_capabilities: acp::PromptCapabilities,
  52        available_commands: Vec<acp::AvailableCommand>,
  53    ) -> Self {
  54        Self {
  55            prompt_capabilities,
  56            available_commands,
  57        }
  58    }
  59
  60    pub fn supports_images(&self) -> bool {
  61        self.prompt_capabilities.image
  62    }
  63
  64    pub fn supports_embedded_context(&self) -> bool {
  65        self.prompt_capabilities.embedded_context
  66    }
  67
  68    pub fn available_commands(&self) -> &[acp::AvailableCommand] {
  69        &self.available_commands
  70    }
  71
  72    fn supported_modes(&self, has_thread_store: bool) -> Vec<PromptContextType> {
  73        let mut supported = vec![PromptContextType::File, PromptContextType::Symbol];
  74        if self.prompt_capabilities.embedded_context {
  75            if has_thread_store {
  76                supported.push(PromptContextType::Thread);
  77            }
  78            supported.extend(&[
  79                PromptContextType::Diagnostics,
  80                PromptContextType::Fetch,
  81                PromptContextType::Rules,
  82                PromptContextType::BranchDiff,
  83            ]);
  84        }
  85        supported
  86    }
  87
  88    pub fn completion_commands(&self) -> Vec<crate::completion_provider::AvailableCommand> {
  89        self.available_commands
  90            .iter()
  91            .map(|cmd| crate::completion_provider::AvailableCommand {
  92                name: cmd.name.clone().into(),
  93                description: cmd.description.clone().into(),
  94                requires_argument: cmd.input.is_some(),
  95            })
  96            .collect()
  97    }
  98
  99    pub fn set_prompt_capabilities(&mut self, prompt_capabilities: acp::PromptCapabilities) {
 100        self.prompt_capabilities = prompt_capabilities;
 101    }
 102
 103    pub fn set_available_commands(&mut self, available_commands: Vec<acp::AvailableCommand>) {
 104        self.available_commands = available_commands;
 105    }
 106}
 107
 108pub type SharedSessionCapabilities = Arc<RwLock<SessionCapabilities>>;
 109
 110struct MessageEditorCompletionDelegate {
 111    session_capabilities: SharedSessionCapabilities,
 112    has_thread_store: bool,
 113    message_editor: WeakEntity<MessageEditor>,
 114}
 115
 116impl PromptCompletionProviderDelegate for MessageEditorCompletionDelegate {
 117    fn supports_images(&self, _cx: &App) -> bool {
 118        self.session_capabilities.read().supports_images()
 119    }
 120
 121    fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
 122        self.session_capabilities
 123            .read()
 124            .supported_modes(self.has_thread_store)
 125    }
 126
 127    fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
 128        self.session_capabilities.read().completion_commands()
 129    }
 130
 131    fn confirm_command(&self, cx: &mut App) {
 132        let _ = self.message_editor.update(cx, |this, cx| this.send(cx));
 133    }
 134}
 135
 136pub struct MessageEditor {
 137    mention_set: Entity<MentionSet>,
 138    editor: Entity<Editor>,
 139    workspace: WeakEntity<Workspace>,
 140    session_capabilities: SharedSessionCapabilities,
 141    agent_id: AgentId,
 142    thread_store: Option<Entity<ThreadStore>>,
 143    _subscriptions: Vec<Subscription>,
 144    _parse_slash_command_task: Task<()>,
 145}
 146
 147#[derive(Clone, Debug)]
 148pub enum MessageEditorEvent {
 149    Send,
 150    SendImmediately,
 151    Cancel,
 152    Focus,
 153    LostFocus,
 154    InputAttempted {
 155        text: Arc<str>,
 156        cursor_offset: usize,
 157    },
 158}
 159
 160impl EventEmitter<MessageEditorEvent> for MessageEditor {}
 161
 162const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
 163
 164enum MentionInsertPosition {
 165    AtCursor,
 166    EndOfBuffer,
 167}
 168
 169fn insert_mention_for_project_path(
 170    project_path: &ProjectPath,
 171    position: MentionInsertPosition,
 172    editor: &Entity<Editor>,
 173    mention_set: &Entity<MentionSet>,
 174    project: &Entity<Project>,
 175    workspace: &Entity<Workspace>,
 176    supports_images: bool,
 177    window: &mut Window,
 178    cx: &mut App,
 179) -> Option<Task<()>> {
 180    let (file_name, mention_uri) = {
 181        let project = project.read(cx);
 182        let path_style = project.path_style(cx);
 183        let entry = project.entry_for_path(project_path, cx)?;
 184        let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
 185        let abs_path = worktree.read(cx).absolutize(&project_path.path);
 186        let (file_name, _) = crate::completion_provider::extract_file_name_and_directory(
 187            &project_path.path,
 188            worktree.read(cx).root_name(),
 189            path_style,
 190        );
 191        let mention_uri = if entry.is_dir() {
 192            MentionUri::Directory { abs_path }
 193        } else {
 194            MentionUri::File { abs_path }
 195        };
 196        (file_name, mention_uri)
 197    };
 198
 199    let mention_text = mention_uri.as_link().to_string();
 200    let content_len = mention_text.len();
 201
 202    let text_anchor = match position {
 203        MentionInsertPosition::AtCursor => editor.update(cx, |editor, cx| {
 204            let buffer = editor.buffer().read(cx);
 205            let snapshot = buffer.snapshot(cx);
 206            let (_, _, buffer_snapshot) = snapshot.as_singleton()?;
 207            let text_anchor = editor
 208                .selections
 209                .newest_anchor()
 210                .start
 211                .text_anchor
 212                .bias_left(&buffer_snapshot);
 213
 214            editor.insert(&mention_text, window, cx);
 215            editor.insert(" ", window, cx);
 216
 217            Some(text_anchor)
 218        }),
 219        MentionInsertPosition::EndOfBuffer => {
 220            let multi_buffer = editor.read(cx).buffer().clone();
 221            let buffer = multi_buffer.read(cx).as_singleton()?;
 222            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
 223            let new_text = format!("{mention_text} ");
 224            editor.update(cx, |editor, cx| {
 225                editor.edit(
 226                    [(
 227                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 228                        new_text,
 229                    )],
 230                    cx,
 231                );
 232            });
 233            Some(anchor)
 234        }
 235    }?;
 236
 237    Some(mention_set.update(cx, |mention_set, cx| {
 238        mention_set.confirm_mention_completion(
 239            file_name,
 240            text_anchor,
 241            content_len,
 242            mention_uri,
 243            supports_images,
 244            editor.clone(),
 245            workspace,
 246            window,
 247            cx,
 248        )
 249    }))
 250}
 251
 252enum ResolvedPastedContextItem {
 253    Image(gpui::Image, gpui::SharedString),
 254    ProjectPath(ProjectPath),
 255}
 256
 257async fn resolve_pasted_context_items(
 258    project: Entity<Project>,
 259    project_is_local: bool,
 260    supports_images: bool,
 261    entries: Vec<ClipboardEntry>,
 262    cx: &mut gpui::AsyncWindowContext,
 263) -> (Vec<ResolvedPastedContextItem>, Vec<Entity<Worktree>>) {
 264    let mut items = Vec::new();
 265    let mut added_worktrees = Vec::new();
 266    let default_image_name: SharedString = MentionUri::PastedImage.name().into();
 267
 268    for entry in entries {
 269        match entry {
 270            ClipboardEntry::String(_) => {}
 271            ClipboardEntry::Image(image) => {
 272                if supports_images {
 273                    items.push(ResolvedPastedContextItem::Image(
 274                        image,
 275                        default_image_name.clone(),
 276                    ));
 277                }
 278            }
 279            ClipboardEntry::ExternalPaths(paths) => {
 280                for path in paths.paths().iter() {
 281                    if let Some((image, name)) = cx
 282                        .background_spawn({
 283                            let path = path.clone();
 284                            let default_image_name = default_image_name.clone();
 285                            async move {
 286                                crate::mention_set::load_external_image_from_path(
 287                                    &path,
 288                                    &default_image_name,
 289                                )
 290                            }
 291                        })
 292                        .await
 293                    {
 294                        if supports_images {
 295                            items.push(ResolvedPastedContextItem::Image(image, name));
 296                        }
 297                        continue;
 298                    }
 299
 300                    if !project_is_local {
 301                        continue;
 302                    }
 303
 304                    let path = path.clone();
 305                    let Ok(resolve_task) = cx.update({
 306                        let project = project.clone();
 307                        move |_, cx| Workspace::project_path_for_path(project, &path, false, cx)
 308                    }) else {
 309                        continue;
 310                    };
 311
 312                    if let Some((worktree, project_path)) = resolve_task.await.log_err() {
 313                        added_worktrees.push(worktree);
 314                        items.push(ResolvedPastedContextItem::ProjectPath(project_path));
 315                    }
 316                }
 317            }
 318        }
 319    }
 320
 321    (items, added_worktrees)
 322}
 323
 324fn insert_project_path_as_context(
 325    project_path: ProjectPath,
 326    editor: Entity<Editor>,
 327    mention_set: Entity<MentionSet>,
 328    workspace: WeakEntity<Workspace>,
 329    supports_images: bool,
 330    cx: &mut gpui::AsyncWindowContext,
 331) -> Option<Task<()>> {
 332    let workspace = workspace.upgrade()?;
 333
 334    cx.update(move |window, cx| {
 335        let project = workspace.read(cx).project().clone();
 336        insert_mention_for_project_path(
 337            &project_path,
 338            MentionInsertPosition::AtCursor,
 339            &editor,
 340            &mention_set,
 341            &project,
 342            &workspace,
 343            supports_images,
 344            window,
 345            cx,
 346        )
 347    })
 348    .ok()
 349    .flatten()
 350}
 351
 352async fn insert_resolved_pasted_context_items(
 353    items: Vec<ResolvedPastedContextItem>,
 354    added_worktrees: Vec<Entity<Worktree>>,
 355    editor: Entity<Editor>,
 356    mention_set: Entity<MentionSet>,
 357    workspace: WeakEntity<Workspace>,
 358    supports_images: bool,
 359    cx: &mut gpui::AsyncWindowContext,
 360) {
 361    let mut path_mention_tasks = Vec::new();
 362
 363    for item in items {
 364        match item {
 365            ResolvedPastedContextItem::Image(image, name) => {
 366                crate::mention_set::insert_images_as_context(
 367                    vec![(image, name)],
 368                    editor.clone(),
 369                    mention_set.clone(),
 370                    workspace.clone(),
 371                    cx,
 372                )
 373                .await;
 374            }
 375            ResolvedPastedContextItem::ProjectPath(project_path) => {
 376                if let Some(task) = insert_project_path_as_context(
 377                    project_path,
 378                    editor.clone(),
 379                    mention_set.clone(),
 380                    workspace.clone(),
 381                    supports_images,
 382                    cx,
 383                ) {
 384                    path_mention_tasks.push(task);
 385                }
 386            }
 387        }
 388    }
 389
 390    join_all(path_mention_tasks).await;
 391    drop(added_worktrees);
 392}
 393
 394impl MessageEditor {
 395    pub fn new(
 396        workspace: WeakEntity<Workspace>,
 397        project: WeakEntity<Project>,
 398        thread_store: Option<Entity<ThreadStore>>,
 399        history: Option<WeakEntity<ThreadHistory>>,
 400        prompt_store: Option<Entity<PromptStore>>,
 401        session_capabilities: SharedSessionCapabilities,
 402        agent_id: AgentId,
 403        placeholder: &str,
 404        mode: EditorMode,
 405        window: &mut Window,
 406        cx: &mut Context<Self>,
 407    ) -> Self {
 408        let language_registry = project
 409            .upgrade()
 410            .map(|project| project.read(cx).languages().clone());
 411
 412        let editor = cx.new(|cx| {
 413            let buffer = cx.new(|cx| {
 414                let buffer = Buffer::local("", cx);
 415                if let Some(language_registry) = language_registry.as_ref() {
 416                    buffer.set_language_registry(language_registry.clone());
 417                }
 418                buffer
 419            });
 420            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 421
 422            let mut editor = Editor::new(mode, buffer, None, window, cx);
 423            editor.set_placeholder_text(placeholder, window, cx);
 424            editor.set_show_indent_guides(false, cx);
 425            editor.set_show_completions_on_input(Some(true));
 426            editor.set_soft_wrap();
 427            editor.set_use_modal_editing(true);
 428            editor.set_context_menu_options(ContextMenuOptions {
 429                min_entries_visible: 12,
 430                max_entries_visible: 12,
 431                placement: None,
 432            });
 433            editor.register_addon(MessageEditorAddon::new());
 434
 435            editor.set_custom_context_menu(|editor, _point, window, cx| {
 436                let has_selection = editor.has_non_empty_selection(&editor.display_snapshot(cx));
 437
 438                Some(ContextMenu::build(window, cx, |menu, _, _| {
 439                    menu.action("Cut", Box::new(editor::actions::Cut))
 440                        .action_disabled_when(
 441                            !has_selection,
 442                            "Copy",
 443                            Box::new(editor::actions::Copy),
 444                        )
 445                        .action("Paste", Box::new(editor::actions::Paste))
 446                        .action("Paste as Plain Text", Box::new(PasteRaw))
 447                }))
 448            });
 449
 450            editor
 451        });
 452        let mention_set =
 453            cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));
 454        let completion_provider = Rc::new(PromptCompletionProvider::new(
 455            MessageEditorCompletionDelegate {
 456                session_capabilities: session_capabilities.clone(),
 457                has_thread_store: thread_store.is_some(),
 458                message_editor: cx.weak_entity(),
 459            },
 460            editor.downgrade(),
 461            mention_set.clone(),
 462            history,
 463            prompt_store.clone(),
 464            workspace.clone(),
 465        ));
 466        editor.update(cx, |editor, _cx| {
 467            editor.set_completion_provider(Some(completion_provider.clone()))
 468        });
 469
 470        cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
 471            cx.emit(MessageEditorEvent::Focus)
 472        })
 473        .detach();
 474        cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
 475            cx.emit(MessageEditorEvent::LostFocus)
 476        })
 477        .detach();
 478
 479        let mut has_hint = false;
 480        let mut subscriptions = Vec::new();
 481
 482        subscriptions.push(cx.subscribe_in(&editor, window, {
 483            move |this, editor, event, window, cx| {
 484                let input_attempted_text = match event {
 485                    EditorEvent::InputHandled { text, .. } => Some(text),
 486                    EditorEvent::InputIgnored { text } => Some(text),
 487                    _ => None,
 488                };
 489                if let Some(text) = input_attempted_text
 490                    && editor.read(cx).read_only(cx)
 491                    && !text.is_empty()
 492                {
 493                    let editor = editor.read(cx);
 494                    let cursor_anchor = editor.selections.newest_anchor().head();
 495                    let cursor_offset = cursor_anchor
 496                        .to_offset(&editor.buffer().read(cx).snapshot(cx))
 497                        .0;
 498                    cx.emit(MessageEditorEvent::InputAttempted {
 499                        text: text.clone(),
 500                        cursor_offset,
 501                    });
 502                }
 503
 504                if let EditorEvent::Edited { .. } = event
 505                    && !editor.read(cx).read_only(cx)
 506                {
 507                    editor.update(cx, |editor, cx| {
 508                        let snapshot = editor.snapshot(window, cx);
 509                        this.mention_set
 510                            .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
 511
 512                        let new_hints = this
 513                            .command_hint(snapshot.buffer())
 514                            .into_iter()
 515                            .collect::<Vec<_>>();
 516                        let has_new_hint = !new_hints.is_empty();
 517                        editor.splice_inlays(
 518                            if has_hint {
 519                                &[COMMAND_HINT_INLAY_ID]
 520                            } else {
 521                                &[]
 522                            },
 523                            new_hints,
 524                            cx,
 525                        );
 526                        has_hint = has_new_hint;
 527                    });
 528                    cx.notify();
 529                }
 530            }
 531        }));
 532
 533        if let Some(language_registry) = language_registry {
 534            let editor = editor.clone();
 535            cx.spawn(async move |_, cx| {
 536                let markdown = language_registry.language_for_name("Markdown").await?;
 537                editor.update(cx, |editor, cx| {
 538                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
 539                        buffer.update(cx, |buffer, cx| {
 540                            buffer.set_language(Some(markdown), cx);
 541                        });
 542                    }
 543                });
 544                anyhow::Ok(())
 545            })
 546            .detach_and_log_err(cx);
 547        }
 548
 549        Self {
 550            editor,
 551            mention_set,
 552            workspace,
 553            session_capabilities,
 554            agent_id,
 555            thread_store,
 556            _subscriptions: subscriptions,
 557            _parse_slash_command_task: Task::ready(()),
 558        }
 559    }
 560
 561    pub fn set_session_capabilities(
 562        &mut self,
 563        session_capabilities: SharedSessionCapabilities,
 564        _cx: &mut Context<Self>,
 565    ) {
 566        self.session_capabilities = session_capabilities;
 567    }
 568
 569    fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> {
 570        let session_capabilities = self.session_capabilities.read();
 571        let available_commands = session_capabilities.available_commands();
 572        if available_commands.is_empty() {
 573            return None;
 574        }
 575
 576        let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
 577        if parsed_command.argument.is_some() {
 578            return None;
 579        }
 580
 581        let command_name = parsed_command.command?;
 582        let available_command = available_commands
 583            .iter()
 584            .find(|command| command.name == command_name)?;
 585
 586        let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput {
 587            mut hint,
 588            ..
 589        }) = available_command.input.clone()?
 590        else {
 591            return None;
 592        };
 593
 594        let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize;
 595        if hint_pos > snapshot.len() {
 596            hint_pos = snapshot.len();
 597            hint.insert(0, ' ');
 598        }
 599
 600        let hint_pos = snapshot.anchor_after(hint_pos);
 601
 602        Some(Inlay::hint(
 603            COMMAND_HINT_INLAY_ID,
 604            hint_pos,
 605            &InlayHint {
 606                position: hint_pos.text_anchor,
 607                label: InlayHintLabel::String(hint),
 608                kind: Some(InlayHintKind::Parameter),
 609                padding_left: false,
 610                padding_right: false,
 611                tooltip: None,
 612                resolve_state: project::ResolveState::Resolved,
 613            },
 614        ))
 615    }
 616
 617    pub fn insert_thread_summary(
 618        &mut self,
 619        session_id: acp::SessionId,
 620        title: Option<SharedString>,
 621        window: &mut Window,
 622        cx: &mut Context<Self>,
 623    ) {
 624        if self.thread_store.is_none() {
 625            return;
 626        }
 627        let Some(workspace) = self.workspace.upgrade() else {
 628            return;
 629        };
 630        let thread_title = title
 631            .filter(|title| !title.is_empty())
 632            .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
 633        let uri = MentionUri::Thread {
 634            id: session_id,
 635            name: thread_title.to_string(),
 636        };
 637        let content = format!("{}\n", uri.as_link());
 638
 639        let content_len = content.len() - 1;
 640
 641        let start = self.editor.update(cx, |editor, cx| {
 642            editor.set_text(content, window, cx);
 643            editor
 644                .buffer()
 645                .read(cx)
 646                .snapshot(cx)
 647                .anchor_before(Point::zero())
 648                .text_anchor
 649        });
 650
 651        let supports_images = self.session_capabilities.read().supports_images();
 652
 653        self.mention_set
 654            .update(cx, |mention_set, cx| {
 655                mention_set.confirm_mention_completion(
 656                    thread_title,
 657                    start,
 658                    content_len,
 659                    uri,
 660                    supports_images,
 661                    self.editor.clone(),
 662                    &workspace,
 663                    window,
 664                    cx,
 665                )
 666            })
 667            .detach();
 668    }
 669
 670    #[cfg(test)]
 671    pub(crate) fn editor(&self) -> &Entity<Editor> {
 672        &self.editor
 673    }
 674
 675    pub fn is_empty(&self, cx: &App) -> bool {
 676        self.editor.read(cx).is_empty(cx)
 677    }
 678
 679    pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
 680        self.editor
 681            .read(cx)
 682            .context_menu()
 683            .borrow()
 684            .as_ref()
 685            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
 686    }
 687
 688    #[cfg(test)]
 689    pub fn mention_set(&self) -> &Entity<MentionSet> {
 690        &self.mention_set
 691    }
 692
 693    fn validate_slash_commands(
 694        text: &str,
 695        available_commands: &[acp::AvailableCommand],
 696        agent_id: &AgentId,
 697    ) -> Result<()> {
 698        if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
 699            if let Some(command_name) = parsed_command.command {
 700                // Check if this command is in the list of available commands from the server
 701                let is_supported = available_commands
 702                    .iter()
 703                    .any(|cmd| cmd.name == command_name);
 704
 705                if !is_supported {
 706                    return Err(anyhow!(
 707                        "The /{} command is not supported by {}.\n\nAvailable commands: {}",
 708                        command_name,
 709                        agent_id,
 710                        if available_commands.is_empty() {
 711                            "none".to_string()
 712                        } else {
 713                            available_commands
 714                                .iter()
 715                                .map(|cmd| format!("/{}", cmd.name))
 716                                .collect::<Vec<_>>()
 717                                .join(", ")
 718                        }
 719                    ));
 720                }
 721            }
 722        }
 723        Ok(())
 724    }
 725
 726    pub fn contents(
 727        &self,
 728        full_mention_content: bool,
 729        cx: &mut Context<Self>,
 730    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 731        let text = self.editor.read(cx).text(cx);
 732        let available_commands = self
 733            .session_capabilities
 734            .read()
 735            .available_commands()
 736            .to_vec();
 737        let agent_id = self.agent_id.clone();
 738        let build_task = self.build_content_blocks(full_mention_content, cx);
 739
 740        cx.spawn(async move |_, _cx| {
 741            Self::validate_slash_commands(&text, &available_commands, &agent_id)?;
 742            build_task.await
 743        })
 744    }
 745
 746    pub fn draft_contents(&self, cx: &mut Context<Self>) -> Task<Result<Vec<acp::ContentBlock>>> {
 747        let build_task = self.build_content_blocks(false, cx);
 748        cx.spawn(async move |_, _cx| {
 749            let (blocks, _tracked_buffers) = build_task.await?;
 750            Ok(blocks)
 751        })
 752    }
 753
 754    fn build_content_blocks(
 755        &self,
 756        full_mention_content: bool,
 757        cx: &mut Context<Self>,
 758    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 759        let contents = self
 760            .mention_set
 761            .update(cx, |store, cx| store.contents(full_mention_content, cx));
 762        let editor = self.editor.clone();
 763        let supports_embedded_context =
 764            self.session_capabilities.read().supports_embedded_context();
 765
 766        cx.spawn(async move |_, cx| {
 767            let contents = contents.await?;
 768            let mut all_tracked_buffers = Vec::new();
 769
 770            let result = editor.update(cx, |editor, cx| {
 771                let text = editor.text(cx);
 772                let (mut ix, _) = text
 773                    .char_indices()
 774                    .find(|(_, c)| !c.is_whitespace())
 775                    .unwrap_or((0, '\0'));
 776                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
 777                editor.display_map.update(cx, |map, cx| {
 778                    let snapshot = map.snapshot(cx);
 779                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 780                        let Some((uri, mention)) = contents.get(&crease_id) else {
 781                            continue;
 782                        };
 783
 784                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot());
 785                        if crease_range.start.0 > ix {
 786                            let chunk = text[ix..crease_range.start.0].into();
 787                            chunks.push(chunk);
 788                        }
 789                        let chunk = match mention {
 790                            Mention::Text {
 791                                content,
 792                                tracked_buffers,
 793                            } => {
 794                                all_tracked_buffers.extend(tracked_buffers.iter().cloned());
 795                                if supports_embedded_context {
 796                                    acp::ContentBlock::Resource(acp::EmbeddedResource::new(
 797                                        acp::EmbeddedResourceResource::TextResourceContents(
 798                                            acp::TextResourceContents::new(
 799                                                content.clone(),
 800                                                uri.to_uri().to_string(),
 801                                            ),
 802                                        ),
 803                                    ))
 804                                } else {
 805                                    acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
 806                                        uri.name(),
 807                                        uri.to_uri().to_string(),
 808                                    ))
 809                                }
 810                            }
 811                            Mention::Image(mention_image) => acp::ContentBlock::Image(
 812                                acp::ImageContent::new(
 813                                    mention_image.data.clone(),
 814                                    mention_image.format.mime_type(),
 815                                )
 816                                .uri(match uri {
 817                                    MentionUri::File { .. } => Some(uri.to_uri().to_string()),
 818                                    MentionUri::PastedImage => None,
 819                                    other => {
 820                                        debug_panic!(
 821                                            "unexpected mention uri for image: {:?}",
 822                                            other
 823                                        );
 824                                        None
 825                                    }
 826                                }),
 827                            ),
 828                            Mention::Link => acp::ContentBlock::ResourceLink(
 829                                acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()),
 830                            ),
 831                        };
 832                        chunks.push(chunk);
 833                        ix = crease_range.end.0;
 834                    }
 835
 836                    if ix < text.len() {
 837                        let last_chunk = text[ix..].trim_end().to_owned();
 838                        if !last_chunk.is_empty() {
 839                            chunks.push(last_chunk.into());
 840                        }
 841                    }
 842                });
 843                anyhow::Ok((chunks, all_tracked_buffers))
 844            })?;
 845            Ok(result)
 846        })
 847    }
 848
 849    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 850        self.editor.update(cx, |editor, cx| {
 851            editor.clear(window, cx);
 852            editor.remove_creases(
 853                self.mention_set.update(cx, |mention_set, _cx| {
 854                    mention_set
 855                        .clear()
 856                        .map(|(crease_id, _)| crease_id)
 857                        .collect::<Vec<_>>()
 858                }),
 859                cx,
 860            )
 861        });
 862    }
 863
 864    pub fn send(&mut self, cx: &mut Context<Self>) {
 865        if !self.is_empty(cx) {
 866            self.editor.update(cx, |editor, cx| {
 867                editor.clear_inlay_hints(cx);
 868            });
 869        }
 870        cx.emit(MessageEditorEvent::Send)
 871    }
 872
 873    pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 874        self.insert_context_prefix("@", window, cx);
 875    }
 876
 877    pub fn insert_context_type(
 878        &mut self,
 879        context_keyword: &str,
 880        window: &mut Window,
 881        cx: &mut Context<Self>,
 882    ) {
 883        let prefix = format!("@{}", context_keyword);
 884        self.insert_context_prefix(&prefix, window, cx);
 885    }
 886
 887    fn insert_context_prefix(&mut self, prefix: &str, window: &mut Window, cx: &mut Context<Self>) {
 888        let editor = self.editor.clone();
 889        let prefix = prefix.to_string();
 890
 891        cx.spawn_in(window, async move |_, cx| {
 892            editor
 893                .update_in(cx, |editor, window, cx| {
 894                    let menu_is_open =
 895                        editor.context_menu().borrow().as_ref().is_some_and(|menu| {
 896                            matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
 897                        });
 898
 899                    let has_prefix = {
 900                        let snapshot = editor.display_snapshot(cx);
 901                        let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
 902                        let offset = cursor.to_offset(&snapshot);
 903                        let buffer_snapshot = snapshot.buffer_snapshot();
 904                        let prefix_char_count = prefix.chars().count();
 905                        buffer_snapshot
 906                            .reversed_chars_at(offset)
 907                            .take(prefix_char_count)
 908                            .eq(prefix.chars().rev())
 909                    };
 910
 911                    if menu_is_open && has_prefix {
 912                        return;
 913                    }
 914
 915                    editor.insert(&prefix, window, cx);
 916                    editor.show_completions(&editor::actions::ShowCompletions, window, cx);
 917                })
 918                .log_err();
 919        })
 920        .detach();
 921    }
 922
 923    fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
 924        self.send(cx);
 925    }
 926
 927    fn send_immediately(&mut self, _: &SendImmediately, _: &mut Window, cx: &mut Context<Self>) {
 928        if self.is_empty(cx) {
 929            return;
 930        }
 931
 932        self.editor.update(cx, |editor, cx| {
 933            editor.clear_inlay_hints(cx);
 934        });
 935
 936        cx.emit(MessageEditorEvent::SendImmediately)
 937    }
 938
 939    fn chat_with_follow(
 940        &mut self,
 941        _: &ChatWithFollow,
 942        window: &mut Window,
 943        cx: &mut Context<Self>,
 944    ) {
 945        self.workspace
 946            .update(cx, |this, cx| {
 947                this.follow(CollaboratorId::Agent, window, cx)
 948            })
 949            .log_err();
 950
 951        self.send(cx);
 952    }
 953
 954    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 955        cx.emit(MessageEditorEvent::Cancel)
 956    }
 957
 958    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 959        let Some(workspace) = self.workspace.upgrade() else {
 960            return;
 961        };
 962        let editor_clipboard_selections = cx.read_from_clipboard().and_then(|item| {
 963            item.entries().iter().find_map(|entry| match entry {
 964                ClipboardEntry::String(text) => {
 965                    text.metadata_json::<Vec<editor::ClipboardSelection>>()
 966                }
 967                _ => None,
 968            })
 969        });
 970
 971        // Insert creases for pasted clipboard selections that:
 972        // 1. Contain exactly one selection
 973        // 2. Have an associated file path
 974        // 3. Span multiple lines (not single-line selections)
 975        // 4. Belong to a file that exists in the current project
 976        let should_insert_creases = util::maybe!({
 977            let selections = editor_clipboard_selections.as_ref()?;
 978            if selections.len() > 1 {
 979                return Some(false);
 980            }
 981            let selection = selections.first()?;
 982            let file_path = selection.file_path.as_ref()?;
 983            let line_range = selection.line_range.as_ref()?;
 984
 985            if line_range.start() == line_range.end() {
 986                return Some(false);
 987            }
 988
 989            Some(
 990                workspace
 991                    .read(cx)
 992                    .project()
 993                    .read(cx)
 994                    .project_path_for_absolute_path(file_path, cx)
 995                    .is_some(),
 996            )
 997        })
 998        .unwrap_or(false);
 999
1000        if should_insert_creases && let Some(selections) = editor_clipboard_selections {
1001            cx.stop_propagation();
1002            let insertion_target = self
1003                .editor
1004                .read(cx)
1005                .selections
1006                .newest_anchor()
1007                .start
1008                .text_anchor;
1009
1010            let project = workspace.read(cx).project().clone();
1011            for selection in selections {
1012                if let (Some(file_path), Some(line_range)) =
1013                    (selection.file_path, selection.line_range)
1014                {
1015                    let crease_text =
1016                        acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
1017
1018                    let mention_uri = MentionUri::Selection {
1019                        abs_path: Some(file_path.clone()),
1020                        line_range: line_range.clone(),
1021                    };
1022
1023                    let mention_text = mention_uri.as_link().to_string();
1024                    let (excerpt_id, text_anchor, content_len) =
1025                        self.editor.update(cx, |editor, cx| {
1026                            let buffer = editor.buffer().read(cx);
1027                            let snapshot = buffer.snapshot(cx);
1028                            let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
1029                            let text_anchor = insertion_target.bias_left(&buffer_snapshot);
1030
1031                            editor.insert(&mention_text, window, cx);
1032                            editor.insert(" ", window, cx);
1033
1034                            (excerpt_id, text_anchor, mention_text.len())
1035                        });
1036
1037                    let Some((crease_id, tx)) = insert_crease_for_mention(
1038                        excerpt_id,
1039                        text_anchor,
1040                        content_len,
1041                        crease_text.into(),
1042                        mention_uri.icon_path(cx),
1043                        mention_uri.tooltip_text(),
1044                        Some(mention_uri.clone()),
1045                        Some(self.workspace.clone()),
1046                        None,
1047                        self.editor.clone(),
1048                        window,
1049                        cx,
1050                    ) else {
1051                        continue;
1052                    };
1053                    drop(tx);
1054
1055                    let mention_task = cx
1056                        .spawn({
1057                            let project = project.clone();
1058                            async move |_, cx| {
1059                                let project_path = project
1060                                    .update(cx, |project, cx| {
1061                                        project.project_path_for_absolute_path(&file_path, cx)
1062                                    })
1063                                    .ok_or_else(|| "project path not found".to_string())?;
1064
1065                                let buffer = project
1066                                    .update(cx, |project, cx| project.open_buffer(project_path, cx))
1067                                    .await
1068                                    .map_err(|e| e.to_string())?;
1069
1070                                Ok(buffer.update(cx, |buffer, cx| {
1071                                    let start =
1072                                        Point::new(*line_range.start(), 0).min(buffer.max_point());
1073                                    let end = Point::new(*line_range.end() + 1, 0)
1074                                        .min(buffer.max_point());
1075                                    let content = buffer.text_for_range(start..end).collect();
1076                                    Mention::Text {
1077                                        content,
1078                                        tracked_buffers: vec![cx.entity()],
1079                                    }
1080                                }))
1081                            }
1082                        })
1083                        .shared();
1084
1085                    self.mention_set.update(cx, |mention_set, _cx| {
1086                        mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
1087                    });
1088                }
1089            }
1090            return;
1091        }
1092        // Handle text paste with potential markdown mention links before
1093        // clipboard context entries so markdown text still pastes as text.
1094        if let Some(clipboard_text) = cx.read_from_clipboard().and_then(|item| {
1095            item.entries().iter().find_map(|entry| match entry {
1096                ClipboardEntry::String(text) => Some(text.text().to_string()),
1097                _ => None,
1098            })
1099        }) {
1100            if clipboard_text.contains("[@") {
1101                cx.stop_propagation();
1102                let selections_before = self.editor.update(cx, |editor, cx| {
1103                    let snapshot = editor.buffer().read(cx).snapshot(cx);
1104                    editor
1105                        .selections
1106                        .disjoint_anchors()
1107                        .iter()
1108                        .map(|selection| {
1109                            (
1110                                selection.start.bias_left(&snapshot),
1111                                selection.end.bias_right(&snapshot),
1112                            )
1113                        })
1114                        .collect::<Vec<_>>()
1115                });
1116
1117                self.editor.update(cx, |editor, cx| {
1118                    editor.insert(&clipboard_text, window, cx);
1119                });
1120
1121                let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
1122                let path_style = workspace.read(cx).project().read(cx).path_style(cx);
1123
1124                let mut all_mentions = Vec::new();
1125                for (start_anchor, end_anchor) in selections_before {
1126                    let start_offset = start_anchor.to_offset(&snapshot);
1127                    let end_offset = end_anchor.to_offset(&snapshot);
1128
1129                    // Get the actual inserted text from the buffer (may differ due to auto-indent)
1130                    let inserted_text: String =
1131                        snapshot.text_for_range(start_offset..end_offset).collect();
1132
1133                    let parsed_mentions = parse_mention_links(&inserted_text, path_style);
1134                    for (range, mention_uri) in parsed_mentions {
1135                        let mention_start_offset = MultiBufferOffset(start_offset.0 + range.start);
1136                        let anchor = snapshot.anchor_before(mention_start_offset);
1137                        let content_len = range.end - range.start;
1138                        all_mentions.push((anchor, content_len, mention_uri));
1139                    }
1140                }
1141
1142                if !all_mentions.is_empty() {
1143                    let supports_images = self.session_capabilities.read().supports_images();
1144                    let http_client = workspace.read(cx).client().http_client();
1145
1146                    for (anchor, content_len, mention_uri) in all_mentions {
1147                        let Some((crease_id, tx)) = insert_crease_for_mention(
1148                            anchor.excerpt_id,
1149                            anchor.text_anchor,
1150                            content_len,
1151                            mention_uri.name().into(),
1152                            mention_uri.icon_path(cx),
1153                            mention_uri.tooltip_text(),
1154                            Some(mention_uri.clone()),
1155                            Some(self.workspace.clone()),
1156                            None,
1157                            self.editor.clone(),
1158                            window,
1159                            cx,
1160                        ) else {
1161                            continue;
1162                        };
1163
1164                        // Create the confirmation task based on the mention URI type.
1165                        // This properly loads file content, fetches URLs, etc.
1166                        let task = self.mention_set.update(cx, |mention_set, cx| {
1167                            mention_set.confirm_mention_for_uri(
1168                                mention_uri.clone(),
1169                                supports_images,
1170                                http_client.clone(),
1171                                cx,
1172                            )
1173                        });
1174                        let task = cx
1175                            .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
1176                            .shared();
1177
1178                        self.mention_set.update(cx, |mention_set, _cx| {
1179                            mention_set.insert_mention(crease_id, mention_uri.clone(), task.clone())
1180                        });
1181
1182                        // Drop the tx after inserting to signal the crease is ready
1183                        drop(tx);
1184                    }
1185                    return;
1186                }
1187            }
1188        }
1189
1190        if self.handle_pasted_context(window, cx) {
1191            return;
1192        }
1193
1194        // Fall through to default editor paste
1195        cx.propagate();
1196    }
1197
1198    fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
1199        let editor = self.editor.clone();
1200        window.defer(cx, move |window, cx| {
1201            editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
1202        });
1203    }
1204
1205    fn handle_pasted_context(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1206        let Some(clipboard) = cx.read_from_clipboard() else {
1207            return false;
1208        };
1209
1210        if matches!(
1211            clipboard.entries().first(),
1212            Some(ClipboardEntry::String(_)) | None
1213        ) {
1214            return false;
1215        }
1216
1217        let Some(workspace) = self.workspace.upgrade() else {
1218            return false;
1219        };
1220        let project = workspace.read(cx).project().clone();
1221        let project_is_local = project.read(cx).is_local();
1222        let supports_images = self.session_capabilities.read().supports_images();
1223        if !project_is_local && !supports_images {
1224            return false;
1225        }
1226        let editor = self.editor.clone();
1227        let mention_set = self.mention_set.clone();
1228        let workspace = self.workspace.clone();
1229        let entries = clipboard.into_entries().collect::<Vec<_>>();
1230
1231        cx.stop_propagation();
1232
1233        window
1234            .spawn(cx, async move |mut cx| {
1235                let (items, added_worktrees) = resolve_pasted_context_items(
1236                    project,
1237                    project_is_local,
1238                    supports_images,
1239                    entries,
1240                    &mut cx,
1241                )
1242                .await;
1243                insert_resolved_pasted_context_items(
1244                    items,
1245                    added_worktrees,
1246                    editor,
1247                    mention_set,
1248                    workspace,
1249                    supports_images,
1250                    &mut cx,
1251                )
1252                .await;
1253                Ok::<(), anyhow::Error>(())
1254            })
1255            .detach_and_log_err(cx);
1256
1257        true
1258    }
1259
1260    pub fn insert_dragged_files(
1261        &mut self,
1262        paths: Vec<project::ProjectPath>,
1263        added_worktrees: Vec<Entity<Worktree>>,
1264        window: &mut Window,
1265        cx: &mut Context<Self>,
1266    ) {
1267        let Some(workspace) = self.workspace.upgrade() else {
1268            return;
1269        };
1270        let project = workspace.read(cx).project().clone();
1271        let supports_images = self.session_capabilities.read().supports_images();
1272        let mut tasks = Vec::new();
1273        for path in paths {
1274            if let Some(task) = insert_mention_for_project_path(
1275                &path,
1276                MentionInsertPosition::EndOfBuffer,
1277                &self.editor,
1278                &self.mention_set,
1279                &project,
1280                &workspace,
1281                supports_images,
1282                window,
1283                cx,
1284            ) {
1285                tasks.push(task);
1286            }
1287        }
1288        cx.spawn(async move |_, _| {
1289            join_all(tasks).await;
1290            drop(added_worktrees);
1291        })
1292        .detach();
1293    }
1294
1295    /// Inserts code snippets as creases into the editor.
1296    /// Each tuple contains (code_text, crease_title).
1297    pub fn insert_code_creases(
1298        &mut self,
1299        creases: Vec<(String, String)>,
1300        window: &mut Window,
1301        cx: &mut Context<Self>,
1302    ) {
1303        self.editor.update(cx, |editor, cx| {
1304            editor.insert("\n", window, cx);
1305        });
1306        for (text, crease_title) in creases {
1307            self.insert_crease_impl(text, crease_title, IconName::TextSnippet, true, window, cx);
1308        }
1309    }
1310
1311    pub fn insert_terminal_crease(
1312        &mut self,
1313        text: String,
1314        window: &mut Window,
1315        cx: &mut Context<Self>,
1316    ) {
1317        let line_count = text.lines().count() as u32;
1318        let mention_uri = MentionUri::TerminalSelection { line_count };
1319        let mention_text = mention_uri.as_link().to_string();
1320
1321        let (excerpt_id, text_anchor, content_len) = self.editor.update(cx, |editor, cx| {
1322            let buffer = editor.buffer().read(cx);
1323            let snapshot = buffer.snapshot(cx);
1324            let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
1325            let text_anchor = editor
1326                .selections
1327                .newest_anchor()
1328                .start
1329                .text_anchor
1330                .bias_left(&buffer_snapshot);
1331
1332            editor.insert(&mention_text, window, cx);
1333            editor.insert(" ", window, cx);
1334
1335            (excerpt_id, text_anchor, mention_text.len())
1336        });
1337
1338        let Some((crease_id, tx)) = insert_crease_for_mention(
1339            excerpt_id,
1340            text_anchor,
1341            content_len,
1342            mention_uri.name().into(),
1343            mention_uri.icon_path(cx),
1344            mention_uri.tooltip_text(),
1345            Some(mention_uri.clone()),
1346            Some(self.workspace.clone()),
1347            None,
1348            self.editor.clone(),
1349            window,
1350            cx,
1351        ) else {
1352            return;
1353        };
1354        drop(tx);
1355
1356        let mention_task = Task::ready(Ok(Mention::Text {
1357            content: text,
1358            tracked_buffers: vec![],
1359        }))
1360        .shared();
1361
1362        self.mention_set.update(cx, |mention_set, _| {
1363            mention_set.insert_mention(crease_id, mention_uri, mention_task);
1364        });
1365    }
1366
1367    pub fn insert_branch_diff_crease(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1368        let Some(workspace) = self.workspace.upgrade() else {
1369            return;
1370        };
1371
1372        let project = workspace.read(cx).project().clone();
1373
1374        let Some(repo) = project.read(cx).active_repository(cx) else {
1375            return;
1376        };
1377
1378        let default_branch_receiver = repo.update(cx, |repo, _| repo.default_branch(false));
1379        let editor = self.editor.clone();
1380        let mention_set = self.mention_set.clone();
1381        let weak_workspace = self.workspace.clone();
1382
1383        window
1384            .spawn(cx, async move |cx| {
1385                let base_ref: SharedString = default_branch_receiver
1386                    .await
1387                    .ok()
1388                    .and_then(|r| r.ok())
1389                    .flatten()
1390                    .ok_or_else(|| anyhow!("Could not determine default branch"))?;
1391
1392                cx.update(|window, cx| {
1393                    let mention_uri = MentionUri::GitDiff {
1394                        base_ref: base_ref.to_string(),
1395                    };
1396                    let mention_text = mention_uri.as_link().to_string();
1397
1398                    let (excerpt_id, text_anchor, content_len) = editor.update(cx, |editor, cx| {
1399                        let buffer = editor.buffer().read(cx);
1400                        let snapshot = buffer.snapshot(cx);
1401                        let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
1402                        let text_anchor = editor
1403                            .selections
1404                            .newest_anchor()
1405                            .start
1406                            .text_anchor
1407                            .bias_left(&buffer_snapshot);
1408
1409                        editor.insert(&mention_text, window, cx);
1410                        editor.insert(" ", window, cx);
1411
1412                        (excerpt_id, text_anchor, mention_text.len())
1413                    });
1414
1415                    let Some((crease_id, tx)) = insert_crease_for_mention(
1416                        excerpt_id,
1417                        text_anchor,
1418                        content_len,
1419                        mention_uri.name().into(),
1420                        mention_uri.icon_path(cx),
1421                        mention_uri.tooltip_text(),
1422                        Some(mention_uri.clone()),
1423                        Some(weak_workspace),
1424                        None,
1425                        editor,
1426                        window,
1427                        cx,
1428                    ) else {
1429                        return;
1430                    };
1431                    drop(tx);
1432
1433                    let confirm_task = mention_set.update(cx, |mention_set, cx| {
1434                        mention_set.confirm_mention_for_git_diff(base_ref, cx)
1435                    });
1436
1437                    let mention_task = cx
1438                        .spawn(async move |_cx| confirm_task.await.map_err(|e| e.to_string()))
1439                        .shared();
1440
1441                    mention_set.update(cx, |mention_set, _| {
1442                        mention_set.insert_mention(crease_id, mention_uri, mention_task);
1443                    });
1444                })
1445            })
1446            .detach_and_log_err(cx);
1447    }
1448
1449    fn insert_crease_impl(
1450        &mut self,
1451        text: String,
1452        title: String,
1453        icon: IconName,
1454        add_trailing_newline: bool,
1455        window: &mut Window,
1456        cx: &mut Context<Self>,
1457    ) {
1458        use editor::display_map::{Crease, FoldPlaceholder};
1459        use multi_buffer::MultiBufferRow;
1460        use rope::Point;
1461
1462        self.editor.update(cx, |editor, cx| {
1463            let point = editor
1464                .selections
1465                .newest::<Point>(&editor.display_snapshot(cx))
1466                .head();
1467            let start_row = MultiBufferRow(point.row);
1468
1469            editor.insert(&text, window, cx);
1470
1471            let snapshot = editor.buffer().read(cx).snapshot(cx);
1472            let anchor_before = snapshot.anchor_after(point);
1473            let anchor_after = editor
1474                .selections
1475                .newest_anchor()
1476                .head()
1477                .bias_left(&snapshot);
1478
1479            if add_trailing_newline {
1480                editor.insert("\n", window, cx);
1481            }
1482
1483            let fold_placeholder = FoldPlaceholder {
1484                render: Arc::new({
1485                    let title = title.clone();
1486                    move |_fold_id, _fold_range, _cx| {
1487                        Button::new("crease", title.clone())
1488                            .layer(ElevationIndex::ElevatedSurface)
1489                            .start_icon(Icon::new(icon))
1490                            .into_any_element()
1491                    }
1492                }),
1493                merge_adjacent: false,
1494                ..Default::default()
1495            };
1496
1497            let crease = Crease::inline(
1498                anchor_before..anchor_after,
1499                fold_placeholder,
1500                |row, is_folded, fold, _window, _cx| {
1501                    Disclosure::new(("crease-toggle", row.0 as u64), !is_folded)
1502                        .toggle_state(is_folded)
1503                        .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
1504                        .into_any_element()
1505                },
1506                |_, _, _, _| gpui::Empty.into_any(),
1507            );
1508            editor.insert_creases(vec![crease], cx);
1509            editor.fold_at(start_row, window, cx);
1510        });
1511    }
1512
1513    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1514        let editor = self.editor.read(cx);
1515        let editor_buffer = editor.buffer().read(cx);
1516        let Some(buffer) = editor_buffer.as_singleton() else {
1517            return;
1518        };
1519        let cursor_anchor = editor.selections.newest_anchor().head();
1520        let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
1521        let anchor = buffer.update(cx, |buffer, _cx| {
1522            buffer.anchor_before(cursor_offset.0.min(buffer.len()))
1523        });
1524        let Some(workspace) = self.workspace.upgrade() else {
1525            return;
1526        };
1527        let Some(completion) =
1528            PromptCompletionProvider::<MessageEditorCompletionDelegate>::completion_for_action(
1529                PromptContextAction::AddSelections,
1530                anchor..anchor,
1531                self.editor.downgrade(),
1532                self.mention_set.downgrade(),
1533                &workspace,
1534                cx,
1535            )
1536        else {
1537            return;
1538        };
1539
1540        self.editor.update(cx, |message_editor, cx| {
1541            message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
1542            message_editor.request_autoscroll(Autoscroll::fit(), cx);
1543        });
1544        if let Some(confirm) = completion.confirm {
1545            confirm(CompletionIntent::Complete, window, cx);
1546        }
1547    }
1548
1549    pub fn add_images_from_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1550        if !self.session_capabilities.read().supports_images() {
1551            return;
1552        }
1553
1554        let editor = self.editor.clone();
1555        let mention_set = self.mention_set.clone();
1556        let workspace = self.workspace.clone();
1557
1558        let paths_receiver = cx.prompt_for_paths(gpui::PathPromptOptions {
1559            files: true,
1560            directories: false,
1561            multiple: true,
1562            prompt: Some("Select Images".into()),
1563        });
1564
1565        window
1566            .spawn(cx, async move |cx| {
1567                let paths = match paths_receiver.await {
1568                    Ok(Ok(Some(paths))) => paths,
1569                    _ => return Ok::<(), anyhow::Error>(()),
1570                };
1571
1572                let default_image_name: SharedString = "Image".into();
1573                let images = cx
1574                    .background_spawn(async move {
1575                        paths
1576                            .into_iter()
1577                            .filter_map(|path| {
1578                                crate::mention_set::load_external_image_from_path(
1579                                    &path,
1580                                    &default_image_name,
1581                                )
1582                            })
1583                            .collect::<Vec<_>>()
1584                    })
1585                    .await;
1586
1587                crate::mention_set::insert_images_as_context(
1588                    images,
1589                    editor,
1590                    mention_set,
1591                    workspace,
1592                    cx,
1593                )
1594                .await;
1595                Ok(())
1596            })
1597            .detach_and_log_err(cx);
1598    }
1599
1600    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
1601        self.editor.update(cx, |message_editor, cx| {
1602            message_editor.set_read_only(read_only);
1603            cx.notify()
1604        })
1605    }
1606
1607    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
1608        self.editor.update(cx, |editor, cx| {
1609            if *editor.mode() != mode {
1610                editor.set_mode(mode);
1611                cx.notify()
1612            }
1613        });
1614    }
1615
1616    pub fn set_message(
1617        &mut self,
1618        message: Vec<acp::ContentBlock>,
1619        window: &mut Window,
1620        cx: &mut Context<Self>,
1621    ) {
1622        self.clear(window, cx);
1623        self.insert_message_blocks(message, false, window, cx);
1624    }
1625
1626    pub fn append_message(
1627        &mut self,
1628        message: Vec<acp::ContentBlock>,
1629        separator: Option<&str>,
1630        window: &mut Window,
1631        cx: &mut Context<Self>,
1632    ) {
1633        if message.is_empty() {
1634            return;
1635        }
1636
1637        if let Some(separator) = separator
1638            && !separator.is_empty()
1639            && !self.is_empty(cx)
1640        {
1641            self.editor.update(cx, |editor, cx| {
1642                editor.insert(separator, window, cx);
1643            });
1644        }
1645
1646        self.insert_message_blocks(message, true, window, cx);
1647    }
1648
1649    fn insert_message_blocks(
1650        &mut self,
1651        message: Vec<acp::ContentBlock>,
1652        append_to_existing: bool,
1653        window: &mut Window,
1654        cx: &mut Context<Self>,
1655    ) {
1656        let Some(workspace) = self.workspace.upgrade() else {
1657            return;
1658        };
1659
1660        let path_style = workspace.read(cx).project().read(cx).path_style(cx);
1661        let mut text = String::new();
1662        let mut mentions = Vec::new();
1663
1664        for chunk in message {
1665            match chunk {
1666                acp::ContentBlock::Text(text_content) => {
1667                    text.push_str(&text_content.text);
1668                }
1669                acp::ContentBlock::Resource(acp::EmbeddedResource {
1670                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
1671                    ..
1672                }) => {
1673                    let Some(mention_uri) = MentionUri::parse(&resource.uri, path_style).log_err()
1674                    else {
1675                        continue;
1676                    };
1677                    let start = text.len();
1678                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1679                    let end = text.len();
1680                    mentions.push((
1681                        start..end,
1682                        mention_uri,
1683                        Mention::Text {
1684                            content: resource.text,
1685                            tracked_buffers: Vec::new(),
1686                        },
1687                    ));
1688                }
1689                acp::ContentBlock::ResourceLink(resource) => {
1690                    if let Some(mention_uri) =
1691                        MentionUri::parse(&resource.uri, path_style).log_err()
1692                    {
1693                        let start = text.len();
1694                        write!(&mut text, "{}", mention_uri.as_link()).ok();
1695                        let end = text.len();
1696                        mentions.push((start..end, mention_uri, Mention::Link));
1697                    }
1698                }
1699                acp::ContentBlock::Image(acp::ImageContent {
1700                    uri,
1701                    data,
1702                    mime_type,
1703                    ..
1704                }) => {
1705                    let mention_uri = if let Some(uri) = uri {
1706                        MentionUri::parse(&uri, path_style)
1707                    } else {
1708                        Ok(MentionUri::PastedImage)
1709                    };
1710                    let Some(mention_uri) = mention_uri.log_err() else {
1711                        continue;
1712                    };
1713                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
1714                        log::error!("failed to parse MIME type for image: {mime_type:?}");
1715                        continue;
1716                    };
1717                    let start = text.len();
1718                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1719                    let end = text.len();
1720                    mentions.push((
1721                        start..end,
1722                        mention_uri,
1723                        Mention::Image(MentionImage {
1724                            data: data.into(),
1725                            format,
1726                        }),
1727                    ));
1728                }
1729                _ => {}
1730            }
1731        }
1732
1733        if text.is_empty() && mentions.is_empty() {
1734            return;
1735        }
1736
1737        let insertion_start = if append_to_existing {
1738            self.editor.read(cx).text(cx).len()
1739        } else {
1740            0
1741        };
1742
1743        let snapshot = if append_to_existing {
1744            self.editor.update(cx, |editor, cx| {
1745                editor.insert(&text, window, cx);
1746                editor.buffer().read(cx).snapshot(cx)
1747            })
1748        } else {
1749            self.editor.update(cx, |editor, cx| {
1750                editor.set_text(text, window, cx);
1751                editor.buffer().read(cx).snapshot(cx)
1752            })
1753        };
1754
1755        for (range, mention_uri, mention) in mentions {
1756            let adjusted_start = insertion_start + range.start;
1757            let anchor = snapshot.anchor_before(MultiBufferOffset(adjusted_start));
1758            let Some((crease_id, tx)) = insert_crease_for_mention(
1759                anchor.excerpt_id,
1760                anchor.text_anchor,
1761                range.end - range.start,
1762                mention_uri.name().into(),
1763                mention_uri.icon_path(cx),
1764                mention_uri.tooltip_text(),
1765                Some(mention_uri.clone()),
1766                Some(self.workspace.clone()),
1767                None,
1768                self.editor.clone(),
1769                window,
1770                cx,
1771            ) else {
1772                continue;
1773            };
1774            drop(tx);
1775
1776            self.mention_set.update(cx, |mention_set, _cx| {
1777                mention_set.insert_mention(
1778                    crease_id,
1779                    mention_uri.clone(),
1780                    Task::ready(Ok(mention)).shared(),
1781                )
1782            });
1783        }
1784
1785        cx.notify();
1786    }
1787
1788    pub fn text(&self, cx: &App) -> String {
1789        self.editor.read(cx).text(cx)
1790    }
1791
1792    pub fn set_cursor_offset(
1793        &mut self,
1794        offset: usize,
1795        window: &mut Window,
1796        cx: &mut Context<Self>,
1797    ) {
1798        self.editor.update(cx, |editor, cx| {
1799            let snapshot = editor.buffer().read(cx).snapshot(cx);
1800            let offset = snapshot.clip_offset(MultiBufferOffset(offset), text::Bias::Left);
1801            editor.change_selections(Default::default(), window, cx, |selections| {
1802                selections.select_ranges([offset..offset]);
1803            });
1804        });
1805    }
1806
1807    pub fn insert_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1808        if text.is_empty() {
1809            return;
1810        }
1811
1812        self.editor.update(cx, |editor, cx| {
1813            editor.insert(text, window, cx);
1814        });
1815    }
1816
1817    pub fn set_placeholder_text(
1818        &mut self,
1819        placeholder: &str,
1820        window: &mut Window,
1821        cx: &mut Context<Self>,
1822    ) {
1823        self.editor.update(cx, |editor, cx| {
1824            editor.set_placeholder_text(placeholder, window, cx);
1825        });
1826    }
1827
1828    #[cfg(any(test, feature = "test-support"))]
1829    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1830        self.editor.update(cx, |editor, cx| {
1831            editor.set_text(text, window, cx);
1832        });
1833    }
1834}
1835
1836impl Focusable for MessageEditor {
1837    fn focus_handle(&self, cx: &App) -> FocusHandle {
1838        self.editor.focus_handle(cx)
1839    }
1840}
1841
1842impl Render for MessageEditor {
1843    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1844        div()
1845            .key_context("MessageEditor")
1846            .on_action(cx.listener(Self::chat))
1847            .on_action(cx.listener(Self::send_immediately))
1848            .on_action(cx.listener(Self::chat_with_follow))
1849            .on_action(cx.listener(Self::cancel))
1850            .on_action(cx.listener(Self::paste_raw))
1851            .capture_action(cx.listener(Self::paste))
1852            .flex_1()
1853            .child({
1854                let settings = ThemeSettings::get_global(cx);
1855
1856                let text_style = TextStyle {
1857                    color: cx.theme().colors().text,
1858                    font_family: settings.buffer_font.family.clone(),
1859                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
1860                    font_features: settings.buffer_font.features.clone(),
1861                    font_size: settings.agent_buffer_font_size(cx).into(),
1862                    font_weight: settings.buffer_font.weight,
1863                    line_height: relative(settings.buffer_line_height.value()),
1864                    ..Default::default()
1865                };
1866
1867                EditorElement::new(
1868                    &self.editor,
1869                    EditorStyle {
1870                        background: cx.theme().colors().editor_background,
1871                        local_player: cx.theme().players().local(),
1872                        text: text_style,
1873                        syntax: cx.theme().syntax().clone(),
1874                        inlay_hints_style: editor::make_inlay_hints_style(cx),
1875                        ..Default::default()
1876                    },
1877                )
1878            })
1879    }
1880}
1881
1882pub struct MessageEditorAddon {}
1883
1884impl MessageEditorAddon {
1885    pub fn new() -> Self {
1886        Self {}
1887    }
1888}
1889
1890impl Addon for MessageEditorAddon {
1891    fn to_any(&self) -> &dyn std::any::Any {
1892        self
1893    }
1894
1895    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1896        Some(self)
1897    }
1898
1899    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1900        let settings = agent_settings::AgentSettings::get_global(cx);
1901        if settings.use_modifier_to_send {
1902            key_context.add("use_modifier_to_send");
1903        }
1904    }
1905}
1906
1907/// Parses markdown mention links in the format `[@name](uri)` from text.
1908/// Returns a vector of (range, MentionUri) pairs where range is the byte range in the text.
1909fn parse_mention_links(text: &str, path_style: PathStyle) -> Vec<(Range<usize>, MentionUri)> {
1910    let mut mentions = Vec::new();
1911    let mut search_start = 0;
1912
1913    while let Some(link_start) = text[search_start..].find("[@") {
1914        let absolute_start = search_start + link_start;
1915
1916        // Find the matching closing bracket for the name, handling nested brackets.
1917        // Start at the '[' character so find_matching_bracket can track depth correctly.
1918        let Some(name_end) = find_matching_bracket(&text[absolute_start..], '[', ']') else {
1919            search_start = absolute_start + 2;
1920            continue;
1921        };
1922        let name_end = absolute_start + name_end;
1923
1924        // Check for opening parenthesis immediately after
1925        if text.get(name_end + 1..name_end + 2) != Some("(") {
1926            search_start = name_end + 1;
1927            continue;
1928        }
1929
1930        // Find the matching closing parenthesis for the URI, handling nested parens
1931        let uri_start = name_end + 2;
1932        let Some(uri_end_relative) = find_matching_bracket(&text[name_end + 1..], '(', ')') else {
1933            search_start = uri_start;
1934            continue;
1935        };
1936        let uri_end = name_end + 1 + uri_end_relative;
1937        let link_end = uri_end + 1;
1938
1939        let uri_str = &text[uri_start..uri_end];
1940
1941        // Try to parse the URI as a MentionUri
1942        if let Ok(mention_uri) = MentionUri::parse(uri_str, path_style) {
1943            mentions.push((absolute_start..link_end, mention_uri));
1944        }
1945
1946        search_start = link_end;
1947    }
1948
1949    mentions
1950}
1951
1952/// Finds the position of the matching closing bracket, handling nested brackets.
1953/// The input `text` should start with the opening bracket.
1954/// Returns the index of the matching closing bracket relative to `text`.
1955fn find_matching_bracket(text: &str, open: char, close: char) -> Option<usize> {
1956    let mut depth = 0;
1957    for (index, character) in text.char_indices() {
1958        if character == open {
1959            depth += 1;
1960        } else if character == close {
1961            depth -= 1;
1962            if depth == 0 {
1963                return Some(index);
1964            }
1965        }
1966    }
1967    None
1968}
1969
1970#[cfg(test)]
1971mod tests {
1972    use std::{ops::Range, path::Path, path::PathBuf, sync::Arc};
1973
1974    use acp_thread::MentionUri;
1975    use agent::{ThreadStore, outline};
1976    use agent_client_protocol as acp;
1977    use base64::Engine as _;
1978    use editor::{
1979        AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset, SelectionEffects,
1980        actions::Paste,
1981    };
1982
1983    use fs::FakeFs;
1984    use futures::StreamExt as _;
1985    use gpui::{
1986        AppContext, ClipboardEntry, ClipboardItem, Entity, EventEmitter, ExternalPaths,
1987        FocusHandle, Focusable, TestAppContext, VisualTestContext,
1988    };
1989    use language_model::LanguageModelRegistry;
1990    use lsp::{CompletionContext, CompletionTriggerKind};
1991    use parking_lot::RwLock;
1992    use project::{CompletionIntent, Project, ProjectPath};
1993    use serde_json::{Value, json};
1994
1995    use text::Point;
1996    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1997    use util::{path, paths::PathStyle, rel_path::rel_path};
1998    use workspace::{AppState, Item, MultiWorkspace};
1999
2000    use crate::completion_provider::PromptContextType;
2001    use crate::{
2002        conversation_view::tests::init_test,
2003        message_editor::{Mention, MessageEditor, SessionCapabilities, parse_mention_links},
2004    };
2005
2006    #[test]
2007    fn test_parse_mention_links() {
2008        // Single file mention
2009        let text = "[@bundle-mac](file:///Users/test/zed/script/bundle-mac)";
2010        let mentions = parse_mention_links(text, PathStyle::local());
2011        assert_eq!(mentions.len(), 1);
2012        assert_eq!(mentions[0].0, 0..text.len());
2013        assert!(matches!(mentions[0].1, MentionUri::File { .. }));
2014
2015        // Multiple mentions
2016        let text = "Check [@file1](file:///path/to/file1) and [@file2](file:///path/to/file2)!";
2017        let mentions = parse_mention_links(text, PathStyle::local());
2018        assert_eq!(mentions.len(), 2);
2019
2020        // Text without mentions
2021        let text = "Just some regular text without mentions";
2022        let mentions = parse_mention_links(text, PathStyle::local());
2023        assert_eq!(mentions.len(), 0);
2024
2025        // Malformed mentions (should be skipped)
2026        let text = "[@incomplete](invalid://uri) and [@missing](";
2027        let mentions = parse_mention_links(text, PathStyle::local());
2028        assert_eq!(mentions.len(), 0);
2029
2030        // Mixed content with valid mention
2031        let text = "Before [@valid](file:///path/to/file) after";
2032        let mentions = parse_mention_links(text, PathStyle::local());
2033        assert_eq!(mentions.len(), 1);
2034        assert_eq!(mentions[0].0.start, 7);
2035
2036        // HTTP URL mention (Fetch)
2037        let text = "Check out [@docs](https://example.com/docs) for more info";
2038        let mentions = parse_mention_links(text, PathStyle::local());
2039        assert_eq!(mentions.len(), 1);
2040        assert!(matches!(mentions[0].1, MentionUri::Fetch { .. }));
2041
2042        // Directory mention (trailing slash)
2043        let text = "[@src](file:///path/to/src/)";
2044        let mentions = parse_mention_links(text, PathStyle::local());
2045        assert_eq!(mentions.len(), 1);
2046        assert!(matches!(mentions[0].1, MentionUri::Directory { .. }));
2047
2048        // Multiple different mention types
2049        let text = "File [@f](file:///a) and URL [@u](https://b.com) and dir [@d](file:///c/)";
2050        let mentions = parse_mention_links(text, PathStyle::local());
2051        assert_eq!(mentions.len(), 3);
2052        assert!(matches!(mentions[0].1, MentionUri::File { .. }));
2053        assert!(matches!(mentions[1].1, MentionUri::Fetch { .. }));
2054        assert!(matches!(mentions[2].1, MentionUri::Directory { .. }));
2055
2056        // Adjacent mentions without separator
2057        let text = "[@a](file:///a)[@b](file:///b)";
2058        let mentions = parse_mention_links(text, PathStyle::local());
2059        assert_eq!(mentions.len(), 2);
2060
2061        // Regular markdown link (not a mention) should be ignored
2062        let text = "[regular link](https://example.com)";
2063        let mentions = parse_mention_links(text, PathStyle::local());
2064        assert_eq!(mentions.len(), 0);
2065
2066        // Incomplete mention link patterns
2067        let text = "[@name] without url and [@name( malformed";
2068        let mentions = parse_mention_links(text, PathStyle::local());
2069        assert_eq!(mentions.len(), 0);
2070
2071        // Nested brackets in name portion
2072        let text = "[@name [with brackets]](file:///path/to/file)";
2073        let mentions = parse_mention_links(text, PathStyle::local());
2074        assert_eq!(mentions.len(), 1);
2075        assert_eq!(mentions[0].0, 0..text.len());
2076
2077        // Deeply nested brackets
2078        let text = "[@outer [inner [deep]]](file:///path)";
2079        let mentions = parse_mention_links(text, PathStyle::local());
2080        assert_eq!(mentions.len(), 1);
2081
2082        // Unbalanced brackets should fail gracefully
2083        let text = "[@unbalanced [bracket](file:///path)";
2084        let mentions = parse_mention_links(text, PathStyle::local());
2085        assert_eq!(mentions.len(), 0);
2086
2087        // Nested parentheses in URI (common in URLs with query params)
2088        let text = "[@wiki](https://en.wikipedia.org/wiki/Rust_(programming_language))";
2089        let mentions = parse_mention_links(text, PathStyle::local());
2090        assert_eq!(mentions.len(), 1);
2091        if let MentionUri::Fetch { url } = &mentions[0].1 {
2092            assert!(url.as_str().contains("Rust_(programming_language)"));
2093        } else {
2094            panic!("Expected Fetch URI");
2095        }
2096    }
2097
2098    #[gpui::test]
2099    async fn test_at_mention_removal(cx: &mut TestAppContext) {
2100        init_test(cx);
2101
2102        let fs = FakeFs::new(cx.executor());
2103        fs.insert_tree("/project", json!({"file": ""})).await;
2104        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2105
2106        let (multi_workspace, cx) =
2107            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2108        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2109
2110        let thread_store = None;
2111
2112        let message_editor = cx.update(|window, cx| {
2113            cx.new(|cx| {
2114                MessageEditor::new(
2115                    workspace.downgrade(),
2116                    project.downgrade(),
2117                    thread_store.clone(),
2118                    None,
2119                    None,
2120                    Default::default(),
2121                    "Test Agent".into(),
2122                    "Test",
2123                    EditorMode::AutoHeight {
2124                        min_lines: 1,
2125                        max_lines: None,
2126                    },
2127                    window,
2128                    cx,
2129                )
2130            })
2131        });
2132        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
2133
2134        cx.run_until_parked();
2135
2136        let excerpt_id = editor.update(cx, |editor, cx| {
2137            editor
2138                .buffer()
2139                .read(cx)
2140                .excerpt_ids()
2141                .into_iter()
2142                .next()
2143                .unwrap()
2144        });
2145        let completions = editor.update_in(cx, |editor, window, cx| {
2146            editor.set_text("Hello @file ", window, cx);
2147            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
2148            let completion_provider = editor.completion_provider().unwrap();
2149            completion_provider.completions(
2150                excerpt_id,
2151                &buffer,
2152                text::Anchor::MAX,
2153                CompletionContext {
2154                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
2155                    trigger_character: Some("@".into()),
2156                },
2157                window,
2158                cx,
2159            )
2160        });
2161        let [_, completion]: [_; 2] = completions
2162            .await
2163            .unwrap()
2164            .into_iter()
2165            .flat_map(|response| response.completions)
2166            .collect::<Vec<_>>()
2167            .try_into()
2168            .unwrap();
2169
2170        editor.update_in(cx, |editor, window, cx| {
2171            let snapshot = editor.buffer().read(cx).snapshot(cx);
2172            let range = snapshot
2173                .anchor_range_in_excerpt(excerpt_id, completion.replace_range)
2174                .unwrap();
2175            editor.edit([(range, completion.new_text)], cx);
2176            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
2177        });
2178
2179        cx.run_until_parked();
2180
2181        // Backspace over the inserted crease (and the following space).
2182        editor.update_in(cx, |editor, window, cx| {
2183            editor.backspace(&Default::default(), window, cx);
2184            editor.backspace(&Default::default(), window, cx);
2185        });
2186
2187        let (content, _) = message_editor
2188            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2189            .await
2190            .unwrap();
2191
2192        // We don't send a resource link for the deleted crease.
2193        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
2194    }
2195
2196    #[gpui::test]
2197    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
2198        init_test(cx);
2199        let fs = FakeFs::new(cx.executor());
2200        fs.insert_tree(
2201            "/test",
2202            json!({
2203                ".zed": {
2204                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
2205                },
2206                "src": {
2207                    "main.rs": "fn main() {}",
2208                },
2209            }),
2210        )
2211        .await;
2212
2213        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
2214        let thread_store = None;
2215        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
2216            acp::PromptCapabilities::default(),
2217            vec![],
2218        )));
2219
2220        let (multi_workspace, cx) =
2221            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2222        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2223        let workspace_handle = workspace.downgrade();
2224        let message_editor = workspace.update_in(cx, |_, window, cx| {
2225            cx.new(|cx| {
2226                MessageEditor::new(
2227                    workspace_handle.clone(),
2228                    project.downgrade(),
2229                    thread_store.clone(),
2230                    None,
2231                    None,
2232                    session_capabilities.clone(),
2233                    "Claude Agent".into(),
2234                    "Test",
2235                    EditorMode::AutoHeight {
2236                        min_lines: 1,
2237                        max_lines: None,
2238                    },
2239                    window,
2240                    cx,
2241                )
2242            })
2243        });
2244        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
2245
2246        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
2247        editor.update_in(cx, |editor, window, cx| {
2248            editor.set_text("/file test.txt", window, cx);
2249        });
2250
2251        let contents_result = message_editor
2252            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2253            .await;
2254
2255        // Should fail because available_commands is empty (no commands supported)
2256        assert!(contents_result.is_err());
2257        let error_message = contents_result.unwrap_err().to_string();
2258        assert!(error_message.contains("not supported by Claude Agent"));
2259        assert!(error_message.contains("Available commands: none"));
2260
2261        // Now simulate Claude providing its list of available commands (which doesn't include file)
2262        session_capabilities
2263            .write()
2264            .set_available_commands(vec![acp::AvailableCommand::new("help", "Get help")]);
2265
2266        // Test that unsupported slash commands trigger an error when we have a list of available commands
2267        editor.update_in(cx, |editor, window, cx| {
2268            editor.set_text("/file test.txt", window, cx);
2269        });
2270
2271        let contents_result = message_editor
2272            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2273            .await;
2274
2275        assert!(contents_result.is_err());
2276        let error_message = contents_result.unwrap_err().to_string();
2277        assert!(error_message.contains("not supported by Claude Agent"));
2278        assert!(error_message.contains("/file"));
2279        assert!(error_message.contains("Available commands: /help"));
2280
2281        // Test that supported commands work fine
2282        editor.update_in(cx, |editor, window, cx| {
2283            editor.set_text("/help", window, cx);
2284        });
2285
2286        let contents_result = message_editor
2287            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2288            .await;
2289
2290        // Should succeed because /help is in available_commands
2291        assert!(contents_result.is_ok());
2292
2293        // Test that regular text works fine
2294        editor.update_in(cx, |editor, window, cx| {
2295            editor.set_text("Hello Claude!", window, cx);
2296        });
2297
2298        let (content, _) = message_editor
2299            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2300            .await
2301            .unwrap();
2302
2303        assert_eq!(content.len(), 1);
2304        if let acp::ContentBlock::Text(text) = &content[0] {
2305            assert_eq!(text.text, "Hello Claude!");
2306        } else {
2307            panic!("Expected ContentBlock::Text");
2308        }
2309
2310        // Test that @ mentions still work
2311        editor.update_in(cx, |editor, window, cx| {
2312            editor.set_text("Check this @", window, cx);
2313        });
2314
2315        // The @ mention functionality should not be affected
2316        let (content, _) = message_editor
2317            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2318            .await
2319            .unwrap();
2320
2321        assert_eq!(content.len(), 1);
2322        if let acp::ContentBlock::Text(text) = &content[0] {
2323            assert_eq!(text.text, "Check this @");
2324        } else {
2325            panic!("Expected ContentBlock::Text");
2326        }
2327    }
2328
2329    struct MessageEditorItem(Entity<MessageEditor>);
2330
2331    impl Item for MessageEditorItem {
2332        type Event = ();
2333
2334        fn include_in_nav_history() -> bool {
2335            false
2336        }
2337
2338        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
2339            "Test".into()
2340        }
2341    }
2342
2343    impl EventEmitter<()> for MessageEditorItem {}
2344
2345    impl Focusable for MessageEditorItem {
2346        fn focus_handle(&self, cx: &App) -> FocusHandle {
2347            self.0.read(cx).focus_handle(cx)
2348        }
2349    }
2350
2351    impl Render for MessageEditorItem {
2352        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
2353            self.0.clone().into_any_element()
2354        }
2355    }
2356
2357    #[gpui::test]
2358    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
2359        init_test(cx);
2360
2361        let app_state = cx.update(AppState::test);
2362
2363        cx.update(|cx| {
2364            editor::init(cx);
2365            workspace::init(app_state.clone(), cx);
2366        });
2367
2368        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2369        let window =
2370            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2371        let workspace = window
2372            .read_with(cx, |mw, _| mw.workspace().clone())
2373            .unwrap();
2374
2375        let mut cx = VisualTestContext::from_window(window.into(), cx);
2376
2377        let thread_store = None;
2378        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
2379            acp::PromptCapabilities::default(),
2380            vec![
2381                acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
2382                acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
2383                    acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
2384                        "<name>",
2385                    )),
2386                ),
2387            ],
2388        )));
2389
2390        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
2391            let workspace_handle = cx.weak_entity();
2392            let message_editor = cx.new(|cx| {
2393                MessageEditor::new(
2394                    workspace_handle,
2395                    project.downgrade(),
2396                    thread_store.clone(),
2397                    None,
2398                    None,
2399                    session_capabilities.clone(),
2400                    "Test Agent".into(),
2401                    "Test",
2402                    EditorMode::AutoHeight {
2403                        max_lines: None,
2404                        min_lines: 1,
2405                    },
2406                    window,
2407                    cx,
2408                )
2409            });
2410            workspace.active_pane().update(cx, |pane, cx| {
2411                pane.add_item(
2412                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2413                    true,
2414                    true,
2415                    None,
2416                    window,
2417                    cx,
2418                );
2419            });
2420            message_editor.read(cx).focus_handle(cx).focus(window, cx);
2421            message_editor.read(cx).editor().clone()
2422        });
2423
2424        cx.simulate_input("/");
2425
2426        editor.update_in(&mut cx, |editor, window, cx| {
2427            assert_eq!(editor.text(cx), "/");
2428            assert!(editor.has_visible_completions_menu());
2429
2430            assert_eq!(
2431                current_completion_labels_with_documentation(editor),
2432                &[
2433                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
2434                    ("say-hello".into(), "Say hello to whoever you want".into())
2435                ]
2436            );
2437            editor.set_text("", window, cx);
2438        });
2439
2440        cx.simulate_input("/qui");
2441
2442        editor.update_in(&mut cx, |editor, window, cx| {
2443            assert_eq!(editor.text(cx), "/qui");
2444            assert!(editor.has_visible_completions_menu());
2445
2446            assert_eq!(
2447                current_completion_labels_with_documentation(editor),
2448                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
2449            );
2450            editor.set_text("", window, cx);
2451        });
2452
2453        editor.update_in(&mut cx, |editor, window, cx| {
2454            assert!(editor.has_visible_completions_menu());
2455            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2456        });
2457
2458        cx.run_until_parked();
2459
2460        editor.update_in(&mut cx, |editor, window, cx| {
2461            assert_eq!(editor.display_text(cx), "/quick-math ");
2462            assert!(!editor.has_visible_completions_menu());
2463            editor.set_text("", window, cx);
2464        });
2465
2466        cx.simulate_input("/say");
2467
2468        editor.update_in(&mut cx, |editor, _window, cx| {
2469            assert_eq!(editor.display_text(cx), "/say");
2470            assert!(editor.has_visible_completions_menu());
2471
2472            assert_eq!(
2473                current_completion_labels_with_documentation(editor),
2474                &[("say-hello".into(), "Say hello to whoever you want".into())]
2475            );
2476        });
2477
2478        editor.update_in(&mut cx, |editor, window, cx| {
2479            assert!(editor.has_visible_completions_menu());
2480            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2481        });
2482
2483        cx.run_until_parked();
2484
2485        editor.update_in(&mut cx, |editor, _window, cx| {
2486            assert_eq!(editor.text(cx), "/say-hello ");
2487            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2488            assert!(!editor.has_visible_completions_menu());
2489        });
2490
2491        cx.simulate_input("GPT5");
2492
2493        cx.run_until_parked();
2494
2495        editor.update_in(&mut cx, |editor, window, cx| {
2496            assert_eq!(editor.text(cx), "/say-hello GPT5");
2497            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
2498            assert!(!editor.has_visible_completions_menu());
2499
2500            // Delete argument
2501            for _ in 0..5 {
2502                editor.backspace(&editor::actions::Backspace, window, cx);
2503            }
2504        });
2505
2506        cx.run_until_parked();
2507
2508        editor.update_in(&mut cx, |editor, window, cx| {
2509            assert_eq!(editor.text(cx), "/say-hello");
2510            // Hint is visible because argument was deleted
2511            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2512
2513            // Delete last command letter
2514            editor.backspace(&editor::actions::Backspace, window, cx);
2515        });
2516
2517        cx.run_until_parked();
2518
2519        editor.update_in(&mut cx, |editor, _window, cx| {
2520            // Hint goes away once command no longer matches an available one
2521            assert_eq!(editor.text(cx), "/say-hell");
2522            assert_eq!(editor.display_text(cx), "/say-hell");
2523            assert!(!editor.has_visible_completions_menu());
2524        });
2525    }
2526
2527    #[gpui::test]
2528    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
2529        init_test(cx);
2530
2531        let app_state = cx.update(AppState::test);
2532
2533        cx.update(|cx| {
2534            editor::init(cx);
2535            workspace::init(app_state.clone(), cx);
2536        });
2537
2538        app_state
2539            .fs
2540            .as_fake()
2541            .insert_tree(
2542                path!("/dir"),
2543                json!({
2544                    "editor": "",
2545                    "a": {
2546                        "one.txt": "1",
2547                        "two.txt": "2",
2548                        "three.txt": "3",
2549                        "four.txt": "4"
2550                    },
2551                    "b": {
2552                        "five.txt": "5",
2553                        "six.txt": "6",
2554                        "seven.txt": "7",
2555                        "eight.txt": "8",
2556                    },
2557                    "x.png": "",
2558                }),
2559            )
2560            .await;
2561
2562        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2563        let window =
2564            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2565        let workspace = window
2566            .read_with(cx, |mw, _| mw.workspace().clone())
2567            .unwrap();
2568
2569        let worktree = project.update(cx, |project, cx| {
2570            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2571            assert_eq!(worktrees.len(), 1);
2572            worktrees.pop().unwrap()
2573        });
2574        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2575
2576        let mut cx = VisualTestContext::from_window(window.into(), cx);
2577
2578        let paths = vec![
2579            rel_path("a/one.txt"),
2580            rel_path("a/two.txt"),
2581            rel_path("a/three.txt"),
2582            rel_path("a/four.txt"),
2583            rel_path("b/five.txt"),
2584            rel_path("b/six.txt"),
2585            rel_path("b/seven.txt"),
2586            rel_path("b/eight.txt"),
2587        ];
2588
2589        let slash = PathStyle::local().primary_separator();
2590
2591        let mut opened_editors = Vec::new();
2592        for path in paths {
2593            let buffer = workspace
2594                .update_in(&mut cx, |workspace, window, cx| {
2595                    workspace.open_path(
2596                        ProjectPath {
2597                            worktree_id,
2598                            path: path.into(),
2599                        },
2600                        None,
2601                        false,
2602                        window,
2603                        cx,
2604                    )
2605                })
2606                .await
2607                .unwrap();
2608            opened_editors.push(buffer);
2609        }
2610
2611        let thread_store = cx.new(|cx| ThreadStore::new(cx));
2612        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
2613            acp::PromptCapabilities::default(),
2614            vec![],
2615        )));
2616
2617        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
2618            let workspace_handle = cx.weak_entity();
2619            let message_editor = cx.new(|cx| {
2620                MessageEditor::new(
2621                    workspace_handle,
2622                    project.downgrade(),
2623                    Some(thread_store),
2624                    None,
2625                    None,
2626                    session_capabilities.clone(),
2627                    "Test Agent".into(),
2628                    "Test",
2629                    EditorMode::AutoHeight {
2630                        max_lines: None,
2631                        min_lines: 1,
2632                    },
2633                    window,
2634                    cx,
2635                )
2636            });
2637            workspace.active_pane().update(cx, |pane, cx| {
2638                pane.add_item(
2639                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2640                    true,
2641                    true,
2642                    None,
2643                    window,
2644                    cx,
2645                );
2646            });
2647            message_editor.read(cx).focus_handle(cx).focus(window, cx);
2648            let editor = message_editor.read(cx).editor().clone();
2649            (message_editor, editor)
2650        });
2651
2652        cx.simulate_input("Lorem @");
2653
2654        editor.update_in(&mut cx, |editor, window, cx| {
2655            assert_eq!(editor.text(cx), "Lorem @");
2656            assert!(editor.has_visible_completions_menu());
2657
2658            assert_eq!(
2659                current_completion_labels(editor),
2660                &[
2661                    format!("eight.txt b{slash}"),
2662                    format!("seven.txt b{slash}"),
2663                    format!("six.txt b{slash}"),
2664                    format!("five.txt b{slash}"),
2665                    "Files & Directories".into(),
2666                    "Symbols".into()
2667                ]
2668            );
2669            editor.set_text("", window, cx);
2670        });
2671
2672        message_editor.update(&mut cx, |editor, _cx| {
2673            editor.session_capabilities.write().set_prompt_capabilities(
2674                acp::PromptCapabilities::new()
2675                    .image(true)
2676                    .audio(true)
2677                    .embedded_context(true),
2678            );
2679        });
2680
2681        cx.simulate_input("Lorem ");
2682
2683        editor.update(&mut cx, |editor, cx| {
2684            assert_eq!(editor.text(cx), "Lorem ");
2685            assert!(!editor.has_visible_completions_menu());
2686        });
2687
2688        cx.simulate_input("@");
2689
2690        editor.update(&mut cx, |editor, cx| {
2691            assert_eq!(editor.text(cx), "Lorem @");
2692            assert!(editor.has_visible_completions_menu());
2693            assert_eq!(
2694                current_completion_labels(editor),
2695                &[
2696                    format!("eight.txt b{slash}"),
2697                    format!("seven.txt b{slash}"),
2698                    format!("six.txt b{slash}"),
2699                    format!("five.txt b{slash}"),
2700                    "Files & Directories".into(),
2701                    "Symbols".into(),
2702                    "Threads".into(),
2703                    "Fetch".into()
2704                ]
2705            );
2706        });
2707
2708        // Select and confirm "File"
2709        editor.update_in(&mut cx, |editor, window, cx| {
2710            assert!(editor.has_visible_completions_menu());
2711            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2712            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2713            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2714            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2715            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2716        });
2717
2718        cx.run_until_parked();
2719
2720        editor.update(&mut cx, |editor, cx| {
2721            assert_eq!(editor.text(cx), "Lorem @file ");
2722            assert!(editor.has_visible_completions_menu());
2723        });
2724
2725        cx.simulate_input("one");
2726
2727        editor.update(&mut cx, |editor, cx| {
2728            assert_eq!(editor.text(cx), "Lorem @file one");
2729            assert!(editor.has_visible_completions_menu());
2730            assert_eq!(
2731                current_completion_labels(editor),
2732                vec![format!("one.txt a{slash}")]
2733            );
2734        });
2735
2736        editor.update_in(&mut cx, |editor, window, cx| {
2737            assert!(editor.has_visible_completions_menu());
2738            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2739        });
2740
2741        let url_one = MentionUri::File {
2742            abs_path: path!("/dir/a/one.txt").into(),
2743        }
2744        .to_uri()
2745        .to_string();
2746        editor.update(&mut cx, |editor, cx| {
2747            let text = editor.text(cx);
2748            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2749            assert!(!editor.has_visible_completions_menu());
2750            assert_eq!(fold_ranges(editor, cx).len(), 1);
2751        });
2752
2753        let contents = message_editor
2754            .update(&mut cx, |message_editor, cx| {
2755                message_editor
2756                    .mention_set()
2757                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2758            })
2759            .await
2760            .unwrap()
2761            .into_values()
2762            .collect::<Vec<_>>();
2763
2764        {
2765            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
2766                panic!("Unexpected mentions");
2767            };
2768            pretty_assertions::assert_eq!(content, "1");
2769            pretty_assertions::assert_eq!(
2770                uri,
2771                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
2772            );
2773        }
2774
2775        cx.simulate_input(" ");
2776
2777        editor.update(&mut cx, |editor, cx| {
2778            let text = editor.text(cx);
2779            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
2780            assert!(!editor.has_visible_completions_menu());
2781            assert_eq!(fold_ranges(editor, cx).len(), 1);
2782        });
2783
2784        cx.simulate_input("Ipsum ");
2785
2786        editor.update(&mut cx, |editor, cx| {
2787            let text = editor.text(cx);
2788            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
2789            assert!(!editor.has_visible_completions_menu());
2790            assert_eq!(fold_ranges(editor, cx).len(), 1);
2791        });
2792
2793        cx.simulate_input("@file ");
2794
2795        editor.update(&mut cx, |editor, cx| {
2796            let text = editor.text(cx);
2797            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
2798            assert!(editor.has_visible_completions_menu());
2799            assert_eq!(fold_ranges(editor, cx).len(), 1);
2800        });
2801
2802        editor.update_in(&mut cx, |editor, window, cx| {
2803            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2804        });
2805
2806        cx.run_until_parked();
2807
2808        let contents = message_editor
2809            .update(&mut cx, |message_editor, cx| {
2810                message_editor
2811                    .mention_set()
2812                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2813            })
2814            .await
2815            .unwrap()
2816            .into_values()
2817            .collect::<Vec<_>>();
2818
2819        let url_eight = MentionUri::File {
2820            abs_path: path!("/dir/b/eight.txt").into(),
2821        }
2822        .to_uri()
2823        .to_string();
2824
2825        {
2826            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2827                panic!("Unexpected mentions");
2828            };
2829            pretty_assertions::assert_eq!(content, "8");
2830            pretty_assertions::assert_eq!(
2831                uri,
2832                &MentionUri::parse(&url_eight, PathStyle::local()).unwrap()
2833            );
2834        }
2835
2836        editor.update(&mut cx, |editor, cx| {
2837            assert_eq!(
2838                editor.text(cx),
2839                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
2840            );
2841            assert!(!editor.has_visible_completions_menu());
2842            assert_eq!(fold_ranges(editor, cx).len(), 2);
2843        });
2844
2845        let plain_text_language = Arc::new(language::Language::new(
2846            language::LanguageConfig {
2847                name: "Plain Text".into(),
2848                matcher: language::LanguageMatcher {
2849                    path_suffixes: vec!["txt".to_string()],
2850                    ..Default::default()
2851                },
2852                ..Default::default()
2853            },
2854            None,
2855        ));
2856
2857        // Register the language and fake LSP
2858        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2859        language_registry.add(plain_text_language);
2860
2861        let mut fake_language_servers = language_registry.register_fake_lsp(
2862            "Plain Text",
2863            language::FakeLspAdapter {
2864                capabilities: lsp::ServerCapabilities {
2865                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2866                    ..Default::default()
2867                },
2868                ..Default::default()
2869            },
2870        );
2871
2872        // Open the buffer to trigger LSP initialization
2873        let buffer = project
2874            .update(&mut cx, |project, cx| {
2875                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2876            })
2877            .await
2878            .unwrap();
2879
2880        // Register the buffer with language servers
2881        let _handle = project.update(&mut cx, |project, cx| {
2882            project.register_buffer_with_language_servers(&buffer, cx)
2883        });
2884
2885        cx.run_until_parked();
2886
2887        let fake_language_server = fake_language_servers.next().await.unwrap();
2888        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2889            move |_, _| async move {
2890                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2891                    #[allow(deprecated)]
2892                    lsp::SymbolInformation {
2893                        name: "MySymbol".into(),
2894                        location: lsp::Location {
2895                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2896                            range: lsp::Range::new(
2897                                lsp::Position::new(0, 0),
2898                                lsp::Position::new(0, 1),
2899                            ),
2900                        },
2901                        kind: lsp::SymbolKind::CONSTANT,
2902                        tags: None,
2903                        container_name: None,
2904                        deprecated: None,
2905                    },
2906                ])))
2907            },
2908        );
2909
2910        cx.simulate_input("@symbol ");
2911
2912        editor.update(&mut cx, |editor, cx| {
2913            assert_eq!(
2914                editor.text(cx),
2915                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
2916            );
2917            assert!(editor.has_visible_completions_menu());
2918            assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]);
2919        });
2920
2921        editor.update_in(&mut cx, |editor, window, cx| {
2922            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2923        });
2924
2925        let symbol = MentionUri::Symbol {
2926            abs_path: path!("/dir/a/one.txt").into(),
2927            name: "MySymbol".into(),
2928            line_range: 0..=0,
2929        };
2930
2931        let contents = message_editor
2932            .update(&mut cx, |message_editor, cx| {
2933                message_editor
2934                    .mention_set()
2935                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2936            })
2937            .await
2938            .unwrap()
2939            .into_values()
2940            .collect::<Vec<_>>();
2941
2942        {
2943            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2944                panic!("Unexpected mentions");
2945            };
2946            pretty_assertions::assert_eq!(content, "1");
2947            pretty_assertions::assert_eq!(uri, &symbol);
2948        }
2949
2950        cx.run_until_parked();
2951
2952        editor.read_with(&cx, |editor, cx| {
2953            assert_eq!(
2954                editor.text(cx),
2955                format!(
2956                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2957                    symbol.to_uri(),
2958                )
2959            );
2960        });
2961
2962        // Try to mention an "image" file that will fail to load
2963        cx.simulate_input("@file x.png");
2964
2965        editor.update(&mut cx, |editor, cx| {
2966            assert_eq!(
2967                editor.text(cx),
2968                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2969            );
2970            assert!(editor.has_visible_completions_menu());
2971            assert_eq!(current_completion_labels(editor), &["x.png "]);
2972        });
2973
2974        editor.update_in(&mut cx, |editor, window, cx| {
2975            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2976        });
2977
2978        // Getting the message contents fails
2979        message_editor
2980            .update(&mut cx, |message_editor, cx| {
2981                message_editor
2982                    .mention_set()
2983                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2984            })
2985            .await
2986            .expect_err("Should fail to load x.png");
2987
2988        cx.run_until_parked();
2989
2990        // Mention was removed
2991        editor.read_with(&cx, |editor, cx| {
2992            assert_eq!(
2993                editor.text(cx),
2994                format!(
2995                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2996                    symbol.to_uri()
2997                )
2998            );
2999        });
3000
3001        // Once more
3002        cx.simulate_input("@file x.png");
3003
3004        editor.update(&mut cx, |editor, cx| {
3005                    assert_eq!(
3006                        editor.text(cx),
3007                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
3008                    );
3009                    assert!(editor.has_visible_completions_menu());
3010                    assert_eq!(current_completion_labels(editor), &["x.png "]);
3011                });
3012
3013        editor.update_in(&mut cx, |editor, window, cx| {
3014            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
3015        });
3016
3017        // This time don't immediately get the contents, just let the confirmed completion settle
3018        cx.run_until_parked();
3019
3020        // Mention was removed
3021        editor.read_with(&cx, |editor, cx| {
3022            assert_eq!(
3023                editor.text(cx),
3024                format!(
3025                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
3026                    symbol.to_uri()
3027                )
3028            );
3029        });
3030
3031        // Now getting the contents succeeds, because the invalid mention was removed
3032        let contents = message_editor
3033            .update(&mut cx, |message_editor, cx| {
3034                message_editor
3035                    .mention_set()
3036                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
3037            })
3038            .await
3039            .unwrap();
3040        assert_eq!(contents.len(), 3);
3041    }
3042
3043    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
3044        let snapshot = editor.buffer().read(cx).snapshot(cx);
3045        editor.display_map.update(cx, |display_map, cx| {
3046            display_map
3047                .snapshot(cx)
3048                .folds_in_range(MultiBufferOffset(0)..snapshot.len())
3049                .map(|fold| fold.range.to_point(&snapshot))
3050                .collect()
3051        })
3052    }
3053
3054    fn current_completion_labels(editor: &Editor) -> Vec<String> {
3055        let completions = editor.current_completions().expect("Missing completions");
3056        completions
3057            .into_iter()
3058            .map(|completion| completion.label.text)
3059            .collect::<Vec<_>>()
3060    }
3061
3062    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
3063        let completions = editor.current_completions().expect("Missing completions");
3064        completions
3065            .into_iter()
3066            .map(|completion| {
3067                (
3068                    completion.label.text,
3069                    completion
3070                        .documentation
3071                        .map(|d| d.text().to_string())
3072                        .unwrap_or_default(),
3073                )
3074            })
3075            .collect::<Vec<_>>()
3076    }
3077
3078    #[gpui::test]
3079    async fn test_large_file_mention_fallback(cx: &mut TestAppContext) {
3080        init_test(cx);
3081
3082        let fs = FakeFs::new(cx.executor());
3083
3084        // Create a large file that exceeds AUTO_OUTLINE_SIZE
3085        // Using plain text without a configured language, so no outline is available
3086        const LINE: &str = "This is a line of text in the file\n";
3087        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
3088        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
3089
3090        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
3091        let small_content = "fn small_function() { /* small */ }\n";
3092        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
3093
3094        fs.insert_tree(
3095            "/project",
3096            json!({
3097                "large_file.txt": large_content.clone(),
3098                "small_file.txt": small_content,
3099            }),
3100        )
3101        .await;
3102
3103        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3104
3105        let (multi_workspace, cx) =
3106            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3107        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3108
3109        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3110
3111        let message_editor = cx.update(|window, cx| {
3112            cx.new(|cx| {
3113                let editor = MessageEditor::new(
3114                    workspace.downgrade(),
3115                    project.downgrade(),
3116                    thread_store.clone(),
3117                    None,
3118                    None,
3119                    Default::default(),
3120                    "Test Agent".into(),
3121                    "Test",
3122                    EditorMode::AutoHeight {
3123                        min_lines: 1,
3124                        max_lines: None,
3125                    },
3126                    window,
3127                    cx,
3128                );
3129                // Enable embedded context so files are actually included
3130                editor
3131                    .session_capabilities
3132                    .write()
3133                    .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
3134                editor
3135            })
3136        });
3137
3138        // Test large file mention
3139        // Get the absolute path using the project's worktree
3140        let large_file_abs_path = project.read_with(cx, |project, cx| {
3141            let worktree = project.worktrees(cx).next().unwrap();
3142            let worktree_root = worktree.read(cx).abs_path();
3143            worktree_root.join("large_file.txt")
3144        });
3145        let large_file_task = message_editor.update(cx, |editor, cx| {
3146            editor.mention_set().update(cx, |set, cx| {
3147                set.confirm_mention_for_file(large_file_abs_path, true, cx)
3148            })
3149        });
3150
3151        let large_file_mention = large_file_task.await.unwrap();
3152        match large_file_mention {
3153            Mention::Text { content, .. } => {
3154                // Should contain some of the content but not all of it
3155                assert!(
3156                    content.contains(LINE),
3157                    "Should contain some of the file content"
3158                );
3159                assert!(
3160                    !content.contains(&LINE.repeat(100)),
3161                    "Should not contain the full file"
3162                );
3163                // Should be much smaller than original
3164                assert!(
3165                    content.len() < large_content.len() / 10,
3166                    "Should be significantly truncated"
3167                );
3168            }
3169            _ => panic!("Expected Text mention for large file"),
3170        }
3171
3172        // Test small file mention
3173        // Get the absolute path using the project's worktree
3174        let small_file_abs_path = project.read_with(cx, |project, cx| {
3175            let worktree = project.worktrees(cx).next().unwrap();
3176            let worktree_root = worktree.read(cx).abs_path();
3177            worktree_root.join("small_file.txt")
3178        });
3179        let small_file_task = message_editor.update(cx, |editor, cx| {
3180            editor.mention_set().update(cx, |set, cx| {
3181                set.confirm_mention_for_file(small_file_abs_path, true, cx)
3182            })
3183        });
3184
3185        let small_file_mention = small_file_task.await.unwrap();
3186        match small_file_mention {
3187            Mention::Text { content, .. } => {
3188                // Should contain the full actual content
3189                assert_eq!(content, small_content);
3190            }
3191            _ => panic!("Expected Text mention for small file"),
3192        }
3193    }
3194
3195    #[gpui::test]
3196    async fn test_insert_thread_summary(cx: &mut TestAppContext) {
3197        init_test(cx);
3198        cx.update(LanguageModelRegistry::test);
3199
3200        let fs = FakeFs::new(cx.executor());
3201        fs.insert_tree("/project", json!({"file": ""})).await;
3202        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3203
3204        let (multi_workspace, cx) =
3205            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3206        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3207
3208        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3209
3210        let session_id = acp::SessionId::new("thread-123");
3211        let title = Some("Previous Conversation".into());
3212
3213        let message_editor = cx.update(|window, cx| {
3214            cx.new(|cx| {
3215                let mut editor = MessageEditor::new(
3216                    workspace.downgrade(),
3217                    project.downgrade(),
3218                    thread_store.clone(),
3219                    None,
3220                    None,
3221                    Default::default(),
3222                    "Test Agent".into(),
3223                    "Test",
3224                    EditorMode::AutoHeight {
3225                        min_lines: 1,
3226                        max_lines: None,
3227                    },
3228                    window,
3229                    cx,
3230                );
3231                editor.insert_thread_summary(session_id.clone(), title.clone(), window, cx);
3232                editor
3233            })
3234        });
3235
3236        // Construct expected values for verification
3237        let expected_uri = MentionUri::Thread {
3238            id: session_id.clone(),
3239            name: title.as_ref().unwrap().to_string(),
3240        };
3241        let expected_title = title.as_ref().unwrap();
3242        let expected_link = format!("[@{}]({})", expected_title, expected_uri.to_uri());
3243
3244        message_editor.read_with(cx, |editor, cx| {
3245            let text = editor.text(cx);
3246
3247            assert!(
3248                text.contains(&expected_link),
3249                "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}",
3250                expected_link,
3251                text
3252            );
3253
3254            let mentions = editor.mention_set().read(cx).mentions();
3255            assert_eq!(
3256                mentions.len(),
3257                1,
3258                "Expected exactly one mention after inserting thread summary"
3259            );
3260
3261            assert!(
3262                mentions.contains(&expected_uri),
3263                "Expected mentions to contain the thread URI"
3264            );
3265        });
3266    }
3267
3268    #[gpui::test]
3269    async fn test_insert_thread_summary_skipped_for_external_agents(cx: &mut TestAppContext) {
3270        init_test(cx);
3271        cx.update(LanguageModelRegistry::test);
3272
3273        let fs = FakeFs::new(cx.executor());
3274        fs.insert_tree("/project", json!({"file": ""})).await;
3275        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3276
3277        let (multi_workspace, cx) =
3278            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3279        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3280
3281        let thread_store = None;
3282
3283        let message_editor = cx.update(|window, cx| {
3284            cx.new(|cx| {
3285                let mut editor = MessageEditor::new(
3286                    workspace.downgrade(),
3287                    project.downgrade(),
3288                    thread_store.clone(),
3289                    None,
3290                    None,
3291                    Default::default(),
3292                    "Test Agent".into(),
3293                    "Test",
3294                    EditorMode::AutoHeight {
3295                        min_lines: 1,
3296                        max_lines: None,
3297                    },
3298                    window,
3299                    cx,
3300                );
3301                editor.insert_thread_summary(
3302                    acp::SessionId::new("thread-123"),
3303                    Some("Previous Conversation".into()),
3304                    window,
3305                    cx,
3306                );
3307                editor
3308            })
3309        });
3310
3311        message_editor.read_with(cx, |editor, cx| {
3312            assert!(
3313                editor.text(cx).is_empty(),
3314                "Expected thread summary to be skipped for external agents"
3315            );
3316            assert!(
3317                editor.mention_set().read(cx).mentions().is_empty(),
3318                "Expected no mentions when thread summary is skipped"
3319            );
3320        });
3321    }
3322
3323    #[gpui::test]
3324    async fn test_thread_mode_hidden_when_disabled(cx: &mut TestAppContext) {
3325        init_test(cx);
3326
3327        let fs = FakeFs::new(cx.executor());
3328        fs.insert_tree("/project", json!({"file": ""})).await;
3329        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3330
3331        let (multi_workspace, cx) =
3332            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3333        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3334
3335        let thread_store = None;
3336
3337        let message_editor = cx.update(|window, cx| {
3338            cx.new(|cx| {
3339                MessageEditor::new(
3340                    workspace.downgrade(),
3341                    project.downgrade(),
3342                    thread_store.clone(),
3343                    None,
3344                    None,
3345                    Default::default(),
3346                    "Test Agent".into(),
3347                    "Test",
3348                    EditorMode::AutoHeight {
3349                        min_lines: 1,
3350                        max_lines: None,
3351                    },
3352                    window,
3353                    cx,
3354                )
3355            })
3356        });
3357
3358        message_editor.update(cx, |editor, _cx| {
3359            editor
3360                .session_capabilities
3361                .write()
3362                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
3363        });
3364
3365        let supported_modes = {
3366            let app = cx.app.borrow();
3367            let _ = &app;
3368            message_editor
3369                .read(&app)
3370                .session_capabilities
3371                .read()
3372                .supported_modes(false)
3373        };
3374
3375        assert!(
3376            !supported_modes.contains(&PromptContextType::Thread),
3377            "Expected thread mode to be hidden when thread mentions are disabled"
3378        );
3379    }
3380
3381    #[gpui::test]
3382    async fn test_thread_mode_visible_when_enabled(cx: &mut TestAppContext) {
3383        init_test(cx);
3384
3385        let fs = FakeFs::new(cx.executor());
3386        fs.insert_tree("/project", json!({"file": ""})).await;
3387        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3388
3389        let (multi_workspace, cx) =
3390            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3391        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3392
3393        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3394
3395        let message_editor = cx.update(|window, cx| {
3396            cx.new(|cx| {
3397                MessageEditor::new(
3398                    workspace.downgrade(),
3399                    project.downgrade(),
3400                    thread_store.clone(),
3401                    None,
3402                    None,
3403                    Default::default(),
3404                    "Test Agent".into(),
3405                    "Test",
3406                    EditorMode::AutoHeight {
3407                        min_lines: 1,
3408                        max_lines: None,
3409                    },
3410                    window,
3411                    cx,
3412                )
3413            })
3414        });
3415
3416        message_editor.update(cx, |editor, _cx| {
3417            editor
3418                .session_capabilities
3419                .write()
3420                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
3421        });
3422
3423        let supported_modes = {
3424            let app = cx.app.borrow();
3425            let _ = &app;
3426            message_editor
3427                .read(&app)
3428                .session_capabilities
3429                .read()
3430                .supported_modes(true)
3431        };
3432
3433        assert!(
3434            supported_modes.contains(&PromptContextType::Thread),
3435            "Expected thread mode to be visible when enabled"
3436        );
3437    }
3438
3439    #[gpui::test]
3440    async fn test_whitespace_trimming(cx: &mut TestAppContext) {
3441        init_test(cx);
3442
3443        let fs = FakeFs::new(cx.executor());
3444        fs.insert_tree("/project", json!({"file.rs": "fn main() {}"}))
3445            .await;
3446        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3447
3448        let (multi_workspace, cx) =
3449            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3450        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3451
3452        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3453
3454        let message_editor = cx.update(|window, cx| {
3455            cx.new(|cx| {
3456                MessageEditor::new(
3457                    workspace.downgrade(),
3458                    project.downgrade(),
3459                    thread_store.clone(),
3460                    None,
3461                    None,
3462                    Default::default(),
3463                    "Test Agent".into(),
3464                    "Test",
3465                    EditorMode::AutoHeight {
3466                        min_lines: 1,
3467                        max_lines: None,
3468                    },
3469                    window,
3470                    cx,
3471                )
3472            })
3473        });
3474        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
3475
3476        cx.run_until_parked();
3477
3478        editor.update_in(cx, |editor, window, cx| {
3479            editor.set_text("  \u{A0}してhello world  ", window, cx);
3480        });
3481
3482        let (content, _) = message_editor
3483            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
3484            .await
3485            .unwrap();
3486
3487        assert_eq!(content, vec!["してhello world".into()]);
3488    }
3489
3490    #[gpui::test]
3491    async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
3492        init_test(cx);
3493
3494        let fs = FakeFs::new(cx.executor());
3495
3496        let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
3497
3498        fs.insert_tree(
3499            "/project",
3500            json!({
3501                "src": {
3502                    "main.rs": file_content,
3503                }
3504            }),
3505        )
3506        .await;
3507
3508        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3509
3510        let (multi_workspace, cx) =
3511            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3512        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3513
3514        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3515
3516        let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
3517            let workspace_handle = cx.weak_entity();
3518            let message_editor = cx.new(|cx| {
3519                MessageEditor::new(
3520                    workspace_handle,
3521                    project.downgrade(),
3522                    thread_store.clone(),
3523                    None,
3524                    None,
3525                    Default::default(),
3526                    "Test Agent".into(),
3527                    "Test",
3528                    EditorMode::AutoHeight {
3529                        min_lines: 1,
3530                        max_lines: None,
3531                    },
3532                    window,
3533                    cx,
3534                )
3535            });
3536            workspace.active_pane().update(cx, |pane, cx| {
3537                pane.add_item(
3538                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3539                    true,
3540                    true,
3541                    None,
3542                    window,
3543                    cx,
3544                );
3545            });
3546            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3547            let editor = message_editor.read(cx).editor().clone();
3548            (message_editor, editor)
3549        });
3550
3551        cx.simulate_input("What is in @file main");
3552
3553        editor.update_in(cx, |editor, window, cx| {
3554            assert!(editor.has_visible_completions_menu());
3555            assert_eq!(editor.text(cx), "What is in @file main");
3556            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
3557        });
3558
3559        let content = message_editor
3560            .update(cx, |editor, cx| editor.contents(false, cx))
3561            .await
3562            .unwrap()
3563            .0;
3564
3565        let main_rs_uri = if cfg!(windows) {
3566            "file:///C:/project/src/main.rs"
3567        } else {
3568            "file:///project/src/main.rs"
3569        };
3570
3571        // When embedded context is `false` we should get a resource link
3572        pretty_assertions::assert_eq!(
3573            content,
3574            vec![
3575                "What is in ".into(),
3576                acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
3577            ]
3578        );
3579
3580        message_editor.update(cx, |editor, _cx| {
3581            editor
3582                .session_capabilities
3583                .write()
3584                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true))
3585        });
3586
3587        let content = message_editor
3588            .update(cx, |editor, cx| editor.contents(false, cx))
3589            .await
3590            .unwrap()
3591            .0;
3592
3593        // When embedded context is `true` we should get a resource
3594        pretty_assertions::assert_eq!(
3595            content,
3596            vec![
3597                "What is in ".into(),
3598                acp::ContentBlock::Resource(acp::EmbeddedResource::new(
3599                    acp::EmbeddedResourceResource::TextResourceContents(
3600                        acp::TextResourceContents::new(file_content, main_rs_uri)
3601                    )
3602                ))
3603            ]
3604        );
3605    }
3606
3607    #[gpui::test]
3608    async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
3609        init_test(cx);
3610
3611        let app_state = cx.update(AppState::test);
3612
3613        cx.update(|cx| {
3614            editor::init(cx);
3615            workspace::init(app_state.clone(), cx);
3616        });
3617
3618        app_state
3619            .fs
3620            .as_fake()
3621            .insert_tree(
3622                path!("/dir"),
3623                json!({
3624                    "test.txt": "line1\nline2\nline3\nline4\nline5\n",
3625                }),
3626            )
3627            .await;
3628
3629        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
3630        let window =
3631            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3632        let workspace = window
3633            .read_with(cx, |mw, _| mw.workspace().clone())
3634            .unwrap();
3635
3636        let worktree = project.update(cx, |project, cx| {
3637            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
3638            assert_eq!(worktrees.len(), 1);
3639            worktrees.pop().unwrap()
3640        });
3641        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
3642
3643        let mut cx = VisualTestContext::from_window(window.into(), cx);
3644
3645        // Open a regular editor with the created file, and select a portion of
3646        // the text that will be used for the selections that are meant to be
3647        // inserted in the agent panel.
3648        let editor = workspace
3649            .update_in(&mut cx, |workspace, window, cx| {
3650                workspace.open_path(
3651                    ProjectPath {
3652                        worktree_id,
3653                        path: rel_path("test.txt").into(),
3654                    },
3655                    None,
3656                    false,
3657                    window,
3658                    cx,
3659                )
3660            })
3661            .await
3662            .unwrap()
3663            .downcast::<Editor>()
3664            .unwrap();
3665
3666        editor.update_in(&mut cx, |editor, window, cx| {
3667            editor.change_selections(Default::default(), window, cx, |selections| {
3668                selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
3669            });
3670        });
3671
3672        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3673
3674        // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
3675        // to ensure we have a fixed viewport, so we can eventually actually
3676        // place the cursor outside of the visible area.
3677        let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
3678            let workspace_handle = cx.weak_entity();
3679            let message_editor = cx.new(|cx| {
3680                MessageEditor::new(
3681                    workspace_handle,
3682                    project.downgrade(),
3683                    thread_store.clone(),
3684                    None,
3685                    None,
3686                    Default::default(),
3687                    "Test Agent".into(),
3688                    "Test",
3689                    EditorMode::full(),
3690                    window,
3691                    cx,
3692                )
3693            });
3694            workspace.active_pane().update(cx, |pane, cx| {
3695                pane.add_item(
3696                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3697                    true,
3698                    true,
3699                    None,
3700                    window,
3701                    cx,
3702                );
3703            });
3704
3705            message_editor
3706        });
3707
3708        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3709            message_editor.editor.update(cx, |editor, cx| {
3710                // Update the Agent Panel's Message Editor text to have 100
3711                // lines, ensuring that the cursor is set at line 90 and that we
3712                // then scroll all the way to the top, so the cursor's position
3713                // remains off screen.
3714                let mut lines = String::new();
3715                for _ in 1..=100 {
3716                    lines.push_str(&"Another line in the agent panel's message editor\n");
3717                }
3718                editor.set_text(lines.as_str(), window, cx);
3719                editor.change_selections(Default::default(), window, cx, |selections| {
3720                    selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
3721                });
3722                editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
3723            });
3724        });
3725
3726        cx.run_until_parked();
3727
3728        // Before proceeding, let's assert that the cursor is indeed off screen,
3729        // otherwise the rest of the test doesn't make sense.
3730        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3731            message_editor.editor.update(cx, |editor, cx| {
3732                let snapshot = editor.snapshot(window, cx);
3733                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3734                let scroll_top = snapshot.scroll_position().y as u32;
3735                let visible_lines = editor.visible_line_count().unwrap() as u32;
3736                let visible_range = scroll_top..(scroll_top + visible_lines);
3737
3738                assert!(!visible_range.contains(&cursor_row));
3739            })
3740        });
3741
3742        // Now let's insert the selection in the Agent Panel's editor and
3743        // confirm that, after the insertion, the cursor is now in the visible
3744        // range.
3745        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3746            message_editor.insert_selections(window, cx);
3747        });
3748
3749        cx.run_until_parked();
3750
3751        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3752            message_editor.editor.update(cx, |editor, cx| {
3753                let snapshot = editor.snapshot(window, cx);
3754                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3755                let scroll_top = snapshot.scroll_position().y as u32;
3756                let visible_lines = editor.visible_line_count().unwrap() as u32;
3757                let visible_range = scroll_top..(scroll_top + visible_lines);
3758
3759                assert!(visible_range.contains(&cursor_row));
3760            })
3761        });
3762    }
3763
3764    #[gpui::test]
3765    async fn test_insert_context_with_multibyte_characters(cx: &mut TestAppContext) {
3766        init_test(cx);
3767
3768        let app_state = cx.update(AppState::test);
3769
3770        cx.update(|cx| {
3771            editor::init(cx);
3772            workspace::init(app_state.clone(), cx);
3773        });
3774
3775        app_state
3776            .fs
3777            .as_fake()
3778            .insert_tree(path!("/dir"), json!({}))
3779            .await;
3780
3781        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
3782        let window =
3783            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3784        let workspace = window
3785            .read_with(cx, |mw, _| mw.workspace().clone())
3786            .unwrap();
3787
3788        let mut cx = VisualTestContext::from_window(window.into(), cx);
3789
3790        let thread_store = cx.new(|cx| ThreadStore::new(cx));
3791
3792        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
3793            let workspace_handle = cx.weak_entity();
3794            let message_editor = cx.new(|cx| {
3795                MessageEditor::new(
3796                    workspace_handle,
3797                    project.downgrade(),
3798                    Some(thread_store.clone()),
3799                    None,
3800                    None,
3801                    Default::default(),
3802                    "Test Agent".into(),
3803                    "Test",
3804                    EditorMode::AutoHeight {
3805                        max_lines: None,
3806                        min_lines: 1,
3807                    },
3808                    window,
3809                    cx,
3810                )
3811            });
3812            workspace.active_pane().update(cx, |pane, cx| {
3813                pane.add_item(
3814                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3815                    true,
3816                    true,
3817                    None,
3818                    window,
3819                    cx,
3820                );
3821            });
3822            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3823            let editor = message_editor.read(cx).editor().clone();
3824            (message_editor, editor)
3825        });
3826
3827        editor.update_in(&mut cx, |editor, window, cx| {
3828            editor.set_text("😄😄", window, cx);
3829        });
3830
3831        cx.run_until_parked();
3832
3833        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3834            message_editor.insert_context_type("file", window, cx);
3835        });
3836
3837        cx.run_until_parked();
3838
3839        editor.update(&mut cx, |editor, cx| {
3840            assert_eq!(editor.text(cx), "😄😄@file");
3841        });
3842    }
3843
3844    #[gpui::test]
3845    async fn test_paste_mention_link_with_multiple_selections(cx: &mut TestAppContext) {
3846        init_test(cx);
3847
3848        let app_state = cx.update(AppState::test);
3849
3850        cx.update(|cx| {
3851            editor::init(cx);
3852            workspace::init(app_state.clone(), cx);
3853        });
3854
3855        app_state
3856            .fs
3857            .as_fake()
3858            .insert_tree(path!("/project"), json!({"file.txt": "content"}))
3859            .await;
3860
3861        let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
3862        let window =
3863            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3864        let workspace = window
3865            .read_with(cx, |mw, _| mw.workspace().clone())
3866            .unwrap();
3867
3868        let mut cx = VisualTestContext::from_window(window.into(), cx);
3869
3870        let thread_store = cx.new(|cx| ThreadStore::new(cx));
3871
3872        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
3873            let workspace_handle = cx.weak_entity();
3874            let message_editor = cx.new(|cx| {
3875                MessageEditor::new(
3876                    workspace_handle,
3877                    project.downgrade(),
3878                    Some(thread_store),
3879                    None,
3880                    None,
3881                    Default::default(),
3882                    "Test Agent".into(),
3883                    "Test",
3884                    EditorMode::AutoHeight {
3885                        max_lines: None,
3886                        min_lines: 1,
3887                    },
3888                    window,
3889                    cx,
3890                )
3891            });
3892            workspace.active_pane().update(cx, |pane, cx| {
3893                pane.add_item(
3894                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3895                    true,
3896                    true,
3897                    None,
3898                    window,
3899                    cx,
3900                );
3901            });
3902            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3903            let editor = message_editor.read(cx).editor().clone();
3904            (message_editor, editor)
3905        });
3906
3907        editor.update_in(&mut cx, |editor, window, cx| {
3908            editor.set_text(
3909                "AAAAAAAAAAAAAAAAAAAAAAAAA     AAAAAAAAAAAAAAAAAAAAAAAAA",
3910                window,
3911                cx,
3912            );
3913        });
3914
3915        cx.run_until_parked();
3916
3917        editor.update_in(&mut cx, |editor, window, cx| {
3918            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3919                s.select_ranges([
3920                    MultiBufferOffset(0)..MultiBufferOffset(25), // First selection (large)
3921                    MultiBufferOffset(30)..MultiBufferOffset(55), // Second selection (newest)
3922                ]);
3923            });
3924        });
3925
3926        let mention_link = "[@f](file:///test.txt)";
3927        cx.write_to_clipboard(ClipboardItem::new_string(mention_link.into()));
3928
3929        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3930            message_editor.paste(&Paste, window, cx);
3931        });
3932
3933        let text = editor.update(&mut cx, |editor, cx| editor.text(cx));
3934        assert!(
3935            text.contains("[@f](file:///test.txt)"),
3936            "Expected mention link to be pasted, got: {}",
3937            text
3938        );
3939    }
3940
3941    #[gpui::test]
3942    async fn test_paste_mention_link_with_completion_trigger_does_not_panic(
3943        cx: &mut TestAppContext,
3944    ) {
3945        init_test(cx);
3946
3947        let app_state = cx.update(AppState::test);
3948
3949        cx.update(|cx| {
3950            editor::init(cx);
3951            workspace::init(app_state.clone(), cx);
3952        });
3953
3954        app_state
3955            .fs
3956            .as_fake()
3957            .insert_tree(path!("/project"), json!({"file.txt": "content"}))
3958            .await;
3959
3960        let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
3961        let window =
3962            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3963        let workspace = window
3964            .read_with(cx, |mw, _| mw.workspace().clone())
3965            .unwrap();
3966
3967        let mut cx = VisualTestContext::from_window(window.into(), cx);
3968
3969        let thread_store = cx.new(|cx| ThreadStore::new(cx));
3970
3971        let (_message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
3972            let workspace_handle = cx.weak_entity();
3973            let message_editor = cx.new(|cx| {
3974                MessageEditor::new(
3975                    workspace_handle,
3976                    project.downgrade(),
3977                    Some(thread_store),
3978                    None,
3979                    None,
3980                    Default::default(),
3981                    "Test Agent".into(),
3982                    "Test",
3983                    EditorMode::AutoHeight {
3984                        max_lines: None,
3985                        min_lines: 1,
3986                    },
3987                    window,
3988                    cx,
3989                )
3990            });
3991            workspace.active_pane().update(cx, |pane, cx| {
3992                pane.add_item(
3993                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3994                    true,
3995                    true,
3996                    None,
3997                    window,
3998                    cx,
3999                );
4000            });
4001            message_editor.read(cx).focus_handle(cx).focus(window, cx);
4002            let editor = message_editor.read(cx).editor().clone();
4003            (message_editor, editor)
4004        });
4005
4006        cx.simulate_input("@");
4007
4008        editor.update(&mut cx, |editor, cx| {
4009            assert_eq!(editor.text(cx), "@");
4010            assert!(editor.has_visible_completions_menu());
4011        });
4012
4013        cx.write_to_clipboard(ClipboardItem::new_string("[@f](file:///test.txt) @".into()));
4014        cx.dispatch_action(Paste);
4015
4016        editor.update(&mut cx, |editor, cx| {
4017            assert!(editor.text(cx).contains("[@f](file:///test.txt)"));
4018        });
4019    }
4020
4021    #[gpui::test]
4022    async fn test_paste_external_file_path_inserts_file_mention(cx: &mut TestAppContext) {
4023        init_test(cx);
4024        let (message_editor, editor, mut cx) =
4025            setup_paste_test_message_editor(json!({"file.txt": "content"}), cx).await;
4026        paste_external_paths(
4027            &message_editor,
4028            vec![PathBuf::from(path!("/project/file.txt"))],
4029            &mut cx,
4030        );
4031
4032        let expected_uri = MentionUri::File {
4033            abs_path: path!("/project/file.txt").into(),
4034        }
4035        .to_uri()
4036        .to_string();
4037
4038        editor.update(&mut cx, |editor, cx| {
4039            assert_eq!(editor.text(cx), format!("[@file.txt]({expected_uri}) "));
4040        });
4041
4042        let contents = mention_contents(&message_editor, &mut cx).await;
4043
4044        let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
4045            panic!("Unexpected mentions");
4046        };
4047        assert_eq!(content, "content");
4048        assert_eq!(
4049            uri,
4050            &MentionUri::File {
4051                abs_path: path!("/project/file.txt").into(),
4052            }
4053        );
4054    }
4055
4056    #[gpui::test]
4057    async fn test_paste_external_directory_path_inserts_directory_mention(cx: &mut TestAppContext) {
4058        init_test(cx);
4059        let (message_editor, editor, mut cx) = setup_paste_test_message_editor(
4060            json!({
4061                "src": {
4062                    "main.rs": "fn main() {}\n",
4063                }
4064            }),
4065            cx,
4066        )
4067        .await;
4068        paste_external_paths(
4069            &message_editor,
4070            vec![PathBuf::from(path!("/project/src"))],
4071            &mut cx,
4072        );
4073
4074        let expected_uri = MentionUri::Directory {
4075            abs_path: path!("/project/src").into(),
4076        }
4077        .to_uri()
4078        .to_string();
4079
4080        editor.update(&mut cx, |editor, cx| {
4081            assert_eq!(editor.text(cx), format!("[@src]({expected_uri}) "));
4082        });
4083
4084        let contents = mention_contents(&message_editor, &mut cx).await;
4085
4086        let [(uri, Mention::Link)] = contents.as_slice() else {
4087            panic!("Unexpected mentions");
4088        };
4089        assert_eq!(
4090            uri,
4091            &MentionUri::Directory {
4092                abs_path: path!("/project/src").into(),
4093            }
4094        );
4095    }
4096
4097    #[gpui::test]
4098    async fn test_paste_external_file_path_inserts_at_cursor(cx: &mut TestAppContext) {
4099        init_test(cx);
4100        let (message_editor, editor, mut cx) =
4101            setup_paste_test_message_editor(json!({"file.txt": "content"}), cx).await;
4102
4103        editor.update_in(&mut cx, |editor, window, cx| {
4104            editor.set_text("Hello world", window, cx);
4105            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
4106                selections.select_ranges([MultiBufferOffset(6)..MultiBufferOffset(6)]);
4107            });
4108        });
4109
4110        paste_external_paths(
4111            &message_editor,
4112            vec![PathBuf::from(path!("/project/file.txt"))],
4113            &mut cx,
4114        );
4115
4116        let expected_uri = MentionUri::File {
4117            abs_path: path!("/project/file.txt").into(),
4118        }
4119        .to_uri()
4120        .to_string();
4121
4122        editor.update(&mut cx, |editor, cx| {
4123            assert_eq!(
4124                editor.text(cx),
4125                format!("Hello [@file.txt]({expected_uri}) world")
4126            );
4127        });
4128    }
4129
4130    #[gpui::test]
4131    async fn test_paste_mixed_external_image_without_extension_and_file_path(
4132        cx: &mut TestAppContext,
4133    ) {
4134        init_test(cx);
4135        let (message_editor, editor, mut cx) =
4136            setup_paste_test_message_editor(json!({"file.txt": "content"}), cx).await;
4137
4138        message_editor.update(&mut cx, |message_editor, _cx| {
4139            message_editor
4140                .session_capabilities
4141                .write()
4142                .set_prompt_capabilities(acp::PromptCapabilities::new().image(true));
4143        });
4144
4145        let temporary_image_path = write_test_png_file(None);
4146        paste_external_paths(
4147            &message_editor,
4148            vec![
4149                temporary_image_path.clone(),
4150                PathBuf::from(path!("/project/file.txt")),
4151            ],
4152            &mut cx,
4153        );
4154
4155        std::fs::remove_file(&temporary_image_path).expect("remove temp png");
4156
4157        let expected_file_uri = MentionUri::File {
4158            abs_path: path!("/project/file.txt").into(),
4159        }
4160        .to_uri()
4161        .to_string();
4162        let expected_image_uri = MentionUri::PastedImage.to_uri().to_string();
4163
4164        editor.update(&mut cx, |editor, cx| {
4165            assert_eq!(
4166                editor.text(cx),
4167                format!("[@Image]({expected_image_uri}) [@file.txt]({expected_file_uri}) ")
4168            );
4169        });
4170
4171        let contents = mention_contents(&message_editor, &mut cx).await;
4172
4173        assert_eq!(contents.len(), 2);
4174        assert!(contents.iter().any(|(uri, mention)| {
4175            *uri == MentionUri::PastedImage && matches!(mention, Mention::Image(_))
4176        }));
4177        assert!(contents.iter().any(|(uri, mention)| {
4178            *uri == MentionUri::File {
4179                abs_path: path!("/project/file.txt").into(),
4180            } && matches!(
4181                mention,
4182                Mention::Text {
4183                    content,
4184                    tracked_buffers: _,
4185                } if content == "content"
4186            )
4187        }));
4188    }
4189
4190    async fn setup_paste_test_message_editor(
4191        project_tree: Value,
4192        cx: &mut TestAppContext,
4193    ) -> (Entity<MessageEditor>, Entity<Editor>, VisualTestContext) {
4194        let app_state = cx.update(AppState::test);
4195
4196        cx.update(|cx| {
4197            editor::init(cx);
4198            workspace::init(app_state.clone(), cx);
4199        });
4200
4201        app_state
4202            .fs
4203            .as_fake()
4204            .insert_tree(path!("/project"), project_tree)
4205            .await;
4206
4207        let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
4208        let window =
4209            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4210        let workspace = window
4211            .read_with(cx, |mw, _| mw.workspace().clone())
4212            .unwrap();
4213
4214        let mut cx = VisualTestContext::from_window(window.into(), cx);
4215
4216        let thread_store = cx.new(|cx| ThreadStore::new(cx));
4217
4218        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
4219            let workspace_handle = cx.weak_entity();
4220            let message_editor = cx.new(|cx| {
4221                MessageEditor::new(
4222                    workspace_handle,
4223                    project.downgrade(),
4224                    Some(thread_store),
4225                    None,
4226                    None,
4227                    Default::default(),
4228                    "Test Agent".into(),
4229                    "Test",
4230                    EditorMode::AutoHeight {
4231                        max_lines: None,
4232                        min_lines: 1,
4233                    },
4234                    window,
4235                    cx,
4236                )
4237            });
4238            workspace.active_pane().update(cx, |pane, cx| {
4239                pane.add_item(
4240                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
4241                    true,
4242                    true,
4243                    None,
4244                    window,
4245                    cx,
4246                );
4247            });
4248            message_editor.read(cx).focus_handle(cx).focus(window, cx);
4249            let editor = message_editor.read(cx).editor().clone();
4250            (message_editor, editor)
4251        });
4252
4253        (message_editor, editor, cx)
4254    }
4255
4256    fn paste_external_paths(
4257        message_editor: &Entity<MessageEditor>,
4258        paths: Vec<PathBuf>,
4259        cx: &mut VisualTestContext,
4260    ) {
4261        cx.write_to_clipboard(ClipboardItem {
4262            entries: vec![ClipboardEntry::ExternalPaths(ExternalPaths(paths.into()))],
4263        });
4264
4265        message_editor.update_in(cx, |message_editor, window, cx| {
4266            message_editor.paste(&Paste, window, cx);
4267        });
4268        cx.run_until_parked();
4269    }
4270
4271    async fn mention_contents(
4272        message_editor: &Entity<MessageEditor>,
4273        cx: &mut VisualTestContext,
4274    ) -> Vec<(MentionUri, Mention)> {
4275        message_editor
4276            .update(cx, |message_editor, cx| {
4277                message_editor
4278                    .mention_set()
4279                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
4280            })
4281            .await
4282            .unwrap()
4283            .into_values()
4284            .collect::<Vec<_>>()
4285    }
4286
4287    fn write_test_png_file(extension: Option<&str>) -> PathBuf {
4288        let bytes = base64::prelude::BASE64_STANDARD
4289            .decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==")
4290            .expect("decode png");
4291        let file_name = match extension {
4292            Some(extension) => format!("zed-agent-ui-test-{}.{}", uuid::Uuid::new_v4(), extension),
4293            None => format!("zed-agent-ui-test-{}", uuid::Uuid::new_v4()),
4294        };
4295        let path = std::env::temp_dir().join(file_name);
4296        std::fs::write(&path, bytes).expect("write temp png");
4297        path
4298    }
4299
4300    // Helper that creates a minimal MessageEditor inside a window, returning both
4301    // the entity and the underlying VisualTestContext so callers can drive updates.
4302    async fn setup_message_editor(
4303        cx: &mut TestAppContext,
4304    ) -> (Entity<MessageEditor>, &mut VisualTestContext) {
4305        let fs = FakeFs::new(cx.executor());
4306        fs.insert_tree("/project", json!({"file.txt": ""})).await;
4307        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
4308
4309        let (multi_workspace, cx) =
4310            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4311        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4312
4313        let message_editor = cx.update(|window, cx| {
4314            cx.new(|cx| {
4315                MessageEditor::new(
4316                    workspace.downgrade(),
4317                    project.downgrade(),
4318                    None,
4319                    None,
4320                    None,
4321                    Default::default(),
4322                    "Test Agent".into(),
4323                    "Test",
4324                    EditorMode::AutoHeight {
4325                        min_lines: 1,
4326                        max_lines: None,
4327                    },
4328                    window,
4329                    cx,
4330                )
4331            })
4332        });
4333
4334        cx.run_until_parked();
4335        (message_editor, cx)
4336    }
4337
4338    #[gpui::test]
4339    async fn test_set_message_plain_text(cx: &mut TestAppContext) {
4340        init_test(cx);
4341        let (message_editor, cx) = setup_message_editor(cx).await;
4342
4343        message_editor.update_in(cx, |editor, window, cx| {
4344            editor.set_message(
4345                vec![acp::ContentBlock::Text(acp::TextContent::new(
4346                    "hello world".to_string(),
4347                ))],
4348                window,
4349                cx,
4350            );
4351        });
4352
4353        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
4354        assert_eq!(text, "hello world");
4355        assert!(!message_editor.update(cx, |editor, cx| editor.is_empty(cx)));
4356    }
4357
4358    #[gpui::test]
4359    async fn test_set_message_replaces_existing_content(cx: &mut TestAppContext) {
4360        init_test(cx);
4361        let (message_editor, cx) = setup_message_editor(cx).await;
4362
4363        // Set initial content.
4364        message_editor.update_in(cx, |editor, window, cx| {
4365            editor.set_message(
4366                vec![acp::ContentBlock::Text(acp::TextContent::new(
4367                    "old content".to_string(),
4368                ))],
4369                window,
4370                cx,
4371            );
4372        });
4373
4374        // Replace with new content.
4375        message_editor.update_in(cx, |editor, window, cx| {
4376            editor.set_message(
4377                vec![acp::ContentBlock::Text(acp::TextContent::new(
4378                    "new content".to_string(),
4379                ))],
4380                window,
4381                cx,
4382            );
4383        });
4384
4385        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
4386        assert_eq!(
4387            text, "new content",
4388            "set_message should replace old content"
4389        );
4390    }
4391
4392    #[gpui::test]
4393    async fn test_append_message_to_empty_editor(cx: &mut TestAppContext) {
4394        init_test(cx);
4395        let (message_editor, cx) = setup_message_editor(cx).await;
4396
4397        message_editor.update_in(cx, |editor, window, cx| {
4398            editor.append_message(
4399                vec![acp::ContentBlock::Text(acp::TextContent::new(
4400                    "appended".to_string(),
4401                ))],
4402                Some("\n\n"),
4403                window,
4404                cx,
4405            );
4406        });
4407
4408        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
4409        assert_eq!(
4410            text, "appended",
4411            "No separator should be inserted when the editor is empty"
4412        );
4413    }
4414
4415    #[gpui::test]
4416    async fn test_append_message_to_non_empty_editor(cx: &mut TestAppContext) {
4417        init_test(cx);
4418        let (message_editor, cx) = setup_message_editor(cx).await;
4419
4420        // Seed initial content.
4421        message_editor.update_in(cx, |editor, window, cx| {
4422            editor.set_message(
4423                vec![acp::ContentBlock::Text(acp::TextContent::new(
4424                    "initial".to_string(),
4425                ))],
4426                window,
4427                cx,
4428            );
4429        });
4430
4431        // Append with separator.
4432        message_editor.update_in(cx, |editor, window, cx| {
4433            editor.append_message(
4434                vec![acp::ContentBlock::Text(acp::TextContent::new(
4435                    "appended".to_string(),
4436                ))],
4437                Some("\n\n"),
4438                window,
4439                cx,
4440            );
4441        });
4442
4443        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
4444        assert_eq!(
4445            text, "initial\n\nappended",
4446            "Separator should appear between existing and appended content"
4447        );
4448    }
4449
4450    #[gpui::test]
4451    async fn test_append_message_preserves_mention_offset(cx: &mut TestAppContext) {
4452        init_test(cx);
4453
4454        let fs = FakeFs::new(cx.executor());
4455        fs.insert_tree("/project", json!({"file.txt": "content"}))
4456            .await;
4457        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
4458
4459        let (multi_workspace, cx) =
4460            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4461        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4462
4463        let message_editor = cx.update(|window, cx| {
4464            cx.new(|cx| {
4465                MessageEditor::new(
4466                    workspace.downgrade(),
4467                    project.downgrade(),
4468                    None,
4469                    None,
4470                    None,
4471                    Default::default(),
4472                    "Test Agent".into(),
4473                    "Test",
4474                    EditorMode::AutoHeight {
4475                        min_lines: 1,
4476                        max_lines: None,
4477                    },
4478                    window,
4479                    cx,
4480                )
4481            })
4482        });
4483
4484        cx.run_until_parked();
4485
4486        // Seed plain-text prefix so the editor is non-empty before appending.
4487        message_editor.update_in(cx, |editor, window, cx| {
4488            editor.set_message(
4489                vec![acp::ContentBlock::Text(acp::TextContent::new(
4490                    "prefix text".to_string(),
4491                ))],
4492                window,
4493                cx,
4494            );
4495        });
4496
4497        // Append a message that contains a ResourceLink mention.
4498        message_editor.update_in(cx, |editor, window, cx| {
4499            editor.append_message(
4500                vec![acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
4501                    "file.txt",
4502                    "file:///project/file.txt",
4503                ))],
4504                Some("\n\n"),
4505                window,
4506                cx,
4507            );
4508        });
4509
4510        cx.run_until_parked();
4511
4512        // The mention should be registered in the mention_set so that contents()
4513        // will emit it as a structured block rather than plain text.
4514        let mention_uris =
4515            message_editor.update(cx, |editor, cx| editor.mention_set.read(cx).mentions());
4516        assert_eq!(
4517            mention_uris.len(),
4518            1,
4519            "Expected exactly one mention in the mention_set after append, got: {mention_uris:?}"
4520        );
4521
4522        // The editor text should start with the prefix, then the separator, then
4523        // the mention placeholder — confirming the offset was computed correctly.
4524        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
4525        assert!(
4526            text.starts_with("prefix text\n\n"),
4527            "Expected text to start with 'prefix text\\n\\n', got: {text:?}"
4528        );
4529    }
4530}