mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +00:00
config: remove legacy profile write paths (#24055)
## Why [#23883](https://github.com/openai/codex/pull/23883) moved the user-facing `--profile` flag onto profile v2 and [#23886](https://github.com/openai/codex/pull/23886) removed CLI forwarding for the legacy profile-v1 path. Core and TUI config persistence still carried `active_profile` and `ConfigEditsBuilder::with_profile`, which let later writes continue targeting legacy `[profiles.<name>]` tables after profile selection moved to profile-v2 config files. ## What - Remove legacy profile routing from [`ConfigEditsBuilder`](https://github.com/openai/codex/blob/4b38e9c22e762261d7f7eef49d8a21792e241a06/codex-rs/core/src/config/edit.rs#L1064-L1294), so core config edits no longer carry `with_profile` or infer `[profiles.*]` write targets from a `profile` key. - Drop `active_profile` plumbing from runtime `Config`, TUI startup/state, app-server config override forwarding, and Windows sandbox setup persistence. - Make app-server-backed TUI config edits use unscoped model, service-tier, feature, Auto-review, plan-mode, and Windows sandbox paths through [`tui/src/config_update.rs`](https://github.com/openai/codex/blob/4b38e9c22e762261d7f7eef49d8a21792e241a06/codex-rs/tui/src/config_update.rs#L43-L112). - Update config edit coverage so legacy `profile` state stays untouched by direct model writes, and remove tests whose only contract was the deleted profile-scoped persistence path. ## Testing - Not run locally.
This commit is contained in:
1 parent
fd72e99384
commit
2c6605ab35
18 files changed
+107
-974
No files matched your search
@@ -4798,7 +4798,6 @@ model = "gpt-project-local"
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.active_profile, None);
|
||||
assert_eq!(config.model, None);
|
||||
assert!(
|
||||
config.startup_warnings.iter().any(|warning| {
|
||||
@@ -5032,7 +5031,6 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -5063,7 +5061,6 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
|
||||
let empty = BTreeMap::new();
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(empty.clone())],
|
||||
)?;
|
||||
let loaded = load_global_mcp_servers(codex_home.path()).await?;
|
||||
@@ -5379,7 +5376,6 @@ async fn replace_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -5456,7 +5452,6 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -5518,7 +5513,6 @@ async fn replace_mcp_servers_serializes_sourced_env_vars() -> anyhow::Result<()>
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -5570,7 +5564,6 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -5625,7 +5618,6 @@ async fn replace_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -5695,7 +5687,6 @@ async fn replace_mcp_servers_streamable_http_serializes_custom_headers() -> anyh
|
||||
)]);
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -5779,7 +5770,6 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
let serialized_with_optional = std::fs::read_to_string(&config_path)?;
|
||||
@@ -5814,7 +5804,6 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
);
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -5913,7 +5902,6 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -6001,7 +5989,6 @@ async fn replace_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -6052,7 +6039,6 @@ async fn replace_mcp_servers_serializes_required_flag() -> anyhow::Result<()> {
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -6103,7 +6089,6 @@ async fn replace_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -6160,7 +6145,6 @@ async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyh
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
@@ -6283,196 +6267,6 @@ model = "gpt-4.1"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_model_updates_profile() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
ConfigEditsBuilder::new(codex_home.path())
|
||||
.with_profile(Some("dev"))
|
||||
.set_model(Some("gpt-5.4"), Some(ReasoningEffort::Medium))
|
||||
.apply()
|
||||
.await?;
|
||||
|
||||
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
|
||||
let parsed: ConfigToml = toml::from_str(&serialized)?;
|
||||
let profile = parsed
|
||||
.profiles
|
||||
.get("dev")
|
||||
.expect("profile should be created");
|
||||
|
||||
assert_eq!(profile.model.as_deref(), Some("gpt-5.4"));
|
||||
assert_eq!(
|
||||
profile.model_reasoning_effort,
|
||||
Some(ReasoningEffort::Medium)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_model_updates_existing_profile() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
|
||||
|
||||
tokio::fs::write(
|
||||
&config_path,
|
||||
r#"
|
||||
[profiles.dev]
|
||||
model = "gpt-4"
|
||||
model_reasoning_effort = "medium"
|
||||
|
||||
[profiles.prod]
|
||||
model = "gpt-5.4"
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
ConfigEditsBuilder::new(codex_home.path())
|
||||
.with_profile(Some("dev"))
|
||||
.set_model(Some("o4-high"), Some(ReasoningEffort::Medium))
|
||||
.apply()
|
||||
.await?;
|
||||
|
||||
let serialized = tokio::fs::read_to_string(config_path).await?;
|
||||
let parsed: ConfigToml = toml::from_str(&serialized)?;
|
||||
|
||||
let dev_profile = parsed
|
||||
.profiles
|
||||
.get("dev")
|
||||
.expect("dev profile should survive updates");
|
||||
assert_eq!(dev_profile.model.as_deref(), Some("o4-high"));
|
||||
assert_eq!(
|
||||
dev_profile.model_reasoning_effort,
|
||||
Some(ReasoningEffort::Medium)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parsed
|
||||
.profiles
|
||||
.get("prod")
|
||||
.and_then(|profile| profile.model.as_deref()),
|
||||
Some("gpt-5.4"),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_feature_enabled_updates_profile() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
ConfigEditsBuilder::new(codex_home.path())
|
||||
.with_profile(Some("dev"))
|
||||
.set_feature_enabled("guardian_approval", /*enabled*/ true)
|
||||
.apply()
|
||||
.await?;
|
||||
|
||||
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
|
||||
let parsed: ConfigToml = toml::from_str(&serialized)?;
|
||||
let profile = parsed
|
||||
.profiles
|
||||
.get("dev")
|
||||
.expect("profile should be created");
|
||||
|
||||
assert_eq!(
|
||||
profile
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
Some(true),
|
||||
);
|
||||
assert_eq!(
|
||||
parsed
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
None,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_feature_enabled_persists_feature_disable_in_profile() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
ConfigEditsBuilder::new(codex_home.path())
|
||||
.with_profile(Some("dev"))
|
||||
.set_feature_enabled("guardian_approval", /*enabled*/ true)
|
||||
.apply()
|
||||
.await?;
|
||||
|
||||
ConfigEditsBuilder::new(codex_home.path())
|
||||
.with_profile(Some("dev"))
|
||||
.set_feature_enabled("guardian_approval", /*enabled*/ false)
|
||||
.apply()
|
||||
.await?;
|
||||
|
||||
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
|
||||
let parsed: ConfigToml = toml::from_str(&serialized)?;
|
||||
let profile = parsed
|
||||
.profiles
|
||||
.get("dev")
|
||||
.expect("profile should be created");
|
||||
|
||||
assert_eq!(
|
||||
profile
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
Some(false),
|
||||
);
|
||||
assert_eq!(
|
||||
parsed
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
None,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_feature_enabled_profile_disable_overrides_root_enable() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
ConfigEditsBuilder::new(codex_home.path())
|
||||
.set_feature_enabled("guardian_approval", /*enabled*/ true)
|
||||
.apply()
|
||||
.await?;
|
||||
|
||||
ConfigEditsBuilder::new(codex_home.path())
|
||||
.with_profile(Some("dev"))
|
||||
.set_feature_enabled("guardian_approval", /*enabled*/ false)
|
||||
.apply()
|
||||
.await?;
|
||||
|
||||
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
|
||||
let parsed: ConfigToml = toml::from_str(&serialized)?;
|
||||
let profile = parsed
|
||||
.profiles
|
||||
.get("dev")
|
||||
.expect("profile should be created");
|
||||
|
||||
assert_eq!(
|
||||
parsed
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
Some(true),
|
||||
);
|
||||
assert_eq!(
|
||||
profile
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|features| features.entries().get("guardian_approval").copied()),
|
||||
Some(false),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct PrecedenceTestFixture {
|
||||
cwd: TempDir,
|
||||
codex_home: TempDir,
|
||||
|
||||
@@ -513,13 +513,6 @@ mod document_helpers {
|
||||
|
||||
struct ConfigDocument {
|
||||
doc: DocumentMut,
|
||||
profile: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Scope {
|
||||
Global,
|
||||
Profile,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -529,25 +522,25 @@ enum TraversalMode {
|
||||
}
|
||||
|
||||
impl ConfigDocument {
|
||||
fn new(doc: DocumentMut, profile: Option<String>) -> Self {
|
||||
Self { doc, profile }
|
||||
fn new(doc: DocumentMut) -> Self {
|
||||
Self { doc }
|
||||
}
|
||||
|
||||
fn apply(&mut self, edit: &ConfigEdit) -> anyhow::Result<bool> {
|
||||
match edit {
|
||||
ConfigEdit::SetModel { model, effort } => Ok({
|
||||
let mut mutated = false;
|
||||
mutated |= self.write_profile_value(
|
||||
mutated |= self.write_optional_value(
|
||||
&["model"],
|
||||
model.as_ref().map(|model_value| value(model_value.clone())),
|
||||
);
|
||||
mutated |= self.write_profile_value(
|
||||
mutated |= self.write_optional_value(
|
||||
&["model_reasoning_effort"],
|
||||
effort.map(|effort| value(effort.to_string())),
|
||||
);
|
||||
mutated
|
||||
}),
|
||||
ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_profile_value(
|
||||
ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_optional_value(
|
||||
&["service_tier"],
|
||||
service_tier.as_ref().map(|service_tier| {
|
||||
// Keep the legacy config spelling stable. Runtime values use
|
||||
@@ -560,35 +553,30 @@ impl ConfigDocument {
|
||||
value(config_value)
|
||||
}),
|
||||
)),
|
||||
ConfigEdit::SetModelPersonality { personality } => Ok(self.write_profile_value(
|
||||
ConfigEdit::SetModelPersonality { personality } => Ok(self.write_optional_value(
|
||||
&["personality"],
|
||||
personality.map(|personality| value(personality.to_string())),
|
||||
)),
|
||||
ConfigEdit::SetNoticeHideFullAccessWarning(acknowledged) => Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[NOTICE_TABLE_KEY, "hide_full_access_warning"],
|
||||
value(*acknowledged),
|
||||
)),
|
||||
ConfigEdit::SetNoticeHideWorldWritableWarning(acknowledged) => Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[NOTICE_TABLE_KEY, "hide_world_writable_warning"],
|
||||
value(*acknowledged),
|
||||
)),
|
||||
ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged) => Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[NOTICE_TABLE_KEY, "hide_rate_limit_model_nudge"],
|
||||
value(*acknowledged),
|
||||
)),
|
||||
ConfigEdit::SetNoticeHideModelMigrationPrompt(migration_config, acknowledged) => {
|
||||
Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[NOTICE_TABLE_KEY, migration_config.as_str()],
|
||||
value(*acknowledged),
|
||||
))
|
||||
}
|
||||
ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome(acknowledged) => Ok(self
|
||||
.write_value(
|
||||
Scope::Global,
|
||||
&[
|
||||
NOTICE_TABLE_KEY,
|
||||
"external_config_migration_prompts",
|
||||
@@ -598,7 +586,6 @@ impl ConfigDocument {
|
||||
)),
|
||||
ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(timestamp) => {
|
||||
Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[
|
||||
NOTICE_TABLE_KEY,
|
||||
"external_config_migration_prompts",
|
||||
@@ -611,7 +598,6 @@ impl ConfigDocument {
|
||||
project,
|
||||
acknowledged,
|
||||
) => Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[
|
||||
NOTICE_TABLE_KEY,
|
||||
"external_config_migration_prompts",
|
||||
@@ -624,7 +610,6 @@ impl ConfigDocument {
|
||||
project,
|
||||
timestamp,
|
||||
) => Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[
|
||||
NOTICE_TABLE_KEY,
|
||||
"external_config_migration_prompts",
|
||||
@@ -634,7 +619,6 @@ impl ConfigDocument {
|
||||
value(*timestamp),
|
||||
)),
|
||||
ConfigEdit::RecordModelMigrationSeen { from, to } => Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[NOTICE_TABLE_KEY, "model_migrations", from.as_str()],
|
||||
value(to.clone()),
|
||||
)),
|
||||
@@ -663,20 +647,26 @@ impl ConfigDocument {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_profile_value(&mut self, segments: &[&str], value: Option<TomlItem>) -> bool {
|
||||
fn write_optional_value(&mut self, segments: &[&str], value: Option<TomlItem>) -> bool {
|
||||
match value {
|
||||
Some(item) => self.write_value(Scope::Profile, segments, item),
|
||||
None => self.clear(Scope::Profile, segments),
|
||||
Some(item) => self.write_value(segments, item),
|
||||
None => self.clear(segments),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_value(&mut self, scope: Scope, segments: &[&str], value: TomlItem) -> bool {
|
||||
let resolved = self.scoped_segments(scope, segments);
|
||||
fn write_value(&mut self, segments: &[&str], value: TomlItem) -> bool {
|
||||
let resolved = segments
|
||||
.iter()
|
||||
.map(|segment| (*segment).to_string())
|
||||
.collect::<Vec<_>>();
|
||||
self.insert(&resolved, value)
|
||||
}
|
||||
|
||||
fn clear(&mut self, scope: Scope, segments: &[&str]) -> bool {
|
||||
let resolved = self.scoped_segments(scope, segments);
|
||||
fn clear(&mut self, segments: &[&str]) -> bool {
|
||||
let resolved = segments
|
||||
.iter()
|
||||
.map(|segment| (*segment).to_string())
|
||||
.collect::<Vec<_>>();
|
||||
self.remove(&resolved)
|
||||
}
|
||||
|
||||
@@ -709,7 +699,6 @@ impl ConfigDocument {
|
||||
.filter(|disabled_tool| seen.insert(disabled_tool.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
self.write_value(
|
||||
Scope::Global,
|
||||
&["tool_suggest", "disabled_tools"],
|
||||
document_helpers::tool_suggest_disabled_tools_value(&disabled_tools),
|
||||
)
|
||||
@@ -721,7 +710,7 @@ impl ConfigDocument {
|
||||
|
||||
fn replace_mcp_servers(&mut self, servers: &BTreeMap<String, McpServerConfig>) -> bool {
|
||||
if servers.is_empty() {
|
||||
return self.clear(Scope::Global, &["mcp_servers"]);
|
||||
return self.clear(&["mcp_servers"]);
|
||||
}
|
||||
|
||||
let root = self.doc.as_table_mut();
|
||||
@@ -883,26 +872,6 @@ impl ConfigDocument {
|
||||
mutated
|
||||
}
|
||||
|
||||
fn scoped_segments(&self, scope: Scope, segments: &[&str]) -> Vec<String> {
|
||||
let resolved: Vec<String> = segments
|
||||
.iter()
|
||||
.map(|segment| (*segment).to_string())
|
||||
.collect();
|
||||
|
||||
if matches!(scope, Scope::Profile)
|
||||
&& resolved.first().is_none_or(|segment| segment != "profiles")
|
||||
&& let Some(profile) = self.profile.as_deref()
|
||||
{
|
||||
let mut scoped = Vec::with_capacity(resolved.len() + 2);
|
||||
scoped.push("profiles".to_string());
|
||||
scoped.push(profile.to_string());
|
||||
scoped.extend(resolved);
|
||||
return scoped;
|
||||
}
|
||||
|
||||
resolved
|
||||
}
|
||||
|
||||
fn insert(&mut self, segments: &[String], value: TomlItem) -> bool {
|
||||
let Some((last, parents)) = segments.split_last() else {
|
||||
return false;
|
||||
@@ -1030,18 +999,13 @@ fn write_skill_config_selector(table: &mut TomlTable, selector: &SkillConfigSele
|
||||
}
|
||||
|
||||
/// Persist edits using a blocking strategy.
|
||||
pub fn apply_blocking(
|
||||
codex_home: &Path,
|
||||
profile: Option<&str>,
|
||||
edits: &[ConfigEdit],
|
||||
) -> anyhow::Result<()> {
|
||||
pub fn apply_blocking(codex_home: &Path, edits: &[ConfigEdit]) -> anyhow::Result<()> {
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
apply_blocking_to_resolved_file(&config_path, profile, edits)
|
||||
apply_blocking_to_resolved_file(&config_path, edits)
|
||||
}
|
||||
|
||||
fn apply_blocking_to_resolved_file(
|
||||
resolved_config_file: &Path,
|
||||
legacy_profile: Option<&str>,
|
||||
edits: &[ConfigEdit],
|
||||
) -> anyhow::Result<()> {
|
||||
if edits.is_empty() {
|
||||
@@ -1064,13 +1028,7 @@ fn apply_blocking_to_resolved_file(
|
||||
serialized.parse::<DocumentMut>()?
|
||||
};
|
||||
|
||||
let profile = legacy_profile.map(ToOwned::to_owned).or_else(|| {
|
||||
doc.get("profile")
|
||||
.and_then(|item| item.as_str())
|
||||
.map(ToOwned::to_owned)
|
||||
});
|
||||
|
||||
let mut document = ConfigDocument::new(doc, profile);
|
||||
let mut document = ConfigDocument::new(doc);
|
||||
let mut mutated = false;
|
||||
|
||||
for edit in edits {
|
||||
@@ -1093,29 +1051,18 @@ fn apply_blocking_to_resolved_file(
|
||||
|
||||
/// Persist edits asynchronously by offloading the blocking writer.
|
||||
///
|
||||
/// `profile` selects a legacy `[profiles.<name>]` section inside
|
||||
/// `$CODEX_HOME/config.toml`; profile-v2 callers should resolve their target
|
||||
/// file before constructing a [ConfigEditsBuilder].
|
||||
pub async fn apply(
|
||||
codex_home: &Path,
|
||||
profile: Option<&str>,
|
||||
edits: Vec<ConfigEdit>,
|
||||
) -> anyhow::Result<()> {
|
||||
pub async fn apply(codex_home: &Path, edits: Vec<ConfigEdit>) -> anyhow::Result<()> {
|
||||
let codex_home = codex_home.to_path_buf();
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let profile = profile.map(ToOwned::to_owned);
|
||||
task::spawn_blocking(move || {
|
||||
apply_blocking_to_resolved_file(&config_path, profile.as_deref(), &edits)
|
||||
})
|
||||
.await
|
||||
.context("config persistence task panicked")?
|
||||
task::spawn_blocking(move || apply_blocking_to_resolved_file(&config_path, &edits))
|
||||
.await
|
||||
.context("config persistence task panicked")?
|
||||
}
|
||||
|
||||
/// Fluent builder to batch config edits and apply them atomically.
|
||||
#[derive(Default)]
|
||||
pub struct ConfigEditsBuilder {
|
||||
config_path: PathBuf,
|
||||
profile: Option<String>,
|
||||
edits: Vec<ConfigEdit>,
|
||||
}
|
||||
|
||||
@@ -1136,16 +1083,10 @@ impl ConfigEditsBuilder {
|
||||
pub fn for_config_path(config_path: &Path) -> Self {
|
||||
Self {
|
||||
config_path: config_path.to_path_buf(),
|
||||
profile: None,
|
||||
edits: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_profile(mut self, profile: Option<&str>) -> Self {
|
||||
self.profile = profile.map(ToOwned::to_owned);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_model(mut self, model: Option<&str>, effort: Option<ReasoningEffort>) -> Self {
|
||||
self.edits.push(ConfigEdit::SetModel {
|
||||
model: model.map(ToOwned::to_owned),
|
||||
@@ -1248,27 +1189,16 @@ impl ConfigEditsBuilder {
|
||||
|
||||
/// Enable or disable a feature flag by key under the `[features]` table.
|
||||
///
|
||||
/// Disabling a default-false feature clears the root-scoped key instead of
|
||||
/// Disabling a default-false feature clears the key instead of
|
||||
/// persisting `false`, so the config does not pin the feature once it
|
||||
/// graduates to globally enabled. Profile-scoped disables still persist
|
||||
/// `false` so they can override an inherited root enable.
|
||||
/// graduates to globally enabled.
|
||||
pub fn set_feature_enabled(mut self, key: &str, enabled: bool) -> Self {
|
||||
let profile_scoped = self.profile.is_some();
|
||||
let segments = if let Some(profile) = self.profile.as_ref() {
|
||||
vec![
|
||||
"profiles".to_string(),
|
||||
profile.clone(),
|
||||
"features".to_string(),
|
||||
key.to_string(),
|
||||
]
|
||||
} else {
|
||||
vec!["features".to_string(), key.to_string()]
|
||||
};
|
||||
let segments = vec!["features".to_string(), key.to_string()];
|
||||
let is_default_false_feature = FEATURES
|
||||
.iter()
|
||||
.find(|spec| spec.key == key)
|
||||
.is_some_and(|spec| !spec.default_enabled);
|
||||
if enabled || profile_scoped || !is_default_false_feature {
|
||||
if enabled || !is_default_false_feature {
|
||||
self.edits.push(ConfigEdit::SetPath {
|
||||
segments,
|
||||
value: value(enabled),
|
||||
@@ -1280,18 +1210,8 @@ impl ConfigEditsBuilder {
|
||||
}
|
||||
|
||||
pub fn set_windows_sandbox_mode(mut self, mode: &str) -> Self {
|
||||
let segments = if let Some(profile) = self.profile.as_ref() {
|
||||
vec![
|
||||
"profiles".to_string(),
|
||||
profile.clone(),
|
||||
"windows".to_string(),
|
||||
"sandbox".to_string(),
|
||||
]
|
||||
} else {
|
||||
vec!["windows".to_string(), "sandbox".to_string()]
|
||||
};
|
||||
self.edits.push(ConfigEdit::SetPath {
|
||||
segments,
|
||||
segments: vec!["windows".to_string(), "sandbox".to_string()],
|
||||
value: value(mode),
|
||||
});
|
||||
self
|
||||
@@ -1339,34 +1259,15 @@ impl ConfigEditsBuilder {
|
||||
"elevated_windows_sandbox",
|
||||
"enable_experimental_windows_sandbox",
|
||||
] {
|
||||
let mut segments = vec!["features".to_string(), key.to_string()];
|
||||
if let Some(profile) = self.profile.as_ref() {
|
||||
segments = vec![
|
||||
"profiles".to_string(),
|
||||
profile.clone(),
|
||||
"features".to_string(),
|
||||
key.to_string(),
|
||||
];
|
||||
}
|
||||
let segments = vec!["features".to_string(), key.to_string()];
|
||||
self.edits.push(ConfigEdit::ClearPath { segments });
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_session_picker_view(mut self, mode: SessionPickerViewMode) -> Self {
|
||||
let segments = if let Some(profile) = self.profile.as_ref() {
|
||||
vec![
|
||||
"profiles".to_string(),
|
||||
profile.clone(),
|
||||
"tui".to_string(),
|
||||
"session_picker_view".to_string(),
|
||||
]
|
||||
} else {
|
||||
vec!["tui".to_string(), "session_picker_view".to_string()]
|
||||
};
|
||||
|
||||
self.edits.push(ConfigEdit::SetPath {
|
||||
segments,
|
||||
segments: vec!["tui".to_string(), "session_picker_view".to_string()],
|
||||
value: value(mode.to_string()),
|
||||
});
|
||||
self
|
||||
@@ -1382,13 +1283,13 @@ impl ConfigEditsBuilder {
|
||||
|
||||
/// Apply edits on a blocking thread.
|
||||
pub fn apply_blocking(self) -> anyhow::Result<()> {
|
||||
apply_blocking_to_resolved_file(&self.config_path, self.profile.as_deref(), &self.edits)
|
||||
apply_blocking_to_resolved_file(&self.config_path, &self.edits)
|
||||
}
|
||||
|
||||
/// Apply edits asynchronously via a blocking offload.
|
||||
pub async fn apply(self) -> anyhow::Result<()> {
|
||||
task::spawn_blocking(move || {
|
||||
apply_blocking_to_resolved_file(&self.config_path, self.profile.as_deref(), &self.edits)
|
||||
apply_blocking_to_resolved_file(&self.config_path, &self.edits)
|
||||
})
|
||||
.await
|
||||
.context("config persistence task panicked")?
|
||||
|
||||
@@ -20,7 +20,6 @@ fn blocking_set_model_top_level() {
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetModel {
|
||||
model: Some("gpt-5.4".to_string()),
|
||||
effort: Some(ReasoningEffort::High),
|
||||
@@ -111,24 +110,6 @@ session_picker_view = "dense"
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_picker_view_builder_respects_active_profile() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_profile(Some("work"))
|
||||
.set_session_picker_view(SessionPickerViewMode::Dense)
|
||||
.apply_blocking()
|
||||
.expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[profiles.work.tui]
|
||||
session_picker_view = "dense"
|
||||
"#;
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_binding_edit_writes_root_action_binding() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
@@ -380,7 +361,7 @@ enabled = false
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_set_model_preserves_inline_table_contents() {
|
||||
fn blocking_set_model_ignores_inline_legacy_profile_contents() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
@@ -396,7 +377,6 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetModel {
|
||||
model: Some("o4-mini".to_string()),
|
||||
effort: None,
|
||||
@@ -407,7 +387,12 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
|
||||
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let value: TomlValue = toml::from_str(&raw).expect("parse config");
|
||||
|
||||
// Ensure sandbox_mode is preserved under profiles.fast and model updated.
|
||||
assert_eq!(
|
||||
value.get("model").and_then(TomlValue::as_str),
|
||||
Some("o4-mini")
|
||||
);
|
||||
|
||||
// Legacy profile values stay untouched when root settings are updated.
|
||||
let profiles_tbl = value
|
||||
.get("profiles")
|
||||
.and_then(|v| v.as_table())
|
||||
@@ -422,7 +407,7 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
|
||||
);
|
||||
assert_eq!(
|
||||
fast_tbl.get("model").and_then(|v| v.as_str()),
|
||||
Some("o4-mini")
|
||||
Some("gpt-4o")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -441,7 +426,6 @@ fn blocking_set_model_writes_through_symlink_chain() {
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetModel {
|
||||
model: Some("gpt-5.4".to_string()),
|
||||
effort: Some(ReasoningEffort::High),
|
||||
@@ -474,7 +458,6 @@ fn blocking_set_model_replaces_symlink_on_cycle() {
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetModel {
|
||||
model: Some("gpt-5.4".to_string()),
|
||||
effort: None,
|
||||
@@ -513,7 +496,6 @@ network_access = false
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[
|
||||
ConfigEdit::SetPath {
|
||||
segments: vec![
|
||||
@@ -553,7 +535,7 @@ network_access = true
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_clear_model_removes_inline_table_entry() {
|
||||
fn blocking_clear_model_does_not_follow_legacy_active_profile() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
@@ -568,7 +550,6 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetModel {
|
||||
model: None,
|
||||
effort: Some(ReasoningEffort::High),
|
||||
@@ -579,15 +560,14 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"profile = "fast"
|
||||
|
||||
[profiles.fast]
|
||||
sandbox_mode = "strict"
|
||||
profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
|
||||
model_reasoning_effort = "high"
|
||||
"#;
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_set_model_scopes_to_active_profile() {
|
||||
fn blocking_set_model_does_not_follow_legacy_active_profile() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
std::fs::write(
|
||||
@@ -602,7 +582,6 @@ model_reasoning_effort = "low"
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetModel {
|
||||
model: Some("o5-preview".to_string()),
|
||||
effort: Some(ReasoningEffort::Minimal),
|
||||
@@ -612,39 +591,11 @@ model_reasoning_effort = "low"
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"profile = "team"
|
||||
model = "o5-preview"
|
||||
model_reasoning_effort = "minimal"
|
||||
|
||||
[profiles.team]
|
||||
model_reasoning_effort = "minimal"
|
||||
model = "o5-preview"
|
||||
"#;
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_set_model_with_explicit_profile() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
std::fs::write(
|
||||
codex_home.join(CONFIG_TOML_FILE),
|
||||
r#"[profiles."team a"]
|
||||
model = "gpt-5.4"
|
||||
"#,
|
||||
)
|
||||
.expect("seed");
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
Some("team a"),
|
||||
&[ConfigEdit::SetModel {
|
||||
model: Some("o4-mini".to_string()),
|
||||
effort: None,
|
||||
}],
|
||||
)
|
||||
.expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[profiles."team a"]
|
||||
model = "o4-mini"
|
||||
model_reasoning_effort = "low"
|
||||
"#;
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
@@ -666,7 +617,6 @@ existing = "value"
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetNoticeHideFullAccessWarning(true)],
|
||||
)
|
||||
.expect("persist");
|
||||
@@ -696,7 +646,6 @@ existing = "value"
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetNoticeHideRateLimitModelNudge(true)],
|
||||
)
|
||||
.expect("persist");
|
||||
@@ -722,7 +671,6 @@ existing = "value"
|
||||
.expect("seed");
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetNoticeHideModelMigrationPrompt(
|
||||
"hide_gpt5_1_migration_prompt".to_string(),
|
||||
true,
|
||||
@@ -751,7 +699,6 @@ existing = "value"
|
||||
.expect("seed");
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetNoticeHideModelMigrationPrompt(
|
||||
"hide_gpt-5.1-codex-max_migration_prompt".to_string(),
|
||||
true,
|
||||
@@ -780,7 +727,6 @@ existing = "value"
|
||||
.expect("seed");
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::RecordModelMigrationSeen {
|
||||
from: "gpt-5.2".to_string(),
|
||||
to: "gpt-5.4".to_string(),
|
||||
@@ -811,7 +757,6 @@ existing = "value"
|
||||
.expect("seed");
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome(
|
||||
true,
|
||||
)],
|
||||
@@ -841,7 +786,6 @@ existing = "value"
|
||||
.expect("seed");
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[
|
||||
ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject(
|
||||
"/Users/alexsong/code/skills".to_string(),
|
||||
@@ -874,7 +818,6 @@ existing = "value"
|
||||
.expect("seed");
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(1_760_000_000)],
|
||||
)
|
||||
.expect("persist");
|
||||
@@ -902,7 +845,6 @@ existing = "value"
|
||||
.expect("seed");
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[
|
||||
ConfigEdit::SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt(
|
||||
"/Users/alexsong/code/skills".to_string(),
|
||||
@@ -996,7 +938,6 @@ fn blocking_replace_mcp_servers_round_trips() {
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)
|
||||
.expect("persist");
|
||||
@@ -1069,12 +1010,7 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
|
||||
},
|
||||
);
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers)],
|
||||
)
|
||||
.expect("persist");
|
||||
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
|
||||
|
||||
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = "\
|
||||
@@ -1129,12 +1065,7 @@ foo = { command = "cmd" }
|
||||
},
|
||||
);
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers)],
|
||||
)
|
||||
.expect("persist");
|
||||
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[mcp_servers]
|
||||
@@ -1184,12 +1115,7 @@ foo = { command = "cmd" } # keep me
|
||||
},
|
||||
);
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers)],
|
||||
)
|
||||
.expect("persist");
|
||||
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[mcp_servers]
|
||||
@@ -1238,12 +1164,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
|
||||
},
|
||||
);
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers)],
|
||||
)
|
||||
.expect("persist");
|
||||
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[mcp_servers]
|
||||
@@ -1293,12 +1214,7 @@ foo = { command = "cmd" }
|
||||
},
|
||||
);
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers)],
|
||||
)
|
||||
.expect("persist");
|
||||
apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[mcp_servers]
|
||||
@@ -1315,7 +1231,6 @@ fn blocking_clear_path_noop_when_missing() {
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ClearPath {
|
||||
segments: vec!["missing".to_string()],
|
||||
}],
|
||||
@@ -1336,7 +1251,6 @@ fn blocking_set_path_updates_notifications() {
|
||||
let item = value(false);
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::SetPath {
|
||||
segments: vec!["tui".to_string(), "notifications".to_string()],
|
||||
value: item,
|
||||
@@ -1518,7 +1432,6 @@ fn replace_mcp_servers_blocking_clears_table_when_empty() {
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(BTreeMap::new())],
|
||||
)
|
||||
.expect("persist");
|
||||
|
||||
@@ -958,9 +958,6 @@ pub struct Config {
|
||||
/// When `true`, suppress warnings about unstable (under development) features.
|
||||
pub suppress_unstable_features_warning: bool,
|
||||
|
||||
/// The active profile name used to derive this `Config` (if any).
|
||||
pub active_profile: Option<String>,
|
||||
|
||||
/// The currently active project config, resolved by checking if cwd:
|
||||
/// is (1) part of a git repo, (2) a git worktree, or (3) just using the cwd
|
||||
pub active_project: ProjectConfig,
|
||||
@@ -3499,7 +3496,6 @@ impl Config {
|
||||
suppress_unstable_features_warning: cfg
|
||||
.suppress_unstable_features_warning
|
||||
.unwrap_or(false),
|
||||
active_profile: None,
|
||||
active_project,
|
||||
notices,
|
||||
check_for_update_on_startup,
|
||||
|
||||
Reference in new issue
Block a user