mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
ef00014a46
commit
2202675632
+21
@@ -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
|
||||
+21
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user