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(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 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}