Files
codex/codex-rs/tui/src/bottom_pane/status_surface_preview.rs
T
Joe Savona 66a56b17a1 Keep configured status lines on the native renderer (#60027)
## Why

Enabling `tui_extensions_status_line` routed configured status lines through a plain-text extension renderer and hid the “Use theme colors” option in the status line picker.

## What changed

Always use the native status line renderer for configured items and their live picker previews, preserving theme colors and pull request links regardless of the extensions flag. Always expose the theme color option in the picker, and clarify that the flag enables experimental MCP status line providers.

## Testing

Update coverage to require native status line content across extensions flag changes and exercise the live picker preview with the flag enabled. Retain checks for pull request hyperlinks and color preference preservation.

GitOrigin-RevId: bce7df5696c2bd3583d3ef99c43e7a2014e17294
2026-11-07 01:51:31 +00:00

316 lines
10 KiB
Rust

use std::collections::BTreeMap;
use super::footer_content::FooterContent;
use codex_protocol::ThreadId;
use super::status_line_from_segments;
use super::status_line_setup::StatusLineItem;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) enum StatusSurfacePreviewItem {
AppName,
ProjectName,
ProjectRoot,
CurrentDir,
Hostname,
Status,
ThreadName,
ThreadTitle,
GitBranch,
PullRequestNumber,
BranchChanges,
Permissions,
ApprovalMode,
ContextRemaining,
ContextUsed,
FiveHourLimit,
WeeklyLimit,
CodexVersion,
ContextWindowSize,
UsedTokens,
TotalInputTokens,
TotalOutputTokens,
ThreadCredits,
EstimatedThreadCost,
SessionId,
FastMode,
Daybreak,
RawOutput,
WorkspaceHeadline,
Model,
ModelWithReasoning,
Reasoning,
Profile,
Provider,
TaskProgress,
}
impl StatusSurfacePreviewItem {
fn placeholder(self) -> &'static str {
match self {
StatusSurfacePreviewItem::AppName => "codex",
StatusSurfacePreviewItem::ProjectName => "my-project",
StatusSurfacePreviewItem::ProjectRoot => "my-project",
StatusSurfacePreviewItem::CurrentDir => "~/my-project/subdir",
StatusSurfacePreviewItem::Hostname => "my-host",
StatusSurfacePreviewItem::Status => "Working",
StatusSurfacePreviewItem::ThreadName => "thread name",
StatusSurfacePreviewItem::ThreadTitle => "thread title",
StatusSurfacePreviewItem::GitBranch => "feat/awesome-feature",
StatusSurfacePreviewItem::PullRequestNumber => "PR #123",
StatusSurfacePreviewItem::BranchChanges => "+12 -3",
StatusSurfacePreviewItem::Permissions => "Workspace",
StatusSurfacePreviewItem::ApprovalMode => "on-request",
StatusSurfacePreviewItem::ContextRemaining => "Context 0% left",
StatusSurfacePreviewItem::ContextUsed => "Context 0% used",
StatusSurfacePreviewItem::FiveHourLimit => "primary 0%",
StatusSurfacePreviewItem::WeeklyLimit => "secondary 0%",
StatusSurfacePreviewItem::CodexVersion => "0.0.0",
StatusSurfacePreviewItem::ContextWindowSize => "0 window",
StatusSurfacePreviewItem::UsedTokens => "0 used",
StatusSurfacePreviewItem::TotalInputTokens => "0 in",
StatusSurfacePreviewItem::TotalOutputTokens => "0 out",
StatusSurfacePreviewItem::ThreadCredits => "5.2 credits",
StatusSurfacePreviewItem::EstimatedThreadCost => "~$1.82",
StatusSurfacePreviewItem::SessionId => "550e8400-e29b-41d4",
StatusSurfacePreviewItem::FastMode => "Fast",
StatusSurfacePreviewItem::Daybreak => "Daybreak off",
StatusSurfacePreviewItem::RawOutput => "raw output",
StatusSurfacePreviewItem::WorkspaceHeadline => "Workspace headline",
StatusSurfacePreviewItem::Model => "gpt-5.2-codex",
StatusSurfacePreviewItem::ModelWithReasoning => "gpt-5.2-codex medium",
StatusSurfacePreviewItem::Reasoning => "medium",
StatusSurfacePreviewItem::Profile => "profile: <name>",
StatusSurfacePreviewItem::Provider => "provider: <id>",
StatusSurfacePreviewItem::TaskProgress => "Tasks 0/0",
}
}
pub(crate) fn iter() -> impl Iterator<Item = Self> {
[
Self::AppName,
Self::ProjectName,
Self::ProjectRoot,
Self::CurrentDir,
Self::Hostname,
Self::Status,
Self::ThreadName,
Self::ThreadTitle,
Self::GitBranch,
Self::PullRequestNumber,
Self::BranchChanges,
Self::Permissions,
Self::ApprovalMode,
Self::ContextRemaining,
Self::ContextUsed,
Self::FiveHourLimit,
Self::WeeklyLimit,
Self::CodexVersion,
Self::ContextWindowSize,
Self::UsedTokens,
Self::TotalInputTokens,
Self::TotalOutputTokens,
Self::ThreadCredits,
Self::EstimatedThreadCost,
Self::SessionId,
Self::FastMode,
Self::Daybreak,
Self::RawOutput,
Self::WorkspaceHeadline,
Self::Model,
Self::ModelWithReasoning,
Self::Reasoning,
Self::Profile,
Self::Provider,
Self::TaskProgress,
]
.into_iter()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct PreviewValue {
text: String,
is_placeholder: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct StatusSurfacePreviewData {
pub(crate) thread_id: Option<ThreadId>,
pub(crate) pull_request_url: Option<String>,
values: BTreeMap<StatusSurfacePreviewItem, PreviewValue>,
}
impl Default for StatusSurfacePreviewData {
fn default() -> Self {
let mut data = Self {
thread_id: None,
pull_request_url: None,
values: BTreeMap::new(),
};
for item in StatusSurfacePreviewItem::iter() {
data.set_placeholder(item, item.placeholder());
}
data
}
}
impl StatusSurfacePreviewData {
pub(crate) fn from_iter<I, V>(values: I) -> Self
where
I: IntoIterator<Item = (StatusSurfacePreviewItem, V)>,
V: Into<String>,
{
let mut data = Self::default();
for (item, value) in values {
data.set_live(item, value);
}
data
}
pub(crate) fn set_live<V>(&mut self, item: StatusSurfacePreviewItem, value: V)
where
V: Into<String>,
{
self.values.insert(
item,
PreviewValue {
text: value.into(),
is_placeholder: false,
},
);
}
pub(crate) fn set_placeholder<V>(&mut self, item: StatusSurfacePreviewItem, value: V)
where
V: Into<String>,
{
if self
.values
.get(&item)
.is_some_and(|value| !value.is_placeholder)
{
return;
}
self.values.insert(
item,
PreviewValue {
text: value.into(),
is_placeholder: true,
},
);
}
pub(crate) fn suppress_placeholder(&mut self, item: StatusSurfacePreviewItem) {
if self
.values
.get(&item)
.is_some_and(|value| value.is_placeholder)
{
self.values.remove(&item);
}
}
pub(crate) fn rate_limit_item_name(
&self,
item: StatusSurfacePreviewItem,
fallback: &str,
) -> String {
self.live_value_for(item)
.and_then(rate_limit_preview_copy)
.map(|copy| copy.name.to_string())
.unwrap_or_else(|| fallback.to_string())
}
pub(crate) fn rate_limit_item_description(
&self,
item: StatusSurfacePreviewItem,
fallback: &str,
) -> String {
self.live_value_for(item)
.and_then(rate_limit_preview_copy)
.map(|copy| copy.description.to_string())
.unwrap_or_else(|| fallback.to_string())
}
pub(crate) fn value_for(&self, item: StatusSurfacePreviewItem) -> Option<&str> {
self.values.get(&item).map(|value| value.text.as_str())
}
fn live_value_for(&self, item: StatusSurfacePreviewItem) -> Option<&str> {
self.values
.get(&item)
.filter(|value| !value.is_placeholder)
.map(|value| value.text.as_str())
}
pub(crate) fn status_line_for_items<I>(
&self,
items: I,
use_theme_colors: bool,
) -> Option<FooterContent>
where
I: IntoIterator<Item = StatusLineItem>,
{
let segments = items.into_iter().filter_map(|item| {
self.value_for(item.preview_item())
.map(|value| (item, value.to_string()))
});
status_line_from_segments(
segments,
use_theme_colors,
self.thread_id,
self.live_value_for(StatusSurfacePreviewItem::PullRequestNumber)
.and(self.pull_request_url.as_deref()),
)
.map(FooterContent::HyperlinkLine)
}
}
struct RateLimitPreviewCopy {
name: &'static str,
description: &'static str,
}
fn rate_limit_preview_copy(value: &str) -> Option<RateLimitPreviewCopy> {
let value = value.trim_start();
if value.starts_with("secondary usage ") {
Some(RateLimitPreviewCopy {
name: "secondary-usage-limit",
description: "Remaining usage on the secondary usage limit (omitted when unavailable)",
})
} else if value.starts_with("usage ") {
Some(RateLimitPreviewCopy {
name: "usage-limit",
description: "Remaining usage on the primary usage limit (omitted when unavailable)",
})
} else if value.starts_with("5h ") {
Some(RateLimitPreviewCopy {
name: "five-hour-limit",
description: "Remaining usage on the 5-hour usage limit (omitted when unavailable)",
})
} else if value.starts_with("daily ") {
Some(RateLimitPreviewCopy {
name: "daily-limit",
description: "Remaining usage on the daily usage limit (omitted when unavailable)",
})
} else if value.starts_with("weekly ") {
Some(RateLimitPreviewCopy {
name: "weekly-limit",
description: "Remaining usage on the weekly usage limit (omitted when unavailable)",
})
} else if value.starts_with("monthly ") {
Some(RateLimitPreviewCopy {
name: "monthly-limit",
description: "Remaining usage on the monthly usage limit (omitted when unavailable)",
})
} else if value.starts_with("annual ") {
Some(RateLimitPreviewCopy {
name: "annual-limit",
description: "Remaining usage on the annual usage limit (omitted when unavailable)",
})
} else {
None
}
}