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.
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,