mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Support skills shortDescription. (#8278)
Allow SKILL.md to specify a more human-readable short description as skill metadata.
This commit is contained in:
committed by
GitHub
Unverified
parent
1cd1cf17c6
commit
358a5baba0
@@ -1057,6 +1057,9 @@ pub enum SkillScope {
|
||||
pub struct SkillMetadata {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[ts(optional)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub short_description: Option<String>,
|
||||
pub path: PathBuf,
|
||||
pub scope: SkillScope,
|
||||
}
|
||||
@@ -1083,6 +1086,7 @@ impl From<CoreSkillMetadata> for SkillMetadata {
|
||||
Self {
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
short_description: value.short_description,
|
||||
path: value.path,
|
||||
scope: value.scope.into(),
|
||||
}
|
||||
|
||||
@@ -3319,6 +3319,7 @@ fn skills_to_info(
|
||||
.map(|skill| codex_app_server_protocol::SkillMetadata {
|
||||
name: skill.name.clone(),
|
||||
description: skill.description.clone(),
|
||||
short_description: skill.short_description.clone(),
|
||||
path: skill.path.clone(),
|
||||
scope: skill.scope.into(),
|
||||
})
|
||||
|
||||
@@ -2187,6 +2187,7 @@ fn skills_to_info(skills: &[SkillMetadata]) -> Vec<ProtocolSkillMetadata> {
|
||||
.map(|skill| ProtocolSkillMetadata {
|
||||
name: skill.name.clone(),
|
||||
description: skill.description.clone(),
|
||||
short_description: skill.short_description.clone(),
|
||||
path: skill.path.clone(),
|
||||
scope: skill.scope,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
name: plan
|
||||
description: Generate a plan for how an agent should accomplish a complex coding task. Use when a user asks for a plan, and optionally when they want to save, find, read, update, or delete plan files in $CODEX_HOME/plans (default ~/.codex/plans).
|
||||
metadata:
|
||||
short-description: Create and manage plan markdown files under $CODEX_HOME/plans.
|
||||
---
|
||||
|
||||
# Plan
|
||||
|
||||
@@ -20,6 +20,14 @@ use tracing::error;
|
||||
struct SkillFrontmatter {
|
||||
name: String,
|
||||
description: String,
|
||||
#[serde(default)]
|
||||
metadata: SkillFrontmatterMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct SkillFrontmatterMetadata {
|
||||
#[serde(default, rename = "short-description")]
|
||||
short_description: Option<String>,
|
||||
}
|
||||
|
||||
const SKILLS_FILENAME: &str = "SKILL.md";
|
||||
@@ -27,6 +35,7 @@ const SKILLS_DIR_NAME: &str = "skills";
|
||||
const REPO_ROOT_CONFIG_DIR_NAME: &str = ".codex";
|
||||
const MAX_NAME_LEN: usize = 64;
|
||||
const MAX_DESCRIPTION_LEN: usize = 1024;
|
||||
const MAX_SHORT_DESCRIPTION_LEN: usize = MAX_DESCRIPTION_LEN;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SkillParseError {
|
||||
@@ -218,15 +227,29 @@ fn parse_skill_file(path: &Path, scope: SkillScope) -> Result<SkillMetadata, Ski
|
||||
|
||||
let name = sanitize_single_line(&parsed.name);
|
||||
let description = sanitize_single_line(&parsed.description);
|
||||
let short_description = parsed
|
||||
.metadata
|
||||
.short_description
|
||||
.as_deref()
|
||||
.map(sanitize_single_line)
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
validate_field(&name, MAX_NAME_LEN, "name")?;
|
||||
validate_field(&description, MAX_DESCRIPTION_LEN, "description")?;
|
||||
if let Some(short_description) = short_description.as_deref() {
|
||||
validate_field(
|
||||
short_description,
|
||||
MAX_SHORT_DESCRIPTION_LEN,
|
||||
"metadata.short-description",
|
||||
)?;
|
||||
}
|
||||
|
||||
let resolved_path = normalize_path(path).unwrap_or_else(|_| path.to_path_buf());
|
||||
|
||||
Ok(SkillMetadata {
|
||||
name,
|
||||
description,
|
||||
short_description,
|
||||
path: resolved_path,
|
||||
scope,
|
||||
})
|
||||
@@ -345,6 +368,7 @@ mod tests {
|
||||
let skill = &outcome.skills[0];
|
||||
assert_eq!(skill.name, "demo-skill");
|
||||
assert_eq!(skill.description, "does things carefully");
|
||||
assert_eq!(skill.short_description, None);
|
||||
let path_str = skill.path.to_string_lossy().replace('\\', "/");
|
||||
assert!(
|
||||
path_str.ends_with("skills/demo/SKILL.md"),
|
||||
@@ -352,6 +376,52 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_short_description_from_metadata() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let skill_dir = codex_home.path().join("skills/demo");
|
||||
fs::create_dir_all(&skill_dir).unwrap();
|
||||
let contents = "---\nname: demo-skill\ndescription: long description\nmetadata:\n short-description: short summary\n---\n\n# Body\n";
|
||||
fs::write(skill_dir.join(SKILLS_FILENAME), contents).unwrap();
|
||||
|
||||
let cfg = make_config(&codex_home);
|
||||
let outcome = load_skills(&cfg);
|
||||
assert!(
|
||||
outcome.errors.is_empty(),
|
||||
"unexpected errors: {:?}",
|
||||
outcome.errors
|
||||
);
|
||||
assert_eq!(outcome.skills.len(), 1);
|
||||
assert_eq!(
|
||||
outcome.skills[0].short_description,
|
||||
Some("short summary".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enforces_short_description_length_limits() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let skill_dir = codex_home.path().join("skills/demo");
|
||||
fs::create_dir_all(&skill_dir).unwrap();
|
||||
let too_long = "x".repeat(MAX_SHORT_DESCRIPTION_LEN + 1);
|
||||
let contents = format!(
|
||||
"---\nname: demo-skill\ndescription: long description\nmetadata:\n short-description: {too_long}\n---\n\n# Body\n"
|
||||
);
|
||||
fs::write(skill_dir.join(SKILLS_FILENAME), contents).unwrap();
|
||||
|
||||
let cfg = make_config(&codex_home);
|
||||
let outcome = load_skills(&cfg);
|
||||
assert_eq!(outcome.skills.len(), 0);
|
||||
assert_eq!(outcome.errors.len(), 1);
|
||||
assert!(
|
||||
outcome.errors[0]
|
||||
.message
|
||||
.contains("invalid metadata.short-description"),
|
||||
"expected length error, got: {:?}",
|
||||
outcome.errors
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_hidden_and_invalid() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_protocol::protocol::SkillScope;
|
||||
pub struct SkillMetadata {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub short_description: Option<String>,
|
||||
pub path: PathBuf,
|
||||
pub scope: SkillScope,
|
||||
}
|
||||
|
||||
@@ -1697,6 +1697,9 @@ pub enum SkillScope {
|
||||
pub struct SkillMetadata {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[ts(optional)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub short_description: Option<String>,
|
||||
pub path: PathBuf,
|
||||
pub scope: SkillScope,
|
||||
}
|
||||
|
||||
@@ -86,7 +86,11 @@ impl SkillPopup {
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or(&skill.name);
|
||||
let name = format!("{} ({slug})", skill.name);
|
||||
let description = skill.description.clone();
|
||||
let description = skill
|
||||
.short_description
|
||||
.as_ref()
|
||||
.unwrap_or(&skill.description)
|
||||
.clone();
|
||||
GenericDisplayRow {
|
||||
name,
|
||||
match_indices: indices,
|
||||
|
||||
@@ -3634,6 +3634,7 @@ fn skills_for_cwd(cwd: &Path, skills_entries: &[SkillsListEntry]) -> Vec<SkillMe
|
||||
.map(|skill| SkillMetadata {
|
||||
name: skill.name.clone(),
|
||||
description: skill.description.clone(),
|
||||
short_description: skill.short_description.clone(),
|
||||
path: skill.path.clone(),
|
||||
scope: skill.scope,
|
||||
})
|
||||
|
||||
@@ -86,7 +86,11 @@ impl SkillPopup {
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or(&skill.name);
|
||||
let name = format!("{} ({slug})", skill.name);
|
||||
let description = skill.description.clone();
|
||||
let description = skill
|
||||
.short_description
|
||||
.as_ref()
|
||||
.unwrap_or(&skill.description)
|
||||
.clone();
|
||||
GenericDisplayRow {
|
||||
name,
|
||||
match_indices: indices,
|
||||
|
||||
@@ -3539,6 +3539,7 @@ fn skills_for_cwd(cwd: &Path, skills_entries: &[SkillsListEntry]) -> Vec<SkillMe
|
||||
.map(|skill| SkillMetadata {
|
||||
name: skill.name.clone(),
|
||||
description: skill.description.clone(),
|
||||
short_description: skill.short_description.clone(),
|
||||
path: skill.path.clone(),
|
||||
scope: skill.scope,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user