From 00e21e5036b0f138ae811f07db9edd97a2dff6d3 Mon Sep 17 00:00:00 2001 From: charlesgong-openai Date: Thu, 9 Jul 2026 19:40:44 -0400 Subject: [PATCH] default external imports without a source picker --- codex-rs/app-server/README.md | 4 +- .../tests/suite/v2/external_agent_config.rs | 3 - .../v2/external_agent_config_source_cu.rs | 332 ------------------ codex-rs/tui/src/app_server_session.rs | 10 +- .../external_agent_config_migration/render.rs | 11 +- .../external_agent_config_migration_flow.rs | 96 ++--- ...ernal_agent_config_migration_flow_tests.rs | 4 +- .../external_agent_config_migration_model.rs | 2 +- .../external_agent_config_migration_source.rs | 200 ----------- ...nal_agent_config_migration_source_tests.rs | 42 --- codex-rs/tui/src/lib.rs | 1 - ...rnal_agent_config_migration_customize.snap | 6 +- ...ent_config_migration_customize_action.snap | 6 +- ...ig_migration_customize_action_windows.snap | 6 +- ...nt_config_migration_customize_windows.snap | 6 +- ...xternal_agent_config_migration_prompt.snap | 7 +- ...agent_config_migration_prompt_windows.snap | 7 +- ...ernal_agent_config_migration_messages.snap | 16 +- ...__external_agent_config_source_prompt.snap | 13 - 19 files changed, 67 insertions(+), 705 deletions(-) delete mode 100644 codex-rs/app-server/tests/suite/v2/external_agent_config_source_cu.rs delete mode 100644 codex-rs/tui/src/external_agent_config_migration_source.rs delete mode 100644 codex-rs/tui/src/external_agent_config_migration_source_tests.rs delete mode 100644 codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration_source__tests__external_agent_config_source_prompt.snap diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index e835c9cfab..d4e21a1e56 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -239,8 +239,8 @@ Example with notification opt-out: - `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`. - `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id. - `config/read` — fetch the effective config on disk after resolving config layering, including opaque `desktop` values stored in `config.toml`. -- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome` and optional `cwds`. Pass `source: "cursor"` to detect Cursor artifacts; omitted, `null`, or unrecognized values retain the default Claude Code behavior. Each detected item includes `cwd` (`null` for home), and multi-item migrations may additionally include structured `details` with plugin ids, skill names, session metadata, or other artifact names. -- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any `details` returned by detect. Pass the same `source` used for detection so the server reads from the matching product; `source: "cursor"` selects Cursor, while omitted, `null`, or unrecognized values retain the default Claude Code behavior. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately. +- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome`, optional `cwds`, and an optional source selector. Omitted, `null`, or unrecognized source values retain the default behavior. Each detected item includes `cwd` (`null` for home), and multi-item migrations may additionally include structured `details` with plugin ids, skill names, session metadata, or other artifact names. +- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any `details` returned by detect. Pass the same optional source used for detection so the server reads from the matching source; omitted, `null`, or unrecognized values retain the default behavior. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately. - `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. - `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs index 5af5833753..53c50ea43f 100644 --- a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs +++ b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs @@ -51,9 +51,6 @@ fn assert_import_response(response: ExternalAgentConfigImportResponse) -> String response.import_id } -#[path = "external_agent_config_source_cu.rs"] -mod source_cu_tests; - #[tokio::test] async fn external_agent_config_import_sends_completion_notification_for_sync_only_import() -> Result<()> { diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_config_source_cu.rs b/codex-rs/app-server/tests/suite/v2/external_agent_config_source_cu.rs deleted file mode 100644 index 3852dd2102..0000000000 --- a/codex-rs/app-server/tests/suite/v2/external_agent_config_source_cu.rs +++ /dev/null @@ -1,332 +0,0 @@ -use super::*; -use pretty_assertions::assert_eq; - -#[tokio::test] -async fn external_agent_config_imports_source_cu_repo_artifacts() -> Result<()> { - let codex_home = TempDir::new()?; - let project_root = codex_home.path().join("repo"); - let source_dir = project_root.join(".cursor"); - let nested_scope = project_root.join("backend"); - std::fs::create_dir_all(project_root.join(".git"))?; - std::fs::create_dir_all(source_dir.join("skills/release"))?; - std::fs::create_dir_all(source_dir.join("commands"))?; - std::fs::create_dir_all(source_dir.join("agents"))?; - std::fs::create_dir_all(source_dir.join("rules/team"))?; - std::fs::create_dir_all(nested_scope.join(".cursor/rules"))?; - std::fs::write( - source_dir.join("mcp.json"), - r#"{"mcpServers":{"docs":{"command":"docs-server","args":["--stdio"]}}}"#, - )?; - std::fs::write( - source_dir.join("skills/release/SKILL.md"), - "---\nname: release\ndescription: Release with Cursor\n---\n\nUse Cursor to release. Advance the database cursor after each page.\n", - )?; - std::fs::write( - source_dir.join("commands/review.md"), - "Review the change with Cursor. Keep the text cursor visible.\n", - )?; - std::fs::write( - source_dir.join("agents/researcher.md"), - "---\nname: researcher\ndescription: Cursor research role\n---\nResearch with Cursor.\n", - )?; - std::fs::write( - project_root.join(".cursorrules"), - "Use Cursor for repository work. Keep the text cursor visible.\n", - )?; - std::fs::write( - source_dir.join("rules/always.mdc"), - "---\ndescription: Always apply\nalwaysApply: true # YAML comments are valid\n---\nRun checks in Cursor.\n", - )?; - std::fs::write( - source_dir.join("rules/team/database.mdc"), - "---\ndescription: Database guidance\nalwaysApply: true\n---\nDocument database cursor behavior.\n", - )?; - std::fs::write( - source_dir.join("rules/scoped.mdc"), - "---\ndescription: TypeScript only\nglobs: '*.ts'\nalwaysApply: false\n---\nUse TypeScript.\n", - )?; - std::fs::write( - nested_scope.join(".cursor/rules/always.mdc"), - "---\ndescription: Backend guidance\nalwaysApply: true\n---\nUse Cursor for backend work.\n", - )?; - - let home_dir = codex_home.path().display().to_string(); - let mut mcp = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .with_env_overrides(&[("HOME", Some(home_dir.as_str()))]) - .build() - .await?; - timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - - let request_id = mcp - .send_raw_request( - "externalAgentConfig/detect", - Some(serde_json::json!({ - "includeHome": false, - "cwds": [&project_root], - "source": "cursor" - })), - ) - .await?; - let response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??; - let detected: ExternalAgentConfigDetectResponse = to_response(response)?; - assert_eq!( - detected - .items - .iter() - .map(|item| item.item_type) - .collect::>(), - vec![ - ExternalAgentConfigMigrationItemType::McpServerConfig, - ExternalAgentConfigMigrationItemType::Skills, - ExternalAgentConfigMigrationItemType::Commands, - ExternalAgentConfigMigrationItemType::Subagents, - ExternalAgentConfigMigrationItemType::AgentsMd, - ExternalAgentConfigMigrationItemType::AgentsMd, - ] - ); - - let request_id = mcp - .send_raw_request( - "externalAgentConfig/import", - Some(serde_json::json!({ - "migrationItems": detected.items, - "source": "cursor" - })), - ) - .await?; - let response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??; - let response: ExternalAgentConfigImportResponse = to_response(response)?; - let import_id = assert_import_response(response); - let notification = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_notification_message("externalAgentConfig/import/completed"), - ) - .await??; - let completed: ExternalAgentConfigImportCompletedNotification = - serde_json::from_value(notification.params.expect("completed params"))?; - assert_eq!(completed.import_id, import_id); - assert!( - completed - .item_type_results - .iter() - .all(|result| result.failures.is_empty()) - ); - - let config = std::fs::read_to_string(project_root.join(".codex/config.toml"))?; - assert!(config.contains("[mcp_servers.docs]")); - assert!(config.contains("command = \"docs-server\"")); - let skill = std::fs::read_to_string(project_root.join(".agents/skills/release/SKILL.md"))?; - assert!(skill.contains("Release with Codex")); - assert!(skill.contains("database cursor")); - let command = std::fs::read_to_string( - project_root.join(".agents/skills/source-command-review/SKILL.md"), - )?; - assert!(command.contains("Review the change with Codex")); - assert!(command.contains("Migrated source command `review`")); - assert!(command.contains("text cursor")); - let subagent = std::fs::read_to_string(project_root.join(".codex/agents/researcher.toml"))?; - assert!(subagent.contains("Codex research role")); - let agents_md = std::fs::read_to_string(project_root.join("AGENTS.md"))?; - assert!(agents_md.contains("Use Codex for repository work.")); - assert!(agents_md.contains("Run checks in Codex.")); - assert!(agents_md.contains("Document database cursor behavior.")); - assert!(agents_md.contains("text cursor")); - assert!(!agents_md.contains("Use TypeScript.")); - assert!(!agents_md.contains("alwaysApply")); - let nested_agents_md = std::fs::read_to_string(nested_scope.join("AGENTS.md"))?; - assert!(nested_agents_md.contains("Use Codex for backend work.")); - - Ok(()) -} - -#[tokio::test] -async fn external_agent_config_imports_source_cu_home_artifacts() -> Result<()> { - let root = TempDir::new()?; - let home = root.path().join("home"); - let codex_home = home.join(".codex"); - let source_dir = home.join(".cursor"); - std::fs::create_dir_all(&codex_home)?; - std::fs::create_dir_all(source_dir.join("skills/home-release"))?; - std::fs::create_dir_all(source_dir.join("commands"))?; - std::fs::create_dir_all(source_dir.join("agents"))?; - std::fs::create_dir_all(source_dir.join("rules"))?; - std::fs::write( - source_dir.join("mcp.json"), - r#"{"mcpServers":{"home-docs":{"command":"home-docs-server"}}}"#, - )?; - std::fs::write( - source_dir.join("skills/home-release/SKILL.md"), - "---\nname: home-release\ndescription: Release with Cursor\n---\n\nUse Cursor for home releases.\n", - )?; - std::fs::write( - source_dir.join("commands/home-review.md"), - "Review home changes with Cursor.\n", - )?; - std::fs::write( - source_dir.join("agents/home-researcher.md"), - "---\nname: home-researcher\ndescription: Research with Cursor\n---\nResearch with Cursor.\n", - )?; - std::fs::write( - source_dir.join("rules/always.mdc"), - "---\ndescription: Home guidance\nalwaysApply: true\n---\nUse Cursor for home work.\n", - )?; - - let home_dir = home.display().to_string(); - let mut mcp = TestAppServer::builder() - .with_codex_home(&codex_home) - .with_env_overrides(&[("HOME", Some(home_dir.as_str()))]) - .build() - .await?; - timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - - let request_id = mcp - .send_raw_request( - "externalAgentConfig/detect", - Some(serde_json::json!({ - "includeHome": true, - "cwds": [], - "source": "cursor" - })), - ) - .await?; - let response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??; - let detected: ExternalAgentConfigDetectResponse = to_response(response)?; - assert_eq!( - detected - .items - .iter() - .map(|item| item.item_type) - .collect::>(), - vec![ - ExternalAgentConfigMigrationItemType::McpServerConfig, - ExternalAgentConfigMigrationItemType::Skills, - ExternalAgentConfigMigrationItemType::Commands, - ExternalAgentConfigMigrationItemType::Subagents, - ExternalAgentConfigMigrationItemType::AgentsMd, - ] - ); - - let request_id = mcp - .send_raw_request( - "externalAgentConfig/import", - Some(serde_json::json!({ - "migrationItems": detected.items, - "source": "cursor" - })), - ) - .await?; - let response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??; - let response: ExternalAgentConfigImportResponse = to_response(response)?; - let import_id = assert_import_response(response); - let notification = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_notification_message("externalAgentConfig/import/completed"), - ) - .await??; - let completed: ExternalAgentConfigImportCompletedNotification = - serde_json::from_value(notification.params.expect("completed params"))?; - assert_eq!(completed.import_id, import_id); - assert!( - completed - .item_type_results - .iter() - .all(|result| result.failures.is_empty()) - ); - - let config = std::fs::read_to_string(codex_home.join("config.toml"))?; - assert!(config.contains("[mcp_servers.home-docs]")); - let skill = std::fs::read_to_string(home.join(".agents/skills/home-release/SKILL.md"))?; - assert!(skill.contains("Release with Codex")); - assert!(skill.contains("Use Codex for home releases.")); - let command = - std::fs::read_to_string(home.join(".agents/skills/source-command-home-review/SKILL.md"))?; - assert!(command.contains("Review home changes with Codex.")); - let subagent = std::fs::read_to_string(codex_home.join("agents/home-researcher.toml"))?; - assert!(subagent.contains("Research with Codex")); - let agents_md = std::fs::read_to_string(codex_home.join("AGENTS.md"))?; - assert!(agents_md.contains("Use Codex for home work.")); - - Ok(()) -} - -#[tokio::test] -async fn source_cu_import_does_not_accept_source_cl_sessions() -> Result<()> { - let codex_home = TempDir::new()?; - let project_root = codex_home.path().join("repo"); - let session_path = external_agent_home(codex_home.path()).join("projects/repo/session.jsonl"); - std::fs::create_dir_all(session_path.parent().expect("session parent"))?; - std::fs::write(&session_path, "{}\n")?; - - let home_dir = codex_home.path().display().to_string(); - let mut mcp = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .with_env_overrides(&[("HOME", Some(home_dir.as_str()))]) - .build() - .await?; - timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - - let request_id = mcp - .send_raw_request( - "externalAgentConfig/import", - Some(serde_json::json!({ - "source": "cursor", - "migrationItems": [{ - "itemType": "SESSIONS", - "description": "Migrate recent sessions", - "cwd": null, - "details": { - "sessions": [{ - "path": session_path, - "cwd": project_root, - "title": "wrong source" - }] - } - }] - })), - ) - .await?; - let response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??; - let response: ExternalAgentConfigImportResponse = to_response(response)?; - let import_id = assert_import_response(response); - let notification = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_notification_message("externalAgentConfig/import/completed"), - ) - .await??; - let completed: ExternalAgentConfigImportCompletedNotification = - serde_json::from_value(notification.params.expect("completed params"))?; - - assert_eq!(completed.import_id, import_id); - assert_eq!(completed.item_type_results.len(), 1); - let result = &completed.item_type_results[0]; - assert_eq!( - result.item_type, - ExternalAgentConfigMigrationItemType::Sessions - ); - assert!(result.successes.is_empty()); - assert_eq!(result.failures.len(), 1); - assert_eq!(result.failures[0].failure_stage, "session_missing"); - - Ok(()) -} diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index ede3d01694..ab29dfd598 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -136,7 +136,8 @@ use uuid::Uuid; const JSONRPC_INVALID_REQUEST: i64 = -32600; const JSONRPC_METHOD_NOT_FOUND: i64 = -32601; -pub(crate) const EXTERNAL_AGENT_CONFIG_IMPORT_IN_PROGRESS_MESSAGE: &str = "A previous external agent import is still running. Wait for it to finish before importing again."; +pub(crate) const EXTERNAL_AGENT_CONFIG_IMPORT_IN_PROGRESS_MESSAGE: &str = + "A previous Claude Code import is still running. Wait for it to finish before importing again."; const THREAD_SETTINGS_UPDATE_METHOD: &str = "thread/settings/update"; fn bootstrap_request_error(context: &'static str, err: TypedRequestError) -> color_eyre::Report { @@ -397,13 +398,12 @@ impl AppServerSession { self.client .request_typed(ClientRequest::ExternalAgentConfigDetect { request_id, params }) .await - .wrap_err("externalAgentConfig/detect failed during external agent import") + .wrap_err("externalAgentConfig/detect failed during Claude Code import") } pub(crate) async fn external_agent_config_import( &mut self, migration_items: Vec, - source: Option, ) -> Result<()> { // Mark the import active before sending the request so a fast completion notification // cannot arrive before the TUI records it. @@ -420,11 +420,11 @@ impl AppServerSession { request_id, params: ExternalAgentConfigImportParams { migration_items, - source, + source: None, }, }) .await - .wrap_err("externalAgentConfig/import failed during external agent import"); + .wrap_err("externalAgentConfig/import failed during Claude Code import"); match response { Ok(_) => Ok(()), Err(err) => { diff --git a/codex-rs/tui/src/external_agent_config_migration/render.rs b/codex-rs/tui/src/external_agent_config_migration/render.rs index 508a3c0aa5..e0e4926a9e 100644 --- a/codex-rs/tui/src/external_agent_config_migration/render.rs +++ b/codex-rs/tui/src/external_agent_config_migration/render.rs @@ -78,14 +78,15 @@ impl WidgetRef for &ExternalAgentConfigMigrationScreen { let error_height = u16::from(self.error.is_some()); let intro_lines = match self.view { MigrationView::Summary => vec![ - Line::from("Bring over supported setup from another coding agent."), + Line::from("Bring over your setup, current project, and recent chats."), Line::from("Codex may add files to your current project folder."), - Line::from("Your existing setup will not be changed."), + Line::from("Your existing Claude Code setup will not be changed."), + Line::from("Standard Claude Chat data cannot be imported."), ], MigrationView::Customize => vec![ - Line::from("Choose items to import."), + Line::from("Choose the Claude Code items to import."), Line::from("Codex may add files to your current project folder."), - Line::from("Your existing setup will not be changed."), + Line::from("Your existing Claude Code setup will not be changed."), ], }; let intro_height = intro_lines.len() as u16; @@ -118,7 +119,7 @@ impl WidgetRef for &ExternalAgentConfigMigrationScreen { .areas(inner_area); let title = match self.view { - MigrationView::Summary => "Import setup", + MigrationView::Summary => "Import from Claude Code", MigrationView::Customize => "Choose what to import", }; let heading = Line::from(vec!["> ".into(), title.bold()]); diff --git a/codex-rs/tui/src/external_agent_config_migration_flow.rs b/codex-rs/tui/src/external_agent_config_migration_flow.rs index b5d0d91936..c1cef68dfd 100644 --- a/codex-rs/tui/src/external_agent_config_migration_flow.rs +++ b/codex-rs/tui/src/external_agent_config_migration_flow.rs @@ -4,8 +4,6 @@ use crate::external_agent_config_migration::ExternalAgentConfigMigrationOutcome; use crate::external_agent_config_migration::run_external_agent_config_migration_prompt; use crate::external_agent_config_migration_model::external_agent_config_migration_item_count; use crate::external_agent_config_migration_model::external_agent_config_migration_type_label; -use crate::external_agent_config_migration_source::ExternalAgentConfigMigrationSource; -use crate::external_agent_config_migration_source::run_external_agent_config_source_prompt; use crate::legacy_core::config::Config; use crate::tui; use codex_app_server_protocol::ExternalAgentConfigDetectParams; @@ -16,9 +14,9 @@ use ratatui::prelude::Stylize as _; use ratatui::text::Line; pub(crate) const EXTERNAL_AGENT_CONFIG_MIGRATION_NO_ITEMS_MESSAGE: &str = - "No compatible setup was found to import."; -pub(crate) const EXTERNAL_AGENT_CONFIG_MIGRATION_REMOTE_UNAVAILABLE_MESSAGE: &str = "Import from other apps is unavailable in remote sessions. Start Codex locally and run /import."; -pub(crate) const EXTERNAL_AGENT_CONFIG_MIGRATION_DAEMON_UNAVAILABLE_MESSAGE: &str = "Import from other apps is unavailable while Codex is connected to the local app-server daemon. Stop the daemon, restart Codex, and run /import."; + "No Claude Code setup was found to import."; +pub(crate) const EXTERNAL_AGENT_CONFIG_MIGRATION_REMOTE_UNAVAILABLE_MESSAGE: &str = "Import from Claude Code is unavailable in remote sessions. Start Codex locally and run /import."; +pub(crate) const EXTERNAL_AGENT_CONFIG_MIGRATION_DAEMON_UNAVAILABLE_MESSAGE: &str = "Import from Claude Code is unavailable while Codex is connected to the local app-server daemon. Stop the daemon, restart Codex, and run /import."; pub(crate) enum ExternalAgentConfigMigrationFlowOutcome { Started(Vec>), @@ -26,11 +24,6 @@ pub(crate) enum ExternalAgentConfigMigrationFlowOutcome { Cancelled, } -struct DetectedExternalAgentConfigSource { - source: ExternalAgentConfigMigrationSource, - items: Vec, -} - fn external_agent_config_migration_started_lines( selected_items: &[ExternalAgentConfigMigrationItem], remaining_item_count: usize, @@ -96,7 +89,7 @@ fn external_agent_config_migration_started_lines( let mut lines = vec![ vec![ "• ".dim(), - "Import started.".cyan(), + "Claude Code import started.".cyan(), " You can keep working while it finishes.".into(), ] .into(), @@ -152,7 +145,7 @@ pub(crate) fn external_agent_config_migration_finished_lines( let mut lines = vec![ vec![ "• ".dim(), - "Import finished: ".into(), + "Claude Code import finished: ".into(), format!("{imported_count} imported").green(), ", ".into(), failed_count, @@ -219,65 +212,28 @@ pub(crate) async fn handle_external_agent_config_migration_prompt( } let cwd = config.cwd.to_path_buf(); - let mut detected_sources = Vec::new(); - let mut detection_errors = Vec::new(); - for source in ExternalAgentConfigMigrationSource::ALL { - let response = match app_server - .external_agent_config_detect(ExternalAgentConfigDetectParams { - include_home: true, - cwds: Some(vec![cwd.clone()]), - source: Some(source.api_source().to_string()), - }) - .await - { - Ok(response) => response, - Err(err) => { - tracing::warn!( - error = %err, - cwd = %cwd.display(), - source = source.label(), - "failed to detect external agent config migrations" - ); - detection_errors.push(format!("{}: {err}", source.label())); - continue; - } - }; - if !response.items.is_empty() { - detected_sources.push(DetectedExternalAgentConfigSource { - source, - items: response.items, - }); + let detected_items = match app_server + .external_agent_config_detect(ExternalAgentConfigDetectParams { + include_home: true, + cwds: Some(vec![cwd.clone()]), + }) + .await + { + Ok(response) => response.items, + Err(err) => { + tracing::warn!( + error = %err, + cwd = %cwd.display(), + "failed to detect external agent config migrations" + ); + return Err(format!("Could not check for Claude Code setup: {err}")); } - } + }; - if detected_sources.is_empty() { - if !detection_errors.is_empty() { - return Err(format!( - "Could not check for importable setup: {}", - detection_errors.join("; ") - )); - } + if detected_items.is_empty() { return Ok(ExternalAgentConfigMigrationFlowOutcome::NoItems); } - let selected_source = if detected_sources.len() == 1 { - detected_sources[0].source - } else { - let sources = detected_sources - .iter() - .map(|detected| detected.source) - .collect::>(); - let Some(source) = run_external_agent_config_source_prompt(tui, &sources).await else { - return Ok(ExternalAgentConfigMigrationFlowOutcome::Cancelled); - }; - source - }; - let detected_items = detected_sources - .into_iter() - .find(|detected| detected.source == selected_source) - .map(|detected| detected.items) - .unwrap_or_default(); - let mut selected_items = detected_items.clone(); let mut error: Option = None; @@ -292,13 +248,7 @@ pub(crate) async fn handle_external_agent_config_migration_prompt( { ExternalAgentConfigMigrationOutcome::Proceed(items) => { selected_items = items.clone(); - match app_server - .external_agent_config_import( - items, - Some(selected_source.api_source().to_string()), - ) - .await - { + match app_server.external_agent_config_import(items).await { Ok(()) => { let remaining_item_count = detected_items.len().saturating_sub(selected_items.len()); diff --git a/codex-rs/tui/src/external_agent_config_migration_flow_tests.rs b/codex-rs/tui/src/external_agent_config_migration_flow_tests.rs index 6c4430f38a..9a241eee7b 100644 --- a/codex-rs/tui/src/external_agent_config_migration_flow_tests.rs +++ b/codex-rs/tui/src/external_agent_config_migration_flow_tests.rs @@ -173,7 +173,7 @@ fn external_agent_config_migration_status_lines_use_semantic_colors() { vec![ Line::from(vec![ "• ".dim(), - "Import started.".cyan(), + "Claude Code import started.".cyan(), " You can keep working while it finishes.".into(), ]), Line::from(vec![ @@ -227,7 +227,7 @@ fn external_agent_config_migration_status_lines_use_semantic_colors() { vec![ Line::from(vec![ "• ".dim(), - "Import finished: ".into(), + "Claude Code import finished: ".into(), "2 imported".green(), ", ".into(), "1 failed".red(), diff --git a/codex-rs/tui/src/external_agent_config_migration_model.rs b/codex-rs/tui/src/external_agent_config_migration_model.rs index baf89967d4..87d3310b36 100644 --- a/codex-rs/tui/src/external_agent_config_migration_model.rs +++ b/codex-rs/tui/src/external_agent_config_migration_model.rs @@ -79,7 +79,7 @@ pub(crate) fn external_agent_config_migration_item_label( item: &ExternalAgentConfigMigrationItem, ) -> &'static str { match item.item_type { - ExternalAgentConfigMigrationItemType::AgentsMd => "Instructions", + ExternalAgentConfigMigrationItemType::AgentsMd => "Instructions (CLAUDE.md -> AGENTS.md)", ExternalAgentConfigMigrationItemType::Config => "Settings (settings.json -> config.toml)", ExternalAgentConfigMigrationItemType::Skills => "Skills", ExternalAgentConfigMigrationItemType::Plugins => "Plugins", diff --git a/codex-rs/tui/src/external_agent_config_migration_source.rs b/codex-rs/tui/src/external_agent_config_migration_source.rs deleted file mode 100644 index 479dab06fa..0000000000 --- a/codex-rs/tui/src/external_agent_config_migration_source.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::key_hint; -use crate::render::Insets; -use crate::render::renderable::ColumnRenderable; -use crate::render::renderable::Renderable; -use crate::render::renderable::RenderableExt as _; -use crate::tui::FrameRequester; -use crate::tui::Tui; -use crate::tui::TuiEvent; -use crossterm::event::KeyCode; -use crossterm::event::KeyEvent; -use crossterm::event::KeyEventKind; -use crossterm::event::KeyModifiers; -use ratatui::buffer::Buffer; -use ratatui::layout::Rect; -use ratatui::prelude::Stylize as _; -use ratatui::prelude::Widget; -use ratatui::text::Line; -use ratatui::text::Span; -use ratatui::widgets::Clear; -use ratatui::widgets::WidgetRef; -use tokio_stream::StreamExt; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum ExternalAgentConfigMigrationSource { - Cl, - Cu, -} - -impl ExternalAgentConfigMigrationSource { - pub(crate) const ALL: [Self; 2] = [Self::Cl, Self::Cu]; - - pub(crate) fn api_source(self) -> &'static str { - match self { - Self::Cl => "claude-code", - Self::Cu => "cursor", - } - } - - pub(crate) fn label(self) -> &'static str { - match self { - Self::Cl => "Claude Code", - Self::Cu => "Cursor", - } - } -} - -pub(crate) async fn run_external_agent_config_source_prompt( - tui: &mut Tui, - sources: &[ExternalAgentConfigMigrationSource], -) -> Option { - let mut screen = ExternalAgentConfigSourceScreen::new(tui.frame_requester(), sources); - let _ = tui.draw(u16::MAX, |frame| { - frame.render_widget_ref(&screen, frame.area()); - }); - - let events = tui.event_stream(); - tokio::pin!(events); - - while !screen.is_done() { - if let Some(event) = events.next().await { - match event { - TuiEvent::Key(key_event) => screen.handle_key(key_event), - TuiEvent::Paste(_) => {} - TuiEvent::Draw | TuiEvent::Resize => { - let _ = tui.draw(u16::MAX, |frame| { - frame.render_widget_ref(&screen, frame.area()); - }); - } - } - } else { - screen.cancel(); - } - } - - screen.selection() -} - -struct ExternalAgentConfigSourceScreen { - request_frame: FrameRequester, - sources: Vec, - highlighted_idx: usize, - selection: Option, - done: bool, -} - -impl ExternalAgentConfigSourceScreen { - fn new(request_frame: FrameRequester, sources: &[ExternalAgentConfigMigrationSource]) -> Self { - Self { - request_frame, - sources: sources.to_vec(), - highlighted_idx: 0, - selection: None, - done: sources.is_empty(), - } - } - - fn handle_key(&mut self, key_event: KeyEvent) { - if key_event.kind == KeyEventKind::Release || self.sources.is_empty() { - return; - } - if key_event.modifiers.contains(KeyModifiers::CONTROL) - && matches!(key_event.code, KeyCode::Char('c') | KeyCode::Char('d')) - { - self.cancel(); - return; - } - match key_event.code { - KeyCode::Up | KeyCode::Char('k') => self.move_highlight(-1), - KeyCode::Down | KeyCode::Char('j') => self.move_highlight(1), - KeyCode::Char(character) if character.is_ascii_digit() => { - let Some(index) = character - .to_digit(10) - .and_then(|index| index.checked_sub(1)) - else { - return; - }; - self.select(index as usize); - } - KeyCode::Enter => self.select(self.highlighted_idx), - KeyCode::Esc => self.cancel(), - _ => {} - } - } - - fn move_highlight(&mut self, delta: isize) { - self.highlighted_idx = self - .highlighted_idx - .saturating_add_signed(delta) - .min(self.sources.len().saturating_sub(1)); - self.request_frame.schedule_frame(); - } - - fn select(&mut self, index: usize) { - let Some(source) = self.sources.get(index).copied() else { - return; - }; - self.selection = Some(source); - self.done = true; - self.request_frame.schedule_frame(); - } - - fn cancel(&mut self) { - self.selection = None; - self.done = true; - self.request_frame.schedule_frame(); - } - - fn is_done(&self) -> bool { - self.done - } - - fn selection(&self) -> Option { - self.selection - } -} - -impl WidgetRef for &ExternalAgentConfigSourceScreen { - fn render_ref(&self, area: Rect, buf: &mut Buffer) { - Clear.render(area, buf); - let mut column = ColumnRenderable::new(); - column.push(""); - column.push("Choose an import source".bold()); - column.push(""); - column.push( - Line::from("Select the app whose setup you want to import.".dim()).inset(Insets::tlbr( - /*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 0, - )), - ); - column.push(""); - for (index, source) in self.sources.iter().enumerate() { - let prefix = if index == self.highlighted_idx { - "›" - } else { - " " - }; - let label = Span::from(format!("{prefix} {}. {}", index + 1, source.label())); - column.push(Line::from(if index == self.highlighted_idx { - label.cyan() - } else { - label - })); - } - column.push(""); - column.push( - Line::from(vec![ - "Press ".dim(), - key_hint::plain(KeyCode::Enter).into(), - " to continue".dim(), - ]) - .inset(Insets::tlbr( - /*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 0, - )), - ); - column.render(area, buf); - } -} - -#[cfg(test)] -#[path = "external_agent_config_migration_source_tests.rs"] -mod tests; diff --git a/codex-rs/tui/src/external_agent_config_migration_source_tests.rs b/codex-rs/tui/src/external_agent_config_migration_source_tests.rs deleted file mode 100644 index a754da0ca5..0000000000 --- a/codex-rs/tui/src/external_agent_config_migration_source_tests.rs +++ /dev/null @@ -1,42 +0,0 @@ -use super::*; -use crate::test_backend::VT100Backend; -use crossterm::event::KeyEvent; -use crossterm::event::KeyModifiers; -use pretty_assertions::assert_eq; -use ratatui::Terminal; - -fn new_screen() -> ExternalAgentConfigSourceScreen { - ExternalAgentConfigSourceScreen::new( - FrameRequester::test_dummy(), - &ExternalAgentConfigMigrationSource::ALL, - ) -} - -#[test] -fn external_agent_config_source_prompt_snapshot() { - let screen = new_screen(); - let mut terminal = - Terminal::new(VT100Backend::new(/*width*/ 80, /*height*/ 12)).expect("terminal"); - terminal - .draw(|frame| frame.render_widget_ref(&screen, frame.area())) - .expect("render source prompt"); - insta::assert_snapshot!("external_agent_config_source_prompt", terminal.backend()); -} - -#[test] -fn external_agent_config_source_prompt_selects_highlighted_source() { - let mut screen = new_screen(); - screen.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); - screen.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - assert_eq!( - screen.selection(), - Some(ExternalAgentConfigMigrationSource::Cu) - ); -} - -#[test] -fn external_agent_config_source_prompt_can_cancel() { - let mut screen = new_screen(); - screen.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); - assert_eq!(screen.selection(), None); -} diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index c1c7878cef..dc8262f7db 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -116,7 +116,6 @@ mod exec_command; mod external_agent_config_migration; mod external_agent_config_migration_flow; mod external_agent_config_migration_model; -mod external_agent_config_migration_source; mod external_editor; mod file_search; mod frames; diff --git a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize.snap b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize.snap index 0848b419fa..8809334281 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize.snap @@ -4,9 +4,9 @@ expression: rendered --- > Choose what to import - Choose items to import. + Choose the Claude Code items to import. Codex may add files to your current project folder. - Your existing setup will not be changed. + Your existing Claude Code setup will not be changed. Home › [x] Settings (settings.json -> config.toml) Import /Users/alex/.claude/settings.json into /Users/alex/.codex/conf… @@ -21,7 +21,7 @@ expression: rendered • team-marketplace: asana • debug: sample • +1 more marketplaces - [x] Instructions + [x] Instructions (CLAUDE.md -> AGENTS.md) Import CLAUDE.md to AGENTS.md Selected 4 of 4 items. diff --git a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_action.snap b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_action.snap index 85ac123773..70132d75ae 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_action.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_action.snap @@ -4,9 +4,9 @@ expression: rendered --- > Choose what to import - Choose items to import. + Choose the Claude Code items to import. Codex may add files to your current project folder. - Your existing setup will not be changed. + Your existing Claude Code setup will not be changed. Home [x] Settings (settings.json -> config.toml) Import /Users/alex/.claude/settings.json into /Users/alex/.codex/conf… @@ -21,7 +21,7 @@ expression: rendered • team-marketplace: asana • debug: sample • +1 more marketplaces - [x] Instructions + [x] Instructions (CLAUDE.md -> AGENTS.md) Import CLAUDE.md to AGENTS.md Selected 4 of 4 items. diff --git a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_action_windows.snap b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_action_windows.snap index f9a89c85e2..48dfe2e22b 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_action_windows.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_action_windows.snap @@ -4,9 +4,9 @@ expression: rendered --- > Choose what to import - Choose items to import. + Choose the Claude Code items to import. Codex may add files to your current project folder. - Your existing setup will not be changed. + Your existing Claude Code setup will not be changed. Home [x] Settings (settings.json -> config.toml) Import /Users/alex/.claude/settings.json into /Users/alex/.codex/conf… @@ -21,7 +21,7 @@ expression: rendered • team-marketplace: asana • debug: sample • +1 more marketplaces - [x] Instructions + [x] Instructions (CLAUDE.md -> AGENTS.md) Import CLAUDE.md to AGENTS.md Selected 4 of 4 items. diff --git a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_windows.snap b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_windows.snap index ae7e7e310f..db6c14a0ca 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_windows.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_customize_windows.snap @@ -4,9 +4,9 @@ expression: rendered --- > Choose what to import - Choose items to import. + Choose the Claude Code items to import. Codex may add files to your current project folder. - Your existing setup will not be changed. + Your existing Claude Code setup will not be changed. Home › [x] Settings (settings.json -> config.toml) Import /Users/alex/.claude/settings.json into /Users/alex/.codex/conf… @@ -21,7 +21,7 @@ expression: rendered • team-marketplace: asana • debug: sample • +1 more marketplaces - [x] Instructions + [x] Instructions (CLAUDE.md -> AGENTS.md) Import CLAUDE.md to AGENTS.md Selected 4 of 4 items. diff --git a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_prompt.snap b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_prompt.snap index 4d9ded3ea6..165473c5a7 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_prompt.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_prompt.snap @@ -3,10 +3,11 @@ source: tui/src/external_agent_config_migration.rs expression: rendered --- - > Import setup - Bring over supported setup from another coding agent. + > Import from Claude Code + Bring over your setup, current project, and recent chats. Codex may add files to your current project folder. - Your existing setup will not be changed. + Your existing Claude Code setup will not be changed. + Standard Claude Chat data cannot be imported. [x] Tools & setup Settings, instructions, integrations, agents, commands, and skills Importing: Settings 1 diff --git a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_prompt_windows.snap b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_prompt_windows.snap index 4d9ded3ea6..165473c5a7 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_prompt_windows.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_prompt_windows.snap @@ -3,10 +3,11 @@ source: tui/src/external_agent_config_migration.rs expression: rendered --- - > Import setup - Bring over supported setup from another coding agent. + > Import from Claude Code + Bring over your setup, current project, and recent chats. Codex may add files to your current project folder. - Your existing setup will not be changed. + Your existing Claude Code setup will not be changed. + Standard Claude Chat data cannot be imported. [x] Tools & setup Settings, instructions, integrations, agents, commands, and skills Importing: Settings 1 diff --git a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration_flow__tests__external_agent_config_migration_messages.snap b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration_flow__tests__external_agent_config_migration_messages.snap index cf1e6ce6b4..51367fe90a 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration_flow__tests__external_agent_config_migration_messages.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration_flow__tests__external_agent_config_migration_messages.snap @@ -2,7 +2,7 @@ source: tui/src/external_agent_config_migration_flow_tests.rs expression: messages --- -• Import started. You can keep working while it finishes. +• Claude Code import started. You can keep working while it finishes. Imported setup will apply to new chats. Importing: Settings: 1 @@ -10,7 +10,7 @@ expression: messages MCP servers: 2 — docs, issues Chat sessions: 3 — Alpha rollout, Beta review, Gamma notes Plugins: 2 — formatter, reviewer -• Import started. You can keep working while it finishes. +• Claude Code import started. You can keep working while it finishes. Imported setup will apply to new chats. Importing: Settings: 1 @@ -19,7 +19,7 @@ expression: messages Chat sessions: 3 — Alpha rollout, Beta review, Gamma notes Plugins: 2 — formatter, reviewer 1 additional item remains. After it finishes, run /import again to review it. -• Import started. You can keep working while it finishes. +• Claude Code import started. You can keep working while it finishes. Imported setup will apply to new chats. Importing: Settings: 1 @@ -28,12 +28,12 @@ expression: messages Chat sessions: 3 — Alpha rollout, Beta review, Gamma notes Plugins: 2 — formatter, reviewer 2 additional items remain. After it finishes, run /import again to review them. -• Import finished: 2 imported, 1 failed. +• Claude Code import finished: 2 imported, 1 failed. Results by type: Settings: 1 imported, 0 failed Plugins: 1 imported, 1 failed Run /import again to check for additional items. -No compatible setup was found to import. -Import from other apps is unavailable in remote sessions. Start Codex locally and run /import. -Import from other apps is unavailable while Codex is connected to the local app-server daemon. Stop the daemon, restart Codex, and run /import. -A previous external agent import is still running. Wait for it to finish before importing again. +No Claude Code setup was found to import. +Import from Claude Code is unavailable in remote sessions. Start Codex locally and run /import. +Import from Claude Code is unavailable while Codex is connected to the local app-server daemon. Stop the daemon, restart Codex, and run /import. +A previous Claude Code import is still running. Wait for it to finish before importing again. diff --git a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration_source__tests__external_agent_config_source_prompt.snap b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration_source__tests__external_agent_config_source_prompt.snap deleted file mode 100644 index 0e994508bf..0000000000 --- a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration_source__tests__external_agent_config_source_prompt.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: tui/src/external_agent_config_migration_source_tests.rs -expression: terminal.backend() ---- - -Choose an import source - - Select the app whose setup you want to import. - -› 1. Claude Code - 2. Cursor - - Press enter to continue