Skip to main content

vtcode_config/loader/manager/
inspect.rs

1use super::*;
2
3impl ConfigManager {
4    /// Get the loaded configuration
5    pub fn config(&self) -> &VTCodeConfig {
6        &self.config
7    }
8
9    /// Get the timing metrics recorded during loading, if available.
10    pub fn phase_timing(&self) -> Option<ConfigPhaseTiming> {
11        self.phase_timing
12    }
13
14    /// Get the configuration file path (if loaded from file)
15    pub fn config_path(&self) -> Option<&Path> {
16        self.config_path.as_deref()
17    }
18
19    /// Return whether a path belongs to a repository-controlled config layer.
20    ///
21    /// This keeps callers that persist a full config document from having to
22    /// duplicate layer-origin and explicit-session checks.
23    #[must_use]
24    pub fn is_repository_controlled_path(&self, path: &Path) -> bool {
25        if self.explicit_config_is_trusted {
26            return false;
27        }
28
29        if self
30            .layer_stack
31            .layers()
32            .iter()
33            .any(|layer| layer.is_enabled() && Self::repository_source_matches_path(&layer.source, path))
34        {
35            return true;
36        }
37
38        self.config_path.is_none()
39            && self.workspace_root.as_deref().is_some_and(|workspace| {
40                Self::repository_source_matches_path(
41                    &ConfigLayerSource::Workspace { file: workspace.join(&self.config_file_name) },
42                    path,
43                )
44            })
45    }
46
47    /// Get the active workspace root for this manager.
48    pub fn workspace_root(&self) -> Option<&Path> {
49        self.workspace_root.as_deref()
50    }
51
52    /// Resolve the workspace-level config file this manager reads from, i.e.
53    /// the highest enabled `Workspace` layer.
54    ///
55    /// With a session-explicit config file (`--config` / `VTCODE_CONFIG_PATH`)
56    /// that layer is the override file itself, so config writes land where the
57    /// session actually reads from. Falls back to `<workspace>/<config_file_name>`
58    /// when no workspace layer is present.
59    pub fn preferred_workspace_config_path(&self, workspace: &Path) -> PathBuf {
60        self.layer_stack
61            .layers()
62            .iter()
63            .rev()
64            .find_map(|layer| match &layer.source {
65                ConfigLayerSource::Workspace { file } if layer.is_enabled() => Some(file.clone()),
66                _ => None,
67            })
68            .unwrap_or_else(|| workspace.join(&self.config_file_name))
69    }
70
71    /// Get the config filename used by this manager (usually `vtcode.toml`).
72    pub fn config_file_name(&self) -> &str {
73        &self.config_file_name
74    }
75
76    /// Get the configuration layer stack
77    pub fn layer_stack(&self) -> &ConfigLayerStack {
78        &self.layer_stack
79    }
80
81    /// Resolve the canonical user-level config file VT Code should write to.
82    pub fn preferred_user_config_path(&self) -> Option<PathBuf> {
83        self.canonical_user_config_path.clone()
84    }
85
86    /// Return every supported user-level config path for the loaded config
87    /// filename, including paths that do not exist yet.
88    ///
89    /// Callers that monitor configuration must retain the nonexistent paths:
90    /// a later `None -> Some(mtime)` transition is a real configuration change.
91    pub fn user_config_paths(&self) -> Vec<PathBuf> {
92        let mut paths = self.tracked_user_config_paths.clone();
93
94        for path in self
95            .layer_stack
96            .layers()
97            .iter()
98            .filter_map(|layer| match (&layer.source, layer.is_enabled()) {
99                (ConfigLayerSource::User { file }, true) => Some(file),
100                _ => None,
101            })
102        {
103            if !paths.iter().any(|existing| existing == path) {
104                paths.push(path.clone());
105            }
106        }
107
108        paths
109    }
110
111    /// Return every configuration file location that can affect a workspace.
112    ///
113    /// The list intentionally includes files that do not exist yet. Polling
114    /// callers can therefore observe file creation as well as modification or
115    /// deletion. Paths retain their configured spelling so consumers that
116    /// write a target can still apply the no-follow file policy at the final
117    /// path component.
118    pub fn watched_config_paths(workspace: &Path) -> Vec<PathBuf> {
119        let provider = defaults::current_config_defaults();
120        let config_file_name = provider.config_file_name().to_string();
121        let workspace_paths = provider.workspace_paths_for(workspace);
122        let workspace_root = workspace_paths.workspace_root().to_path_buf();
123        let mut paths = Vec::new();
124
125        let mut push_unique = |path: PathBuf| {
126            if !paths.iter().any(|existing| existing == &path) {
127                paths.push(path);
128            }
129        };
130
131        if let Some(explicit_path) = session_override::explicit_config_path() {
132            push_unique(explicit_path);
133        }
134        if let Ok(system_paths) = provider.system_config_paths(&config_file_name) {
135            for path in system_paths {
136                push_unique(path);
137            }
138        }
139        for path in provider.home_config_paths(&config_file_name) {
140            push_unique(path);
141        }
142        if let Ok(Some(canonical_path)) = provider.canonical_user_config_path(&config_file_name) {
143            push_unique(canonical_path);
144        }
145
146        if let Some(project_name) = Self::current_project_name(&workspace_root) {
147            push_unique(
148                workspace_paths
149                    .config_dir()
150                    .join("projects")
151                    .join(project_name)
152                    .join("config")
153                    .join(&config_file_name),
154            );
155        }
156
157        push_unique(workspace_paths.config_dir().join(&config_file_name));
158        push_unique(workspace_root.join(&config_file_name));
159        push_unique(workspace_root.join(".vtcode").join("theme.toml"));
160        paths
161    }
162
163    pub(super) fn repository_config_paths(workspace: &Path) -> Vec<PathBuf> {
164        let provider = defaults::current_config_defaults();
165        let config_file_name = provider.config_file_name().to_string();
166        let workspace_paths = provider.workspace_paths_for(workspace);
167        let workspace_root = canonicalize_workspace_root(workspace_paths.workspace_root());
168        let config_dir = workspace_paths.config_dir();
169        let mut paths = Vec::with_capacity(3);
170
171        let mut push_unique = |path: PathBuf| {
172            if !paths.iter().any(|existing| existing == &path) {
173                paths.push(path);
174            }
175        };
176
177        if let Some(project_path) = Self::project_config_path(&config_dir, &workspace_root, &config_file_name) {
178            push_unique(project_path);
179        }
180        push_unique(config_dir.join(&config_file_name));
181        push_unique(workspace_root.join(&config_file_name));
182        paths
183    }
184
185    pub(super) fn repository_source_matches_path(source: &ConfigLayerSource, path: &Path) -> bool {
186        let Some(source_file) = (match source {
187            ConfigLayerSource::Project { file } | ConfigLayerSource::Workspace { file } => Some(file),
188            ConfigLayerSource::System { .. } | ConfigLayerSource::User { .. } | ConfigLayerSource::Runtime => None,
189        }) else {
190            return false;
191        };
192
193        canonicalize_workspace_root(source_file) == canonicalize_workspace_root(path)
194    }
195
196    /// Get the effective TOML configuration
197    pub fn effective_config(&self) -> toml::Value {
198        self.layer_stack.effective_config()
199    }
200
201    /// Return whether any enabled layer explicitly sets a top-level key.
202    ///
203    /// Borrow-only scan that avoids a full TOML merge for single-key presence
204    /// checks on the startup hot path (e.g. `default_primary_agent`).
205    pub fn has_explicit_top_level_key(&self, key: &str) -> bool {
206        self.layer_stack
207            .layers()
208            .iter()
209            .filter(|layer| layer.is_enabled())
210            .any(|layer| layer.config.as_table().is_some_and(|table| table.contains_key(key)))
211    }
212
213    /// Get session duration from agent config
214    pub fn session_duration(&self) -> std::time::Duration {
215        std::time::Duration::from_secs(60 * 60) // Default 1 hour
216    }
217}