Skip to main content

vtcode_core/prompts/
context.rs

1use crate::config::ConfigManager;
2use crate::config::types::CapabilityLevel;
3use crate::ide_context::EditorContextSnapshot;
4use crate::skills::command_skills::is_model_catalog_eligible;
5use crate::skills::manager::SkillsManager;
6use crate::skills::model::SkillMetadata;
7use crate::tools::search_runtime::snapshot_for_workspace;
8use std::path::Path;
9use std::path::PathBuf;
10
11/// Context information for prompt generation
12#[derive(Debug, Clone, Default)]
13pub struct PromptContext {
14    /// Current workspace path
15    pub workspace: Option<PathBuf>,
16    /// Detected programming languages
17    pub languages: Vec<String>,
18    /// Project type (if detected)
19    pub project_type: Option<String>,
20    /// Available tools
21    pub available_tools: Vec<String>,
22    /// Available skills (name: description)
23    pub available_skills: Vec<(String, String)>,
24    /// Available skill metadata for lean prompt rendering
25    pub available_skill_metadata: Vec<SkillMetadata>,
26    /// User preferences
27    pub user_preferences: Option<UserPreferences>,
28    /// Capability level (inferred from tools or explicitly set)
29    pub capability_level: Option<CapabilityLevel>,
30    /// Current working directory (different from workspace root)
31    pub current_directory: Option<PathBuf>,
32    /// Active IDE/editor context snapshot when available.
33    pub editor_context: Option<EditorContextSnapshot>,
34}
35
36/// User preferences for prompt customization
37#[derive(Debug, Clone)]
38pub struct UserPreferences {
39    /// Preferred programming languages
40    pub preferred_languages: Vec<String>,
41    /// Coding style preferences
42    pub coding_style: Option<String>,
43    /// Framework preferences
44    pub preferred_frameworks: Vec<String>,
45}
46
47impl PromptContext {
48    /// Create context from workspace
49    pub fn from_workspace(workspace: PathBuf) -> Self {
50        Self {
51            workspace: Some(workspace),
52            ..Default::default()
53        }
54    }
55
56    /// Add detected language
57    pub fn add_language(&mut self, language: String) {
58        if !self.languages.contains(&language) {
59            self.languages.push(language);
60        }
61    }
62
63    /// Set project type
64    pub fn set_project_type(&mut self, project_type: String) {
65        self.project_type = Some(project_type);
66    }
67
68    /// Add available tool
69    pub fn add_tool(&mut self, tool: String) {
70        if !self.available_tools.contains(&tool) {
71            self.available_tools.push(tool);
72        }
73    }
74
75    /// Add available skill
76    pub fn add_skill(&mut self, name: String, description: String) {
77        if !self.available_skills.iter().any(|(n, _)| n == &name) {
78            self.available_skills.push((name, description));
79        }
80    }
81
82    /// Add available skill metadata
83    pub fn add_skill_metadata(&mut self, metadata: SkillMetadata) {
84        self.add_skill(metadata.name.clone(), metadata.description.clone());
85        if !self
86            .available_skill_metadata
87            .iter()
88            .any(|skill| skill.name == metadata.name)
89        {
90            self.available_skill_metadata.push(metadata);
91        }
92    }
93
94    /// Add multiple skill metadata entries
95    pub fn add_skill_metadata_entries(&mut self, skills: Vec<SkillMetadata>) {
96        for skill in skills {
97            self.add_skill_metadata(skill);
98        }
99    }
100
101    /// Add multiple skills
102    pub fn add_skills(&mut self, skills: Vec<(String, String)>) {
103        for (name, description) in skills {
104            self.add_skill(name, description);
105        }
106    }
107
108    /// Set capability level explicitly
109    pub fn set_capability_level(&mut self, level: CapabilityLevel) {
110        self.capability_level = Some(level);
111    }
112
113    /// Infer capability level from available tools
114    pub fn infer_capability_level(&mut self) {
115        self.capability_level = Some(crate::prompts::guidelines::infer_capability_level(
116            &self.available_tools,
117        ));
118    }
119
120    /// Set current working directory
121    pub fn set_current_directory(&mut self, dir: PathBuf) {
122        self.current_directory = Some(dir);
123    }
124
125    pub fn set_editor_context(&mut self, snapshot: Option<EditorContextSnapshot>) {
126        self.editor_context = snapshot;
127    }
128
129    pub fn load_available_skills(&mut self) {
130        let home_dir = default_codex_home_dir();
131        self.load_available_skills_with_home_dir(home_dir.as_deref());
132    }
133
134    pub(crate) fn load_available_skills_with_home_dir(&mut self, home_dir: Option<&Path>) {
135        let Some(workspace) = self.workspace.as_deref() else {
136            return;
137        };
138        let Some(home_dir) = home_dir else {
139            return;
140        };
141
142        let bundled_skills_enabled = ConfigManager::load_from_workspace(workspace)
143            .map(|manager| manager.config().skills.bundled.enabled)
144            .unwrap_or(true);
145        let manager = SkillsManager::new_with_bundled_skills_enabled(
146            home_dir.to_path_buf(),
147            bundled_skills_enabled,
148        );
149        let outcome = manager.skills_metadata_lightweight(workspace);
150        self.add_skill_metadata_entries(
151            outcome
152                .skills
153                .into_iter()
154                .filter(is_model_catalog_eligible)
155                .collect(),
156        );
157    }
158
159    /// Build prompt context from a workspace and the tool names exposed to the model.
160    pub fn from_workspace_tools(
161        workspace: impl AsRef<Path>,
162        available_tools: impl IntoIterator<Item = impl Into<String>>,
163    ) -> Self {
164        let mut context = Self {
165            workspace: Some(workspace.as_ref().to_path_buf()),
166            ..Default::default()
167        };
168
169        if let Ok(cwd) = std::env::current_dir() {
170            context.set_current_directory(cwd);
171        }
172
173        if let Ok(snapshot) = EditorContextSnapshot::read_from_env() {
174            context.set_editor_context(snapshot);
175        }
176
177        for language in snapshot_for_workspace(workspace.as_ref()).workspace_languages {
178            context.add_language(language);
179        }
180
181        for tool in available_tools {
182            context.add_tool(tool.into());
183        }
184
185        if !context.available_tools.is_empty() {
186            context.infer_capability_level();
187        }
188
189        context
190    }
191}
192
193fn default_codex_home_dir() -> Option<PathBuf> {
194    std::env::var_os("CODEX_HOME")
195        .filter(|value| !value.is_empty())
196        .map(PathBuf::from)
197        .or_else(|| dirs::home_dir().map(|home| home.join(".codex")))
198}
199
200#[cfg(test)]
201mod tests {
202    use super::PromptContext;
203    use std::fs;
204    use std::path::PathBuf;
205    use tempfile::TempDir;
206
207    fn write_skill(skill_dir: &std::path::Path, name: &str, description: &str) {
208        fs::create_dir_all(skill_dir).expect("create skill dir");
209        fs::write(
210            skill_dir.join("SKILL.md"),
211            format!("---\nname: {name}\ndescription: {description}\n---\n# {name}\n"),
212        )
213        .expect("write skill");
214    }
215
216    #[test]
217    fn from_workspace_tools_populates_workspace_and_tools() {
218        let context = PromptContext::from_workspace_tools(
219            PathBuf::from("/tmp/vtcode"),
220            ["unified_search", "unified_exec"],
221        );
222
223        assert_eq!(context.workspace, Some(PathBuf::from("/tmp/vtcode")));
224        assert_eq!(context.available_tools.len(), 2);
225        assert!(context.capability_level.is_some());
226        assert!(context.current_directory.is_some());
227    }
228
229    #[test]
230    fn from_workspace_tools_populates_detected_languages() {
231        let workspace = TempDir::new().expect("workspace tempdir");
232        fs::create_dir_all(workspace.path().join("src")).expect("create src");
233        fs::create_dir_all(workspace.path().join("web")).expect("create web");
234        fs::write(workspace.path().join("src/lib.rs"), "fn alpha() {}\n").expect("write rust");
235        fs::write(workspace.path().join("web/app.ts"), "const app = 1;\n").expect("write ts");
236
237        let context = PromptContext::from_workspace_tools(workspace.path(), ["unified_search"]);
238
239        assert_eq!(
240            context.languages,
241            vec!["Rust".to_string(), "TypeScript".to_string()]
242        );
243    }
244
245    #[test]
246    fn load_available_skills_discovers_repo_and_system_skills() {
247        let workspace = TempDir::new().expect("workspace tempdir");
248        fs::create_dir(workspace.path().join(".git")).expect("create git dir");
249        write_skill(
250            &workspace.path().join(".agents/skills/repo-skill"),
251            "repo-skill",
252            "Repo-local skill",
253        );
254
255        let home = TempDir::new().expect("home tempdir");
256        let mut context = PromptContext::from_workspace_tools(workspace.path(), ["unified_search"]);
257
258        assert!(context.available_skill_metadata.is_empty());
259
260        context.load_available_skills_with_home_dir(Some(home.path()));
261
262        let skill_names = context
263            .available_skill_metadata
264            .iter()
265            .map(|skill| skill.name.as_str())
266            .collect::<Vec<_>>();
267
268        assert!(skill_names.contains(&"ast-grep"));
269        assert!(skill_names.contains(&"repo-skill"));
270        assert!(skill_names.contains(&"skill-creator"));
271        assert!(!skill_names.contains(&"cmd-review"));
272    }
273}