mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] add /import for external agents (#27071)
## Why External-agent import should be discoverable and deliberate without blocking startup or claiming the public `codex [PROMPT]` CLI namespace. The slash command keeps the flow local to the interactive TUI and reuses the existing app-server import API. ## What changed - add the user-facing `/import` slash command - detect external-agent importable items only when the command is invoked - run imports through the embedded local app-server - show start and completion messages, refresh configuration, and block duplicate imports while one is pending - reject the flow for unsupported remote and local-daemon sessions ## Validation - `just test -p codex-tui external_agent_config_migration` (10 passed) - manually exercised an isolated TUI fixture with existing external-agent setup and session data using a fresh `CODEX_HOME` - verified picker customization, plugin and session detection, import completion, repeated invocation, and imported-session resume context - the broader `just test -p codex-tui` run passed 2,805 tests, with 2 unrelated guardian feature-flag failures and 4 skipped tests ## Draft follow-ups - review whether completion messaging should remain attached to the initiating chat if the user switches chats during an import - review shutdown semantics for an in-progress background import ## 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 4.** Draft while the lower stack dependencies are reviewed.
This commit is contained in:
committed by
GitHub
Unverified
parent
1deae7bd4a
commit
b4445f2758
@@ -158,6 +158,7 @@ pub(crate) fn server_notification_requires_delivery(notification: &ServerNotific
|
||||
ServerNotification::TurnCompleted(_)
|
||||
| ServerNotification::ThreadSettingsUpdated(_)
|
||||
| ServerNotification::ItemCompleted(_)
|
||||
| ServerNotification::ExternalAgentConfigImportCompleted(_)
|
||||
| ServerNotification::AgentMessageDelta(_)
|
||||
| ServerNotification::PlanDelta(_)
|
||||
| ServerNotification::ReasoningSummaryTextDelta(_)
|
||||
@@ -2139,6 +2140,13 @@ mod tests {
|
||||
)
|
||||
)
|
||||
));
|
||||
assert!(event_requires_delivery(
|
||||
&InProcessServerEvent::ServerNotification(
|
||||
codex_app_server_protocol::ServerNotification::ExternalAgentConfigImportCompleted(
|
||||
codex_app_server_protocol::ExternalAgentConfigImportCompletedNotification {},
|
||||
)
|
||||
)
|
||||
));
|
||||
assert!(!event_requires_delivery(&InProcessServerEvent::Lagged {
|
||||
skipped: 1
|
||||
}));
|
||||
|
||||
@@ -104,7 +104,9 @@ type PendingClientRequestResponse = std::result::Result<Result, JSONRPCErrorErro
|
||||
fn server_notification_requires_delivery(notification: &ServerNotification) -> bool {
|
||||
matches!(
|
||||
notification,
|
||||
ServerNotification::TurnCompleted(_) | ServerNotification::ThreadSettingsUpdated(_)
|
||||
ServerNotification::TurnCompleted(_)
|
||||
| ServerNotification::ThreadSettingsUpdated(_)
|
||||
| ServerNotification::ExternalAgentConfigImportCompleted(_)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -729,6 +731,7 @@ mod tests {
|
||||
use super::*;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ConfigRequirementsReadResponse;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportCompletedNotification;
|
||||
use codex_app_server_protocol::SessionSource as ApiSessionSource;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
@@ -893,5 +896,10 @@ mod tests {
|
||||
},
|
||||
})
|
||||
));
|
||||
assert!(server_notification_requires_delivery(
|
||||
&ServerNotification::ExternalAgentConfigImportCompleted(
|
||||
ExternalAgentConfigImportCompletedNotification {},
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2600,6 +2600,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_remains_an_interactive_prompt() {
|
||||
let cli = MultitoolCli::try_parse_from(["codex", "import"]).expect("parse");
|
||||
|
||||
assert!(cli.subcommand.is_none());
|
||||
assert_eq!(cli.interactive.prompt.as_deref(), Some("import"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_v2_rejects_non_plain_names_at_parse_time() {
|
||||
assert!(
|
||||
|
||||
@@ -93,16 +93,25 @@ impl App {
|
||||
return;
|
||||
}
|
||||
ServerNotification::ExternalAgentConfigImportCompleted(_) => {
|
||||
let cwd = self.chat_widget.config_ref().cwd.to_path_buf();
|
||||
let should_report_completion =
|
||||
app_server_client.consume_external_agent_config_import_completion();
|
||||
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"failed to refresh config after external agent config import"
|
||||
);
|
||||
}
|
||||
let cwd = self.chat_widget.config_ref().cwd.to_path_buf();
|
||||
self.chat_widget.refresh_plugin_mentions();
|
||||
self.chat_widget.submit_op(AppCommand::reload_user_config());
|
||||
self.fetch_plugins_list(app_server_client, cwd);
|
||||
if should_report_completion {
|
||||
self.chat_widget.add_info_message(
|
||||
crate::external_agent_config_migration_flow::EXTERNAL_AGENT_CONFIG_MIGRATION_FINISHED_MESSAGE
|
||||
.to_string(),
|
||||
/*hint*/ None,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ServerNotification::AppListUpdated(notification) => {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use super::resize_reflow::trailing_run_start;
|
||||
use super::*;
|
||||
use crate::config_update::format_config_error;
|
||||
use crate::external_agent_config_migration_flow::ExternalAgentConfigMigrationFlowOutcome;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_config::types::WindowsSandboxModeToml;
|
||||
|
||||
@@ -110,6 +111,31 @@ impl App {
|
||||
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
AppEvent::OpenExternalAgentConfigMigration => {
|
||||
match crate::external_agent_config_migration_flow::handle_external_agent_config_migration_prompt(
|
||||
tui,
|
||||
app_server,
|
||||
&self.config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(ExternalAgentConfigMigrationFlowOutcome::Started(message)) => {
|
||||
self.chat_widget.add_info_message(message, /*hint*/ None);
|
||||
}
|
||||
Ok(ExternalAgentConfigMigrationFlowOutcome::NoItems) => {
|
||||
self.chat_widget.add_info_message(
|
||||
crate::external_agent_config_migration_flow::EXTERNAL_AGENT_CONFIG_MIGRATION_NO_ITEMS_MESSAGE
|
||||
.to_string(),
|
||||
/*hint*/ None,
|
||||
);
|
||||
}
|
||||
Ok(ExternalAgentConfigMigrationFlowOutcome::Cancelled) => {}
|
||||
Err(error_message) => {
|
||||
self.chat_widget.add_error_message(error_message);
|
||||
}
|
||||
}
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
AppEvent::ResumeSessionByIdOrName(id_or_name) => {
|
||||
match crate::lookup_session_target_with_app_server(app_server, &id_or_name).await? {
|
||||
Some(target_session) => {
|
||||
|
||||
@@ -221,6 +221,9 @@ pub(crate) enum AppEvent {
|
||||
/// Open the resume picker inside the running TUI session.
|
||||
OpenResumePicker,
|
||||
|
||||
/// Open the external agent migration picker inside the running TUI session.
|
||||
OpenExternalAgentConfigMigration,
|
||||
|
||||
/// Resume a thread by UUID or thread name inside the running TUI session.
|
||||
ResumeSessionByIdOrName(String),
|
||||
|
||||
|
||||
@@ -22,6 +22,11 @@ use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
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::GetAccountRateLimitsResponse;
|
||||
use codex_app_server_protocol::GetAccountResponse;
|
||||
@@ -123,12 +128,16 @@ use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
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 agent 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 {
|
||||
@@ -171,6 +180,7 @@ pub(crate) struct AppServerSession {
|
||||
thread_settings_update_supported: bool,
|
||||
default_model: Option<String>,
|
||||
available_models: Vec<ModelPreset>,
|
||||
external_agent_config_import_completion_pending: AtomicBool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -214,6 +224,7 @@ impl AppServerSession {
|
||||
thread_settings_update_supported: true,
|
||||
default_model: None,
|
||||
available_models: Vec::new(),
|
||||
external_agent_config_import_completion_pending: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +241,10 @@ impl AppServerSession {
|
||||
matches!(self.thread_params_mode, ThreadParamsMode::Remote)
|
||||
}
|
||||
|
||||
pub(crate) fn uses_embedded_app_server(&self) -> bool {
|
||||
matches!(&self.client, AppServerClient::InProcess(_))
|
||||
}
|
||||
|
||||
pub(crate) fn server_version(&self) -> Option<&str> {
|
||||
let AppServerClient::Remote(client) = &self.client else {
|
||||
return None;
|
||||
@@ -344,6 +359,58 @@ impl AppServerSession {
|
||||
.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 agent import")
|
||||
}
|
||||
|
||||
pub(crate) async fn external_agent_config_import(
|
||||
&mut self,
|
||||
migration_items: Vec<ExternalAgentConfigMigrationItem>,
|
||||
) -> Result<()> {
|
||||
// Mark the import active before sending the request so a fast completion notification
|
||||
// cannot arrive before the TUI records it.
|
||||
if self
|
||||
.external_agent_config_import_completion_pending
|
||||
.swap(true, Ordering::Relaxed)
|
||||
{
|
||||
color_eyre::eyre::bail!(EXTERNAL_AGENT_CONFIG_IMPORT_IN_PROGRESS_MESSAGE);
|
||||
}
|
||||
let request_id = self.next_request_id();
|
||||
let response: Result<ExternalAgentConfigImportResponse> = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ExternalAgentConfigImport {
|
||||
request_id,
|
||||
params: ExternalAgentConfigImportParams { migration_items },
|
||||
})
|
||||
.await
|
||||
.wrap_err("externalAgentConfig/import failed during agent import");
|
||||
match response {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
self.external_agent_config_import_completion_pending
|
||||
.store(false, Ordering::Relaxed);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn external_agent_config_import_in_progress(&self) -> bool {
|
||||
self.external_agent_config_import_completion_pending
|
||||
.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn consume_external_agent_config_import_completion(&self) -> bool {
|
||||
self.external_agent_config_import_completion_pending
|
||||
.swap(false, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) async fn next_event(&mut self) -> Option<AppServerEvent> {
|
||||
self.client.next_event().await
|
||||
}
|
||||
|
||||
@@ -398,6 +398,10 @@ impl ChatWidget {
|
||||
SlashCommand::Skills => {
|
||||
self.open_skills_menu();
|
||||
}
|
||||
SlashCommand::Import => {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::OpenExternalAgentConfigMigration);
|
||||
}
|
||||
SlashCommand::Hooks => {
|
||||
self.add_hooks_output();
|
||||
}
|
||||
@@ -1010,6 +1014,7 @@ impl ChatWidget {
|
||||
| SlashCommand::Logout
|
||||
| SlashCommand::Mention
|
||||
| SlashCommand::Skills
|
||||
| SlashCommand::Import
|
||||
| SlashCommand::Hooks
|
||||
| SlashCommand::Title
|
||||
| SlashCommand::Statusline
|
||||
|
||||
@@ -1861,6 +1861,18 @@ async fn slash_resume_opens_picker() {
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenResumePicker));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_import_opens_claude_code_import_picker() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Import);
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::OpenExternalAgentConfigMigration)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_archive_confirmation_requests_current_thread_archive() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -758,7 +758,7 @@ mod tests {
|
||||
},
|
||||
ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Sessions,
|
||||
description: "Migrate recent Claude Code sessions".to_string(),
|
||||
description: "Migrate recent chat sessions".to_string(),
|
||||
cwd: None,
|
||||
details: Some(codex_app_server_protocol::MigrationDetails {
|
||||
sessions: vec![SessionMigration {
|
||||
|
||||
@@ -80,13 +80,13 @@ impl WidgetRef for &ExternalAgentConfigMigrationScreen {
|
||||
MigrationView::Summary => vec![
|
||||
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 Claude Code setup will not be changed."),
|
||||
Line::from("Standard Claude Chat data cannot be imported."),
|
||||
Line::from("Your existing agent setup will not be changed."),
|
||||
Line::from("Cloud-hosted chat data cannot be imported."),
|
||||
],
|
||||
MigrationView::Customize => vec![
|
||||
Line::from("Choose the Claude Code items to import."),
|
||||
Line::from("Choose the items to import."),
|
||||
Line::from("Codex may add files to your current project folder."),
|
||||
Line::from("Your existing Claude Code setup will not be changed."),
|
||||
Line::from("Your existing agent setup will not be changed."),
|
||||
],
|
||||
};
|
||||
let intro_height = intro_lines.len() as u16;
|
||||
@@ -119,7 +119,7 @@ impl WidgetRef for &ExternalAgentConfigMigrationScreen {
|
||||
.areas(inner_area);
|
||||
|
||||
let title = match self.view {
|
||||
MigrationView::Summary => "Import from Claude Code",
|
||||
MigrationView::Summary => "Import from another coding agent",
|
||||
MigrationView::Customize => "Choose what to import",
|
||||
};
|
||||
let heading = Line::from(vec!["> ".into(), title.bold()]);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::app_server_session::EXTERNAL_AGENT_CONFIG_IMPORT_IN_PROGRESS_MESSAGE;
|
||||
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::tui;
|
||||
use codex_app_server_protocol::ExternalAgentConfigDetectParams;
|
||||
|
||||
pub(crate) const EXTERNAL_AGENT_CONFIG_MIGRATION_FINISHED_MESSAGE: &str =
|
||||
"Agent import finished. Run /import again to check for additional items.";
|
||||
pub(crate) const EXTERNAL_AGENT_CONFIG_MIGRATION_NO_ITEMS_MESSAGE: &str =
|
||||
"No supported agent setup was found to import.";
|
||||
pub(crate) const EXTERNAL_AGENT_CONFIG_MIGRATION_REMOTE_UNAVAILABLE_MESSAGE: &str =
|
||||
"Agent import is unavailable in remote sessions. Start Codex locally and run /import.";
|
||||
pub(crate) const EXTERNAL_AGENT_CONFIG_MIGRATION_DAEMON_UNAVAILABLE_MESSAGE: &str = "Agent import 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(String),
|
||||
NoItems,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
fn external_agent_config_migration_success_message(remaining_item_count: usize) -> String {
|
||||
let message = "Agent import started. You can keep working while it finishes. Imported setup will apply to new chats.";
|
||||
match remaining_items_handoff(remaining_item_count) {
|
||||
Some(remaining_items_handoff) => format!("{message} {remaining_items_handoff}"),
|
||||
None => message.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn remaining_items_handoff(remaining_item_count: usize) -> Option<String> {
|
||||
match remaining_item_count {
|
||||
0 => None,
|
||||
1 => Some(
|
||||
"1 additional item remains. After it finishes, run /import again to review it."
|
||||
.to_string(),
|
||||
),
|
||||
_ => Some(format!(
|
||||
"{remaining_item_count} additional items remain. After it finishes, run /import again to review them."
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_external_agent_config_migration_prompt(
|
||||
tui: &mut tui::Tui,
|
||||
app_server: &mut AppServerSession,
|
||||
config: &Config,
|
||||
) -> Result<ExternalAgentConfigMigrationFlowOutcome, String> {
|
||||
if app_server.uses_remote_workspace() {
|
||||
return Err(EXTERNAL_AGENT_CONFIG_MIGRATION_REMOTE_UNAVAILABLE_MESSAGE.to_string());
|
||||
}
|
||||
if !app_server.uses_embedded_app_server() {
|
||||
return Err(EXTERNAL_AGENT_CONFIG_MIGRATION_DAEMON_UNAVAILABLE_MESSAGE.to_string());
|
||||
}
|
||||
if app_server.external_agent_config_import_in_progress() {
|
||||
return Err(EXTERNAL_AGENT_CONFIG_IMPORT_IN_PROGRESS_MESSAGE.to_string());
|
||||
}
|
||||
|
||||
let cwd = config.cwd.to_path_buf();
|
||||
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 agent setup: {err}"));
|
||||
}
|
||||
};
|
||||
|
||||
if detected_items.is_empty() {
|
||||
return Ok(ExternalAgentConfigMigrationFlowOutcome::NoItems);
|
||||
}
|
||||
|
||||
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 remaining_item_count =
|
||||
detected_items.len().saturating_sub(selected_items.len());
|
||||
let success_message =
|
||||
external_agent_config_migration_success_message(remaining_item_count);
|
||||
return Ok(ExternalAgentConfigMigrationFlowOutcome::Started(
|
||||
success_message,
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
cwd = %cwd.display(),
|
||||
"failed to import external agent config migration items"
|
||||
);
|
||||
error = Some(format!("Import failed: {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
ExternalAgentConfigMigrationOutcome::Skip => {
|
||||
return Ok(ExternalAgentConfigMigrationFlowOutcome::Cancelled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "external_agent_config_migration_flow_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,21 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn external_agent_config_migration_messages_snapshot() {
|
||||
let cases = [0, 1, 2];
|
||||
|
||||
let messages = cases
|
||||
.map(external_agent_config_migration_success_message)
|
||||
.into_iter()
|
||||
.chain([
|
||||
EXTERNAL_AGENT_CONFIG_MIGRATION_FINISHED_MESSAGE.to_string(),
|
||||
EXTERNAL_AGENT_CONFIG_MIGRATION_NO_ITEMS_MESSAGE.to_string(),
|
||||
EXTERNAL_AGENT_CONFIG_MIGRATION_REMOTE_UNAVAILABLE_MESSAGE.to_string(),
|
||||
EXTERNAL_AGENT_CONFIG_MIGRATION_DAEMON_UNAVAILABLE_MESSAGE.to_string(),
|
||||
EXTERNAL_AGENT_CONFIG_IMPORT_IN_PROGRESS_MESSAGE.to_string(),
|
||||
])
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
insta::assert_snapshot!("external_agent_config_migration_messages", messages);
|
||||
}
|
||||
@@ -130,8 +130,8 @@ mod diff_model;
|
||||
mod diff_render;
|
||||
mod exec_cell;
|
||||
mod exec_command;
|
||||
#[allow(dead_code)]
|
||||
mod external_agent_config_migration;
|
||||
mod external_agent_config_migration_flow;
|
||||
mod external_agent_config_migration_model;
|
||||
mod external_editor;
|
||||
mod file_search;
|
||||
|
||||
@@ -26,6 +26,7 @@ pub enum SlashCommand {
|
||||
AutoReview,
|
||||
Memories,
|
||||
Skills,
|
||||
Import,
|
||||
Hooks,
|
||||
Review,
|
||||
Rename,
|
||||
@@ -98,6 +99,9 @@ impl SlashCommand {
|
||||
SlashCommand::Diff => "show git diff (including untracked files)",
|
||||
SlashCommand::Mention => "mention a file",
|
||||
SlashCommand::Skills => "use skills to improve how Codex performs specific tasks",
|
||||
SlashCommand::Import => {
|
||||
"import setup, this project, and recent chats from another coding agent"
|
||||
}
|
||||
SlashCommand::Hooks => "view and manage lifecycle hooks",
|
||||
SlashCommand::Status => "show current session configuration and token usage",
|
||||
SlashCommand::DebugConfig => "show config layers and requirement sources for debugging",
|
||||
@@ -198,6 +202,7 @@ impl SlashCommand {
|
||||
| SlashCommand::SandboxReadRoot
|
||||
| SlashCommand::Experimental
|
||||
| SlashCommand::Memories
|
||||
| SlashCommand::Import
|
||||
| SlashCommand::Review
|
||||
| SlashCommand::Plan
|
||||
| SlashCommand::Clear
|
||||
|
||||
+3
-3
@@ -4,14 +4,14 @@ expression: rendered
|
||||
---
|
||||
|
||||
> Choose what to import
|
||||
Choose the Claude Code items to import.
|
||||
Choose the items to import.
|
||||
Codex may add files to your current project folder.
|
||||
Your existing Claude Code setup will not be changed.
|
||||
Your existing agent setup will not be changed.
|
||||
Home
|
||||
› [x] Settings (settings.json -> config.toml)
|
||||
Import /Users/alex/.claude/settings.json into /Users/alex/.codex/conf…
|
||||
[x] Recent chat sessions
|
||||
Import recent Claude Code sessions
|
||||
Import recent chat sessions
|
||||
1 chat session: Investigate migration UX
|
||||
|
||||
Current project: /workspace/project
|
||||
|
||||
+3
-3
@@ -4,14 +4,14 @@ expression: rendered
|
||||
---
|
||||
|
||||
> Choose what to import
|
||||
Choose the Claude Code items to import.
|
||||
Choose the items to import.
|
||||
Codex may add files to your current project folder.
|
||||
Your existing Claude Code setup will not be changed.
|
||||
Your existing agent setup will not be changed.
|
||||
Home
|
||||
[x] Settings (settings.json -> config.toml)
|
||||
Import /Users/alex/.claude/settings.json into /Users/alex/.codex/conf…
|
||||
[x] Recent chat sessions
|
||||
Import recent Claude Code sessions
|
||||
Import recent chat sessions
|
||||
1 chat session: Investigate migration UX
|
||||
|
||||
Current project: /workspace/project
|
||||
|
||||
+3
-3
@@ -4,14 +4,14 @@ expression: rendered
|
||||
---
|
||||
|
||||
> Choose what to import
|
||||
Choose the Claude Code items to import.
|
||||
Choose the items to import.
|
||||
Codex may add files to your current project folder.
|
||||
Your existing Claude Code setup will not be changed.
|
||||
Your existing agent setup will not be changed.
|
||||
Home
|
||||
[x] Settings (settings.json -> config.toml)
|
||||
Import /Users/alex/.claude/settings.json into /Users/alex/.codex/conf…
|
||||
[x] Recent chat sessions
|
||||
Import recent Claude Code sessions
|
||||
Import recent chat sessions
|
||||
1 chat session: Investigate migration UX
|
||||
|
||||
Current project: C:\workspace\project
|
||||
|
||||
+3
-3
@@ -4,14 +4,14 @@ expression: rendered
|
||||
---
|
||||
|
||||
> Choose what to import
|
||||
Choose the Claude Code items to import.
|
||||
Choose the items to import.
|
||||
Codex may add files to your current project folder.
|
||||
Your existing Claude Code setup will not be changed.
|
||||
Your existing agent setup will not be changed.
|
||||
Home
|
||||
› [x] Settings (settings.json -> config.toml)
|
||||
Import /Users/alex/.claude/settings.json into /Users/alex/.codex/conf…
|
||||
[x] Recent chat sessions
|
||||
Import recent Claude Code sessions
|
||||
Import recent chat sessions
|
||||
1 chat session: Investigate migration UX
|
||||
|
||||
Current project: C:\workspace\project
|
||||
|
||||
+3
-3
@@ -3,11 +3,11 @@ source: tui/src/external_agent_config_migration.rs
|
||||
expression: rendered
|
||||
---
|
||||
|
||||
> Import from Claude Code
|
||||
> Import from another coding agent
|
||||
Bring over your setup, current project, and recent chats.
|
||||
Codex may add files to your current project folder.
|
||||
Your existing Claude Code setup will not be changed.
|
||||
Standard Claude Chat data cannot be imported.
|
||||
Your existing agent setup will not be changed.
|
||||
Cloud-hosted chat data cannot be imported.
|
||||
[x] Tools & setup
|
||||
Settings, instructions, integrations, agents, commands, and skills
|
||||
[x] Current project
|
||||
|
||||
+3
-3
@@ -3,11 +3,11 @@ source: tui/src/external_agent_config_migration.rs
|
||||
expression: rendered
|
||||
---
|
||||
|
||||
> Import from Claude Code
|
||||
> Import from another coding agent
|
||||
Bring over your setup, current project, and recent chats.
|
||||
Codex may add files to your current project folder.
|
||||
Your existing Claude Code setup will not be changed.
|
||||
Standard Claude Chat data cannot be imported.
|
||||
Your existing agent setup will not be changed.
|
||||
Cloud-hosted chat data cannot be imported.
|
||||
[x] Tools & setup
|
||||
Settings, instructions, integrations, agents, commands, and skills
|
||||
[x] Current project
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: tui/src/external_agent_config_migration_flow_tests.rs
|
||||
expression: messages
|
||||
---
|
||||
Agent import started. You can keep working while it finishes. Imported setup will apply to new chats.
|
||||
Agent import started. You can keep working while it finishes. Imported setup will apply to new chats. 1 additional item remains. After it finishes, run /import again to review it.
|
||||
Agent import started. You can keep working while it finishes. Imported setup will apply to new chats. 2 additional items remain. After it finishes, run /import again to review them.
|
||||
Agent import finished. Run /import again to check for additional items.
|
||||
No supported agent setup was found to import.
|
||||
Agent import is unavailable in remote sessions. Start Codex locally and run /import.
|
||||
Agent import is unavailable while Codex is connected to the local app-server daemon. Stop the daemon, restart Codex, and run /import.
|
||||
A previous agent import is still running. Wait for it to finish before importing again.
|
||||
Reference in New Issue
Block a user