Skip to main content

vtcode_config/loader/
merge.rs

1use crate::loader::layers::ConfigLayerMetadata;
2use hashbrown::HashMap;
3
4/// Recursively merge two TOML values.
5///
6/// If both values are tables, they are merged recursively.
7/// Otherwise, the `overlay` value replaces the `base` value.
8pub fn merge_toml_values(base: &mut toml::Value, overlay: &toml::Value) {
9    match (base, overlay) {
10        (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
11            for (key, value) in overlay_table {
12                if let Some(base_value) = base_table.get_mut(key) {
13                    merge_toml_values(base_value, value);
14                } else {
15                    base_table.insert(key.clone(), value.clone());
16                }
17            }
18        }
19        (base, overlay) => {
20            *base = overlay.clone();
21        }
22    }
23}
24
25/// Recursively merge two TOML values and record which layer last wrote each path.
26pub fn merge_toml_values_with_origins(
27    base: &mut toml::Value,
28    overlay: &toml::Value,
29    origins: &mut HashMap<String, ConfigLayerMetadata>,
30    layer: &ConfigLayerMetadata,
31) {
32    merge_with_origins(base, overlay, "", origins, layer);
33}
34
35fn merge_with_origins(
36    base: &mut toml::Value,
37    overlay: &toml::Value,
38    path: &str,
39    origins: &mut HashMap<String, ConfigLayerMetadata>,
40    layer: &ConfigLayerMetadata,
41) {
42    match (base, overlay) {
43        (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
44            for (key, value) in overlay_table {
45                let child_path = if path.is_empty() {
46                    key.clone()
47                } else {
48                    format!("{path}.{key}")
49                };
50
51                if let Some(base_value) = base_table.get_mut(key) {
52                    if base_value.is_table() && value.is_table() {
53                        merge_with_origins(base_value, value, &child_path, origins, layer);
54                    } else {
55                        *base_value = value.clone();
56                        assign_origins(value, &child_path, origins, layer);
57                    }
58                } else {
59                    base_table.insert(key.clone(), value.clone());
60                    assign_origins(value, &child_path, origins, layer);
61                }
62            }
63        }
64        (base, overlay) => {
65            *base = overlay.clone();
66            if !path.is_empty() {
67                assign_origins(overlay, path, origins, layer);
68            }
69        }
70    }
71}
72
73fn assign_origins(
74    value: &toml::Value,
75    path: &str,
76    origins: &mut HashMap<String, ConfigLayerMetadata>,
77    layer: &ConfigLayerMetadata,
78) {
79    match value {
80        toml::Value::Table(table) => {
81            for (key, child) in table {
82                let child_path = if path.is_empty() {
83                    key.clone()
84                } else {
85                    format!("{path}.{key}")
86                };
87                assign_origins(child, &child_path, origins, layer);
88            }
89        }
90        _ => {
91            origins.insert(path.to_string(), layer.clone());
92        }
93    }
94}
95
96/// Promote legacy top-level `default_provider` / `default_model` keys into the
97/// canonical `[agent]` table within a single layer value.
98///
99/// Older global files (and hand-edited configs) use bare
100/// `default_provider = "ollama"` / `default_model = "..."` at the document
101/// root. `VTCodeConfig` only deserializes `agent.provider` /
102/// `agent.default_model`, so without this promotion those keys are silently
103/// dropped and the runtime falls back to the compiled-in default (openrouter).
104/// The promotion is per-layer so normal layer precedence still applies: an
105/// explicit `[agent] provider` in the same file wins over its own top-level
106/// alias, and higher-precedence layers win over lower ones.
107pub(crate) fn normalize_legacy_top_level_provider_aliases(value: &mut toml::Value) {
108    let Some(table) = value.as_table_mut() else {
109        return;
110    };
111
112    let legacy_provider = table
113        .get("default_provider")
114        .and_then(|v| v.as_str())
115        .map(str::trim)
116        .filter(|s| !s.is_empty())
117        .map(ToOwned::to_owned);
118    let legacy_model = table
119        .get("default_model")
120        .and_then(|v| v.as_str())
121        .map(str::trim)
122        .filter(|s| !s.is_empty())
123        .map(ToOwned::to_owned);
124
125    if legacy_provider.is_none() && legacy_model.is_none() {
126        return;
127    }
128
129    let agent_entry = table
130        .entry("agent".to_string())
131        .or_insert(toml::Value::Table(toml::Table::new()));
132    let Some(agent_table) = agent_entry.as_table_mut() else {
133        return;
134    };
135
136    if let Some(provider) = legacy_provider
137        && !agent_table.contains_key("provider")
138    {
139        agent_table.insert("provider".to_string(), toml::Value::String(provider));
140    }
141    if let Some(model) = legacy_model
142        && !agent_table.contains_key("default_model")
143    {
144        agent_table.insert("default_model".to_string(), toml::Value::String(model));
145    }
146}