#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
use crate::skills::types::Skill;
pub fn render_available_skills_xml(skills: &[Skill]) -> String {
let visible: Vec<&Skill> = skills
.iter()
.filter(|s| !s.disable_model_invocation)
.collect();
if visible.is_empty() {
return String::new();
}
let mut out = String::new();
out.push_str("\n\nThe following skills provide specialized instructions for specific tasks.\n");
out.push_str(
"Use the `read` tool to load a skill's file when the task matches its description.\n",
);
out.push_str("References inside a skill resolve against the skill's directory.\n");
out.push_str("\n<available_skills>\n");
for s in visible {
out.push_str(" <skill>\n");
out.push_str(&format!(" <name>{}</name>\n", escape_xml(&s.name)));
out.push_str(&format!(
" <description>{}</description>\n",
escape_xml(&s.description)
));
out.push_str(&format!(
" <location>{}</location>\n",
escape_xml(&s.file_path.display().to_string())
));
out.push_str(" </skill>\n");
}
out.push_str("</available_skills>");
out
}
fn escape_xml(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::skills::types::SkillSource;
use std::path::PathBuf;
fn s(name: &str, hidden: bool) -> Skill {
Skill {
name: name.into(),
description: format!("desc for {name}"),
file_path: PathBuf::from(format!("/skills/{name}.md")),
base_dir: PathBuf::from("/skills"),
disable_model_invocation: hidden,
source: SkillSource::Global,
}
}
#[test]
fn empty_when_no_skills() {
assert_eq!(render_available_skills_xml(&[]), "");
}
#[test]
fn empty_when_all_skills_hidden() {
assert_eq!(
render_available_skills_xml(&[s("a", true), s("b", true)]),
""
);
}
#[test]
fn renders_visible_skills_only() {
let out = render_available_skills_xml(&[s("a", false), s("b", true), s("c", false)]);
assert!(out.contains("<available_skills>"));
assert!(out.contains("</available_skills>"));
assert!(out.contains("<name>a</name>"));
assert!(out.contains("<name>c</name>"));
assert!(!out.contains("<name>b</name>"));
assert!(out.contains("<location>/skills/a.md</location>"));
assert!(out.contains("<description>desc for a</description>"));
}
#[test]
fn escapes_xml_special_chars() {
let mut skill = s("ok", false);
skill.description = "uses < and & and >".into();
let out = render_available_skills_xml(&[skill]);
assert!(out.contains("<"));
assert!(out.contains("&"));
assert!(out.contains(">"));
}
}