mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-10 00:13:29 +00:00
settings_ui: Move skills management into settings UI (#58701)
Closes AI-344 Closes AI-342 Moves agent skills management out of the agent panel and into the settings UI. The skill creator (previously its own crate) now lives in `settings_ui` as a settings page, alongside the skills setup page. The agent panel's ellipsis menu now links to the settings page instead of hosting skill management directly. Release Notes: - Improved agent skills management by moving it into the settings UI --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
parent
1d1a4bee34
commit
4eebf3fdbc
24 changed files with 793 additions and 786 deletions
32
Cargo.lock
generated
32
Cargo.lock
generated
|
|
@ -519,7 +519,6 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"skill_creator",
|
||||
"streaming_diff",
|
||||
"task",
|
||||
"telemetry",
|
||||
|
|
@ -16551,6 +16550,7 @@ dependencies = [
|
|||
"fuzzy",
|
||||
"gpui",
|
||||
"heck 0.5.0",
|
||||
"http_client",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
|
|
@ -16567,6 +16567,7 @@ dependencies = [
|
|||
"search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml_ng",
|
||||
"settings",
|
||||
"shell_command_parser",
|
||||
"strum 0.27.2",
|
||||
|
|
@ -16575,6 +16576,7 @@ dependencies = [
|
|||
"theme_settings",
|
||||
"title_bar",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
|
|
@ -16849,33 +16851,6 @@ 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",
|
||||
"futures 0.3.32",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
"menu",
|
||||
"notifications",
|
||||
"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"
|
||||
|
|
@ -23090,7 +23065,6 @@ dependencies = [
|
|||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"util",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -170,7 +170,6 @@ members = [
|
|||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/sandbox",
|
||||
"crates/skill_creator",
|
||||
"crates/scheduler",
|
||||
"crates/schema_generator",
|
||||
"crates/search",
|
||||
|
|
@ -428,7 +427,6 @@ reqwest_client = { path = "crates/reqwest_client" }
|
|||
rodio = { git = "https://github.com/RustAudio/rodio", rev = "e50e726ddd0292f6ef9de0dda6b90af4ed1fb66a", features = ["wav", "playback", "wav_output", "recording"] }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
skill_creator = { path = "crates/skill_creator" }
|
||||
scheduler = { path = "crates/scheduler" }
|
||||
sandbox = { path = "crates/sandbox" }
|
||||
search = { path = "crates/search" }
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@
|
|||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl--": "pane::GoBack",
|
||||
"ctrl-alt-p": "agent::ManageProfiles",
|
||||
"ctrl-alt-l": "agent::OpenRulesLibrary",
|
||||
"ctrl-alt-l": "agent::ManageSkills",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@
|
|||
"bindings": {
|
||||
"cmd-n": "agent::NewThread",
|
||||
"ctrl--": "pane::GoBack",
|
||||
"cmd-alt-l": "agent::OpenRulesLibrary",
|
||||
"cmd-alt-l": "agent::ManageSkills",
|
||||
"cmd-alt-p": "agent::ManageProfiles",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@
|
|||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl--": "pane::GoBack",
|
||||
"shift-alt-l": "agent::OpenRulesLibrary",
|
||||
"shift-alt-l": "agent::ManageSkills",
|
||||
"shift-alt-p": "agent::ManageProfiles",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ use anyhow::{Context as _, Result};
|
|||
use const_format::{concatcp, formatcp};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{Global, SharedString};
|
||||
use gpui::{App, Global, SharedString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
use util::paths::component_matches_ignore_ascii_case;
|
||||
|
|
@ -195,6 +196,11 @@ pub struct ProjectSkillGroup {
|
|||
|
||||
impl Global for SkillIndex {}
|
||||
|
||||
/// Rescan skill agent skill directories when skills are created or modified via UI
|
||||
pub struct SkillsUpdatedHook(pub Rc<dyn Fn(&mut App)>);
|
||||
|
||||
impl Global for SkillsUpdatedHook {}
|
||||
|
||||
/// Just the frontmatter, used for parsing
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SkillMetadata {
|
||||
|
|
@ -204,7 +210,7 @@ pub struct SkillMetadata {
|
|||
pub disable_model_invocation: bool,
|
||||
}
|
||||
|
||||
/// Minimal skill info for system prompt (not full content).
|
||||
/// Minimal skill info for system prompt.
|
||||
///
|
||||
/// `Serialize` is required for handlebars rendering of the system prompt
|
||||
/// template (see `ProjectContext` in `prompt_store`). `PartialEq, Eq` lets
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ release_channel.workspace = true
|
|||
remote.workspace = true
|
||||
remote_connection.workspace = true
|
||||
rope.workspace = true
|
||||
skill_creator.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ use zed_actions::{
|
|||
ResolveConflictsWithAgent, ReviewBranchDiff,
|
||||
},
|
||||
assistant::{
|
||||
CreateSkillFromUrl, FocusAgent, OpenGlobalAgentsMdRules, OpenProjectAgentsMdRules,
|
||||
OpenRulesLibrary, OpenSkillCreator, Toggle, ToggleFocus,
|
||||
FocusAgent, ManageSkills, OpenGlobalAgentsMdRules, OpenProjectAgentsMdRules, Toggle,
|
||||
ToggleFocus,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ use notifications::status_toast::StatusToast;
|
|||
use project::{Project, ProjectPath, Worktree};
|
||||
use settings::TerminalDockPosition;
|
||||
use settings::{NotifyWhenAgentWaiting, Settings, update_settings_file};
|
||||
use skill_creator::{SkillCreatorOpenMode, is_supported_skill_url, open_skill_creator};
|
||||
|
||||
use terminal::{Event as TerminalEvent, terminal_settings::TerminalSettings};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use text::OffsetRangeExt;
|
||||
|
|
@ -422,12 +422,10 @@ pub fn init(cx: &mut App) {
|
|||
});
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
|
||||
.register_action(|workspace, action: &ManageSkills, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.deploy_rules_library(action, window, cx)
|
||||
});
|
||||
panel.update(cx, |panel, cx| panel.manage_skills(action, window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenGlobalAgentsMdRules, window, cx| {
|
||||
|
|
@ -436,22 +434,6 @@ pub fn init(cx: &mut App) {
|
|||
.register_action(|workspace, _: &OpenProjectAgentsMdRules, window, cx| {
|
||||
open_project_rules(workspace, window, cx);
|
||||
})
|
||||
.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, action: &CreateSkillFromUrl, 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_from_url(action, window, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &Follow, window, cx| {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
})
|
||||
|
|
@ -3405,90 +3387,46 @@ impl AgentPanel {
|
|||
self.set_base_view(thread.into(), focus, window, cx);
|
||||
}
|
||||
|
||||
fn deploy_rules_library(
|
||||
fn manage_skills(
|
||||
&mut self,
|
||||
_action: &OpenRulesLibrary,
|
||||
_action: &ManageSkills,
|
||||
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>,
|
||||
) {
|
||||
self.open_skill_creator(SkillCreatorOpenMode::Form, cx);
|
||||
}
|
||||
|
||||
fn deploy_skill_creator_from_url(
|
||||
&mut self,
|
||||
_action: &CreateSkillFromUrl,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let initial_url = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|clipboard| clipboard.text())
|
||||
.map(|text| text.trim().to_string())
|
||||
.filter(|text| is_supported_skill_url(text));
|
||||
|
||||
self.open_skill_creator(SkillCreatorOpenMode::Url { initial_url }, cx);
|
||||
}
|
||||
|
||||
/// Open the skill creator pre-filled with a skill received from a
|
||||
/// `zed://skill` share link, so the user can review it and choose a scope
|
||||
/// before installing.
|
||||
pub fn install_shared_skill(&mut self, content: String, cx: &mut Context<Self>) {
|
||||
self.open_skill_creator(SkillCreatorOpenMode::Install { content }, cx);
|
||||
}
|
||||
|
||||
fn open_skill_creator(&mut self, open_mode: SkillCreatorOpenMode, cx: &mut Context<Self>) {
|
||||
let this = cx.weak_entity();
|
||||
let on_saved: Rc<dyn Fn(&mut App)> = Rc::new(move |cx: &mut App| {
|
||||
this.update(cx, |this, cx| {
|
||||
if !this.has_open_project(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ensure_native_agent_connection(cx);
|
||||
let Some(connect_task) = this.connection_store.update(cx, |store, cx| {
|
||||
store
|
||||
.entry(&Agent::NativeAgent)
|
||||
.map(|entry| entry.read(cx).wait_for_connection())
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let project = this.project.clone();
|
||||
cx.spawn(async move |_this, cx| -> Result<()> {
|
||||
let connected = connect_task.await?;
|
||||
if let Some(native_connection) = connected
|
||||
.connection
|
||||
.downcast::<agent::NativeAgentConnection>()
|
||||
{
|
||||
cx.update(|cx| native_connection.refresh_skills_for_project(project, cx));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.log_err();
|
||||
});
|
||||
|
||||
open_skill_creator(
|
||||
Some(self.workspace.clone()),
|
||||
self.language_registry.clone(),
|
||||
self.fs.clone(),
|
||||
open_mode,
|
||||
Some(on_saved),
|
||||
window.dispatch_action(
|
||||
Box::new(zed_actions::OpenSettingsAt {
|
||||
path: zed_actions::AGENT_SKILLS_SETTINGS_PATH.to_string(),
|
||||
target: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// Refresh the native agent's view of available skills
|
||||
pub fn refresh_skills(&mut self, cx: &mut Context<Self>) {
|
||||
if !self.has_open_project(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ensure_native_agent_connection(cx);
|
||||
let Some(connect_task) = self.connection_store.update(cx, |store, cx| {
|
||||
store
|
||||
.entry(&Agent::NativeAgent)
|
||||
.map(|entry| entry.read(cx).wait_for_connection())
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let project = self.project.clone();
|
||||
cx.spawn(async move |_this, cx| -> Result<()> {
|
||||
let connected = connect_task.await?;
|
||||
if let Some(native_connection) = connected
|
||||
.connection
|
||||
.downcast::<agent::NativeAgentConnection>()
|
||||
{
|
||||
cx.update(|cx| native_connection.refresh_skills_for_project(project, cx));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
|
|
@ -5595,7 +5533,7 @@ impl AgentPanel {
|
|||
) -> 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.
|
||||
// shadow panel-level commands such as ManageSkills.
|
||||
let menu_action_context = match &self.base_view {
|
||||
BaseView::AgentThread { conversation_view } => conversation_view
|
||||
.read(cx)
|
||||
|
|
@ -5692,28 +5630,10 @@ impl AgentPanel {
|
|||
}),
|
||||
)
|
||||
.separator()
|
||||
.header("Skills")
|
||||
.entry(
|
||||
"Create Skill…",
|
||||
Some(Box::new(OpenRulesLibrary::default())),
|
||||
|window, cx| {
|
||||
window.dispatch_action(Box::new(OpenSkillCreator), cx);
|
||||
},
|
||||
)
|
||||
.entry("Manage Skills…", None, |window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(zed_actions::OpenSettingsAt {
|
||||
path: "agent.skills".to_string(),
|
||||
target: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.separator();
|
||||
.header("Context")
|
||||
.action("Skills", Box::new(ManageSkills));
|
||||
|
||||
if project_agents_md_path.is_some() || global_agents_md_loaded {
|
||||
menu = menu.header("Rules");
|
||||
|
||||
if global_agents_md_loaded {
|
||||
let workspace = workspace.clone();
|
||||
|
||||
|
|
@ -6606,8 +6526,7 @@ impl Render for AgentPanel {
|
|||
this.open_configuration(window, cx);
|
||||
}))
|
||||
.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::manage_skills))
|
||||
.on_action(cx.listener(Self::go_back))
|
||||
.on_action(cx.listener(Self::toggle_options_menu))
|
||||
.on_action(cx.listener(Self::increase_font_size))
|
||||
|
|
@ -9313,7 +9232,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_skills_menu_entry_shows_rules_shortcut(cx: &mut TestAppContext) {
|
||||
async fn test_skills_menu_entry_shows_manage_skills_shortcut(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
|
||||
|
|
@ -9357,12 +9276,12 @@ mod tests {
|
|||
cx.run_until_parked();
|
||||
|
||||
assert!(
|
||||
cx.debug_bounds("MENU_ITEM-Create Skill…").is_some(),
|
||||
"Create Skill… menu item should be visible"
|
||||
cx.debug_bounds("MENU_ITEM-Skills").is_some(),
|
||||
"Skills menu item should be visible"
|
||||
);
|
||||
assert!(
|
||||
cx.debug_bounds("KEY_BINDING-l").is_some(),
|
||||
"Create Skill… menu item should show the OpenRulesLibrary shortcut"
|
||||
"Skills menu item should show the ManageSkills shortcut"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -556,7 +556,26 @@ pub fn init(
|
|||
) {
|
||||
agent::ThreadStore::init_global(cx);
|
||||
prompt_store::init(cx);
|
||||
skill_creator::init(cx);
|
||||
|
||||
cx.set_global(agent_skills::SkillsUpdatedHook(std::rc::Rc::new(|cx| {
|
||||
let workspaces: Vec<_> = workspace::AppState::global(cx)
|
||||
.workspace_store
|
||||
.read(cx)
|
||||
.workspaces()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for workspace in workspaces {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| panel.refresh_skills(cx));
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})));
|
||||
|
||||
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.
|
||||
|
|
@ -763,7 +782,7 @@ fn update_command_palette_filter(cx: &mut App) {
|
|||
TypeId::of::<ToggleEditPrediction>(),
|
||||
];
|
||||
|
||||
let open_rules_library_action = [TypeId::of::<zed_actions::assistant::OpenRulesLibrary>()];
|
||||
let manage_skills_action = [TypeId::of::<zed_actions::assistant::ManageSkills>()];
|
||||
let skill_creator_actions = [
|
||||
TypeId::of::<zed_actions::assistant::OpenSkillCreator>(),
|
||||
TypeId::of::<zed_actions::assistant::CreateSkillFromUrl>(),
|
||||
|
|
@ -818,16 +837,15 @@ fn update_command_palette_filter(cx: &mut App) {
|
|||
filter.show_namespace("multi_workspace");
|
||||
}
|
||||
|
||||
// Hide `assistant: open rules library` — Rules are surfaced
|
||||
// through the Skills UI now. Applied after the disable-ai /
|
||||
// agent-enabled branches so it overrides the
|
||||
// `show_namespace("assistant")` call above without affecting the
|
||||
// rest of that namespace's actions.
|
||||
// Hide `agent: manage skills` — skills are surfaced through the
|
||||
// settings UI now. Applied after the disable-ai / agent-enabled
|
||||
// branches so it overrides the `show_namespace("assistant")` call
|
||||
// above without affecting the rest of that namespace's actions.
|
||||
if !disable_ai {
|
||||
filter.hide_action_types(&open_rules_library_action);
|
||||
filter.hide_action_types(&manage_skills_action);
|
||||
filter.show_action_types(skill_creator_actions.iter());
|
||||
} else {
|
||||
filter.show_action_types(open_rules_library_action.iter());
|
||||
filter.show_action_types(manage_skills_action.iter());
|
||||
filter.hide_action_types(&skill_creator_actions);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9879,7 +9879,8 @@ impl ThreadView {
|
|||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(zed_actions::OpenSettingsAt {
|
||||
path: "agent.skills".to_string(),
|
||||
path: zed_actions::AGENT_SKILLS_SETTINGS_PATH
|
||||
.to_string(),
|
||||
target: target.clone(),
|
||||
}),
|
||||
cx,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ futures.workspace = true
|
|||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
heck.workspace = true
|
||||
http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
|
|
@ -51,6 +52,7 @@ schemars.workspace = true
|
|||
search.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml_ng.workspace = true
|
||||
settings.workspace = true
|
||||
shell_command_parser.workspace = true
|
||||
strum.workspace = true
|
||||
|
|
@ -58,6 +60,7 @@ telemetry.workspace = true
|
|||
theme.workspace = true
|
||||
theme_settings.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
|
|
|||
|
|
@ -7825,7 +7825,7 @@ fn ai_page(cx: &App) -> SettingsPage {
|
|||
SettingsPageItem::SubPageLink(SubPageLink {
|
||||
title: "Skills".into(),
|
||||
r#type: Default::default(),
|
||||
json_path: Some("agent.skills"),
|
||||
json_path: Some(zed_actions::AGENT_SKILLS_SETTINGS_PATH),
|
||||
description: Some("View and manage agent skills installed globally or in project worktrees.".into()),
|
||||
in_json: false,
|
||||
files: USER | PROJECT,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ mod audio_input_output_setup;
|
|||
mod audio_test_window;
|
||||
mod edit_prediction_provider_setup;
|
||||
mod feature_flags;
|
||||
mod skill_creator;
|
||||
mod skills_setup;
|
||||
mod tool_permissions_setup;
|
||||
|
||||
|
|
@ -11,6 +12,10 @@ pub(crate) use audio_input_output_setup::{
|
|||
pub(crate) use audio_test_window::open_audio_test_window;
|
||||
pub(crate) use edit_prediction_provider_setup::render_edit_prediction_setup_page;
|
||||
pub(crate) use feature_flags::render_feature_flags_page;
|
||||
pub use skill_creator::SkillCreatorOpenMode;
|
||||
pub(crate) use skill_creator::{
|
||||
SkillCreatorEvent, SkillCreatorPage, render_skill_creator_page, skill_url_from_clipboard,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use skills_setup::displayed_skills;
|
||||
pub(crate) use skills_setup::render_skills_setup_page;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,13 @@
|
|||
use agent_skills::{MAX_SKILL_DESCRIPTION_LEN, Skill, SkillIndex, encode_skill_share_link};
|
||||
use fs::RemoveOptions;
|
||||
use gpui::{Action as _, App, ClipboardItem, ScrollHandle, SharedString, prelude::*};
|
||||
use gpui::{App, ClipboardItem, ScrollHandle, SharedString, prelude::*};
|
||||
|
||||
use ui::{Divider, Tooltip, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::pages::SkillCreatorOpenMode;
|
||||
use crate::{SettingsUiFile, SettingsWindow};
|
||||
|
||||
/// Skills shown on the Skills page for the currently selected settings file:
|
||||
|
|
@ -63,36 +64,26 @@ pub(crate) fn render_skills_setup_page(
|
|||
_ => "No skills available for this context.",
|
||||
};
|
||||
|
||||
let original_window = settings_window.original_window;
|
||||
|
||||
this.px_8().items_center().justify_center().child(
|
||||
v_flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(Label::new(message).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("open-skill-creator", "Create a Skill")
|
||||
Button::new("open-skill-creator-empty", "Create a Skill")
|
||||
.tab_index(0_isize)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.end_icon(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.start_icon(
|
||||
Icon::new(IconName::Plus)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.on_click(cx.listener(move |_this, _event, window, cx| {
|
||||
let Some(original_window) = original_window else {
|
||||
return;
|
||||
};
|
||||
original_window
|
||||
.update(cx, |_workspace, original_window, cx| {
|
||||
original_window.dispatch_action(
|
||||
zed_actions::assistant::OpenSkillCreator
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
window.remove_window();
|
||||
.on_click(cx.listener(move |this, _event, window, cx| {
|
||||
this.open_skill_creator_sub_page(
|
||||
SkillCreatorOpenMode::Form,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@ use workspace::{
|
|||
AppState, MultiWorkspace, OpenOptions, OpenVisible, Workspace, WorkspaceSettings,
|
||||
client_side_decorations,
|
||||
};
|
||||
use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt, OpenSettingsAtTarget};
|
||||
use zed_actions::{
|
||||
AGENT_SKILLS_SETTINGS_PATH, OpenProjectSettings, OpenSettings, OpenSettingsAt,
|
||||
OpenSettingsAtTarget,
|
||||
};
|
||||
|
||||
use crate::components::{
|
||||
EnumVariantDropdown, NumberField, NumberFieldMode, NumberFieldType, SettingsInputField,
|
||||
|
|
@ -420,6 +423,13 @@ pub fn init(cx: &mut App) {
|
|||
cx.on_action(|_: &OpenSettings, cx| {
|
||||
open_settings_editor(None, None, None, cx);
|
||||
});
|
||||
cx.on_action(|_: &zed_actions::assistant::OpenSkillCreator, cx| {
|
||||
open_skill_creator(pages::SkillCreatorOpenMode::Form, None, cx);
|
||||
});
|
||||
cx.on_action(|_: &zed_actions::assistant::CreateSkillFromUrl, cx| {
|
||||
let initial_url = pages::skill_url_from_clipboard(cx);
|
||||
open_skill_creator(pages::SkillCreatorOpenMode::Url { initial_url }, None, cx);
|
||||
});
|
||||
|
||||
cx.observe_new(|workspace: &mut workspace::Workspace, _, _| {
|
||||
workspace
|
||||
|
|
@ -449,7 +459,24 @@ pub fn init(cx: &mut App) {
|
|||
.then_some(tree.read(cx).id())
|
||||
});
|
||||
open_settings_editor(None, target_worktree_id, window_handle, cx);
|
||||
});
|
||||
})
|
||||
.register_action(
|
||||
|_, _: &zed_actions::assistant::OpenSkillCreator, window, cx| {
|
||||
let window_handle = window.window_handle().downcast::<MultiWorkspace>();
|
||||
open_skill_creator(pages::SkillCreatorOpenMode::Form, window_handle, cx);
|
||||
},
|
||||
)
|
||||
.register_action(
|
||||
|_, _: &zed_actions::assistant::CreateSkillFromUrl, window, cx| {
|
||||
let window_handle = window.window_handle().downcast::<MultiWorkspace>();
|
||||
let initial_url = pages::skill_url_from_clipboard(cx);
|
||||
open_skill_creator(
|
||||
pages::SkillCreatorOpenMode::Url { initial_url },
|
||||
window_handle,
|
||||
cx,
|
||||
);
|
||||
},
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
|
@ -635,8 +662,6 @@ fn open_settings_editor_at_target(
|
|||
workspace_handle: Option<WindowHandle<MultiWorkspace>>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
telemetry::event!("Settings Viewed");
|
||||
|
||||
fn select_target_file(
|
||||
target_file: SettingsFileTarget,
|
||||
settings_window: &mut SettingsWindow,
|
||||
|
|
@ -699,6 +724,36 @@ fn open_settings_editor_at_target(
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
let path = path.map(ToOwned::to_owned);
|
||||
open_settings_editor_with(workspace_handle, cx, move |settings_window, window, cx| {
|
||||
if let Some(target_file) = target_file {
|
||||
select_target_file(target_file, settings_window, window, cx);
|
||||
}
|
||||
if let Some(path) = path {
|
||||
open_path(&path, settings_window, window, cx);
|
||||
} else if target_file.is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn open_skill_creator(
|
||||
open_mode: pages::SkillCreatorOpenMode,
|
||||
workspace_handle: Option<WindowHandle<MultiWorkspace>>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
open_settings_editor_with(workspace_handle, cx, |settings_window, window, cx| {
|
||||
settings_window.navigate_to_skill_creator(open_mode, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn open_settings_editor_with(
|
||||
workspace_handle: Option<WindowHandle<MultiWorkspace>>,
|
||||
cx: &mut App,
|
||||
callback: impl FnOnce(&mut SettingsWindow, &mut Window, &mut Context<SettingsWindow>) + 'static,
|
||||
) {
|
||||
telemetry::event!("Settings Viewed");
|
||||
|
||||
let existing_window = cx
|
||||
.windows()
|
||||
.into_iter()
|
||||
|
|
@ -710,21 +765,13 @@ fn open_settings_editor_at_target(
|
|||
settings_window.original_window = workspace_handle;
|
||||
|
||||
window.activate_window();
|
||||
if let Some(target_file) = target_file {
|
||||
select_target_file(target_file, settings_window, window, cx);
|
||||
}
|
||||
if let Some(path) = path {
|
||||
open_path(path, settings_window, window, cx);
|
||||
} else if target_file.is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
callback(settings_window, window, cx);
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
}
|
||||
|
||||
// We have to defer this to get the workspace off the stack.
|
||||
let path = path.map(ToOwned::to_owned);
|
||||
cx.defer(move |cx| {
|
||||
let current_rem_size: f32 = theme_settings::ThemeSettings::get_global(cx)
|
||||
.ui_font_size(cx)
|
||||
|
|
@ -773,12 +820,7 @@ fn open_settings_editor_at_target(
|
|||
let settings_window =
|
||||
cx.new(|cx| SettingsWindow::new(workspace_handle, window, cx));
|
||||
settings_window.update(cx, |settings_window, cx| {
|
||||
if let Some(target_file) = target_file {
|
||||
select_target_file(target_file, settings_window, window, cx);
|
||||
}
|
||||
if let Some(path) = path {
|
||||
open_path(&path, settings_window, window, cx);
|
||||
}
|
||||
callback(settings_window, window, cx);
|
||||
});
|
||||
|
||||
settings_window
|
||||
|
|
@ -845,6 +887,7 @@ pub struct SettingsWindow {
|
|||
/// Directory path of the skill whose share link was most recently copied,
|
||||
/// used to show a transient "copied" checkmark on its share button.
|
||||
pub(crate) last_copied_skill_directory_path: Option<PathBuf>,
|
||||
skill_creator_page: Option<(Entity<pages::SkillCreatorPage>, Subscription)>,
|
||||
}
|
||||
|
||||
struct SearchDocument {
|
||||
|
|
@ -1472,6 +1515,7 @@ impl PartialEq for SettingItem {
|
|||
#[derive(Clone, PartialEq, Default)]
|
||||
enum SubPageType {
|
||||
Language,
|
||||
SkillCreator,
|
||||
#[default]
|
||||
Other,
|
||||
}
|
||||
|
|
@ -1819,6 +1863,7 @@ impl SettingsWindow {
|
|||
list_state,
|
||||
last_copied_link_path: None,
|
||||
last_copied_skill_directory_path: None,
|
||||
skill_creator_page: None,
|
||||
};
|
||||
|
||||
this.fetch_files(window, cx);
|
||||
|
|
@ -3513,6 +3558,8 @@ impl SettingsWindow {
|
|||
let page_content;
|
||||
|
||||
if let Some(current_sub_page) = self.sub_page_stack.last() {
|
||||
let is_skills_page =
|
||||
current_sub_page.link.json_path == Some(AGENT_SKILLS_SETTINGS_PATH);
|
||||
page_header = h_flex()
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
|
|
@ -3532,23 +3579,39 @@ impl SettingsWindow {
|
|||
)
|
||||
.child(self.render_sub_page_breadcrumbs(window, cx)),
|
||||
)
|
||||
.when(current_sub_page.link.in_json, |this| {
|
||||
this.child(
|
||||
div().flex_shrink_0().child(
|
||||
Button::new("open-in-settings-file", "Edit in settings.json")
|
||||
.tab_index(0_isize)
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Edit in settings.json",
|
||||
&OpenCurrentFile,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_current_settings_file(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.when(current_sub_page.link.in_json, |this| {
|
||||
this.child(
|
||||
Button::new("open-in-settings-file", "Edit in settings.json")
|
||||
.tab_index(0_isize)
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Edit in settings.json",
|
||||
&OpenCurrentFile,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_current_settings_file(window, cx);
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(is_skills_page, |this| {
|
||||
this.child(
|
||||
Button::new("open-skill-creator", "Create Skill")
|
||||
.tab_index(0_isize)
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_skill_creator_sub_page(
|
||||
pages::SkillCreatorOpenMode::Form,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
let active_page_render_fn = ¤t_sub_page.link.render;
|
||||
|
|
@ -3973,6 +4036,101 @@ impl SettingsWindow {
|
|||
self.push_sub_page(sub_page_link, section_header.into(), window, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn skill_creator_page(&self) -> Option<Entity<pages::SkillCreatorPage>> {
|
||||
self.skill_creator_page
|
||||
.as_ref()
|
||||
.map(|(page, _)| page.clone())
|
||||
}
|
||||
|
||||
/// If the creator is already the active sub-page, the open mode is applied
|
||||
/// to the existing form instead
|
||||
pub fn open_skill_creator_sub_page(
|
||||
&mut self,
|
||||
open_mode: pages::SkillCreatorOpenMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<SettingsWindow>,
|
||||
) {
|
||||
let creator_is_active_sub_page = self
|
||||
.sub_page_stack
|
||||
.last()
|
||||
.is_some_and(|sub_page| sub_page.link.r#type == SubPageType::SkillCreator);
|
||||
|
||||
if creator_is_active_sub_page && let Some((page, _)) = &self.skill_creator_page {
|
||||
let page = page.clone();
|
||||
page.update(cx, |page, cx| page.apply_open_mode(open_mode, window, cx));
|
||||
return;
|
||||
}
|
||||
|
||||
let settings_window = cx.weak_entity();
|
||||
let page = cx.new(|cx| pages::SkillCreatorPage::new(settings_window, window, cx));
|
||||
|
||||
let subscription =
|
||||
cx.subscribe_in(
|
||||
&page,
|
||||
window,
|
||||
|this, _page, event: &pages::SkillCreatorEvent, window, cx| match event {
|
||||
pages::SkillCreatorEvent::Dismissed | pages::SkillCreatorEvent::Saved => {
|
||||
if this.sub_page_stack.last().is_some_and(|sub_page| {
|
||||
sub_page.link.r#type == SubPageType::SkillCreator
|
||||
}) {
|
||||
this.pop_sub_page(window, cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
self.skill_creator_page = Some((page.clone(), subscription));
|
||||
|
||||
let sub_page_link = SubPageLink {
|
||||
title: "Create Skill".into(),
|
||||
r#type: SubPageType::SkillCreator,
|
||||
description: None,
|
||||
json_path: None,
|
||||
in_json: false,
|
||||
files: USER | PROJECT,
|
||||
render: pages::render_skill_creator_page,
|
||||
};
|
||||
|
||||
self.push_sub_page(sub_page_link, "Agent".into(), window, cx);
|
||||
|
||||
let creating_from_url = !matches!(open_mode, pages::SkillCreatorOpenMode::Url { .. });
|
||||
page.update(cx, |page, cx| {
|
||||
page.apply_open_mode(open_mode, window, cx);
|
||||
});
|
||||
if creating_from_url {
|
||||
let name_editor_focus_handle = page.read(cx).name_editor_focus_handle(cx);
|
||||
window.focus(&name_editor_focus_handle, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn navigate_to_skill_creator(
|
||||
&mut self,
|
||||
open_mode: pages::SkillCreatorOpenMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<SettingsWindow>,
|
||||
) {
|
||||
self.sub_page_stack.clear();
|
||||
let skills_page_index = self.pages.iter().position(|page| {
|
||||
page.items.iter().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
SettingsPageItem::SubPageLink(link)
|
||||
if link.json_path == Some(AGENT_SKILLS_SETTINGS_PATH)
|
||||
)
|
||||
})
|
||||
});
|
||||
if let Some(page_index) = skills_page_index
|
||||
&& let Some(navbar_entry_index) = self
|
||||
.navbar_entries
|
||||
.iter()
|
||||
.position(|entry| entry.page_index == page_index && entry.is_root)
|
||||
{
|
||||
self.open_navbar_entry_page(navbar_entry_index);
|
||||
}
|
||||
self.navigate_to_sub_page(AGENT_SKILLS_SETTINGS_PATH, window, cx);
|
||||
self.open_skill_creator_sub_page(open_mode, window, cx);
|
||||
}
|
||||
|
||||
/// Navigate to a sub-page by its json_path.
|
||||
/// Returns true if the sub-page was found and pushed, false otherwise.
|
||||
pub fn navigate_to_sub_page(
|
||||
|
|
@ -4046,7 +4204,11 @@ impl SettingsWindow {
|
|||
|
||||
fn pop_sub_page(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
|
||||
self.regex_validation_error = None;
|
||||
self.sub_page_stack.pop();
|
||||
if let Some(popped) = self.sub_page_stack.pop()
|
||||
&& popped.link.r#type == SubPageType::SkillCreator
|
||||
{
|
||||
self.skill_creator_page = None;
|
||||
}
|
||||
self.content_focus_handle.focus_handle(cx).focus(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
|
@ -4202,7 +4364,7 @@ impl Render for SettingsWindow {
|
|||
}
|
||||
}
|
||||
|
||||
fn all_projects(
|
||||
pub(crate) fn all_projects(
|
||||
window: Option<&WindowHandle<MultiWorkspace>>,
|
||||
cx: &App,
|
||||
) -> impl Iterator<Item = Entity<Project>> {
|
||||
|
|
@ -4857,6 +5019,7 @@ pub mod test {
|
|||
regex_validation_error: None,
|
||||
last_copied_link_path: None,
|
||||
last_copied_skill_directory_path: None,
|
||||
skill_creator_page: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4985,6 +5148,7 @@ pub mod test {
|
|||
regex_validation_error: None,
|
||||
last_copied_link_path: None,
|
||||
last_copied_skill_directory_path: None,
|
||||
skill_creator_page: None,
|
||||
};
|
||||
|
||||
settings_window.build_filter_table();
|
||||
|
|
@ -5739,7 +5903,7 @@ pub mod test {
|
|||
|
||||
assert_eq!(settings_window.current_file, SettingsUiFile::User);
|
||||
assert!(
|
||||
settings_window.navigate_to_sub_page("agent.skills", window, cx),
|
||||
settings_window.navigate_to_sub_page(AGENT_SKILLS_SETTINGS_PATH, window, cx),
|
||||
"Skills sub-page should exist"
|
||||
);
|
||||
assert_eq!(displayed_skill_names(settings_window, cx), ["global-skill"]);
|
||||
|
|
@ -5778,6 +5942,184 @@ pub mod test {
|
|||
assert_eq!(displayed_skill_names(settings_window, cx), ["global-skill"]);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_skill_creator_navigates_to_sub_page(cx: &mut gpui::TestAppContext) {
|
||||
use project::Project;
|
||||
|
||||
cx.update(|cx| {
|
||||
register_settings(cx);
|
||||
});
|
||||
|
||||
let app_state = cx.update(|cx| {
|
||||
let app_state = AppState::test(cx);
|
||||
AppState::set_global(app_state.clone(), cx);
|
||||
app_state
|
||||
});
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree("/project", serde_json::json!({ "main.rs": "fn main() {}" }))
|
||||
.await;
|
||||
|
||||
let project = cx.update(|cx| {
|
||||
Project::local(
|
||||
app_state.client.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
project::LocalProjectFlags::default(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree("/project", true, cx)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create worktree");
|
||||
|
||||
let (_multi_workspace, cx) = cx.add_window_view(|window, cx| {
|
||||
let workspace = cx.new(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
project.clone(),
|
||||
app_state.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
MultiWorkspace::new(workspace, window, cx)
|
||||
});
|
||||
let workspace_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let (settings_window, cx) = cx
|
||||
.add_window_view(|window, cx| SettingsWindow::new(Some(workspace_handle), window, cx));
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
settings_window.update_in(cx, |settings_window, window, cx| {
|
||||
settings_window.navigate_to_skill_creator(
|
||||
pages::SkillCreatorOpenMode::Form,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
settings_window.read_with(cx, |settings_window, _| {
|
||||
let titles: Vec<_> = settings_window
|
||||
.sub_page_stack
|
||||
.iter()
|
||||
.map(|sub_page| sub_page.link.title.to_string())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
titles,
|
||||
["Skills", "Create Skill"],
|
||||
"skill creator should be pushed on top of the skills page"
|
||||
);
|
||||
assert!(
|
||||
settings_window.skill_creator_page().is_some(),
|
||||
"skill creator page state should exist"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_skill_creator_action_opens_settings_window_at_sub_page(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
use project::Project;
|
||||
|
||||
cx.update(|cx| {
|
||||
register_settings(cx);
|
||||
release_channel::init("0.0.0".parse().unwrap(), cx);
|
||||
crate::init(cx);
|
||||
});
|
||||
|
||||
let app_state = cx.update(|cx| {
|
||||
let app_state = AppState::test(cx);
|
||||
AppState::set_global(app_state.clone(), cx);
|
||||
app_state
|
||||
});
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree("/project", serde_json::json!({ "main.rs": "fn main() {}" }))
|
||||
.await;
|
||||
|
||||
let project = cx.update(|cx| {
|
||||
Project::local(
|
||||
app_state.client.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
project::LocalProjectFlags::default(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree("/project", true, cx)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create worktree");
|
||||
|
||||
let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
|
||||
let workspace = cx.new(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
project.clone(),
|
||||
app_state.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
MultiWorkspace::new(workspace, window, cx)
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Dispatch the action the way the command palette does: on the
|
||||
// workspace window.
|
||||
multi_workspace.update_in(cx, |_multi_workspace, window, cx| {
|
||||
window.dispatch_action(Box::new(zed_actions::assistant::OpenSkillCreator), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let settings_window = cx
|
||||
.update(|_, cx| {
|
||||
cx.windows()
|
||||
.into_iter()
|
||||
.find_map(|window| window.downcast::<SettingsWindow>())
|
||||
})
|
||||
.expect("dispatching agent::OpenSkillCreator should open the settings window");
|
||||
|
||||
settings_window
|
||||
.read_with(cx, |settings_window, _| {
|
||||
let titles: Vec<_> = settings_window
|
||||
.sub_page_stack
|
||||
.iter()
|
||||
.map(|sub_page| sub_page.link.title.to_string())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
titles,
|
||||
["Skills", "Create Skill"],
|
||||
"skill creator should be pushed on top of the skills page"
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
[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
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
notifications.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 +0,0 @@
|
|||
../../LICENSE-GPL
|
||||
|
|
@ -41,6 +41,9 @@ pub struct InputField {
|
|||
tab_stop: bool,
|
||||
/// Whether the field content is masked (for sensitive fields like passwords or API keys).
|
||||
masked: Option<bool>,
|
||||
/// An optional validation error. When set, the field's border turns red
|
||||
/// and the message is shown as hint subtext below the field.
|
||||
error: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Focusable for InputField {
|
||||
|
|
@ -67,6 +70,7 @@ impl InputField {
|
|||
tab_index: None,
|
||||
tab_stop: true,
|
||||
masked: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +110,14 @@ impl InputField {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets a validation error message, turning the field's border red and
|
||||
/// showing the message as hint subtext below the field. Pass `None` to
|
||||
/// clear the error.
|
||||
pub fn set_error(&mut self, error: Option<impl Into<SharedString>>, cx: &mut Context<Self>) {
|
||||
self.error = error.map(Into::into);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn is_empty(&self, cx: &App) -> bool {
|
||||
self.editor().text(cx).trim().is_empty()
|
||||
}
|
||||
|
|
@ -149,6 +161,9 @@ impl Render for InputField {
|
|||
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
|
||||
let has_error = self.error.is_some();
|
||||
let error_border = cx.theme().status().error_border;
|
||||
|
||||
let configured_handle = if let Some(tab_index) = self.tab_index {
|
||||
focus_handle.tab_index(tab_index).tab_stop(self.tab_stop)
|
||||
} else if !self.tab_stop {
|
||||
|
|
@ -186,6 +201,7 @@ impl Render for InputField {
|
|||
editor.focus_handle(cx).contains_focused(window, cx),
|
||||
|this| this.border_color(theme_color.border_focused),
|
||||
)
|
||||
.when(has_error, |this| this.border_color(error_border))
|
||||
.when_some(self.start_icon, |this, icon| {
|
||||
this.gap_1()
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||
|
|
@ -216,6 +232,9 @@ impl Render for InputField {
|
|||
)
|
||||
}),
|
||||
)
|
||||
.when_some(self.error.clone(), |this, error| {
|
||||
this.child(Label::new(error).size(LabelSize::Small).color(Color::Error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1150,19 +1150,12 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
|||
let multi_workspace =
|
||||
workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?;
|
||||
|
||||
multi_workspace.update(cx, |multi_workspace, window, cx| {
|
||||
multi_workspace.workspace().update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.focus_panel::<AgentPanel>(window, cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.install_shared_skill(content, cx);
|
||||
});
|
||||
} else {
|
||||
log::warn!(
|
||||
"zed://skill received but the AgentPanel is not registered \
|
||||
(is `disable_ai` enabled?)"
|
||||
);
|
||||
}
|
||||
});
|
||||
multi_workspace.update(cx, |_multi_workspace, _window, cx| {
|
||||
settings_ui::open_skill_creator(
|
||||
settings_ui::pages::SkillCreatorOpenMode::Install { content },
|
||||
Some(multi_workspace),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
|
|
|||
|
|
@ -13,4 +13,3 @@ gpui.workspace = true
|
|||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
|
|
|||
|
|
@ -149,6 +149,9 @@ pub struct OpenSettingsAt {
|
|||
pub target: Option<OpenSettingsAtTarget>,
|
||||
}
|
||||
|
||||
/// `OpenSettingsAt` path of the agent skills page in the settings UI.
|
||||
pub const AGENT_SKILLS_SETTINGS_PATH: &str = "agent.skills";
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OpenSettingsAtTarget {
|
||||
|
|
@ -577,7 +580,6 @@ pub mod assistant {
|
|||
use gpui::{Action, actions};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
actions!(
|
||||
agent,
|
||||
|
|
@ -597,18 +599,12 @@ pub mod assistant {
|
|||
/// Opens the project AGENTS.md rules file.
|
||||
#[action(name = "OpenProjectAGENTS.mdRules")]
|
||||
OpenProjectAgentsMdRules,
|
||||
/// Opens the skills manager in the settings window.
|
||||
#[action(deprecated_aliases = ["agent::OpenRulesLibrary", "assistant::OpenRulesLibrary", "assistant::DeployPromptLibrary"])]
|
||||
ManageSkills,
|
||||
]
|
||||
);
|
||||
|
||||
/// Opens the rules library for managing agent rules and prompts.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent, deprecated_aliases = ["assistant::OpenRulesLibrary", "assistant::DeployPromptLibrary"])]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OpenRulesLibrary {
|
||||
#[serde(skip)]
|
||||
pub prompt_to_select: Option<Uuid>,
|
||||
}
|
||||
|
||||
/// Deploys the assistant interface with the specified configuration.
|
||||
#[derive(Clone, Default, Deserialize, PartialEq, JsonSchema, Action)]
|
||||
#[action(namespace = assistant)]
|
||||
|
|
|
|||
|
|
@ -242,10 +242,17 @@ See [Feature-specific models](./ai/agent-settings.md#feature-specific-models) fo
|
|||
}
|
||||
```
|
||||
|
||||
To customize the format of generated commit messages, run {#action agent::OpenRulesLibrary} and select the "Commit message" rule on the left side.
|
||||
From there, you can modify the prompt to match your desired format.
|
||||
To add custom instructions that apply only to commit message generation, use the `commit_message_instructions` field in your agent settings:
|
||||
|
||||
Any specific instructions for commit messages added to [Rules files](./ai/rules.md) are also picked up by the model tasked with writing your commit message.
|
||||
```json [settings]
|
||||
{
|
||||
"agent": {
|
||||
"commit_message_instructions": "Use the Conventional Commits format: <type>(<scope>): <description>."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These instructions are sent to the model in addition to any instruction files, such as `.rules` or `AGENTS.md`. To add instructions that apply to both commit messages and the agent more broadly, use the global `AGENTS.md` file located `~/.config/zed/AGENTS.md` on macOS and Linux, `%APPDATA%\Zed\AGENTS.md` on Windows.
|
||||
|
||||
## Git Integrations {#git-integrations}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ A skill is a folder containing a `SKILL.md` file with metadata and instructions.
|
|||
|
||||
Zed includes a built-in `create-skill` skill — invoke it with `/create-skill` and the agent walks you through the process.
|
||||
|
||||
You can also open the Skill Creator from the Agent Panel using {#kb agent::OpenRulesLibrary}, or by clicking `...` and selecting **Skills**. Outside the panel, use the {#action agent::OpenSkillCreator} action from the command palette. It opens a window where you fill in the skill's name, description, scope (global or project-local), body, and optionally toggle `disable-model-invocation`.
|
||||
You can also open the Skills Manager from the Agent Panel using {#kb agent::ManageSkills}, or by clicking `...` and selecting **Skills**. Outside the panel, use the {#action agent::OpenSkillCreator} action from the command palette, or click **Create Skill** on the **AI > Skills** settings page. The creator opens as a page in the settings window where you fill in the skill's name, description, body, and optionally toggle `disable-model-invocation`. The skill is saved to the scope of the settings file selected in the settings window — the **User** tab creates a global skill, while a **Project** tab creates a project-local skill — and the form shows exactly where the file will be written.
|
||||
|
||||
Lastly, it's also possible to add a skill through importing it from an existing GitHub Markdown file. Open the command palette and look for the {#action agent::CreateSkillFromUrl} action. If your clipboard contains a supported GitHub `.md` URL, Zed pre-fills and fetches it automatically.
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ To install a skill, copy the skill's folder into `~/.agents/skills/` for global
|
|||
|
||||
Open the Settings Editor (`Cmd+,` on macOS, `Ctrl+,` on Linux/Windows) and navigate to **AI > Skills**, or go directly to [agent.skills](zed://settings/agent.skills).
|
||||
|
||||
The **User** tab shows your global skills. The **Project** tab shows skills for the current project.
|
||||
The **User** tab shows your global skills, and each **Project** tab shows the skills for that project.
|
||||
|
||||
For each skill you can:
|
||||
|
||||
|
|
@ -43,13 +43,16 @@ For each skill you can:
|
|||
- **Open** — opens the skill's `SKILL.md` file in the editor
|
||||
- **Delete** — removes the skill folder from disk
|
||||
|
||||
If no skills are installed, the page shows a **Create a Skill** button that opens the Skill Creator.
|
||||
In the skills page, you'll see a **Create Skill** button that opens the settings window, providing the ability to create a skill directly through the UI.
|
||||
|
||||
## Sharing Skills {#sharing-skills}
|
||||
|
||||
You can hand a skill to a teammate without hosting it anywhere. In the Skills settings page, click the **link** icon on a skill row to copy a `zed://skill?data=…` link to your clipboard. The link is self-contained: it embeds the full `SKILL.md` contents (base64url-encoded), so the recipient doesn't need access to your project or any registry.
|
||||
You can hand a skill to a teammate without hosting it anywhere. In the Skills settings page, click the **link** icon on a skill row to copy a `zed://skill?data=…` link to your clipboard.
|
||||
The link is self-contained: it embeds the full `SKILL.md` contents (base64url-encoded), so the recipient doesn't need access to your project or any registry.
|
||||
|
||||
When someone opens that link (for example by pasting it into their browser or clicking it in a chat), Zed launches the Skill Creator pre-filled with the shared skill. The recipient can review the name, description, and full body, choose a scope (global or project-local), and click **Save** to install it. Nothing is written to disk until they explicitly save, so a shared link can never silently install instructions into someone's agent.
|
||||
When someone opens that link (for example by pasting it into their browser or clicking it in a chat), Zed opens the "Create Skill" page in the settings window, pre-filled with the shared skill.
|
||||
The recipient can review the name, description, and full body, choose a scope by selecting the **User** tab (global) or a **Project** tab, and click **Save** to install it.
|
||||
Nothing is written to disk until they explicitly save, so a shared link can never silently install instructions into someone's agent.
|
||||
|
||||
## Using Skills {#using-skills}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue