diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index c00974967829..97f96774a4b5 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -831,11 +831,15 @@ impl App { ); let (mut chat_widget, initial_started_thread) = match session_selection { SessionSelection::StartFresh | SessionSelection::Exit => { + // Only count a startup tooltip once the fresh thread can actually render it. + let startup_tooltip_override = prepare_startup_tooltip_override( + &mut app_server, + &mut config, + &available_models, + is_first_run, + ) + .await; spawn_startup_thread_start(&app_server, config.clone(), app_event_tx.clone()); - // Count a startup tooltip once the initial chat widget can render it. - let startup_tooltip_override = - prepare_startup_tooltip_override(&mut config, &available_models, is_first_run) - .await; let init = crate::chatwidget::ChatWidgetInit { config: config.clone(), frame_requester: tui.frame_requester(), diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 1cba3c85a0c4..166a53a56899 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -515,9 +515,7 @@ impl App { 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}" - )); + let mut show_overridden_message = true; if let Some(effective_config) = self .read_effective_config_after_overridden_write( app_server, @@ -529,6 +527,20 @@ impl App { &effective_config, &feature_updates_to_apply, ); + if feature_updates_to_apply + .iter() + .any(|(feature, enabled)| *feature == Feature::GuardianApproval && !*enabled) + && !self.config.features.enabled(Feature::GuardianApproval) + { + show_overridden_message = false; + self.submit_permission_settings_override( + /*approval_policy*/ None, + Some(ApprovalsReviewer::User), + /*permission_profile*/ None, + /*active_permission_profile*/ None, + ) + .await; + } self.sync_auto_review_runtime_state_from_effective_config( &effective_config, &feature_updates_to_apply, @@ -537,6 +549,17 @@ impl App { 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, + ); + } + } + if show_overridden_message { + self.chat_widget.add_error_message(format!( + "Experimental feature changes were saved but not applied: {message}" + )); } return; } @@ -591,34 +614,18 @@ impl App { || 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, + self.submit_permission_settings_override( 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; - } + ) + .await; } if windows_sandbox_changed { @@ -912,16 +919,32 @@ impl App { 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, + self.submit_permission_settings_override( Some(approval_policy), Some(self.config.approvals_reviewer), /*permission_profile*/ None, Some(auto_review_preset.active_permission_profile), + ) + .await; + } + + async fn submit_permission_settings_override( + &mut self, + approval_policy: Option, + approvals_reviewer: Option, + permission_profile: Option, + active_permission_profile: Option, + ) { + self.sync_active_thread_permission_settings_to_cached_session() + .await; + let op = AppCommand::override_turn_context( + /*cwd*/ None, + approval_policy, + approvals_reviewer, + permission_profile, + active_permission_profile, /*windows_sandbox_level*/ None, /*model*/ None, /*effort*/ None, diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index d103bb738d71..7c1efe9a0aa0 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -1661,10 +1661,12 @@ impl App { .await; } AppEvent::PersistFullAccessWarningAcknowledged => { - if let Err(err) = ConfigEditsBuilder::for_config(&self.config) - .set_hide_full_access_warning(/*acknowledged*/ true) - .apply() - .await + if let Err(err) = crate::config_update::write_config_value( + app_server.request_handle(), + "notice.hide_full_access_warning", + serde_json::json!(true), + ) + .await { tracing::error!( error = %err, @@ -1676,10 +1678,12 @@ impl App { } } AppEvent::PersistWorldWritableWarningAcknowledged => { - if let Err(err) = ConfigEditsBuilder::for_config(&self.config) - .set_hide_world_writable_warning(/*acknowledged*/ true) - .apply() - .await + if let Err(err) = crate::config_update::write_config_value( + app_server.request_handle(), + "notice.hide_world_writable_warning", + serde_json::json!(true), + ) + .await { tracing::error!( error = %err, @@ -1691,10 +1695,12 @@ impl App { } } AppEvent::PersistRateLimitSwitchPromptHidden => { - if let Err(err) = ConfigEditsBuilder::for_config(&self.config) - .set_hide_rate_limit_model_nudge(/*acknowledged*/ true) - .apply() - .await + if let Err(err) = crate::config_update::write_config_value( + app_server.request_handle(), + "notice.hide_rate_limit_model_nudge", + serde_json::json!(true), + ) + .await { tracing::error!( error = %err, @@ -1744,10 +1750,14 @@ impl App { from_model, to_model, } => { - if let Err(err) = ConfigEditsBuilder::for_config(&self.config) - .record_model_migration_seen(from_model.as_str(), to_model.as_str()) - .apply() - .await + let mut migration_update = serde_json::Map::new(); + migration_update.insert(from_model, serde_json::json!(to_model)); + if let Err(err) = crate::config_update::write_upsert_config_value( + app_server.request_handle(), + "notice.model_migrations", + serde_json::Value::Object(migration_update), + ) + .await { tracing::error!( error = %err, diff --git a/codex-rs/tui/src/app/startup_prompts.rs b/codex-rs/tui/src/app/startup_prompts.rs index 802cda3f80c6..f90bf13297a8 100644 --- a/codex-rs/tui/src/app/startup_prompts.rs +++ b/codex-rs/tui/src/app/startup_prompts.rs @@ -187,11 +187,11 @@ pub(super) fn select_model_availability_nux( }) } -pub(super) async fn prepare_startup_tooltip_override( - config: &mut Config, +pub(super) fn next_startup_tooltip_override( + config: &Config, available_models: &[ModelPreset], is_first_run: bool, -) -> Option { +) -> Option<(StartupTooltipOverride, HashMap)> { if is_first_run || !config.show_tooltips { return None; } @@ -208,11 +208,24 @@ pub(super) async fn prepare_startup_tooltip_override( let next_count = shown_count.saturating_add(1); let mut updated_shown_count = config.model_availability_nux.shown_count.clone(); updated_shown_count.insert(tooltip_override.model_slug.clone(), next_count); + Some((tooltip_override, updated_shown_count)) +} - if let Err(err) = ConfigEditsBuilder::for_config(config) - .set_model_availability_nux_count(&updated_shown_count) - .apply() - .await +pub(super) async fn prepare_startup_tooltip_override( + app_server: &mut AppServerSession, + config: &mut Config, + available_models: &[ModelPreset], + is_first_run: bool, +) -> Option { + let (tooltip_override, updated_shown_count) = + next_startup_tooltip_override(config, available_models, is_first_run)?; + + if let Err(err) = crate::config_update::write_config_value( + app_server.request_handle(), + "tui.model_availability_nux", + serde_json::json!(updated_shown_count), + ) + .await { tracing::error!( error = %err, diff --git a/codex-rs/tui/src/app/tests/model_catalog.rs b/codex-rs/tui/src/app/tests/model_catalog.rs index 7d2eb9c125a9..36b2df8d966d 100644 --- a/codex-rs/tui/src/app/tests/model_catalog.rs +++ b/codex-rs/tui/src/app/tests/model_catalog.rs @@ -175,9 +175,9 @@ fn select_model_availability_nux_returns_none_when_all_models_are_exhausted() { } #[tokio::test] -async fn prepare_startup_tooltip_override_persists_model_availability_nux_count() { +async fn next_startup_tooltip_override_advances_model_availability_nux_count() { let codex_home = tempdir().expect("temp codex home"); - let mut config = ConfigBuilder::default() + let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .build() .await @@ -194,24 +194,12 @@ async fn prepare_startup_tooltip_override_persists_model_availability_nux_count( message: "gpt-5.4 is available".to_string(), }); - let tooltip = - prepare_startup_tooltip_override(&mut config, &presets, /*is_first_run*/ false).await; - - assert_eq!(tooltip.as_deref(), Some("gpt-5.4 is available")); - assert_eq!( - config.model_availability_nux.shown_count, - HashMap::from([("gpt-5.4".to_string(), 1)]) - ); + let (tooltip, shown_count) = + next_startup_tooltip_override(&config, &presets, /*is_first_run*/ false) + .expect("tooltip update"); - let reloaded = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .build() - .await - .expect("reloaded config"); - assert_eq!( - reloaded.model_availability_nux.shown_count, - HashMap::from([("gpt-5.4".to_string(), 1)]) - ); + assert_eq!(tooltip.message, "gpt-5.4 is available"); + assert_eq!(shown_count, HashMap::from([("gpt-5.4".to_string(), 1)])); } #[tokio::test] diff --git a/codex-rs/tui/src/config_update.rs b/codex-rs/tui/src/config_update.rs index c18ea44ce938..447db50660df 100644 --- a/codex-rs/tui/src/config_update.rs +++ b/codex-rs/tui/src/config_update.rs @@ -15,12 +15,14 @@ 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_utils_absolute_path::AbsolutePathBuf; use color_eyre::eyre::Result; use color_eyre::eyre::WrapErr; use serde_json::Value as JsonValue; +use std::path::Path; use uuid::Uuid; pub(crate) fn replace_config_value(key_path: impl Into, value: JsonValue) -> ConfigEdit { @@ -31,6 +33,14 @@ pub(crate) fn replace_config_value(key_path: impl Into, value: JsonValue } } +pub(crate) fn upsert_config_value(key_path: impl Into, value: JsonValue) -> ConfigEdit { + ConfigEdit { + key_path: key_path.into(), + value, + merge_strategy: MergeStrategy::Upsert, + } +} + pub(crate) fn clear_config_value(key_path: impl Into) -> ConfigEdit { replace_config_value(key_path, JsonValue::Null) } @@ -149,6 +159,57 @@ pub(crate) fn build_memory_settings_edits( ] } +pub(crate) async fn write_oss_provider( + request_handle: AppServerRequestHandle, + provider: String, +) -> Result<()> { + write_config_value(request_handle, "oss_provider", serde_json::json!(provider)).await +} + +pub(crate) async fn write_trusted_project( + request_handle: AppServerRequestHandle, + trust_target: &Path, +) -> Result<()> { + let mut project_update = serde_json::Map::new(); + project_update.insert( + project_trust_key(trust_target), + serde_json::json!({ "trust_level": "trusted" }), + ); + write_upsert_config_value( + request_handle, + "projects", + serde_json::Value::Object(project_update), + ) + .await + .wrap_err_with(|| { + format!( + "failed to persist trusted project {}", + trust_target.display() + ) + })?; + Ok(()) +} + +pub(crate) async fn write_config_value( + request_handle: AppServerRequestHandle, + key_path: impl Into, + value: JsonValue, +) -> Result<()> { + write_config_batch(request_handle, vec![replace_config_value(key_path, value)]) + .await + .map(|_| ()) +} + +pub(crate) async fn write_upsert_config_value( + request_handle: AppServerRequestHandle, + key_path: impl Into, + value: JsonValue, +) -> Result<()> { + write_config_batch(request_handle, vec![upsert_config_value(key_path, value)]) + .await + .map(|_| ()) +} + pub(crate) async fn write_config_batch( request_handle: AppServerRequestHandle, edits: Vec, diff --git a/codex-rs/tui/src/external_agent_config_migration_startup.rs b/codex-rs/tui/src/external_agent_config_migration_startup.rs index 3ec2ca390337..12d9c7dafdca 100644 --- a/codex-rs/tui/src/external_agent_config_migration_startup.rs +++ b/codex-rs/tui/src/external_agent_config_migration_startup.rs @@ -4,8 +4,6 @@ use crate::external_agent_config_migration::run_external_agent_config_migration_ use crate::legacy_core::config::Config; use crate::legacy_core::config::ConfigBuilder; use crate::legacy_core::config::ConfigOverrides; -use crate::legacy_core::config::edit::ConfigEdit; -use crate::legacy_core::config::edit::ConfigEditsBuilder; use crate::tui; use codex_app_server_protocol::ExternalAgentConfigDetectParams; use codex_app_server_protocol::ExternalAgentConfigMigrationItem; @@ -120,40 +118,43 @@ fn unix_seconds_now() -> i64 { } async fn persist_external_agent_config_migration_prompt_shown( + app_server: &mut AppServerSession, config: &mut Config, items: &[ExternalAgentConfigMigrationItem], now_unix_seconds: i64, ) -> Result<()> { - let mut edits = Vec::new(); + let mut prompt_update = serde_json::Map::new(); if items.iter().any(|item| item.cwd.is_none()) { - edits.push( - ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(now_unix_seconds), + prompt_update.insert( + "home_last_prompted_at".to_string(), + serde_json::json!(now_unix_seconds), ); } - for project in items + let project_updates = items .iter() .filter_map(|item| item.cwd.as_deref()) .map(external_config_migration_project_key) - { - edits.push( - ConfigEdit::SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt( - project, - now_unix_seconds, - ), + .map(|project| (project, serde_json::json!(now_unix_seconds))) + .collect::>(); + if !project_updates.is_empty() { + prompt_update.insert( + "project_last_prompted_at".to_string(), + serde_json::Value::Object(project_updates), ); } - if edits.is_empty() { + if prompt_update.is_empty() { return Ok(()); } - ConfigEditsBuilder::for_config(config) - .with_edits(edits) - .apply() - .await - .map_err(|err| color_eyre::eyre::eyre!("{err}")) - .wrap_err("Failed to save external config migration prompt timestamp")?; + crate::config_update::write_upsert_config_value( + app_server.request_handle(), + "notice.external_config_migration_prompts", + serde_json::Value::Object(prompt_update), + ) + .await + .wrap_err("Failed to save external config migration prompt timestamp")?; if items.iter().any(|item| item.cwd.is_none()) { config @@ -177,6 +178,7 @@ async fn persist_external_agent_config_migration_prompt_shown( } async fn persist_external_agent_config_migration_prompt_dismissal( + app_server: &mut AppServerSession, config: &mut Config, items: &[ExternalAgentConfigMigrationItem], ) -> Result<()> { @@ -187,7 +189,7 @@ async fn persist_external_agent_config_migration_prompt_dismissal( .map(external_config_migration_project_key) .collect::>(); - let mut edits = Vec::new(); + let mut prompt_update = serde_json::Map::new(); if hide_home && !config .notices @@ -195,10 +197,9 @@ async fn persist_external_agent_config_migration_prompt_dismissal( .home .unwrap_or(false) { - edits.push(ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome( - true, - )); + prompt_update.insert("home".to_string(), serde_json::json!(true)); } + let mut project_updates = serde_json::Map::new(); for project in &projects { if !config .notices @@ -208,25 +209,27 @@ async fn persist_external_agent_config_migration_prompt_dismissal( .copied() .unwrap_or(false) { - edits.push( - ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject( - project.clone(), - true, - ), - ); + project_updates.insert(project.clone(), serde_json::json!(true)); } } + if !project_updates.is_empty() { + prompt_update.insert( + "projects".to_string(), + serde_json::Value::Object(project_updates), + ); + } - if edits.is_empty() { + if prompt_update.is_empty() { return Ok(()); } - ConfigEditsBuilder::for_config(config) - .with_edits(edits) - .apply() - .await - .map_err(|err| color_eyre::eyre::eyre!("{err}")) - .wrap_err("Failed to save external config migration prompt preference")?; + crate::config_update::write_upsert_config_value( + app_server.request_handle(), + "notice.external_config_migration_prompts", + serde_json::Value::Object(prompt_update), + ) + .await + .wrap_err("Failed to save external config migration prompt preference")?; if hide_home { config.notices.external_config_migration_prompts.home = Some(true); @@ -286,6 +289,7 @@ pub(crate) async fn handle_external_agent_config_migration_prompt_if_needed( } if let Err(err) = persist_external_agent_config_migration_prompt_shown( + app_server, config, &detected_items, now_unix_seconds, @@ -345,6 +349,7 @@ pub(crate) async fn handle_external_agent_config_migration_prompt_if_needed( } ExternalAgentConfigMigrationOutcome::SkipForever => { match persist_external_agent_config_migration_prompt_dismissal( + app_server, config, &detected_items, ) diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 92f4e9971211..f47305889264 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -993,6 +993,7 @@ pub async fn run_main( ) .await; + let mut oss_provider_to_persist = None; let model_provider_override = if cli.oss { let resolved = resolve_oss_provider( cli.oss_provider.as_deref(), @@ -1004,12 +1005,15 @@ pub async fn run_main( Some(provider) } else { // No provider configured, prompt the user - let provider = oss_selection::select_oss_provider(&codex_home).await?; + let (provider, persist) = oss_selection::select_oss_provider().await?; if provider == "__CANCELLED__" { return Err(std::io::Error::other( "OSS provider selection was cancelled by user", )); } + if persist { + oss_provider_to_persist = Some(provider.clone()); + } Some(provider) } } else { @@ -1263,6 +1267,7 @@ pub async fn run_main( log_db, state_db, environment_manager, + oss_provider_to_persist, ) .await .map_err(|err| std::io::Error::other(err.to_string())) @@ -1284,6 +1289,7 @@ async fn run_ratatui_app( log_db: Option, state_db: Option, environment_manager: Arc, + oss_provider_to_persist: Option, ) -> color_eyre::Result { let uses_remote_workspace = app_server_target.uses_remote_workspace(); color_eyre::install()?; @@ -1357,6 +1363,16 @@ async fn run_ratatui_app( } .with_remote_cwd_override(remote_cwd_override.clone()); let mut app_server = Some(app_server_session); + if let Some(provider) = oss_provider_to_persist { + let Some(app_server) = app_server.as_ref() else { + unreachable!("app server should exist before OSS provider persistence"); + }; + if let Err(err) = + crate::config_update::write_oss_provider(app_server.request_handle(), provider).await + { + tracing::warn!(error = %err, "failed to persist OSS provider preference"); + } + } let should_show_trust_screen_flag = !uses_remote_workspace && should_show_trust_screen(&initial_config); @@ -1405,7 +1421,7 @@ async fn run_ratatui_app( exit_reason: ExitReason::UserRequested, }); } - trust_decision_was_made = onboarding_result.directory_trust_decision.is_some(); + trust_decision_was_made = onboarding_result.trusted_directory.is_some(); // If this onboarding run included the login step, always refresh cloud requirements and // rebuild config. This avoids missing newly available cloud requirements due to login // status detection edge cases. @@ -1421,7 +1437,7 @@ async fn run_ratatui_app( // If the user made an explicit trust decision, or we showed the login flow, reload config // so current process state reflects persisted trust/auth changes. - if onboarding_result.directory_trust_decision.is_some() + if onboarding_result.trusted_directory.is_some() || (show_login_screen && !uses_remote_workspace) { load_config_or_exit( diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index 6edb6a6d8600..1c48a8160cf7 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -47,6 +47,7 @@ use crate::tui::FrameRequester; use crate::tui::Tui; use crate::tui::TuiEvent; use color_eyre::eyre::Result; +use std::path::PathBuf; use std::sync::Arc; use std::sync::RwLock; @@ -89,7 +90,7 @@ pub(crate) struct OnboardingScreenArgs { } pub(crate) struct OnboardingResult { - pub directory_trust_decision: Option, + pub trusted_directory: Option, pub should_exit: bool, } @@ -111,7 +112,6 @@ impl OnboardingScreen { config, } = args; let cwd = config.cwd.to_path_buf(); - let codex_home = config.codex_home.to_path_buf(); let forced_login_method = config.forced_login_method; let mut steps: Vec = Vec::new(); steps.push(Step::Welcome(WelcomeWidget::new( @@ -154,7 +154,6 @@ impl OnboardingScreen { steps.push(Step::TrustDirectory(TrustDirectoryWidget { cwd, trust_target, - codex_home, show_windows_create_sandbox_hint, should_quit: false, selection: None, @@ -223,17 +222,30 @@ impl OnboardingScreen { .any(|step| matches!(step.get_step_state(), StepState::InProgress)) } - pub fn directory_trust_decision(&self) -> Option { - self.steps - .iter() - .find_map(|step| { - if let Step::TrustDirectory(TrustDirectoryWidget { selection, .. }) = step { - Some(*selection) - } else { - None - } - }) - .flatten() + fn trusted_directory_target(&self) -> Option { + self.steps.iter().find_map(|step| { + if let Step::TrustDirectory(TrustDirectoryWidget { + selection: Some(TrustDirectorySelection::Trust), + trust_target, + .. + }) = step + { + Some(trust_target.clone()) + } else { + None + } + }) + } + + fn set_trust_directory_error(&mut self, message: String) { + if let Some(Step::TrustDirectory(widget)) = self + .steps + .iter_mut() + .find(|step| matches!(step, Step::TrustDirectory(_))) + { + widget.selection = None; + widget.error = Some(message); + } } pub fn should_exit(&self) -> bool { @@ -493,7 +505,9 @@ pub(crate) async fn run_onboarding_app( ) -> Result { use tokio_stream::StreamExt; + let trust_request_handle = args.app_server_request_handle.clone(); let mut onboarding_screen = OnboardingScreen::new(tui, args).await; + let mut trusted_directory = None; // One-time guard to fully clear the screen after ChatGPT login success message is shown let mut did_full_clear_after_success = false; @@ -573,9 +587,46 @@ pub(crate) async fn run_onboarding_app( } } } + if trusted_directory.is_none() + && let Some(trust_target) = onboarding_screen.trusted_directory_target() + { + match trust_request_handle.clone() { + Some(request_handle) => { + if let Err(err) = crate::config_update::write_trusted_project( + request_handle, + trust_target.as_path(), + ) + .await + { + tracing::warn!( + error = %err, + project = %trust_target.display(), + "failed to persist trusted project state through app server" + ); + onboarding_screen.set_trust_directory_error(format!( + "Failed to set trust for {}: {err}", + trust_target.display() + )); + let _ = tui.draw(u16::MAX, |frame| { + frame.render_widget_ref(&onboarding_screen, frame.area()); + }); + } else { + trusted_directory = Some(trust_target); + } + } + None => { + onboarding_screen.set_trust_directory_error( + "Failed to set trust: app server is unavailable".to_string(), + ); + let _ = tui.draw(u16::MAX, |frame| { + frame.render_widget_ref(&onboarding_screen, frame.area()); + }); + } + } + } } Ok(OnboardingResult { - directory_trust_decision: onboarding_screen.directory_trust_decision(), + trusted_directory, should_exit: onboarding_screen.should_exit(), }) } diff --git a/codex-rs/tui/src/onboarding/trust_directory.rs b/codex-rs/tui/src/onboarding/trust_directory.rs index ffc0c040a97c..235fe9b885df 100644 --- a/codex-rs/tui/src/onboarding/trust_directory.rs +++ b/codex-rs/tui/src/onboarding/trust_directory.rs @@ -1,7 +1,5 @@ use std::path::PathBuf; -use crate::legacy_core::config::set_project_trust_level; -use codex_protocol::config_types::TrustLevel; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use ratatui::buffer::Buffer; @@ -24,7 +22,6 @@ use crate::selection_list::selection_option_row; use super::onboarding_screen::StepState; pub(crate) struct TrustDirectoryWidget { - pub codex_home: PathBuf, pub cwd: PathBuf, pub trust_target: PathBuf, pub show_windows_create_sandbox_hint: bool, @@ -166,12 +163,7 @@ impl StepStateProvider for TrustDirectoryWidget { impl TrustDirectoryWidget { fn handle_trust(&mut self) { - let target = self.trust_target.clone(); - if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) { - tracing::error!("Failed to set project trusted: {e:?}"); - self.error = Some(format!("Failed to set trust for {}: {e}", target.display())); - } - + self.error = None; self.selection = Some(TrustDirectorySelection::Trust); } @@ -197,13 +189,10 @@ mod tests { use pretty_assertions::assert_eq; use ratatui::Terminal; use std::path::PathBuf; - use tempfile::TempDir; #[test] fn release_event_does_not_change_selection() { - let codex_home = TempDir::new().expect("temp home"); let mut widget = TrustDirectoryWidget { - codex_home: codex_home.path().to_path_buf(), cwd: PathBuf::from("."), trust_target: PathBuf::from("."), show_windows_create_sandbox_hint: false, @@ -227,9 +216,7 @@ mod tests { #[test] fn renders_snapshot_for_git_repo() { - let codex_home = TempDir::new().expect("temp home"); let widget = TrustDirectoryWidget { - codex_home: codex_home.path().to_path_buf(), cwd: PathBuf::from("/workspace/project"), trust_target: PathBuf::from("/workspace/project"), show_windows_create_sandbox_hint: false, diff --git a/codex-rs/tui/src/oss_selection.rs b/codex-rs/tui/src/oss_selection.rs index ab9176ffbec7..5784ac9bced9 100644 --- a/codex-rs/tui/src/oss_selection.rs +++ b/codex-rs/tui/src/oss_selection.rs @@ -4,7 +4,6 @@ use std::sync::LazyLock; use crate::key_hint; use crate::key_hint::KeyBinding; use crate::key_hint::KeyBindingListExt; -use crate::legacy_core::config::set_default_oss_provider; use codex_model_provider_info::DEFAULT_LMSTUDIO_PORT; use codex_model_provider_info::DEFAULT_OLLAMA_PORT; use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; @@ -309,7 +308,7 @@ fn get_status_symbol_and_color(status: &ProviderStatus) -> (&'static str, Color) } } -pub async fn select_oss_provider(codex_home: &std::path::Path) -> io::Result { +pub async fn select_oss_provider() -> io::Result<(String, bool)> { // Check provider statuses first let lmstudio_status = check_lmstudio_status().await; let ollama_status = check_ollama_status().await; @@ -318,11 +317,11 @@ pub async fn select_oss_provider(codex_home: &std::path::Path) -> io::Result { let provider = LMSTUDIO_OSS_PROVIDER_ID.to_string(); - return Ok(provider); + return Ok((provider, /*persist*/ false)); } (ProviderStatus::NotRunning, ProviderStatus::Running) => { let provider = OLLAMA_OSS_PROVIDER_ID.to_string(); - return Ok(provider); + return Ok((provider, /*persist*/ false)); } _ => { // Both running or both not running - show UI @@ -353,15 +352,7 @@ pub async fn select_oss_provider(codex_home: &std::path::Path) -> io::Result ProviderStatus {