//! App-server-backed config update helpers for the TUI.//!//! This module centralizes the small typed update helpers the TUI uses//! when a config mutation must be owned by the app server rather than written//! to the local `config.toml` directly.use codex_app_server_client::AppServerRequestHandle;use codex_app_server_protocol::ClientRequest;use codex_app_server_protocol::ConfigBatchWriteParams;use codex_app_server_protocol::ConfigEdit;use codex_app_server_protocol::ConfigReadParams;use codex_app_server_protocol::ConfigReadResponse;use codex_app_server_protocol::ConfigWriteResponse;use codex_app_server_protocol::MergeStrategy;use codex_app_server_protocol::RequestId;use codex_app_server_protocol::SkillsConfigWriteParams;use codex_app_server_protocol::SkillsConfigWriteResponse;use codex_config::loader::project_trust_key;use codex_features::FEATURES;use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;use codex_protocol::config_types::TrustLevel;use codex_utils_absolute_path::AbsolutePathBuf;use color_eyre::eyre::Result;use color_eyre::eyre::WrapErr;use serde_json::Value as JsonValue;use std::fmt::Display;use std::path::Path;use uuid::Uuid;pub(crate) fn replace_config_value(key_path: impl Into<String>, value: JsonValue) -> ConfigEdit { ConfigEdit { key_path: key_path.into(), value, merge_strategy: MergeStrategy::Replace, }}pub(crate) fn clear_config_value(key_path: impl Into<String>) -> ConfigEdit { replace_config_value(key_path, JsonValue::Null)}pub(crate) fn app_scoped_key_path(app_id: &str, key_path: &str) -> String { let app_id = serde_json::Value::String(app_id.to_string()).to_string(); format!("apps.{app_id}.{key_path}")}pub(crate) fn format_config_error(err: &impl Display) -> String { format!("{err:#}")}fn trusted_project_edit(project_path: &Path) -> ConfigEdit { let project_key = project_trust_key(project_path) .replace('\\', "\\\\") .replace('"', "\\\""); replace_config_value( format!("projects.\"{project_key}\".trust_level"), serde_json::json!(TrustLevel::Trusted.to_string()), )}pub(crate) fn build_model_selection_edits( model: &str, effort: Option<impl ToString>,) -> Vec<ConfigEdit> { let effort_edit = effort.map_or_else( || clear_config_value("model_reasoning_effort"), |effort| { replace_config_value( "model_reasoning_effort", serde_json::json!(effort.to_string()), ) }, ); vec![ replace_config_value("model", serde_json::json!(model)), effort_edit, ]}pub(crate) fn build_service_tier_selection_edits(service_tier: Option<&str>) -> Vec<ConfigEdit> { let service_tier_edit = service_tier.map_or_else( || clear_config_value("service_tier"), |service_tier| { let config_value = if service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE { SERVICE_TIER_DEFAULT_REQUEST_VALUE } else { match codex_protocol::config_types::ServiceTier::from_request_value(service_tier) { Some(codex_protocol::config_types::ServiceTier::Fast) => "fast", Some(codex_protocol::config_types::ServiceTier::Flex) => "flex", None => service_tier, } }; replace_config_value("service_tier", serde_json::json!(config_value)) }, ); vec![service_tier_edit]}#[cfg(target_os = "windows")]pub(crate) fn build_windows_sandbox_mode_edits(elevated_enabled: bool) -> Vec<ConfigEdit> { let feature_key_path = |feature: &str| format!("features.{feature}"); vec![ replace_config_value( "windows.sandbox", serde_json::json!(if elevated_enabled { "elevated" } else { "unelevated" }), ), clear_config_value(feature_key_path("experimental_windows_sandbox")), clear_config_value(feature_key_path("elevated_windows_sandbox")), clear_config_value(feature_key_path("enable_experimental_windows_sandbox")), ]}pub(crate) fn build_feature_enabled_edit(feature_key: &str, enabled: bool) -> ConfigEdit { let key_path = format!("features.{feature_key}"); let is_default_false_feature = FEATURES .iter() .find(|spec| spec.key == feature_key) .is_some_and(|spec| !spec.default_enabled); if enabled || !is_default_false_feature { replace_config_value(key_path, serde_json::json!(enabled)) } else { clear_config_value(key_path) }}pub(crate) fn build_memory_settings_edits( use_memories: bool, generate_memories: bool,) -> Vec<ConfigEdit> { vec![ replace_config_value("memories.use_memories", serde_json::json!(use_memories)), replace_config_value( "memories.generate_memories", serde_json::json!(generate_memories), ), ]}pub(crate) fn build_oss_provider_edit(provider: &str) -> ConfigEdit { replace_config_value("oss_provider", serde_json::json!(provider))}pub(crate) async fn write_config_batch( request_handle: AppServerRequestHandle, edits: Vec<ConfigEdit>,) -> Result<ConfigWriteResponse> { let request_id = RequestId::String(format!("tui-config-write-{}", Uuid::new_v4())); request_handle .request_typed(ClientRequest::ConfigBatchWrite { request_id, params: ConfigBatchWriteParams { edits, file_path: None, expected_version: None, reload_user_config: true, }, }) .await .wrap_err("config/batchWrite failed in TUI")}pub(crate) async fn write_trusted_project( request_handle: AppServerRequestHandle, project_path: &Path,) -> Result<ConfigWriteResponse> { write_config_batch(request_handle, vec![trusted_project_edit(project_path)]).await}pub(crate) async fn read_effective_config( request_handle: AppServerRequestHandle, cwd: String,) -> Result<ConfigReadResponse> { let request_id = RequestId::String(format!("tui-config-read-{}", Uuid::new_v4())); request_handle .request_typed(ClientRequest::ConfigRead { request_id, params: ConfigReadParams { include_layers: false, cwd: Some(cwd), }, }) .await .wrap_err("config/read failed in TUI")}pub(crate) async fn write_skill_enabled( request_handle: AppServerRequestHandle, path: AbsolutePathBuf, enabled: bool,) -> Result<()> { let request_id = RequestId::String(format!("tui-skill-config-write-{}", Uuid::new_v4())); let _: SkillsConfigWriteResponse = request_handle .request_typed(ClientRequest::SkillsConfigWrite { request_id, params: SkillsConfigWriteParams { path: Some(path), name: None, enabled, }, }) .await .wrap_err("skills/config/write failed in TUI")?; Ok(())}#[cfg(test)]#[path = "config_update_tests.rs"]mod tests;