Codex Handbook
tui/src/status/helpers.rs 274 lines
use crate::exec_command::relativize_to_home;use crate::legacy_core::config::Config;use crate::status::StatusAccountDisplay;use crate::text_formatting;use chrono::DateTime;use chrono::Local;use codex_protocol::account::PlanType;use codex_utils_absolute_path::AbsolutePathBuf;use std::path::Path;use unicode_width::UnicodeWidthStr;fn normalize_agents_display_path(path: &Path) -> String {    dunce::simplified(path).display().to_string()}pub(crate) fn compose_model_display(    model_name: &str,    entries: &[(&str, String)],) -> (String, Vec<String>) {    let mut details: Vec<String> = Vec::new();    if let Some((_, effort)) = entries.iter().find(|(k, _)| *k == "reasoning effort") {        details.push(format!("reasoning {}", effort.to_ascii_lowercase()));    }    if let Some((_, summary)) = entries.iter().find(|(k, _)| *k == "reasoning summaries") {        let summary = summary.trim();        if summary.eq_ignore_ascii_case("none") || summary.eq_ignore_ascii_case("off") {            details.push("summaries off".to_string());        } else if !summary.is_empty() {            details.push(format!("summaries {}", summary.to_ascii_lowercase()));        }    }    (model_name.to_string(), details)}pub(crate) fn compose_agents_summary(config: &Config, paths: &[AbsolutePathBuf]) -> String {    let mut rels: Vec<String> = Vec::new();    for p in paths {        let p = p.as_path();        let file_name = p            .file_name()            .map(|name| name.to_string_lossy().to_string())            .unwrap_or_else(|| "<unknown>".to_string());        let display = if let Some(parent) = p.parent() {            if parent == config.cwd.as_path() {                file_name.clone()            } else {                let mut cur = config.cwd.as_path();                let mut ups = 0usize;                let mut reached = false;                while let Some(c) = cur.parent() {                    if cur == parent {                        reached = true;                        break;                    }                    cur = c;                    ups += 1;                }                if reached {                    let up = format!("..{}", std::path::MAIN_SEPARATOR);                    format!("{}{}", up.repeat(ups), file_name)                } else if let Ok(stripped) = p.strip_prefix(&config.cwd) {                    normalize_agents_display_path(stripped)                } else {                    normalize_agents_display_path(p)                }            }        } else {            normalize_agents_display_path(p)        };        rels.push(display);    }    if rels.is_empty() {        "<none>".to_string()    } else {        rels.join(", ")    }}pub(crate) fn compose_account_display(    account_display: Option<&StatusAccountDisplay>,) -> Option<StatusAccountDisplay> {    account_display.cloned()}pub(crate) fn plan_type_display_name(plan_type: PlanType) -> String {    if plan_type.is_team_like() {        "Business".to_string()    } else if plan_type.is_business_like() {        "Enterprise".to_string()    } else if plan_type == PlanType::ProLite {        "Pro Lite".to_string()    } else {        title_case(format!("{plan_type:?}").as_str())    }}pub(crate) fn format_tokens_compact(value: i64) -> String {    let value = value.max(0);    if value == 0 {        return "0".to_string();    }    if value < 1_000 {        return value.to_string();    }    let value_f64 = value as f64;    let (scaled, suffix) = if value >= 1_000_000_000_000 {        (value_f64 / 1_000_000_000_000.0, "T")    } else if value >= 1_000_000_000 {        (value_f64 / 1_000_000_000.0, "B")    } else if value >= 1_000_000 {        (value_f64 / 1_000_000.0, "M")    } else {        (value_f64 / 1_000.0, "K")    };    let decimals = if scaled < 10.0 {        2    } else if scaled < 100.0 {        1    } else {        0    };    let mut formatted = format!("{scaled:.decimals$}");    if formatted.contains('.') {        while formatted.ends_with('0') {            formatted.pop();        }        if formatted.ends_with('.') {            formatted.pop();        }    }    format!("{formatted}{suffix}")}pub(crate) fn format_directory_display(directory: &Path, max_width: Option<usize>) -> String {    let formatted = if let Some(rel) = relativize_to_home(directory) {        if rel.as_os_str().is_empty() {            "~".to_string()        } else {            format!("~{}{}", std::path::MAIN_SEPARATOR, rel.display())        }    } else {        directory.display().to_string()    };    if let Some(max_width) = max_width {        if max_width == 0 {            return String::new();        }        if UnicodeWidthStr::width(formatted.as_str()) > max_width {            return text_formatting::center_truncate_path(&formatted, max_width);        }    }    formatted}pub(crate) fn format_reset_timestamp(dt: DateTime<Local>, captured_at: DateTime<Local>) -> String {    let time = dt.format("%H:%M").to_string();    if dt.date_naive() == captured_at.date_naive() {        time    } else {        format!("{time} on {}", dt.format("%-d %b"))    }}fn title_case(s: &str) -> String {    if s.is_empty() {        return String::new();    }    let mut chars = s.chars();    let Some(first) = chars.next() else {        return String::new();    };    let rest = chars.as_str().to_ascii_lowercase();    first.to_uppercase().collect::<String>() + &rest}#[cfg(test)]mod tests {    use super::*;    use crate::legacy_core::config::ConfigBuilder;    use codex_utils_absolute_path::test_support::PathBufExt;    use pretty_assertions::assert_eq;    use tempfile::TempDir;    async fn test_config(codex_home: &TempDir, cwd: &TempDir) -> Config {        ConfigBuilder::default()            .codex_home(codex_home.path().to_path_buf())            .fallback_cwd(Some(cwd.path().to_path_buf()))            .build()            .await            .expect("load config")    }    #[test]    fn plan_type_display_name_remaps_display_labels() {        let cases = [            (PlanType::Free, "Free"),            (PlanType::Go, "Go"),            (PlanType::Plus, "Plus"),            (PlanType::Pro, "Pro"),            (PlanType::ProLite, "Pro Lite"),            (PlanType::Team, "Business"),            (PlanType::SelfServeBusinessUsageBased, "Business"),            (PlanType::Business, "Enterprise"),            (PlanType::EnterpriseCbpUsageBased, "Enterprise"),            (PlanType::Enterprise, "Enterprise"),            (PlanType::Edu, "Edu"),            (PlanType::Unknown, "Unknown"),        ];        for (plan_type, expected) in cases {            assert_eq!(plan_type_display_name(plan_type), expected);        }    }    #[tokio::test]    async fn compose_agents_summary_includes_global_agents_path() {        let codex_home = TempDir::new().expect("temp codex home");        let cwd = TempDir::new().expect("temp cwd");        let global_agents_path = codex_home.path().join("global.md");        let config = test_config(&codex_home, &cwd).await;        assert_eq!(            compose_agents_summary(&config, &[global_agents_path.abs()]),            format_directory_display(&global_agents_path, /*max_width*/ None)        );    }    #[tokio::test]    async fn compose_agents_summary_names_global_agents_override() {        let codex_home = TempDir::new().expect("temp codex home");        let cwd = TempDir::new().expect("temp cwd");        let override_path = codex_home.path().join("override.md");        let config = test_config(&codex_home, &cwd).await;        assert_eq!(            compose_agents_summary(&config, &[override_path.abs()]),            format_directory_display(&override_path, /*max_width*/ None)        );    }    #[tokio::test]    async fn compose_agents_summary_orders_global_before_project_agents() {        let codex_home = TempDir::new().expect("temp codex home");        let cwd = TempDir::new().expect("temp cwd");        let global_agents_path = codex_home.path().join("global.md");        let project_agents_path = cwd.path().join("project.md");        let config = test_config(&codex_home, &cwd).await;        let summary = compose_agents_summary(            &config,            &[                global_agents_path.clone().abs(),                project_agents_path.clone().abs(),            ],        );        let mut paths = summary.split(", ");        assert_eq!(            paths.next(),            Some(format_directory_display(&global_agents_path, /*max_width*/ None).as_str())        );        let project_path = paths.next().expect("project agents path");        assert!(project_path.ends_with("project.md"));        assert_eq!(paths.next(), None);    }}