mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
tui: persist startup and onboarding config via app server
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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,36 @@ 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.sync_active_thread_permission_settings_to_cached_session()
|
||||
.await;
|
||||
let op = AppCommand::override_turn_context(
|
||||
/*cwd*/ None,
|
||||
/*approval_policy*/ None,
|
||||
Some(ApprovalsReviewer::User),
|
||||
/*permission_profile*/ None,
|
||||
/*active_permission_profile*/ None,
|
||||
/*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;
|
||||
}
|
||||
}
|
||||
self.sync_auto_review_runtime_state_from_effective_config(
|
||||
&effective_config,
|
||||
&feature_updates_to_apply,
|
||||
@@ -537,6 +565,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;
|
||||
}
|
||||
@@ -910,7 +949,8 @@ impl App {
|
||||
return;
|
||||
}
|
||||
|
||||
self.runtime_permission_profile_override = Some(permission_profile);
|
||||
self.runtime_permission_profile_override =
|
||||
Some(RuntimePermissionProfileOverride::from_config(&self.config));
|
||||
self.sync_active_thread_permission_settings_to_cached_session()
|
||||
.await;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
let (tooltip, shown_count) =
|
||||
next_startup_tooltip_override(&config, &presets, /*is_first_run*/ false)
|
||||
.expect("tooltip update");
|
||||
|
||||
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 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]
|
||||
|
||||
@@ -15,12 +15,16 @@ 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_exec_server::LOCAL_FS;
|
||||
use codex_features::FEATURES;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
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::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) fn replace_config_value(key_path: impl Into<String>, value: JsonValue) -> ConfigEdit {
|
||||
@@ -31,6 +35,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)
|
||||
}
|
||||
@@ -149,6 +161,61 @@ 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,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> Result<PathBuf> {
|
||||
let trust_target = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), cwd)
|
||||
.await
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(|| cwd.to_path_buf());
|
||||
let mut project_update = serde_json::Map::new();
|
||||
project_update.insert(
|
||||
project_trust_key(trust_target.as_path()),
|
||||
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(trust_target)
|
||||
}
|
||||
|
||||
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>,
|
||||
|
||||
@@ -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::<serde_json::Map<_, _>>();
|
||||
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::<BTreeSet<_>>();
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
+36
-1
@@ -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<log_db::LogDbLayer>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
oss_provider_to_persist: Option<String>,
|
||||
) -> color_eyre::Result<AppExitInfo> {
|
||||
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,6 +1421,25 @@ async fn run_ratatui_app(
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
});
|
||||
}
|
||||
if matches!(
|
||||
onboarding_result.directory_trust_decision,
|
||||
Some(crate::onboarding::TrustDirectorySelection::Trust)
|
||||
) {
|
||||
let Some(app_server) = app_server.as_ref() else {
|
||||
unreachable!("app server should exist while onboarding is active");
|
||||
};
|
||||
if let Err(err) = crate::config_update::write_trusted_project(
|
||||
app_server.request_handle(),
|
||||
&initial_config.cwd,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"failed to persist trusted project state through app server"
|
||||
);
|
||||
}
|
||||
}
|
||||
trust_decision_was_made = onboarding_result.directory_trust_decision.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
|
||||
|
||||
@@ -4,4 +4,5 @@ pub(crate) mod onboarding_screen;
|
||||
mod trust_directory;
|
||||
pub(crate) use auth::mark_underlined_hyperlink;
|
||||
pub(crate) use auth::mark_url_hyperlink;
|
||||
pub(crate) use trust_directory::TrustDirectorySelection;
|
||||
mod welcome;
|
||||
|
||||
@@ -111,7 +111,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<Step> = Vec::new();
|
||||
steps.push(Step::Welcome(WelcomeWidget::new(
|
||||
@@ -154,7 +153,6 @@ impl OnboardingScreen {
|
||||
steps.push(Step::TrustDirectory(TrustDirectoryWidget {
|
||||
cwd,
|
||||
trust_target,
|
||||
codex_home,
|
||||
show_windows_create_sandbox_hint,
|
||||
should_quit: false,
|
||||
selection: None,
|
||||
|
||||
@@ -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,6 @@ 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.selection = Some(TrustDirectorySelection::Trust);
|
||||
}
|
||||
|
||||
@@ -197,13 +188,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 +215,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,
|
||||
|
||||
@@ -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<String> {
|
||||
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<Str
|
||||
match (&lmstudio_status, &ollama_status) {
|
||||
(ProviderStatus::Running, ProviderStatus::NotRunning) => {
|
||||
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<Str
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
|
||||
// If the user manually selected an OSS provider, we save it as the
|
||||
// default one to use later.
|
||||
if let Ok(ref provider) = result
|
||||
&& let Err(e) = set_default_oss_provider(codex_home, provider)
|
||||
{
|
||||
tracing::warn!("Failed to save OSS provider preference: {e}");
|
||||
}
|
||||
|
||||
result
|
||||
result.map(|provider| (provider, /*persist*/ true))
|
||||
}
|
||||
|
||||
async fn check_lmstudio_status() -> ProviderStatus {
|
||||
|
||||
Reference in New Issue
Block a user