mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex] remove blocking external agent migration flow (#27064)
## Why External-agent import should be initiated deliberately instead of interrupting eligible TUI startups. This cleanup removes the blocking startup flow before the replacement import experience is introduced later in the stack. ## What changed - remove the startup-blocking external-agent migration prompt - remove the now-unused external migration feature gate - remove the obsolete TUI app-server migration wrappers - retain the dormant picker behind a module-scoped dead-code allowance until the next stack item wires it back in - keep normal TUI startup focused on entering Codex immediately ## Validation - `bazel build --config=clippy //codex-rs/tui:tui //codex-rs/tui:tui-unit-tests-bin` - `just test -p codex-tui external_agent_config_migration` (8 passed) - `just test -p codex-tui` (2,786 passed, 12 unrelated local environment-sensitive failures, 4 skipped) - `just fix -p codex-tui` - `just fmt` ## Stack 1. [#27064](https://github.com/openai/codex/pull/27064): remove the startup migration flow 2. [#27065](https://github.com/openai/codex/pull/27065): extract the picker renderer 3. [#27070](https://github.com/openai/codex/pull/27070): add the external-agent import picker UX 4. [#27071](https://github.com/openai/codex/pull/27071): expose the flow through `/import` **This PR is stack item 1.**
This commit is contained in:
1 parent
3691fe5b76
commit
636cc11398
6 files changed
+18
-682
No files matched your search
@@ -177,7 +177,7 @@ pub enum Feature {
|
|||||||
RemotePlugin,
|
RemotePlugin,
|
||||||
/// Enable remote plugin sharing flows.
|
/// Enable remote plugin sharing flows.
|
||||||
PluginSharing,
|
PluginSharing,
|
||||||
/// Show the startup prompt for migrating external agent config into Codex.
|
/// Removed compatibility flag retained as a no-op.
|
||||||
ExternalMigration,
|
ExternalMigration,
|
||||||
/// Allow the model to invoke the built-in image generation tool.
|
/// Allow the model to invoke the built-in image generation tool.
|
||||||
ImageGeneration,
|
ImageGeneration,
|
||||||
@@ -1071,11 +1071,7 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||||||
FeatureSpec {
|
FeatureSpec {
|
||||||
id: Feature::ExternalMigration,
|
id: Feature::ExternalMigration,
|
||||||
key: "external_migration",
|
key: "external_migration",
|
||||||
stage: Stage::Experimental {
|
stage: Stage::Removed,
|
||||||
name: "External migration",
|
|
||||||
menu_description: "Show a startup prompt when Codex detects migratable external agent config for this machine or project.",
|
|
||||||
announcement: "",
|
|
||||||
},
|
|
||||||
default_enabled: false,
|
default_enabled: false,
|
||||||
},
|
},
|
||||||
FeatureSpec {
|
FeatureSpec {
|
||||||
|
|||||||
@@ -83,6 +83,16 @@ fn plugin_hooks_is_removed_and_disabled_by_default() {
|
|||||||
assert_eq!(feature_for_key("plugin_hooks"), Some(Feature::PluginHooks));
|
assert_eq!(feature_for_key("plugin_hooks"), Some(Feature::PluginHooks));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_migration_is_removed_and_disabled_by_default() {
|
||||||
|
assert_eq!(Feature::ExternalMigration.stage(), Stage::Removed);
|
||||||
|
assert_eq!(Feature::ExternalMigration.default_enabled(), false);
|
||||||
|
assert_eq!(
|
||||||
|
feature_for_key("external_migration"),
|
||||||
|
Some(Feature::ExternalMigration)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn removed_apps_mcp_path_override_shapes_are_ignored() {
|
fn removed_apps_mcp_path_override_shapes_are_ignored() {
|
||||||
let features = [
|
let features = [
|
||||||
@@ -122,23 +132,6 @@ fn guardian_approval_is_stable_and_enabled_by_default() {
|
|||||||
assert_eq!(Feature::GuardianApproval.default_enabled(), true);
|
assert_eq!(Feature::GuardianApproval.default_enabled(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn external_migration_is_experimental_and_disabled_by_default() {
|
|
||||||
let spec = Feature::ExternalMigration.info();
|
|
||||||
let stage = spec.stage;
|
|
||||||
|
|
||||||
assert!(matches!(stage, Stage::Experimental { .. }));
|
|
||||||
assert_eq!(stage.experimental_menu_name(), Some("External migration"));
|
|
||||||
assert_eq!(
|
|
||||||
stage.experimental_menu_description(),
|
|
||||||
Some(
|
|
||||||
"Show a startup prompt when Codex detects migratable external agent config for this machine or project."
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(stage.experimental_announcement(), None);
|
|
||||||
assert_eq!(Feature::ExternalMigration.default_enabled(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn request_permissions_is_under_development() {
|
fn request_permissions_is_under_development() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ use crate::cwd_prompt::CwdPromptAction;
|
|||||||
use crate::diff_render::DiffSummary;
|
use crate::diff_render::DiffSummary;
|
||||||
use crate::exec_command::split_command_string;
|
use crate::exec_command::split_command_string;
|
||||||
use crate::exec_command::strip_bash_lc_and_escape;
|
use crate::exec_command::strip_bash_lc_and_escape;
|
||||||
use crate::external_agent_config_migration_startup::ExternalAgentConfigMigrationStartupOutcome;
|
|
||||||
use crate::external_agent_config_migration_startup::handle_external_agent_config_migration_prompt_if_needed;
|
|
||||||
use crate::external_editor;
|
use crate::external_editor;
|
||||||
use crate::file_search::FileSearchManager;
|
use crate::file_search::FileSearchManager;
|
||||||
use crate::history_cell;
|
use crate::history_cell;
|
||||||
@@ -764,7 +762,6 @@ impl App {
|
|||||||
session_selection: SessionSelection,
|
session_selection: SessionSelection,
|
||||||
feedback: codex_feedback::CodexFeedback,
|
feedback: codex_feedback::CodexFeedback,
|
||||||
is_first_run: bool,
|
is_first_run: bool,
|
||||||
entered_trust_nux: bool,
|
|
||||||
should_prompt_windows_sandbox_nux_at_startup: bool,
|
should_prompt_windows_sandbox_nux_at_startup: bool,
|
||||||
app_server_target: AppServerTarget,
|
app_server_target: AppServerTarget,
|
||||||
state_db: Option<StateDbHandle>,
|
state_db: Option<StateDbHandle>,
|
||||||
@@ -786,39 +783,6 @@ impl App {
|
|||||||
|
|
||||||
let harness_overrides =
|
let harness_overrides =
|
||||||
normalize_harness_overrides_for_cwd(harness_overrides, &config.cwd)?;
|
normalize_harness_overrides_for_cwd(harness_overrides, &config.cwd)?;
|
||||||
let external_agent_config_migration_outcome =
|
|
||||||
handle_external_agent_config_migration_prompt_if_needed(
|
|
||||||
tui,
|
|
||||||
&mut app_server,
|
|
||||||
&mut config,
|
|
||||||
&cli_kv_overrides,
|
|
||||||
&harness_overrides,
|
|
||||||
&cloud_config_bundle,
|
|
||||||
entered_trust_nux,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let external_agent_config_migration_message = match external_agent_config_migration_outcome
|
|
||||||
{
|
|
||||||
ExternalAgentConfigMigrationStartupOutcome::Continue { success_message } => {
|
|
||||||
success_message
|
|
||||||
}
|
|
||||||
ExternalAgentConfigMigrationStartupOutcome::ExitRequested => {
|
|
||||||
app_server
|
|
||||||
.shutdown()
|
|
||||||
.await
|
|
||||||
.inspect_err(|err| {
|
|
||||||
tracing::warn!("app-server shutdown failed: {err}");
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
return Ok(AppExitInfo {
|
|
||||||
token_usage: TokenUsage::default(),
|
|
||||||
thread_id: None,
|
|
||||||
thread_name: None,
|
|
||||||
update_action: None,
|
|
||||||
exit_reason: ExitReason::UserRequested,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let bootstrap = match startup_bootstrap {
|
let bootstrap = match startup_bootstrap {
|
||||||
Some(bootstrap) => bootstrap,
|
Some(bootstrap) => bootstrap,
|
||||||
None => app_server.bootstrap(&config).await?,
|
None => app_server.bootstrap(&config).await?,
|
||||||
@@ -1014,10 +978,6 @@ impl App {
|
|||||||
};
|
};
|
||||||
chat_widget.remote_connection = remote_connection;
|
chat_widget.remote_connection = remote_connection;
|
||||||
let thread_and_widget_ms = thread_and_widget_started_at.elapsed().as_millis();
|
let thread_and_widget_ms = thread_and_widget_started_at.elapsed().as_millis();
|
||||||
if let Some(message) = external_agent_config_migration_message {
|
|
||||||
chat_widget.add_info_message(message, /*hint*/ None);
|
|
||||||
}
|
|
||||||
|
|
||||||
chat_widget
|
chat_widget
|
||||||
.maybe_prompt_windows_sandbox_enable(should_prompt_windows_sandbox_nux_at_startup);
|
.maybe_prompt_windows_sandbox_enable(should_prompt_windows_sandbox_nux_at_startup);
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,6 @@ use codex_app_server_protocol::AuthMode;
|
|||||||
use codex_app_server_protocol::ClientRequest;
|
use codex_app_server_protocol::ClientRequest;
|
||||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||||
use codex_app_server_protocol::ConfigWriteResponse;
|
use codex_app_server_protocol::ConfigWriteResponse;
|
||||||
use codex_app_server_protocol::ExternalAgentConfigDetectParams;
|
|
||||||
use codex_app_server_protocol::ExternalAgentConfigDetectResponse;
|
|
||||||
use codex_app_server_protocol::ExternalAgentConfigImportParams;
|
|
||||||
use codex_app_server_protocol::ExternalAgentConfigImportResponse;
|
|
||||||
use codex_app_server_protocol::ExternalAgentConfigMigrationItem;
|
|
||||||
use codex_app_server_protocol::GetAccountParams;
|
use codex_app_server_protocol::GetAccountParams;
|
||||||
use codex_app_server_protocol::GetAccountRateLimitsResponse;
|
use codex_app_server_protocol::GetAccountRateLimitsResponse;
|
||||||
use codex_app_server_protocol::GetAccountResponse;
|
use codex_app_server_protocol::GetAccountResponse;
|
||||||
@@ -349,31 +344,6 @@ impl AppServerSession {
|
|||||||
.map_err(|err| bootstrap_request_error("account/read failed during TUI bootstrap", err))
|
.map_err(|err| bootstrap_request_error("account/read failed during TUI bootstrap", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn external_agent_config_detect(
|
|
||||||
&mut self,
|
|
||||||
params: ExternalAgentConfigDetectParams,
|
|
||||||
) -> Result<ExternalAgentConfigDetectResponse> {
|
|
||||||
let request_id = self.next_request_id();
|
|
||||||
self.client
|
|
||||||
.request_typed(ClientRequest::ExternalAgentConfigDetect { request_id, params })
|
|
||||||
.await
|
|
||||||
.wrap_err("externalAgentConfig/detect failed during TUI startup")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn external_agent_config_import(
|
|
||||||
&mut self,
|
|
||||||
migration_items: Vec<ExternalAgentConfigMigrationItem>,
|
|
||||||
) -> Result<ExternalAgentConfigImportResponse> {
|
|
||||||
let request_id = self.next_request_id();
|
|
||||||
self.client
|
|
||||||
.request_typed(ClientRequest::ExternalAgentConfigImport {
|
|
||||||
request_id,
|
|
||||||
params: ExternalAgentConfigImportParams { migration_items },
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.wrap_err("externalAgentConfig/import failed during TUI startup")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn next_event(&mut self) -> Option<AppServerEvent> {
|
pub(crate) async fn next_event(&mut self) -> Option<AppServerEvent> {
|
||||||
self.client.next_event().await
|
self.client.next_event().await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,570 +0,0 @@
|
|||||||
use crate::app_server_session::AppServerSession;
|
|
||||||
use crate::external_agent_config_migration::ExternalAgentConfigMigrationOutcome;
|
|
||||||
use crate::external_agent_config_migration::run_external_agent_config_migration_prompt;
|
|
||||||
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;
|
|
||||||
use codex_config::CloudConfigBundleLoader;
|
|
||||||
use codex_features::Feature;
|
|
||||||
use color_eyre::eyre::Result;
|
|
||||||
use color_eyre::eyre::WrapErr;
|
|
||||||
use std::collections::BTreeSet;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::time::SystemTime;
|
|
||||||
use std::time::UNIX_EPOCH;
|
|
||||||
use toml::Value as TomlValue;
|
|
||||||
|
|
||||||
const EXTERNAL_CONFIG_MIGRATION_PROMPT_COOLDOWN_SECS: i64 = 5 * 24 * 60 * 60;
|
|
||||||
|
|
||||||
pub(crate) enum ExternalAgentConfigMigrationStartupOutcome {
|
|
||||||
Continue { success_message: Option<String> },
|
|
||||||
ExitRequested,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn should_show_external_agent_config_migration_prompt(
|
|
||||||
config: &Config,
|
|
||||||
entered_trust_nux: bool,
|
|
||||||
) -> bool {
|
|
||||||
entered_trust_nux && config.features.enabled(Feature::ExternalMigration)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn external_config_migration_project_key(path: &Path) -> String {
|
|
||||||
path.display().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_external_config_migration_scope_hidden(config: &Config, cwd: Option<&Path>) -> bool {
|
|
||||||
match cwd {
|
|
||||||
Some(cwd) => config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.projects
|
|
||||||
.get(&external_config_migration_project_key(cwd))
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(false),
|
|
||||||
None => config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.home
|
|
||||||
.unwrap_or(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn external_config_migration_last_prompted_at(config: &Config, cwd: Option<&Path>) -> Option<i64> {
|
|
||||||
match cwd {
|
|
||||||
Some(cwd) => config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.project_last_prompted_at
|
|
||||||
.get(&external_config_migration_project_key(cwd))
|
|
||||||
.copied(),
|
|
||||||
None => {
|
|
||||||
config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.home_last_prompted_at
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_external_config_migration_scope_cooling_down(
|
|
||||||
config: &Config,
|
|
||||||
cwd: Option<&Path>,
|
|
||||||
now_unix_seconds: i64,
|
|
||||||
) -> bool {
|
|
||||||
external_config_migration_last_prompted_at(config, cwd).is_some_and(|last_prompted_at| {
|
|
||||||
last_prompted_at.saturating_add(EXTERNAL_CONFIG_MIGRATION_PROMPT_COOLDOWN_SECS)
|
|
||||||
> now_unix_seconds
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visible_external_agent_config_migration_items(
|
|
||||||
config: &Config,
|
|
||||||
items: Vec<ExternalAgentConfigMigrationItem>,
|
|
||||||
now_unix_seconds: i64,
|
|
||||||
) -> Vec<ExternalAgentConfigMigrationItem> {
|
|
||||||
items
|
|
||||||
.into_iter()
|
|
||||||
.filter(|item| {
|
|
||||||
!is_external_config_migration_scope_hidden(config, item.cwd.as_deref())
|
|
||||||
&& !is_external_config_migration_scope_cooling_down(
|
|
||||||
config,
|
|
||||||
item.cwd.as_deref(),
|
|
||||||
now_unix_seconds,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn external_agent_config_migration_success_message(
|
|
||||||
items: &[ExternalAgentConfigMigrationItem],
|
|
||||||
) -> String {
|
|
||||||
if items.iter().any(|item| {
|
|
||||||
item.item_type == codex_app_server_protocol::ExternalAgentConfigMigrationItemType::Plugins
|
|
||||||
}) {
|
|
||||||
"External config migration completed. Plugin migration is still in progress and may take a few minutes."
|
|
||||||
.to_string()
|
|
||||||
} else {
|
|
||||||
"External config migration completed successfully.".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unix_seconds_now() -> i64 {
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs() as i64
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn persist_external_agent_config_migration_prompt_shown(
|
|
||||||
config: &mut Config,
|
|
||||||
items: &[ExternalAgentConfigMigrationItem],
|
|
||||||
now_unix_seconds: i64,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut edits = Vec::new();
|
|
||||||
if items.iter().any(|item| item.cwd.is_none()) {
|
|
||||||
edits.push(
|
|
||||||
ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(now_unix_seconds),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for project in items
|
|
||||||
.iter()
|
|
||||||
.filter_map(|item| item.cwd.as_deref())
|
|
||||||
.map(external_config_migration_project_key)
|
|
||||||
{
|
|
||||||
edits.push(
|
|
||||||
ConfigEdit::SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt(
|
|
||||||
project,
|
|
||||||
now_unix_seconds,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if edits.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")?;
|
|
||||||
|
|
||||||
if items.iter().any(|item| item.cwd.is_none()) {
|
|
||||||
config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.home_last_prompted_at = Some(now_unix_seconds);
|
|
||||||
}
|
|
||||||
for project in items
|
|
||||||
.iter()
|
|
||||||
.filter_map(|item| item.cwd.as_deref())
|
|
||||||
.map(external_config_migration_project_key)
|
|
||||||
{
|
|
||||||
config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.project_last_prompted_at
|
|
||||||
.insert(project, now_unix_seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn persist_external_agent_config_migration_prompt_dismissal(
|
|
||||||
config: &mut Config,
|
|
||||||
items: &[ExternalAgentConfigMigrationItem],
|
|
||||||
) -> Result<()> {
|
|
||||||
let hide_home = items.iter().any(|item| item.cwd.is_none());
|
|
||||||
let projects = items
|
|
||||||
.iter()
|
|
||||||
.filter_map(|item| item.cwd.as_deref())
|
|
||||||
.map(external_config_migration_project_key)
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
|
|
||||||
let mut edits = Vec::new();
|
|
||||||
if hide_home
|
|
||||||
&& !config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.home
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
edits.push(ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome(
|
|
||||||
true,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
for project in &projects {
|
|
||||||
if !config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.projects
|
|
||||||
.get(project)
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
edits.push(
|
|
||||||
ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject(
|
|
||||||
project.clone(),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if edits.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")?;
|
|
||||||
|
|
||||||
if hide_home {
|
|
||||||
config.notices.external_config_migration_prompts.home = Some(true);
|
|
||||||
}
|
|
||||||
for project in projects {
|
|
||||||
config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.projects
|
|
||||||
.insert(project, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn handle_external_agent_config_migration_prompt_if_needed(
|
|
||||||
tui: &mut tui::Tui,
|
|
||||||
app_server: &mut AppServerSession,
|
|
||||||
config: &mut Config,
|
|
||||||
cli_kv_overrides: &[(String, TomlValue)],
|
|
||||||
harness_overrides: &ConfigOverrides,
|
|
||||||
cloud_config_bundle: &CloudConfigBundleLoader,
|
|
||||||
entered_trust_nux: bool,
|
|
||||||
) -> Result<ExternalAgentConfigMigrationStartupOutcome> {
|
|
||||||
if !should_show_external_agent_config_migration_prompt(config, entered_trust_nux) {
|
|
||||||
return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue {
|
|
||||||
success_message: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let now_unix_seconds = unix_seconds_now();
|
|
||||||
let detected_items = match app_server
|
|
||||||
.external_agent_config_detect(ExternalAgentConfigDetectParams {
|
|
||||||
include_home: true,
|
|
||||||
cwds: Some(vec![config.cwd.to_path_buf()]),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(response) => {
|
|
||||||
visible_external_agent_config_migration_items(config, response.items, now_unix_seconds)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!(
|
|
||||||
error = %err,
|
|
||||||
cwd = %config.cwd.display(),
|
|
||||||
"failed to detect external agent config migrations; continuing startup"
|
|
||||||
);
|
|
||||||
return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue {
|
|
||||||
success_message: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if detected_items.is_empty() {
|
|
||||||
return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue {
|
|
||||||
success_message: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(err) = persist_external_agent_config_migration_prompt_shown(
|
|
||||||
config,
|
|
||||||
&detected_items,
|
|
||||||
now_unix_seconds,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::warn!(
|
|
||||||
error = %err,
|
|
||||||
cwd = %config.cwd.display(),
|
|
||||||
"failed to persist external config migration prompt timestamp"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut selected_items = detected_items.clone();
|
|
||||||
let mut error: Option<String> = None;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match run_external_agent_config_migration_prompt(
|
|
||||||
tui,
|
|
||||||
&detected_items,
|
|
||||||
&selected_items,
|
|
||||||
error.as_deref(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
ExternalAgentConfigMigrationOutcome::Proceed(items) => {
|
|
||||||
selected_items = items.clone();
|
|
||||||
match app_server.external_agent_config_import(items).await {
|
|
||||||
Ok(_) => {
|
|
||||||
let success_message =
|
|
||||||
external_agent_config_migration_success_message(&selected_items);
|
|
||||||
*config = ConfigBuilder::default()
|
|
||||||
.codex_home(config.codex_home.to_path_buf())
|
|
||||||
.cli_overrides(cli_kv_overrides.to_vec())
|
|
||||||
.harness_overrides(harness_overrides.clone())
|
|
||||||
.cloud_config_bundle(cloud_config_bundle.clone())
|
|
||||||
.build()
|
|
||||||
.await
|
|
||||||
.wrap_err("Failed to reload config after external agent migration")?;
|
|
||||||
return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue {
|
|
||||||
success_message: Some(success_message),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!(
|
|
||||||
error = %err,
|
|
||||||
cwd = %config.cwd.display(),
|
|
||||||
"failed to import external agent config migration items"
|
|
||||||
);
|
|
||||||
error = Some(format!("Migration failed: {err}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ExternalAgentConfigMigrationOutcome::Skip => {
|
|
||||||
return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue {
|
|
||||||
success_message: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ExternalAgentConfigMigrationOutcome::SkipForever => {
|
|
||||||
match persist_external_agent_config_migration_prompt_dismissal(
|
|
||||||
config,
|
|
||||||
&detected_items,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(()) => {
|
|
||||||
return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue {
|
|
||||||
success_message: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!(
|
|
||||||
error = %err,
|
|
||||||
cwd = %config.cwd.display(),
|
|
||||||
"failed to persist external config migration prompt dismissal"
|
|
||||||
);
|
|
||||||
error = Some(format!("Failed to save preference: {err}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ExternalAgentConfigMigrationOutcome::Exit => {
|
|
||||||
return Ok(ExternalAgentConfigMigrationStartupOutcome::ExitRequested);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn visible_external_agent_config_migration_items_omits_hidden_scopes() {
|
|
||||||
let codex_home = tempdir().expect("temp codex home");
|
|
||||||
let mut config = ConfigBuilder::default()
|
|
||||||
.codex_home(codex_home.path().to_path_buf())
|
|
||||||
.build()
|
|
||||||
.await
|
|
||||||
.expect("config");
|
|
||||||
config.notices.external_config_migration_prompts.home = Some(true);
|
|
||||||
config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.projects
|
|
||||||
.insert("/tmp/project".to_string(), true);
|
|
||||||
|
|
||||||
let visible = visible_external_agent_config_migration_items(
|
|
||||||
&config,
|
|
||||||
vec![
|
|
||||||
ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Config,
|
|
||||||
description: "home".to_string(),
|
|
||||||
cwd: None,
|
|
||||||
details: None,
|
|
||||||
},
|
|
||||||
ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
|
||||||
description: "project".to_string(),
|
|
||||||
cwd: Some(PathBuf::from("/tmp/project")),
|
|
||||||
details: None,
|
|
||||||
},
|
|
||||||
ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Skills,
|
|
||||||
description: "other project".to_string(),
|
|
||||||
cwd: Some(PathBuf::from("/tmp/other")),
|
|
||||||
details: None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
/*now_unix_seconds*/ 1_760_000_000,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
visible,
|
|
||||||
vec![ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Skills,
|
|
||||||
description: "other project".to_string(),
|
|
||||||
cwd: Some(PathBuf::from("/tmp/other")),
|
|
||||||
details: None,
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn visible_external_agent_config_migration_items_omits_recently_prompted_scopes() {
|
|
||||||
let codex_home = tempdir().expect("temp codex home");
|
|
||||||
let mut config = ConfigBuilder::default()
|
|
||||||
.codex_home(codex_home.path().to_path_buf())
|
|
||||||
.build()
|
|
||||||
.await
|
|
||||||
.expect("config");
|
|
||||||
config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.home_last_prompted_at = Some(1_760_000_000);
|
|
||||||
config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.project_last_prompted_at
|
|
||||||
.insert("/tmp/project".to_string(), 1_760_000_000);
|
|
||||||
|
|
||||||
let visible = visible_external_agent_config_migration_items(
|
|
||||||
&config,
|
|
||||||
vec![
|
|
||||||
ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Config,
|
|
||||||
description: "home".to_string(),
|
|
||||||
cwd: None,
|
|
||||||
details: None,
|
|
||||||
},
|
|
||||||
ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
|
||||||
description: "project".to_string(),
|
|
||||||
cwd: Some(PathBuf::from("/tmp/project")),
|
|
||||||
details: None,
|
|
||||||
},
|
|
||||||
ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Skills,
|
|
||||||
description: "other project".to_string(),
|
|
||||||
cwd: Some(PathBuf::from("/tmp/other")),
|
|
||||||
details: None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
/*now_unix_seconds*/
|
|
||||||
1_760_000_000 + EXTERNAL_CONFIG_MIGRATION_PROMPT_COOLDOWN_SECS - 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
visible,
|
|
||||||
vec![ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Skills,
|
|
||||||
description: "other project".to_string(),
|
|
||||||
cwd: Some(PathBuf::from("/tmp/other")),
|
|
||||||
details: None,
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn external_config_migration_scope_cooldown_expires_after_five_days() {
|
|
||||||
let codex_home = tempdir().expect("temp codex home");
|
|
||||||
let mut config = ConfigBuilder::default()
|
|
||||||
.codex_home(codex_home.path().to_path_buf())
|
|
||||||
.build()
|
|
||||||
.await
|
|
||||||
.expect("config");
|
|
||||||
config
|
|
||||||
.notices
|
|
||||||
.external_config_migration_prompts
|
|
||||||
.home_last_prompted_at = Some(1_760_000_000);
|
|
||||||
|
|
||||||
assert!(is_external_config_migration_scope_cooling_down(
|
|
||||||
&config,
|
|
||||||
/*cwd*/ None,
|
|
||||||
1_760_000_000 + EXTERNAL_CONFIG_MIGRATION_PROMPT_COOLDOWN_SECS - 1,
|
|
||||||
));
|
|
||||||
assert!(!is_external_config_migration_scope_cooling_down(
|
|
||||||
&config,
|
|
||||||
/*cwd*/ None,
|
|
||||||
1_760_000_000 + EXTERNAL_CONFIG_MIGRATION_PROMPT_COOLDOWN_SECS,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn external_agent_config_migration_success_message_mentions_plugins_when_present() {
|
|
||||||
let message = external_agent_config_migration_success_message(&[
|
|
||||||
ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Config,
|
|
||||||
description: String::new(),
|
|
||||||
cwd: None,
|
|
||||||
details: None,
|
|
||||||
},
|
|
||||||
ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
|
||||||
description: String::new(),
|
|
||||||
cwd: None,
|
|
||||||
details: None,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
message,
|
|
||||||
"External config migration completed. Plugin migration is still in progress and may take a few minutes."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn external_agent_config_migration_success_message_omits_plugins_copy_when_absent() {
|
|
||||||
let message =
|
|
||||||
external_agent_config_migration_success_message(&[ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
|
||||||
description: String::new(),
|
|
||||||
cwd: None,
|
|
||||||
details: None,
|
|
||||||
}]);
|
|
||||||
|
|
||||||
assert_eq!(message, "External config migration completed successfully.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn external_agent_config_migration_prompt_requires_trust_nux_entry() {
|
|
||||||
let codex_home = tempdir().expect("temp codex home");
|
|
||||||
let mut config = ConfigBuilder::default()
|
|
||||||
.codex_home(codex_home.path().to_path_buf())
|
|
||||||
.build()
|
|
||||||
.await
|
|
||||||
.expect("config");
|
|
||||||
let _ = config.features.enable(Feature::ExternalMigration);
|
|
||||||
|
|
||||||
assert!(!should_show_external_agent_config_migration_prompt(
|
|
||||||
&config, /*entered_trust_nux*/ false,
|
|
||||||
));
|
|
||||||
assert!(should_show_external_agent_config_migration_prompt(
|
|
||||||
&config, /*entered_trust_nux*/ true,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+6
-19
@@ -130,8 +130,8 @@ mod diff_model;
|
|||||||
mod diff_render;
|
mod diff_render;
|
||||||
mod exec_cell;
|
mod exec_cell;
|
||||||
mod exec_command;
|
mod exec_command;
|
||||||
|
#[allow(dead_code)]
|
||||||
mod external_agent_config_migration;
|
mod external_agent_config_migration;
|
||||||
mod external_agent_config_migration_startup;
|
|
||||||
mod external_editor;
|
mod external_editor;
|
||||||
mod file_search;
|
mod file_search;
|
||||||
mod frames;
|
mod frames;
|
||||||
@@ -1814,23 +1814,11 @@ async fn run_ratatui_app(
|
|||||||
let hooks_request_handle = app_server.request_handle();
|
let hooks_request_handle = app_server.request_handle();
|
||||||
let hooks_cwd = config.cwd.to_path_buf();
|
let hooks_cwd = config.cwd.to_path_buf();
|
||||||
let startup_prefetch_started_at = Instant::now();
|
let startup_prefetch_started_at = Instant::now();
|
||||||
let should_defer_bootstrap =
|
let (startup_bootstrap, startup_hooks_entry) = tokio::join!(
|
||||||
external_agent_config_migration_startup::should_show_external_agent_config_migration_prompt(
|
app_server.bootstrap(&config),
|
||||||
&config,
|
load_startup_hooks_review_entry(hooks_request_handle, hooks_cwd),
|
||||||
should_show_trust_screen_flag,
|
);
|
||||||
);
|
let startup_bootstrap = Some(startup_bootstrap?);
|
||||||
let (startup_bootstrap, startup_hooks_entry) = if should_defer_bootstrap {
|
|
||||||
(
|
|
||||||
None,
|
|
||||||
load_startup_hooks_review_entry(hooks_request_handle, hooks_cwd).await,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let (bootstrap, entry) = tokio::join!(
|
|
||||||
app_server.bootstrap(&config),
|
|
||||||
load_startup_hooks_review_entry(hooks_request_handle, hooks_cwd),
|
|
||||||
);
|
|
||||||
(Some(bootstrap?), entry)
|
|
||||||
};
|
|
||||||
let startup_elapsed_before_app = startup_prefetch_started_at.elapsed();
|
let startup_elapsed_before_app = startup_prefetch_started_at.elapsed();
|
||||||
let startup_hooks_browser = match maybe_run_startup_hooks_review(
|
let startup_hooks_browser = match maybe_run_startup_hooks_review(
|
||||||
&mut app_server,
|
&mut app_server,
|
||||||
@@ -1858,7 +1846,6 @@ async fn run_ratatui_app(
|
|||||||
session_selection,
|
session_selection,
|
||||||
feedback,
|
feedback,
|
||||||
should_show_trust_screen, // Proxy to: is it a first run in this directory?
|
should_show_trust_screen, // Proxy to: is it a first run in this directory?
|
||||||
should_show_trust_screen_flag, // Preserve the startup-time trust NUX signal before onboarding
|
|
||||||
should_prompt_windows_sandbox_nux_at_startup,
|
should_prompt_windows_sandbox_nux_at_startup,
|
||||||
app_server_target,
|
app_server_target,
|
||||||
state_db,
|
state_db,
|
||||||
|
|||||||
Reference in new issue
Block a user