Skip to main content

tycode_core/skills/
prompt.rs

1use crate::module::{PromptComponent, PromptComponentId};
2use crate::settings::config::Settings;
3
4use super::discovery::SkillsManager;
5
6/// Prompt component ID for skills.
7pub const SKILLS_PROMPT_ID: PromptComponentId = PromptComponentId("skills");
8
9/// Prompt component that lists available skills in the system prompt.
10///
11/// This provides "Level 1" loading of skills - just metadata (name + description)
12/// that helps the AI decide when to invoke skills.
13pub 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            // Truncate description for table display
46            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}