use crate::omo_config::types::{AgentDefinition, AgentsConfig, OhMyOpencodeConfig};
use std::collections::HashMap;
pub fn merge_agent_configs(configs: &[OhMyOpencodeConfig]) -> OhMyOpencodeConfig {
configs
.iter()
.fold(OhMyOpencodeConfig::default(), |acc, config| {
merge_two(acc, config.clone())
})
}
fn merge_two(lower: OhMyOpencodeConfig, higher: OhMyOpencodeConfig) -> OhMyOpencodeConfig {
let mut result = lower;
if higher.schema.is_some() {
result.schema = higher.schema;
}
if higher.new_task_system_enabled.is_some() {
result.new_task_system_enabled = higher.new_task_system_enabled;
}
if higher.default_run_agent.is_some() {
result.default_run_agent = higher.default_run_agent;
}
if higher.hashline_edit.is_some() {
result.hashline_edit = higher.hashline_edit;
}
if higher.model_fallback.is_some() {
result.model_fallback = higher.model_fallback;
}
if higher.agent_order.is_some() {
result.agent_order = higher.agent_order;
}
if higher.agent_definitions.is_some() {
result.agent_definitions = higher.agent_definitions;
}
if higher.disabled_mcps.is_some() {
result.disabled_mcps = higher.disabled_mcps;
}
if higher.disabled_agents.is_some() {
result.disabled_agents = higher.disabled_agents;
}
if higher.disabled_skills.is_some() {
result.disabled_skills = higher.disabled_skills;
}
if higher.disabled_hooks.is_some() {
result.disabled_hooks = higher.disabled_hooks;
}
if higher.disabled_commands.is_some() {
result.disabled_commands = higher.disabled_commands;
}
if higher.disabled_tools.is_some() {
result.disabled_tools = higher.disabled_tools;
}
if higher.disabled_providers.is_some() {
result.disabled_providers = higher.disabled_providers;
}
if higher.mcp_env_allowlist.is_some() {
result.mcp_env_allowlist = higher.mcp_env_allowlist;
}
if higher.disabled_categories.is_some() {
result.disabled_categories = higher.disabled_categories;
}
if higher.categories.is_some() {
result.categories = higher.categories;
}
if higher.background_task.is_some() {
result.background_task = higher.background_task;
}
if higher.tmux.is_some() {
result.tmux = higher.tmux;
}
if higher.experimental.is_some() {
result.experimental = higher.experimental;
}
if higher.sisyphus_agent.is_some() {
result.sisyphus_agent = higher.sisyphus_agent;
}
if higher.sisyphus.is_some() {
result.sisyphus = higher.sisyphus;
}
if higher.skills.is_some() {
result.skills = higher.skills;
}
if higher.browser_automation_engine.is_some() {
result.browser_automation_engine = higher.browser_automation_engine;
}
if higher.git_master.is_some() {
result.git_master = higher.git_master;
}
if higher.comment_checker.is_some() {
result.comment_checker = higher.comment_checker;
}
if higher.notification.is_some() {
result.notification = higher.notification;
}
if higher.lsp.is_some() {
result.lsp = higher.lsp;
}
if higher.runtime_fallback.is_some() {
result.runtime_fallback = higher.runtime_fallback;
}
if let Some(higher_agents) = higher.agents {
result.agents = Some(match result.agents {
Some(lower_agents) => merge_agents(lower_agents, higher_agents),
None => higher_agents,
});
}
for (key, value) in higher.extra {
if let (Some(lower_obj), Some(higher_obj)) = (
result.extra.get(&key).and_then(|v| v.as_object()),
value.as_object(),
) {
let mut merged = lower_obj.clone();
for (k, v) in higher_obj {
merged.insert(k.clone(), v.clone());
}
result.extra.insert(key, serde_json::Value::Object(merged));
} else {
result.extra.insert(key, value);
}
}
result
}
fn merge_agents(lower: AgentsConfig, higher: AgentsConfig) -> AgentsConfig {
AgentsConfig {
build: merge_option_agent(lower.build, higher.build),
plan: merge_option_agent(lower.plan, higher.plan),
sisyphus: merge_option_agent(lower.sisyphus, higher.sisyphus),
hephaestus: merge_option_agent(lower.hephaestus, higher.hephaestus),
prometheus: merge_option_agent(lower.prometheus, higher.prometheus),
oracle: merge_option_agent(lower.oracle, higher.oracle),
librarian: merge_option_agent(lower.librarian, higher.librarian),
explore: merge_option_agent(lower.explore, higher.explore),
multimodal_looker: merge_option_agent(lower.multimodal_looker, higher.multimodal_looker),
metis: merge_option_agent(lower.metis, higher.metis),
momus: merge_option_agent(lower.momus, higher.momus),
atlas: merge_option_agent(lower.atlas, higher.atlas),
custom: merge_agent_map(lower.custom, higher.custom),
}
}
fn merge_option_agent(
lower: Option<AgentDefinition>,
higher: Option<AgentDefinition>,
) -> Option<AgentDefinition> {
match (lower, higher) {
(Some(l), Some(h)) => Some(merge_agent_definition(l, h)),
(None, Some(h)) => Some(h),
(l, None) => l,
}
}
fn merge_agent_definition(lower: AgentDefinition, higher: AgentDefinition) -> AgentDefinition {
let mut result = lower;
macro_rules! override_if_some {
($field:ident) => {
if higher.$field.is_some() {
result.$field = higher.$field;
}
};
}
override_if_some!(model);
override_if_some!(fallback_models);
override_if_some!(variant);
override_if_some!(category);
override_if_some!(skills);
override_if_some!(temperature);
override_if_some!(top_p);
override_if_some!(prompt);
override_if_some!(prompt_append);
override_if_some!(tools);
override_if_some!(disable);
override_if_some!(description);
override_if_some!(mode);
override_if_some!(color);
override_if_some!(display_name);
override_if_some!(permission);
override_if_some!(max_tokens);
override_if_some!(thinking);
override_if_some!(reasoning_effort);
override_if_some!(text_verbosity);
override_if_some!(provider_options);
override_if_some!(ultrawork);
override_if_some!(compaction);
result
}
fn merge_agent_map(
lower: HashMap<String, AgentDefinition>,
higher: HashMap<String, AgentDefinition>,
) -> HashMap<String, AgentDefinition> {
let mut result = lower;
for (key, value) in higher {
result
.entry(key)
.and_modify(|existing| {
*existing = merge_agent_definition(existing.clone(), value.clone())
})
.or_insert(value);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::omo_config::types::{AgentDefinition, AgentMode, AgentsConfig, OhMyOpencodeConfig};
#[test]
fn test_merge_simple_fields() {
let global = OhMyOpencodeConfig {
new_task_system_enabled: Some(false),
default_run_agent: Some("build".to_string()),
..Default::default()
};
let project = OhMyOpencodeConfig {
new_task_system_enabled: Some(true),
..Default::default()
};
let merged = merge_agent_configs(&[global, project]);
assert_eq!(merged.new_task_system_enabled, Some(true));
assert_eq!(merged.default_run_agent.as_deref(), Some("build"));
}
#[test]
fn test_merge_agents_deep() {
let global = OhMyOpencodeConfig {
agents: Some(AgentsConfig {
build: Some(AgentDefinition {
model: Some("global-model".to_string()),
temperature: Some(0.5),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let project = OhMyOpencodeConfig {
agents: Some(AgentsConfig {
build: Some(AgentDefinition {
temperature: Some(0.9),
mode: Some(AgentMode::Primary),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let merged = merge_agent_configs(&[global, project]);
let build = merged.agents.unwrap().build.unwrap();
assert_eq!(build.model.as_deref(), Some("global-model")); assert_eq!(build.temperature, Some(0.9)); assert_eq!(build.mode, Some(AgentMode::Primary)); }
#[test]
fn test_merge_arrays_replace() {
let global = OhMyOpencodeConfig {
disabled_skills: Some(vec![crate::omo_config::types::DisabledSkill::Playwright]),
..Default::default()
};
let project = OhMyOpencodeConfig {
disabled_skills: Some(vec![
crate::omo_config::types::DisabledSkill::AgentBrowser,
crate::omo_config::types::DisabledSkill::DevBrowser,
]),
..Default::default()
};
let merged = merge_agent_configs(&[global, project]);
let disabled = merged.disabled_skills.unwrap();
assert_eq!(disabled.len(), 2);
assert!(matches!(
disabled[0],
crate::omo_config::types::DisabledSkill::AgentBrowser
));
}
}