Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src-tauri/src/agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,7 @@ pub(crate) fn agent_has_connection_evidence(
)
.map_err(|e| e.to_string())?,
Some("yaml" | "yml") => serde_yaml::from_str(content).map_err(|e| e.to_string())?,
_ => json5::from_str(content).map_err(|e| e.to_string())?,
_ => super::transactions::parse_json_or_json5(content)?,
};
let provider_present = client != AgentClient::Codex
&& [
Expand Down Expand Up @@ -3587,7 +3587,8 @@ pub(crate) fn read_agent_json5_or_empty(
return Ok(serde_json::json!({}));
}
let value: serde_json::Value =
json5::from_str(&content).map_err(|error| format!("Failed to parse {label}: {error}"))?;
super::transactions::parse_json_or_json5(&content)
.map_err(|error| format!("Failed to parse {label}: {error}"))?;
if !value.is_object() {
return Err(format!("{label} root must be an object"));
}
Expand Down
11 changes: 10 additions & 1 deletion src-tauri/src/agents/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,15 @@ pub(crate) fn text(bytes: Option<&[u8]>) -> Result<Option<&str>, String> {
.transpose()
}

/// Parses JSON configuration, accepting JSON5 syntax (comments, trailing commas) as well.
/// Strict JSON is tried first: the json5 (pest) parser needs about 80x the input size in memory,
/// so a 64 MB generated Codex model catalog took more than 5 GB at every start.
pub(crate) fn parse_json_or_json5(content: &str) -> Result<Value, String> {
serde_json::from_str::<Value>(content)
.or_else(|_| json5::from_str::<Value>(content))
.map_err(|e| e.to_string())
}

pub(crate) fn parse(path: &Path, content: Option<&str>) -> Result<Value, String> {
let Some(content) = content else {
return Ok(serde_json::json!({}));
Expand All @@ -1181,7 +1190,7 @@ pub(crate) fn parse(path: &Path, content: Option<&str>) -> Result<Value, String>
.map_err(|e| e.to_string())
.and_then(|v| serde_json::to_value(v).map_err(|e| e.to_string())),
Some("yaml" | "yml") => serde_norway::from_str::<Value>(content).map_err(|e| e.to_string()),
_ => json5::from_str::<Value>(content).map_err(|e| e.to_string()),
_ => parse_json_or_json5(content),
};
let value = result.map_err(|_| {
format!(
Expand Down
21 changes: 21 additions & 0 deletions src-tauri/src/agents/transactions/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,24 @@ fn relocated_configuration_root_accepts_writes_but_nested_links_do_not() {
"a nested configuration link targeting the temp root must stay rejected"
);
}

#[test]
fn parse_json_or_json5_reads_strict_json_and_json5() {
let strict = parse_json_or_json5(r#"{"models":[{"slug":"a","n":1}]}"#).unwrap();
assert_eq!(strict["models"][0]["slug"], "a");

let json5 = parse_json_or_json5("{\n // comment\n models: [{slug: 'b',},],\n}").unwrap();
assert_eq!(json5["models"][0]["slug"], "b");

assert!(parse_json_or_json5("{\"models\":").is_err());
}

#[test]
fn parse_keeps_json5_config_files_working() {
let value = parse(
Path::new("opencode.jsonc"),
Some("{\n // keep comments working\n \"provider\": {\"cpa-gui\": {},},\n}"),
)
.unwrap();
assert!(value["provider"]["cpa-gui"].is_object());
}
Loading