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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
75 changes: 49 additions & 26 deletions codex-rs/tui/src/app/config_persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<AskForApproval>,
approvals_reviewer: Option<ApprovalsReviewer>,
permission_profile: Option<PermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
) {
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,
Expand Down
42 changes: 26 additions & 16 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
27 changes: 20 additions & 7 deletions codex-rs/tui/src/app/startup_prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
) -> Option<(StartupTooltipOverride, HashMap<String, u32>)> {
if is_first_run || !config.show_tooltips {
return None;
}
Expand All @@ -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<String> {
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,
Expand Down
26 changes: 7 additions & 19 deletions codex-rs/tui/src/app/tests/model_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
61 changes: 61 additions & 0 deletions codex-rs/tui/src/config_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>, value: JsonValue) -> ConfigEdit {
Expand All @@ -31,6 +33,14 @@ pub(crate) fn replace_config_value(key_path: impl Into<String>, value: JsonValue
}
}

pub(crate) fn upsert_config_value(key_path: impl Into<String>, value: JsonValue) -> ConfigEdit {
ConfigEdit {
key_path: key_path.into(),
value,
merge_strategy: MergeStrategy::Upsert,
}
}

pub(crate) fn clear_config_value(key_path: impl Into<String>) -> ConfigEdit {
replace_config_value(key_path, JsonValue::Null)
}
Expand Down Expand Up @@ -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<String>,
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<String>,
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<ConfigEdit>,
Expand Down
Loading
Loading