Codex Handbook
tui/src/app/config_persistence.rs 1383 lines
//! Runtime configuration persistence helpers for the TUI app.//!//! This module owns the app-level glue between config.toml edits, in-memory `Config` refreshes,//! and the ChatWidget copy of session settings, keeping persistence-heavy code out of the main app//! loop.use super::*;#[cfg(target_os = "windows")]use codex_utils_approval_presets::ApprovalPreset;#[cfg(target_os = "windows")]pub(super) struct WindowsSetupPermissions {    pub(super) permission_profile: PermissionProfile,    pub(super) workspace_roots: Vec<AbsolutePathBuf>,}async fn build_config_on_runtime_worker(    builder: ConfigBuilder,    error_context: String,) -> Result<Config> {    match tokio::spawn(async move { builder.build().await }).await {        Ok(build_result) => build_result.wrap_err(error_context),        Err(err) if err.is_panic() => std::panic::resume_unwind(err.into_panic()),        Err(err) => Err(err).wrap_err_with(|| format!("{error_context} task failed")),    }}impl App {    pub(super) async fn rebuild_config_for_cwd(&self, cwd: PathBuf) -> Result<Config> {        let mut overrides = self.harness_overrides.clone();        overrides.cwd = Some(cwd.clone());        let cwd_display = cwd.display().to_string();        let builder = ConfigBuilder::default()            .codex_home(self.config.codex_home.to_path_buf())            .cli_overrides(self.cli_kv_overrides.clone())            .harness_overrides(overrides)            .loader_overrides(self.loader_overrides.clone())            .cloud_config_bundle(self.cloud_config_bundle.clone());        build_config_on_runtime_worker(            builder,            format!("Failed to rebuild config for cwd {cwd_display}"),        )        .await    }    pub(super) async fn rebuild_config_for_permission_profile(        &self,        profile_id: &str,    ) -> Result<Config> {        let mut overrides = self.harness_overrides.clone();        overrides.cwd = Some(self.chat_widget.config_ref().cwd.to_path_buf());        overrides.sandbox_mode = None;        overrides.permission_profile = None;        overrides.default_permissions = Some(profile_id.to_string());        let builder = ConfigBuilder::default()            .codex_home(self.config.codex_home.to_path_buf())            .cli_overrides(self.cli_kv_overrides.clone())            .harness_overrides(overrides)            .loader_overrides(self.loader_overrides.clone())            .cloud_config_bundle(self.cloud_config_bundle.clone());        build_config_on_runtime_worker(            builder,            format!("Failed to rebuild config for permission profile {profile_id}"),        )        .await    }    #[cfg(target_os = "windows")]    pub(super) async fn windows_setup_permissions(        &self,        preset: &ApprovalPreset,        profile_selection: Option<&PermissionProfileSelection>,    ) -> Result<WindowsSetupPermissions> {        match profile_selection {            Some(selection) => {                let selected_config = self                    .rebuild_config_for_permission_profile(selection.profile_id.as_str())                    .await?;                Ok(WindowsSetupPermissions {                    permission_profile: selected_config.permissions.permission_profile().clone(),                    workspace_roots: selected_config.effective_workspace_roots(),                })            }            None => Ok(WindowsSetupPermissions {                permission_profile: preset.permission_profile.clone(),                workspace_roots: self.config.effective_workspace_roots(),            }),        }    }    pub(super) async fn apply_permission_profile_selection(        &mut self,        selection: PermissionProfileSelection,    ) -> bool {        let PermissionProfileSelection {            profile_id,            approval_policy,            approvals_reviewer,            display_label,        } = selection;        let selected_config = match self            .rebuild_config_for_permission_profile(profile_id.as_str())            .await        {            Ok(config) => config,            Err(err) => {                tracing::warn!(                    error = %err,                    profile_id,                    "failed to resolve selected permission profile"                );                self.chat_widget.add_error_message(format!(                    "Failed to set permission profile `{profile_id}`: {err}"                ));                return false;            }        };        let permission_profile = selected_config.permissions.permission_profile();        let active_permission_profile = selected_config.permissions.active_permission_profile();        let network = selected_config.permissions.network.clone();        let mut config = self.config.clone();        if let Some(policy) = approval_policy            && !self.try_set_approval_policy_on_config(                &mut config,                policy,                "Failed to set approval policy",                "failed to set selected permission profile approval policy on app config",            )        {            return false;        }        if let Err(err) = config            .permissions            .set_permission_profile_from_session_snapshot(                PermissionProfileSnapshot::from_session_snapshot(                    permission_profile.clone(),                    active_permission_profile.clone(),                ),            )        {            tracing::warn!(                error = %err,                profile_id,                "failed to set selected permission profile on app config"            );            self.chat_widget.add_error_message(format!(                "Failed to set permission profile `{profile_id}`: {err}"            ));            return false;        }        if let Some(reviewer) = approvals_reviewer {            config.approvals_reviewer = reviewer;        }        config.permissions.network = network.clone();        self.config = config;        if let Some(policy) = approval_policy {            self.runtime_approval_policy_override = Some(policy);            self.chat_widget.set_approval_policy(policy);        }        if let Err(err) = self.chat_widget.set_permission_profile_with_active_profile(            permission_profile.clone(),            active_permission_profile.clone(),        ) {            tracing::warn!(                error = %err,                profile_id,                "failed to set selected permission profile on chat config"            );            self.chat_widget.add_error_message(format!(                "Failed to set permission profile `{profile_id}`: {err}"            ));            return false;        }        if let Some(reviewer) = approvals_reviewer {            self.chat_widget.set_approvals_reviewer(reviewer);        }        self.chat_widget.set_permission_network(network);        self.runtime_permission_profile_override =            Some(RuntimePermissionProfileOverride::from_config(&self.config));        self.sync_active_thread_permission_settings_to_cached_session()            .await;        self.app_event_tx            .send(AppEvent::CodexOp(AppCommand::override_turn_context(                /*cwd*/ None,                approval_policy,                approvals_reviewer,                Some(permission_profile.clone()),                active_permission_profile,                /*windows_sandbox_level*/ None,                /*model*/ None,                /*effort*/ None,                /*summary*/ None,                /*service_tier*/ None,                /*collaboration_mode*/ None,                /*personality*/ None,            )));        self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(            history_cell::new_info_event(                format!("Permissions updated to {display_label}"),                /*hint*/ None,            ),        )));        true    }    pub(super) async fn refresh_in_memory_config_from_disk(&mut self) -> Result<()> {        let mut config = self            .rebuild_config_for_cwd(self.chat_widget.config_ref().cwd.to_path_buf())            .await?;        self.apply_runtime_policy_overrides(&mut config);        self.config = config;        self.chat_widget.sync_plugin_mentions_config(&self.config);        Ok(())    }    pub(super) async fn refresh_in_memory_config_from_disk_best_effort(&mut self, action: &str) {        if let Err(err) = self.refresh_in_memory_config_from_disk().await {            tracing::warn!(                error = %err,                action,                "failed to refresh config before thread transition; continuing with current in-memory config"            );        }    }    pub(super) async fn read_effective_config_after_overridden_write(        &mut self,        app_server: &mut AppServerSession,        setting: &str,    ) -> Option<ConfigReadResponse> {        let cwd = self.chat_widget.config_ref().cwd.display().to_string();        match crate::config_update::read_effective_config(app_server.request_handle(), cwd).await {            Ok(response) => Some(response),            Err(err) => {                tracing::warn!(                    error = %err,                    setting,                    "failed to refresh effective config after an overridden write"                );                self.chat_widget.add_error_message(format!(                    "{setting} were saved, but Codex could not refresh the effective config: {err}"                ));                None            }        }    }    pub(super) async fn rebuild_config_for_resume_or_fallback(        &mut self,        current_cwd: &Path,        resume_cwd: PathBuf,    ) -> Result<Config> {        match self.rebuild_config_for_cwd(resume_cwd.clone()).await {            Ok(config) => Ok(config),            Err(err) => {                if crate::session_resume::cwds_differ(current_cwd, &resume_cwd) {                    Err(err)                } else {                    let resume_cwd_display = resume_cwd.display().to_string();                    tracing::warn!(                        error = %err,                        cwd = %resume_cwd_display,                        "failed to rebuild config for same-cwd resume; using current in-memory config"                    );                    Ok(self.config.clone())                }            }        }    }    pub(super) fn apply_runtime_policy_overrides(&mut self, config: &mut Config) {        if let Some(policy) = self.runtime_approval_policy_override.as_ref()            && let Err(err) = config.permissions.approval_policy.set(policy.to_core())        {            tracing::warn!(%err, "failed to carry forward approval policy override");            self.chat_widget.add_error_message(format!(                "Failed to carry forward approval policy override: {err}"            ));        }        if let Some(profile_override) = self.runtime_permission_profile_override.as_ref() {            match config                .permissions                .set_permission_profile_from_session_snapshot(                    PermissionProfileSnapshot::from_session_snapshot(                        profile_override.permission_profile.clone(),                        profile_override.active_permission_profile.clone(),                    ),                ) {                Ok(()) => {                    config.permissions.network = profile_override.network.clone();                }                Err(err) => {                    tracing::warn!(%err, "failed to carry forward permission profile override");                    self.chat_widget.add_error_message(format!(                        "Failed to carry forward permission profile override: {err}"                    ));                }            }        }    }    pub(super) fn set_approvals_reviewer_in_app_and_widget(&mut self, reviewer: ApprovalsReviewer) {        self.config.approvals_reviewer = reviewer;        self.chat_widget.set_approvals_reviewer(reviewer);    }    pub(super) fn try_set_approval_policy_on_config(        &mut self,        config: &mut Config,        policy: AskForApproval,        user_message_prefix: &str,        log_message: &str,    ) -> bool {        if let Err(err) = config.permissions.approval_policy.set(policy.to_core()) {            tracing::warn!(error = %err, "{log_message}");            self.chat_widget                .add_error_message(format!("{user_message_prefix}: {err}"));            return false;        }        true    }    pub(super) fn try_set_builtin_active_permission_profile_on_config(        &mut self,        config: &mut Config,        active_permission_profile: ActivePermissionProfile,        user_message_prefix: &str,        log_message: &str,    ) -> Option<PermissionProfile> {        let Some(permission_profile) =            builtin_permission_profile_for_active_permission_profile(&active_permission_profile)        else {            tracing::warn!(                id = %active_permission_profile.id,                "{log_message}: unsupported active permission profile"            );            self.chat_widget.add_error_message(format!(                "{user_message_prefix}: unsupported active permission profile `{}`",                active_permission_profile.id            ));            return None;        };        if let Err(err) = config            .permissions            .set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(                permission_profile.clone(),                active_permission_profile,            ))        {            tracing::warn!(error = %err, "{log_message}");            self.chat_widget                .add_error_message(format!("{user_message_prefix}: {err}"));            return None;        }        Some(permission_profile)    }    pub(super) async fn update_feature_flags(        &mut self,        app_server: &mut AppServerSession,        updates: Vec<(Feature, bool)>,    ) {        if updates.is_empty() {            return;        }        let auto_review_preset = auto_review_mode();        let mut next_config = self.config.clone();        let windows_sandbox_changed = updates.iter().any(|(feature, _)| {            matches!(                feature,                Feature::WindowsSandbox | Feature::WindowsSandboxElevated            )        });        let mut approval_policy_override = None;        let mut approvals_reviewer_override = None;        let mut permission_profile_override = None;        let mut active_permission_profile_override = None;        let mut feature_updates_to_apply = Vec::with_capacity(updates.len());        let mut permissions_history_label: Option<&'static str> = None;        let mut config_edits = Vec::new();        for (feature, enabled) in updates {            let feature_key = feature.key();            let mut feature_edits = Vec::new();            let mut feature_config = next_config.clone();            if let Err(err) = feature_config.features.set_enabled(feature, enabled) {                tracing::error!(                    error = %err,                    feature = feature_key,                    "failed to update constrained feature flags"                );                self.chat_widget.add_error_message(format!(                    "Failed to update experimental feature `{feature_key}`: {err}"                ));                continue;            }            let effective_enabled = feature_config.features.enabled(feature);            if feature == Feature::GuardianApproval {                let previous_approvals_reviewer = feature_config.approvals_reviewer;                if effective_enabled {                    // Persist the reviewer setting so future sessions keep the                    // experiment's matching `/permissions` mode until the user                    // changes it explicitly.                    feature_config.approvals_reviewer = auto_review_preset.approvals_reviewer;                    feature_edits.push(crate::config_update::replace_config_value(                        "approvals_reviewer",                        serde_json::json!(auto_review_preset.approvals_reviewer.to_string()),                    ));                    if previous_approvals_reviewer != auto_review_preset.approvals_reviewer {                        permissions_history_label = Some("Approve for me");                    }                } else if !effective_enabled {                    feature_edits.push(crate::config_update::clear_config_value(                        "approvals_reviewer",                    ));                    feature_config.approvals_reviewer = ApprovalsReviewer::User;                    if previous_approvals_reviewer != ApprovalsReviewer::User {                        permissions_history_label = Some("Ask for approval");                    }                }                approvals_reviewer_override = Some(feature_config.approvals_reviewer);            }            if feature == Feature::GuardianApproval && effective_enabled {                // The feature flag alone is not enough for the live session.                // We also align approval policy + sandbox to the Auto-review                // preset so enabling the experiment immediately                // makes guardian review observable in the current thread.                if !self.try_set_approval_policy_on_config(                    &mut feature_config,                    auto_review_preset.approval_policy,                    "Failed to enable Approve for me",                    "failed to set auto-review approval policy on staged config",                ) {                    continue;                }                let Some(permission_profile) = self                    .try_set_builtin_active_permission_profile_on_config(                        &mut feature_config,                        auto_review_preset.active_permission_profile.clone(),                        "Failed to enable Approve for me",                        "failed to set auto-review permission profile on staged config",                    )                else {                    continue;                };                feature_edits.extend([                    crate::config_update::replace_config_value(                        "approval_policy",                        serde_json::json!("on-request"),                    ),                    crate::config_update::replace_config_value(                        "sandbox_mode",                        serde_json::json!("workspace-write"),                    ),                ]);                approval_policy_override = Some(auto_review_preset.approval_policy);                permission_profile_override = Some(permission_profile);                active_permission_profile_override =                    Some(auto_review_preset.active_permission_profile.clone());            }            next_config = feature_config;            feature_updates_to_apply.push((feature, effective_enabled));            config_edits.extend(feature_edits);            config_edits.push(crate::config_update::build_feature_enabled_edit(                feature_key,                effective_enabled,            ));        }        // Persist first so the live session does not diverge from disk if the        // config edit fails. Runtime/UI state is patched below only after the        // durable config update succeeds.        let write_response = match crate::config_update::write_config_batch(            app_server.request_handle(),            config_edits,        )        .await        {            Ok(response) => response,            Err(err) => {                let error = crate::config_update::format_config_error(&err);                tracing::error!(error = %error, "failed to persist feature flags");                self.chat_widget                    .add_error_message(format!("Failed to update experimental features: {error}"));                return;            }        };        if write_response.status == WriteStatus::OkOverridden {            let message = overridden_write_message(&write_response);            tracing::warn!(                message,                "feature flag config write was overridden by effective config"            );            self.chat_widget.add_error_message(format!(                "Experimental feature changes were saved but not applied: {message}"            ));            if let Some(effective_config) = self                .read_effective_config_after_overridden_write(                    app_server,                    "Experimental feature changes",                )                .await            {                self.sync_feature_state_from_effective_config(                    &effective_config,                    &feature_updates_to_apply,                );                self.sync_auto_review_runtime_state_from_effective_config(                    &effective_config,                    &feature_updates_to_apply,                )                .await;                if windows_sandbox_changed {                    self.propagate_windows_sandbox_turn_context();                }            }            return;        }        let memory_tool_was_enabled = self.config.features.enabled(Feature::MemoryTool);        self.config = next_config;        let show_memory_enable_notice =            feature_updates_to_apply.iter().any(|(feature, enabled)| {                *feature == Feature::MemoryTool && *enabled && !memory_tool_was_enabled            });        for (feature, effective_enabled) in feature_updates_to_apply {            self.chat_widget                .set_feature_enabled(feature, effective_enabled);        }        if show_memory_enable_notice {            self.chat_widget.add_memories_enable_notice();        }        if approvals_reviewer_override.is_some() {            self.set_approvals_reviewer_in_app_and_widget(self.config.approvals_reviewer);        }        if approval_policy_override.is_some() {            self.chat_widget.set_approval_policy(AskForApproval::from(                self.config.permissions.approval_policy.value(),            ));        }        let permission_profile_override_value = permission_profile_override            .is_some()            .then(|| self.config.permissions.permission_profile().clone());        if let Some(permission_profile) = permission_profile_override_value.as_ref()            && let Err(err) = self                .chat_widget                .set_permission_profile_from_session_snapshot(                    PermissionProfileSnapshot::from_session_snapshot(                        permission_profile.clone(),                        active_permission_profile_override.clone(),                    ),                )        {            tracing::error!(                error = %err,                "failed to set auto-review permission profile on chat config"            );            self.chat_widget                .add_error_message(format!("Failed to enable Approve for me: {err}"));        }        if permission_profile_override.is_some() {            self.runtime_permission_profile_override =                Some(RuntimePermissionProfileOverride::from_config(&self.config));        }        if approval_policy_override.is_some()            || approvals_reviewer_override.is_some()            || permission_profile_override.is_some()        {            self.sync_active_thread_permission_settings_to_cached_session()                .await;            // This uses `OverrideTurnContext` intentionally: toggling the            // experiment should update the active thread's effective approval            // settings immediately, just like a `/permissions` selection. Without            // this runtime patch, the config edit would only affect future            // sessions or turns recreated from disk.            let op = AppCommand::override_turn_context(                /*cwd*/ None,                approval_policy_override,                approvals_reviewer_override,                permission_profile_override,                active_permission_profile_override,                /*windows_sandbox_level*/ None,                /*model*/ None,                /*effort*/ None,                /*summary*/ None,                /*service_tier*/ None,                /*collaboration_mode*/ None,                /*personality*/ None,            );            let replay_state_op =                ThreadEventStore::op_can_change_pending_replay_state(&op).then(|| op.clone());            let submitted = self.chat_widget.submit_op(op);            if submitted && let Some(op) = replay_state_op.as_ref() {                self.note_active_thread_outbound_op(op).await;                self.refresh_pending_thread_approvals().await;            }        }        if windows_sandbox_changed {            self.propagate_windows_sandbox_turn_context();        }        if let Some(label) = permissions_history_label {            self.chat_widget.add_info_message(                format!("Permissions updated to {label}"),                /*hint*/ None,            );        }    }    pub(super) async fn update_memory_settings(        &mut self,        app_server: &mut AppServerSession,        use_memories: bool,        generate_memories: bool,    ) -> bool {        let edits =            crate::config_update::build_memory_settings_edits(use_memories, generate_memories);        let write_response = match crate::config_update::write_config_batch(            app_server.request_handle(),            edits,        )        .await        {            Ok(response) => response,            Err(err) => {                tracing::error!(error = %err, "failed to persist memory settings");                self.chat_widget                    .add_error_message(format!("Failed to save memory settings: {err}"));                return false;            }        };        if write_response.status == WriteStatus::OkOverridden {            let message = overridden_write_message(&write_response);            tracing::warn!(                message,                "memory settings config write was overridden by effective config"            );            self.chat_widget.add_error_message(format!(                "Memory setting changes were saved but not applied: {message}"            ));            let Some(effective_config) = self                .read_effective_config_after_overridden_write(app_server, "Memory setting changes")                .await            else {                return false;            };            return self.sync_memory_state_from_effective_config(&effective_config);        }        self.config.memories.use_memories = use_memories;        self.config.memories.generate_memories = generate_memories;        self.chat_widget            .set_memory_settings(use_memories, generate_memories);        true    }    pub(super) async fn update_memory_settings_with_app_server(        &mut self,        app_server: &mut AppServerSession,        use_memories: bool,        generate_memories: bool,    ) {        let previous_generate_memories = self.config.memories.generate_memories;        if !self            .update_memory_settings(app_server, use_memories, generate_memories)            .await        {            return;        }        let generate_memories = self.config.memories.generate_memories;        if previous_generate_memories == generate_memories {            return;        }        let Some(thread_id) = self.current_displayed_thread_id() else {            return;        };        let mode = if generate_memories {            ThreadMemoryMode::Enabled        } else {            ThreadMemoryMode::Disabled        };        if let Err(err) = app_server.thread_memory_mode_set(thread_id, mode).await {            tracing::error!(error = %err, %thread_id, "failed to update thread memory mode");            self.chat_widget.add_error_message(format!(                "Saved memory settings, but failed to update the current thread: {err}"            ));        }    }    pub(super) async fn reset_memories_with_app_server(        &mut self,        app_server: &mut AppServerSession,    ) {        if let Err(err) = app_server.memory_reset().await {            tracing::error!(error = %err, "failed to reset memories");            self.chat_widget                .add_error_message(format!("Failed to reset memories: {err}"));            return;        }        self.chat_widget            .add_info_message("Reset local memories.".to_string(), /*hint*/ None);    }    pub(super) fn reasoning_label(reasoning_effort: Option<&ReasoningEffortConfig>) -> String {        match reasoning_effort {            None | Some(ReasoningEffortConfig::None) => "default".to_string(),            Some(reasoning_effort) => reasoning_effort.as_str().to_string(),        }    }    pub(super) fn reasoning_label_for(        model: &str,        reasoning_effort: Option<&ReasoningEffortConfig>,    ) -> Option<String> {        (!model.starts_with("codex-auto-")).then(|| Self::reasoning_label(reasoning_effort))    }    pub(crate) fn token_usage(&self) -> crate::token_usage::TokenUsage {        self.chat_widget.token_usage()    }    pub(super) fn on_update_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {        // TODO(aibrahim): Remove this and don't use config as a state object.        // Instead, explicitly pass the stored collaboration mode's effort into new sessions.        self.config.model_reasoning_effort = effort.clone();        self.chat_widget.set_reasoning_effort(effort);    }    pub(super) fn on_update_personality(&mut self, personality: Personality) {        self.config.personality = Some(personality);        self.chat_widget.set_personality(personality);    }    pub(super) fn sync_tui_theme_selection(&mut self, name: String) {        self.config.tui_theme = Some(name.clone());        self.chat_widget.set_tui_theme(Some(name));    }    #[cfg(test)]    pub(super) fn sync_tui_pet_selection(&mut self, pet: String) {        self.config.tui_pet = Some(pet.clone());        self.chat_widget.set_tui_pet(Some(pet));    }    pub(super) fn sync_tui_pet_disabled(&mut self) {        let pet = crate::pets::DISABLED_PET_ID.to_string();        self.config.tui_pet = Some(pet.clone());        self.chat_widget.set_tui_pet(Some(pet));    }    pub(super) fn restore_runtime_theme_from_config(&self) {        if let Some(name) = self.config.tui_theme.as_deref()            && let Some(theme) =                crate::render::highlight::resolve_theme_by_name(name, Some(&self.config.codex_home))        {            crate::render::highlight::set_syntax_theme(theme);            return;        }        let auto_theme_name = crate::render::highlight::adaptive_default_theme_name();        if let Some(theme) = crate::render::highlight::resolve_theme_by_name(            auto_theme_name,            Some(&self.config.codex_home),        ) {            crate::render::highlight::set_syntax_theme(theme);        }    }    pub(super) fn personality_label(personality: Personality) -> &'static str {        match personality {            Personality::None => "None",            Personality::Friendly => "Friendly",            Personality::Pragmatic => "Pragmatic",        }    }    fn sync_feature_state_from_effective_config(        &mut self,        effective_config: &ConfigReadResponse,        feature_updates: &[(Feature, bool)],    ) {        for (feature, _) in feature_updates {            let enabled = feature_enabled_from_effective_config(effective_config, *feature);            if let Err(err) = self.config.features.set_enabled(*feature, enabled) {                tracing::warn!(                    error = %err,                    feature = feature.key(),                    "failed to sync effective feature state after an overridden write"                );                continue;            }            self.chat_widget.set_feature_enabled(*feature, enabled);        }        if feature_updates            .iter()            .any(|(feature, _)| *feature == Feature::GuardianApproval)            && !self.config.features.enabled(Feature::GuardianApproval)        {            self.set_approvals_reviewer_in_app_and_widget(ApprovalsReviewer::User);            return;        }        if let Some(reviewer) = approvals_reviewer_from_effective_config(effective_config) {            self.set_approvals_reviewer_in_app_and_widget(reviewer);        }        if let Some(policy) = approval_policy_from_effective_config(effective_config) {            if let Err(err) = self                .config                .permissions                .approval_policy                .set(policy.to_core())            {                tracing::warn!(                    error = %err,                    "failed to sync effective approval policy after an overridden write"                );                self.chat_widget.add_error_message(format!(                    "Failed to refresh overridden Approve for me settings: {err}"                ));            } else {                self.chat_widget.set_approval_policy(policy);            }        }    }    async fn sync_auto_review_runtime_state_from_effective_config(        &mut self,        effective_config: &ConfigReadResponse,        feature_updates: &[(Feature, bool)],    ) {        if !feature_updates            .iter()            .any(|(feature, _)| *feature == Feature::GuardianApproval)            || !self.config.features.enabled(Feature::GuardianApproval)            || sandbox_mode_from_effective_config(effective_config)                != Some(AppServerSandboxMode::WorkspaceWrite)        {            return;        }        let auto_review_preset = auto_review_mode();        let mut config = self.config.clone();        let Some(permission_profile) = self.try_set_builtin_active_permission_profile_on_config(            &mut config,            auto_review_preset.active_permission_profile.clone(),            "Failed to refresh overridden Approve for me settings",            "failed to sync overridden Auto-review permission profile",        ) else {            return;        };        self.config = config;        if let Err(err) = self            .chat_widget            .set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(                permission_profile.clone(),                auto_review_preset.active_permission_profile.clone(),            ))        {            tracing::warn!(                error = %err,                "failed to sync overridden Auto-review permission profile on chat config"            );            self.chat_widget.add_error_message(format!(                "Failed to refresh overridden Approve for me settings: {err}"            ));            return;        }        self.runtime_permission_profile_override =            Some(RuntimePermissionProfileOverride::from_config(&self.config));        self.sync_active_thread_permission_settings_to_cached_session()            .await;        let approval_policy = AskForApproval::from(self.config.permissions.approval_policy.value());        let op = AppCommand::override_turn_context(            /*cwd*/ None,            Some(approval_policy),            Some(self.config.approvals_reviewer),            /*permission_profile*/ None,            Some(auto_review_preset.active_permission_profile),            /*windows_sandbox_level*/ None,            /*model*/ None,            /*effort*/ None,            /*summary*/ None,            /*service_tier*/ None,            /*collaboration_mode*/ None,            /*personality*/ None,        );        let replay_state_op =            ThreadEventStore::op_can_change_pending_replay_state(&op).then(|| op.clone());        let submitted = self.chat_widget.submit_op(op);        if submitted && let Some(op) = replay_state_op.as_ref() {            self.note_active_thread_outbound_op(op).await;            self.refresh_pending_thread_approvals().await;        }    }    fn sync_memory_state_from_effective_config(        &mut self,        effective_config: &ConfigReadResponse,    ) -> bool {        let Some(memories) = memories_from_effective_config(effective_config) else {            tracing::warn!(                "config/read omitted memories after an overridden memory settings write"            );            return false;        };        let use_memories = memories            .use_memories            .unwrap_or(self.config.memories.use_memories);        let generate_memories = memories            .generate_memories            .unwrap_or(self.config.memories.generate_memories);        self.config.memories.use_memories = use_memories;        self.config.memories.generate_memories = generate_memories;        self.chat_widget            .set_memory_settings(use_memories, generate_memories);        true    }    #[cfg(target_os = "windows")]    pub(super) async fn sync_windows_sandbox_after_overridden_write(        &mut self,        app_server: &mut AppServerSession,        write_response: &ConfigWriteResponse,    ) {        let message = overridden_write_message(write_response);        tracing::warn!(            message,            "Windows sandbox config write was overridden by effective config"        );        self.chat_widget.add_error_message(format!(            "Windows sandbox changes were saved but not applied: {message}"        ));        let Some(effective_config) = self            .read_effective_config_after_overridden_write(app_server, "Windows sandbox changes")            .await        else {            return;        };        let Some(mode) = windows_sandbox_mode_from_effective_config(&effective_config) else {            return;        };        self.config.permissions.windows_sandbox_mode = Some(mode);        self.chat_widget.set_windows_sandbox_mode(Some(mode));        self.propagate_windows_sandbox_turn_context();    }    fn propagate_windows_sandbox_turn_context(&self) {        #[cfg(target_os = "windows")]        {            let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config);            self.app_event_tx                .send(AppEvent::CodexOp(AppCommand::override_turn_context(                    /*cwd*/ None,                    /*approval_policy*/ None,                    /*approvals_reviewer*/ None,                    /*permission_profile*/ None,                    /*active_permission_profile*/ None,                    Some(windows_sandbox_level),                    /*model*/ None,                    /*effort*/ None,                    /*summary*/ None,                    /*service_tier*/ None,                    /*collaboration_mode*/ None,                    /*personality*/ None,                )));        }    }}fn overridden_write_message(write_response: &ConfigWriteResponse) -> &str {    write_response        .overridden_metadata        .as_ref()        .map(|metadata| metadata.message.as_str())        .unwrap_or("the effective config is overridden by a higher-priority layer")}fn feature_enabled_from_effective_config(    effective_config: &ConfigReadResponse,    feature: Feature,) -> bool {    let root_features = effective_config        .config        .additional        .get("features")        .and_then(features_toml_from_json);    root_features        .as_ref()        .and_then(|features| features.entries().get(feature.key()).copied())        .unwrap_or_else(|| feature.default_enabled())}fn approvals_reviewer_from_effective_config(    effective_config: &ConfigReadResponse,) -> Option<ApprovalsReviewer> {    effective_config        .config        .approvals_reviewer        .map(codex_app_server_protocol::ApprovalsReviewer::to_core)}fn approval_policy_from_effective_config(    effective_config: &ConfigReadResponse,) -> Option<AskForApproval> {    effective_config.config.approval_policy}fn sandbox_mode_from_effective_config(    effective_config: &ConfigReadResponse,) -> Option<AppServerSandboxMode> {    effective_config.config.sandbox_mode}fn memories_from_effective_config(effective_config: &ConfigReadResponse) -> Option<MemoriesToml> {    effective_config        .config        .additional        .get("memories")        .and_then(|memories| serde_json::from_value(memories.clone()).ok())}fn features_toml_from_json(value: &serde_json::Value) -> Option<FeaturesToml> {    serde_json::from_value(value.clone()).ok()}#[cfg(target_os = "windows")]fn windows_sandbox_mode_from_effective_config(    effective_config: &ConfigReadResponse,) -> Option<codex_config::types::WindowsSandboxModeToml> {    let root_windows = effective_config        .config        .additional        .get("windows")        .and_then(windows_toml_from_json);    root_windows.and_then(|windows| windows.sandbox)}#[cfg(target_os = "windows")]fn windows_toml_from_json(value: &serde_json::Value) -> Option<WindowsToml> {    serde_json::from_value(value.clone()).ok()}#[cfg(test)]mod tests {    use super::*;    use crate::app::test_support::app_enabled_in_effective_config;    use crate::app::test_support::make_test_app;    use crate::legacy_core::config::edit::ConfigEdit;    use crate::test_support::PathBufExt;    use codex_protocol::models::PermissionProfile;    use pretty_assertions::assert_eq;    use tempfile::tempdir;    #[tokio::test]    async fn update_reasoning_effort_updates_collaboration_mode() {        let mut app = make_test_app().await;        app.chat_widget            .set_reasoning_effort(Some(ReasoningEffortConfig::Medium));        app.on_update_reasoning_effort(Some(ReasoningEffortConfig::High));        assert_eq!(            app.chat_widget.current_reasoning_effort(),            Some(ReasoningEffortConfig::High)        );        assert_eq!(            app.config.model_reasoning_effort,            Some(ReasoningEffortConfig::High)        );    }    #[tokio::test]    async fn refresh_in_memory_config_from_disk_loads_latest_apps_state() -> Result<()> {        let mut app = make_test_app().await;        let codex_home = tempdir()?;        app.config.codex_home = codex_home.path().to_path_buf().abs();        let app_id = "unit_test_refresh_in_memory_config_connector".to_string();        assert_eq!(app_enabled_in_effective_config(&app.config, &app_id), None);        ConfigEditsBuilder::for_config(&app.config)            .with_edits([                ConfigEdit::SetPath {                    segments: vec!["apps".to_string(), app_id.clone(), "enabled".to_string()],                    value: false.into(),                },                ConfigEdit::SetPath {                    segments: vec![                        "apps".to_string(),                        app_id.clone(),                        "disabled_reason".to_string(),                    ],                    value: "user".into(),                },            ])            .apply()            .await            .expect("persist app toggle");        assert_eq!(app_enabled_in_effective_config(&app.config, &app_id), None);        app.refresh_in_memory_config_from_disk().await?;        assert_eq!(            app_enabled_in_effective_config(&app.config, &app_id),            Some(false)        );        Ok(())    }    // Regression coverage for `/new` and `/clear`: cloud requirements    // must survive the config refresh that runs before thread transitions.    #[tokio::test]    async fn refresh_in_memory_config_from_disk_keeps_cloud_requirements_for_thread_transitions()    -> Result<()> {        let mut app = make_test_app().await;        let codex_home = tempdir()?;        let required_policy = codex_protocol::protocol::AskForApproval::Never;        let cloud_config_bundle =            codex_config::test_support::CloudConfigBundleFixture::loader_with_enterprise_requirement(                r#"allowed_approval_policies = ["never"]"#,            );        let config = ConfigBuilder::default()            .codex_home(codex_home.path().to_path_buf())            .loader_overrides(LoaderOverrides::without_managed_config_for_tests())            .cloud_config_bundle(cloud_config_bundle.clone())            .build()            .await?;        app.config = config;        app.cloud_config_bundle = cloud_config_bundle;        let app_id = "unit_test_cloud_requirements_reload_marker";        std::fs::write(            codex_home.path().join("config.toml"),            format!(                r#"[apps.{app_id}]enabled = false"#            ),        )?;        let assert_cloud_requirements = |app: &App| {            let config = app.fresh_session_config();            assert_eq!(                config                    .config_layer_stack                    .requirements_toml()                    .allowed_approval_policies                    .clone(),                Some(vec![required_policy])            );            assert_eq!(config.permissions.approval_policy.value(), required_policy);        };        assert_cloud_requirements(&app);        assert_eq!(app_enabled_in_effective_config(&app.config, app_id), None);        // This is the fallible reload that the best-effort `/new`, `/clear`,        // `/fork`, side-conversation, and session-picker paths wrap.        app.refresh_in_memory_config_from_disk().await?;        assert_eq!(            app_enabled_in_effective_config(&app.config, app_id),            Some(false)        );        assert_cloud_requirements(&app);        Ok(())    }    #[tokio::test]    async fn refresh_in_memory_config_from_disk_best_effort_keeps_current_config_on_error()    -> Result<()> {        let mut app = make_test_app().await;        let codex_home = tempdir()?;        app.config.codex_home = codex_home.path().to_path_buf().abs();        std::fs::write(codex_home.path().join("config.toml"), "[broken")?;        let original_config = app.config.clone();        app.refresh_in_memory_config_from_disk_best_effort("starting a new thread")            .await;        assert_eq!(app.config, original_config);        Ok(())    }    #[tokio::test]    async fn refresh_in_memory_config_from_disk_uses_active_chat_widget_cwd() -> Result<()> {        let mut app = make_test_app().await;        let original_cwd = app.config.cwd.clone();        let next_cwd_tmp = tempdir()?;        let next_cwd = next_cwd_tmp.path().to_path_buf();        app.chat_widget            .handle_thread_session(crate::session_state::ThreadSessionState {                thread_id: ThreadId::new(),                forked_from_id: None,                fork_parent_title: None,                thread_name: None,                model: "gpt-test".to_string(),                model_provider_id: "test-provider".to_string(),                service_tier: None,                approval_policy: AskForApproval::Never,                approvals_reviewer: ApprovalsReviewer::User,                permission_profile: PermissionProfile::read_only(),                active_permission_profile: None,                cwd: next_cwd.clone().abs(),                runtime_workspace_roots: Vec::new(),                instruction_source_paths: Vec::new(),                reasoning_effort: None,                collaboration_mode: None,                personality: None,                message_history: None,                network_proxy: None,                rollout_path: Some(PathBuf::new()),            });        assert_eq!(app.chat_widget.config_ref().cwd.to_path_buf(), next_cwd);        assert_eq!(app.config.cwd, original_cwd);        app.refresh_in_memory_config_from_disk().await?;        assert_eq!(app.config.cwd, app.chat_widget.config_ref().cwd);        Ok(())    }    #[tokio::test]    async fn refresh_in_memory_config_from_disk_updates_resize_reflow_config() -> Result<()> {        let mut app = make_test_app().await;        let codex_home = tempdir()?;        app.config.codex_home = codex_home.path().to_path_buf().abs();        std::fs::write(            codex_home.path().join("config.toml"),            r#"[tui]terminal_resize_reflow_max_rows = 9000"#,        )?;        app.refresh_in_memory_config_from_disk().await?;        assert_eq!(            app.config.terminal_resize_reflow.max_rows,            crate::legacy_core::config::TerminalResizeReflowMaxRows::Limit(9000)        );        Ok(())    }    #[tokio::test]    async fn overridden_disabled_guardian_does_not_apply_auto_review_companions() -> Result<()> {        let mut app = make_test_app().await;        let original_policy = app.config.permissions.approval_policy.value();        let effective_config: ConfigReadResponse = serde_json::from_value(serde_json::json!({            "config": {                "approval_policy": AskForApproval::OnRequest,                "approvals_reviewer": codex_app_server_protocol::ApprovalsReviewer::AutoReview,                "sandbox_mode": AppServerSandboxMode::WorkspaceWrite,                "features": {                    "guardian_approval": false,                },            },            "origins": {},        }))?;        app.sync_feature_state_from_effective_config(            &effective_config,            &[(Feature::GuardianApproval, /*enabled*/ true)],        );        assert!(!app.config.features.enabled(Feature::GuardianApproval));        assert!(            !app.chat_widget                .config_ref()                .features                .enabled(Feature::GuardianApproval)        );        assert_eq!(app.config.approvals_reviewer, ApprovalsReviewer::User);        assert_eq!(            app.chat_widget.config_ref().approvals_reviewer,            ApprovalsReviewer::User        );        assert_eq!(            app.config.permissions.approval_policy.value(),            original_policy        );        Ok(())    }    #[tokio::test]    async fn rebuild_config_for_resume_or_fallback_uses_current_config_on_same_cwd_error()    -> Result<()> {        let mut app = make_test_app().await;        let codex_home = tempdir()?;        app.config.codex_home = codex_home.path().to_path_buf().abs();        std::fs::write(codex_home.path().join("config.toml"), "[broken")?;        let current_config = app.config.clone();        let current_cwd = current_config.cwd.clone();        let resume_config = app            .rebuild_config_for_resume_or_fallback(&current_cwd, current_cwd.to_path_buf())            .await?;        assert_eq!(resume_config, current_config);        Ok(())    }    #[tokio::test]    async fn rebuild_config_for_resume_or_fallback_errors_when_cwd_changes() -> Result<()> {        let mut app = make_test_app().await;        let codex_home = tempdir()?;        app.config.codex_home = codex_home.path().to_path_buf().abs();        std::fs::write(codex_home.path().join("config.toml"), "[broken")?;        let current_cwd = app.config.cwd.clone();        let next_cwd_tmp = tempdir()?;        let next_cwd = next_cwd_tmp.path().to_path_buf();        let result = app            .rebuild_config_for_resume_or_fallback(&current_cwd, next_cwd)            .await;        assert!(result.is_err());        Ok(())    }    #[tokio::test]    async fn sync_tui_theme_selection_updates_chat_widget_config_copy() {        let mut app = make_test_app().await;        app.sync_tui_theme_selection("dracula".to_string());        assert_eq!(app.config.tui_theme.as_deref(), Some("dracula"));        assert_eq!(            app.chat_widget.config_ref().tui_theme.as_deref(),            Some("dracula")        );    }    #[tokio::test]    async fn sync_tui_pet_selection_updates_chat_widget_config_copy() {        let mut app = make_test_app().await;        app.sync_tui_pet_selection("chefito".to_string());        assert_eq!(app.config.tui_pet.as_deref(), Some("chefito"));        assert_eq!(            app.chat_widget.config_ref().tui_pet.as_deref(),            Some("chefito")        );    }    #[tokio::test]    async fn sync_tui_pet_disabled_updates_chat_widget_config_copy() {        let mut app = make_test_app().await;        app.sync_tui_pet_disabled();        assert_eq!(            app.config.tui_pet.as_deref(),            Some(crate::pets::DISABLED_PET_ID)        );        assert_eq!(            app.chat_widget.config_ref().tui_pet.as_deref(),            Some(crate::pets::DISABLED_PET_ID)        );    }}