Codex Handbook
tui/src/external_agent_config_migration_model.rs 147 lines
use codex_app_server_protocol::ExternalAgentConfigMigrationItem;use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;use std::collections::BTreeSet;#[derive(Clone, Debug)]pub(crate) struct ExternalAgentConfigMigrationGroupModel {    pub(crate) label: String,    pub(crate) description: &'static str,    pub(crate) item_indices: Vec<usize>,}pub(crate) fn external_agent_config_migration_groups(    items: &[ExternalAgentConfigMigrationItem],) -> Vec<ExternalAgentConfigMigrationGroupModel> {    let tools_and_setup = items        .iter()        .enumerate()        .filter_map(|(idx, item)| {            (item.cwd.is_none() && item.item_type != ExternalAgentConfigMigrationItemType::Sessions)                .then_some(idx)        })        .collect::<Vec<_>>();    let projects = items        .iter()        .enumerate()        .filter_map(|(idx, item)| {            (item.cwd.is_some() && item.item_type != ExternalAgentConfigMigrationItemType::Sessions)                .then_some(idx)        })        .collect::<Vec<_>>();    let chat_sessions = items        .iter()        .enumerate()        .filter_map(|(idx, item)| {            (item.item_type == ExternalAgentConfigMigrationItemType::Sessions).then_some(idx)        })        .collect::<Vec<_>>();    let mut groups = Vec::new();    if !tools_and_setup.is_empty() {        groups.push(ExternalAgentConfigMigrationGroupModel {            label: "Tools & setup".to_string(),            description: "Settings, instructions, integrations, agents, commands, and skills",            item_indices: tools_and_setup,        });    }    if !projects.is_empty() {        let project_count = projects            .iter()            .filter_map(|idx| items[*idx].cwd.as_deref())            .collect::<BTreeSet<_>>()            .len();        groups.push(ExternalAgentConfigMigrationGroupModel {            label: if project_count == 1 {                "Current project".to_string()            } else {                format!("Projects ({project_count})")            },            description: "Add Codex files alongside your existing project files",            item_indices: projects,        });    }    if !chat_sessions.is_empty() {        let session_count = chat_sessions            .iter()            .filter_map(|idx| items[*idx].details.as_ref())            .map(|details| details.sessions.len())            .sum::<usize>();        groups.push(ExternalAgentConfigMigrationGroupModel {            label: format!("Chat sessions ({session_count})"),            description: "Last 30 days of chats",            item_indices: chat_sessions,        });    }    groups}pub(crate) fn external_agent_config_migration_item_label(    item: &ExternalAgentConfigMigrationItem,) -> &'static str {    match item.item_type {        ExternalAgentConfigMigrationItemType::AgentsMd => "Instructions (CLAUDE.md -> AGENTS.md)",        ExternalAgentConfigMigrationItemType::Config => "Settings (settings.json -> config.toml)",        ExternalAgentConfigMigrationItemType::Skills => "Skills",        ExternalAgentConfigMigrationItemType::Plugins => "Plugins",        ExternalAgentConfigMigrationItemType::McpServerConfig => "MCP servers",        ExternalAgentConfigMigrationItemType::Subagents => "Agents",        ExternalAgentConfigMigrationItemType::Hooks => "Hooks",        ExternalAgentConfigMigrationItemType::Commands => "Slash commands",        ExternalAgentConfigMigrationItemType::Sessions => "Recent chat sessions",    }}pub(crate) fn external_agent_config_migration_item_detail(    item: &ExternalAgentConfigMigrationItem,) -> Option<String> {    let details = item.details.as_ref()?;    match item.item_type {        ExternalAgentConfigMigrationItemType::Plugins => None,        ExternalAgentConfigMigrationItemType::McpServerConfig => Some(format_counted_details(            "MCP server",            details.mcp_servers.len(),            details                .mcp_servers                .iter()                .map(|server| server.name.as_str()),        )),        ExternalAgentConfigMigrationItemType::Subagents => Some(format_counted_details(            "agent",            details.subagents.len(),            details.subagents.iter().map(|agent| agent.name.as_str()),        )),        ExternalAgentConfigMigrationItemType::Hooks => Some(format_counted_details(            "hook",            details.hooks.len(),            details.hooks.iter().map(|hook| hook.name.as_str()),        )),        ExternalAgentConfigMigrationItemType::Commands => Some(format_counted_details(            "slash command",            details.commands.len(),            details.commands.iter().map(|command| command.name.as_str()),        )),        ExternalAgentConfigMigrationItemType::Sessions => Some(format_counted_details(            "chat session",            details.sessions.len(),            details                .sessions                .iter()                .filter_map(|session| session.title.as_deref()),        )),        ExternalAgentConfigMigrationItemType::AgentsMd        | ExternalAgentConfigMigrationItemType::Config        | ExternalAgentConfigMigrationItemType::Skills => None,    }}fn format_counted_details<'a>(    noun: &str,    count: usize,    names: impl Iterator<Item = &'a str>,) -> String {    let suffix = if count == 1 { "" } else { "s" };    match names.take(4).collect::<Vec<_>>() {        names if names.is_empty() => format!("{count} {noun}{suffix}"),        names => format!("{count} {noun}{suffix}: {}", names.join(", ")),    }}