//! Status-line and terminal-title rendering helpers for `ChatWidget`.//!//! Keeping this logic in a focused submodule makes the additive title/status//! behavior easier to review without paging through the rest of `chatwidget.rs`.use super::*;use crate::bottom_pane::status_line_from_segments;use crate::branch_summary;use crate::chatwidget::limit_label_for_window;use crate::chatwidget::rate_limits::get_limits_duration;use crate::legacy_core::config::Config;use crate::status::format_tokens_compact;use codex_app_server_protocol::AskForApproval;use codex_protocol::config_types::ApprovalsReviewer;use codex_protocol::config_types::ServiceTier;use codex_protocol::models::PermissionProfile;use codex_utils_sandbox_summary::summarize_permission_profile;use super::status_state::TerminalTitleStatusKind;/// Items shown in the terminal title when the user has not configured a/// custom selection. Intentionally minimal: activity indicator + project name.pub(super) const DEFAULT_TERMINAL_TITLE_ITEMS: [&str; 2] = ["activity", "project-name"];/// Braille-pattern dot-spinner frames for the terminal title animation.pub(super) const TERMINAL_TITLE_SPINNER_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];/// Time between spinner frame advances in the terminal title.pub(super) const TERMINAL_TITLE_SPINNER_INTERVAL: Duration = Duration::from_millis(100);/// Time between action-required blink phases in the terminal title.const TERMINAL_TITLE_ACTION_REQUIRED_INTERVAL: Duration = Duration::from_secs(1);/// Prefix shown in the terminal title when the agent is blocked on user input.const TERMINAL_TITLE_ACTION_REQUIRED_PREFIX: &str = "[ ! ] Action Required";const TERMINAL_TITLE_ACTION_REQUIRED_PREFIX_HIDDEN: &str = "[ . ] Action Required";#[derive(Debug)]/// Parsed status-surface configuration for one refresh pass.////// The status line and terminal title share some expensive or stateful inputs/// (notably git branch lookup and invalid-item warnings). This snapshot lets one/// refresh pass compute those shared concerns once, then render both surfaces/// from the same selection set.struct StatusSurfaceSelections { status_line_items: Vec<StatusLineItem>, invalid_status_line_items: Vec<String>, terminal_title_items: Vec<TerminalTitleItem>, invalid_terminal_title_items: Vec<String>,}impl StatusSurfaceSelections { fn uses_git_branch(&self) -> bool { self.status_line_items.contains(&StatusLineItem::GitBranch) || self .terminal_title_items .contains(&TerminalTitleItem::GitBranch) } fn uses_git_summary(&self) -> bool { self.status_line_items .contains(&StatusLineItem::PullRequestNumber) || self .status_line_items .contains(&StatusLineItem::BranchChanges) }}/// Cached project-root display name keyed by the cwd used for the last lookup.////// Terminal-title refreshes can happen very frequently, so the title path avoids/// repeatedly walking up the filesystem to rediscover the same project root name/// while the working directory is unchanged.#[derive(Clone, Debug)]pub(super) struct CachedProjectRootName { pub(super) cwd: PathBuf, pub(super) root_name: Option<String>,}impl ChatWidget { fn status_surface_selections(&self) -> StatusSurfaceSelections { let (status_line_items, invalid_status_line_items) = self.status_line_items_with_invalids(); let (terminal_title_items, invalid_terminal_title_items) = self.terminal_title_items_with_invalids(); StatusSurfaceSelections { status_line_items, invalid_status_line_items, terminal_title_items, invalid_terminal_title_items, } } fn warn_invalid_status_line_items_once(&mut self, invalid_items: &[String]) { if self.thread_id.is_some() && !invalid_items.is_empty() && self .status_line_invalid_items_warned .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) .is_ok() { let label = if invalid_items.len() == 1 { "item" } else { "items" }; let message = format!( "Ignored invalid status line {label}: {}.", proper_join(invalid_items) ); self.on_warning(message); } } fn warn_invalid_terminal_title_items_once(&mut self, invalid_items: &[String]) { if self.thread_id.is_some() && !invalid_items.is_empty() && self .terminal_title_invalid_items_warned .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) .is_ok() { let label = if invalid_items.len() == 1 { "item" } else { "items" }; let message = format!( "Ignored invalid terminal title {label}: {}.", proper_join(invalid_items) ); self.on_warning(message); } } fn sync_status_surface_shared_state(&mut self, selections: &StatusSurfaceSelections) { if !selections.uses_git_branch() { self.status_line_branch = None; self.status_line_branch_pending = false; self.status_line_branch_lookup_complete = false; } else { let cwd = self.status_line_cwd().to_path_buf(); self.sync_status_line_branch_state(&cwd); if !self.status_line_branch_lookup_complete { self.request_status_line_branch(cwd); } } if !selections.uses_git_summary() { self.status_line_git_summary = None; self.status_line_git_summary_pending = false; self.status_line_git_summary_lookup_complete = false; } else { let cwd = self.status_line_cwd().to_path_buf(); self.sync_status_line_git_summary_state(&cwd); if !self.status_line_git_summary_lookup_complete { self.request_status_line_git_summary(cwd); } } } fn refresh_status_line_from_selections(&mut self, selections: &StatusSurfaceSelections) { let enabled = !selections.status_line_items.is_empty(); self.bottom_pane.set_status_line_enabled(enabled); if !enabled { self.set_status_line(/*status_line*/ None); self.set_status_line_hyperlink(/*url*/ None); return; } let mut segments = Vec::new(); for item in &selections.status_line_items { if let Some(value) = self.status_line_value_for_item(*item) { segments.push((*item, value)); } } self.set_status_line(status_line_from_segments( segments, self.config.tui_status_line_use_colors, )); let hyperlink_url = selections .status_line_items .contains(&StatusLineItem::PullRequestNumber) .then(|| self.status_line_pull_request_url()) .flatten(); self.set_status_line_hyperlink(hyperlink_url); } /// Clears the terminal title Codex most recently wrote, if any. /// /// This does not attempt to restore the shell or terminal's previous title; /// it only clears the managed title and updates the cache after a successful /// OSC write. pub(crate) fn clear_managed_terminal_title(&mut self) -> std::io::Result<()> { if self.last_terminal_title.is_some() { clear_terminal_title()?; self.last_terminal_title = None; } Ok(()) } /// Renders and applies the terminal title for one parsed selection snapshot. /// /// Empty selections clear the managed title. Non-empty selections render the /// current values in configured order, skip unavailable segments, and cache /// the last successfully written title so redundant OSC writes are avoided. /// When the `activity` item is present in an animated running state, this also /// schedules the next frame so the title animation keeps advancing. fn refresh_terminal_title_from_selections(&mut self, selections: &StatusSurfaceSelections) { self.last_terminal_title_requires_action = self.terminal_title_shows_action_required_with_selections(selections); if selections.terminal_title_items.is_empty() { if let Err(err) = self.clear_managed_terminal_title() { tracing::debug!(error = %err, "failed to clear terminal title"); } return; } let now = Instant::now(); let title = self.terminal_title_text_for_selections(selections, now); let animation_interval = self.terminal_title_animation_interval_with_selections(selections); if self.last_terminal_title == title { if let Some(interval) = animation_interval { self.frame_requester.schedule_frame_in(interval); } return; } match title { Some(title) => match set_terminal_title(&title) { Ok(SetTerminalTitleResult::Applied) => { self.last_terminal_title = Some(title); } Ok(SetTerminalTitleResult::NoVisibleContent) => { if let Err(err) = self.clear_managed_terminal_title() { tracing::debug!(error = %err, "failed to clear terminal title"); } } Err(err) => { tracing::debug!(error = %err, "failed to set terminal title"); } }, None => { if let Err(err) = self.clear_managed_terminal_title() { tracing::debug!(error = %err, "failed to clear terminal title"); } } } if let Some(interval) = animation_interval { self.frame_requester.schedule_frame_in(interval); } } /// Recomputes both status surfaces from one shared config snapshot. /// /// This is the common refresh entrypoint for the footer status line and the /// terminal title. It parses both configurations once, emits invalid-item /// warnings once, synchronizes shared cached state (such as git-branch /// lookup), then renders each surface from that shared snapshot. pub(crate) fn refresh_status_surfaces(&mut self) { let selections = self.status_surface_selections(); self.warn_invalid_status_line_items_once(&selections.invalid_status_line_items); self.warn_invalid_terminal_title_items_once(&selections.invalid_terminal_title_items); self.sync_status_surface_shared_state(&selections); self.refresh_status_line_from_selections(&selections); self.refresh_terminal_title_from_selections(&selections); } /// Recomputes and emits the terminal title from config and runtime state. pub(crate) fn refresh_terminal_title(&mut self) { let selections = self.status_surface_selections(); self.warn_invalid_terminal_title_items_once(&selections.invalid_terminal_title_items); self.sync_status_surface_shared_state(&selections); self.refresh_terminal_title_from_selections(&selections); } fn terminal_title_requires_action(&self) -> bool { self.bottom_pane.terminal_title_requires_action() } pub(super) fn terminal_title_shows_action_required(&self) -> bool { self.terminal_title_requires_action() && self.terminal_title_uses_activity() } fn terminal_title_text_for_selections( &mut self, selections: &StatusSurfaceSelections, now: Instant, ) -> Option<String> { if self.terminal_title_shows_action_required_with_selections(selections) { return Some(self.action_required_terminal_title_text(selections, now)); } let mut previous = None; let title = selections .terminal_title_items .iter() .copied() .filter_map(|item| { self.terminal_title_value_for_item(item, now) .map(|value| (item, value)) }) .fold(String::new(), |mut title, (item, value)| { title.push_str(item.separator_from_previous(previous)); title.push_str(&value); previous = Some(item); title }); (!title.is_empty()).then_some(title) } fn action_required_terminal_title_text( &mut self, selections: &StatusSurfaceSelections, now: Instant, ) -> String { crate::bottom_pane::build_action_required_title_text( self.action_required_terminal_title_prefix_at(now), selections.terminal_title_items.iter().copied(), &[TerminalTitleItem::Status], |item| self.terminal_title_value_for_item(item, now), ) } fn action_required_terminal_title_prefix_at(&self, now: Instant) -> &'static str { if !self.config.animations { return TERMINAL_TITLE_ACTION_REQUIRED_PREFIX; } let elapsed = now.saturating_duration_since(self.terminal_title_animation_origin); let phase = (elapsed.as_millis() / TERMINAL_TITLE_ACTION_REQUIRED_INTERVAL.as_millis()) % 2; if phase == 0 { TERMINAL_TITLE_ACTION_REQUIRED_PREFIX } else { TERMINAL_TITLE_ACTION_REQUIRED_PREFIX_HIDDEN } } fn terminal_title_shows_action_required_with_selections( &self, selections: &StatusSurfaceSelections, ) -> bool { self.terminal_title_requires_action() && selections .terminal_title_items .contains(&TerminalTitleItem::Spinner) } fn terminal_title_animation_interval_with_selections( &self, selections: &StatusSurfaceSelections, ) -> Option<Duration> { if self.config.animations && self.terminal_title_shows_action_required_with_selections(selections) { return Some(TERMINAL_TITLE_ACTION_REQUIRED_INTERVAL); } self.should_animate_terminal_title_spinner_with_selections(selections) .then_some(TERMINAL_TITLE_SPINNER_INTERVAL) } pub(super) fn request_status_line_branch_refresh(&mut self) { let selections = self.status_surface_selections(); if !selections.uses_git_branch() { return; } let cwd = self.status_line_cwd().to_path_buf(); self.sync_status_line_branch_state(&cwd); self.request_status_line_branch(cwd); } pub(super) fn request_status_line_git_summary_refresh(&mut self) { let selections = self.status_surface_selections(); if !selections.uses_git_summary() { return; } let cwd = self.status_line_cwd().to_path_buf(); self.sync_status_line_git_summary_state(&cwd); self.request_status_line_git_summary(cwd); } /// Parses configured status-line ids into known items and collects unknown ids. /// /// Unknown ids are deduplicated in insertion order for warning messages. fn status_line_items_with_invalids(&self) -> (Vec<StatusLineItem>, Vec<String>) { parse_items_with_invalids(self.configured_status_line_items()) } pub(super) fn configured_status_line_items(&self) -> Vec<String> { self.config.tui_status_line.clone().unwrap_or_else(|| { DEFAULT_STATUS_LINE_ITEMS .iter() .map(ToString::to_string) .collect() }) } /// Parses configured terminal-title ids into known items and collects unknown ids. /// /// Unknown ids are deduplicated in insertion order for warning messages. fn terminal_title_items_with_invalids(&self) -> (Vec<TerminalTitleItem>, Vec<String>) { parse_items_with_invalids(self.configured_terminal_title_items()) } /// Returns the configured terminal-title ids, or the default ordering when unset. pub(super) fn configured_terminal_title_items(&self) -> Vec<String> { self.config.tui_terminal_title.clone().unwrap_or_else(|| { DEFAULT_TERMINAL_TITLE_ITEMS .iter() .map(ToString::to_string) .collect() }) } fn status_line_cwd(&self) -> &Path { self.current_cwd .as_deref() .unwrap_or(self.config.cwd.as_path()) } /// Resolves the project root associated with `cwd`. /// /// Git repository root wins when available. Otherwise we fall back to the /// nearest project config layer so non-git projects can still surface a /// stable project label. fn status_line_project_root_for_cwd(&self, cwd: &Path) -> Option<PathBuf> { if let Some(repo_root) = get_git_repo_root(cwd) { return Some(repo_root); } self.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(Path::to_path_buf) } _ => None, }) } fn status_line_project_root_name_for_cwd(&self, cwd: &Path) -> Option<String> { self.status_line_project_root_for_cwd(cwd).map(|root| { root.file_name() .map(|name| name.to_string_lossy().to_string()) .unwrap_or_else(|| format_directory_display(&root, /*max_width*/ None)) }) } /// Returns a cached project-root display name for the active cwd. fn status_line_project_root_name(&mut self) -> Option<String> { let cwd = self.status_line_cwd().to_path_buf(); if let Some(cache) = &self.status_line_project_root_name_cache && cache.cwd == cwd { return cache.root_name.clone(); } let root_name = self.status_line_project_root_name_for_cwd(&cwd); self.status_line_project_root_name_cache = Some(CachedProjectRootName { cwd, root_name: root_name.clone(), }); root_name } /// Produces the terminal-title `project` value. /// /// This prefers the cached project-root name and falls back to the current /// directory name when no project root can be inferred. fn terminal_title_project_name(&mut self) -> Option<String> { let project = self.status_line_project_root_name().or_else(|| { let cwd = self.status_line_cwd(); Some( cwd.file_name() .map(|name| name.to_string_lossy().to_string()) .unwrap_or_else(|| format_directory_display(cwd, /*max_width*/ None)), ) })?; Some(Self::truncate_terminal_title_part( project, /*max_chars*/ 24, )) } /// Resets git-branch cache state when the status-line cwd changes. /// /// The branch cache is keyed by cwd because branch lookup is performed relative to that path. /// Keeping stale branch values across cwd changes would surface incorrect repository context. fn sync_status_line_branch_state(&mut self, cwd: &Path) { if self .status_line_branch_cwd .as_ref() .is_some_and(|path| path == cwd) { return; } self.status_line_branch_cwd = Some(cwd.to_path_buf()); self.status_line_branch = None; self.status_line_branch_pending = false; self.status_line_branch_lookup_complete = false; } fn sync_status_line_git_summary_state(&mut self, cwd: &Path) { if self.status_line_git_summary_cwd.as_deref() == Some(cwd) { return; } self.status_line_git_summary_cwd = Some(cwd.to_path_buf()); self.status_line_git_summary = None; self.status_line_git_summary_pending = false; self.status_line_git_summary_lookup_complete = false; } /// Starts an async git-branch lookup unless one is already running. /// /// The resulting `StatusLineBranchUpdated` event carries the lookup cwd so callers can reject /// stale completions after directory changes. fn request_status_line_branch(&mut self, cwd: PathBuf) { if self.status_line_branch_pending { return; } let Some(runner) = self.workspace_command_runner.clone() else { self.status_line_branch_lookup_complete = true; return; }; self.status_line_branch_pending = true; let tx = self.app_event_tx.clone(); tokio::spawn(async move { let branch = branch_summary::current_branch_name(runner.as_ref(), &cwd).await; tx.send(AppEvent::StatusLineBranchUpdated { cwd, branch }); }); } fn request_status_line_git_summary(&mut self, cwd: PathBuf) { if self.status_line_git_summary_pending { return; } let Some(runner) = self.workspace_command_runner.clone() else { self.status_line_git_summary_lookup_complete = true; return; }; self.status_line_git_summary_pending = true; let tx = self.app_event_tx.clone(); tokio::spawn(async move { let summary = branch_summary::status_line_git_summary(runner.as_ref(), &cwd).await; tx.send(AppEvent::StatusLineGitSummaryUpdated { cwd, summary }); }); } /// Resolves a display string for one configured status-line item. /// /// Returning `None` means "omit this item for now", not "configuration error". Callers rely on /// this to keep partially available status lines readable while waiting for session, token, or /// git metadata. pub(super) fn status_line_value_for_item(&mut self, item: StatusLineItem) -> Option<String> { match item { StatusLineItem::ModelName => Some(self.model_display_name().to_string()), StatusLineItem::ModelWithReasoning => Some(self.model_with_reasoning_display_name()), StatusLineItem::Reasoning => Some(self.reasoning_display_name()), StatusLineItem::CurrentDir => { Some(format_directory_display( self.status_line_cwd(), /*max_width*/ None, )) } StatusLineItem::ProjectRoot => self.status_line_project_root_name(), StatusLineItem::GitBranch => self.status_line_branch.clone(), StatusLineItem::PullRequestNumber => self .status_line_git_summary .as_ref() .and_then(|summary| summary.pull_request.as_ref()) .map(|pull_request| format!("PR #{}", pull_request.number)), StatusLineItem::BranchChanges => self .status_line_git_summary .as_ref() .and_then(|summary| summary.branch_change_stats.as_ref()) .map(|stats| { if stats.additions == 0 && stats.deletions == 0 { "No changes".to_string() } else { format!("+{} -{}", stats.additions, stats.deletions) } }), StatusLineItem::Status => Some(self.run_state_status_text()), StatusLineItem::Permissions => Some(permissions_display(&self.config)), StatusLineItem::ApprovalMode => Some(approval_mode_display(&self.config)), StatusLineItem::UsedTokens => { let usage = self.status_line_total_usage(); let total = usage.blended_total(); if total <= 0 { None } else { Some(format!("{} used", format_tokens_compact(total))) } } StatusLineItem::ContextRemaining => self .status_line_context_remaining_percent() .map(|remaining| format!("Context {remaining}% left")), StatusLineItem::ContextUsed => self .status_line_context_used_percent() .map(|used| format!("Context {used}% used")), StatusLineItem::FiveHourLimit => { let (window, is_secondary) = self .rate_limit_snapshots_by_limit_id .get("codex") .and_then(five_hour_status_window)?; let label = limit_label_for_window(window.window_minutes, is_secondary); self.status_line_limit_display(Some(window), &label) } StatusLineItem::WeeklyLimit => { let (window, is_secondary) = self .rate_limit_snapshots_by_limit_id .get("codex") .and_then(weekly_status_window)?; let label = limit_label_for_window(window.window_minutes, is_secondary); self.status_line_limit_display(Some(window), &label) } StatusLineItem::CodexVersion => Some(CODEX_CLI_VERSION.to_string()), StatusLineItem::ContextWindowSize => self .status_line_context_window_size() .map(|cws| format!("{} window", format_tokens_compact(cws))), StatusLineItem::TotalInputTokens => Some(format!( "{} in", format_tokens_compact(self.status_line_total_usage().input_tokens) )), StatusLineItem::TotalOutputTokens => Some(format!( "{} out", format_tokens_compact(self.status_line_total_usage().output_tokens) )), StatusLineItem::SessionId => self.thread_id.map(|id| id.to_string()), StatusLineItem::FastMode => Some( if self.current_service_tier() == Some(ServiceTier::Fast.request_value()) { "Fast on".to_string() } else { "Fast off".to_string() }, ), StatusLineItem::RawOutput => self.raw_output_mode().then(|| "raw output".to_string()), StatusLineItem::ThreadTitle => self.thread_name.as_ref().map_or_else( || self.thread_id.map(|id| id.to_string()), |name| { let trimmed = name.trim(); if trimmed.is_empty() { self.thread_id.map(|id| id.to_string()) } else { Some(trimmed.to_string()) } }, ), StatusLineItem::TaskProgress => self.terminal_title_task_progress(), } } fn status_line_pull_request_url(&self) -> Option<String> { self.status_line_git_summary .as_ref() .and_then(|summary| summary.pull_request.as_ref()) .map(|pull_request| pull_request.url.clone()) } pub(super) fn status_surface_preview_value_for_item( &mut self, item: StatusSurfacePreviewItem, ) -> Option<String> { let status_line_item = match item { StatusSurfacePreviewItem::AppName => return Some("codex".to_string()), StatusSurfacePreviewItem::ProjectName => return self.terminal_title_project_name(), StatusSurfacePreviewItem::ProjectRoot => StatusLineItem::ProjectRoot, StatusSurfacePreviewItem::Status => return Some(self.run_state_status_text()), StatusSurfacePreviewItem::TaskProgress => return self.terminal_title_task_progress(), StatusSurfacePreviewItem::CurrentDir => StatusLineItem::CurrentDir, StatusSurfacePreviewItem::ThreadTitle => StatusLineItem::ThreadTitle, StatusSurfacePreviewItem::GitBranch => StatusLineItem::GitBranch, StatusSurfacePreviewItem::PullRequestNumber => StatusLineItem::PullRequestNumber, StatusSurfacePreviewItem::BranchChanges => StatusLineItem::BranchChanges, StatusSurfacePreviewItem::Permissions => StatusLineItem::Permissions, StatusSurfacePreviewItem::ApprovalMode => StatusLineItem::ApprovalMode, StatusSurfacePreviewItem::ContextRemaining => StatusLineItem::ContextRemaining, StatusSurfacePreviewItem::ContextUsed => StatusLineItem::ContextUsed, StatusSurfacePreviewItem::FiveHourLimit => StatusLineItem::FiveHourLimit, StatusSurfacePreviewItem::WeeklyLimit => StatusLineItem::WeeklyLimit, StatusSurfacePreviewItem::CodexVersion => StatusLineItem::CodexVersion, StatusSurfacePreviewItem::ContextWindowSize => StatusLineItem::ContextWindowSize, StatusSurfacePreviewItem::UsedTokens => StatusLineItem::UsedTokens, StatusSurfacePreviewItem::TotalInputTokens => StatusLineItem::TotalInputTokens, StatusSurfacePreviewItem::TotalOutputTokens => StatusLineItem::TotalOutputTokens, StatusSurfacePreviewItem::SessionId => StatusLineItem::SessionId, StatusSurfacePreviewItem::FastMode => StatusLineItem::FastMode, StatusSurfacePreviewItem::RawOutput => StatusLineItem::RawOutput, StatusSurfacePreviewItem::Model => StatusLineItem::ModelName, StatusSurfacePreviewItem::ModelWithReasoning => StatusLineItem::ModelWithReasoning, StatusSurfacePreviewItem::Reasoning => StatusLineItem::Reasoning, }; self.status_line_value_for_item(status_line_item) } /// Resolves one configured terminal-title item into a displayable segment. /// /// Returning `None` means "omit this segment for now" so callers can keep /// the configured order while hiding values that are not yet available. pub(super) fn terminal_title_value_for_item( &mut self, item: TerminalTitleItem, now: Instant, ) -> Option<String> { match item { TerminalTitleItem::AppName => Some("codex".to_string()), TerminalTitleItem::Project => self.terminal_title_project_name(), TerminalTitleItem::CurrentDir => Some(Self::truncate_terminal_title_part( format_directory_display(self.status_line_cwd(), /*max_width*/ None), /*max_chars*/ 32, )), TerminalTitleItem::Spinner => self.terminal_title_spinner_text_at(now), TerminalTitleItem::Status => Some(self.run_state_status_text()), TerminalTitleItem::Thread => self .status_line_value_for_item(StatusLineItem::ThreadTitle) .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 48)), TerminalTitleItem::GitBranch => self.status_line_branch.as_ref().map(|branch| { Self::truncate_terminal_title_part(branch.clone(), /*max_chars*/ 32) }), TerminalTitleItem::ContextRemaining => self .status_line_value_for_item(StatusLineItem::ContextRemaining) .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), TerminalTitleItem::ContextUsed => self .status_line_value_for_item(StatusLineItem::ContextUsed) .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), TerminalTitleItem::FiveHourLimit => self .status_line_value_for_item(StatusLineItem::FiveHourLimit) .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), TerminalTitleItem::WeeklyLimit => self .status_line_value_for_item(StatusLineItem::WeeklyLimit) .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), TerminalTitleItem::CodexVersion => self .status_line_value_for_item(StatusLineItem::CodexVersion) .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), TerminalTitleItem::UsedTokens => self .status_line_value_for_item(StatusLineItem::UsedTokens) .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), TerminalTitleItem::TotalInputTokens => self .status_line_value_for_item(StatusLineItem::TotalInputTokens) .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), TerminalTitleItem::TotalOutputTokens => self .status_line_value_for_item(StatusLineItem::TotalOutputTokens) .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), TerminalTitleItem::SessionId => self .status_line_value_for_item(StatusLineItem::SessionId) .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), TerminalTitleItem::FastMode => self .status_line_value_for_item(StatusLineItem::FastMode) .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), TerminalTitleItem::Model => Some(Self::truncate_terminal_title_part( self.model_display_name().to_string(), /*max_chars*/ 32, )), TerminalTitleItem::ModelWithReasoning => Some(Self::truncate_terminal_title_part( self.model_with_reasoning_display_name(), /*max_chars*/ 32, )), TerminalTitleItem::Reasoning => Some(Self::truncate_terminal_title_part( self.reasoning_display_name(), /*max_chars*/ 32, )), TerminalTitleItem::TaskProgress => self.terminal_title_task_progress(), } } fn reasoning_display_name(&self) -> String { let effort = self.effective_reasoning_effort(); Self::status_line_reasoning_effort_label(effort.as_ref()) } fn model_with_reasoning_display_name(&self) -> String { let label = self.reasoning_display_name(); let service_tier_label = self .current_service_tier() .and_then(|service_tier| { self.current_model_service_tier_commands() .into_iter() .find(|tier| tier.id == service_tier) .map(|tier| tier.name) }) .filter(|_| self.has_chatgpt_account) .map(|tier| format!(" {tier}")) .unwrap_or_default(); format!("{} {label}{service_tier_label}", self.model_display_name()) } /// Computes the compact runtime status label used by word-based status items. /// /// Startup takes precedence over normal task states, and idle state renders /// as `Ready` regardless of the last active status bucket. pub(super) fn run_state_status_text(&self) -> String { if self.mcp_startup_status.is_some() { return "Starting".to_string(); } match self.status_state.terminal_title_status_kind { TerminalTitleStatusKind::Working if !self.bottom_pane.is_task_running() => { "Ready".to_string() } TerminalTitleStatusKind::WaitingForBackgroundTerminal if !self.bottom_pane.is_task_running() => { "Ready".to_string() } TerminalTitleStatusKind::Thinking if !self.bottom_pane.is_task_running() => { "Ready".to_string() } TerminalTitleStatusKind::Working => "Working".to_string(), TerminalTitleStatusKind::WaitingForBackgroundTerminal => "Waiting".to_string(), TerminalTitleStatusKind::Thinking => "Thinking".to_string(), } } pub(super) fn terminal_title_spinner_text_at(&self, now: Instant) -> Option<String> { if !self.config.animations { return None; } if !self.terminal_title_has_active_progress() { return None; } Some(self.terminal_title_spinner_frame_at(now).to_string()) } fn terminal_title_spinner_frame_at(&self, now: Instant) -> &'static str { let elapsed = now.saturating_duration_since(self.terminal_title_animation_origin); let frame_index = (elapsed.as_millis() / TERMINAL_TITLE_SPINNER_INTERVAL.as_millis()) as usize; TERMINAL_TITLE_SPINNER_FRAMES[frame_index % TERMINAL_TITLE_SPINNER_FRAMES.len()] } fn terminal_title_uses_activity(&self) -> bool { self.config.tui_terminal_title.as_ref().is_none_or(|items| { items .iter() .any(|item| item == "activity" || item == "spinner") }) } fn terminal_title_has_active_progress(&self) -> bool { if self.terminal_title_shows_action_required() { return false; } self.mcp_startup_status.is_some() || self.bottom_pane.is_task_running() } pub(super) fn should_animate_terminal_title_spinner(&self) -> bool { self.config.animations && self.terminal_title_uses_activity() && self.terminal_title_has_active_progress() } pub(super) fn should_animate_terminal_title_action_required(&self) -> bool { self.config.animations && self.terminal_title_shows_action_required() } fn should_animate_terminal_title_spinner_with_selections( &self, selections: &StatusSurfaceSelections, ) -> bool { self.config.animations && selections .terminal_title_items .contains(&TerminalTitleItem::Spinner) && self.terminal_title_has_active_progress() } /// Formats the last `update_plan` progress snapshot for terminal-title display. pub(super) fn terminal_title_task_progress(&self) -> Option<String> { let (completed, total) = self.transcript.last_plan_progress?; if total == 0 { return None; } Some(format!("Tasks {completed}/{total}")) } /// Truncates a title segment by grapheme cluster and appends `...` when needed. pub(super) fn truncate_terminal_title_part(value: String, max_chars: usize) -> String { if max_chars == 0 { return String::new(); } let mut graphemes = value.graphemes(true); let head: String = graphemes.by_ref().take(max_chars).collect(); if graphemes.next().is_none() || max_chars <= 3 { return head; } let mut truncated = head.graphemes(true).take(max_chars - 3).collect::<String>(); truncated.push_str("..."); truncated }}fn five_hour_status_window( snapshot: &RateLimitSnapshotDisplay,) -> Option<(&RateLimitWindowDisplay, bool)> { find_primary_codex_window(snapshot, "5h") .or_else(|| secondary_window_with_label_when_weekly_is_available(snapshot, "5h")) .or_else(|| non_weekly_primary_window(snapshot)) .or_else(|| non_weekly_secondary_window_when_primary_is_weekly(snapshot))}fn weekly_status_window( snapshot: &RateLimitSnapshotDisplay,) -> Option<(&RateLimitWindowDisplay, bool)> { find_codex_window(snapshot, "weekly") .or_else(|| snapshot.secondary.as_ref().map(|window| (window, true)))}fn find_codex_window<'a>( snapshot: &'a RateLimitSnapshotDisplay, label: &str,) -> Option<(&'a RateLimitWindowDisplay, bool)> { if let Some(primary) = snapshot.primary.as_ref() && matches_window_label(primary, label) { return Some((primary, false)); } if let Some(secondary) = snapshot.secondary.as_ref() && matches_window_label(secondary, label) { return Some((secondary, true)); } None}fn find_primary_codex_window<'a>( snapshot: &'a RateLimitSnapshotDisplay, label: &str,) -> Option<(&'a RateLimitWindowDisplay, bool)> { let primary = snapshot.primary.as_ref()?; if matches_window_label(primary, label) { Some((primary, false)) } else { None }}fn secondary_window_with_label_when_weekly_is_available<'a>( snapshot: &'a RateLimitSnapshotDisplay, label: &str,) -> Option<(&'a RateLimitWindowDisplay, bool)> { find_codex_window(snapshot, "weekly")?; let secondary = snapshot.secondary.as_ref()?; if matches_window_label(secondary, label) { Some((secondary, true)) } else { None }}fn non_weekly_primary_window( snapshot: &RateLimitSnapshotDisplay,) -> Option<(&RateLimitWindowDisplay, bool)> { let primary = snapshot.primary.as_ref()?; if matches_window_label(primary, "weekly") { None } else { Some((primary, false)) }}fn non_weekly_secondary_window_when_primary_is_weekly( snapshot: &RateLimitSnapshotDisplay,) -> Option<(&RateLimitWindowDisplay, bool)> { let primary = snapshot.primary.as_ref()?; if !matches_window_label(primary, "weekly") { return None; } let secondary = snapshot.secondary.as_ref()?; if matches_window_label(secondary, "weekly") { None } else { Some((secondary, true)) }}fn matches_window_label(window: &RateLimitWindowDisplay, label: &str) -> bool { window .window_minutes .and_then(get_limits_duration) .as_deref() == Some(label)}fn permissions_display(config: &Config) -> String { let active_permission_profile = config.permissions.active_permission_profile(); if let Some(active_permission_profile) = active_permission_profile.as_ref() && !active_permission_profile.id.starts_with(':') { return active_permission_profile.id.clone(); } let permission_profile = config.permissions.effective_permission_profile(); let workspace_roots = config.effective_workspace_roots(); let summary = summarize_permission_profile(&permission_profile, &config.cwd, workspace_roots.as_slice()); if let Some(details) = summary.strip_prefix("read-only") && !details.contains("(network access enabled)") { return "Read Only".to_string(); } if let Some(details) = summary.strip_prefix("workspace-write") && !details.contains("(network access enabled)") { return "Workspace".to_string(); } if permission_profile == PermissionProfile::Disabled { return "Full Access".to_string(); } "Custom permissions".to_string()}fn approval_mode_display(config: &Config) -> String { let approval_policy = AskForApproval::from(config.permissions.approval_policy.value()); if approval_policy == AskForApproval::OnRequest { return match config.approvals_reviewer { ApprovalsReviewer::AutoReview => "Approve for me".to_string(), ApprovalsReviewer::User => "Ask for approval".to_string(), }; } config.permissions.approval_policy.value().to_string()}fn parse_items_with_invalids<T>(ids: impl IntoIterator<Item = String>) -> (Vec<T>, Vec<String>)where T: std::str::FromStr,{ let mut invalid = Vec::new(); let mut invalid_seen = HashSet::new(); let mut items = Vec::new(); for id in ids { match id.parse::<T>() { Ok(item) => items.push(item), Err(_) => { if invalid_seen.insert(id.clone()) { invalid.push(format!(r#""{id}""#)); } } } } (items, invalid)}