Codex Handbook
cli/src/doctor/title.rs 406 lines
use std::collections::HashSet;use std::path::Path;use std::path::PathBuf;use codex_config::ConfigLayerSource;use codex_config::ConfigLayerStackOrdering;use codex_core::config::Config;use codex_git_utils::get_git_repo_root;use unicode_segmentation::UnicodeSegmentation;use super::CheckStatus;use super::DoctorCheck;const DEFAULT_TERMINAL_TITLE_ITEMS: &[&str] = &["activity", "project-name"];const PROJECT_TITLE_MAX_CHARS: usize = 24;#[derive(Clone, Debug, Eq, PartialEq)]struct TerminalTitleInputs {    configured_items: Option<Vec<String>>,    cwd: PathBuf,    project_root: Option<ProjectTitleRoot>,}#[derive(Clone, Debug, Eq, PartialEq)]struct ProjectTitleRoot {    source: &'static str,    path: PathBuf,}pub(super) fn terminal_title_check(config: &Config) -> DoctorCheck {    terminal_title_check_from_inputs(TerminalTitleInputs {        configured_items: config.tui_terminal_title.clone(),        cwd: config.cwd.to_path_buf(),        project_root: terminal_title_project_root(config, &config.cwd),    })}fn terminal_title_check_from_inputs(inputs: TerminalTitleInputs) -> DoctorCheck {    let (source, items, invalid_items) = match inputs.configured_items {        Some(items) if items.is_empty() => ("disabled", Vec::new(), Vec::new()),        Some(items) => {            let (items, invalid_items) = parse_terminal_title_items(items);            ("configured", items, invalid_items)        }        None => (            "default",            DEFAULT_TERMINAL_TITLE_ITEMS                .iter()                .map(ToString::to_string)                .collect(),            Vec::new(),        ),    };    let mut details = vec![        format!("terminal title source: {source}"),        format!(            "terminal title items: {}",            if items.is_empty() {                "none".to_string()            } else {                items.join(", ")            }        ),        format!("terminal title activity: {}", activity_enabled(&items)),    ];    if !invalid_items.is_empty() {        details.push(format!(            "terminal title invalid items: {}",            invalid_items.join(", ")        ));    }    if project_title_selected(&items) {        let (project_source, project_value) =            project_title_candidate(inputs.project_root, &inputs.cwd);        details.push(format!("terminal title project source: {project_source}"));        if let Some(project_value) = project_value {            details.push(format!("terminal title project value: {project_value}"));        }    }    let status = if invalid_items.is_empty() {        CheckStatus::Ok    } else {        CheckStatus::Warning    };    let summary = if invalid_items.is_empty() {        format!("terminal title {source}")    } else {        format!("terminal title {source} with invalid items")    };    let mut check = DoctorCheck::new("terminal.title", "title", status, summary).details(details);    if !invalid_items.is_empty() {        check = check.issue(            super::DoctorIssue::new(                CheckStatus::Warning,                "terminal title configuration contains unknown item identifiers",            )            .measured(invalid_items.join(", "))            .expected("known terminal title item identifiers")            .remedy("Remove or replace the unknown entries in [tui].terminal_title.")            .field("terminal title invalid items"),        );    }    check}fn parse_terminal_title_items(items: Vec<String>) -> (Vec<String>, Vec<String>) {    let mut invalid = Vec::new();    let mut invalid_seen = HashSet::new();    let mut parsed = Vec::new();    for item in items {        match terminal_title_item_id(&item) {            Some(id) => parsed.push(id.to_string()),            None => {                if invalid_seen.insert(item.clone()) {                    invalid.push(format!(r#""{item}""#));                }            }        }    }    (parsed, invalid)}fn terminal_title_item_id(item: &str) -> Option<&'static str> {    match item {        "app-name" => Some("app-name"),        "project-name" | "project" => Some("project-name"),        "current-dir" => Some("current-dir"),        "activity" | "spinner" => Some("activity"),        "run-state" | "status" => Some("run-state"),        "thread-title" | "thread" => Some("thread-title"),        "git-branch" => Some("git-branch"),        "context-remaining" => Some("context-remaining"),        "context-used" | "context-usage" => Some("context-used"),        "five-hour-limit" => Some("five-hour-limit"),        "weekly-limit" => Some("weekly-limit"),        "codex-version" => Some("codex-version"),        "used-tokens" => Some("used-tokens"),        "total-input-tokens" => Some("total-input-tokens"),        "total-output-tokens" => Some("total-output-tokens"),        "thread-id" | "session-id" => Some("thread-id"),        "fast-mode" => Some("fast-mode"),        "model" | "model-name" => Some("model"),        "model-with-reasoning" => Some("model-with-reasoning"),        "reasoning" => Some("reasoning"),        "task-progress" => Some("task-progress"),        _ => None,    }}fn activity_enabled(items: &[String]) -> bool {    items        .iter()        .any(|item| item == "activity" || item == "spinner")}fn project_title_selected(items: &[String]) -> bool {    items        .iter()        .any(|item| item == "project-name" || item == "project")}fn terminal_title_project_root(config: &Config, cwd: &Path) -> Option<ProjectTitleRoot> {    if let Some(repo_root) = get_git_repo_root(cwd) {        return Some(ProjectTitleRoot {            source: "git repo root",            path: repo_root,        });    }    config        .config_layer_stack        .get_layers(            ConfigLayerStackOrdering::LowestPrecedenceFirst,            /*include_disabled*/ true,        )        .iter()        .find_map(|layer| match &layer.name {            ConfigLayerSource::Project { dot_codex_folder } => dot_codex_folder                .as_path()                .parent()                .map(|root| ProjectTitleRoot {                    source: "project config",                    path: root.to_path_buf(),                }),            _ => None,        })}fn project_title_candidate(    project_root: Option<ProjectTitleRoot>,    cwd: &Path,) -> (&'static str, Option<String>) {    if let Some(project_root) = project_root {        return (            project_root.source,            Some(truncate_title_part(path_display_name(&project_root.path))),        );    }    ("cwd", Some(truncate_title_part(path_display_name(cwd))))}fn path_display_name(path: &Path) -> String {    path.file_name()        .map(|name| name.to_string_lossy().to_string())        .unwrap_or_else(|| path.display().to_string())}fn truncate_title_part(value: String) -> String {    let mut graphemes = value.graphemes(true);    let head = graphemes        .by_ref()        .take(PROJECT_TITLE_MAX_CHARS)        .collect::<String>();    if graphemes.next().is_none() || PROJECT_TITLE_MAX_CHARS <= 3 {        return head;    }    let mut truncated = head        .graphemes(true)        .take(PROJECT_TITLE_MAX_CHARS - 3)        .collect::<String>();    truncated.push_str("...");    truncated}#[cfg(test)]mod tests {    use pretty_assertions::assert_eq;    use super::*;    #[test]    fn terminal_title_reports_default_items_and_git_project_name() {        let check = terminal_title_check_from_inputs(TerminalTitleInputs {            configured_items: None,            cwd: PathBuf::from("/repo/subdir"),            project_root: Some(ProjectTitleRoot {                source: "git repo root",                path: PathBuf::from("/repo"),            }),        });        assert_eq!(check.summary, "terminal title default");        assert!(            check                .details                .contains(&"terminal title items: activity, project-name".to_string())        );        assert!(            check                .details                .contains(&"terminal title project source: git repo root".to_string())        );        assert!(            check                .details                .contains(&"terminal title project value: repo".to_string())        );    }    #[test]    fn terminal_title_reports_disabled_configuration() {        let check = terminal_title_check_from_inputs(TerminalTitleInputs {            configured_items: Some(Vec::new()),            cwd: PathBuf::from("/workspace"),            project_root: None,        });        assert_eq!(check.summary, "terminal title disabled");        assert!(            check                .details                .contains(&"terminal title items: none".to_string())        );        assert!(            check                .details                .contains(&"terminal title activity: false".to_string())        );        assert!(            !check                .details                .iter()                .any(|detail| detail.starts_with("terminal title project "))        );    }    #[test]    fn terminal_title_reports_project_config_fallback() {        let check = terminal_title_check_from_inputs(TerminalTitleInputs {            configured_items: Some(vec!["project".to_string()]),            cwd: PathBuf::from("/workspace/project/subdir"),            project_root: Some(ProjectTitleRoot {                source: "project config",                path: PathBuf::from("/workspace/project"),            }),        });        assert_eq!(check.summary, "terminal title configured");        assert!(            check                .details                .contains(&"terminal title project source: project config".to_string())        );        assert!(            check                .details                .contains(&"terminal title project value: project".to_string())        );    }    #[test]    fn terminal_title_omits_project_when_project_item_is_not_selected() {        let check = terminal_title_check_from_inputs(TerminalTitleInputs {            configured_items: Some(vec!["model".to_string()]),            cwd: PathBuf::from("/workspace/project"),            project_root: Some(ProjectTitleRoot {                source: "project config",                path: PathBuf::from("/workspace/project"),            }),        });        assert_eq!(check.summary, "terminal title configured");        assert!(            !check                .details                .iter()                .any(|detail| detail.starts_with("terminal title project "))        );    }    #[test]    fn terminal_title_warns_for_invalid_configured_items() {        let check = terminal_title_check_from_inputs(TerminalTitleInputs {            configured_items: Some(vec![                "project".to_string(),                "bogus".to_string(),                "activity".to_string(),                "bogus".to_string(),            ]),            cwd: PathBuf::from("/workspace/project"),            project_root: Some(ProjectTitleRoot {                source: "project config",                path: PathBuf::from("/workspace/project"),            }),        });        assert_eq!(check.status, CheckStatus::Warning);        assert_eq!(            check.summary,            "terminal title configured with invalid items"        );        assert!(            check                .details                .contains(&"terminal title items: project-name, activity".to_string())        );        assert!(            check                .details                .contains(&r#"terminal title invalid items: "bogus""#.to_string())        );        assert_eq!(check.issues.len(), 1);    }    #[test]    fn terminal_title_warns_when_all_configured_items_are_invalid() {        let check = terminal_title_check_from_inputs(TerminalTitleInputs {            configured_items: Some(vec!["bogus".to_string()]),            cwd: PathBuf::from("/workspace/project"),            project_root: None,        });        assert_eq!(check.status, CheckStatus::Warning);        assert!(            check                .details                .contains(&"terminal title items: none".to_string())        );        assert!(            check                .details                .contains(&r#"terminal title invalid items: "bogus""#.to_string())        );    }    #[test]    fn terminal_title_project_value_uses_tui_truncation_shape() {        let check = terminal_title_check_from_inputs(TerminalTitleInputs {            configured_items: Some(vec!["project".to_string()]),            cwd: PathBuf::from("/workspace/abcdefghijklmnopqrstuvwxyz"),            project_root: Some(ProjectTitleRoot {                source: "project config",                path: PathBuf::from("/workspace/abcdefghijklmnopqrstuvwxyz"),            }),        });        assert!(            check                .details                .contains(&"terminal title project value: abcdefghijklmnopqrstu...".to_string())        );    }}