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 fn replace_available_skills_with_named(&mut self, names: &[String]) {
135        let home_dir = default_codex_home_dir();
136        self.replace_available_skills_with_named_and_home_dir(names, home_dir.as_deref());
137    }
138
139    pub(crate) fn load_available_skills_with_home_dir(&mut self, home_dir: Option<&Path>) {
140        let Some(workspace) = self.workspace.as_deref() else {
141            return;
142        };
143        let Some(home_dir) = home_dir else {
144            return;
145        };
146
147        let bundled_skills_enabled = ConfigManager::load_from_workspace(workspace)
148            .map(|manager| manager.config().skills.bundled.enabled)
149            .unwrap_or(true);
150        let manager = SkillsManager::new_with_bundled_skills_enabled(
151            home_dir.to_path_buf(),
152            bundled_skills_enabled,
153        );
154        let outcome = manager.skills_metadata_lightweight(workspace);
155        self.add_skill_metadata_entries(
156            outcome
157                .skills
158                .into_iter()
159                .filter(is_model_catalog_eligible)
160                .collect(),
161        );
162    }
163
164    pub(crate) fn replace_available_skills_with_named_and_home_dir(
165        &mut self,
166        names: &[String],
167        home_dir: Option<&Path>,
168    ) {
169        self.available_skills.clear();
170        self.available_skill_metadata.clear();
171
172        if names.is_empty() {
173            return;
174        }
175
176        let Some(workspace) = self.workspace.as_deref() else {
177            return;
178        };
179        let Some(home_dir) = home_dir else {
180            return;
181        };
182
183        let requested = names
184            .iter()
185            .map(|name| name.trim().to_ascii_lowercase())
186            .filter(|name| !name.is_empty())
187            .collect::<std::collections::HashSet<_>>();
188        if requested.is_empty() {
189            return;
190        }
191
192        let bundled_skills_enabled = ConfigManager::load_from_workspace(workspace)
193            .map(|manager| manager.config().skills.bundled.enabled)
194            .unwrap_or(true);
195        let manager = SkillsManager::new_with_bundled_skills_enabled(
196            home_dir.to_path_buf(),
197            bundled_skills_enabled,
198        );
199        let outcome = manager.skills_metadata_lightweight(workspace);
200        self.add_skill_metadata_entries(
201            outcome
202                .skills
203                .into_iter()
204                .filter(is_model_catalog_eligible)
205                .filter(|skill| requested.contains(&skill.name.to_ascii_lowercase()))
206                .collect(),
207        );
208    }
209
210    /// Build prompt context from a workspace and the tool names exposed to the model.
211    pub fn from_workspace_tools(
212        workspace: impl AsRef<Path>,
213        available_tools: impl IntoIterator<Item = impl Into<String>>,
214    ) -> Self {
215        let mut context = Self {
216            workspace: Some(workspace.as_ref().to_path_buf()),
217            ..Default::default()
218        };
219
220        if let Ok(cwd) = std::env::current_dir() {
221            context.set_current_directory(cwd);
222        }
223
224        if let Ok(snapshot) = EditorContextSnapshot::read_from_env() {
225            context.set_editor_context(snapshot);
226        }
227
228        for language in snapshot_for_workspace(workspace.as_ref()).workspace_languages {
229            context.add_language(language);
230        }
231
232        for tool in available_tools {
233            context.add_tool(tool.into());
234        }
235
236        if !context.available_tools.is_empty() {
237            context.infer_capability_level();
238        }
239
240        context
241    }
242}
243
244fn default_codex_home_dir() -> Option<PathBuf> {
245    std::env::var_os("CODEX_HOME")
246        .filter(|value| !value.is_empty())
247        .map(PathBuf::from)
248        .or_else(|| dirs::home_dir().map(|home| home.join(".codex")))
249}
250
251#[cfg(test)]
252mod tests {
253    use super::PromptContext;
254    use std::fs;
255    use std::path::PathBuf;
256    use tempfile::TempDir;
257
258    fn write_skill(skill_dir: &std::path::Path, name: &str, description: &str) {
259        fs::create_dir_all(skill_dir).expect("create skill dir");
260        fs::write(
261            skill_dir.join("SKILL.md"),
262            format!("---\nname: {name}\ndescription: {description}\n---\n# {name}\n"),
263        )
264        .expect("write skill");
265    }
266
267    #[test]
268    fn from_workspace_tools_populates_workspace_and_tools() {
269        let context = PromptContext::from_workspace_tools(
270            PathBuf::from("/tmp/vtcode"),
271            ["unified_search", "unified_exec"],
272        );
273
274        assert_eq!(context.workspace, Some(PathBuf::from("/tmp/vtcode")));
275        assert_eq!(context.available_tools.len(), 2);
276        assert!(context.capability_level.is_some());
277        assert!(context.current_directory.is_some());
278    }
279
280    #[test]
281    fn from_workspace_tools_populates_detected_languages() {
282        let workspace = TempDir::new().expect("workspace tempdir");
283        fs::create_dir_all(workspace.path().join("src")).expect("create src");
284        fs::create_dir_all(workspace.path().join("web")).expect("create web");
285        fs::write(workspace.path().join("src/lib.rs"), "fn alpha() {}\n").expect("write rust");
286        fs::write(workspace.path().join("web/app.ts"), "const app = 1;\n").expect("write ts");
287
288        let context = PromptContext::from_workspace_tools(workspace.path(), ["unified_search"]);
289
290        assert_eq!(
291            context.languages,
292            vec!["Rust".to_string(), "TypeScript".to_string()]
293        );
294    }
295
296    #[test]
297    fn load_available_skills_discovers_repo_and_system_skills() {
298        let workspace = TempDir::new().expect("workspace tempdir");
299        fs::create_dir(workspace.path().join(".git")).expect("create git dir");
300        write_skill(
301            &workspace.path().join(".agents/skills/repo-skill"),
302            "repo-skill",
303            "Repo-local skill",
304        );
305
306        let home = TempDir::new().expect("home tempdir");
307        let mut context = PromptContext::from_workspace_tools(workspace.path(), ["unified_search"]);
308
309        assert!(context.available_skill_metadata.is_empty());
310
311        context.load_available_skills_with_home_dir(Some(home.path()));
312
313        let skill_names = context
314            .available_skill_metadata
315            .iter()
316            .map(|skill| skill.name.as_str())
317            .collect::<Vec<_>>();
318
319        assert!(skill_names.contains(&"ast-grep"));
320        assert!(skill_names.contains(&"repo-skill"));
321        assert!(skill_names.contains(&"skill-creator"));
322        assert!(!skill_names.contains(&"cmd-review"));
323    }
324
325    #[test]
326    fn replace_available_skills_with_named_recomputes_scope() {
327        let workspace = TempDir::new().expect("workspace tempdir");
328        fs::create_dir(workspace.path().join(".git")).expect("create git dir");
329        write_skill(
330            &workspace.path().join(".agents/skills/alpha"),
331            "alpha",
332            "Alpha skill",
333        );
334        write_skill(
335            &workspace.path().join(".agents/skills/beta"),
336            "beta",
337            "Beta skill",
338        );
339
340        let home = TempDir::new().expect("home tempdir");
341        let mut context = PromptContext::from_workspace_tools(workspace.path(), ["unified_search"]);
342
343        context.replace_available_skills_with_named_and_home_dir(
344            &["alpha".to_string()],
345            Some(home.path()),
346        );
347        assert_eq!(
348            context
349                .available_skill_metadata
350                .iter()
351                .map(|skill| skill.name.as_str())
352                .collect::<Vec<_>>(),
353            vec!["alpha"]
354        );
355
356        context.replace_available_skills_with_named_and_home_dir(
357            &["beta".to_string()],
358            Some(home.path()),
359        );
360        assert_eq!(
361            context
362                .available_skill_metadata
363                .iter()
364                .map(|skill| skill.name.as_str())
365                .collect::<Vec<_>>(),
366            vec!["beta"]
367        );
368
369        context.replace_available_skills_with_named_and_home_dir(&[], Some(home.path()));
370        assert!(context.available_skill_metadata.is_empty());
371        assert!(context.available_skills.is_empty());
372    }
373}