Replace Rules UI with Skills creation UI (#56924)

Replaces the legacy Rules creation UI with a focused "New Skill"
creation window when the `SkillsFeatureFlag` is on.

The new `skills_library` crate provides a single-skill form with:
- Name, Description, and Body fields (placeholders double as labels)
- Live validation matching the SKILL.md spec (name: 1–64 lowercase +
digits + hyphens, no leading/trailing hyphen; description: non-empty,
≤1024 chars)
- A scope dropdown listing every local worktree in the originating
workspace plus "Global"
- An Optional Parameters card with the `disable-model-invocation`
checkbox
- Save writes `<scope>/.agents/skills/<name>/SKILL.md` via
`serde_yaml_ng` (YAML-safe escaping), refuses to overwrite existing
skills, shows a success toast in the originating workspace, then closes
the window

The "Rules" entry in the agent panel triple-dot menu is renamed to
"Skills" when the flag is on. The existing `cmd-alt-l` keyboard shortcut
still works: `AgentPanel::deploy_rules_library` reroutes to
`deploy_skills_library` when the flag is on, so any persisted keymaps
and the menu's automatic shortcut lookup keep functioning without
changes to the default keymap files.

The old `rules_library` crate is intentionally left in place for this PR
— once the flag rolls out and the few remaining `OpenRulesLibrary {
prompt_to_select }` call sites (in thread_view link handlers and
@-mention crease) are migrated to an "open existing skill" flow, a
follow-up can delete the crate entirely.

15 unit tests cover the validation rules, frontmatter formatting
(including YAML-special-character round-tripping), and disk write
behavior (creates directory, refuses overwrite).

Closes AI-247

Release Notes:

- Added a new Skills creation UI that replaces the old Rules library
when the Skills feature flag is enabled.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
MartinYe1234andDanilo Leal authored and GitHub committed 2026-05-20 02:40:57 +00:00
1 parent d39dbc4e6c
commit 37c19c17bf
16 files changed
+1466 -112

No files matched your search

Generated
+25
View File
@@ -438,6 +438,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"settings",
"skill_creator",
"streaming_diff",
"task",
"telemetry",
@@ -16532,6 +16533,30 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "skill_creator"
version = "0.1.0"
dependencies = [
"agent_skills",
"anyhow",
"editor",
"fs",
"gpui",
"language",
"menu",
"platform_title_bar",
"release_channel",
"serde_json",
"serde_yaml_ng",
"settings",
"theme_settings",
"ui",
"ui_input",
"util",
"workspace",
"worktree",
]
[[package]]
name = "skrifa"
version = "0.37.0"
+2
View File
@@ -172,6 +172,7 @@ members = [
"crates/rope",
"crates/rpc",
"crates/rules_library",
"crates/skill_creator",
"crates/scheduler",
"crates/schema_generator",
"crates/search",
@@ -432,6 +433,7 @@ rodio = { git = "https://github.com/RustAudio/rodio", rev = "e50e726ddd0292f6ef9
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
skill_creator = { path = "crates/skill_creator" }
scheduler = { path = "crates/scheduler" }
search = { path = "crates/search" }
session = { path = "crates/session" }
+16
View File
@@ -1558,4 +1558,20 @@
"shift-tab": "git_graph::FocusPreviousTabStop",
},
},
{
"context": "SkillCreator",
"bindings": {
"ctrl-w": "workspace::CloseWindow",
"tab": "skill_creator::FocusNextField",
"shift-tab": "skill_creator::FocusPreviousField",
},
},
{
"context": "SkillCreator > Editor",
"bindings": {
"ctrl-w": "workspace::CloseWindow",
"tab": "skill_creator::FocusNextField",
"shift-tab": "skill_creator::FocusPreviousField",
},
},
]
+18
View File
@@ -1651,4 +1651,22 @@
"shift-tab": "git_graph::FocusPreviousTabStop",
},
},
{
"context": "SkillCreator",
"use_key_equivalents": true,
"bindings": {
"cmd-w": "workspace::CloseWindow",
"tab": "skill_creator::FocusNextField",
"shift-tab": "skill_creator::FocusPreviousField",
},
},
{
"context": "SkillCreator > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-w": "workspace::CloseWindow",
"tab": "skill_creator::FocusNextField",
"shift-tab": "skill_creator::FocusPreviousField",
},
},
]
+18
View File
@@ -1577,4 +1577,22 @@
"shift-tab": "git_graph::FocusPreviousTabStop",
},
},
{
"context": "SkillCreator",
"use_key_equivalents": true,
"bindings": {
"ctrl-w": "workspace::CloseWindow",
"tab": "skill_creator::FocusNextField",
"shift-tab": "skill_creator::FocusPreviousField",
},
},
{
"context": "SkillCreator > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-w": "workspace::CloseWindow",
"tab": "skill_creator::FocusNextField",
"shift-tab": "skill_creator::FocusPreviousField",
},
},
]
+17 -3
View File
@@ -943,7 +943,7 @@
"space w j": "workspace::ActivatePaneDown",
"space w k": "workspace::ActivatePaneUp",
"space w l": "workspace::ActivatePaneRight",
"space w q": "pane::CloseActiveItem",
"space w q": "pane::CloseActiveItem",
},
},
{
@@ -1056,8 +1056,8 @@
"ctrl-d": "git_graph::ScrollDown",
"ctrl-u": "git_graph::ScrollUp",
"shift-g": "menu::SelectLast",
"g g": "menu::SelectFirst"
}
"g g": "menu::SelectFirst",
},
},
{
"context": "GitPanel && ChangesList && !GitBranchSelector",
@@ -1205,4 +1205,18 @@
"enter": "editor::Newline",
},
},
{
"context": "SkillCreator",
"bindings": {
"tab": "skill_creator::FocusNextField",
"shift-tab": "skill_creator::FocusPreviousField",
},
},
{
"context": "SkillCreator > Editor",
"bindings": {
"tab": "skill_creator::FocusNextField",
"shift-tab": "skill_creator::FocusPreviousField",
},
},
]
+136 -31
View File
@@ -1,5 +1,5 @@
use anyhow::{Context as _, Result};
use const_format::concatcp;
use const_format::{concatcp, formatcp};
use fs::Fs;
use futures::StreamExt;
use gpui::{Global, SharedString};
@@ -251,8 +251,8 @@ pub fn parse_skill_frontmatter(
let (metadata, _body) = extract_frontmatter(content)?;
validate_name(&metadata.name)?;
validate_description(&metadata.description)?;
validate_name(&metadata.name).map_err(anyhow::Error::msg)?;
validate_description(&metadata.description).map_err(anyhow::Error::msg)?;
let directory_path = skill_file_path
.parent()
@@ -346,6 +346,14 @@ fn extract_frontmatter(content: &str) -> Result<(SkillMetadata, &str)> {
/// by [`validate_name`].
pub const MAX_SKILL_NAME_LEN: usize = 64;
/// Maximum length (in bytes) for a valid skill description. Mirrors the
/// upper bound enforced by [`validate_description`].
///
/// Byte-based rather than char-based because that's what `.len()` returns
/// and what every caller currently measures; the UI also surfaces this
/// limit as a byte count so the editor's counter matches the validator.
pub const MAX_SKILL_DESCRIPTION_LEN: usize = 1024;
/// Convert an arbitrary human-readable string into a valid skill name, or
/// return `None` if no valid name can be produced (e.g. the input contains
/// no ASCII alphanumeric characters at all).
@@ -410,34 +418,54 @@ pub fn slugify_skill_name(input: &str) -> Option<String> {
if slug.is_empty() { None } else { Some(slug) }
}
fn validate_name(name: &str) -> Result<()> {
/// Validate a skill name against the rules enforced by both the loader
/// and the create-skill UI.
///
/// Rules:
/// * non-empty
/// * at most [`MAX_SKILL_NAME_LEN`] bytes
/// * ASCII lowercase letters, digits, and hyphens only
/// * must not start or end with a hyphen — [`slugify_skill_name`]
/// already guarantees this for its output, so requiring it in the
/// validator keeps hand-written `SKILL.md` files consistent with
/// slugifier output
///
/// Error messages are returned as `&'static str` (interpolated at
/// compile time via `formatcp!`) so that UI surfaces can store them in
/// `Option<&'static str>` fields without allocating, and loader callers
/// can convert them to `anyhow::Error` via `anyhow::Error::msg`.
pub fn validate_name(name: &str) -> Result<(), &'static str> {
if name.is_empty() {
anyhow::bail!("Skill name cannot be empty");
return Err("Skill name cannot be empty");
}
if name.len() > MAX_SKILL_NAME_LEN {
anyhow::bail!("Skill name must be at most {MAX_SKILL_NAME_LEN} characters");
return Err(formatcp!(
"Skill name must be at most {MAX_SKILL_NAME_LEN} characters"
));
}
if name.starts_with('-') || name.ends_with('-') {
return Err("Skill name must not start or end with a hyphen");
}
if !name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
anyhow::bail!("Skill name must contain only lowercase letters, numbers, and hyphens");
return Err("Skill name must contain only lowercase letters, numbers, and hyphens");
}
Ok(())
}
fn validate_description(description: &str) -> Result<()> {
if description.is_empty() {
anyhow::bail!("Skill description cannot be empty");
/// Validate a skill description against the rules enforced by both the
/// loader and the create-skill UI.
pub fn validate_description(description: &str) -> Result<(), &'static str> {
if description.trim().is_empty() {
return Err("Skill description cannot be empty");
}
if description.len() > 1024 {
anyhow::bail!("Skill description must be at most 1024 characters");
if description.len() > MAX_SKILL_DESCRIPTION_LEN {
return Err(formatcp!(
"Skill description must be at most {MAX_SKILL_DESCRIPTION_LEN} bytes"
));
}
Ok(())
}
@@ -672,8 +700,8 @@ pub fn builtin_skills() -> Vec<Skill> {
/// gets a synthetic `<built-in>` path since it doesn't live on disk.
fn parse_builtin_skill(name: &str, content: &'static str) -> Result<Skill> {
let (metadata, body) = extract_frontmatter(content)?;
validate_name(&metadata.name)?;
validate_description(&metadata.description)?;
validate_name(&metadata.name).map_err(anyhow::Error::msg)?;
validate_description(&metadata.description).map_err(anyhow::Error::msg)?;
let synthetic_dir = PathBuf::from(format!("<built-in>/{}", name));
let synthetic_path = synthetic_dir.join(SKILL_FILE_NAME);
@@ -1004,12 +1032,8 @@ Content.
SkillSource::Global,
);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("at most 64 characters")
);
let expected = format!("at most {MAX_SKILL_NAME_LEN} characters");
assert!(result.unwrap_err().to_string().contains(&expected));
}
#[test]
@@ -1285,12 +1309,8 @@ Content.
SkillSource::Global,
);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("at most 1024 characters")
);
let expected = format!("at most {MAX_SKILL_DESCRIPTION_LEN} bytes");
assert!(result.unwrap_err().to_string().contains(&expected));
}
#[test]
@@ -1891,4 +1911,89 @@ description: A skill with no body content
"project/.AGENTS/SKILLS/foo"
)));
}
#[test]
fn validate_name_accepts_valid_names() {
assert!(validate_name("draft-pr").is_ok());
assert!(validate_name("a").is_ok());
assert!(validate_name("skill1").is_ok());
assert!(validate_name(&"a".repeat(MAX_SKILL_NAME_LEN)).is_ok());
}
#[test]
fn validate_name_rejects_empty() {
assert!(validate_name("").is_err());
}
#[test]
fn validate_name_rejects_uppercase() {
assert!(validate_name("Draft-PR").is_err());
}
#[test]
fn validate_name_rejects_leading_and_trailing_hyphens() {
assert!(validate_name("-draft").is_err());
assert!(validate_name("draft-").is_err());
}
#[test]
fn validate_name_rejects_invalid_chars() {
assert!(validate_name("draft_pr").is_err());
assert!(validate_name("draft pr").is_err());
assert!(validate_name("draft.pr").is_err());
}
#[test]
fn validate_name_rejects_too_long() {
assert!(validate_name(&"a".repeat(MAX_SKILL_NAME_LEN + 1)).is_err());
}
#[test]
fn validate_description_accepts_valid() {
assert!(validate_description("A useful skill").is_ok());
}
#[test]
fn validate_description_rejects_empty_and_whitespace_only() {
assert!(validate_description("").is_err());
assert!(validate_description(" ").is_err());
assert!(validate_description("\t\n ").is_err());
}
#[test]
fn validate_description_rejects_too_long() {
assert!(validate_description(&"a".repeat(MAX_SKILL_DESCRIPTION_LEN + 1)).is_err());
}
#[test]
fn validate_description_length_is_measured_in_bytes() {
// "é" is 2 bytes in UTF-8. A string of MAX/2 + 1 "é" characters has
// only ~MAX/2 + 1 chars but exceeds MAX bytes, so it must be
// rejected by a byte-based validator (and accepted by a char-based
// one). This regression-tests the byte semantics that the loader
// and UI both rely on.
let chars = MAX_SKILL_DESCRIPTION_LEN / 2 + 1;
let description = "é".repeat(chars);
assert!(description.chars().count() <= MAX_SKILL_DESCRIPTION_LEN);
assert!(description.len() > MAX_SKILL_DESCRIPTION_LEN);
assert!(validate_description(&description).is_err());
}
#[test]
fn slugify_output_always_passes_validate_name() {
for input in [
"foo",
"Foo Bar",
"rock & roll",
"---weird---",
"a".repeat(200).as_str(),
] {
if let Some(slug) = slugify_skill_name(input) {
assert!(
validate_name(&slug).is_ok(),
"slug {slug:?} from {input:?} failed validate_name"
);
}
}
}
}
+1
View File
@@ -89,6 +89,7 @@ remote.workspace = true
remote_connection.workspace = true
rope.workspace = true
rules_library.workspace = true
skill_creator.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
+113 -75
View File
@@ -27,7 +27,7 @@ use zed_actions::{
ResetAgentZoom, ResetOnboarding, ResolveConflictedFilesWithAgent,
ResolveConflictsWithAgent, ReviewBranchDiff,
},
assistant::{FocusAgent, OpenRulesLibrary, Toggle, ToggleFocus},
assistant::{FocusAgent, OpenRulesLibrary, OpenSkillCreator, Toggle, ToggleFocus},
};
use crate::ExpandMessageEditor;
@@ -38,7 +38,7 @@ use crate::terminal_thread_metadata_store::{TerminalThreadMetadata, TerminalThre
use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent};
use crate::{
AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow,
InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
ResetTrialEndUpsell, ResetTrialUpsell, ShowAllSidebarThreadMetadata, ShowThreadMetadata,
ToggleNewThreadMenu, ToggleOptionsMenu,
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
@@ -66,16 +66,16 @@ use fs::Fs;
use gpui::{
Action, Anchor, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem,
Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels,
PlatformDisplay, Subscription, Task, TaskExt, UpdateGlobal, WeakEntity, WindowHandle,
prelude::*, pulsating_between,
PlatformDisplay, Subscription, Task, TaskExt, WeakEntity, WindowHandle, prelude::*,
pulsating_between,
};
use language::LanguageRegistry;
use language_model::LanguageModelRegistry;
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library};
use prompt_store::PromptStore;
use settings::TerminalDockPosition;
use settings::{NotifyWhenAgentWaiting, Settings, update_settings_file};
use skill_creator::open_skill_creator;
use terminal::{Event as TerminalEvent, terminal_settings::TerminalSettings};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use theme_settings::ThemeSettings;
@@ -300,6 +300,14 @@ pub fn init(cx: &mut App) {
});
}
})
.register_action(|workspace, action: &OpenSkillCreator, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.deploy_skill_creator(action, window, cx)
});
}
})
.register_action(|workspace, _: &Follow, window, cx| {
workspace.follow(CollaboratorId::Agent, window, cx);
})
@@ -2980,16 +2988,27 @@ impl AgentPanel {
fn deploy_rules_library(
&mut self,
action: &OpenRulesLibrary,
_action: &OpenRulesLibrary,
window: &mut Window,
cx: &mut Context<Self>,
) {
// The legacy Rules action is rerouted to the skill creator so the
// existing keyboard shortcut (still bound to `OpenRulesLibrary` in
// the default keymaps) and any persisted user keymap entries keep
// working.
self.deploy_skill_creator(&OpenSkillCreator, window, cx);
}
fn deploy_skill_creator(
&mut self,
_action: &OpenSkillCreator,
_window: &mut Window,
cx: &mut Context<Self>,
) {
open_rules_library(
open_skill_creator(
Some(self.workspace.clone()),
self.language_registry.clone(),
Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
action
.prompt_to_select
.map(|uuid| UserPromptId(uuid).into()),
self.fs.clone(),
cx,
)
.detach_and_log_err(cx);
@@ -4670,6 +4689,16 @@ impl AgentPanel {
cx: &mut Context<Self>,
) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
// Resolve menu shortcuts at the thread root; the active editor can
// shadow panel-level commands such as OpenRulesLibrary.
let menu_action_context = match &self.base_view {
BaseView::AgentThread { conversation_view } => conversation_view
.read(cx)
.active_thread()
.map(|thread| thread.read(cx).focus_handle.clone())
.unwrap_or_else(|| focus_handle.clone()),
_ => focus_handle.clone(),
};
let showing_terminal = matches!(self.visible_surface(), VisibleSurface::Terminal(_));
let conversation_view = match &self.base_view {
@@ -4698,16 +4727,13 @@ impl AgentPanel {
.trigger_with_tooltip(
IconButton::new("agent-options-menu", IconName::Ellipsis)
.icon_size(IconSize::Small),
{
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in(
"Toggle Agent Menu",
&ToggleOptionsMenu,
&focus_handle,
cx,
)
}
move |_window, cx| {
Tooltip::for_action_in(
"Toggle Agent Menu",
&ToggleOptionsMenu,
&focus_handle,
cx,
)
},
)
.anchor(Anchor::TopRight)
@@ -4715,7 +4741,7 @@ impl AgentPanel {
.menu({
move |window, cx| {
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
menu = menu.context(focus_handle.clone());
menu = menu.context(menu_action_context.clone());
if can_regenerate_thread_title {
menu = menu.header("Current Thread");
@@ -4750,6 +4776,14 @@ impl AgentPanel {
.action("Add Custom Server…", Box::new(AddContextServer))
.separator();
menu = menu.entry(
"Skills",
Some(Box::new(OpenRulesLibrary::default())),
|window, cx| {
window.dispatch_action(Box::new(OpenSkillCreator), cx);
},
);
menu = menu.action("Profiles", Box::new(ManageProfiles::default()));
}
@@ -5575,6 +5609,7 @@ impl Render for AgentPanel {
}))
.on_action(cx.listener(Self::open_active_thread_as_markdown))
.on_action(cx.listener(Self::deploy_rules_library))
.on_action(cx.listener(Self::deploy_skill_creator))
.on_action(cx.listener(Self::go_back))
.on_action(cx.listener(Self::toggle_options_menu))
.on_action(cx.listener(Self::increase_font_size))
@@ -5626,57 +5661,6 @@ impl Render for AgentPanel {
}
}
struct PromptLibraryInlineAssist {
workspace: WeakEntity<Workspace>,
}
impl PromptLibraryInlineAssist {
pub fn new(workspace: WeakEntity<Workspace>) -> Self {
Self { workspace }
}
}
impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
fn assist(
&self,
prompt_editor: &Entity<Editor>,
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut Context<RulesLibrary>,
) {
InlineAssistant::update_global(cx, |assistant, cx| {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
return;
};
let project = workspace.read(cx).project().downgrade();
let panel = panel.read(cx);
let thread_store = panel.thread_store().clone();
assistant.assist(
prompt_editor,
self.workspace.clone(),
project,
thread_store,
None,
initial_prompt,
window,
cx,
);
})
}
fn focus_agent_panel(
&self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> bool {
workspace.focus_panel::<AgentPanel>(window, cx).is_some()
}
}
struct OnboardingUpsell;
impl Dismissable for OnboardingUpsell {
@@ -5958,7 +5942,7 @@ mod tests {
use anyhow::{Result, anyhow};
use feature_flags::FeatureFlagAppExt;
use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext};
use gpui::{App, TestAppContext, UpdateGlobal, VisualTestContext};
use parking_lot::Mutex;
use project::{Project, WorktreePaths};
use std::any::Any;
@@ -8030,6 +8014,60 @@ mod tests {
});
}
#[gpui::test]
async fn test_skills_menu_entry_shows_rules_shortcut(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
"keymaps/default-macos.json",
cx,
)
.unwrap();
cx.bind_keys(default_key_bindings);
agent::ThreadStore::init_global(cx);
language_model::LanguageModelRegistry::test(cx);
});
let fs = FakeFs::new(cx.executor());
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
fs.insert_tree("/project", json!({ "file.txt": "" })).await;
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
let multi_workspace =
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace
.read_with(cx, |multi_workspace, _cx| {
multi_workspace.workspace().clone()
})
.unwrap();
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
workspace.add_panel(panel.clone(), window, cx);
panel
});
open_thread_with_connection(&panel, StubAgentConnection::new(), &mut cx);
workspace.update_in(&mut cx, |workspace, window, cx| {
workspace.focus_panel::<AgentPanel>(window, cx);
});
cx.run_until_parked();
panel.update_in(&mut cx, |panel, window, cx| {
panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
});
cx.run_until_parked();
assert!(
cx.debug_bounds("MENU_ITEM-Skills").is_some(),
"Skills menu item should be visible"
);
assert!(
cx.debug_bounds("KEY_BINDING-l").is_some(),
"Skills menu item should show the OpenRulesLibrary shortcut"
);
}
#[gpui::test]
async fn test_terminal_close_event_closes_without_sidebar(cx: &mut TestAppContext) {
let (panel, mut cx) = setup_panel(cx).await;
+4
View File
@@ -506,6 +506,7 @@ pub fn init(
) {
agent::ThreadStore::init_global(cx);
rules_library::init(cx);
skill_creator::init(cx);
if !is_eval {
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
// we're not running inside of the eval.
@@ -650,6 +651,7 @@ fn update_command_palette_filter(cx: &mut App) {
];
let open_rules_library_action = [TypeId::of::<zed_actions::assistant::OpenRulesLibrary>()];
let open_skill_creator_action = [TypeId::of::<zed_actions::assistant::OpenSkillCreator>()];
if disable_ai {
filter.hide_namespace("agent");
@@ -707,8 +709,10 @@ fn update_command_palette_filter(cx: &mut App) {
// rest of that namespace's actions.
if !disable_ai {
filter.hide_action_types(&open_rules_library_action);
filter.show_action_types(open_skill_creator_action.iter());
} else {
filter.show_action_types(open_rules_library_action.iter());
filter.hide_action_types(&open_skill_creator_action);
}
});
}
+3 -3
View File
@@ -56,7 +56,7 @@ pub(crate) fn render_skills_setup_page(
.gap_2()
.child(Label::new(message).color(Color::Muted))
.child(
Button::new("open-agent-panel", "Create with Agent")
Button::new("open-skill-creator", "Create a Skill")
.tab_index(0_isize)
.style(ButtonStyle::Outlined)
.end_icon(
@@ -71,10 +71,10 @@ pub(crate) fn render_skills_setup_page(
original_window
.update(cx, |_workspace, original_window, cx| {
original_window.dispatch_action(
zed_actions::assistant::ToggleFocus.boxed_clone(),
zed_actions::assistant::OpenSkillCreator
.boxed_clone(),
cx,
);
original_window.activate_window();
})
.log_err();
window.remove_window();
+36
View File
@@ -0,0 +1,36 @@
[package]
name = "skill_creator"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/skill_creator.rs"
[dependencies]
agent_skills.workspace = true
anyhow.workspace = true
editor.workspace = true
fs.workspace = true
gpui.workspace = true
language.workspace = true
menu.workspace = true
platform_title_bar.workspace = true
release_channel.workspace = true
serde_yaml_ng.workspace = true
settings.workspace = true
theme_settings.workspace = true
ui.workspace = true
ui_input.workspace = true
util.workspace = true
workspace.workspace = true
worktree.workspace = true
[dev-dependencies]
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
serde_json.workspace = true
+1
View File
@@ -0,0 +1 @@
../../LICENSE-GPL
File diff suppressed because it is too large. Load diff
+1
View File
@@ -5253,6 +5253,7 @@ mod tests {
"search",
"settings_editor",
"settings_profile_selector",
"skill_creator",
"snippets",
"stash_picker",
"svg",
+2
View File
@@ -570,6 +570,8 @@ pub mod assistant {
#[action(deprecated_aliases = ["assistant::ToggleFocus"])]
ToggleFocus,
FocusAgent,
/// Opens the skill creator window for creating a new skill.
OpenSkillCreator,
]
);