1use crate::types::{
4 BashConfig, Config, FormatConfig, MagicKeywordsConfig, QuestionToolConfig, SlopConfig,
5 ToolsConfig, default_auto_background_after_secs, default_auto_index_chunks,
6 default_auto_index_files, default_code_min_score, default_code_top_k, default_compaction_llm,
7 default_compaction_method, default_compaction_threshold, default_consolidate_max,
8 default_goal_max_turns, default_intent_guidance, default_max_background_jobs,
9 default_max_tokens, default_memory_backend, default_memory_embed_dim,
10 default_memory_index_bytes, default_memory_index_lines, default_memory_min_score,
11 default_memory_scope, default_memory_token_budget, default_memory_top_k, default_model_race,
12 default_prompt_cache, default_prompt_suggestions, default_race_after_ms,
13 default_response_cache, default_retain_every_n, default_retain_max_facts,
14 default_risk_threshold, default_sandbox_fallback, default_sandbox_mode,
15 default_session_min_score, default_session_top_k, default_stream_retry_secs,
16 default_swarm_max_agents, default_tool_profile, default_undo_depth,
17};
18use whycodes_core::types::PermissionSet;
19
20impl Config {
21 pub fn merge_with(&self, other: &Config) -> Config {
27 let mut merged = self.clone();
28 merged.merge_providers(other);
29 merged.merge_models(other);
30 merged.merge_agents(other);
31 merged.merge_defaults(other);
32 merged.merge_command_configs(other);
33 merged.tools = self.tools.merge_with(&other.tools);
34 merged.merge_session(other);
35 merged.merge_tui(other);
36 merged.merge_general(other);
37 merged.merge_servers(other);
38 merged.merge_maps(other);
39 merged.merge_security(other);
40 merged.merge_hooks(other);
41 merged.merge_memory(other);
42 merged.merge_memory_index(other);
43 merged.merge_swarm(other);
44 merged.merge_tail(other);
45 merged
46 }
47
48 fn merge_providers(&mut self, other: &Config) {
49 for (key, provider) in &other.providers {
51 self.providers
52 .entry(key.clone())
53 .and_modify(|existing| {
54 if provider.api_key.is_some() {
55 existing.api_key = provider.api_key.clone();
56 }
57 if provider.api_base.is_some() {
58 existing.api_base = provider.api_base.clone();
59 }
60 if provider.base_url.is_some() {
61 existing.base_url = provider.base_url.clone();
62 }
63 if provider.headers.is_some() {
64 existing.headers = provider.headers.clone();
65 }
66 if !provider.models.is_empty() {
67 existing.models = provider.models.clone();
68 }
69 if provider.tool_arguments.is_some() {
70 existing.tool_arguments = provider.tool_arguments;
71 }
72 if !provider.extra.is_empty() {
73 existing.extra = provider.extra.clone();
74 }
75 if !provider.credentials.is_empty() {
76 existing.credentials = provider.credentials.clone();
77 }
78 })
79 .or_insert_with(|| provider.clone());
80 }
81 }
82
83 fn merge_models(&mut self, other: &Config) {
84 for (key, model) in &other.models {
86 self.models
87 .entry(key.clone())
88 .and_modify(|existing| {
89 if model.max_tokens.is_some() {
90 existing.max_tokens = model.max_tokens;
91 }
92 if model.context_window.is_some() {
93 existing.context_window = model.context_window;
94 }
95 if model.temperature.is_some() {
96 existing.temperature = model.temperature;
97 }
98 if model.top_p.is_some() {
99 existing.top_p = model.top_p;
100 }
101 if model.thinking.is_some() {
102 existing.thinking = model.thinking;
103 }
104 if model.supports_tools.is_some() {
105 existing.supports_tools = model.supports_tools;
106 }
107 if model.supports_images.is_some() {
108 existing.supports_images = model.supports_images;
109 }
110 })
111 .or_insert_with(|| model.clone());
112 }
113 }
114
115 fn merge_agents(&mut self, other: &Config) {
116 for agent in &other.agents {
121 match self.agents.iter_mut().find(|a| a.name == agent.name) {
122 Some(existing) => {
123 if agent.model.is_some() {
124 existing.model = agent.model.clone();
125 }
126 if agent.temperature.is_some() {
127 existing.temperature = agent.temperature;
128 }
129 if agent.top_p.is_some() {
130 existing.top_p = agent.top_p;
131 }
132 }
133 None => self.agents.push(agent.clone()),
134 }
135 }
136 }
137
138 fn merge_defaults(&mut self, other: &Config) {
139 if !other.default_agent.is_empty() {
142 self.default_agent = other.default_agent.clone();
143 }
144
145 if other.default_model.is_some() {
147 self.default_model = other.default_model.clone();
148 }
149 }
150
151 fn merge_command_configs(&mut self, other: &Config) {
152 for (cmd, cmd_cfg) in &other.command_configs {
154 self.command_configs
155 .entry(cmd.clone())
156 .and_modify(|existing| {
157 if cmd_cfg.model.is_some() {
158 existing.model = cmd_cfg.model.clone();
159 }
160 if cmd_cfg.agent.is_some() {
161 existing.agent = cmd_cfg.agent.clone();
162 }
163 if cmd_cfg.max_turns.is_some() {
164 existing.max_turns = cmd_cfg.max_turns;
165 }
166 })
167 .or_insert_with(|| cmd_cfg.clone());
168 }
169 }
170
171 fn merge_session(&mut self, other: &Config) {
172 if other.session.max_context_tokens != default_max_tokens() {
174 self.session.max_context_tokens = other.session.max_context_tokens;
175 }
176 if other.session.compaction_threshold != default_compaction_threshold() {
177 self.session.compaction_threshold = other.session.compaction_threshold;
178 }
179 if other.session.compaction_buffer_percent.is_some() {
180 self.session.compaction_buffer_percent = other.session.compaction_buffer_percent;
181 }
182 if other.session.store_path.is_some() {
183 self.session.store_path = other.session.store_path.clone();
184 }
185 if !other.session.auto_title {
189 self.session.auto_title = false;
190 }
191 if other.session.title_model.is_some() {
192 self.session.title_model = other.session.title_model.clone();
193 }
194 if other.session.tool_profile != default_tool_profile() {
195 self.session.tool_profile = other.session.tool_profile.clone();
196 }
197 if other.session.prompt_cache != default_prompt_cache() {
198 self.session.prompt_cache = other.session.prompt_cache.clone();
199 }
200 if other.session.cache_warm_minutes != 0 {
201 self.session.cache_warm_minutes = other.session.cache_warm_minutes;
202 }
203 if other.session.undo_depth != default_undo_depth() {
204 self.session.undo_depth = other.session.undo_depth;
205 }
206 if other.session.compaction_llm != default_compaction_llm() {
207 self.session.compaction_llm = other.session.compaction_llm.clone();
208 }
209 if other.session.compaction_method != default_compaction_method() {
210 self.session.compaction_method = other.session.compaction_method.clone();
211 }
212 if other.session.reasoning_effort.is_some() {
213 self.session.reasoning_effort = other.session.reasoning_effort.clone();
214 }
215 if other.session.model_fast.is_some() {
216 self.session.model_fast = other.session.model_fast.clone();
217 }
218 if other.session.goal_model.is_some() {
219 self.session.goal_model = other.session.goal_model.clone();
220 }
221 if other.session.goal_max_turns != default_goal_max_turns() {
222 self.session.goal_max_turns = other.session.goal_max_turns;
223 }
224 if other.session.model_smol.is_some() {
225 self.session.model_smol = other.session.model_smol.clone();
226 }
227 if other.session.model_plan.is_some() {
228 self.session.model_plan = other.session.model_plan.clone();
229 }
230 if other.session.prewalk.is_some() {
231 self.session.prewalk = other.session.prewalk;
232 }
233 if !other.session.stream_rules.is_empty() {
234 self.session.stream_rules = other.session.stream_rules.clone();
235 }
236 if other.session.model_race != default_model_race() {
237 self.session.model_race = other.session.model_race.clone();
238 }
239 if other.session.race_after_ms != default_race_after_ms() {
240 self.session.race_after_ms = other.session.race_after_ms;
241 }
242 if other.session.stream_retry_secs != default_stream_retry_secs() {
243 self.session.stream_retry_secs = other.session.stream_retry_secs;
244 }
245 if other.session.response_cache != default_response_cache() {
246 self.session.response_cache = other.session.response_cache.clone();
247 }
248 if !other.session.preconnect {
250 self.session.preconnect = false;
251 }
252 if other.session.intent_guidance != default_intent_guidance() {
253 self.session.intent_guidance = other.session.intent_guidance.clone();
254 }
255 if other.session.magic_keywords != MagicKeywordsConfig::default() {
256 self.session.magic_keywords = other.session.magic_keywords.clone();
257 }
258 if other.session.headless_ask.is_some() {
259 self.session.headless_ask = other.session.headless_ask;
260 }
261 if !other.session.fallback_models.is_empty() {
262 self.session.fallback_models = other.session.fallback_models.clone();
263 }
264 }
265
266 fn merge_tui(&mut self, other: &Config) {
267 if other.tui.theme.is_some() {
269 self.tui.theme = other.tui.theme.clone();
270 }
271 if other.tui.key_bindings.is_some() {
272 self.tui.key_bindings = other.tui.key_bindings.clone();
273 }
274 if other.tui.prompt_suggestions != default_prompt_suggestions() {
275 self.tui.prompt_suggestions = other.tui.prompt_suggestions.clone();
276 }
277 self.tui.show_sidebar |= other.tui.show_sidebar;
279 self.tui.skip_openrouter_key_prompt |= other.tui.skip_openrouter_key_prompt;
280 for (name, spec) in &other.tui.agent_colors {
281 self.tui.agent_colors.insert(name.clone(), spec.clone());
282 }
283 }
284
285 fn merge_general(&mut self, other: &Config) {
286 if other.general.project_path.is_some() {
288 self.general.project_path = other.general.project_path.clone();
289 }
290 if other.general.log_level.is_some() {
291 self.general.log_level = other.general.log_level.clone();
292 }
293 if other.general.default_gcp_project.is_some() {
294 self.general.default_gcp_project = other.general.default_gcp_project.clone();
295 }
296 self.general.auto_update &= other.general.auto_update;
298 if other.general.approval_mode.is_some() {
299 self.general.approval_mode = other.general.approval_mode;
300 }
301 if other.schema_version > self.schema_version {
302 self.schema_version = other.schema_version;
303 }
304 }
305
306 fn merge_servers(&mut self, other: &Config) {
307 for (name, server) in &other.mcp_servers {
309 self.mcp_servers.insert(name.clone(), server.clone());
310 }
311
312 if other.lsp.idle_timeout_ms.is_some() {
314 self.lsp.idle_timeout_ms = other.lsp.idle_timeout_ms;
315 }
316 if other.lsp.edit_diagnostics_ms.is_some() {
317 self.lsp.edit_diagnostics_ms = other.lsp.edit_diagnostics_ms;
318 }
319 for (name, server) in &other.lsp.servers {
320 self.lsp.servers.insert(name.clone(), server.clone());
321 }
322 }
323
324 fn merge_maps(&mut self, other: &Config) {
325 for (k, v) in &other.permission {
327 self.permission.insert(k.clone(), *v);
328 }
329
330 for (k, v) in &other.commands {
332 self.commands.insert(k.clone(), v.clone());
333 }
334
335 for (k, v) in &other.pricing {
337 self.pricing.insert(k.clone(), *v);
338 }
339 }
340
341 fn merge_security(&mut self, other: &Config) {
342 if other.security.bash_risk_threshold != default_risk_threshold() {
344 self.security.bash_risk_threshold = other.security.bash_risk_threshold.clone();
345 }
346 if other.security.sandbox != default_sandbox_mode() {
347 self.security.sandbox = other.security.sandbox.clone();
348 }
349 if !other.security.sandbox_network {
350 self.security.sandbox_network = false;
351 }
352 if !other.security.redact_secrets {
353 self.security.redact_secrets = false;
354 }
355 if other.security.sandbox_fallback != default_sandbox_fallback() {
356 self.security.sandbox_fallback = other.security.sandbox_fallback.clone();
357 }
358 if !other.security.network_allowlist.is_empty() {
360 self.security.network_allowlist = other.security.network_allowlist.clone();
361 }
362 if !other.security.network_denylist.is_empty() {
363 self.security.network_denylist = other.security.network_denylist.clone();
364 }
365 }
366
367 fn merge_hooks(&mut self, other: &Config) {
368 if !other.hooks.is_empty() {
370 self.hooks = other.hooks.clone();
371 }
372 }
373
374 fn merge_memory(&mut self, other: &Config) {
375 if !other.memory.enabled {
378 self.memory.enabled = false;
379 }
380 if !other.memory.auto_inject {
381 self.memory.auto_inject = false;
382 }
383 if !other.memory.auto_retain {
384 self.memory.auto_retain = false;
385 }
386 if !other.memory.retain_llm {
387 self.memory.retain_llm = false;
388 }
389 if other.memory.retain_llm_always {
390 self.memory.retain_llm_always = true;
391 }
392 if other.memory.retain_every_n != default_retain_every_n() {
393 self.memory.retain_every_n = other.memory.retain_every_n;
394 }
395 if other.memory.retain_max_facts != default_retain_max_facts() {
396 self.memory.retain_max_facts = other.memory.retain_max_facts;
397 }
398 if other.memory.max_index_lines != default_memory_index_lines() {
399 self.memory.max_index_lines = other.memory.max_index_lines;
400 }
401 if other.memory.max_index_bytes != default_memory_index_bytes() {
402 self.memory.max_index_bytes = other.memory.max_index_bytes;
403 }
404 if other.memory.recall_top_k != default_memory_top_k() {
405 self.memory.recall_top_k = other.memory.recall_top_k;
406 }
407 if (other.memory.recall_min_score - default_memory_min_score()).abs() > f32::EPSILON {
408 self.memory.recall_min_score = other.memory.recall_min_score;
409 }
410 if other.memory.recall_token_budget != default_memory_token_budget() {
411 self.memory.recall_token_budget = other.memory.recall_token_budget;
412 }
413 if other.memory.embed_dim != default_memory_embed_dim() {
414 self.memory.embed_dim = other.memory.embed_dim;
415 }
416 if other.memory.scope != default_memory_scope() {
417 self.memory.scope = other.memory.scope.clone();
418 }
419 if other.memory.embed_backend != default_memory_backend() {
420 self.memory.embed_backend = other.memory.embed_backend.clone();
421 }
422 }
423
424 fn merge_memory_index(&mut self, other: &Config) {
425 if !other.memory.code_inject {
426 self.memory.code_inject = false;
427 }
428 if other.memory.code_top_k != default_code_top_k() {
429 self.memory.code_top_k = other.memory.code_top_k;
430 }
431 if (other.memory.code_min_score - default_code_min_score()).abs() > f32::EPSILON {
432 self.memory.code_min_score = other.memory.code_min_score;
433 }
434 if !other.memory.subagent_banks {
435 self.memory.subagent_banks = false;
436 }
437 if !other.memory.auto_index {
438 self.memory.auto_index = false;
439 }
440 if other.memory.auto_index_max_files != default_auto_index_files() {
441 self.memory.auto_index_max_files = other.memory.auto_index_max_files;
442 }
443 if other.memory.auto_index_max_chunks != default_auto_index_chunks() {
444 self.memory.auto_index_max_chunks = other.memory.auto_index_max_chunks;
445 }
446 if !other.memory.session_inject {
447 self.memory.session_inject = false;
448 }
449 if other.memory.session_top_k != default_session_top_k() {
450 self.memory.session_top_k = other.memory.session_top_k;
451 }
452 if (other.memory.session_min_score - default_session_min_score()).abs() > f32::EPSILON {
453 self.memory.session_min_score = other.memory.session_min_score;
454 }
455 if !other.memory.consolidate {
456 self.memory.consolidate = false;
457 }
458 if other.memory.consolidate_max != default_consolidate_max() {
459 self.memory.consolidate_max = other.memory.consolidate_max;
460 }
461 }
462
463 fn merge_swarm(&mut self, other: &Config) {
464 if !other.swarm.enabled {
466 self.swarm.enabled = false;
467 }
468 if other.swarm.max_agents != default_swarm_max_agents() {
469 self.swarm.max_agents = other.swarm.max_agents;
470 }
471 if !other.swarm.worktrees {
472 self.swarm.worktrees = false;
473 }
474 if other.swarm.isolation.is_some() {
475 self.swarm.isolation = other.swarm.isolation.clone();
476 }
477 }
478
479 fn merge_tail(&mut self, other: &Config) {
480 if other.automation.max_background_jobs != default_max_background_jobs() {
481 self.automation.max_background_jobs = other.automation.max_background_jobs;
482 }
483
484 if other.slop != SlopConfig::default() {
485 self.slop = self.slop.merge_with(&other.slop);
486 }
487
488 self.notify = self.notify.merge_with(&other.notify);
489 self.websearch = self.websearch.merge_with(&other.websearch);
490
491 for (k, v) in &other.system_prompt_overlays.providers {
492 self.system_prompt_overlays
493 .providers
494 .insert(k.clone(), v.clone());
495 }
496 for (k, v) in &other.system_prompt_overlays.models {
497 self.system_prompt_overlays
498 .models
499 .insert(k.clone(), v.clone());
500 }
501 }
502
503 pub fn effective_permission(&self, agent: &PermissionSet) -> PermissionSet {
505 let mut out = agent.clone();
506 for (k, v) in &self.permission {
507 out.rules.entry(k.clone()).or_insert(*v);
508 }
509 for (k, v) in &agent.rules {
512 out.rules.insert(k.clone(), *v);
513 }
514 out
515 }
516}
517
518impl ToolsConfig {
521 pub(crate) fn merge_with(&self, other: &ToolsConfig) -> ToolsConfig {
522 let mut merged = self.clone();
523
524 let defaults = ToolsConfig::default();
529 if other.enable_read != defaults.enable_read {
530 merged.enable_read = other.enable_read;
531 }
532 if other.enable_write != defaults.enable_write {
533 merged.enable_write = other.enable_write;
534 }
535 if other.enable_edit != defaults.enable_edit {
536 merged.enable_edit = other.enable_edit;
537 }
538 if other.enable_glob != defaults.enable_glob {
539 merged.enable_glob = other.enable_glob;
540 }
541 if other.enable_grep != defaults.enable_grep {
542 merged.enable_grep = other.enable_grep;
543 }
544 if other.enable_shell != defaults.enable_shell {
545 merged.enable_shell = other.enable_shell;
546 }
547 if other.enable_webfetch != defaults.enable_webfetch {
548 merged.enable_webfetch = other.enable_webfetch;
549 }
550 if other.enable_websearch != defaults.enable_websearch {
551 merged.enable_websearch = other.enable_websearch;
552 }
553
554 let qdef = QuestionToolConfig::default();
555 if other.question.timeout_enabled != qdef.timeout_enabled {
556 merged.question.timeout_enabled = other.question.timeout_enabled;
557 }
558 if other.question.timeout_secs != qdef.timeout_secs {
559 merged.question.timeout_secs = other.question.timeout_secs;
560 }
561
562 let bdef = BashConfig::default();
563 if other.bash.auto_background != bdef.auto_background {
564 merged.bash.auto_background = other.bash.auto_background;
565 }
566 if other.bash.auto_background_after_secs != default_auto_background_after_secs() {
567 merged.bash.auto_background_after_secs = other.bash.auto_background_after_secs;
568 }
569 if other.format != FormatConfig::default() {
570 merged.format = other.format.clone();
571 }
572
573 if !other.disabled_tools.is_empty() {
574 merged.disabled_tools = other.disabled_tools.clone();
575 }
576 for (name, tool) in &other.custom_tools {
577 merged
578 .custom_tools
579 .entry(name.clone())
580 .or_insert_with(|| tool.clone());
581 }
582
583 merged
584 }
585}
586
587#[cfg(test)]
588mod tests {
589 use crate::types::Config;
590
591 #[test]
592 fn merge_overrides_default_agent_and_model() {
593 let base = Config::default();
594 let other = Config {
595 default_agent: "review".into(),
596 default_model: Some(whycodes_core::types::ModelConfig {
597 model_id: "m".into(),
598 provider_id: "p".into(),
599 max_tokens: Some(1),
600 context_window: None,
601 temperature: None,
602 top_p: None,
603 thinking: None,
604 supports_tools: None,
605 supports_images: None,
606 }),
607 ..Config::default()
608 };
609 let merged = base.merge_with(&other);
610 assert_eq!(merged.default_agent, "review");
611 assert_eq!(merged.default_model.as_ref().unwrap().model_id, "m");
612 }
613}