vtcode_core/prompts/
context.rs1use 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#[derive(Debug, Clone, Default)]
13pub struct PromptContext {
14 pub workspace: Option<PathBuf>,
16 pub languages: Vec<String>,
18 pub project_type: Option<String>,
20 pub available_tools: Vec<String>,
22 pub available_skills: Vec<(String, String)>,
24 pub available_skill_metadata: Vec<SkillMetadata>,
26 pub user_preferences: Option<UserPreferences>,
28 pub capability_level: Option<CapabilityLevel>,
30 pub current_directory: Option<PathBuf>,
32 pub editor_context: Option<EditorContextSnapshot>,
34}
35
36#[derive(Debug, Clone)]
38pub struct UserPreferences {
39 pub preferred_languages: Vec<String>,
41 pub coding_style: Option<String>,
43 pub preferred_frameworks: Vec<String>,
45}
46
47impl PromptContext {
48 pub fn from_workspace(workspace: PathBuf) -> Self {
50 Self {
51 workspace: Some(workspace),
52 ..Default::default()
53 }
54 }
55
56 pub fn add_language(&mut self, language: String) {
58 if !self.languages.contains(&language) {
59 self.languages.push(language);
60 }
61 }
62
63 pub fn set_project_type(&mut self, project_type: String) {
65 self.project_type = Some(project_type);
66 }
67
68 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 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 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 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 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 pub fn set_capability_level(&mut self, level: CapabilityLevel) {
110 self.capability_level = Some(level);
111 }
112
113 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 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 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}