Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions codex-rs/app-server-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(_)
Expand Down Expand Up @@ -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
}));
Expand Down
10 changes: 9 additions & 1 deletion codex-rs/app-server/src/in_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(_)
)
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -893,5 +896,10 @@ mod tests {
},
})
));
assert!(server_notification_requires_delivery(
&ServerNotification::ExternalAgentConfigImportCompleted(
ExternalAgentConfigImportCompletedNotification {},
)
));
}
}
8 changes: 8 additions & 0 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
11 changes: 10 additions & 1 deletion codex-rs/tui/src/app/app_server_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
26 changes: 26 additions & 0 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) => {
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down
67 changes: 67 additions & 0 deletions codex-rs/tui/src/app_server_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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),
}
}

Expand All @@ -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;
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/tui/src/chatwidget/slash_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -1010,6 +1014,7 @@ impl ChatWidget {
| SlashCommand::Logout
| SlashCommand::Mention
| SlashCommand::Skills
| SlashCommand::Import
| SlashCommand::Hooks
| SlashCommand::Title
| SlashCommand::Statusline
Expand Down
12 changes: 12 additions & 0 deletions codex-rs/tui/src/chatwidget/tests/slash_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/external_agent_config_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions codex-rs/tui/src/external_agent_config_migration/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()]);
Expand Down
Loading
Loading