vtcode_core/prompts/
context.rs1use crate::config::ConfigManager;
2use crate::config::types::CapabilityLevel;
3use crate::skills::command_skills::is_model_catalog_eligible;
4use crate::skills::manager::SkillsManager;
5use crate::skills::model::SkillMetadata;
6use crate::tools::search_runtime::snapshot_for_workspace;
7use std::path::Path;
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Default)]
12pub struct PromptContext {
13 pub workspace: Option<PathBuf>,
15 pub languages: Vec<String>,
17 pub project_type: Option<String>,
19 pub available_tools: Vec<String>,
21 pub available_skills: Vec<(String, String)>,
23 pub available_skill_metadata: Vec<SkillMetadata>,
25 pub user_preferences: Option<UserPreferences>,
27 pub capability_level: Option<CapabilityLevel>,
29 pub current_directory: Option<PathBuf>,
31}
32
33#[derive(Debug, Clone)]
35pub struct UserPreferences {
36 pub preferred_languages: Vec<String>,
38 pub coding_style: Option<String>,
40 pub preferred_frameworks: Vec<String>,
42}
43
44impl PromptContext {
45 pub fn from_workspace(workspace: PathBuf) -> Self {
47 Self { workspace: Some(workspace), ..Default::default() }
48 }
49
50 pub fn add_language(&mut self, language: String) {
52 if !self.languages.contains(&language) {
53 self.languages.push(language);
54 }
55 }
56
57 pub fn set_project_type(&mut self, project_type: String) {
59 self.project_type = Some(project_type);
60 }
61
62 pub fn add_tool(&mut self, tool: String) {
64 if !self.available_tools.contains(&tool) {
65 self.available_tools.push(tool);
66 }
67 }
68
69 pub fn add_skill(&mut self, name: String, description: String) {
71 if !self.available_skills.iter().any(|(n, _)| n == &name) {
72 self.available_skills.push((name, description));
73 }
74 }
75
76 pub fn add_skill_metadata(&mut self, metadata: SkillMetadata) {
78 self.add_skill(metadata.name.clone(), metadata.description.clone());
79 if !self.available_skill_metadata.iter().any(|skill| skill.name == metadata.name) {
80 self.available_skill_metadata.push(metadata);
81 }
82 }
83
84 pub fn add_skill_metadata_entries(&mut self, skills: Vec<SkillMetadata>) {
86 for skill in skills {
87 self.add_skill_metadata(skill);
88 }
89 }
90
91 pub fn add_skills(&mut self, skills: Vec<(String, String)>) {
93 for (name, description) in skills {
94 self.add_skill(name, description);
95 }
96 }
97
98 pub fn set_capability_level(&mut self, level: CapabilityLevel) {
100 self.capability_level = Some(level);
101 }
102
103 pub fn infer_capability_level(&mut self) {
105 self.capability_level = Some(crate::prompts::guidelines::infer_capability_level(&self.available_tools));
106 }
107
108 pub fn set_current_directory(&mut self, dir: PathBuf) {
110 self.current_directory = Some(dir);
111 }
112
113 pub fn load_available_skills(&mut self) {
114 let home_dir = default_codex_home_dir();
115 self.load_available_skills_with_home_dir(home_dir.as_deref());
116 }
117
118 pub async fn load_available_skills_async(&mut self) {
124 let Some(workspace) = self.workspace.clone() else {
125 return;
126 };
127 let Some(home_dir) = default_codex_home_dir() else {
128 return;
129 };
130
131 let result = tokio::task::spawn_blocking(move || collect_available_skill_metadata(&workspace, &home_dir)).await;
132 match result {
133 Ok(skills) => self.add_skill_metadata_entries(skills),
134 Err(error) => tracing::warn!(%error, "skill metadata discovery task failed"),
135 }
136 }
137
138 pub fn replace_available_skills_with_named(&mut self, names: &[String]) {
139 let home_dir = default_codex_home_dir();
140 self.replace_available_skills_with_named_and_home_dir(names, home_dir.as_deref());
141 }
142
143 fn load_available_skills_with_home_dir(&mut self, home_dir: Option<&Path>) {
144 let Some(workspace) = self.workspace.as_deref() else {
145 return;
146 };
147 let Some(home_dir) = home_dir else {
148 return;
149 };
150
151 self.add_skill_metadata_entries(collect_available_skill_metadata(workspace, home_dir));
152 }
153
154 fn replace_available_skills_with_named_and_home_dir(&mut self, names: &[String], home_dir: Option<&Path>) {
155 self.available_skills.clear();
156 self.available_skill_metadata.clear();
157
158 if names.is_empty() {
159 return;
160 }
161
162 let Some(workspace) = self.workspace.as_deref() else {
163 return;
164 };
165 let Some(home_dir) = home_dir else {
166 return;
167 };
168
169 let requested = names
170 .iter()
171 .map(|name| name.trim().to_ascii_lowercase())
172 .filter(|name| !name.is_empty())
173 .collect::<std::collections::HashSet<_>>();
174 if requested.is_empty() {
175 return;
176 }
177
178 let bundled_skills_enabled = ConfigManager::load_from_workspace(workspace)
179 .map(|manager| manager.config().skills.bundled.enabled)
180 .unwrap_or(true);
181 let manager = SkillsManager::new_with_bundled_skills_enabled(home_dir.to_path_buf(), bundled_skills_enabled);
182 let outcome = manager.skills_metadata_lightweight(workspace);
183 self.add_skill_metadata_entries(
184 outcome
185 .skills
186 .into_iter()
187 .filter(is_model_catalog_eligible)
188 .filter(|skill| requested.contains(&skill.name.to_ascii_lowercase()))
189 .collect(),
190 );
191 }
192
193 pub fn from_workspace_tools(
195 workspace: impl AsRef<Path>,
196 available_tools: impl IntoIterator<Item = impl Into<String>>,
197 ) -> Self {
198 let mut context = Self {
199 workspace: Some(workspace.as_ref().to_path_buf()),
200 ..Default::default()
201 };
202
203 if let Ok(cwd) = std::env::current_dir() {
204 context.set_current_directory(cwd);
205 }
206
207 for language in snapshot_for_workspace(workspace.as_ref()).workspace_languages {
208 context.add_language(language);
209 }
210
211 for tool in available_tools {
212 context.add_tool(tool.into());
213 }
214
215 if !context.available_tools.is_empty() {
216 context.infer_capability_level();
217 }
218
219 context
220 }
221}
222
223fn collect_available_skill_metadata(workspace: &Path, home_dir: &Path) -> Vec<SkillMetadata> {
224 let bundled_skills_enabled = ConfigManager::load_from_workspace(workspace)
225 .map(|manager| manager.config().skills.bundled.enabled)
226 .unwrap_or(true);
227 let manager = SkillsManager::new_with_bundled_skills_enabled(home_dir.to_path_buf(), bundled_skills_enabled);
228 manager
229 .skills_metadata_lightweight(workspace)
230 .skills
231 .into_iter()
232 .filter(is_model_catalog_eligible)
233 .collect()
234}
235
236fn default_codex_home_dir() -> Option<PathBuf> {
237 std::env::var_os("CODEX_HOME")
238 .filter(|value| !value.is_empty())
239 .map(PathBuf::from)
240 .or_else(|| dirs::home_dir().map(|home| home.join(".codex")))
241}
242
243#[cfg(test)]
244mod tests {
245 use super::PromptContext;
246 use crate::config::constants::tools;
247 use std::fs;
248 use std::path::PathBuf;
249 use tempfile::TempDir;
250
251 fn write_skill(skill_dir: &std::path::Path, name: &str, description: &str) {
252 fs::create_dir_all(skill_dir).expect("create skill dir");
253 fs::write(
254 skill_dir.join("SKILL.md"),
255 format!("---\nname: {name}\ndescription: {description}\n---\n# {name}\n"),
256 )
257 .expect("write skill");
258 }
259
260 #[test]
261 fn from_workspace_tools_populates_workspace_and_tools() {
262 let context = PromptContext::from_workspace_tools(
263 PathBuf::from("/tmp/vtcode"),
264 [tools::CODE_SEARCH, tools::EXEC_COMMAND],
265 );
266
267 assert_eq!(context.workspace, Some(PathBuf::from("/tmp/vtcode")));
268 assert_eq!(context.available_tools.len(), 2);
269 assert!(context.capability_level.is_some());
270 assert!(context.current_directory.is_some());
271 }
272
273 #[test]
274 fn from_workspace_tools_populates_detected_languages() {
275 let workspace = TempDir::new().expect("workspace tempdir");
276 fs::create_dir_all(workspace.path().join("src")).expect("create src");
277 fs::create_dir_all(workspace.path().join("web")).expect("create web");
278 fs::write(workspace.path().join("src/lib.rs"), "fn alpha() {}\n").expect("write rust");
279 fs::write(workspace.path().join("web/app.ts"), "const app = 1;\n").expect("write ts");
280
281 let context = PromptContext::from_workspace_tools(workspace.path(), [tools::CODE_SEARCH]);
282
283 assert_eq!(context.languages, vec!["Rust".to_string(), "TypeScript".to_string()]);
284 }
285
286 #[test]
287 fn load_available_skills_discovers_repo_and_system_skills() {
288 let workspace = TempDir::new().expect("workspace tempdir");
289 fs::create_dir(workspace.path().join(".git")).expect("create git dir");
290 write_skill(&workspace.path().join(".agents/skills/repo-skill"), "repo-skill", "Repo-local skill");
291
292 let home = TempDir::new().expect("home tempdir");
293 let mut context = PromptContext::from_workspace_tools(workspace.path(), [tools::CODE_SEARCH]);
294
295 assert!(context.available_skill_metadata.is_empty());
296
297 context.load_available_skills_with_home_dir(Some(home.path()));
298
299 let skill_names = context
300 .available_skill_metadata
301 .iter()
302 .map(|skill| skill.name.as_str())
303 .collect::<Vec<_>>();
304
305 assert!(skill_names.contains(&"ast-grep"));
306 assert!(skill_names.contains(&"repo-skill"));
307 assert!(skill_names.contains(&"skill-creator"));
308 assert!(!skill_names.contains(&"cmd-review"));
309 }
310
311 #[test]
312 fn replace_available_skills_with_named_recomputes_scope() {
313 let workspace = TempDir::new().expect("workspace tempdir");
314 fs::create_dir(workspace.path().join(".git")).expect("create git dir");
315 write_skill(&workspace.path().join(".agents/skills/alpha"), "alpha", "Alpha skill");
316 write_skill(&workspace.path().join(".agents/skills/beta"), "beta", "Beta skill");
317
318 let home = TempDir::new().expect("home tempdir");
319 let mut context = PromptContext::from_workspace_tools(workspace.path(), [tools::CODE_SEARCH]);
320
321 context.replace_available_skills_with_named_and_home_dir(&["alpha".to_string()], Some(home.path()));
322 assert_eq!(
323 context
324 .available_skill_metadata
325 .iter()
326 .map(|skill| skill.name.as_str())
327 .collect::<Vec<_>>(),
328 vec!["alpha"]
329 );
330
331 context.replace_available_skills_with_named_and_home_dir(&["beta".to_string()], Some(home.path()));
332 assert_eq!(
333 context
334 .available_skill_metadata
335 .iter()
336 .map(|skill| skill.name.as_str())
337 .collect::<Vec<_>>(),
338 vec!["beta"]
339 );
340
341 context.replace_available_skills_with_named_and_home_dir(&[], Some(home.path()));
342 assert!(context.available_skill_metadata.is_empty());
343 assert!(context.available_skills.is_empty());
344 }
345}