Skip to content

Commit c7b177d

Browse files
authored
Merge pull request GCWing#706 from wgqqqqq/codex/fix-mode-config-null-startup
fix(config): tolerate null mode config entries
2 parents fa3deb2 + 83b5bcf commit c7b177d

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

src/crates/core/src/service/config/mode_config_canonicalizer.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ fn canonicalize_mode_config(
220220
let Some(raw_mode) = raw_mode else {
221221
return Ok(None);
222222
};
223+
if raw_mode.is_null() {
224+
return Ok(None);
225+
}
223226

224227
let mut stored: ModeConfig = serde_json::from_value(raw_mode.clone()).map_err(|error| {
225228
BitFunError::config(format!(
@@ -454,7 +457,10 @@ pub async fn canonicalize_mode_configs() -> BitFunResult<ModeConfigCanonicalizat
454457

455458
#[cfg(test)]
456459
mod tests {
457-
use super::{normalize_skill_override_lists, stored_mode_from_overrides};
460+
use super::{
461+
canonicalize_mode_config, normalize_skill_override_lists, stored_mode_from_overrides,
462+
};
463+
use serde_json::Value;
458464
use std::collections::HashSet;
459465

460466
#[test]
@@ -496,4 +502,17 @@ mod tests {
496502
);
497503
assert!(stored.disabled_user_skills.is_empty());
498504
}
505+
506+
#[test]
507+
fn canonicalize_mode_config_treats_null_as_missing() {
508+
let canonical = canonicalize_mode_config(
509+
"Claw",
510+
Some(&Value::Null),
511+
&[],
512+
&HashSet::new(),
513+
)
514+
.expect("null mode config should be ignored");
515+
516+
assert!(canonical.is_none());
517+
}
499518
}

src/crates/core/src/service/config/types.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ use async_trait::async_trait;
77
use serde::{Deserialize, Serialize};
88
use std::collections::HashMap;
99

10+
fn deserialize_mode_configs<'de, D>(deserializer: D) -> Result<HashMap<String, ModeConfig>, D::Error>
11+
where
12+
D: serde::Deserializer<'de>,
13+
{
14+
let raw = Option::<HashMap<String, Option<ModeConfig>>>::deserialize(deserializer)?;
15+
Ok(raw
16+
.unwrap_or_default()
17+
.into_iter()
18+
.filter_map(|(mode_id, config)| config.map(|config| (mode_id, config)))
19+
.collect())
20+
}
21+
1022
/// Web UI font preferences (settings → basics). Keys match `FontPreference` in the frontend (camelCase).
1123
#[derive(Debug, Clone, Serialize, Deserialize)]
1224
#[serde(rename_all = "camelCase")]
@@ -520,7 +532,7 @@ pub struct AIConfig {
520532

521533
/// Mode configuration.
522534
/// mode_id -> ModeConfig
523-
#[serde(default)]
535+
#[serde(default, deserialize_with = "deserialize_mode_configs")]
524536
pub mode_configs: HashMap<String, ModeConfig>,
525537

526538
/// SubAgent configuration (enable/disable state).
@@ -2027,6 +2039,39 @@ mod tests {
20272039
assert_eq!(config.subagent_max_concurrency, 9);
20282040
}
20292041

2042+
#[test]
2043+
fn deserializes_mode_configs_with_null_entries() {
2044+
let config: AIConfig = serde_json::from_value(serde_json::json!({
2045+
"models": [],
2046+
"agent_models": {},
2047+
"func_agent_models": {},
2048+
"default_models": {},
2049+
"mode_configs": {
2050+
"Claw": null,
2051+
"Cowork": {
2052+
"mode_id": "Cowork",
2053+
"removed_tools": ["shell"]
2054+
}
2055+
},
2056+
"subagent_configs": {},
2057+
"proxy": {
2058+
"enabled": false,
2059+
"url": ""
2060+
}
2061+
}))
2062+
.expect("config with null mode config entries should deserialize");
2063+
2064+
assert!(!config.mode_configs.contains_key("Claw"));
2065+
assert_eq!(
2066+
config
2067+
.mode_configs
2068+
.get("Cowork")
2069+
.expect("non-null mode config should be retained")
2070+
.removed_tools,
2071+
vec!["shell".to_string()]
2072+
);
2073+
}
2074+
20302075
#[test]
20312076
fn deserializes_explicit_default_review_team_config() {
20322077
let config: AIConfig = serde_json::from_value(serde_json::json!({

0 commit comments

Comments
 (0)