tycode_core/skills/
prompt.rs1use crate::module::{PromptComponent, PromptComponentId};
2use crate::settings::config::Settings;
3
4use super::discovery::SkillsManager;
5
6pub const SKILLS_PROMPT_ID: PromptComponentId = PromptComponentId("skills");
8
9pub struct SkillsPromptComponent {
14 manager: SkillsManager,
15}
16
17impl SkillsPromptComponent {
18 pub fn new(manager: SkillsManager) -> Self {
19 Self { manager }
20 }
21}
22
23impl PromptComponent for SkillsPromptComponent {
24 fn id(&self) -> PromptComponentId {
25 SKILLS_PROMPT_ID
26 }
27
28 fn build_prompt_section(&self, _settings: &Settings) -> Option<String> {
29 let skills = self.manager.get_enabled_metadata();
30
31 if skills.is_empty() {
32 return None;
33 }
34
35 let mut output = String::new();
36 output.push_str("## Available Skills\n\n");
37 output.push_str("You have access to skills that provide specialized capabilities. ");
38 output.push_str("When a user's request matches a skill's description, ");
39 output.push_str("you MUST use the `invoke_skill` tool to load the skill instructions.\n\n");
40
41 output.push_str("| Skill | When to Use |\n");
42 output.push_str("|-------|-------------|\n");
43
44 for skill in &skills {
45 let desc = if skill.description.len() > 80 {
47 format!("{}...", &skill.description[..77])
48 } else {
49 skill.description.clone()
50 };
51 output.push_str(&format!("| {} | {} |\n", skill.name, desc));
52 }
53
54 output.push_str("\n**CRITICAL**: You MUST call `invoke_skill` tool with the skill name to load instructions. ");
55 output.push_str(
56 "Do NOT attempt to read SKILL.md files directly via file tools or set_tracked_files. ",
57 );
58 output.push_str("The `invoke_skill` tool is the ONLY correct way to activate a skill.\n");
59
60 Some(output)
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::settings::config::SkillsConfig;
68 use std::fs;
69 use tempfile::TempDir;
70
71 fn create_test_skill(dir: &std::path::Path, name: &str, description: &str) {
72 let skill_dir = dir.join(name);
73 fs::create_dir_all(&skill_dir).unwrap();
74
75 let content = format!(
76 r#"---
77name: {}
78description: {}
79---
80
81# {} Instructions
82"#,
83 name, description, name
84 );
85
86 fs::write(skill_dir.join("SKILL.md"), content).unwrap();
87 }
88
89 #[test]
90 fn test_prompt_with_skills() {
91 let temp = TempDir::new().unwrap();
92 let skills_dir = temp.path().join(".tycode").join("skills");
93 fs::create_dir_all(&skills_dir).unwrap();
94
95 create_test_skill(&skills_dir, "commit", "When committing changes to git");
96 create_test_skill(&skills_dir, "pdf", "When working with PDF documents");
97
98 let config = SkillsConfig::default();
99 let manager = SkillsManager::discover(&[], temp.path(), &config);
100 let component = SkillsPromptComponent::new(manager);
101
102 let settings = Settings::default();
103 let prompt = component.build_prompt_section(&settings).unwrap();
104
105 assert!(prompt.contains("## Available Skills"));
106 assert!(prompt.contains("| commit |"));
107 assert!(prompt.contains("| pdf |"));
108 assert!(prompt.contains("invoke_skill"));
109 assert!(prompt.contains("CRITICAL"));
110 }
111
112 #[test]
113 fn test_prompt_without_skills() {
114 let temp = TempDir::new().unwrap();
115
116 let config = SkillsConfig::default();
117 let manager = SkillsManager::discover(&[], temp.path(), &config);
118 let component = SkillsPromptComponent::new(manager);
119
120 let settings = Settings::default();
121 let prompt = component.build_prompt_section(&settings);
122
123 assert!(prompt.is_none());
124 }
125}