From 7afcc8792718d49ccb2b58005810b322f54488e1 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 21 May 2026 16:29:55 -0300 Subject: [PATCH] agent_ui: Add skills menu item in message editor's context menu (#57407) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes AI-295 This PR adds a skills submenu within the "add context" menu in the agent panel's message editor. This will hopefully be yet another way to find skills in the app. Screenshot 2026-05-21 at 11  24@2x Release Notes: - N/A --- .../src/conversation_view/thread_view.rs | 49 ++++++++++++----- crates/agent_ui/src/message_editor.rs | 55 +++++++++++++++++++ 2 files changed, 89 insertions(+), 15 deletions(-) diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index cc182096d18..3b0c0dd37cc 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -12,6 +12,7 @@ use cloud_api_types::{SubmitAgentThreadFeedbackBody, SubmitAgentThreadFeedbackCo use editor::actions::OpenExcerpts; use feature_flags::AcpBetaFeatureFlag; +use crate::completion_provider::AvailableSkill; use crate::message_editor::SharedSessionCapabilities; use gpui::List; @@ -4157,6 +4158,8 @@ impl ThreadView { let session_capabilities = self.session_capabilities.read(); let supports_images = session_capabilities.supports_images(); let supports_embedded_context = session_capabilities.supports_embedded_context(); + let available_skills = session_capabilities.completion_skills(); + drop(session_capabilities); let has_editor_selection = workspace .upgrade() @@ -4180,7 +4183,6 @@ impl ThreadView { ContextMenu::build(window, cx, move |menu, _window, _cx| { menu.key_context("AddContextMenu") - .header("Context") .item( ContextMenuEntry::new("Files & Directories") .icon(IconName::File) @@ -4226,21 +4228,19 @@ impl ThreadView { } }), ) - .item( - ContextMenuEntry::new("Skills") - .icon(IconName::Sparkle) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .handler({ - let message_editor = message_editor.clone(); - move |window, cx| { - message_editor.focus_handle(cx).focus(window, cx); - message_editor.update(cx, |editor, cx| { - editor.insert_context_type("skill", window, cx); - }); + .when(!available_skills.is_empty(), |this| { + this.submenu_with_colored_icon("Skills", IconName::Sparkle, Color::Muted, { + let message_editor = message_editor.clone(); + let available_skills = available_skills.clone(); + move |mut menu, _window, _cx| { + for skill in &available_skills { + menu = menu + .item(Self::skill_menu_entry(skill, message_editor.clone())); } - }), - ) + menu + } + }) + }) .item( ContextMenuEntry::new("Image") .icon(IconName::Image) @@ -4289,6 +4289,25 @@ impl ThreadView { }) } + fn skill_menu_entry( + skill: &AvailableSkill, + message_editor: Entity, + ) -> ContextMenuEntry { + let label = format!("{} ({})", skill.name, skill.source); + let skill = skill.clone(); + + ContextMenuEntry::new(label) + .icon(IconName::Sparkle) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .handler(move |window, cx| { + message_editor.focus_handle(cx).focus(window, cx); + message_editor.update(cx, |editor, cx| { + editor.insert_skill_crease(&skill, window, cx); + }); + }) + } + fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { let following = self.is_following(cx); diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index ecd1febba72..90cbdffb6db 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1514,6 +1514,61 @@ impl MessageEditor { .detach_and_log_err(cx); } + pub fn insert_skill_crease( + &mut self, + skill: &AvailableSkill, + window: &mut Window, + cx: &mut Context, + ) { + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + + let mention_uri = MentionUri::Skill { + name: skill.name.to_string(), + source: skill.source.to_string(), + skill_file_path: skill.skill_file_path.clone(), + }; + + let link_text = mention_uri.as_link().to_string(); + let content_len = link_text.len(); + let mention_text = format!("{} ", link_text); + let crease_text: SharedString = mention_uri.name().into(); + + let start_anchor = self.editor.update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let buffer_snapshot = snapshot.as_singleton()?; + let cursor = editor.selections.newest_anchor().start; + let text_anchor = snapshot + .anchor_to_buffer_anchor(cursor)? + .0 + .bias_left(buffer_snapshot); + + editor.insert(&mention_text, window, cx); + Some(text_anchor) + }); + + let Some(start_anchor) = start_anchor else { + return; + }; + + self.mention_set + .update(cx, |mention_set, cx| { + mention_set.confirm_mention_completion( + crease_text, + start_anchor, + content_len, + mention_uri, + false, + self.editor.clone(), + &workspace, + window, + cx, + ) + }) + .detach(); + } + pub(crate) fn insert_selections( &mut self, selection: AgentContextSelection,