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::{AgentSessionInfo, MentionUri};use agent::ThreadStore;use agent_client_protocol as acp;use anyhow::{Result, anyhow};use collections::HashSet;use editor::{ Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, 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 project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};use prompt_store::PromptStore;use rope::Point;use settings::Settings;use std::{cell::RefCell, fmt::Write, ops::Range, rc::Rc, sync::Arc};use theme::ThemeSettings;use ui::{ButtonLike, ButtonStyle, ContextMenu, Disclosure, ElevationIndex, prelude::*};use util::paths::PathStyle;use util::{ResultExt, debug_panic};use workspace::{CollaboratorId, Workspace};use zed_actions::agent::{Chat, PasteRaw}; pub struct MessageEditor { mention_set: Entity<MentionSet>, editor: Entity<Editor>, workspace: WeakEntity<Workspace>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_name: SharedString, 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 PromptCompletionProviderDelegate for Entity<MessageEditor> { fn supports_images(&self, cx: &App) -> bool { self.read(cx).prompt_capabilities.borrow().image } fn supported_modes(&self, cx: &App) -> Vec<PromptContextType> { let mut supported = vec![PromptContextType::File, PromptContextType::Symbol]; if self.read(cx).prompt_capabilities.borrow().embedded_context { if self.read(cx).thread_store.is_some() { supported.push(PromptContextType::Thread); } supported.extend(&[ PromptContextType::Diagnostics, PromptContextType::Fetch, PromptContextType::Rules, ]); } supported } fn available_commands(&self, cx: &App) -> Vec<crate::completion_provider::AvailableCommand> { self.read(cx) .available_commands .borrow() .iter() .map(|cmd| crate::completion_provider::AvailableCommand { name: cmd.name.clone().into(), description: cmd.description.clone().into(), requires_argument: cmd.input.is_some(), }) .collect() } fn confirm_command(&self, cx: &mut App) { self.update(cx, |this, cx| this.send(cx)); }} impl MessageEditor { pub fn new( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, history: WeakEntity<ThreadHistory>, prompt_store: Option<Entity<PromptStore>>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_name: SharedString, 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: Some(ContextMenuPlacement::Above), }); 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)) })) }); editor }); let mention_set = cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone())); let completion_provider = Rc::new(PromptCompletionProvider::new( cx.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, prompt_capabilities, available_commands, agent_name, thread_store, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), } } pub fn set_command_state( &mut self, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, _cx: &mut Context<Self>, ) { self.prompt_capabilities = prompt_capabilities; self.available_commands = available_commands; } fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> { let available_commands = self.available_commands.borrow(); 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, thread: AgentSessionInfo, 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 = thread .title .clone() .filter(|title| !title.is_empty()) .unwrap_or_else(|| SharedString::new_static("New Thread")); let uri = MentionUri::Thread { id: thread.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.prompt_capabilities.borrow().image; 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_name: &str, ) -> 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_name, 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.available_commands.borrow().clone(); let agent_name = self.agent_name.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_name)?; 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 .mention_set .update(cx, |store, cx| store.contents(full_mention_content, cx)); let editor = self.editor.clone(); let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context; cx.spawn(async move |_, cx| { let contents = contents.await?; let mut all_tracked_buffers = Vec::new(); let result = editor.update(cx, |editor, cx| { let text = editor.text(cx); let (mut ix, _) = text .char_indices() .find(|(_, c)| !c.is_whitespace()) .unwrap_or((0, '\0')); let mut chunks: Vec<acp::ContentBlock> = Vec::new(); editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { let Some((uri, mention)) = contents.get(&crease_id) else { continue; }; let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot()); if crease_range.start.0 > ix { let chunk = text[ix..crease_range.start.0].into(); chunks.push(chunk); } let chunk = match mention { Mention::Text { content, tracked_buffers, } => { all_tracked_buffers.extend(tracked_buffers.iter().cloned()); if supports_embedded_context { acp::ContentBlock::Resource(acp::EmbeddedResource::new( acp::EmbeddedResourceResource::TextResourceContents( acp::TextResourceContents::new( content.clone(), uri.to_uri().to_string(), ), ), )) } else { acp::ContentBlock::ResourceLink(acp::ResourceLink::new( uri.name(), uri.to_uri().to_string(), )) } } Mention::Image(mention_image) => acp::ContentBlock::Image( acp::ImageContent::new( mention_image.data.clone(), mention_image.format.mime_type(), ) .uri(match uri { MentionUri::File { .. } => Some(uri.to_uri().to_string()), MentionUri::PastedImage => None, other => { debug_panic!(Lines 1–500 of 3801. 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::{AgentSessionInfo, MentionUri}; use agent::ThreadStore; use agent_client_protocol as acp; use anyhow::{Result, anyhow}; use collections::HashSet; use editor::{ Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, 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 project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree}; use prompt_store::PromptStore; use rope::Point; use settings::Settings; use std::{cell::RefCell, fmt::Write, ops::Range, rc::Rc, sync::Arc}; use theme::ThemeSettings; use ui::{ButtonLike, ButtonStyle, ContextMenu, Disclosure, ElevationIndex, prelude::*}; use util::paths::PathStyle; use util::{ResultExt, debug_panic}; use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, PasteRaw}; pub struct MessageEditor { mention_set: Entity<MentionSet>, editor: Entity<Editor>, workspace: WeakEntity<Workspace>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_name: SharedString, 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 PromptCompletionProviderDelegate for Entity<MessageEditor> { fn supports_images(&self, cx: &App) -> bool { self.read(cx).prompt_capabilities.borrow().image } fn supported_modes(&self, cx: &App) -> Vec<PromptContextType> { let mut supported = vec![PromptContextType::File, PromptContextType::Symbol]; if self.read(cx).prompt_capabilities.borrow().embedded_context { if self.read(cx).thread_store.is_some() { supported.push(PromptContextType::Thread); } supported.extend(&[ PromptContextType::Diagnostics, PromptContextType::Fetch, PromptContextType::Rules, ]); } supported } fn available_commands(&self, cx: &App) -> Vec<crate::completion_provider::AvailableCommand> { self.read(cx) .available_commands .borrow() .iter() .map(|cmd| crate::completion_provider::AvailableCommand { name: cmd.name.clone().into(), description: cmd.description.clone().into(), requires_argument: cmd.input.is_some(), }) .collect() } fn confirm_command(&self, cx: &mut App) { self.update(cx, |this, cx| this.send(cx)); } } impl MessageEditor { pub fn new( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, history: WeakEntity<ThreadHistory>, prompt_store: Option<Entity<PromptStore>>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_name: SharedString, 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: Some(ContextMenuPlacement::Above), }); 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)) })) }); editor }); let mention_set = cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone())); let completion_provider = Rc::new(PromptCompletionProvider::new( cx.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, prompt_capabilities, available_commands, agent_name, thread_store, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), } } pub fn set_command_state( &mut self, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, _cx: &mut Context<Self>, ) { self.prompt_capabilities = prompt_capabilities; self.available_commands = available_commands; } fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> { let available_commands = self.available_commands.borrow(); 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, thread: AgentSessionInfo, 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 = thread .title .clone() .filter(|title| !title.is_empty()) .unwrap_or_else(|| SharedString::new_static("New Thread")); let uri = MentionUri::Thread { id: thread.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.prompt_capabilities.borrow().image; 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_name: &str, ) -> 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_name, 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.available_commands.borrow().clone(); let agent_name = self.agent_name.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_name)?; 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 .mention_set .update(cx, |store, cx| store.contents(full_mention_content, cx)); let editor = self.editor.clone(); let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context; cx.spawn(async move |_, cx| { let contents = contents.await?; let mut all_tracked_buffers = Vec::new(); let result = editor.update(cx, |editor, cx| { let text = editor.text(cx); let (mut ix, _) = text .char_indices() .find(|(_, c)| !c.is_whitespace()) .unwrap_or((0, '\0')); let mut chunks: Vec<acp::ContentBlock> = Vec::new(); editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { let Some((uri, mention)) = contents.get(&crease_id) else { continue; }; let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot()); if crease_range.start.0 > ix { let chunk = text[ix..crease_range.start.0].into(); chunks.push(chunk); } let chunk = match mention { Mention::Text { content, tracked_buffers, } => { all_tracked_buffers.extend(tracked_buffers.iter().cloned()); if supports_embedded_context { acp::ContentBlock::Resource(acp::EmbeddedResource::new( acp::EmbeddedResourceResource::TextResourceContents( acp::TextResourceContents::new( content.clone(), uri.to_uri().to_string(), ), ), )) } else { acp::ContentBlock::ResourceLink(acp::ResourceLink::new( uri.name(), uri.to_uri().to_string(), )) } } Mention::Image(mention_image) => acp::ContentBlock::Image( acp::ImageContent::new( mention_image.data.clone(), mention_image.format.mime_type(), ) .uri(match uri { MentionUri::File { .. } => Some(uri.to_uri().to_string()), MentionUri::PastedImage => None, other => { debug_panic!(