use crate::SendImmediately;use crate::ThreadHistory;use crate::{ ChatWithFollow, completion_provider::{ PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction, PromptContextType, SlashCommandCompletion, }, mention_set::{ Mention, MentionImage, MentionSet, insert_crease_for_mention, paste_images_as_context, },};use acp_thread::MentionUri;use agent::ThreadStore;use agent_client_protocol as acp;use anyhow::{Result, anyhow};use collections::HashSet;use editor::{ Addon, AnchorRangeExt, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, MultiBufferSnapshot, ToOffset, actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll,};use futures::{FutureExt as _, future::join_all};use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat, KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity,};use language::{Buffer, Language, language_settings::InlayHintKind};use parking_lot::RwLock;use project::AgentId;use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};use prompt_store::PromptStore;use rope::Point;use settings::Settings;use std::{fmt::Write, ops::Range, rc::Rc, sync::Arc};use theme::ThemeSettings;use ui::{ContextMenu, Disclosure, ElevationIndex, prelude::*};use util::paths::PathStyle;use util::{ResultExt, debug_panic};use workspace::{CollaboratorId, Workspace};use zed_actions::agent::{Chat, PasteRaw}; #[derive(Default)]pub struct SessionCapabilities { prompt_capabilities: acp::PromptCapabilities, available_commands: Vec<acp::AvailableCommand>,} impl SessionCapabilities { pub fn new( prompt_capabilities: acp::PromptCapabilities, available_commands: Vec<acp::AvailableCommand>, ) -> Self { Self { prompt_capabilities, available_commands, } } pub fn supports_images(&self) -> bool { self.prompt_capabilities.image } pub fn supports_embedded_context(&self) -> bool { self.prompt_capabilities.embedded_context } pub fn available_commands(&self) -> &[acp::AvailableCommand] { &self.available_commands } fn supported_modes(&self, has_thread_store: bool) -> Vec<PromptContextType> { let mut supported = vec![PromptContextType::File, PromptContextType::Symbol]; if self.prompt_capabilities.embedded_context { if has_thread_store { supported.push(PromptContextType::Thread); } supported.extend(&[ PromptContextType::Diagnostics, PromptContextType::Fetch, PromptContextType::Rules, PromptContextType::BranchDiff, ]); } supported } pub fn completion_commands(&self) -> Vec<crate::completion_provider::AvailableCommand> { self.available_commands .iter() .map(|cmd| crate::completion_provider::AvailableCommand { name: cmd.name.clone().into(), description: cmd.description.clone().into(), requires_argument: cmd.input.is_some(), }) .collect() } pub fn set_prompt_capabilities(&mut self, prompt_capabilities: acp::PromptCapabilities) { self.prompt_capabilities = prompt_capabilities; } pub fn set_available_commands(&mut self, available_commands: Vec<acp::AvailableCommand>) { self.available_commands = available_commands; }} pub type SharedSessionCapabilities = Arc<RwLock<SessionCapabilities>>; struct MessageEditorCompletionDelegate { session_capabilities: SharedSessionCapabilities, has_thread_store: bool, message_editor: WeakEntity<MessageEditor>,} impl PromptCompletionProviderDelegate for MessageEditorCompletionDelegate { fn supports_images(&self, _cx: &App) -> bool { self.session_capabilities.read().supports_images() } fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> { self.session_capabilities .read() .supported_modes(self.has_thread_store) } fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> { self.session_capabilities.read().completion_commands() } fn confirm_command(&self, cx: &mut App) { let _ = self.message_editor.update(cx, |this, cx| this.send(cx)); }} pub struct MessageEditor { mention_set: Entity<MentionSet>, editor: Entity<Editor>, workspace: WeakEntity<Workspace>, session_capabilities: SharedSessionCapabilities, agent_id: AgentId, thread_store: Option<Entity<ThreadStore>>, _subscriptions: Vec<Subscription>, _parse_slash_command_task: Task<()>,} #[derive(Clone, Debug)]pub enum MessageEditorEvent { Send, SendImmediately, Cancel, Focus, LostFocus, InputAttempted(Arc<str>),} impl EventEmitter<MessageEditorEvent> for MessageEditor {} const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0); impl MessageEditor { pub fn new( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, history: Option<WeakEntity<ThreadHistory>>, prompt_store: Option<Entity<PromptStore>>, session_capabilities: SharedSessionCapabilities, agent_id: AgentId, placeholder: &str, mode: EditorMode, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let language = Language::new( language::LanguageConfig { completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']), ..Default::default() }, None, ); let editor = cx.new(|cx| { let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let mut editor = Editor::new(mode, buffer, None, window, cx); editor.set_placeholder_text(placeholder, window, cx); editor.set_show_indent_guides(false, cx); editor.set_show_completions_on_input(Some(true)); editor.set_soft_wrap(); editor.set_use_modal_editing(true); editor.set_context_menu_options(ContextMenuOptions { min_entries_visible: 12, max_entries_visible: 12, placement: None, }); editor.register_addon(MessageEditorAddon::new()); editor.set_custom_context_menu(|editor, _point, window, cx| { let has_selection = editor.has_non_empty_selection(&editor.display_snapshot(cx)); Some(ContextMenu::build(window, cx, |menu, _, _| { menu.action("Cut", Box::new(editor::actions::Cut)) .action_disabled_when( !has_selection, "Copy", Box::new(editor::actions::Copy), ) .action("Paste", Box::new(editor::actions::Paste)) .action("Paste as Plain Text", Box::new(PasteRaw)) })) }); editor }); let mention_set = cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone())); let completion_provider = Rc::new(PromptCompletionProvider::new( MessageEditorCompletionDelegate { session_capabilities: session_capabilities.clone(), has_thread_store: thread_store.is_some(), message_editor: cx.weak_entity(), }, editor.downgrade(), mention_set.clone(), history, prompt_store.clone(), workspace.clone(), )); editor.update(cx, |editor, _cx| { editor.set_completion_provider(Some(completion_provider.clone())) }); cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| { cx.emit(MessageEditorEvent::Focus) }) .detach(); cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| { cx.emit(MessageEditorEvent::LostFocus) }) .detach(); let mut has_hint = false; let mut subscriptions = Vec::new(); subscriptions.push(cx.subscribe_in(&editor, window, { move |this, editor, event, window, cx| { let input_attempted_text = match event { EditorEvent::InputHandled { text, .. } => Some(text), EditorEvent::InputIgnored { text } => Some(text), _ => None, }; if let Some(text) = input_attempted_text && editor.read(cx).read_only(cx) && !text.is_empty() { cx.emit(MessageEditorEvent::InputAttempted(text.clone())); } if let EditorEvent::Edited { .. } = event && !editor.read(cx).read_only(cx) { editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); this.mention_set .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot)); let new_hints = this .command_hint(snapshot.buffer()) .into_iter() .collect::<Vec<_>>(); let has_new_hint = !new_hints.is_empty(); editor.splice_inlays( if has_hint { &[COMMAND_HINT_INLAY_ID] } else { &[] }, new_hints, cx, ); has_hint = has_new_hint; }); cx.notify(); } } })); Self { editor, mention_set, workspace, session_capabilities, agent_id, thread_store, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), } } pub fn set_session_capabilities( &mut self, session_capabilities: SharedSessionCapabilities, _cx: &mut Context<Self>, ) { self.session_capabilities = session_capabilities; } fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> { let session_capabilities = self.session_capabilities.read(); let available_commands = session_capabilities.available_commands(); if available_commands.is_empty() { return None; } let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?; if parsed_command.argument.is_some() { return None; } let command_name = parsed_command.command?; let available_command = available_commands .iter() .find(|command| command.name == command_name)?; let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput { mut hint, .. }) = available_command.input.clone()? else { return None; }; let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize; if hint_pos > snapshot.len() { hint_pos = snapshot.len(); hint.insert(0, ' '); } let hint_pos = snapshot.anchor_after(hint_pos); Some(Inlay::hint( COMMAND_HINT_INLAY_ID, hint_pos, &InlayHint { position: hint_pos.text_anchor, label: InlayHintLabel::String(hint), kind: Some(InlayHintKind::Parameter), padding_left: false, padding_right: false, tooltip: None, resolve_state: project::ResolveState::Resolved, }, )) } pub fn insert_thread_summary( &mut self, session_id: acp::SessionId, title: Option<SharedString>, window: &mut Window, cx: &mut Context<Self>, ) { if self.thread_store.is_none() { return; } let Some(workspace) = self.workspace.upgrade() else { return; }; let thread_title = title .filter(|title| !title.is_empty()) .unwrap_or_else(|| SharedString::new_static("New Thread")); let uri = MentionUri::Thread { id: session_id, name: thread_title.to_string(), }; let content = format!("{}\n", uri.as_link()); let content_len = content.len() - 1; let start = self.editor.update(cx, |editor, cx| { editor.set_text(content, window, cx); editor .buffer() .read(cx) .snapshot(cx) .anchor_before(Point::zero()) .text_anchor }); let supports_images = self.session_capabilities.read().supports_images(); self.mention_set .update(cx, |mention_set, cx| { mention_set.confirm_mention_completion( thread_title, start, content_len, uri, supports_images, self.editor.clone(), &workspace, window, cx, ) }) .detach(); } #[cfg(test)] pub(crate) fn editor(&self) -> &Entity<Editor> { &self.editor } pub fn is_empty(&self, cx: &App) -> bool { self.editor.read(cx).is_empty(cx) } pub fn is_completions_menu_visible(&self, cx: &App) -> bool { self.editor .read(cx) .context_menu() .borrow() .as_ref() .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()) } #[cfg(test)] pub fn mention_set(&self) -> &Entity<MentionSet> { &self.mention_set } fn validate_slash_commands( text: &str, available_commands: &[acp::AvailableCommand], agent_id: &AgentId, ) -> Result<()> { if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) { if let Some(command_name) = parsed_command.command { // Check if this command is in the list of available commands from the server let is_supported = available_commands .iter() .any(|cmd| cmd.name == command_name); if !is_supported { return Err(anyhow!( "The /{} command is not supported by {}.\n\nAvailable commands: {}", command_name, agent_id, if available_commands.is_empty() { "none".to_string() } else { available_commands .iter() .map(|cmd| format!("/{}", cmd.name)) .collect::<Vec<_>>() .join(", ") } )); } } } Ok(()) } pub fn contents( &self, full_mention_content: bool, cx: &mut Context<Self>, ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> { let text = self.editor.read(cx).text(cx); let available_commands = self .session_capabilities .read() .available_commands() .to_vec(); let agent_id = self.agent_id.clone(); let build_task = self.build_content_blocks(full_mention_content, cx); cx.spawn(async move |_, _cx| { Self::validate_slash_commands(&text, &available_commands, &agent_id)?; build_task.await }) } pub fn draft_contents(&self, cx: &mut Context<Self>) -> Task<Result<Vec<acp::ContentBlock>>> { let build_task = self.build_content_blocks(false, cx); cx.spawn(async move |_, _cx| { let (blocks, _tracked_buffers) = build_task.await?; Ok(blocks) }) } fn build_content_blocks( &self, full_mention_content: bool, cx: &mut Context<Self>, ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> { let contents = selfLines 1–500 of 4003. Load more
use crate::SendImmediately; use crate::ThreadHistory; use crate::{ ChatWithFollow, completion_provider::{ PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction, PromptContextType, SlashCommandCompletion, }, mention_set::{ Mention, MentionImage, MentionSet, insert_crease_for_mention, paste_images_as_context, }, }; use acp_thread::MentionUri; use agent::ThreadStore; use agent_client_protocol as acp; use anyhow::{Result, anyhow}; use collections::HashSet; use editor::{ Addon, AnchorRangeExt, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, MultiBufferSnapshot, ToOffset, actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll, }; use futures::{FutureExt as _, future::join_all}; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat, KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity, }; use language::{Buffer, Language, language_settings::InlayHintKind}; use parking_lot::RwLock; use project::AgentId; use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree}; use prompt_store::PromptStore; use rope::Point; use settings::Settings; use std::{fmt::Write, ops::Range, rc::Rc, sync::Arc}; use theme::ThemeSettings; use ui::{ContextMenu, Disclosure, ElevationIndex, prelude::*}; use util::paths::PathStyle; use util::{ResultExt, debug_panic}; use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, PasteRaw}; #[derive(Default)] pub struct SessionCapabilities { prompt_capabilities: acp::PromptCapabilities, available_commands: Vec<acp::AvailableCommand>, } impl SessionCapabilities { pub fn new( prompt_capabilities: acp::PromptCapabilities, available_commands: Vec<acp::AvailableCommand>, ) -> Self { Self { prompt_capabilities, available_commands, } } pub fn supports_images(&self) -> bool { self.prompt_capabilities.image } pub fn supports_embedded_context(&self) -> bool { self.prompt_capabilities.embedded_context } pub fn available_commands(&self) -> &[acp::AvailableCommand] { &self.available_commands } fn supported_modes(&self, has_thread_store: bool) -> Vec<PromptContextType> { let mut supported = vec![PromptContextType::File, PromptContextType::Symbol]; if self.prompt_capabilities.embedded_context { if has_thread_store { supported.push(PromptContextType::Thread); } supported.extend(&[ PromptContextType::Diagnostics, PromptContextType::Fetch, PromptContextType::Rules, PromptContextType::BranchDiff, ]); } supported } pub fn completion_commands(&self) -> Vec<crate::completion_provider::AvailableCommand> { self.available_commands .iter() .map(|cmd| crate::completion_provider::AvailableCommand { name: cmd.name.clone().into(), description: cmd.description.clone().into(), requires_argument: cmd.input.is_some(), }) .collect() } pub fn set_prompt_capabilities(&mut self, prompt_capabilities: acp::PromptCapabilities) { self.prompt_capabilities = prompt_capabilities; } pub fn set_available_commands(&mut self, available_commands: Vec<acp::AvailableCommand>) { self.available_commands = available_commands; } } pub type SharedSessionCapabilities = Arc<RwLock<SessionCapabilities>>; struct MessageEditorCompletionDelegate { session_capabilities: SharedSessionCapabilities, has_thread_store: bool, message_editor: WeakEntity<MessageEditor>, } impl PromptCompletionProviderDelegate for MessageEditorCompletionDelegate { fn supports_images(&self, _cx: &App) -> bool { self.session_capabilities.read().supports_images() } fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> { self.session_capabilities .read() .supported_modes(self.has_thread_store) } fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> { self.session_capabilities.read().completion_commands() } fn confirm_command(&self, cx: &mut App) { let _ = self.message_editor.update(cx, |this, cx| this.send(cx)); } } pub struct MessageEditor { mention_set: Entity<MentionSet>, editor: Entity<Editor>, workspace: WeakEntity<Workspace>, session_capabilities: SharedSessionCapabilities, agent_id: AgentId, thread_store: Option<Entity<ThreadStore>>, _subscriptions: Vec<Subscription>, _parse_slash_command_task: Task<()>, } #[derive(Clone, Debug)] pub enum MessageEditorEvent { Send, SendImmediately, Cancel, Focus, LostFocus, InputAttempted(Arc<str>), } impl EventEmitter<MessageEditorEvent> for MessageEditor {} const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0); impl MessageEditor { pub fn new( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, history: Option<WeakEntity<ThreadHistory>>, prompt_store: Option<Entity<PromptStore>>, session_capabilities: SharedSessionCapabilities, agent_id: AgentId, placeholder: &str, mode: EditorMode, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let language = Language::new( language::LanguageConfig { completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']), ..Default::default() }, None, ); let editor = cx.new(|cx| { let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let mut editor = Editor::new(mode, buffer, None, window, cx); editor.set_placeholder_text(placeholder, window, cx); editor.set_show_indent_guides(false, cx); editor.set_show_completions_on_input(Some(true)); editor.set_soft_wrap(); editor.set_use_modal_editing(true); editor.set_context_menu_options(ContextMenuOptions { min_entries_visible: 12, max_entries_visible: 12, placement: None, }); editor.register_addon(MessageEditorAddon::new()); editor.set_custom_context_menu(|editor, _point, window, cx| { let has_selection = editor.has_non_empty_selection(&editor.display_snapshot(cx)); Some(ContextMenu::build(window, cx, |menu, _, _| { menu.action("Cut", Box::new(editor::actions::Cut)) .action_disabled_when( !has_selection, "Copy", Box::new(editor::actions::Copy), ) .action("Paste", Box::new(editor::actions::Paste)) .action("Paste as Plain Text", Box::new(PasteRaw)) })) }); editor }); let mention_set = cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone())); let completion_provider = Rc::new(PromptCompletionProvider::new( MessageEditorCompletionDelegate { session_capabilities: session_capabilities.clone(), has_thread_store: thread_store.is_some(), message_editor: cx.weak_entity(), }, editor.downgrade(), mention_set.clone(), history, prompt_store.clone(), workspace.clone(), )); editor.update(cx, |editor, _cx| { editor.set_completion_provider(Some(completion_provider.clone())) }); cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| { cx.emit(MessageEditorEvent::Focus) }) .detach(); cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| { cx.emit(MessageEditorEvent::LostFocus) }) .detach(); let mut has_hint = false; let mut subscriptions = Vec::new(); subscriptions.push(cx.subscribe_in(&editor, window, { move |this, editor, event, window, cx| { let input_attempted_text = match event { EditorEvent::InputHandled { text, .. } => Some(text), EditorEvent::InputIgnored { text } => Some(text), _ => None, }; if let Some(text) = input_attempted_text && editor.read(cx).read_only(cx) && !text.is_empty() { cx.emit(MessageEditorEvent::InputAttempted(text.clone())); } if let EditorEvent::Edited { .. } = event && !editor.read(cx).read_only(cx) { editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); this.mention_set .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot)); let new_hints = this .command_hint(snapshot.buffer()) .into_iter() .collect::<Vec<_>>(); let has_new_hint = !new_hints.is_empty(); editor.splice_inlays( if has_hint { &[COMMAND_HINT_INLAY_ID] } else { &[] }, new_hints, cx, ); has_hint = has_new_hint; }); cx.notify(); } } })); Self { editor, mention_set, workspace, session_capabilities, agent_id, thread_store, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), } } pub fn set_session_capabilities( &mut self, session_capabilities: SharedSessionCapabilities, _cx: &mut Context<Self>, ) { self.session_capabilities = session_capabilities; } fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> { let session_capabilities = self.session_capabilities.read(); let available_commands = session_capabilities.available_commands(); if available_commands.is_empty() { return None; } let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?; if parsed_command.argument.is_some() { return None; } let command_name = parsed_command.command?; let available_command = available_commands .iter() .find(|command| command.name == command_name)?; let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput { mut hint, .. }) = available_command.input.clone()? else { return None; }; let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize; if hint_pos > snapshot.len() { hint_pos = snapshot.len(); hint.insert(0, ' '); } let hint_pos = snapshot.anchor_after(hint_pos); Some(Inlay::hint( COMMAND_HINT_INLAY_ID, hint_pos, &InlayHint { position: hint_pos.text_anchor, label: InlayHintLabel::String(hint), kind: Some(InlayHintKind::Parameter), padding_left: false, padding_right: false, tooltip: None, resolve_state: project::ResolveState::Resolved, }, )) } pub fn insert_thread_summary( &mut self, session_id: acp::SessionId, title: Option<SharedString>, window: &mut Window, cx: &mut Context<Self>, ) { if self.thread_store.is_none() { return; } let Some(workspace) = self.workspace.upgrade() else { return; }; let thread_title = title .filter(|title| !title.is_empty()) .unwrap_or_else(|| SharedString::new_static("New Thread")); let uri = MentionUri::Thread { id: session_id, name: thread_title.to_string(), }; let content = format!("{}\n", uri.as_link()); let content_len = content.len() - 1; let start = self.editor.update(cx, |editor, cx| { editor.set_text(content, window, cx); editor .buffer() .read(cx) .snapshot(cx) .anchor_before(Point::zero()) .text_anchor }); let supports_images = self.session_capabilities.read().supports_images(); self.mention_set .update(cx, |mention_set, cx| { mention_set.confirm_mention_completion( thread_title, start, content_len, uri, supports_images, self.editor.clone(), &workspace, window, cx, ) }) .detach(); } #[cfg(test)] pub(crate) fn editor(&self) -> &Entity<Editor> { &self.editor } pub fn is_empty(&self, cx: &App) -> bool { self.editor.read(cx).is_empty(cx) } pub fn is_completions_menu_visible(&self, cx: &App) -> bool { self.editor .read(cx) .context_menu() .borrow() .as_ref() .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()) } #[cfg(test)] pub fn mention_set(&self) -> &Entity<MentionSet> { &self.mention_set } fn validate_slash_commands( text: &str, available_commands: &[acp::AvailableCommand], agent_id: &AgentId, ) -> Result<()> { if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) { if let Some(command_name) = parsed_command.command { // Check if this command is in the list of available commands from the server let is_supported = available_commands .iter() .any(|cmd| cmd.name == command_name); if !is_supported { return Err(anyhow!( "The /{} command is not supported by {}.\n\nAvailable commands: {}", command_name, agent_id, if available_commands.is_empty() { "none".to_string() } else { available_commands .iter() .map(|cmd| format!("/{}", cmd.name)) .collect::<Vec<_>>() .join(", ") } )); } } } Ok(()) } pub fn contents( &self, full_mention_content: bool, cx: &mut Context<Self>, ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> { let text = self.editor.read(cx).text(cx); let available_commands = self .session_capabilities .read() .available_commands() .to_vec(); let agent_id = self.agent_id.clone(); let build_task = self.build_content_blocks(full_mention_content, cx); cx.spawn(async move |_, _cx| { Self::validate_slash_commands(&text, &available_commands, &agent_id)?; build_task.await }) } pub fn draft_contents(&self, cx: &mut Context<Self>) -> Task<Result<Vec<acp::ContentBlock>>> { let build_task = self.build_content_blocks(false, cx); cx.spawn(async move |_, _cx| { let (blocks, _tracked_buffers) = build_task.await?; Ok(blocks) }) } fn build_content_blocks( &self, full_mention_content: bool, cx: &mut Context<Self>, ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> { let contents = self