Normalize /statusline & /title items (#18886)

This change aligns the `/statusline` and `/title` UIs around the same
normalized item model so both surfaces use consistent ids, labels, and
preview semantics. It keeps the shared preview work from #18435 ,
tightens the remaining mismatches by standardizing item naming, expands
title/status item coverage where appropriate, and makes `/title` preview
use the same title-specific formatting path as the real rendered
terminal title.

- Normalizes persisted item ids and keeps legacy aliases for
compatibility
- Aligns `status-line` and `terminal-title` items with the shared
preview model
- Routes `terminal-title` preview through title-specific formatting and
truncation
- Updates the affected status/title setup snapshots

Added to `/statusline`:
- status
- task-progress
  
Normalized in `/statusline`:
- model-name -> model
- project-root -> project-name

Added to `/title`:
- current-dir
- context-remaining
- context-used
- five-hour-limit
- weekly-limit
- codex-version
- used-tokens
- total-input-tokens
- total-output-tokens
- session-id
- fast-mode
- model-with-reasoning

Normalized in `/title`:
- project -> project-name
- thread -> thread-title
- model-name -> model
This commit is contained in:
canvrno-oai
2026-04-21 16:13:09 -07:00
committed by GitHub
Unverified
parent ef00014a46
commit 2202675632
9 changed files with 570 additions and 33 deletions
@@ -0,0 +1,21 @@
---
source: tui/src/bottom_pane/status_line_setup.rs
expression: "render_lines(&view, 72)"
---
Configure Status Line
Select which items to display in the status line.
Type to search
>
[x] model Current model name
[x] current-dir Current working directory
[x] git-branch Current Git branch (omitted when unavaila…
[ ] model-with-reasoning Current model name with reasoning level
[ ] project-name Project name (omitted when unavailable)
[ ] run-state Compact session run-state text (Ready, Wo…
[ ] context-remaining Percentage of context window remaining (o…
[ ] context-used Percentage of context window used (omitte…
gpt-5-codex · ~/codex-rs · jif/statusline-preview
Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc
@@ -0,0 +1,21 @@
---
source: tui/src/bottom_pane/title_setup.rs
expression: "render_lines(&view, 84)"
---
Configure Terminal Title
Select which items to display in the terminal title.
Type to search
>
[x] project-name Project name (falls back to current directory name)
[x] spinner Animated task spinner (omitted while idle or when animat…
[x] run-state Compact session run-state text (Ready, Working, Thinking)
[x] thread-title Current thread title (omitted when unavailable)
[ ] app-name Codex app name
[ ] current-dir Current working directory
[ ] git-branch Current Git branch (omitted when unavailable)
[ ] context-remaining Percentage of context window remaining (omitted when unk…
my-project ⠋ Working | thread title
Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc to cancel.
@@ -49,6 +49,7 @@ use crate::render::renderable::Renderable;
#[strum(serialize_all = "kebab_case")]
pub(crate) enum StatusLineItem {
/// The current model name.
#[strum(to_string = "model", serialize = "model-name")]
ModelName,
/// Model name with reasoning level suffix.
@@ -58,11 +59,20 @@ pub(crate) enum StatusLineItem {
CurrentDir,
/// Project root directory (if detected).
#[strum(
to_string = "project-name",
serialize = "project",
serialize = "project-root"
)]
ProjectRoot,
/// Current git branch name (if in a repository).
GitBranch,
/// Compact runtime run-state text.
#[strum(to_string = "run-state", serialize = "status")]
Status,
/// Percentage of context window remaining.
ContextRemaining,
@@ -101,6 +111,9 @@ pub(crate) enum StatusLineItem {
/// Current thread title (if set by user).
ThreadTitle,
/// Latest checklist task progress from `update_plan` (if available).
TaskProgress,
}
impl StatusLineItem {
@@ -110,8 +123,9 @@ impl StatusLineItem {
StatusLineItem::ModelName => "Current model name",
StatusLineItem::ModelWithReasoning => "Current model name with reasoning level",
StatusLineItem::CurrentDir => "Current working directory",
StatusLineItem::ProjectRoot => "Project root directory (omitted when unavailable)",
StatusLineItem::ProjectRoot => "Project name (omitted when unavailable)",
StatusLineItem::GitBranch => "Current Git branch (omitted when unavailable)",
StatusLineItem::Status => "Compact session run-state text (Ready, Working, Thinking)",
StatusLineItem::ContextRemaining => {
"Percentage of context window remaining (omitted when unknown)"
}
@@ -135,7 +149,10 @@ impl StatusLineItem {
"Current session identifier (omitted until session starts)"
}
StatusLineItem::FastMode => "Whether Fast mode is currently active",
StatusLineItem::ThreadTitle => "Current thread title (omitted unless changed by user)",
StatusLineItem::ThreadTitle => "Current thread title (omitted when unavailable)",
StatusLineItem::TaskProgress => {
"Latest task progress from update_plan (omitted until available)"
}
}
}
@@ -146,6 +163,7 @@ impl StatusLineItem {
StatusLineItem::CurrentDir => StatusSurfacePreviewItem::CurrentDir,
StatusLineItem::ProjectRoot => StatusSurfacePreviewItem::ProjectRoot,
StatusLineItem::GitBranch => StatusSurfacePreviewItem::GitBranch,
StatusLineItem::Status => StatusSurfacePreviewItem::Status,
StatusLineItem::ContextRemaining => StatusSurfacePreviewItem::ContextRemaining,
StatusLineItem::ContextUsed => StatusSurfacePreviewItem::ContextUsed,
StatusLineItem::FiveHourLimit => StatusSurfacePreviewItem::FiveHourLimit,
@@ -158,6 +176,7 @@ impl StatusLineItem {
StatusLineItem::SessionId => StatusSurfacePreviewItem::SessionId,
StatusLineItem::FastMode => StatusSurfacePreviewItem::FastMode,
StatusLineItem::ThreadTitle => StatusSurfacePreviewItem::ThreadTitle,
StatusLineItem::TaskProgress => StatusSurfacePreviewItem::TaskProgress,
}
}
}
@@ -289,7 +308,15 @@ impl Renderable for StatusLineSetupView {
#[cfg(test)]
mod tests {
use super::*;
use crate::app_event_sender::AppEventSender;
use insta::assert_snapshot;
use pretty_assertions::assert_eq;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::text::Line;
use tokio::sync::mpsc::unbounded_channel;
use crate::app_event::AppEvent;
#[test]
fn context_used_accepts_context_usage_legacy_id() {
@@ -315,4 +342,222 @@ mod tests {
"context-remaining"
);
}
#[test]
fn project_name_is_canonical_and_accepts_legacy_ids() {
assert_eq!(StatusLineItem::ProjectRoot.to_string(), "project-name");
assert_eq!(
"project-name".parse::<StatusLineItem>(),
Ok(StatusLineItem::ProjectRoot)
);
assert_eq!(
"project".parse::<StatusLineItem>(),
Ok(StatusLineItem::ProjectRoot)
);
assert_eq!(
"project-root".parse::<StatusLineItem>(),
Ok(StatusLineItem::ProjectRoot)
);
}
#[test]
fn model_is_canonical_and_accepts_model_name_legacy_id() {
assert_eq!(StatusLineItem::ModelName.to_string(), "model");
assert_eq!(
"model".parse::<StatusLineItem>(),
Ok(StatusLineItem::ModelName)
);
assert_eq!(
"model-name".parse::<StatusLineItem>(),
Ok(StatusLineItem::ModelName)
);
}
#[test]
fn run_state_is_canonical_and_accepts_status_legacy_id() {
assert_eq!(StatusLineItem::Status.to_string(), "run-state");
assert_eq!(
"run-state".parse::<StatusLineItem>(),
Ok(StatusLineItem::Status)
);
assert_eq!(
"status".parse::<StatusLineItem>(),
Ok(StatusLineItem::Status)
);
}
#[test]
fn parse_status_line_items_accepts_title_only_variants() {
let items = ["run-state", "task-progress"]
.into_iter()
.map(str::parse::<StatusLineItem>)
.collect::<Result<Vec<_>, _>>();
assert_eq!(
items,
Ok(vec![StatusLineItem::Status, StatusLineItem::TaskProgress,])
);
}
#[test]
fn preview_uses_runtime_values() {
let preview_data = StatusSurfacePreviewData::from_iter([
(
StatusLineItem::ModelName.preview_item(),
"gpt-5".to_string(),
),
(
StatusLineItem::CurrentDir.preview_item(),
"/repo".to_string(),
),
]);
let items = [
MultiSelectItem {
id: StatusLineItem::ModelName.to_string(),
name: String::new(),
description: None,
enabled: true,
},
MultiSelectItem {
id: StatusLineItem::CurrentDir.to_string(),
name: String::new(),
description: None,
enabled: true,
},
];
assert_eq!(
preview_data.line_for_items(
items
.iter()
.filter_map(|item| item.id.parse::<StatusLineItem>().ok())
.map(StatusLineItem::preview_item),
),
Some(Line::from("gpt-5 · /repo"))
);
}
#[test]
fn preview_uses_placeholders_when_runtime_values_are_missing() {
let preview_data = StatusSurfacePreviewData::from_iter([(
StatusSurfacePreviewItem::Model,
"gpt-5".to_string(),
)]);
let items = [
MultiSelectItem {
id: StatusLineItem::ModelName.to_string(),
name: String::new(),
description: None,
enabled: true,
},
MultiSelectItem {
id: StatusLineItem::GitBranch.to_string(),
name: String::new(),
description: None,
enabled: true,
},
];
assert_eq!(
preview_data.line_for_items(
items
.iter()
.filter_map(|item| item.id.parse::<StatusLineItem>().ok())
.map(StatusLineItem::preview_item),
),
Some(Line::from("gpt-5 · feat/awesome-feature"))
);
}
#[test]
fn preview_includes_thread_title() {
let preview_data = StatusSurfacePreviewData::from_iter([
(
StatusLineItem::ModelName.preview_item(),
"gpt-5".to_string(),
),
(
StatusLineItem::ThreadTitle.preview_item(),
"Roadmap cleanup".to_string(),
),
]);
let items = [
MultiSelectItem {
id: StatusLineItem::ModelName.to_string(),
name: String::new(),
description: None,
enabled: true,
},
MultiSelectItem {
id: StatusLineItem::ThreadTitle.to_string(),
name: String::new(),
description: None,
enabled: true,
},
];
assert_eq!(
preview_data.line_for_items(
items
.iter()
.filter_map(|item| item.id.parse::<StatusLineItem>().ok())
.map(StatusLineItem::preview_item),
),
Some(Line::from("gpt-5 · Roadmap cleanup"))
);
}
#[test]
fn setup_view_snapshot_uses_runtime_preview_values() {
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
let view = StatusLineSetupView::new(
Some(&[
StatusLineItem::ModelName.to_string(),
StatusLineItem::CurrentDir.to_string(),
StatusLineItem::GitBranch.to_string(),
]),
StatusSurfacePreviewData::from_iter([
(
StatusLineItem::ModelName.preview_item(),
"gpt-5-codex".to_string(),
),
(
StatusLineItem::CurrentDir.preview_item(),
"~/codex-rs".to_string(),
),
(
StatusLineItem::GitBranch.preview_item(),
"jif/statusline-preview".to_string(),
),
(
StatusLineItem::WeeklyLimit.preview_item(),
"weekly 82%".to_string(),
),
]),
AppEventSender::new(tx_raw),
);
assert_snapshot!(render_lines(&view, /*width*/ 72));
}
fn render_lines(view: &StatusLineSetupView, width: u16) -> String {
let height = view.desired_height(width);
let area = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(area);
view.render(area, &mut buf);
(0..area.height)
.map(|row| {
let mut line = String::new();
for col in 0..area.width {
let symbol = buf[(area.x + col, area.y + row)].symbol();
if symbol.is_empty() {
line.push(' ');
} else {
line.push_str(symbol);
}
}
line
})
.collect::<Vec<_>>()
.join("\n")
}
}
+219 -6
View File
@@ -37,17 +37,46 @@ pub(crate) enum TerminalTitleItem {
/// Codex app name.
AppName,
/// Project root name, or a compact cwd fallback.
#[strum(to_string = "project-name", serialize = "project")]
Project,
/// Current working directory path.
CurrentDir,
/// Animated task spinner while active.
Spinner,
/// Compact runtime status text.
/// Compact runtime run-state text.
#[strum(to_string = "run-state", serialize = "status")]
Status,
/// Current thread title (if available).
#[strum(to_string = "thread-title", serialize = "thread")]
Thread,
/// Current git branch (if available).
GitBranch,
/// Percentage of context window remaining.
ContextRemaining,
/// Percentage of context window used.
#[strum(to_string = "context-used", serialize = "context-usage")]
ContextUsed,
/// Remaining usage on the 5-hour rate limit.
FiveHourLimit,
/// Remaining usage on the weekly rate limit.
WeeklyLimit,
/// Codex application version.
CodexVersion,
/// Total tokens used in the current session.
UsedTokens,
/// Total input tokens consumed.
TotalInputTokens,
/// Total output tokens generated.
TotalOutputTokens,
/// Full session UUID.
SessionId,
/// Whether Fast mode is currently active.
FastMode,
/// Current model name.
#[strum(to_string = "model", serialize = "model-name")]
Model,
/// Current model name with reasoning level.
ModelWithReasoning,
/// Latest checklist task progress from `update_plan` (if available).
TaskProgress,
}
@@ -57,13 +86,37 @@ impl TerminalTitleItem {
match self {
TerminalTitleItem::AppName => "Codex app name",
TerminalTitleItem::Project => "Project name (falls back to current directory name)",
TerminalTitleItem::CurrentDir => "Current working directory",
TerminalTitleItem::Spinner => {
"Animated task spinner (omitted while idle or when animations are off)"
}
TerminalTitleItem::Status => "Compact session status text (Ready, Working, Thinking)",
TerminalTitleItem::Thread => "Current thread title (omitted until available)",
TerminalTitleItem::Status => {
"Compact session run-state text (Ready, Working, Thinking)"
}
TerminalTitleItem::Thread => "Current thread title (omitted when unavailable)",
TerminalTitleItem::GitBranch => "Current Git branch (omitted when unavailable)",
TerminalTitleItem::ContextRemaining => {
"Percentage of context window remaining (omitted when unknown)"
}
TerminalTitleItem::ContextUsed => {
"Percentage of context window used (omitted when unknown)"
}
TerminalTitleItem::FiveHourLimit => {
"Remaining usage on 5-hour usage limit (omitted when unavailable)"
}
TerminalTitleItem::WeeklyLimit => {
"Remaining usage on weekly usage limit (omitted when unavailable)"
}
TerminalTitleItem::CodexVersion => "Codex application version",
TerminalTitleItem::UsedTokens => "Total tokens used in session (omitted when zero)",
TerminalTitleItem::TotalInputTokens => "Total input tokens used in session",
TerminalTitleItem::TotalOutputTokens => "Total output tokens used in session",
TerminalTitleItem::SessionId => {
"Current session identifier (omitted until session starts)"
}
TerminalTitleItem::FastMode => "Whether Fast mode is currently active",
TerminalTitleItem::Model => "Current model name",
TerminalTitleItem::ModelWithReasoning => "Current model name with reasoning level",
TerminalTitleItem::TaskProgress => {
"Latest task progress from update_plan (omitted until available)"
}
@@ -74,11 +127,27 @@ impl TerminalTitleItem {
match self {
TerminalTitleItem::AppName => Some(StatusSurfacePreviewItem::AppName),
TerminalTitleItem::Project => Some(StatusSurfacePreviewItem::ProjectName),
TerminalTitleItem::CurrentDir => Some(StatusSurfacePreviewItem::CurrentDir),
TerminalTitleItem::Spinner => None,
TerminalTitleItem::Status => Some(StatusSurfacePreviewItem::Status),
TerminalTitleItem::Thread => Some(StatusSurfacePreviewItem::ThreadTitle),
TerminalTitleItem::GitBranch => Some(StatusSurfacePreviewItem::GitBranch),
TerminalTitleItem::ContextRemaining => Some(StatusSurfacePreviewItem::ContextRemaining),
TerminalTitleItem::ContextUsed => Some(StatusSurfacePreviewItem::ContextUsed),
TerminalTitleItem::FiveHourLimit => Some(StatusSurfacePreviewItem::FiveHourLimit),
TerminalTitleItem::WeeklyLimit => Some(StatusSurfacePreviewItem::WeeklyLimit),
TerminalTitleItem::CodexVersion => Some(StatusSurfacePreviewItem::CodexVersion),
TerminalTitleItem::UsedTokens => Some(StatusSurfacePreviewItem::UsedTokens),
TerminalTitleItem::TotalInputTokens => Some(StatusSurfacePreviewItem::TotalInputTokens),
TerminalTitleItem::TotalOutputTokens => {
Some(StatusSurfacePreviewItem::TotalOutputTokens)
}
TerminalTitleItem::SessionId => Some(StatusSurfacePreviewItem::SessionId),
TerminalTitleItem::FastMode => Some(StatusSurfacePreviewItem::FastMode),
TerminalTitleItem::Model => Some(StatusSurfacePreviewItem::Model),
TerminalTitleItem::ModelWithReasoning => {
Some(StatusSurfacePreviewItem::ModelWithReasoning)
}
TerminalTitleItem::TaskProgress => Some(StatusSurfacePreviewItem::TaskProgress),
}
}
@@ -267,12 +336,56 @@ impl Renderable for TerminalTitleSetupView {
#[cfg(test)]
mod tests {
use super::*;
use insta::assert_snapshot;
use pretty_assertions::assert_eq;
use tokio::sync::mpsc::unbounded_channel;
fn render_lines(view: &TerminalTitleSetupView, width: u16) -> String {
let height = view.desired_height(width);
let area = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(area);
view.render(area, &mut buf);
let lines: Vec<String> = (0..area.height)
.map(|row| {
let mut line = String::new();
for col in 0..area.width {
let symbol = buf[(area.x + col, area.y + row)].symbol();
if symbol.is_empty() {
line.push(' ');
} else {
line.push_str(symbol);
}
}
line
})
.collect();
lines.join("\n")
}
#[test]
fn renders_title_setup_popup() {
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let selected = [
"project-name".to_string(),
"spinner".to_string(),
"run-state".to_string(),
"thread-title".to_string(),
];
let view =
TerminalTitleSetupView::new(Some(&selected), StatusSurfacePreviewData::default(), tx);
assert_snapshot!(
"terminal_title_setup_basic",
render_lines(&view, /*width*/ 84)
);
}
#[test]
fn parse_terminal_title_items_preserves_order() {
let items =
parse_terminal_title_items(["project", "spinner", "status", "thread"].into_iter());
let items = parse_terminal_title_items(
["project-name", "spinner", "run-state", "thread-title"].into_iter(),
);
assert_eq!(
items,
Some(vec![
@@ -290,14 +403,114 @@ mod tests {
assert_eq!(items, None);
}
#[test]
fn project_name_is_canonical_and_accepts_project_legacy_id() {
assert_eq!(TerminalTitleItem::Project.to_string(), "project-name");
assert_eq!(
"project-name".parse::<TerminalTitleItem>(),
Ok(TerminalTitleItem::Project)
);
assert_eq!(
"project".parse::<TerminalTitleItem>(),
Ok(TerminalTitleItem::Project)
);
}
#[test]
fn thread_title_is_canonical_and_accepts_thread_legacy_id() {
assert_eq!(TerminalTitleItem::Thread.to_string(), "thread-title");
assert_eq!(
"thread-title".parse::<TerminalTitleItem>(),
Ok(TerminalTitleItem::Thread)
);
assert_eq!(
"thread".parse::<TerminalTitleItem>(),
Ok(TerminalTitleItem::Thread)
);
}
#[test]
fn model_is_canonical_and_accepts_model_name_legacy_id() {
assert_eq!(TerminalTitleItem::Model.to_string(), "model");
assert_eq!(
"model".parse::<TerminalTitleItem>(),
Ok(TerminalTitleItem::Model)
);
assert_eq!(
"model-name".parse::<TerminalTitleItem>(),
Ok(TerminalTitleItem::Model)
);
}
#[test]
fn run_state_is_canonical_and_accepts_status_legacy_id() {
assert_eq!(TerminalTitleItem::Status.to_string(), "run-state");
assert_eq!(
"run-state".parse::<TerminalTitleItem>(),
Ok(TerminalTitleItem::Status)
);
assert_eq!(
"status".parse::<TerminalTitleItem>(),
Ok(TerminalTitleItem::Status)
);
}
#[test]
fn model_with_reasoning_has_distinct_id() {
assert_eq!(
TerminalTitleItem::ModelWithReasoning.to_string(),
"model-with-reasoning"
);
assert_eq!(
"model-with-reasoning".parse::<TerminalTitleItem>(),
Ok(TerminalTitleItem::ModelWithReasoning)
);
}
#[test]
fn parse_terminal_title_items_accepts_kebab_case_variants() {
let items = parse_terminal_title_items(["app-name", "git-branch"].into_iter());
let items = parse_terminal_title_items(
[
"app-name",
"context-remaining",
"context-used",
"five-hour-limit",
"git-branch",
"spinner",
"current-dir",
"project-name",
"model",
"model-with-reasoning",
"weekly-limit",
"codex-version",
"used-tokens",
"total-input-tokens",
"total-output-tokens",
"session-id",
"fast-mode",
]
.into_iter(),
);
assert_eq!(
items,
Some(vec![
TerminalTitleItem::AppName,
TerminalTitleItem::ContextRemaining,
TerminalTitleItem::ContextUsed,
TerminalTitleItem::FiveHourLimit,
TerminalTitleItem::GitBranch,
TerminalTitleItem::Spinner,
TerminalTitleItem::CurrentDir,
TerminalTitleItem::Project,
TerminalTitleItem::Model,
TerminalTitleItem::ModelWithReasoning,
TerminalTitleItem::WeeklyLimit,
TerminalTitleItem::CodexVersion,
TerminalTitleItem::UsedTokens,
TerminalTitleItem::TotalInputTokens,
TerminalTitleItem::TotalOutputTokens,
TerminalTitleItem::SessionId,
TerminalTitleItem::FastMode,
])
);
}
+6 -9
View File
@@ -1897,7 +1897,11 @@ impl ChatWidget {
.config
.tui_terminal_title
.as_ref()
.is_some_and(|items| items.iter().any(|item| item == "status"));
.is_some_and(|items| {
items
.iter()
.any(|item| item == "run-state" || item == "status")
});
let title_uses_spinner = self
.config
.tui_terminal_title
@@ -7670,13 +7674,7 @@ impl ChatWidget {
fn terminal_title_preview_data(&mut self) -> StatusSurfacePreviewData {
let mut preview_data = self.status_surface_preview_data();
let now = Instant::now();
for item in [
TerminalTitleItem::Project,
TerminalTitleItem::Thread,
TerminalTitleItem::GitBranch,
TerminalTitleItem::Model,
TerminalTitleItem::TaskProgress,
] {
for item in TerminalTitleItem::iter() {
let Some(preview_item) = item.preview_item() else {
continue;
};
@@ -7687,7 +7685,6 @@ impl ChatWidget {
}
preview_data
}
fn open_theme_picker(&mut self) {
let codex_home = crate::legacy_core::config::find_codex_home().ok();
let terminal_width = self
@@ -13,7 +13,7 @@ expression: terminal_title_popup_snapshot(&mut chat)
[ ] app-name Codex app name
[ ] project Project name (falls back to current directory name)
[ ] spinner Animated task spinner (omitted while idle or when animations are off)
[ ] status Compact session status text (Ready, Working, Thinking)
[ ] run-state Compact session run-state text (Ready, Working, Thinking)
[ ] model Current model name
thread title | feat/awesome-feature | Tasks 0/0
@@ -13,7 +13,7 @@ expression: terminal_title_popup_snapshot(&mut chat)
[x] task-progress Latest task progress from update_plan (omitted until available)
[ ] app-name Codex app name
[ ] spinner Animated task spinner (omitted while idle or when animations are off)
[ ] status Compact session status text (Ready, Working, Thinking)
[ ] run-state Compact session run-state text (Ready, Working, Thinking)
[ ] model Current model name
preview-live-root | Live preview thread | feature/live-preview-branch | Tasks 2/5
@@ -12,7 +12,7 @@ expression: terminal_title_popup_snapshot(&mut chat)
[x] task-progress Latest task progress from update_plan (omitted until available)
[ ] app-name Codex app name
[ ] spinner Animated task spinner (omitted while idle or when animations are off)
[ ] status Compact session status text (Ready, Working, Thinking)
[ ] run-state Compact session run-state text (Ready, Working, Thinking)
[ ] git-branch Current Git branch (omitted when unavailable)
[ ] model Current model name
+53 -13
View File
@@ -7,7 +7,7 @@ use super::*;
/// Items shown in the terminal title when the user has not configured a
/// custom selection. Intentionally minimal: spinner + project name.
pub(super) const DEFAULT_TERMINAL_TITLE_ITEMS: [&str; 2] = ["spinner", "project"];
pub(super) const DEFAULT_TERMINAL_TITLE_ITEMS: [&str; 2] = ["spinner", "project-name"];
/// Braille-pattern dot-spinner frames for the terminal title animation.
pub(super) const TERMINAL_TITLE_SPINNER_FRAMES: [&str; 10] =
@@ -421,18 +421,7 @@ impl ChatWidget {
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 => {
let label =
Self::status_line_reasoning_effort_label(self.effective_reasoning_effort());
let fast_label = if self
.should_show_fast_status(self.current_model(), self.config.service_tier)
{
" fast"
} else {
""
};
Some(format!("{} {label}{fast_label}", self.model_display_name()))
}
StatusLineItem::ModelWithReasoning => Some(self.model_with_reasoning_display_name()),
StatusLineItem::CurrentDir => {
Some(format_directory_display(
self.status_line_cwd(),
@@ -441,6 +430,7 @@ impl ChatWidget {
}
StatusLineItem::ProjectRoot => self.status_line_project_root_name(),
StatusLineItem::GitBranch => self.status_line_branch.clone(),
StatusLineItem::Status => Some(self.terminal_title_status_text()),
StatusLineItem::UsedTokens => {
let usage = self.status_line_total_usage();
let total = usage.tokens_in_context_window();
@@ -502,6 +492,7 @@ impl ChatWidget {
let trimmed = name.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
}),
StatusLineItem::TaskProgress => self.terminal_title_task_progress(),
}
}
@@ -547,6 +538,10 @@ impl ChatWidget {
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.terminal_title_status_text()),
TerminalTitleItem::Thread => self.thread_name.as_ref().and_then(|name| {
@@ -563,14 +558,59 @@ impl ChatWidget {
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::TaskProgress => self.terminal_title_task_progress(),
}
}
fn model_with_reasoning_display_name(&self) -> String {
let label = Self::status_line_reasoning_effort_label(self.effective_reasoning_effort());
let fast_label =
if self.should_show_fast_status(self.current_model(), self.config.service_tier) {
" fast"
} else {
""
};
format!("{} {label}{fast_label}", self.model_display_name())
}
/// Computes the compact runtime status label used by the terminal title.
///
/// Startup takes precedence over normal task states, and idle state renders