[TUI] add external config migration prompt when start TUI

This commit is contained in:
alexsong-oai
2026-04-14 21:33:02 -07:00
parent 522b8e5a51
commit 14ec2b0b8f
10 changed files with 547 additions and 46 deletions

View File

@@ -138,22 +138,11 @@ impl ExternalAgentConfigService {
let cwd = migration_item.cwd;
let details = migration_item.details;
tokio::spawn(async move {
match service.import_plugins(cwd.as_deref(), details).await {
Ok(outcome) => {
tracing::info!(
succeeded_marketplaces = outcome.succeeded_marketplaces.len(),
succeeded_plugin_ids = outcome.succeeded_plugin_ids.len(),
failed_marketplaces = outcome.failed_marketplaces.len(),
failed_plugin_ids = outcome.failed_plugin_ids.len(),
"external agent config plugin import completed"
);
}
Err(err) => {
tracing::warn!(
error = %err,
"external agent config plugin import failed"
);
}
if let Err(err) = service.import_plugins(cwd.as_deref(), details).await {
tracing::warn!(
error = %err,
"external agent config plugin import failed"
);
}
});
emit_migration_metric(
@@ -174,6 +163,8 @@ impl ExternalAgentConfigService {
repo_root: Option<&Path>,
items: &mut Vec<ExternalAgentConfigMigrationItem>,
) -> io::Result<()> {
let configured_enabled_plugins =
configured_enabled_plugin_ids(&self.codex_home, repo_root)?;
let cwd = repo_root.map(Path::to_path_buf);
let source_settings = repo_root.map_or_else(
|| self.external_agent_home.join("settings.json"),
@@ -224,6 +215,7 @@ impl ExternalAgentConfigService {
source_settings.as_path(),
cwd.clone(),
settings.as_ref(),
&configured_enabled_plugins,
items,
);
@@ -299,9 +291,12 @@ impl ExternalAgentConfigService {
source_settings: &Path,
cwd: Option<PathBuf>,
settings: Option<&JsonValue>,
configured_enabled_plugins: &HashSet<String>,
items: &mut Vec<ExternalAgentConfigMigrationItem>,
) {
let Some(plugin_details) = settings.and_then(extract_plugin_migration_details) else {
let Some(plugin_details) = settings.and_then(|settings| {
extract_plugin_migration_details(settings, configured_enabled_plugins)
}) else {
return;
};
@@ -537,9 +532,15 @@ fn read_external_settings(path: &Path) -> io::Result<Option<JsonValue>> {
Ok(Some(settings))
}
fn extract_plugin_migration_details(settings: &JsonValue) -> Option<MigrationDetails> {
fn extract_plugin_migration_details(
settings: &JsonValue,
configured_enabled_plugins: &HashSet<String>,
) -> Option<MigrationDetails> {
let mut plugins = BTreeMap::new();
for plugin_id in collect_enabled_plugins(settings) {
for plugin_id in collect_enabled_plugins(settings)
.into_iter()
.filter(|plugin_id| !configured_enabled_plugins.contains(plugin_id))
{
let Ok(plugin_id) = PluginId::parse(&plugin_id) else {
continue;
};
@@ -591,6 +592,49 @@ fn collect_enabled_plugins(settings: &JsonValue) -> Vec<String> {
.collect()
}
fn configured_enabled_plugin_ids(
codex_home: &Path,
repo_root: Option<&Path>,
) -> io::Result<HashSet<String>> {
let mut plugin_states = BTreeMap::new();
for config_path in [
Some(codex_home.join("config.toml")),
repo_root.map(|repo_root| repo_root.join(".codex").join("config.toml")),
]
.into_iter()
.flatten()
{
if !config_path.is_file() {
continue;
}
let raw_config = fs::read_to_string(&config_path)?;
if raw_config.trim().is_empty() {
continue;
}
let config = toml::from_str::<TomlValue>(&raw_config)
.map_err(|err| invalid_data_error(format!("invalid config.toml: {err}")))?;
let Some(plugins) = config.get("plugins").and_then(TomlValue::as_table) else {
continue;
};
for (plugin_id, enabled) in plugins.iter().filter_map(|(plugin_id, plugin_config)| {
plugin_config
.get("enabled")
.and_then(TomlValue::as_bool)
.map(|enabled| (plugin_id, enabled))
}) {
plugin_states.insert(plugin_id.clone(), enabled);
}
}
Ok(plugin_states
.into_iter()
.filter_map(|(plugin_id, enabled)| enabled.then_some(plugin_id))
.collect())
}
fn collect_marketplace_import_sources(
settings: &JsonValue,
) -> BTreeMap<String, MarketplaceImportSource> {

View File

@@ -449,6 +449,60 @@ fn detect_home_lists_enabled_plugins_from_settings() {
);
}
#[test]
fn detect_repo_skips_plugins_that_are_already_enabled_in_codex() {
let root = TempDir::new().expect("create tempdir");
let claude_home = root.path().join(".claude");
let codex_home = root.path().join(".codex");
let repo_root = root.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).expect("create git dir");
fs::create_dir_all(repo_root.join(".claude")).expect("create repo claude dir");
fs::create_dir_all(&codex_home).expect("create codex home");
fs::write(
repo_root.join(".claude").join("settings.json"),
r#"{
"enabledPlugins": {
"formatter@acme-tools": true,
"deployer@acme-tools": true
}
}"#,
)
.expect("write repo settings");
fs::write(
codex_home.join("config.toml"),
r#"
[plugins."formatter@acme-tools"]
enabled = true
"#,
)
.expect("write codex config");
let items = service_for_paths(claude_home, codex_home)
.detect(ExternalAgentConfigDetectOptions {
include_home: false,
cwds: Some(vec![repo_root.clone()]),
})
.expect("detect");
assert_eq!(
items,
vec![ExternalAgentConfigMigrationItem {
item_type: ExternalAgentConfigMigrationItemType::Plugins,
description: format!(
"Import enabled plugins from {}",
repo_root.join(".claude").join("settings.json").display()
),
cwd: Some(repo_root),
details: Some(MigrationDetails {
plugins: vec![PluginsMigration {
marketplace_name: "acme-tools".to_string(),
plugin_names: vec!["deployer".to_string()],
}],
}),
}]
);
}
#[tokio::test]
async fn import_plugins_requires_details() {
let (_root, claude_home, codex_home) = fixture_paths();

View File

@@ -150,6 +150,8 @@ pub enum Feature {
ToolSuggest,
/// Enable plugins.
Plugins,
/// Show the startup prompt for migrating external agent config into Codex.
ExternalMigrate,
/// Allow the model to invoke the built-in image generation tool.
ImageGeneration,
/// Allow prompting and installing missing MCP dependencies.
@@ -795,6 +797,16 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::ExternalMigrate,
key: "external_migrate",
stage: Stage::Experimental {
name: "External migrate",
menu_description: "Show a startup prompt when Codex detects migratable external agent config for this machine or project.",
announcement: "",
},
default_enabled: false,
},
FeatureSpec {
id: Feature::ImageGeneration,
key: "image_generation",

View File

@@ -103,6 +103,23 @@ fn guardian_approval_is_experimental_and_user_toggleable() {
assert_eq!(Feature::GuardianApproval.default_enabled(), false);
}
#[test]
fn external_migrate_is_experimental_and_disabled_by_default() {
let spec = Feature::ExternalMigrate.info();
let stage = spec.stage;
assert!(matches!(stage, Stage::Experimental { .. }));
assert_eq!(stage.experimental_menu_name(), Some("External migrate"));
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::ExternalMigrate.default_enabled(), false);
}
#[test]
fn request_permissions_is_under_development() {
assert_eq!(

View File

@@ -893,6 +893,24 @@ fn visible_external_agent_config_migration_items(
.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()
}
}
struct ExternalAgentConfigMigrationPromptResult {
exit_info: Option<AppExitInfo>,
success_message: Option<String>,
}
async fn persist_external_agent_config_migration_prompt_dismissal(
config: &mut Config,
items: &[ExternalAgentConfigMigrationItem],
@@ -965,7 +983,14 @@ async fn handle_external_agent_config_migration_prompt_if_needed(
config: &mut Config,
cli_kv_overrides: &[(String, TomlValue)],
harness_overrides: &ConfigOverrides,
) -> Result<Option<AppExitInfo>> {
) -> Result<ExternalAgentConfigMigrationPromptResult> {
if !config.features.enabled(Feature::ExternalMigrate) {
return Ok(ExternalAgentConfigMigrationPromptResult {
exit_info: None,
success_message: None,
});
}
let detected_items = match app_server
.external_agent_config_detect(ExternalAgentConfigDetectParams {
include_home: true,
@@ -980,12 +1005,18 @@ async fn handle_external_agent_config_migration_prompt_if_needed(
cwd = %config.cwd.display(),
"failed to detect external agent config migrations; continuing startup"
);
return Ok(None);
return Ok(ExternalAgentConfigMigrationPromptResult {
exit_info: None,
success_message: None,
});
}
};
if detected_items.is_empty() {
return Ok(None);
return Ok(ExternalAgentConfigMigrationPromptResult {
exit_info: None,
success_message: None,
});
}
let mut selected_items = detected_items.clone();
@@ -1004,10 +1035,15 @@ async fn handle_external_agent_config_migration_prompt_if_needed(
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 =
rebuild_startup_config(config, cli_kv_overrides, harness_overrides)
.await?;
return Ok(None);
return Ok(ExternalAgentConfigMigrationPromptResult {
exit_info: None,
success_message: Some(success_message),
});
}
Err(err) => {
tracing::warn!(
@@ -1019,7 +1055,12 @@ async fn handle_external_agent_config_migration_prompt_if_needed(
}
}
}
ExternalAgentConfigMigrationOutcome::Skip => return Ok(None),
ExternalAgentConfigMigrationOutcome::Skip => {
return Ok(ExternalAgentConfigMigrationPromptResult {
exit_info: None,
success_message: None,
});
}
ExternalAgentConfigMigrationOutcome::SkipForever => {
match persist_external_agent_config_migration_prompt_dismissal(
config,
@@ -1027,7 +1068,12 @@ async fn handle_external_agent_config_migration_prompt_if_needed(
)
.await
{
Ok(()) => return Ok(None),
Ok(()) => {
return Ok(ExternalAgentConfigMigrationPromptResult {
exit_info: None,
success_message: None,
});
}
Err(err) => {
tracing::warn!(
error = %err,
@@ -1039,13 +1085,16 @@ async fn handle_external_agent_config_migration_prompt_if_needed(
}
}
ExternalAgentConfigMigrationOutcome::Exit => {
return Ok(Some(AppExitInfo {
token_usage: TokenUsage::default(),
thread_id: None,
thread_name: None,
update_action: None,
exit_reason: ExitReason::UserRequested,
}));
return Ok(ExternalAgentConfigMigrationPromptResult {
exit_info: Some(AppExitInfo {
token_usage: TokenUsage::default(),
thread_id: None,
thread_name: None,
update_action: None,
exit_reason: ExitReason::UserRequested,
}),
success_message: None,
});
}
}
}
@@ -3855,15 +3904,19 @@ impl App {
let harness_overrides =
normalize_harness_overrides_for_cwd(harness_overrides, &config.cwd)?;
let exit_info = handle_external_agent_config_migration_prompt_if_needed(
tui,
&mut app_server,
&mut config,
&cli_kv_overrides,
&harness_overrides,
)
.await?;
if let Some(exit_info) = exit_info {
let external_agent_config_migration_result =
handle_external_agent_config_migration_prompt_if_needed(
tui,
&mut app_server,
&mut config,
&cli_kv_overrides,
&harness_overrides,
)
.await?;
let external_agent_config_migration_message = external_agent_config_migration_result
.success_message
.clone();
if let Some(exit_info) = external_agent_config_migration_result.exit_info {
app_server
.shutdown()
.await
@@ -4043,6 +4096,9 @@ impl App {
(ChatWidget::new_with_app_event(init), Some(forked))
}
};
if let Some(message) = external_agent_config_migration_message {
chat_widget.add_info_message(message, /*hint*/ None);
}
chat_widget
.maybe_prompt_windows_sandbox_enable(should_prompt_windows_sandbox_nux_at_startup);
@@ -10376,18 +10432,21 @@ guardian_approval = true
codex_app_server_protocol::ExternalAgentConfigMigrationItemType::Config,
description: "home".to_string(),
cwd: None,
details: None,
},
ExternalAgentConfigMigrationItem {
item_type:
codex_app_server_protocol::ExternalAgentConfigMigrationItemType::AgentsMd,
description: "project".to_string(),
cwd: Some(PathBuf::from("/tmp/project")),
details: None,
},
ExternalAgentConfigMigrationItem {
item_type:
codex_app_server_protocol::ExternalAgentConfigMigrationItemType::Skills,
description: "other project".to_string(),
cwd: Some(PathBuf::from("/tmp/other")),
details: None,
},
],
);
@@ -10398,10 +10457,48 @@ guardian_approval = true
item_type: codex_app_server_protocol::ExternalAgentConfigMigrationItemType::Skills,
description: "other project".to_string(),
cwd: Some(PathBuf::from("/tmp/other")),
details: None,
}]
);
}
#[test]
fn external_agent_config_migration_success_message_mentions_plugins_when_present() {
let message = external_agent_config_migration_success_message(&[
ExternalAgentConfigMigrationItem {
item_type: codex_app_server_protocol::ExternalAgentConfigMigrationItemType::Config,
description: String::new(),
cwd: None,
details: None,
},
ExternalAgentConfigMigrationItem {
item_type: codex_app_server_protocol::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:
codex_app_server_protocol::ExternalAgentConfigMigrationItemType::AgentsMd,
description: String::new(),
cwd: None,
details: None,
}]);
assert_eq!(message, "External config migration completed successfully.");
}
#[tokio::test]
async fn model_migration_prompt_respects_hide_flag_and_self_target() {
let mut seen = BTreeMap::new();

View File

@@ -1,3 +1,4 @@
use crate::diff_render::display_path_for;
use crate::key_hint;
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
use crate::render::Insets;
@@ -7,6 +8,7 @@ use crate::tui::FrameRequester;
use crate::tui::Tui;
use crate::tui::TuiEvent;
use codex_app_server_protocol::ExternalAgentConfigMigrationItem;
use codex_app_server_protocol::PluginsMigration;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
@@ -79,9 +81,17 @@ struct MigrationSelection {
struct RenderLineEntry {
item_idx: Option<usize>,
kind: RenderLineKind,
line: Line<'static>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RenderLineKind {
Section,
Item,
ItemDetail,
}
pub(crate) async fn run_external_agent_config_migration_prompt(
tui: &mut Tui,
items: &[ExternalAgentConfigMigrationItem],
@@ -136,6 +146,78 @@ struct ExternalAgentConfigMigrationScreen {
}
impl ExternalAgentConfigMigrationScreen {
fn display_description(item: &ExternalAgentConfigMigrationItem) -> String {
let Some(cwd) = item.cwd.as_deref() else {
return item.description.clone();
};
fn reformat_description(
description: &str,
prefix: &str,
separator: &str,
cwd: &std::path::Path,
) -> Option<String> {
let remainder = description.strip_prefix(prefix)?;
let (left, right) = remainder.split_once(separator)?;
Some(format!(
"{prefix}{}{}{}",
display_path_for(std::path::Path::new(left), cwd),
separator,
display_path_for(std::path::Path::new(right), cwd)
))
}
if let Some(reformatted) =
reformat_description(&item.description, "Migrate ", " into ", cwd)
{
return reformatted;
}
if let Some(reformatted) =
reformat_description(&item.description, "Copy skill folders from ", " to ", cwd)
{
return reformatted;
}
if let Some(reformatted) = reformat_description(&item.description, "Import ", " to ", cwd) {
return reformatted;
}
if let Some(source) = item
.description
.strip_prefix("Import enabled plugins from ")
{
let description = format!(
"Import enabled plugins from {}",
display_path_for(std::path::Path::new(source), cwd)
);
if let Some(details) = &item.details {
let marketplace_count = details.plugins.len();
let plugin_count = details
.plugins
.iter()
.map(|plugin_group| plugin_group.plugin_names.len())
.sum::<usize>();
return format!(
"{description} ({marketplace_count} {}, {plugin_count} {})",
if marketplace_count == 1 {
"marketplace"
} else {
"marketplaces"
},
if plugin_count == 1 {
"plugin"
} else {
"plugins"
}
);
}
return description;
}
item.description.clone()
}
fn new(
request_frame: FrameRequester,
items: &[ExternalAgentConfigMigrationItem],
@@ -164,6 +246,40 @@ impl ExternalAgentConfigMigrationScreen {
}
}
fn plugin_detail_lines(plugin_groups: &[PluginsMigration]) -> Vec<Line<'static>> {
let mut lines = plugin_groups
.iter()
.take(3)
.map(|plugin_group| {
let mut plugin_names = plugin_group
.plugin_names
.iter()
.take(2)
.cloned()
.collect::<Vec<_>>();
let hidden_plugin_count = plugin_group
.plugin_names
.len()
.saturating_sub(plugin_names.len());
if hidden_plugin_count > 0 {
plugin_names.push(format!("+{hidden_plugin_count} more"));
}
Line::from(format!(
"{}: {}",
plugin_group.marketplace_name,
plugin_names.join(", ")
))
})
.collect::<Vec<_>>();
let hidden_marketplace_count = plugin_groups.len().saturating_sub(lines.len());
if hidden_marketplace_count > 0 {
lines.push(Line::from(format!(
" • +{hidden_marketplace_count} more marketplaces"
)));
}
lines
}
fn is_done(&self) -> bool {
self.done
}
@@ -317,6 +433,21 @@ impl ExternalAgentConfigMigrationScreen {
match key_event.code {
KeyCode::Up | KeyCode::Char('k') => self.move_up(),
KeyCode::Down | KeyCode::Char('j') => self.move_down(),
KeyCode::Char('1') => {
self.focus = FocusArea::Actions;
self.highlighted_action = ActionMenuOption::Proceed;
self.proceed();
}
KeyCode::Char('2') => {
self.focus = FocusArea::Actions;
self.highlighted_action = ActionMenuOption::Skip;
self.skip();
}
KeyCode::Char('3') => {
self.focus = FocusArea::Actions;
self.highlighted_action = ActionMenuOption::SkipForever;
self.skip_forever();
}
KeyCode::Char(' ') => self.toggle_selected_item(),
KeyCode::Char('a') => self.set_all_enabled(/*enabled*/ true),
KeyCode::Char('n') => self.set_all_enabled(/*enabled*/ false),
@@ -370,23 +501,35 @@ impl ExternalAgentConfigMigrationScreen {
if current_scope.is_some() {
lines.push(RenderLineEntry {
item_idx: None,
kind: RenderLineKind::Section,
line: Line::from(""),
});
}
lines.push(RenderLineEntry {
item_idx: None,
kind: RenderLineKind::Section,
line: Self::section_title(scope),
});
current_scope = Some(scope);
}
lines.push(RenderLineEntry {
item_idx: Some(idx),
kind: RenderLineKind::Item,
line: Line::from(format!(
" [{}] {}",
if item.enabled { "x" } else { " " },
item.item.description
Self::display_description(&item.item)
)),
});
if let Some(details) = &item.item.details {
for line in Self::plugin_detail_lines(&details.plugins) {
lines.push(RenderLineEntry {
item_idx: None,
kind: RenderLineKind::ItemDetail,
line,
});
}
}
}
lines
}
@@ -423,7 +566,7 @@ impl ExternalAgentConfigMigrationScreen {
line.spans.iter_mut().for_each(|span| {
span.style = span.style.cyan().bold();
});
} else if entry.item_idx.is_none() && !line.spans.is_empty() {
} else if entry.kind != RenderLineKind::Item && !line.spans.is_empty() {
line.spans.iter_mut().for_each(|span| {
span.style = span.style.dim();
});
@@ -549,6 +692,12 @@ impl WidgetRef for &ExternalAgentConfigMigrationScreen {
" to move, ".dim(),
key_hint::plain(KeyCode::Char(' ')).into(),
" to toggle, ".dim(),
"1".cyan(),
"/".dim(),
"2".cyan(),
"/".dim(),
"3".cyan(),
" to choose, ".dim(),
"a".cyan(),
"/".dim(),
"n".cyan(),
@@ -590,6 +739,7 @@ mod tests {
use crate::tui::FrameRequester;
use codex_app_server_protocol::ExternalAgentConfigMigrationItem;
use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
use codex_app_server_protocol::PluginsMigration;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
@@ -606,12 +756,44 @@ mod tests {
"Migrate /Users/alex/.claude/settings.json into /Users/alex/.codex/config.toml"
.to_string(),
cwd: None,
details: None,
},
ExternalAgentConfigMigrationItem {
item_type: ExternalAgentConfigMigrationItemType::Plugins,
description: "Import enabled plugins from /workspace/project/.claude/settings.json"
.to_string(),
cwd: Some(PathBuf::from("/workspace/project")),
details: Some(codex_app_server_protocol::MigrationDetails {
plugins: vec![
PluginsMigration {
marketplace_name: "acme-tools".to_string(),
plugin_names: vec![
"deployer".to_string(),
"formatter".to_string(),
"lint".to_string(),
],
},
PluginsMigration {
marketplace_name: "team-marketplace".to_string(),
plugin_names: vec!["asana".to_string()],
},
PluginsMigration {
marketplace_name: "debug".to_string(),
plugin_names: vec!["sample".to_string()],
},
PluginsMigration {
marketplace_name: "data-tools".to_string(),
plugin_names: vec!["warehouse".to_string()],
},
],
}),
},
ExternalAgentConfigMigrationItem {
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
description: "Import /workspace/project/CLAUDE.md to /workspace/project/AGENTS.md"
.to_string(),
cwd: Some(PathBuf::from("/workspace/project")),
details: None,
},
]
}
@@ -750,4 +932,45 @@ mod tests {
"expected inline validation error, got:\n{rendered}"
);
}
#[test]
fn numeric_shortcuts_choose_actions() {
let items = sample_items();
let mut proceed_screen = ExternalAgentConfigMigrationScreen::new(
FrameRequester::test_dummy(),
&items,
&items,
/*error*/ None,
);
proceed_screen.handle_key(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE));
assert_eq!(
proceed_screen.outcome(),
ExternalAgentConfigMigrationOutcome::Proceed(items.clone())
);
let mut skip_screen = ExternalAgentConfigMigrationScreen::new(
FrameRequester::test_dummy(),
&items,
&items,
/*error*/ None,
);
skip_screen.handle_key(KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE));
assert_eq!(
skip_screen.outcome(),
ExternalAgentConfigMigrationOutcome::Skip
);
let mut skip_forever_screen = ExternalAgentConfigMigrationScreen::new(
FrameRequester::test_dummy(),
&items,
&items,
/*error*/ None,
);
skip_forever_screen.handle_key(KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE));
assert_eq!(
skip_forever_screen.outcome(),
ExternalAgentConfigMigrationOutcome::SkipForever
);
}
}

View File

@@ -1,5 +1,6 @@
---
source: tui/src/external_agent_config_migration.rs
assertion_line: 757
expression: rendered
---
@@ -10,10 +11,15 @@ expression: rendered
[x] Migrate /Users/alex/.claude/settings.json into /Users/alex/.codex/con…
Project: /workspace/project
[x] Import /workspace/project/CLAUDE.md to /workspace/project/AGENTS.md
[x] Import enabled plugins from .claude/settings.json (4 marketplaces, 6 p…
• acme-tools: deployer, formatter, +1 more
• team-marketplace: asana
• debug: sample
• +1 more marketplaces
[x] Import CLAUDE.md to AGENTS.md
Selected 2 of 2 item(s).
Selected 3 of 3 item(s).
1. Proceed with selected
2. Skip for now
3. Don't ask again for these locations
Use ↑/↓ to move, space to toggle, a/n for all/none
Use ↑/↓ to move, space to toggle, 1/2/3 to choose, a/n for all/none

View File

@@ -0,0 +1,23 @@
---
source: tui/src/external_agent_config_migration.rs
assertion_line: 773
expression: rendered
---
> Migratable external agent config detected
We found settings from another agent that you can add to this project.
Select what to import
Home
[x] Migrate /Users/alex/.claude/settings.json into /Users/alex/.codex/con…
Project: /workspace/project
[x] Import enabled plugins from .claude/settings.json
• acme-tools: deployer, formatter
• team-marketplace: asana
[x] Import CLAUDE.md to AGENTS.md
Selected 3 of 3 item(s).
1. Proceed with selected
2. Skip for now
3. Don't ask again for these locations
Use ↑/↓ to move, space to toggle, 1/2/3 to choose, a/n for all/none

View File

@@ -0,0 +1,11 @@
---
source: tui/src/history_cell.rs
assertion_line: 3367
expression: rendered
---
/mcp
🔌 MCP Tools
• some-server (disabled)
• Reason: requirements (/etc/codex/requirements.toml)

View File

@@ -0,0 +1,14 @@
---
source: tui/src/history_cell.rs
assertion_line: 3327
expression: rendered
---
/mcp
🔌 MCP Tools
• docs (disabled)
• Reason: requirements (/etc/codex/requirements.toml)
• http (disabled)
• Reason: requirements (/etc/codex/requirements.toml)