Files
codex/codex-rs/tui/src/app/thread_settings.rs
T
viyatb-oaiandGitHub 40ad7be2b5 core: refresh active permission profiles at runtime (#22931)
## Why

Once a named permission profile is selected, runtime state has to keep
that profile identity intact instead of collapsing back to anonymous
effective permissions. The session refresh path also needs to rebuild
profile-derived network proxy state so active profile switches take
effect consistently.

## What changed

- Preserve the active permission profile through session updates.
- Rebuild profile-derived runtime/network configuration when the active
profile changes.
- Keep the runtime path aligned with the current session configuration
APIs.
- Tighten the affected tests, including the Windows delete-pending
memory-file case that was intermittently tripping CI.

## Stack

1. **This PR**: runtime/session/network propagation for active
permission profiles.
2. [#23708](https://github.com/openai/codex/pull/23708): TUI selection
plumbing and guardrail flow.
3. [#21559](https://github.com/openai/codex/pull/21559): profile-aware
`/permissions` menu and custom profile display.

<img width="1296" height="906" alt="image"
src="https://github.com/user-attachments/assets/077fa3a7-80cb-4925-80b1-d2395018d90a"
/>
2026-05-20 21:55:21 +00:00

210 lines
7.6 KiB
Rust

//! Thread settings sync between TUI-local state and app-server thread state.
use super::App;
use crate::app_command::AppCommand;
use crate::app_server_session::AppServerSession;
use crate::session_state::ThreadSessionState;
use codex_app_server_protocol::ApprovalsReviewer as AppServerApprovalsReviewer;
use codex_app_server_protocol::ThreadSettings;
use codex_app_server_protocol::ThreadSettingsUpdateParams;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ModeKind;
use codex_protocol::models::PermissionProfile;
impl App {
pub(super) async fn sync_active_thread_model_setting(
&mut self,
app_server: &mut AppServerSession,
model: String,
) {
let Some(params) = self.active_thread_model_setting_update_params(model) else {
return;
};
self.send_thread_settings_update(app_server, params).await;
}
pub(super) fn active_thread_model_setting_update_params(
&self,
model: String,
) -> Option<ThreadSettingsUpdateParams> {
let thread_id = self.active_thread_id?;
Some(ThreadSettingsUpdateParams {
thread_id: thread_id.to_string(),
model: Some(model),
collaboration_mode: Some(self.chat_widget.effective_collaboration_mode()),
..ThreadSettingsUpdateParams::default()
})
}
pub(super) async fn sync_active_thread_reasoning_setting(
&mut self,
app_server: &mut AppServerSession,
effort: Option<codex_protocol::openai_models::ReasoningEffort>,
) {
let Some(params) = self.active_thread_reasoning_setting_update_params(effort) else {
return;
};
self.send_thread_settings_update(app_server, params).await;
}
pub(super) fn active_thread_reasoning_setting_update_params(
&self,
effort: Option<codex_protocol::openai_models::ReasoningEffort>,
) -> Option<ThreadSettingsUpdateParams> {
let thread_id = self.active_thread_id?;
Some(ThreadSettingsUpdateParams {
thread_id: thread_id.to_string(),
effort,
collaboration_mode: Some(self.chat_widget.current_collaboration_mode().clone()),
..ThreadSettingsUpdateParams::default()
})
}
pub(super) async fn sync_active_thread_plan_mode_reasoning_setting(
&mut self,
app_server: &mut AppServerSession,
) {
let Some(thread_id) = self.active_thread_id else {
return;
};
let params = ThreadSettingsUpdateParams {
thread_id: thread_id.to_string(),
collaboration_mode: Some(self.chat_widget.effective_collaboration_mode()),
..ThreadSettingsUpdateParams::default()
};
self.send_thread_settings_update(app_server, params).await;
}
pub(super) async fn sync_active_thread_personality_setting(
&mut self,
app_server: &mut AppServerSession,
personality: codex_protocol::config_types::Personality,
) {
let Some(thread_id) = self.active_thread_id else {
return;
};
let params = ThreadSettingsUpdateParams {
thread_id: thread_id.to_string(),
personality: Some(personality),
..ThreadSettingsUpdateParams::default()
};
self.send_thread_settings_update(app_server, params).await;
}
pub(super) async fn sync_override_turn_context_settings(
&mut self,
app_server: &mut AppServerSession,
thread_id: ThreadId,
op: &AppCommand,
) {
let AppCommand::OverrideTurnContext {
cwd,
approval_policy,
approvals_reviewer,
permission_profile: _,
active_permission_profile,
windows_sandbox_level: _,
model,
effort,
summary,
service_tier,
collaboration_mode,
personality,
} = op
else {
return;
};
let params = ThreadSettingsUpdateParams {
thread_id: thread_id.to_string(),
cwd: cwd.clone(),
approval_policy: *approval_policy,
approvals_reviewer: approvals_reviewer.map(AppServerApprovalsReviewer::from),
permissions: active_permission_profile
.as_ref()
.map(|profile| profile.id.clone()),
model: model.clone(),
effort: effort.unwrap_or_default(),
summary: *summary,
service_tier: service_tier.clone(),
collaboration_mode: collaboration_mode.clone(),
personality: *personality,
..ThreadSettingsUpdateParams::default()
};
self.send_thread_settings_update(app_server, params).await;
}
pub(super) async fn apply_thread_settings_to_cached_session(
&mut self,
thread_id: ThreadId,
settings: &ThreadSettings,
) {
if self.primary_thread_id == Some(thread_id)
&& let Some(session) = self.primary_session_configured.as_mut()
{
apply_thread_settings_to_session(session, settings);
}
if let Some(channel) = self.thread_event_channels.get(&thread_id) {
let mut store = channel.store.lock().await;
if let Some(session) = store.session.as_mut() {
apply_thread_settings_to_session(session, settings);
}
}
}
async fn send_thread_settings_update(
&mut self,
app_server: &mut AppServerSession,
params: ThreadSettingsUpdateParams,
) {
if !thread_settings_update_has_changes(&params) {
return;
}
if let Err(err) = app_server.thread_settings_update(params).await {
tracing::warn!("failed to update app-server thread settings from TUI: {err}");
self.chat_widget
.add_error_message(format!("Failed to update thread settings: {err}"));
}
}
}
fn apply_thread_settings_to_session(session: &mut ThreadSessionState, settings: &ThreadSettings) {
if settings.collaboration_mode.mode == ModeKind::Default {
session.model = settings.model.clone();
session.reasoning_effort = settings.effort;
}
session.model_provider_id = settings.model_provider.clone();
session.service_tier = settings.service_tier.clone();
session.approval_policy = settings.approval_policy;
session.approvals_reviewer = settings.approvals_reviewer.to_core();
session.permission_profile = PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&settings.sandbox_policy.to_core(),
settings.cwd.as_path(),
);
session.active_permission_profile = settings.active_permission_profile.clone().map(Into::into);
session.set_cwd_retargeting_implicit_runtime_workspace_root(settings.cwd.clone());
session.personality = settings.personality;
let mut collaboration_mode = settings.collaboration_mode.clone();
collaboration_mode
.settings
.model
.clone_from(&settings.model);
collaboration_mode.settings.reasoning_effort = settings.effort;
session.collaboration_mode = Some(Box::new(collaboration_mode));
}
fn thread_settings_update_has_changes(params: &ThreadSettingsUpdateParams) -> bool {
params.cwd.is_some()
|| params.approval_policy.is_some()
|| params.approvals_reviewer.is_some()
|| params.sandbox_policy.is_some()
|| params.permissions.is_some()
|| params.model.is_some()
|| params.service_tier.is_some()
|| params.effort.is_some()
|| params.summary.is_some()
|| params.collaboration_mode.is_some()
|| params.personality.is_some()
}